diff --git a/src/membership/admin.py b/src/membership/admin.py index 90e644e..3065e5c 100644 --- a/src/membership/admin.py +++ b/src/membership/admin.py @@ -1,8 +1,13 @@ from django.contrib import admin -from . import models +from .models import Membership, MembershipType -@admin.register(models.Membership) +@admin.register(Membership) class MembershipAdmin(admin.ModelAdmin): pass + + +@admin.register(MembershipType) +class MembershipTypeAdmin(admin.ModelAdmin): + pass diff --git a/src/membership/migrations/0001_initial.py b/src/membership/migrations/0001_initial.py index 7832a54..f50cf79 100644 --- a/src/membership/migrations/0001_initial.py +++ b/src/membership/migrations/0001_initial.py @@ -1,11 +1,9 @@ -# Generated by Django 3.1.7 on 2021-02-27 20:06 +# Generated by Django 3.1.7 on 2021-02-28 21:09 -from decimal import Decimal from django.conf import settings import django.contrib.postgres.fields.ranges from django.db import migrations, models import django.db.models.deletion -import djmoney.models.fields class Migration(migrations.Migration): @@ -18,36 +16,16 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='SubscriptionType', + name='MembershipType', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('modified', models.DateTimeField(auto_now=True, verbose_name='modified')), ('created', models.DateTimeField(auto_now_add=True, verbose_name='oprettet')), ('name', models.CharField(max_length=64, verbose_name='navn')), - ('fee_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default='XYZ', editable=False, max_length=3)), - ('fee', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=16)), - ('fee_vat_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default='XYZ', editable=False, max_length=3)), - ('fee_vat', djmoney.models.fields.MoneyField(decimal_places=2, default=Decimal('0'), max_digits=16)), ], options={ - 'verbose_name': 'subscription type', - 'verbose_name_plural': 'subscription types', - }, - ), - migrations.CreateModel( - name='Subscription', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('modified', models.DateTimeField(auto_now=True, verbose_name='modified')), - ('created', models.DateTimeField(auto_now_add=True, verbose_name='oprettet')), - ('active', models.BooleanField(default=False, help_text='Automatically set by payment system.', verbose_name='aktiv')), - ('duration', django.contrib.postgres.fields.ranges.DateTimeRangeField(help_text='The duration this subscription is for. ')), - ('subscription_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='memberships', to='membership.subscriptiontype', verbose_name='subscription type')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'subscription', - 'verbose_name_plural': 'subscriptions', + 'verbose_name': 'membership type', + 'verbose_name_plural': 'membership types', }, ), migrations.CreateModel( @@ -56,7 +34,9 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('modified', models.DateTimeField(auto_now=True, verbose_name='modified')), ('created', models.DateTimeField(auto_now_add=True, verbose_name='oprettet')), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ('period', django.contrib.postgres.fields.ranges.DateTimeRangeField(help_text='The duration this subscription is for. ')), + ('membership_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='memberships', to='membership.membershiptype', verbose_name='subscription type')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), ], options={ 'verbose_name': 'membership', diff --git a/src/membership/models.py b/src/membership/models.py index bf63d5f..8ed58f5 100644 --- a/src/membership/models.py +++ b/src/membership/models.py @@ -1,7 +1,9 @@ +from typing import Optional + from django.contrib.postgres.fields import DateTimeRangeField from django.db import models +from django.utils import timezone from django.utils.translation import gettext as _ -from djmoney.models.fields import MoneyField class CreatedModifiedAbstract(models.Model): @@ -13,65 +15,67 @@ class CreatedModifiedAbstract(models.Model): abstract = True + class Membership(CreatedModifiedAbstract): """ - A user remains a member of an organization even though the subscription is - unpaid or renewed. This just changes the status/permissions etc. of the - membership, thus we need to track subscription creation, expiry, renewals - etc. and ensure that the membership is modified accordingly. + Tracks that a user has membership of a given type for a given period. """ - user = models.OneToOneField("auth.User", on_delete=models.PROTECT) + class QuerySet(models.QuerySet): - def __str__(self): - return _(f"{self.user.get_full_name()} is a member") + def for_user(self, user): + return self.filter(user=user) + + def _current(self): + return self.filter(period__contains=timezone.now()) + + def current(self) -> Optional["Membership"]: + try: + return self._current().get() + except self.model.DoesNotExist: + return None + + def previous(self): + # A naïve way to get previous by just excluding the current. This + # means that there must be some protection against "future" + # memberships. + return self.all().difference(self._current()) + + objects = QuerySet.as_manager() class Meta: verbose_name = _("membership") verbose_name_plural = _("memberships") + user = models.ForeignKey("auth.User", on_delete=models.PROTECT) -class SubscriptionType(CreatedModifiedAbstract): - """ - Properties of subscriptions are stored here. Should of course not be edited - after subscriptions are created. - """ - - name = models.CharField(verbose_name=_("name"), max_length=64) - - fee = MoneyField(max_digits=16, decimal_places=2) - - fee_vat = MoneyField(max_digits=16, decimal_places=2, default=0) - - class Meta: - verbose_name = _("subscription type") - verbose_name_plural = _("subscription types") - - -class Subscription(CreatedModifiedAbstract): - """ - To not confuse other types of subscriptions, one can be a *subscribed* - member, meaning that they are paying etc. - - A subscription does not track payment, this is done in the accounting app. - """ - - subscription_type = models.ForeignKey( - SubscriptionType, + membership_type = models.ForeignKey( + "membership.MembershipType", related_name="memberships", verbose_name=_("subscription type"), on_delete=models.PROTECT, ) - user = models.ForeignKey("auth.User", on_delete=models.PROTECT) - active = models.BooleanField( - default=False, - verbose_name=_("active"), - help_text=_("Automatically set by payment system."), - ) + period = DateTimeRangeField(help_text=_("The duration this subscription is for. ")) - duration = DateTimeRangeField(help_text=_("The duration this subscription is for. ")) + def __str__(self): + return f"{self.user} - {self.period}" + + +class MembershipType(CreatedModifiedAbstract): + """ + Models membership types. Currently only a name, but will in the future + possibly contain more information like fees. + """ class Meta: - verbose_name = _("subscription") - verbose_name_plural = _("subscriptions") + verbose_name = _("membership type") + verbose_name_plural = _("membership types") + + name = models.CharField(verbose_name=_("name"), max_length=64) + + def __str__(self): + return self.name + + + diff --git a/src/membership/templates/membership_overview.html b/src/membership/templates/membership_overview.html new file mode 100644 index 0000000..9823f5a --- /dev/null +++ b/src/membership/templates/membership_overview.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} + + {% if not current_membership %} +

{% trans "You do not have an active membership!" %}

+ +

{% trans "You can become a member by depositing the membership fee to our bank account." %}

+ + + {% else %} +

{% trans "You are a member!" %}

+ +

{% trans "Period" %}: {{ current_membership.period.lower|date:"SHORT_DATE_FORMAT" }} to {{ current_membership.period.upper|date:"SHORT_DATE_FORMAT" }}

+

{% trans "Type" %}: {{ current_membership.membership_type }}

+ {% endif %} + {% endblock %} \ No newline at end of file diff --git a/src/membership/views.py b/src/membership/views.py new file mode 100644 index 0000000..7baeb04 --- /dev/null +++ b/src/membership/views.py @@ -0,0 +1,18 @@ +from django.contrib.auth.decorators import login_required +from django.shortcuts import render + +from .models import Membership + + +@login_required +def membership_overview(request): + memberships = Membership.objects.for_user(request.user) + current_membership = memberships.current() + previous_memberships = memberships.previous() + + context = dict( + current_membership=current_membership, + previous_memberships=previous_memberships, + ) + + return render(request, "membership_overview.html", context) \ No newline at end of file diff --git a/src/project/templates/base.html b/src/project/templates/base.html index 6dc6a88..7952ad7 100644 --- a/src/project/templates/base.html +++ b/src/project/templates/base.html @@ -166,7 +166,8 @@