Refactoring things and doing stuff in a MVP way. #15
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
|
22
src/membership/templates/membership_overview.html
Normal file
22
src/membership/templates/membership_overview.html
Normal 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
18
src/membership/views.py
Normal 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)
|
|
@ -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>
|
||||
|
|
|
@ -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)),
|
||||
|
|
Loading…
Reference in a new issue