diff --git a/website/payments/static/admin/payments/js/payments.js b/website/payments/static/admin/payments/js/payments.js index d0bc80f290eb950134a619e893acae234aaa0d50..636b087f52c6a655ce307ffb7631f0c155c51c8b 100644 --- a/website/payments/static/admin/payments/js/payments.js +++ b/website/payments/static/admin/payments/js/payments.js @@ -1,5 +1,5 @@ -django.jQuery(function () { - var $ = django.jQuery; +(django.jQuery || jQuery)(function () { + var $ = django.jQuery || jQuery; $(".payments-row a.process").click(function(e) { e.preventDefault(); var type = $(e.target).data('type'); diff --git a/website/registrations/README.rst b/website/registrations/README.rst index 07cb3e93cbc1277a7016f915c416c9489b6cc406..3a8efd02305a6dec77b37ffa803c94dbabc99a9b 100644 --- a/website/registrations/README.rst +++ b/website/registrations/README.rst @@ -5,29 +5,35 @@ Registrations This document explains how the registrations module behaviour is defined. The behaviour of upgrading an existing 'year' membership to a 'study' membership (until graduation) is taken from the HR. If the HR ever changes this behaviour should be changed to reflect those changes. -*Note that registrations and renewals for benefactors are implemented in the models, there are simply no views providing this functionality. If we ever want to implement this then it would be best to create a complete new form just for benefactors registrations.* +This module both provides registration for members and for benefactors. The only difference is the form and view used for their registration since the information we ask from them is different. -New member registration -======================= +New member/benefactor registration +================================== Frontend -------- - User enters information + - If the membership type is 'benefactor': + The amount used in the payment is provided during the registration process by the user. - User accepts privacy policy + This step is obligatory. We do not accept people that do not accept the privacy policy. It's currently implemented as a checkbox in the forms. - System validates info - Correct address - Valid and unique email address - Checked against existing users - Privacy policy accepted - - If the selected member type is 'member': + - If the selected membership type is 'member': - valid and unique student number + - Checked against existing users - selected programme - cohort - Registration model created (status: Awaiting email confirmation) - Email address confirmation sent - User confirms email address - Registration model status changed (status: Ready for review) +- If the registration is for a benefactor an email is sent with a link to get references + - Existing members of Thalia add references using the link Backend ------- @@ -37,9 +43,12 @@ Backend - If it's not unique a username can be entered manually - If it's still not unique the registration cannot be accepted - If it's unique the generated username will be added to the registration - - Payment model is created (processed: False) - - Amount is calculated based on the selected length ('study' or 'year') - - Values are located in thaliawebsite.settings + - Payment model is created (unprocessed at first) + - If the membership type is 'member': + - Amount is calculated based on the selected length ('study' or 'year') + - Values are located in thaliawebsite.settings + - If the membership type is 'benefactor': + - Amount is determined by the value entered during registration - Email is sent as acceptance confirmation containg instructions for `Payment processing`_ 2. Admin rejects registration - Email is sent as rejection message @@ -56,6 +65,8 @@ Frontend - If latest membership has ended or ends within 1 month: also allow 'year' length - If latest membership is 'study' and did not end: do not allow renewal - Renewal model created (status: Ready for review) +- If the renewal is for a benefactor an email is sent with a link to get references + - Existing members of Thalia add references using the link Backend ------- diff --git a/website/registrations/admin.py b/website/registrations/admin.py index a5a2686ee1e66a4970340d64faf03a9e3f9ce707..479b45fe0d670722898359622ff4d30ce09f38b5 100644 --- a/website/registrations/admin.py +++ b/website/registrations/admin.py @@ -7,7 +7,12 @@ from django.utils.translation import ugettext_lazy as _ from payments.widgets import PaymentWidget from . import services -from .models import Entry, Registration, Renewal +from .models import Entry, Registration, Renewal, Reference + + +class ReferenceInline(admin.StackedInline): + model = Reference + extra = 0 def _show_message(admin, request, n, message, error): @@ -29,6 +34,7 @@ class RegistrationAdmin(admin.ModelAdmin): 'created_at', 'payment_status') list_filter = ('status', 'programme', 'payment__type', 'payment__amount') + inlines = (ReferenceInline,) search_fields = ('first_name', 'last_name', 'email', 'phone_number', 'student_number',) date_hierarchy = 'created_at' @@ -38,6 +44,7 @@ class RegistrationAdmin(admin.ModelAdmin): 'updated_at', 'username', 'length', + 'contribution', 'membership_type', 'status', 'payment', @@ -109,8 +116,11 @@ class RegistrationAdmin(admin.ModelAdmin): obj.status == Entry.STATUS_COMPLETED): return ['status', 'created_at', 'updated_at'] else: - return [field.name for field in self.model._meta.get_fields() - if field.editable and not field.name == 'payment'] + return [ + field.name for field in self.model._meta.get_fields() + if not field.name in['payment', 'no_references'] + and field.editable + ] @staticmethod def name(obj): @@ -179,6 +189,7 @@ class RenewalAdmin(RegistrationAdmin): 'created_at', 'updated_at', 'length', + 'contribution', 'membership_type', 'status', 'payment', diff --git a/website/registrations/emails.py b/website/registrations/emails.py index 62333806b32d3ac5ede86ef87394e02986780b6e..1e96595d9de3e7306a145665f9d4bd2599ed1ceb 100644 --- a/website/registrations/emails.py +++ b/website/registrations/emails.py @@ -1,4 +1,6 @@ """The emails defined by the registrations package""" +from typing import Union + from django.conf import settings from django.core import mail from django.template import loader @@ -7,10 +9,11 @@ from django.urls import reverse from django.utils import translation from django.utils.translation import ugettext_lazy as _ -from . import models +from payments.models import Payment +from registrations.models import Registration, Renewal -def send_registration_email_confirmation(registration): +def send_registration_email_confirmation(registration: Registration) -> None: """ Send the email confirmation message @@ -23,8 +26,8 @@ def send_registration_email_confirmation(registration): 'registrations/email/registration_confirm_mail.txt', { 'name': registration.get_full_name(), - 'confirm_link': '{}{}'.format( - settings.BASE_URL, + 'confirm_link': ( + settings.BASE_URL + reverse('registrations:confirm-email', args=[registration.pk]) ) @@ -32,7 +35,8 @@ def send_registration_email_confirmation(registration): ) -def send_registration_accepted_message(registration, payment): +def send_registration_accepted_message(registration: Registration, + payment: Payment) -> None: """ Send the registration acceptance email @@ -51,7 +55,7 @@ def send_registration_accepted_message(registration, payment): ) -def send_registration_rejected_message(registration): +def send_registration_rejected_message(registration: Registration) -> None: """ Send the registration rejection email @@ -68,29 +72,28 @@ def send_registration_rejected_message(registration): ) -def send_new_registration_board_message(entry): +def send_new_registration_board_message(registration: Registration) -> None: """ Send a notification to the board about a new registration - :param entry: the registration entry + :param registration: the registration entry """ - try: - _send_email( - settings.BOARD_NOTIFICATION_ADDRESS, - 'New registration', - 'registrations/email/registration_board.txt', - { - 'name': entry.registration.get_full_name(), - 'url': settings.BASE_URL - + reverse('admin:registrations_registration_change', - args=[entry.registration.pk]) - } - ) - except models.Registration.DoesNotExist: - pass + _send_email( + settings.BOARD_NOTIFICATION_ADDRESS, + 'New registration', + 'registrations/email/registration_board.txt', + { + 'name': registration.get_full_name(), + 'url': ( + settings.BASE_URL + + reverse('admin:registrations_registration_change', + args=[registration.pk]) + ) + } + ) -def send_renewal_accepted_message(renewal, payment): +def send_renewal_accepted_message(renewal: Renewal, payment: Payment) -> None: """ Send the renewal acceptation email @@ -109,7 +112,7 @@ def send_renewal_accepted_message(renewal, payment): ) -def send_renewal_rejected_message(renewal): +def send_renewal_rejected_message(renewal: Renewal) -> None: """ Send the renewal rejection email @@ -126,7 +129,7 @@ def send_renewal_rejected_message(renewal): ) -def send_renewal_complete_message(renewal): +def send_renewal_complete_message(renewal: Renewal) -> None: """ Send the email completing the renewal @@ -143,7 +146,7 @@ def send_renewal_complete_message(renewal): ) -def send_new_renewal_board_message(renewal): +def send_new_renewal_board_message(renewal: Renewal) -> None: """ Send a notification to the board about a new renewal @@ -155,14 +158,53 @@ def send_new_renewal_board_message(renewal): 'registrations/email/renewal_board.txt', { 'name': renewal.member.get_full_name(), - 'url': settings.BASE_URL - + reverse('admin:registrations_renewal_change', - args=[renewal.pk]) + 'url': ( + settings.BASE_URL + + reverse('admin:registrations_renewal_change', + args=[renewal.pk]) + ) } ) -def _send_email(to, subject, body_template, context): +def send_references_information_message( + entry: Union[Registration, Renewal]) -> None: + """ + Send a notification to the user with information about references + These are required for benefactors who have not been a Thalia member + and do not work for iCIS + + :param entry: the registration or renewal entry + """ + + if type(entry).__name__ == 'Registration': + email = entry.email + name = entry.get_full_name() + language = entry.language + else: + email = entry.member.email + name = entry.member.get_full_name() + language = entry.member.profile.language + + print(language) + + with translation.override(language): + _send_email( + email, + _('Information about references'), + 'registrations/email/references_information.txt', + { + 'name': name, + 'reference_link': ( + settings.BASE_URL + + reverse('registrations:reference', args=[entry.pk]) + ) + } + ) + + +def _send_email(to: str, subject: str, + body_template: str, context: dict) -> None: """ Easily send an email with the right subject and a body template diff --git a/website/registrations/forms.py b/website/registrations/forms.py index 857285083bac32369f4b11caa978e155b66fe7d6..1c5193387a049aa5c4e9bf3cc6ac8af6688e03e5 100644 --- a/website/registrations/forms.py +++ b/website/registrations/forms.py @@ -1,15 +1,17 @@ """The forms defined by the registrations package""" from django import forms from django.forms import TypedChoiceField +from django.urls import reverse_lazy from django.utils import timezone +from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from utils.snippets import datetime_to_lectureyear from .models import Registration, Renewal -class MemberRegistrationForm(forms.ModelForm): - """Form for membership registrations""" +class BaseRegistrationForm(forms.ModelForm): + """Base form for membership registrations""" birthday = forms.DateField( widget=forms.widgets.SelectDateWidget(years=[ @@ -20,9 +22,18 @@ class MemberRegistrationForm(forms.ModelForm): privacy_policy = forms.BooleanField( required=True, - label=_('I accept the privacy policy') ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['privacy_policy'].label = mark_safe(_( + 'I accept the privacy policy.').format( + reverse_lazy('privacy-policy'))) + + +class MemberRegistrationForm(BaseRegistrationForm): + """Form for member registrations""" + this_year = datetime_to_lectureyear(timezone.now()) years = reversed([(x, "{} - {}".format(x, x + 1)) for x in range(this_year - 20, this_year + 2)]) @@ -30,25 +41,52 @@ class MemberRegistrationForm(forms.ModelForm): starting_year = TypedChoiceField( choices=years, coerce=int, - empty_value=this_year + empty_value=this_year, + required=False ) class Meta: model = Registration fields = '__all__' - exclude = ['created_at', 'updated_at', 'status', 'username', 'remarks', + exclude = ['created_at', 'updated_at', 'status', 'username', 'payment', 'membership'] -class MemberRenewalForm(forms.ModelForm): +class BenefactorRegistrationForm(BaseRegistrationForm): + """Form for benefactor registrations""" + + icis_employee = forms.BooleanField( + required=False, + label=_('I am an employee of iCIS') + ) + + class Meta: + model = Registration + fields = '__all__' + exclude = ['created_at', 'updated_at', 'status', 'username', + 'starting_year', 'programme', 'payment', 'membership'] + + +class RenewalForm(forms.ModelForm): """Form for membership renewals""" privacy_policy = forms.BooleanField( required=True, - label=_('I accept the privacy policy') ) + icis_employee = forms.BooleanField( + required=False, + label=_('I am an employee of iCIS') + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['privacy_policy'].label = mark_safe(_( + 'I accept the privacy policy.').format( + reverse_lazy('privacy-policy'))) + class Meta: model = Renewal fields = '__all__' - exclude = ['created_at', 'updated_at', 'status', 'remarks'] + exclude = ['created_at', 'updated_at', 'status', + 'payment', 'membership'] diff --git a/website/registrations/locale/nl/LC_MESSAGES/django.mo b/website/registrations/locale/nl/LC_MESSAGES/django.mo index a48a920d7c4254fd26f6eb7c9f8a78f590ce608c..f2b7171996f3e500609f60f88513cd73a941353d 100644 Binary files a/website/registrations/locale/nl/LC_MESSAGES/django.mo and b/website/registrations/locale/nl/LC_MESSAGES/django.mo differ diff --git a/website/registrations/locale/nl/LC_MESSAGES/django.po b/website/registrations/locale/nl/LC_MESSAGES/django.po index 5242a6f3b837d90e9c46aa07abc4fffa8d937eb7..ff696b2bcb0eb44d570189f2890a792f33e2c85d 100644 --- a/website/registrations/locale/nl/LC_MESSAGES/django.po +++ b/website/registrations/locale/nl/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-03-02 16:52+0100\n" -"PO-Revision-Date: 2019-03-02 16:33+0100\n" +"POT-Creation-Date: 2019-04-25 13:34+0200\n" +"PO-Revision-Date: 2019-04-25 13:36+0200\n" "Last-Translator: Thom Wiggers \n" "Language-Team: \n" "Language: nl\n" @@ -96,13 +96,21 @@ msgstr "Verlenging afgekeurd" msgid "Renewal successful" msgstr "Verlenging succesvol" +#: emails.py +msgid "Information about references" +msgstr "Informatie over referenties" + #: forms.py models.py msgid "birthday" msgstr "verjaardag" #: forms.py -msgid "I accept the privacy policy" -msgstr "Ik accepteer het privacybeleid" +msgid "I accept the privacy policy." +msgstr "Ik accepteer het privacybeleid." + +#: forms.py +msgid "I am an employee of iCIS" +msgstr "Ik ben een medewerker van iCIS" #: models.py msgid "created at" @@ -148,6 +156,14 @@ msgstr "Tot afstuderen" msgid "membership length" msgstr "lengte lidmaatschap" +#: models.py +msgid "contribution" +msgstr "contributie" + +#: models.py +msgid "no references required" +msgstr "geen referenties benodigd" + #: models.py templates/registrations/renewal.html msgid "membership type" msgstr "soort lidmaatschap" @@ -157,8 +173,8 @@ msgid "remarks" msgstr "opmerkingen" #: models.py -msgid "Registration entry" -msgstr "Registratie" +msgid "This field is required for benefactors." +msgstr "Dit veld is verplicht voor begunstigers." #: models.py msgid "entry" @@ -281,6 +297,7 @@ msgid "A user with that username already exists." msgstr "Er bestaat al een gebruiker met deze gebruikersnaam." #: models.py templates/registrations/confirm_email.html +#: templates/registrations/register_benefactor.html #: templates/registrations/register_member.html #: templates/registrations/register_success.html msgid "registration" @@ -357,8 +374,10 @@ msgstr "" "lid te worden!" #: templates/registrations/become_a_member.html -msgid "How do I become a member?" -msgstr "Hoe kan ik lid worden?" +msgid "" +"I'm a Computing Science or Information Sciences student at the Radboud " +"University" +msgstr "Ik studeer Informatica of Informatiekunde aan de Radboud Universiteit" #: templates/registrations/become_a_member.html #, python-format @@ -376,37 +395,37 @@ msgstr "" "informatiekunde studeert aan de Radboud Universiteit." #: templates/registrations/become_a_member.html -msgid "Register now" -msgstr "Nu registreren" +msgid "Become a member" +msgstr "Lid worden" #: templates/registrations/become_a_member.html msgid "" "I'm not a Computing Science and Information Sciences student at the Radboud " -"University, but I do want to attend your events. Now what?" +"University" msgstr "" -"Ik studeer geen Informatica of Informatiekunde (aan de Radboud " -"Universiteit), maar ik wil wel naar jullie activiteiten gaan. Wat nu?" +"Ik studeer geen Informatica of Informatiekunde aan de Radboud Universiteit" #: templates/registrations/become_a_member.html +#: templates/registrations/register_benefactor.html #, python-format msgid "" "It is still possible to be associated with Thalia, even if you do not study " "Computing Science or Information Sciences (anymore): You can become a " "benefactor. For at least € %(year_fees)s per year, you too can enjoy " -"everything Thalia has to offer. If you are not a former Thalia member, ICIS " -"staff member or alumni, you must submit a written along with two signatures " -"of current Thalia members. You can fill all of this in on the benefactor " -"form, which you can get at the board room (M1.0.08, ground floor of Mercator " -"1)." +"everything Thalia has to offer. If you are not a former Thalia member, iCIS " +"staff member or alumni, you must collect two references of current Thalia " +"members." msgstr "" "Mocht je nu geen Informatica of Informatiekunde (meer) studeren, maar toch " "verbonden willen zijn, dan kan dat ook. Hiervoor kun je begunstiger van " "Thalia worden. Voor minimaal € %(year_fees)s per jaar kun je ook genieten " -"van alles wat Thalia je te bieden heeft. Ben je geen oud-Thaliaan, ex-" -"Informaticus of -Informatiekundige, dan dien je twee handtekeningen van " -"leden van Thalia samen met een motivatie waarom je begunstiger wil worden af " -"te geven. Dit kun je allemaal invullen op het begunstigerformulier, dat af " -"te halen is in de bestuurskamer (M1.0.08, begane grond Mercator 1)." +"van alles wat Thalia je te bieden heeft. Ben je geen oud-Thaliaan, iCIS " +"medewerker of alumni, dan dien je twee referenties van leden van Thalia te " +"verzamelen." + +#: templates/registrations/become_a_member.html +msgid "Become a benefactor" +msgstr "Begunstiger worden" #: templates/registrations/become_a_member.html msgid "" @@ -433,6 +452,52 @@ msgstr "" "beoordelen. Mocht je vragen hebben, stuur dan een mailtje naar info@thalia." "nu." +#: templates/registrations/email/references_information.txt +#, python-format +msgid "" +"Dear %(name)s,\n" +"\n" +"Our information indicates that you're not an iCIS employee or\n" +"a former Thalia member.\n" +"\n" +"This means that before we can review your membership registration we\n" +"need to receive two references of current Thalia members.\n" +"\n" +"Share the following link with them to obtain their reference:\n" +"%(reference_link)s\n" +"\n" +"If you have any questions, then don't hesitate and send an email to " +"info@thalia.nu.\n" +"\n" +"With kind regards,\n" +"\n" +"The board of Study Association Thalia\n" +"\n" +"————\n" +"\n" +"This email was automatically generated." +msgstr "" +"Beste %(name)s,\n" +"\n" +"Volgens onze informatie ben je geen medewerker van iCIS\n" +"of een voormalig lid van Thalia.\n" +"\n" +"Dit betekent dat we twee referenties van Thalia leden moeten ontvangen\n" +"Voordat we je aanmelding kunnen behandelen.\n" +"\n" +"Deel de volgende link met een Thalia lid om een referentie te verkrijgen:\n" +"%(reference_link)s\n" +"\n" +"Mocht je vragen hebben, stuur dan vooral een mailtje naar info@thalia.nu.\n" +"\n" +"Met vriendelijke groet,\n" +"\n" +"Het bestuur der Studievereniging Thalia\n" +"\n" +"————\n" +"\n" +"Deze e-mail is automatisch gegenereerd." + #: templates/registrations/email/registration_accepted.txt #, python-format msgid "" @@ -693,29 +758,49 @@ msgstr "" "\n" "Deze e-mail is automatisch gegenereerd." -#: templates/registrations/register_member.html +#: templates/registrations/reference.html +#: templates/registrations/reference_success.html +msgid "reference" +msgstr "referentie" + +#: templates/registrations/reference.html +#: templates/registrations/reference_success.html +msgid "give reference" +msgstr "geef referentie" + +#: templates/registrations/reference.html +#: templates/registrations/reference_success.html #, python-format msgid "" -"A membership costs € %(year_fees)s per year, or € %(study_fees)s for your " -"entire study duration.
Note: Only Computing Science and Information " -"Sciences students at the Radboud University can become a member.

" "It is still possible to be associated with Thalia, even if you do not study " "Computing Science or Information Sciences (anymore): You can become a " -"benefactor. For at least € %(year_fees)s per year, you too can enjoy " -"everything Thalia has to offer.
Note that this form is only for " -"member registration. Please visit the board room if you want to become a " -"benefactor." +"benefactor. If you are not a former Thalia member, iCIS staff member or " +"alumni, you must collect two references of current Thalia members.

%(name)s wants to become a benefactor of Thalia and " +"needs such a reference and has asked you to give it to them." msgstr "" -"Een lidmaatschap kost € %(year_fees)s per jaar, of € %(study_fees)s voor je " -"hele studieperiode.
Let wel op: Je kunt alleen lid worden wanneer je " -"informatica of informatiekunde studeert aan de Radboud Universiteit.

Mocht je nu geen Informatica of Informatiekunde (meer) studeren, maar toch " +"Mocht je nu geen Informatica of Informatiekunde (meer) studeren, maar toch " "verbonden willen zijn, dan kan dat ook. Hiervoor kun je begunstiger van " -"Thalia worden. Voor minimaal € %(year_fees)s per jaar kun je ook genieten " -"van alles wat Thalia je te bieden heeft.
Let op dat dit formulier " -"alleen gebruikt kan worden voor registratie van leden. Neem contact op met " -"het bestuur als je begunstiger wilt worden." +"Thalia worden. Ben je geen oud-Thaliaan, iCIS medewerker of alumni, dan dien " +"je twee referenties van leden van Thalia te verzamelen.

" +"%(name)s wil graag begunstiger van Thalia worden en heeft jouw " +"gevraagd om een deze te geven." + +#: templates/registrations/reference.html +msgid "Your reference has been saved." +msgstr "Je referentie is opgeslagen." +#: templates/registrations/reference.html +#: templates/registrations/reference_success.html +msgid "You've already given a reference for this person." +msgstr "Je hebt deze persoon al een referentie gegeven." + +#: templates/registrations/register_benefactor.html +#: templates/registrations/renewal.html +msgid "Benefactor" +msgstr "Begunstiger" + +#: templates/registrations/register_benefactor.html #: templates/registrations/register_member.html msgid "" "If you've been a member before you should login using your existing account " @@ -726,10 +811,18 @@ msgstr "" "en vernieuw je lidmaatschap in de accountinstellingen. Je kunt je niet " "opnieuw registreren via dit formulier." +#: templates/registrations/register_benefactor.html #: templates/registrations/register_member.html -msgid "Display birthday in calendar" -msgstr "Toon verjaardag in de kalender" +msgid "" +"If you have any other questions about Thalia and/or your membership, feel " +"free to email info@thalia.nu!" +msgstr "" +"Mocht je nog verdere vragen hebben over Thalia en/of je lidmaatschap dan kun " +"je altijd mailen naar info@thalia.nu!" +#: templates/registrations/register_benefactor.html #: templates/registrations/register_member.html msgid "" "Receive emails about (amongst others) job opportunities and in-house days " @@ -738,21 +831,45 @@ msgstr "" "Ik wil e-mails ontvangen over (onder andere) baankansen en in-house dagen " "van partners van Thalia." +#: templates/registrations/register_benefactor.html #: templates/registrations/register_member.html -#: templates/registrations/renewal.html -#, python-format -msgid "" -"I accept the privacy " -"policy" -msgstr "" -"Ik ga akkoord met het privacybeleid" +msgid "Display birthday in calendar" +msgstr "Toon verjaardag in de kalender" +#: templates/registrations/register_benefactor.html #: templates/registrations/register_member.html #: templates/registrations/renewal.html msgid "send" msgstr "verstuur" +#: templates/registrations/register_member.html +msgid "Member" +msgstr "Lid" + +#: templates/registrations/register_member.html +#, python-format +msgid "" +"A membership costs € %(year_fees)s per year, or € %(study_fees)s for your " +"entire study duration.
Note: Only Computing Science and Information " +"Sciences students at the Radboud University can become a member.

" +"It is still possible to be associated with Thalia, even if you do not study " +"Computing Science or Information Sciences (anymore): You can become a " +"benefactor. For at least € %(year_fees)s per year, you too can enjoy " +"everything Thalia has to offer.
Note that this form is only for " +"member registration. Please use the benefactor registration page if you want to become a benefactor. " +msgstr "" +"Een lidmaatschap kost € %(year_fees)s per jaar, of € %(study_fees)s voor je " +"hele studieperiode.
Let wel op: Je kunt alleen lid worden wanneer je " +"informatica of informatiekunde studeert aan de Radboud Universiteit.

Mocht je nu geen Informatica of Informatiekunde (meer) studeren, maar toch " +"verbonden willen zijn, dan kan dat ook. Hiervoor kun je begunstiger van " +"Thalia worden. Voor minimaal € %(year_fees)s per jaar kun je ook genieten " +"van alles wat Thalia je te bieden heeft.
Let op dat dit formulier " +"alleen gebruikt kan worden voor registratie van leden. Gebruik de registratiepagina voor begunstigers als je " +"een begunstiger wilt worden." + #: templates/registrations/register_success.html msgid "registration success" msgstr "registratie gelukt" @@ -845,12 +962,6 @@ msgstr "" "Je bent een erelid. Dat betekent dat je je lidmaatschap nooit hoeft te " "verlengen." -#: templates/registrations/renewal.html -msgid "You're a benefactor. Contact the board to renew your membership." -msgstr "" -"Je bent een begunstiger. Neem contact op met het bestuur om je lidmaatschap " -"te verlengen." - #: templates/registrations/renewal.html msgid "" "You currently have an active membership for your entire study duration. Did " @@ -861,10 +972,6 @@ msgstr "" "afstudeert. Ben je klaar met studeren of ben je om een andere reden gestopt? " "Laat ons dat alsjeblieft weten door een mail te sturen naar info@thalia.nu." -#: templates/registrations/renewal.html -msgid "Benefactor" -msgstr "Begunstiger" - #: templates/registrations/renewal_success.html msgid "success" msgstr "gelukt" diff --git a/website/registrations/migrations/0019_auto_20190425_1107.py b/website/registrations/migrations/0019_auto_20190425_1107.py new file mode 100644 index 0000000000000000000000000000000000000000..9aa8570b3bcfc458d3eba1c0eddc4be99bfda105 --- /dev/null +++ b/website/registrations/migrations/0019_auto_20190425_1107.py @@ -0,0 +1,37 @@ +# Generated by Django 2.2 on 2019-04-25 09:07 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('members', '0031_benefactor_model_value'), + ('registrations', '0018_benefactor_model_value'), + ] + + operations = [ + migrations.AddField( + model_name='entry', + name='contribution', + field=models.FloatField(blank=True, default=7.5, null=True, validators=[django.core.validators.MinValueValidator(7.5)], verbose_name='contribution'), + ), + migrations.AddField( + model_name='entry', + name='no_references', + field=models.BooleanField(default=False, verbose_name='no references required'), + ), + migrations.CreateModel( + name='Reference', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('entry', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='registrations.Entry', verbose_name='entry')), + ('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='members.Member', verbose_name='member')), + ], + options={ + 'unique_together': {('member', 'entry')}, + }, + ), + ] diff --git a/website/registrations/models.py b/website/registrations/models.py index e437687978b43b8e9258160b6b87dae27c964534..b09f677e37eb6f6de009b4a51f4d7db82e1105df 100644 --- a/website/registrations/models.py +++ b/website/registrations/models.py @@ -5,13 +5,13 @@ from django.conf import settings from django.contrib.auth import get_user_model from django.core import validators from django.core.exceptions import ValidationError +from django.core.validators import MinValueValidator from django.db import models from django.template.defaultfilters import floatformat from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from members.models import Membership, Profile -from registrations import emails from utils import countries @@ -62,6 +62,19 @@ class Entry(models.Model): MEMBERSHIP_TYPES = [m for m in Membership.MEMBERSHIP_TYPES if m[0] != Membership.HONORARY] + contribution = models.FloatField( + verbose_name=_('contribution'), + validators=[MinValueValidator(settings.MEMBERSHIP_PRICES['year'])], + default=settings.MEMBERSHIP_PRICES['year'], + blank=True, + null=True, + ) + + no_references = models.BooleanField( + verbose_name=_('no references required'), + default=False + ) + membership_type = models.CharField( verbose_name=_('membership type'), choices=MEMBERSHIP_TYPES, @@ -96,8 +109,28 @@ class Entry(models.Model): self.status != self.STATUS_REJECTED): self.updated_at = timezone.now() + if (self.contribution is not None and + self.membership_type != Membership.BENEFACTOR): + self.contribution = None + elif self.membership_type == Membership.BENEFACTOR: + self.length = self.MEMBERSHIP_YEAR + super().save(force_insert, force_update, using, update_fields) + def clean(self): + super().clean() + errors = {} + + if (self.contribution is None and + self.membership_type == Membership.BENEFACTOR): + errors.update({ + 'contribution': + _('This field is required for benefactors.') + }) + + if errors: + raise ValidationError(errors) + def __str__(self): try: return self.registration.__str__() @@ -294,12 +327,6 @@ class Registration(Entry): if errors: raise ValidationError(errors) - def save(self, *args, **kwargs): - send_confirm_email = self.pk is None - super().save(*args, **kwargs) - if send_confirm_email: - emails.send_registration_email_confirmation(self) - def __str__(self): return '{} {} ({})'.format(self.first_name, self.last_name, self.email) @@ -329,7 +356,8 @@ class Renewal(Entry): errors = {} if Renewal.objects.filter( - member=self.member, status=Entry.STATUS_REVIEW).exists(): + member=self.member, status=Entry.STATUS_REVIEW + ).exclude(pk=self.pk).exists(): raise ValidationError(_('You already have a renewal ' 'request queued for review.')) @@ -373,3 +401,28 @@ class Renewal(Entry): class Meta: verbose_name = _('renewal') verbose_name_plural = _('renewals') + + +class Reference(models.Model): + """Describes a reference of a member for a potential member""" + member = models.ForeignKey( + 'members.Member', + on_delete=models.CASCADE, + verbose_name=_('member'), + blank=False, + null=False, + ) + + entry = models.ForeignKey( + 'registrations.Entry', + on_delete=models.CASCADE, + verbose_name=_('entry'), + blank=False, + null=False + ) + + def __str__(self): + return f'Reference from {self.member} for {self.entry}' + + class Meta: + unique_together = ('member', 'entry') diff --git a/website/registrations/services.py b/website/registrations/services.py index 05379c69cedc298e423351f1ac7c3ad74344edbc..b42971b1921bf94354899419cc9af828043590ad 100644 --- a/website/registrations/services.py +++ b/website/registrations/services.py @@ -1,23 +1,24 @@ """The services defined by the registrations package""" import string import unicodedata +from typing import Union from django.conf import settings from django.contrib.admin.models import LogEntry, CHANGE from django.contrib.admin.options import get_content_type_for_model from django.contrib.auth import get_user_model -from django.db.models import Q +from django.db.models import Q, QuerySet from django.utils import timezone import members -from members.models import Membership, Profile +from members.models import Membership, Profile, Member from payments.models import Payment from registrations import emails from registrations.models import Entry, Registration, Renewal from utils.snippets import datetime_to_lectureyear -def _generate_username(registration): +def _generate_username(registration: Registration) -> str: """ Create username from first and lastname @@ -39,7 +40,7 @@ def _generate_username(registration): return username -def check_unique_user(entry): +def check_unique_user(entry: Entry) -> bool: """ Check that the username and email address of the entry are unique. @@ -62,7 +63,7 @@ def check_unique_user(entry): return True -def confirm_entry(queryset): +def confirm_entry(queryset: QuerySet) -> int: """ Confirm all entries in the queryset @@ -77,10 +78,11 @@ def confirm_entry(queryset): return rows_updated -def reject_entries(user_id, queryset): +def reject_entries(user_id: int, queryset: QuerySet) -> int: """ Reject all entries in the queryset + :param user_id: Id of the user executing this action :param queryset: queryset of entries :type queryset: Queryset[Entry] :return: number of updated rows @@ -117,10 +119,11 @@ def reject_entries(user_id, queryset): return rows_updated -def accept_entries(user_id, queryset): +def accept_entries(user_id: int, queryset: QuerySet) -> int: """ Accept all entries in the queryset + :param user_id: Id of the user executing this action :param queryset: queryset of entries :type queryset: Queryset[Entry] :return: number of updated rows @@ -174,10 +177,11 @@ def accept_entries(user_id, queryset): return len(updated_entries) -def revert_entry(user_id, entry): +def revert_entry(user_id: int, entry: Entry) -> None: """ Revert status of entry to review so that it can be corrected + :param user_id: Id of the user executing this action :param entry: Entry that should be reverted """ if not (entry.status in [Entry.STATUS_ACCEPTED, Entry.STATUS_REJECTED]): @@ -201,8 +205,6 @@ def revert_entry(user_id, entry): except Renewal.DoesNotExist: pass - print(log_obj) - if log_obj: LogEntry.objects.log_action( user_id=user_id, @@ -214,7 +216,7 @@ def revert_entry(user_id, entry): ) -def _create_payment_for_entry(entry): +def _create_payment_for_entry(entry: Entry) -> Payment: """ Create payment model for entry @@ -224,6 +226,8 @@ def _create_payment_for_entry(entry): :rtype: Payment """ amount = settings.MEMBERSHIP_PRICES[entry.length] + if entry.contribution and entry.membership_type == Membership.BENEFACTOR: + amount = entry.contribution notes = f'Membership registration. {entry.get_membership_type_display()}.' try: @@ -242,7 +246,7 @@ def _create_payment_for_entry(entry): # we're checking if that is the case so that these members # still get the discount price if (membership is not None and membership.until is not None and - entry.created_at.date() < membership.until and + entry.created_at.date() < membership.until and renewal.length == Entry.MEMBERSHIP_STUDY): amount = (settings.MEMBERSHIP_PRICES[Entry.MEMBERSHIP_STUDY] - settings.MEMBERSHIP_PRICES[Entry.MEMBERSHIP_YEAR]) @@ -255,7 +259,7 @@ def _create_payment_for_entry(entry): ) -def _create_member_from_registration(registration): +def _create_member_from_registration(registration: Registration) -> Member: """ Create User and Member model from Registration @@ -303,10 +307,11 @@ def _create_member_from_registration(registration): # Send welcome message to new member members.emails.send_welcome_message(user, password, registration.language) - return user + return Member.objects.get(pk=user.pk) -def _create_membership_from_entry(entry, member=None): +def _create_membership_from_entry( + entry: Entry, member: Member = None) -> Union[Membership, None]: """ Create or update Membership model based on Entry model information @@ -371,7 +376,7 @@ def _create_membership_from_entry(entry, member=None): ) -def process_payment(payment): +def process_payment(payment: Payment) -> None: """ Process the payment for the entry and send the right emails diff --git a/website/registrations/static/registrations/css/style.scss b/website/registrations/static/registrations/css/style.scss index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..c1e02a505d5fa7a523b8cfa160ee21298f1a44c4 100644 --- a/website/registrations/static/registrations/css/style.scss +++ b/website/registrations/static/registrations/css/style.scss @@ -0,0 +1,9 @@ +#registrations-form { + .required-field { + label::after { + content: '*'; + margin-left: 2px; + color: $dark-grey; + } + } +} diff --git a/website/registrations/static/registrations/js/main.js b/website/registrations/static/registrations/js/main.js new file mode 100644 index 0000000000000000000000000000000000000000..da0666cf3cd257306ce5e4f538064a1f33707e22 --- /dev/null +++ b/website/registrations/static/registrations/js/main.js @@ -0,0 +1,51 @@ +function changeVisiblity(value) { + var bType = $('form').data('benefactor-type'); + if (value === bType) { + $('#id_contribution').parent().removeClass('d-none'); + $('#id_length').parent().addClass('d-none'); + $('#id_length').val('year'); + } else { + $('#id_contribution').parent().addClass('d-none'); + $('#id_length').parent().removeClass('d-none'); + $('#id_length').val(''); + } +} + +$(function() { + var membershipEl = $('select#id_membership_type'); + if (membershipEl.length !== 0) { + changeVisiblity(membershipEl.val()); + + membershipEl.change(function () { + changeVisiblity(this.value); + }); + } + + var input = document.querySelector('#id_address_street'); + var autocomplete = new google.maps.places.Autocomplete(input); + autocomplete.addListener('place_changed', function () { + var place = autocomplete.getPlace(); + + var getAddressItem = function(type, length) { + var address = place.address_components; + var addressItem = address.find(function (item) { + return item.types.includes(type); + }); + var key = length + '_name'; + return addressItem && addressItem[key] ? addressItem[key] : ''; + }; + + $('#id_address_street').val( + getAddressItem('route', 'long') + ' ' + + getAddressItem('street_number', 'long')); + + $('#id_address_city').val( + getAddressItem('locality', 'long')); + + $('#id_address_postal_code').val( + getAddressItem('postal_code', 'long')); + + $('#id_address_country').val( + getAddressItem('country', 'short').toUpperCase()); + }); +}); diff --git a/website/registrations/templates/registrations/become_a_member.html b/website/registrations/templates/registrations/become_a_member.html index 59414506b3e36052c05bf63dc3ea4503924e1f0c..d4d417ab360a1fedf4cff1f500228d3da62cb0b1 100644 --- a/website/registrations/templates/registrations/become_a_member.html +++ b/website/registrations/templates/registrations/become_a_member.html @@ -9,39 +9,53 @@

{% trans "Become a Member" %}

-

{% trans "Thalia is the study association for Computing Science and Information Sciences students at the Radboud University in Nijmegen. Thalia organises a wide variety of activities, such as bowling events, go cart racing, lunch lectures, drinks and much more! Furthermore, members get access to our tests and summaries database, as well as discounts on books. There's no reason not to become a member!" %}

+

{% trans "Thalia is the study association for Computing Science and Information Sciences students at the Radboud University in Nijmegen. Thalia organises a wide variety of activities, such as bowling events, go cart racing, lunch lectures, drinks and much more! Furthermore, members get access to our tests and summaries database, as well as discounts on books. There's no reason not to become a member!" %}

-

{% trans "How do I become a member?" %}

+
+
+

{% trans "I'm a Computing Science or Information Sciences student at the Radboud University" %}

-

- {% blocktrans trimmed %} - You can become a member of Thalia at any time during the year. A membership costs € - {{ year_fees }} per year, or € {{ study_fees }} for your entire study duration. Click on the button - below to go to the registration form. Note: Only Computing Science and Information Sciences students - at the Radboud University can become a member. - {% endblocktrans %} -

+

+ {% blocktrans trimmed %} + You can become a member of Thalia at any time during the year. A membership costs € + {{ year_fees }} per year, or € {{ study_fees }} for your entire study duration. Click on the button + below to go to the registration form. Note: Only Computing Science and Information Sciences students + at the Radboud University can become a member. + {% endblocktrans %} +

+
-

- - {% trans "Register now" %} - -

+ -

{% trans "I'm not a Computing Science and Information Sciences student at the Radboud University, but I do want to attend your events. Now what?" %}

+
+

{% trans "I'm not a Computing Science and Information Sciences student at the Radboud University" %}

-

- {% blocktrans trimmed %} - It is still possible to be associated with Thalia, even if you do not study Computing - Science or Information Sciences (anymore): You can become a benefactor. For at least € - {{ year_fees }} per year, you too can enjoy everything Thalia has to offer. - If you are not a former Thalia member, ICIS staff member or alumni, you must submit a written along - with two signatures of current Thalia members. You can fill all of this in on the benefactor form, - which you can get at the board room (M1.0.08, ground floor of Mercator 1). - {% endblocktrans %} -

+

+ {% blocktrans trimmed %} + It is still possible to be associated with Thalia, even if you do not study Computing + Science or Information Sciences (anymore): You can become a benefactor. For at least € + {{ year_fees }} per year, you too can enjoy everything Thalia has to offer. + If you are not a former Thalia member, iCIS staff member or alumni, you must collect two references of current Thalia members. + {% endblocktrans %} +

+
+ + +
-

+

{% blocktrans trimmed %} Payment can be made both in cash or by card. If you have any other questions about Thalia and/or your membership, feel free to email diff --git a/website/registrations/templates/registrations/email/references_information.txt b/website/registrations/templates/registrations/email/references_information.txt new file mode 100644 index 0000000000000000000000000000000000000000..9e28c846b80520dd242ebf255fee0f71468e125b --- /dev/null +++ b/website/registrations/templates/registrations/email/references_information.txt @@ -0,0 +1,20 @@ +{% load i18n %}{% blocktrans %}Dear {{ name }}, + +Our information indicates that you're not an iCIS employee or +a former Thalia member. + +This means that before we can review your membership registration we +need to receive two references of current Thalia members. + +Share the following link with them to obtain their reference: +{{ reference_link }} + +If you have any questions, then don't hesitate and send an email to info@thalia.nu. + +With kind regards, + +The board of Study Association Thalia + +———— + +This email was automatically generated.{% endblocktrans %} diff --git a/website/registrations/templates/registrations/reference.html b/website/registrations/templates/registrations/reference.html new file mode 100644 index 0000000000000000000000000000000000000000..7d6b08de92591184b163fe27e21657e90572c071 --- /dev/null +++ b/website/registrations/templates/registrations/reference.html @@ -0,0 +1,42 @@ +{% extends "base.html" %} +{% load i18n bootstrap4 alert %} + +{% block title %}{% trans "reference"|capfirst %} — {{ block.super }}{% endblock %} + +{% block body %} +

+
+

{% trans "give reference" %}

+ +

+ {% blocktrans trimmed %} + It is still possible to be associated with Thalia, even if you do not study Computing + Science or Information Sciences (anymore): You can become a benefactor. + If you are not a former Thalia member, iCIS staff member or alumni, + you must collect two references of current Thalia members. +

+ {{ name }} wants to become a benefactor of Thalia and + needs such a reference and has asked you to give it to them. + {% endblocktrans %} +

+ +
+ + {% if success %} + {% trans "Your reference has been saved." as alert_text %} + {% alert 'success' alert_text extra_classes="mt-3" %} + {% elif form.errors %} + {% trans "You've already given a reference for this person." as alert_text %} + {% alert 'danger' alert_text extra_classes="mt-3" %} + {% else %} +
+ {% csrf_token %} + + +
+ {% endif %} +
+
+{% endblock %} diff --git a/website/registrations/templates/registrations/reference_success.html b/website/registrations/templates/registrations/reference_success.html new file mode 100644 index 0000000000000000000000000000000000000000..6b810b1d1fe92518476b9cd8a9dc9c79a3d4b947 --- /dev/null +++ b/website/registrations/templates/registrations/reference_success.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} +{% load i18n bootstrap4 alert %} + +{% block title %}{% trans "reference"|capfirst %} — {{ block.super }}{% endblock %} + +{% block body %} +
+
+

{% trans "give reference" %}

+ +

+ {% blocktrans trimmed %} + It is still possible to be associated with Thalia, even if you do not study Computing + Science or Information Sciences (anymore): You can become a benefactor. + If you are not a former Thalia member, iCIS staff member or alumni, + you must collect two references of current Thalia members. +

+ {{ name }} wants to become a benefactor of Thalia and + needs such a reference and has asked you to give it to them. + {% endblocktrans %} +

+ +
+ + {% if form.errors %} + {% trans "You've already given a reference for this person." as alert_text %} + {% alert 'danger' alert_text extra_classes="mt-3" %} + {% else %} +
+ {% csrf_token %} + + +
+ {% endif %} +
+
+{% endblock %} diff --git a/website/registrations/templates/registrations/register_benefactor.html b/website/registrations/templates/registrations/register_benefactor.html new file mode 100644 index 0000000000000000000000000000000000000000..bc186200e652d40b684989841db61ec75e3d36bf --- /dev/null +++ b/website/registrations/templates/registrations/register_benefactor.html @@ -0,0 +1,97 @@ +{% extends "base.html" %} +{% load i18n static compress bootstrap4 %} + +{% block title %}{% trans "registration"|capfirst %} — {{ block.super }}{% endblock %} + +{% block js_body %} + {{ block.super }} + + {% compress js %} + + {% endcompress %} +{% endblock %} + +{% block body %} +
+
+

{% trans "registration" %} {% trans "Benefactor" %}

+ +

+ {% blocktrans trimmed %} + It is still possible to be associated with Thalia, even if you do not study Computing + Science or Information Sciences (anymore): You can become a benefactor. For at least € + {{ year_fees }} per year, you too can enjoy everything Thalia has to offer. + If you are not a former Thalia member, iCIS staff member or alumni, you must collect two references of current Thalia members. + {% endblocktrans %} +

+ +

+ {% blocktrans trimmed %} + If you've been a member before you should login using your existing account and renew your + membership by visiting the account settings. + You'll be unable to re-register using this form. + {% endblocktrans %} +

+ +

+ {% blocktrans trimmed %} + If you have any other questions about Thalia and/or your membership, feel free to email + info@thalia.nu! + {% endblocktrans %} +

+ +
+ +
+ {% csrf_token %} + +
+ {% bootstrap_field form.first_name %} + {% bootstrap_field form.last_name %} + + {% bootstrap_field form.address_street %} + {% bootstrap_field form.address_street2 %} + {% bootstrap_field form.address_postal_code %} + {% bootstrap_field form.address_city %} + {% bootstrap_field form.address_country %} +
+ +
+ {% bootstrap_field form.email %} + +
+
+ + +
+
+ + {% bootstrap_field form.phone_number %} + + {% bootstrap_field form.birthday %} + +
+
+ + +
+
+ + {% bootstrap_field form.student_number %} + {% bootstrap_field form.icis_employee %} + {% bootstrap_field form.contribution %} + {% bootstrap_field form.privacy_policy %} +
+ + +
+
+
+{% endblock %} diff --git a/website/registrations/templates/registrations/register_member.html b/website/registrations/templates/registrations/register_member.html index 52c5af44380dcd73f5ebb8726eaecad7aef69438..545d802cd90ba378afb60b317468da210b341fc0 100644 --- a/website/registrations/templates/registrations/register_member.html +++ b/website/registrations/templates/registrations/register_member.html @@ -3,12 +3,22 @@ {% block title %}{% trans "registration"|capfirst %} — {{ block.super }}{% endblock %} +{% block js_body %} + {{ block.super }} + + {% compress js %} + + {% endcompress %} +{% endblock %} + {% block body %} -
+
-

{% trans "registration" %}

+

{% trans "registration" %} {% trans "Member" %}

+ {% url 'registrations:register-benefactor' as benefactor_register %} {% blocktrans trimmed %} A membership costs € {{ year_fees }} per year, or € {{ study_fees }} for your entire study duration.
@@ -17,8 +27,11 @@ It is still possible to be associated with Thalia, even if you do not study Computing Science or Information Sciences (anymore): You can become a benefactor. For at least € {{ year_fees }} per year, you too can enjoy everything Thalia has to offer.
- Note that this form is only for member registration. Please visit the board room if you want to - become a benefactor. + + Note that this form is only for member registration. + Please use the benefactor registration page + if you want to become a benefactor. + {% endblocktrans %}

@@ -30,29 +43,32 @@ {% endblocktrans %}

+

+ {% blocktrans trimmed %} + If you have any other questions about Thalia and/or your membership, feel free to email + info@thalia.nu! + {% endblocktrans %} +

+
-
+ {% csrf_token %} -
+
{% bootstrap_field form.length %} -
-
{% bootstrap_field form.first_name %} {% bootstrap_field form.last_name %} - {% bootstrap_field form.birthday %} -
-
- - -
-
+ {% bootstrap_field form.address_street %} + {% bootstrap_field form.address_street2 %} + {% bootstrap_field form.address_postal_code %} + {% bootstrap_field form.address_city %} + {% bootstrap_field form.address_country %} +
+
{% bootstrap_field form.email %}
@@ -65,36 +81,26 @@
{% bootstrap_field form.phone_number %} -
-
- {% bootstrap_field form.address_street %} - {% bootstrap_field form.address_street2 %} - {% bootstrap_field form.address_postal_code %} - {% bootstrap_field form.address_city %} - {% bootstrap_field form.address_country %} -
- -
- {% bootstrap_field form.student_number %} - {% bootstrap_field form.programme %} - {% bootstrap_field form.starting_year %} -
+ {% bootstrap_field form.birthday %} -
- + for="id_optin_birthday">{% trans "Display birthday in calendar" %}
+ + {% bootstrap_field form.student_number %} + {% bootstrap_field form.programme %} + {% bootstrap_field form.starting_year %} + + {% bootstrap_field form.privacy_policy %}
- +
diff --git a/website/registrations/templates/registrations/renewal.html b/website/registrations/templates/registrations/renewal.html index 98a1f225dd696f74d08d85642a44abe6b29f8af0..bf9bb8205679458aaaebe8ed682e2949c9b4764d 100644 --- a/website/registrations/templates/registrations/renewal.html +++ b/website/registrations/templates/registrations/renewal.html @@ -1,8 +1,14 @@ {% extends "base.html" %} -{% load i18n bootstrap4 alert %} +{% load i18n bootstrap4 alert compress static %} -{% block title %}{% trans "renewal"|capfirst %} — - {{ block.super }}{% endblock %} +{% block title %}{% trans "renewal"|capfirst %} — {{ block.super }}{% endblock %} + +{% block js_body %} + {{ block.super }} + {% compress js %} + + {% endcompress %} +{% endblock %} {% block body %}
@@ -109,13 +115,6 @@ have to renew your membership. {% endblocktrans %}

- {% elif latest_membership.type == 'benefactor' and not was_member %} -

- {% blocktrans trimmed %} - You're a benefactor. Contact the board to renew your - membership. - {% endblocktrans %} -

{% elif latest_membership.until is None %}

{% blocktrans trimmed %} @@ -132,44 +131,41 @@ {% endfor %} {% endfor %}

+ class="col-lg-6 offset-lg-3" data-benefactor-type="{{ benefactor_type }}"> {% csrf_token %} +
{% if not latest_membership.type == 'benefactor' %} {% bootstrap_field form.membership_type %} {% bootstrap_field form.length %} + {% bootstrap_field form.contribution form_group_class='form-group required-field d-none' %} {% else %}
+ id="id_membership_type" + type="text" class="form-control" />
+ value="{% trans 'One year' %}" + type="text" + id="id_membership_length" + class="form-control" />
+ {% bootstrap_field form.contribution %} {% endif %} +
-
-
- - -
-
+ {% bootstrap_field form.privacy_policy %} + {% if latest_membership.type == 'benefactor' %} + {% bootstrap_field form.icis_employee %} + {% endif %}
John Doe wants to become a benefactor' + ) + + with self.subTest('Renewal'): + response = self.client.get(reverse('registrations:reference', + args=(self.renewal.pk,))) + self.assertEqual(200, response.status_code) + self.assertEqual('Johnny Test', response.context['name']) + self.assertEqual(False, response.context['success']) + self.assertContains( + response, + 'Johnny Test wants to become a benefactor' + ) + + def test_entry_saves_correctly(self): + """ + If a entry is saved it should redirect to the success page + which should show the right content. And the Reference object + should be saved. + """ + response = self.client.post( + reverse('registrations:reference', args=(self.registration.pk,)), + follow=True + ) + self.assertEqual(200, response.status_code) + self.assertEqual( + [(reverse('registrations:reference-success', + args=(self.registration.pk,)), 302)], + response.redirect_chain + ) + self.assertEqual('John Doe', response.context['name']) + self.assertEqual(True, response.context['success']) + self.assertContains( + response, + 'Your reference has been saved.' + ) + + self.assertTrue( + Reference.objects.filter( + member=self.login_user, + entry=self.registration + ).exists() + ) + + def test_entry_reference_exists(self): + """ + If there is already a reference for an entry then the page should + show an error and not redirect. + """ + Reference.objects.create( + member=self.login_user, + entry=self.registration + ) + + response = self.client.post( + reverse('registrations:reference', args=(self.registration.pk,)), + follow=True + ) + self.assertEqual(200, response.status_code) + self.assertEqual( + [], + response.redirect_chain + ) + self.assertEqual({ + '__all__': ['Reference with this Member and Entry already exists.'] + }, response.context['form'].errors) + self.assertEqual(False, response.context['success']) + self.assertContains( + response, + 'You\'ve already given a reference for this person.' + ) diff --git a/website/registrations/urls.py b/website/registrations/urls.py index 252b5dbbb49b0afb9138357661bc2052d7dcaeaa..fde62b645bf3d992964d4dc4353d55686379105a 100644 --- a/website/registrations/urls.py +++ b/website/registrations/urls.py @@ -4,13 +4,17 @@ from django.views.generic import TemplateView from .views import (BecomeAMemberView, ConfirmEmailView, EntryAdminView, MemberRegistrationFormView, - RenewalFormView) + RenewalFormView, BenefactorRegistrationFormView, + ReferenceCreateView) app_name = "registrations" urlpatterns = [ path('', BecomeAMemberView.as_view(), name='index'), - path('register/', MemberRegistrationFormView.as_view(), name='register'), + path('register/member/', MemberRegistrationFormView.as_view(), + name='register-member'), + path('register/benefactor/', BenefactorRegistrationFormView.as_view(), + name='register-benefactor'), path('register/success/', TemplateView.as_view( template_name='registrations/register_success.html'), name='register-success'), @@ -22,4 +26,8 @@ urlpatterns = [ name='admin-process'), path('confirm-email//', ConfirmEmailView.as_view(), name='confirm-email'), + path('reference//', + ReferenceCreateView.as_view(), name='reference'), + path('reference//success', + ReferenceCreateView.as_view(success=True), name='reference-success'), ] diff --git a/website/registrations/views.py b/website/registrations/views.py index e4619a7a3766aa89793fdfa7974c9cc723e9a583..7f0fee016dcaaa3b57a1f89b169c81b2c2bac5bf 100644 --- a/website/registrations/views.py +++ b/website/registrations/views.py @@ -6,19 +6,21 @@ from django.contrib.admin.views.decorators import staff_member_required from django.contrib.auth.decorators import login_required, permission_required from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError -from django.shortcuts import redirect +from django.http import Http404 +from django.shortcuts import redirect, get_object_or_404 from django.template.defaultfilters import floatformat from django.urls import reverse from django.utils import timezone from django.utils.decorators import method_decorator from django.utils.translation import ugettext_lazy as _ from django.views import View -from django.views.generic import FormView +from django.views.generic import FormView, CreateView from django.views.generic.base import TemplateResponseMixin, TemplateView +from members.decorators import membership_required from members.models import Membership from . import emails, forms, services -from .models import Entry, Registration, Renewal +from .models import Entry, Registration, Renewal, Reference class BecomeAMemberView(TemplateView): @@ -31,7 +33,6 @@ class BecomeAMemberView(TemplateView): Entry.MEMBERSHIP_YEAR], 2) context['study_fees'] = floatformat(settings.MEMBERSHIP_PRICES[ Entry.MEMBERSHIP_STUDY], 2) - context['member_form_url'] = reverse('registrations:register') return context @@ -95,36 +96,42 @@ class ConfirmEmailView(View, TemplateResponseMixin): template_name = 'registrations/confirm_email.html' def get(self, request, *args, **kwargs): - entry = Entry.objects.filter(pk=kwargs['pk']) + queryset = Registration.objects.filter(pk=kwargs['pk']) processed = 0 try: - processed = services.confirm_entry(entry) + processed = services.confirm_entry(queryset) except ValidationError: pass if processed == 0: - return redirect('registrations:register') + return redirect('registrations:register-member') - emails.send_new_registration_board_message(entry.get()) + registration = queryset.get() + + if (registration.membership_type == Membership.BENEFACTOR + and not registration.no_references): + emails.send_references_information_message(registration) + + emails.send_new_registration_board_message(registration) return self.render_to_response({}) -class MemberRegistrationFormView(FormView): +class BaseRegistrationFormView(FormView): """ - View that renders the membership registration form + View that renders a membership registration form """ form_class = forms.MemberRegistrationForm template_name = 'registrations/register_member.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) + context['google_api_key'] = settings.GOOGLE_PLACES_API_KEY context['year_fees'] = floatformat(settings.MEMBERSHIP_PRICES[ Entry.MEMBERSHIP_YEAR], 2) context['study_fees'] = floatformat(settings.MEMBERSHIP_PRICES[ Entry.MEMBERSHIP_STUDY], 2) - context['privacy_policy_url'] = reverse('privacy-policy') return context def get(self, request, *args, **kwargs): @@ -132,6 +139,18 @@ class MemberRegistrationFormView(FormView): return redirect('registrations:renew') return super().get(request, args, kwargs) + def form_valid(self, form): + form.save() + return redirect('registrations:register-success') + + +class MemberRegistrationFormView(BaseRegistrationFormView): + """ + View that renders the `member` membership registration form + """ + form_class = forms.MemberRegistrationForm + template_name = 'registrations/register_member.html' + def post(self, request, *args, **kwargs): request.POST = request.POST.dict() request.POST['language'] = request.LANGUAGE_CODE @@ -139,8 +158,27 @@ class MemberRegistrationFormView(FormView): return super().post(request, *args, **kwargs) def form_valid(self, form): - form.save() - return redirect('registrations:register-success') + response = super().form_valid(form) + emails.send_registration_email_confirmation(form.instance) + return response + + +class BenefactorRegistrationFormView(BaseRegistrationFormView): + """ + View that renders the `benefactor` membership registration form + """ + form_class = forms.BenefactorRegistrationForm + template_name = 'registrations/register_benefactor.html' + + def post(self, request, *args, **kwargs): + request.POST = request.POST.dict() + request.POST['language'] = request.LANGUAGE_CODE + request.POST['membership_type'] = Membership.BENEFACTOR + request.POST['length'] = Entry.MEMBERSHIP_YEAR + request.POST['remarks'] = ('Registered as iCIS employee' + if 'icis_employee' in request.POST else '') + request.POST['no_references'] = 'icis_employee' in request.POST + return super().post(request, *args, **kwargs) @method_decorator(login_required, name='dispatch') @@ -148,7 +186,7 @@ class RenewalFormView(FormView): """ View that renders the membership renewal form """ - form_class = forms.MemberRenewalForm + form_class = forms.RenewalForm template_name = 'registrations/renewal.html' def get_context_data(self, **kwargs): @@ -160,7 +198,7 @@ class RenewalFormView(FormView): context['latest_membership'] = self.request.member.latest_membership context['was_member'] = Membership.objects.filter( user=self.request.member, type=Membership.MEMBER).exists() - context['privacy_policy_url'] = reverse('privacy-policy') + context['benefactor_type'] = Membership.BENEFACTOR return context def get_form(self, form_class=None): @@ -189,9 +227,66 @@ class RenewalFormView(FormView): request.POST['membership_type'] = Membership.BENEFACTOR request.POST['length'] = Entry.MEMBERSHIP_YEAR request.POST['member'] = request.member.pk + request.POST['remarks'] = '' + request.POST['no_references'] = False + + if request.POST['membership_type'] == Membership.BENEFACTOR: + if Membership.objects.filter(user=request.member, + type=Membership.MEMBER).exists(): + request.POST['remarks'] = 'Was a Thalia member in the past.' + request.POST['no_references'] = True + if 'icis_employee' in request.POST: + request.POST['remarks'] = 'Registered as iCIS employee.' + request.POST['no_references'] = True + return super().post(request, *args, **kwargs) def form_valid(self, form): renewal = form.save() + if not renewal.no_references: + emails.send_references_information_message(renewal) emails.send_new_renewal_board_message(renewal) return redirect('registrations:renew-success') + + +@method_decorator(login_required, name='dispatch') +@method_decorator(membership_required, name='dispatch') +class ReferenceCreateView(CreateView): + """ + View that renders a reference creation form + """ + model = Reference + fields = '__all__' + template_name = 'registrations/reference.html' + entry = None + success = False + + def get_success_url(self): + return reverse('registrations:reference-success', + args=(self.entry.pk,)) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + context['success'] = self.success + try: + context['name'] = self.entry.registration.get_full_name() + except Registration.DoesNotExist: + context['name'] = self.entry.renewal.member.get_full_name() + + return context + + def dispatch(self, request, *args, **kwargs): + self.entry = get_object_or_404(Entry, pk=kwargs.get('pk')) + + if (self.entry.no_references or + self.entry.membership_type != Membership.BENEFACTOR): + raise Http404 + + return super().dispatch(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + request.POST = request.POST.dict() + request.POST['member'] = request.member.pk + request.POST['entry'] = kwargs['pk'] + return super().post(request, *args, **kwargs) diff --git a/website/thaliawebsite/settings/settings.py b/website/thaliawebsite/settings/settings.py index b79335320b007ef80a676f957d578ab0de57510a..92d7e94656ecb4786e3521357037bf394d93a44b 100644 --- a/website/thaliawebsite/settings/settings.py +++ b/website/thaliawebsite/settings/settings.py @@ -296,6 +296,7 @@ ACTIVEMEMBERS_NEXTCLOUD_API_SECRET = os.environ.get( # Google maps API key and secrets GOOGLE_MAPS_API_KEY = os.environ.get('GOOGLE_MAPS_API_KEY', '') GOOGLE_MAPS_API_SECRET = os.environ.get('GOOGLE_MAPS_API_SECRET', '') +GOOGLE_PLACES_API_KEY = os.environ.get('GOOGLE_PLACES_API_KEY', '') # Photos settings PHOTO_UPLOAD_SIZE = 1920, 1080 @@ -328,6 +329,10 @@ TINYMCE_DEFAULT_CONFIG = { 'remove_script_host': False, } +BOOTSTRAP4 = { + 'required_css_class': 'required-field' +} + DEFAULT_EXCEPTION_REPORTER_FILTER = ( 'utils.exception_filter.ThaliaSafeExceptionReporterFilter') diff --git a/website/thaliawebsite/static/css/main.scss b/website/thaliawebsite/static/css/main.scss index 63c3d646492f3ce08fc490765506b50c6c121a23..b5210875fe14fb16f535239d98bf505eec1de7fc 100755 --- a/website/thaliawebsite/static/css/main.scss +++ b/website/thaliawebsite/static/css/main.scss @@ -24,6 +24,7 @@ @import "../../pizzas/static/pizzas/css/style.scss"; @import "../../education/static/education/css/style.scss"; @import "../../payments/static/payments/css/style.scss"; +@import "../../registrations/static/registrations/css/style.scss"; @import "./fontawesome/fontawesome.scss"; @import "./fontawesome/solid.scss";