Revamp Membership models to be very simple and do some view/template work for them.

This commit is contained in:
Víðir Valberg Guðmundsson 2021-02-28 23:00:11 +01:00
parent 85e2fd76c0
commit e41eae340b
7 changed files with 106 additions and 74 deletions

View file

@ -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

View file

@ -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',

View file

@ -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

View file

@ -0,0 +1,22 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
{% if not current_membership %}
<p>{% trans "You do not have an active membership!" %}</p>
<p>{% trans "You can become a member by depositing the membership fee to our bank account." %}</p>
<ul>
<li>Reg. 8401 (Merkur)</li>
<li>Kontonr. 1016866</li>
<li>Tekst på overførslen: Your email</li>
</ul>
{% else %}
<p>{% trans "You are a member!" %}</p>
<p>{% trans "Period" %}: {{ current_membership.period.lower|date:"SHORT_DATE_FORMAT" }} to {{ current_membership.period.upper|date:"SHORT_DATE_FORMAT" }}</p>
<p>{% trans "Type" %}: {{ current_membership.membership_type }}</p>
{% endif %}
{% endblock %}

18
src/membership/views.py Normal file
View file

@ -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)

View file

@ -166,7 +166,8 @@
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" href="#">
<a class="nav-link {% active_path "membership-overview" "active" %}"
aria-current="page" href="{% url "membership-overview" %}">
{% trans "Overview" %}
</a>
</li>

View file

@ -7,10 +7,12 @@ from django.urls import path
import debug_toolbar
from .views import index, services_overview
from membership.views import membership_overview
urlpatterns = [
path("", login_required(index), name="index"),
path("services/", login_required(services_overview), name="services-overview"),
path("membership/", membership_overview, name="membership-overview"),
path('accounts/', include('allauth.urls')),
path("admin/", admin.site.urls),
path("__debug__/", include(debug_toolbar.urls)),