forked from data.coop/membersystem
Compare commits
No commits in common. "93e80284130b461874f8b0599a93fed854baab1f" and "1070e9388505aefef310f3f5424ed297a0b7f9e5" have entirely different histories.
93e8028413
...
1070e93885
|
@ -23,7 +23,6 @@ dependencies = [
|
||||||
"django-registries==0.0.3",
|
"django-registries==0.0.3",
|
||||||
"django-view-decorator==0.0.4",
|
"django-view-decorator==0.0.4",
|
||||||
"django-oauth-toolkit~=2.4",
|
"django-oauth-toolkit~=2.4",
|
||||||
"django-ratelimit~=4.1",
|
|
||||||
"django_stubs_ext~=5.0",
|
"django_stubs_ext~=5.0",
|
||||||
"stripe~=10.5",
|
"stripe~=10.5",
|
||||||
]
|
]
|
||||||
|
@ -147,8 +146,6 @@ ignore = [
|
||||||
"TD", # TODO, FIXME, XXX
|
"TD", # TODO, FIXME, XXX
|
||||||
"ANN002", # Missing type annotation for `*args`
|
"ANN002", # Missing type annotation for `*args`
|
||||||
"ANN003", # Missing type annotation for `**kwargs`
|
"ANN003", # Missing type annotation for `**kwargs`
|
||||||
"FBT001", # Misbehaves: Complains about positional arguments that are keyworded.
|
|
||||||
"FBT002", # Misbehaves: Complains about positional arguments that are keyworded.
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff.lint.isort]
|
[tool.ruff.lint.isort]
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
# - django-allauth~=0.63
|
# - django-allauth~=0.63
|
||||||
# - django-money~=3.5
|
# - django-money~=3.5
|
||||||
# - django-oauth-toolkit~=2.4
|
# - django-oauth-toolkit~=2.4
|
||||||
# - django-ratelimit~=4.1
|
|
||||||
# - django-registries==0.0.3
|
# - django-registries==0.0.3
|
||||||
# - django-stubs-ext~=5.0
|
# - django-stubs-ext~=5.0
|
||||||
# - django-view-decorator==0.0.4
|
# - django-view-decorator==0.0.4
|
||||||
|
@ -54,8 +53,6 @@ django-money==3.5.3
|
||||||
# via hatch.envs.default
|
# via hatch.envs.default
|
||||||
django-oauth-toolkit==2.4.0
|
django-oauth-toolkit==2.4.0
|
||||||
# via hatch.envs.default
|
# via hatch.envs.default
|
||||||
django-ratelimit==4.1.0
|
|
||||||
# via hatch.envs.default
|
|
||||||
django-registries==0.0.3
|
django-registries==0.0.3
|
||||||
# via hatch.envs.default
|
# via hatch.envs.default
|
||||||
django-stubs-ext==5.0.4
|
django-stubs-ext==5.0.4
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
# - django-allauth~=0.63
|
# - django-allauth~=0.63
|
||||||
# - django-money~=3.5
|
# - django-money~=3.5
|
||||||
# - django-oauth-toolkit~=2.4
|
# - django-oauth-toolkit~=2.4
|
||||||
# - django-ratelimit~=4.1
|
|
||||||
# - django-registries==0.0.3
|
# - django-registries==0.0.3
|
||||||
# - django-stubs-ext~=5.0
|
# - django-stubs-ext~=5.0
|
||||||
# - django-view-decorator==0.0.4
|
# - django-view-decorator==0.0.4
|
||||||
|
@ -82,8 +81,6 @@ django-money==3.5.3
|
||||||
# via hatch.envs.dev
|
# via hatch.envs.dev
|
||||||
django-oauth-toolkit==2.4.0
|
django-oauth-toolkit==2.4.0
|
||||||
# via hatch.envs.dev
|
# via hatch.envs.dev
|
||||||
django-ratelimit==4.1.0
|
|
||||||
# via hatch.envs.dev
|
|
||||||
django-registries==0.0.3
|
django-registries==0.0.3
|
||||||
# via hatch.envs.dev
|
# via hatch.envs.dev
|
||||||
django-stubs==1.16.0
|
django-stubs==1.16.0
|
||||||
|
|
|
@ -43,9 +43,7 @@ class OrderAdmin(admin.ModelAdmin):
|
||||||
inlines = (OrderProductInline,)
|
inlines = (OrderProductInline,)
|
||||||
form = OrderAdminForm
|
form = OrderAdminForm
|
||||||
|
|
||||||
list_display = ("member", "description", "created", "is_paid", "total_with_vat")
|
list_display = ("member", "description", "created", "is_paid")
|
||||||
search_fields = ("member__email", "membership__membership_type__name", "description")
|
|
||||||
list_filter = ("is_paid", "membership__membership_type")
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Payment)
|
@admin.register(models.Payment)
|
||||||
|
|
|
@ -4,7 +4,6 @@ from hashlib import md5
|
||||||
from typing import Self
|
from typing import Self
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import admin
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.aggregates import Sum
|
from django.db.models.aggregates import Sum
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
@ -97,11 +96,6 @@ class Order(CreatedModifiedAbstract):
|
||||||
return sum(item.vat * item.quantity for item in self.items.all())
|
return sum(item.vat * item.quantity for item in self.items.all())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@admin.display(
|
|
||||||
ordering=None,
|
|
||||||
description="Total (incl. VAT)",
|
|
||||||
boolean=False,
|
|
||||||
)
|
|
||||||
def total_with_vat(self) -> Money:
|
def total_with_vat(self) -> Money:
|
||||||
"""Return the TOTAL amount WITH VAT."""
|
"""Return the TOTAL amount WITH VAT."""
|
||||||
return self.total + self.total_vat
|
return self.total + self.total_vat
|
||||||
|
|
|
@ -102,7 +102,7 @@ def success(request: HttpRequest, order_id: int) -> HttpResponse:
|
||||||
quickly as possible.
|
quickly as possible.
|
||||||
"""
|
"""
|
||||||
user = request.user # People just need to login to pay something, not necessarily be a member
|
user = request.user # People just need to login to pay something, not necessarily be a member
|
||||||
order = get_object_or_404(models.Order, pk=order_id, member=user)
|
order = models.Order.objects.get(pk=order_id, member=user)
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"order": order,
|
"order": order,
|
||||||
|
|
|
@ -29,10 +29,6 @@ admin.site.unregister(User)
|
||||||
class MembershipAdmin(admin.ModelAdmin):
|
class MembershipAdmin(admin.ModelAdmin):
|
||||||
"""Admin for Membership model."""
|
"""Admin for Membership model."""
|
||||||
|
|
||||||
list_display = ("user", "period", "membership_type", "activated", "revoked")
|
|
||||||
list_filter = ("period", "membership_type", "activated", "revoked")
|
|
||||||
search_fields = ("membership_type__name", "user__email", "user__first_name", "user__last_name")
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(MembershipType)
|
@admin.register(MembershipType)
|
||||||
class MembershipTypeAdmin(admin.ModelAdmin):
|
class MembershipTypeAdmin(admin.ModelAdmin):
|
||||||
|
|
|
@ -1,88 +0,0 @@
|
||||||
"""Send email to members, using templates and contexts for the emails.
|
|
||||||
|
|
||||||
* We keep everything as plain text for now.
|
|
||||||
* Notice that emails can be multilingual
|
|
||||||
* Generally, an email consists of templates (for body and subject) and a get_context() method.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.contrib.sites.shortcuts import get_current_site
|
|
||||||
from django.core.mail.message import EmailMessage
|
|
||||||
from django.http import HttpRequest
|
|
||||||
from django.template import loader
|
|
||||||
from django.utils import translation
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
class BaseEmail(EmailMessage):
|
|
||||||
"""Send emails via templated body and subjects.
|
|
||||||
|
|
||||||
This base class is extended for all email functionality.
|
|
||||||
"""
|
|
||||||
|
|
||||||
template = "membership/email/base.txt"
|
|
||||||
# Optional: Set to a template path for subject
|
|
||||||
template_subject = None
|
|
||||||
default_subject = "SET SUBJECT HERE"
|
|
||||||
|
|
||||||
def __init__(self, request: HttpRequest, *args, **kwargs) -> None: # noqa: D107
|
|
||||||
self.context = kwargs.pop("context", {})
|
|
||||||
self.user = kwargs.pop("user", None)
|
|
||||||
if self.user:
|
|
||||||
kwargs["to"] = [self.user.email]
|
|
||||||
self.context["user"] = self.user
|
|
||||||
self.context["recipient_name"] = self.user.get_display_name()
|
|
||||||
|
|
||||||
# Necessary to set request before instantiating body and subject
|
|
||||||
self.request = request
|
|
||||||
kwargs.setdefault("subject", self.get_subject())
|
|
||||||
kwargs.setdefault("body", self.get_body())
|
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def get_context_data(self) -> dict:
|
|
||||||
"""Resolve common context for sending emails.
|
|
||||||
|
|
||||||
When overwriting, remember to call this via super().
|
|
||||||
"""
|
|
||||||
c = self.context
|
|
||||||
site = get_current_site(self.request)
|
|
||||||
c["request"] = self.request
|
|
||||||
c["domain"] = site.domain
|
|
||||||
c["site_name"] = site.name
|
|
||||||
c["protocol"] = "http" if self.request and not self.request.is_secure() else "https"
|
|
||||||
return c
|
|
||||||
|
|
||||||
def get_body(self) -> str:
|
|
||||||
"""Build the email body from template and context."""
|
|
||||||
if self.user and self.user.language_code:
|
|
||||||
with translation.override(self.user.language_code):
|
|
||||||
body = loader.render_to_string(self.template, self.get_context_data())
|
|
||||||
else:
|
|
||||||
body = loader.render_to_string(self.template, self.get_context_data())
|
|
||||||
return body
|
|
||||||
|
|
||||||
def get_subject(self) -> str:
|
|
||||||
"""Build the email subject from template or self.default_subject."""
|
|
||||||
if self.user and self.user.language_code:
|
|
||||||
with translation.override(self.user.language_code):
|
|
||||||
if self.template_subject:
|
|
||||||
subject = loader.render_to_string(self.template_subject, self.get_context_data()).strip()
|
|
||||||
else:
|
|
||||||
subject = str(self.default_subject)
|
|
||||||
elif self.template_subject:
|
|
||||||
subject = loader.render_to_string(self.template_subject, self.get_context_data()).strip()
|
|
||||||
else:
|
|
||||||
subject = str(self.default_subject)
|
|
||||||
return subject
|
|
||||||
|
|
||||||
def send_with_feedback(self, success_msg: str | None = None, no_message: bool = False) -> None:
|
|
||||||
"""Send email, possibly adding feedback via django.contrib.messages."""
|
|
||||||
if not success_msg:
|
|
||||||
success_msg = _("Email successfully sent to {}").format(", ".join(self.to))
|
|
||||||
try:
|
|
||||||
self.send(fail_silently=False)
|
|
||||||
if not no_message:
|
|
||||||
messages.success(self.request, success_msg)
|
|
||||||
except RuntimeError:
|
|
||||||
messages.error(self.request, _("Not sent, something wrong with the mail server."))
|
|
|
@ -1,19 +0,0 @@
|
||||||
# Generated by Django 5.1b1 on 2024-08-04 10:26
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('membership', '0007_membership_activated_membership_activated_on_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='membership',
|
|
||||||
name='membership_type',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='memberships', to='membership.membershiptype', verbose_name='membership type'),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -125,7 +125,7 @@ class Membership(CreatedModifiedAbstract):
|
||||||
membership_type = models.ForeignKey(
|
membership_type = models.ForeignKey(
|
||||||
"membership.MembershipType",
|
"membership.MembershipType",
|
||||||
related_name="memberships",
|
related_name="memberships",
|
||||||
verbose_name=_("membership type"),
|
verbose_name=_("subscription type"),
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
{% load i18n %}{% block greeting %}{% blocktrans %}Dear {{ recipient_name }},{% endblocktrans %}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}{% endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
Regards,
|
|
||||||
{{ site_name }}
|
|
||||||
|
|
||||||
{{ protocol }}://{{ domain }}
|
|
|
@ -1,11 +0,0 @@
|
||||||
{% extends "users/mail/base.txt" %}{% load i18n %}
|
|
||||||
|
|
||||||
{% block content %}{% url 'users:login_token' token=user.token_uuid as login_url %}{% blocktrans with expiry=user.token_expiry next=next %}Here is a 1-time code for confirming your account:
|
|
||||||
|
|
||||||
{{ token_passphrase }}
|
|
||||||
|
|
||||||
Use this code within 1 hour (before {{ expiry }}). You can login here:
|
|
||||||
|
|
||||||
{{ protocol }}://{{ domain }}{{ login_url }}?next={{ next }}
|
|
||||||
|
|
||||||
If you did not request this account, get in touch with us.{% endblocktrans %}{% endblock %}
|
|
|
@ -5,7 +5,6 @@ from __future__ import annotations
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_ratelimit.decorators import ratelimit
|
|
||||||
from django_view_decorator import namespaced_decorator_factory
|
from django_view_decorator import namespaced_decorator_factory
|
||||||
from utils.view_utils import RenderConfig
|
from utils.view_utils import RenderConfig
|
||||||
from utils.view_utils import RowAction
|
from utils.view_utils import RowAction
|
||||||
|
@ -114,27 +113,3 @@ def members_admin_detail(request: HttpRequest, member_id: int) -> HttpResponse:
|
||||||
template_name="membership/members_admin_detail.html",
|
template_name="membership/members_admin_detail.html",
|
||||||
context=context,
|
context=context,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ratelimit(group="membership", key="ip", rate="10/d", method="ALL", block=True)
|
|
||||||
@member_view(
|
|
||||||
paths="invite/<str:token>/",
|
|
||||||
name="membership-invite",
|
|
||||||
login_required=False,
|
|
||||||
)
|
|
||||||
def invite(request: HttpRequest, token: str) -> HttpResponse:
|
|
||||||
"""View to invite a member to create a membership.
|
|
||||||
|
|
||||||
The token belongs to a non-active Member object. If the token is valid,
|
|
||||||
the caller is allowed to create a membership.
|
|
||||||
|
|
||||||
We ratelimit this view so it's not possible to brute-force tokens.
|
|
||||||
"""
|
|
||||||
context = {
|
|
||||||
"token": token,
|
|
||||||
}
|
|
||||||
return render(
|
|
||||||
request=request,
|
|
||||||
template_name="membership/invite.html",
|
|
||||||
context=context,
|
|
||||||
)
|
|
||||||
|
|
Loading…
Reference in a new issue