Refactoring things and doing stuff in a MVP way. #15
|
@ -1,8 +1,13 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from . import models
|
from .models import Membership, MembershipType
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Membership)
|
@admin.register(Membership)
|
||||||
class MembershipAdmin(admin.ModelAdmin):
|
class MembershipAdmin(admin.ModelAdmin):
|
||||||
pass
|
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
|
from django.conf import settings
|
||||||
import django.contrib.postgres.fields.ranges
|
import django.contrib.postgres.fields.ranges
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import djmoney.models.fields
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
@ -18,36 +16,16 @@ class Migration(migrations.Migration):
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='SubscriptionType',
|
name='MembershipType',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
||||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='oprettet')),
|
('created', models.DateTimeField(auto_now_add=True, verbose_name='oprettet')),
|
||||||
('name', models.CharField(max_length=64, verbose_name='navn')),
|
('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={
|
options={
|
||||||
'verbose_name': 'subscription type',
|
'verbose_name': 'membership type',
|
||||||
'verbose_name_plural': 'subscription types',
|
'verbose_name_plural': 'membership 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',
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
|
@ -56,7 +34,9 @@ class Migration(migrations.Migration):
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
||||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='oprettet')),
|
('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={
|
options={
|
||||||
'verbose_name': 'membership',
|
'verbose_name': 'membership',
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from django.contrib.postgres.fields import DateTimeRangeField
|
from django.contrib.postgres.fields import DateTimeRangeField
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from djmoney.models.fields import MoneyField
|
|
||||||
|
|
||||||
|
|
||||||
class CreatedModifiedAbstract(models.Model):
|
class CreatedModifiedAbstract(models.Model):
|
||||||
|
@ -13,65 +15,67 @@ class CreatedModifiedAbstract(models.Model):
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Membership(CreatedModifiedAbstract):
|
class Membership(CreatedModifiedAbstract):
|
||||||
"""
|
"""
|
||||||
A user remains a member of an organization even though the subscription is
|
Tracks that a user has membership of a given type for a given period.
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user = models.OneToOneField("auth.User", on_delete=models.PROTECT)
|
class QuerySet(models.QuerySet):
|
||||||
|
|
||||||
def __str__(self):
|
def for_user(self, user):
|
||||||
return _(f"{self.user.get_full_name()} is a member")
|
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:
|
class Meta:
|
||||||
verbose_name = _("membership")
|
verbose_name = _("membership")
|
||||||
verbose_name_plural = _("memberships")
|
verbose_name_plural = _("memberships")
|
||||||
|
|
||||||
|
user = models.ForeignKey("auth.User", on_delete=models.PROTECT)
|
||||||
|
|
||||||
class SubscriptionType(CreatedModifiedAbstract):
|
membership_type = models.ForeignKey(
|
||||||
"""
|
"membership.MembershipType",
|
||||||
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,
|
|
||||||
related_name="memberships",
|
related_name="memberships",
|
||||||
verbose_name=_("subscription type"),
|
verbose_name=_("subscription type"),
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
)
|
)
|
||||||
user = models.ForeignKey("auth.User", on_delete=models.PROTECT)
|
|
||||||
|
|
||||||
active = models.BooleanField(
|
period = DateTimeRangeField(help_text=_("The duration this subscription is for. "))
|
||||||
default=False,
|
|
||||||
verbose_name=_("active"),
|
|
||||||
help_text=_("Automatically set by payment system."),
|
|
||||||
)
|
|
||||||
|
|
||||||
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:
|
class Meta:
|
||||||
verbose_name = _("subscription")
|
verbose_name = _("membership type")
|
||||||
verbose_name_plural = _("subscriptions")
|
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">
|
<ul class="nav flex-column">
|
||||||
<li class="nav-item">
|
<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" %}
|
{% trans "Overview" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -7,10 +7,12 @@ from django.urls import path
|
||||||
import debug_toolbar
|
import debug_toolbar
|
||||||
|
|
||||||
from .views import index, services_overview
|
from .views import index, services_overview
|
||||||
|
from membership.views import membership_overview
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", login_required(index), name="index"),
|
path("", login_required(index), name="index"),
|
||||||
path("services/", login_required(services_overview), name="services-overview"),
|
path("services/", login_required(services_overview), name="services-overview"),
|
||||||
|
path("membership/", membership_overview, name="membership-overview"),
|
||||||
path('accounts/', include('allauth.urls')),
|
path('accounts/', include('allauth.urls')),
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path("__debug__/", include(debug_toolbar.urls)),
|
path("__debug__/", include(debug_toolbar.urls)),
|
||||||
|
|
Loading…
Reference in a new issue