diff --git a/src/memberdb/approve.py b/src/memberdb/approve.py index 55c232642498b01c9dca5bc1ee5b7637fa41c621..59fe2bc7e46c0d685cd2ecc30309b7474ebbc09f 100644 --- a/src/memberdb/approve.py +++ b/src/memberdb/approve.py @@ -8,110 +8,102 @@ from django.urls import reverse from django.utils import timezone from django import forms -from memberdb.models import Member, Membership, get_membership_type +from memberdb.models import Member, Membership, get_membership_choices from memberdb.forms import MyModelForm from memberdb.views import MyUpdateView -""" -inline admin change list action buttons -see https://medium.com/@hakibenita/how-to-add-custom-action-buttons-to-django-admin-8d266f5b0d41 -and have a look at .admin.MembershipAdmin -""" + class MembershipApprovalForm(MyModelForm): - payment_confirm = forms.BooleanField(label='Confirm payment', required=False) - - class Meta: - model = Membership - fields = ['membership_type', 'payment_method'] - widgets = { - 'membership_type': forms.RadioSelect(), - 'payment_method': forms.RadioSelect(), - } - - """ - Called to validate the data on the form. - here we fill out some fields automatically (ie. approver, date paid / approved, etc.) - TODO: deal with account activation/creation, etc. - """ - def clean(self): - # get the cleaned data from the form API and do something with it - data = super().clean() - now = timezone.now() - #breakpoint() - # find a Member matching our current username - approver = Member.objects.filter(username__exact=self.request.user.username).first() - if (approver == None): - self.add_error(None, 'Cannot set approver: no Member record with username %s' % self.request.user.username) - data['approver'] = approver - data['approved'] = True - data['date_approved'] = now - - if (data['payment_confirm'] == True): - if (data['payment_method'] == ''): - self.add_error('payment_method', 'Please select a payment method') - data['date_paid'] = now - else: - data['date_paid'] = None - - # make sure "no payment" is recorded for Life Members. - # XXX this might not actually be the case, since some life members may want to also be financial members (ie. for constitutional voting rights) - # and so this is probably more annoying than helpful - if (data['membership_type'] == ''): - if (data['payment_method'] != ''): - self.add_error('payment_method', 'Life members shall not pay membership fees!') - data['payment_method'] = '' - - return data - - """ - do the stuff, approve the things - """ - def save(self, commit=True): - ms = super().save(commit=False) - - # copy attributes not specified in fields - ms.approver = self.cleaned_data['approver'] - ms.approved = self.cleaned_data['approved'] - ms.date_approved = self.cleaned_data['date_approved'] - ms.date_paid = self.cleaned_data['date_paid'] - - # do something - if (commit): - ms.save() - return ms + payment_confirm = forms.BooleanField( + label = 'Confirm payment', + required = False + ) + + class Meta: + model = Membership + fields = ['membership_type', 'payment_method'] + widgets = { + 'membership_type': forms.RadioSelect(), + 'payment_method': forms.RadioSelect(), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # override the model membership_type field so we display all the options with prices + self.fields['membership_type'].choices = get_membership_choices() + + def clean(self): + """ + Called to validate the data on the form. + here we fill out some fields automatically (ie. approver, date paid / approved, etc.) + TODO: deal with account activation/creation, etc. + """ + data = super().clean() + now = timezone.now() + approver = self.request.member + if (approver == None): + self.add_error(None, 'Cannot set approver: no Member record associated with current session. (username %s)' % self.request.user.username) + + data['approver'] = approver + data['approved'] = True + data['date_approved'] = now + + if (data['payment_confirm'] == True): + if (data['payment_method'] == ''): + self.add_error('payment_method', 'Please select a payment method') + data['date_paid'] = now + else: + data['date_paid'] = None + + return data + + def save(self, commit=True): + """ save the data into a Membership object """ + ms = super().save(commit=False) + + # copy attributes not specified in editable form fields + ms.approver = self.cleaned_data['approver'] + ms.approved = self.cleaned_data['approved'] + ms.date_approved = self.cleaned_data['date_approved'] + ms.date_paid = self.cleaned_data['date_paid'] + + if (commit): + ms.save() + return ms class MembershipApprovalAdminView(MyUpdateView): - template_name = 'admin/memberdb/membership_approve.html' - form_class = MembershipApprovalForm - model = Membership - pk_url_kwarg = 'object_id' - # override with the instance of ModelAdmin - admin = None - - def get_context_data(self, **kwargs): - ms = self.get_object() - context = super().get_context_data(**kwargs) - context.update(self.admin.admin_site.each_context(self.request)) - context.update({ - 'opts': self.admin.model._meta, - 'ms': ms, - 'member': ms.member, - 'show_member_summary': True, - }) - return context - - """ - called when the approval form is submitted and valid data (according to the form's field types and defined validators) is given - """ - def form_valid(self, form): - ms = form.save() - - self.admin.message_user(self.request, 'Approve success') - url = reverse( - 'admin:memberdb_membership_changelist', - args=[], - current_app=self.admin.admin_site.name, - ) - return HttpResponseRedirect(url) + template_name = 'admin/memberdb/membership_approve.html' + form_class = MembershipApprovalForm + model = Membership + pk_url_kwarg = 'object_id' + # override with the instance of ModelAdmin + admin = None + + def get_context_data(self, **kwargs): + ms = self.get_object() + context = super().get_context_data(**kwargs) + context.update(self.admin.admin_site.each_context(self.request)) + context.update({ + 'opts': self.admin.model._meta, + 'ms': ms, + 'member': ms.member, + 'show_member_summary': True, + }) + return context + + """ + called when the approval form is submitted and valid data (according to the form's field types and defined validators) is given + """ + def form_valid(self, form): + ms = form.save() + + self.admin.message_user(self.request, 'Approve success') + url = reverse( + 'admin:memberdb_membership_changelist', + args=[], + current_app=self.admin.admin_site.name, + ) + return HttpResponseRedirect(url) diff --git a/src/memberdb/models.py b/src/memberdb/models.py index 622049c3cad0e02566b7bc1f2239b68b681805e5..f4bcea338a17f96b31e7bdc918a39d4d28bab5b5 100644 --- a/src/memberdb/models.py +++ b/src/memberdb/models.py @@ -100,13 +100,18 @@ def get_membership_type(member): return best def make_pending_membership(member): - # check if this member already has a pending membership - ms = Membership.objects.filter(member=member, approved__exact=False).first() - if (ms is None): - ms = Membership(member=member, approved=False) - ms.date_submitted = timezone.now() - ms.membership_type = get_membership_type(member) - return ms + """ creates or updates and returns a pending membership for the given member """ + latest = member.get_last_renewal() + if latest is None or latest.date_submitted.year != timezone.now().year: + # create a Membership if none exists already for this year + latest = Membership(member=member) + latest.membership_type = get_membership_type(member) + + # otherwise update the existing membership and mark as pending + latest.approved = False + latest.date_submitted = timezone.now() + + return latest def make_token(): return get_random_string(128) @@ -162,8 +167,6 @@ class Member (IncAssocMember): Note: Privacy laws are a thing, unless people allow it then we cannot provide this info to members. """ - - # data to be entered by user and validated (mostly) manually display_name = models.CharField ('Display name', max_length=200) username = models.SlugField ('Username', max_length=32, null=True, blank=True, unique=False, validators=[RegexValidator(regex='^[a-z0-9._-]*$')]) @@ -182,6 +185,10 @@ class Member (IncAssocMember): has_account = models.BooleanField ('Has AD account', null=False, editable=False, default=False) + def get_last_renewal(self): + """ returns the most recently submitted Membership object """ + return self.memberships.order_by('-date_submitted').first() + # account info def get_uid(self): result, uid = subprocess.getstatusoutput(["id", "-u", self.username]) @@ -210,7 +217,7 @@ class Membership (models.Model): payment_method = models.CharField ('Payment method', max_length=10, blank=True, null=True, choices=PAYMENT_METHODS, default=None) approved = models.BooleanField ('Membership approved', default=False) approver = models.ForeignKey (Member, on_delete=models.SET_NULL, null=True, blank=True, related_name='approved_memberships') - date_submitted = models.DateTimeField ('Date signed up') + date_submitted = models.DateTimeField ('Date signed up', default=timezone.now) date_paid = models.DateTimeField ('Date of payment', blank=True, null=True) date_approved = models.DateTimeField ('Date approved', blank=True, null=True) @@ -220,6 +227,9 @@ class Membership (models.Model): def get_dispense_item(self): return MEMBERSHIP_TYPES[self.membership_type]['dispense'] + def get_pretty_type(self): + return MEMBERSHIP_TYPES[self.membership_type]['desc'] + class Meta: verbose_name = "Membership renewal record" ordering = ['approved', '-date_submitted'] diff --git a/src/memberdb/register.py b/src/memberdb/register.py index 18cac9beab5597c475ffb9b331767f70a2341eb9..06a78b2b638371b8ebc66ea5f7829a366364246b 100644 --- a/src/memberdb/register.py +++ b/src/memberdb/register.py @@ -7,6 +7,7 @@ from django.shortcuts import render from django.urls import reverse from django.contrib.auth.mixins import LoginRequiredMixin from django.utils.safestring import mark_safe +from django.utils import timezone from django.contrib import messages from django import forms @@ -44,7 +45,7 @@ class RegisterRenewForm(MyModelForm): if (self['email_address'].value() != self['confirm_email'].value()): self.add_error('email_address', 'Email addresses must match.') if (self['email_address'].value().lower().split('@')[1] in ["ucc.asn.au", "ucc.gu.uwa.edu.au"]): - self.add_error('email_address', 'Contact address cannot be an UCC address.') + self.add_error('email_address', 'Contact address cannot be an UCC address.') except: pass super().clean(); @@ -57,8 +58,8 @@ class RegisterRenewForm(MyModelForm): # must save otherwise membership creation will fail m.save() - # now create a corresponding Membership (marked as pending / not accepted, mostly default values) ms = make_pending_membership(m) + ms.membership_type = self.cleaned_data['membership_type'] if (commit): ms.save(); @@ -112,13 +113,18 @@ class RegisterView(MyUpdateView): template_name = 'register.html' form_class = RegisterForm model = Member - can_create = False - """ - called when valid form data has been POSTed - invalid form data simply redisplays the form with validation errors - """ + def get_context_data(self, **kwargs): + """ update view context with current renewal year """ + context = super().get_context_data(**kwargs) + context['year'] = timezone.now().year + return context + def form_valid(self, form): + """ + called when valid form data has been POSTed + invalid form data simply redisplays the form with validation errors + """ # save the member data and get the Member instance m, ms = form.save() messages.success(self.request, 'Your registration has been submitted.') @@ -139,29 +145,37 @@ def thanks_view(request, member, ms): } return render(request, 'thanks.html', context) -class RenewView(LoginRequiredMixin, MyUpdateView): +class RenewView(LoginRequiredMixin, RegisterView): template_name = 'renew.html' form_class = RenewForm model = Member def get_object(self): + """ try to get a pending renewal for this year (to edit & resubmit) otherwise create a new one """ u = self.request.user + m = self.request.member - obj = Member.objects.filter(username__exact=u.username).first() - if (obj is None): - # make a new Member object and prefill some data - obj = Member(username=u.username) - obj.first_name = u.first_name - obj.last_name = u.last_name - obj.email_address = u.email - obj.login_token = None # renewing members won't need this - return obj + if m is None: + # this member is not in the DB yet - make a new Member object and prefill some data + m = Member(username=u.username) + m.first_name = u.first_name + m.last_name = u.last_name + m.email_address = u.email + m.login_token = None # renewing members won't need this, make sure it is null for security + return m def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context.update({ - 'is_new': Member.objects.filter(username__exact=self.request.user.username).count() == 0, - }) + last_renewal = self.object.get_last_renewal() + + # renew.html says whether a record exists in the DB or not + context['is_new'] = self.request.member is None + + # let the template check if user has already renewed this year so it displays a warning + if last_renewal is not None: + context['last_renewal'] = last_renewal.date_submitted.year + context['memberships'] = [last_renewal] + return context def form_valid(self, form): diff --git a/src/memberdb/views.py b/src/memberdb/views.py index 32ce1762d16d3600082f85af132c44d7dedc60a3..334daf71bb53d08422bed4c0ebb09e381370e8b8 100644 --- a/src/memberdb/views.py +++ b/src/memberdb/views.py @@ -108,24 +108,13 @@ class MemberHomeView(MemberAccessMixin, MyUpdateView): def get_object(self): return self.request.member - def get_membership_context(self, ms): - """ gets the per-membership-record context data """ - return { - 'id': ms.id, - 'type': MEMBERSHIP_TYPES[ms.membership_type]['desc'], - 'submitted': ms.date_submitted.strftime('%Y-%m-%d %H:%M'), - 'paid': ms.date_paid.strftime('%Y-%m-%d %H:%M') if ms.date_paid is not None else None, - 'approved': ms.date_approved.strftime('%Y-%m-%d %H:%M') if ms.approved else None, - 'is_approved': ms.approved, - } - def get_context_data(self): d = super().get_context_data() m = self.get_object() if m is not None: # get a list of all the membership records associated with this member - ms_list = [ self.get_membership_context(ms) for ms in m.memberships.all() ] + ms_list = m.memberships.all() d.update({ 'memberships': ms_list, }) diff --git a/src/squarepay/dispense.py.orig b/src/squarepay/dispense.py.orig deleted file mode 100644 index fbce946c21781aeb4ab90fd60527ed758afbf57e..0000000000000000000000000000000000000000 --- a/src/squarepay/dispense.py.orig +++ /dev/null @@ -1,102 +0,0 @@ -""" -this file contains utilities for wrapping the opendispense2 CLI utility `dispense` -It is essentially a hack to avoid having to write an actual dispense client here. -""" - -import subprocess -from subprocess import CalledProcessError, TimeoutExpired -from django.conf import settings - -from .payments import log - -DISPENSE_BIN = getattr(settings, 'DISPENSE_BIN', None) - -if DISPENSE_BIN is None: - log.warning("DISPENSE_BIN is not defined! Lookups for prices will fallback to weird numbers (for testing)!") - -def run_dispense(*args): -<<<<<<< HEAD - if DISPENSE_BIN is None: - return None - - cmd = [DISPENSE_BIN] + args - log.info("run_dispense: " + cmd) - try: - # get a string containing the output of the program - res = subprocess.check_output(cmd, timeout=4, universal_newlines=True) - except CalledProcessError as e: - log.warning("dispense returned error code %d, output: '%s'" % (e.returncode, e.output)) - return None - except TimeoutExpired as e: - log.error(e) - return None - return res - -def get_item_price(itemid): - """ gets the price of the given dispense item in cents """ - if (itemid is None or itemid == ""): - return None - if DISPENSE_BIN is None: - return 2223 - - out = run_dispense('iteminfo', itemid) - if out is None: - return None - - s = out.split() # get something like ['pseudo:7', '25.00', 'membership', '(non-student', 'and', 'non-guild)'] - if (s[0] != itemid): - log.warning("get_item_price: got result for incorrect item: %s" + s) - return None - else: - # return the price as a number of cents - return int(float(s[0]) * 100) - -def set_dispense_flag(user, flag, reason): - if DISPENSE_BIN is None: - log.warning("DISPENSE_BIN is not defined, user will not be disabled") - return False - - cmd = [DISPENSE_BIN] + args - out = run_dispense('user', 'type', user, flag, reason) - s = out.split() - if s[2] != "updated": - # user was not updated - log.warning("set_dispense_flag: user was not updated with error: " + out) - return False; - return True; -======= - if DISPENSE_BIN is None: - return None - - cmd = (DISPENSE_BIN, ) + args - log.info("run_dispense: " + str(cmd)) - try: - # get a string containing the output of the program - res = subprocess.check_output(cmd, timeout=4, universal_newlines=True) - except CalledProcessError as e: - log.warning("dispense returned error code %d, output: '%s'" % (e.returncode, e.output)) - return None - except TimeoutExpired as e: - log.error(e) - return None - return res - -def get_item_price(itemid): - """ gets the price of the given dispense item in cents """ - if (itemid is None or itemid == ""): - return None - if DISPENSE_BIN is None: - return 2223 - - out = run_dispense('iteminfo', itemid) - if out is None: - return None - - s = out.split() # get something like ['pseudo:7', '25.00', 'membership', '(non-student', 'and', 'non-guild)'] - if (s[0] != itemid): - log.warning("get_item_price: got result for incorrect item: %s" + s) - return None - else: - # return the price as a number of cents - return int(float(s[1]) * 100) ->>>>>>> origin/frekk-testing diff --git a/src/templates/home.html b/src/templates/home.html index 4961defc30abaa76fb9db16f39810d17687aff52..e3f690a69a9be572bcd98190d461ef707bd448c9 100644 --- a/src/templates/home.html +++ b/src/templates/home.html @@ -31,45 +31,7 @@ <h4>Membership renewals on record</h4> <!-- stuff from the membership record itself --> {% if memberships %} - <table class="membership-details"> - <tr class="{% cycle 'row1' 'row2' as rcl %}"> - <th>Membership type</th> - <th>Submitted</th> - <th>Paid</th> - <th>Approved</th> - <th>Actions</th> - </tr> - {% for ms in memberships %} - <tr class="{% cycle rcl %}"> - <td> - {{ ms.type }} - </td> - <td> - {{ ms.submitted }} - </td> - <td> - {% if ms.paid %} - <img src="{% static 'admin/img/icon-yes.svg' %}" alt="yes"> {{ ms.paid }} - {% else %} - <img src="{% static 'admin/img/icon-no.svg' %}" alt="no"> Not paid yet - {% endif %} - </td> - <td> - {% if ms.approved and ms.is_approved %} - <img src="{% static 'admin/img/icon-yes.svg' %}" alt="yes"> {{ ms.approved }} - {% elif ms.approved and not ms.is_approved %} - <img src="{% static 'admin/img/icon-no.svg' %}" alt="no"> Rejected: {{ ms.approved }} - {% else %} - <img src="{% static 'admin/img/icon-unknown.svg' %}" alt="?"> Not approved yet - {% endif %} - </td> - <td> - <!-- membership actions --> - {% if not ms.paid %}<a class="button" href="{% url 'squarepay:pay_membership' ms.id %}">Pay now</a>{% endif %} - </td> - </tr> - {% endfor %} - </table> + {% include 'membership_list.html' %} {% else %} No membership renewal records for your account exist yet. Please <a href="{% url 'memberdb:renew' %}">renew your membership</a> to get started. {% endif %} diff --git a/src/templates/membership_list.html b/src/templates/membership_list.html new file mode 100644 index 0000000000000000000000000000000000000000..c5e4b1020a3fbc6b05c0b82d6ced82ba5bbaf85a --- /dev/null +++ b/src/templates/membership_list.html @@ -0,0 +1,44 @@ +{% load static %} +<table class="membership-details"> + <tr class="{% cycle 'row1' 'row2' as rcl %}"> + <th>Membership type</th> + <th>Submitted / Updated</th> + <th>Paid</th> + <th>Approved</th> + <th>Actions</th> + </tr> + {% for ms in memberships %} + <tr class="{% cycle rcl %}"> + <td> + {{ ms.get_pretty_type }} + </td> + <td> + {{ ms.date_submitted |date:"Y-m-d H:i" }} + </td> + <td> + {% if ms.date_paid %} + <img src="{% static 'admin/img/icon-yes.svg' %}" alt="yes"> {{ ms.date_paid |date:"Y-m-d H:i" }} + {% else %} + <img src="{% static 'admin/img/icon-no.svg' %}" alt="no"> Not paid yet + {% endif %} + </td> + <td> + {% if ms.date_approved and ms.approved %} + <img src="{% static 'admin/img/icon-yes.svg' %}" alt="yes"> {{ ms.date_approved |date:"Y-m-d H:i" }} + {% elif ms.date_approved and not ms.approved %} + <img src="{% static 'admin/img/icon-no.svg' %}" alt="no"> Rejected: {{ ms.date_approved |date:"Y-m-d H:i" }} + {% else %} + <img src="{% static 'admin/img/icon-unknown.svg' %}" alt="?"> Not approved yet + {% endif %} + </td> + <td> + <!-- membership actions --> + {% if not ms.date_paid %} + <a class="button" href="{% url 'squarepay:pay_membership' ms.id %}">Pay now</a> + {% else %} + - + {% endif %} + </td> + </tr> + {% endfor %} +</table> diff --git a/src/templates/register.html b/src/templates/register.html index 3a318e05a5e26ca892eb50546d32a96d12ca574a..f557da3dc806761d357efbba1361846c08b210c2 100644 --- a/src/templates/register.html +++ b/src/templates/register.html @@ -2,7 +2,7 @@ {% block title %}UCC Registration{% endblock %} -{% block content_title %}<h1>Register as a new member</h1>{% endblock %} +{% block content_title %}<h1>Register as a new member for {{ year }}</h1>{% endblock %} {% block tips %} Enter your details, and press "Register" when you are done.<br> diff --git a/src/templates/renew.html b/src/templates/renew.html index 6fc5842482d01bc541e47ac0d510c059ac79c268..3a67a1f072b80ac4d418589fcab5e5323332807a 100644 --- a/src/templates/renew.html +++ b/src/templates/renew.html @@ -1,7 +1,7 @@ {% extends "base_form.html" %} {% block title %}UCC Membership Renewal{% endblock %} {% block content_title %} - <h1>Renew your membership</h1> + <h1>Renew your membership for {{ year }}</h1> {% endblock %} {% block tips %} @@ -9,17 +9,26 @@ <b>Your account exists already but no membership information has yet been recorded in this system.</b> <br>Please update/correct the details below as necessary. Some have probably been filled for you. {% else %} -Please confirm that the details below have not changed since your last registration or membership renewal. + {% if last_renewal == year %} + You have already submitted a membership renewal for {{ year }}. You may update your details below if they have changed, although any changes you make will need to be approved. + {% else %} + Please confirm that the details below have not changed since your last registration or membership renewal. + {% endif %} {% endif %} {% endblock %} {% block extra_preform %} +{% if memberships and last_renewal == year %} +<div class="form-row"> + {% include 'membership_list.html' %} +</div> +{% endif %} <div class="form-row readonly"> - <label for="id_username">Username:</label> - <span class="text" id="id_username">{{ request.user.username }}</span> + <label for="id_username">Username:</label> + <span class="text" id="id_username">{{ request.user.username }}</span> </div> {% endblock %} {% block action_url %}{% url 'memberdb:renew' %}{% endblock %} -{% block action_text %}Renew{% endblock %} +{% block action_text %}{% if last_renewal == year %}Save & Update{% else %}Renew{% endif %}{% endblock %}