...
 
Commits (2)
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');
......
......@@ -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
-------
......
......@@ -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',
......
"""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
......
"""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 <a href="{}">privacy policy</a>.').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 <a href="{}">privacy policy</a>.').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']
This diff was suppressed by a .gitattributes entry.
# 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')},
},
),
]
......@@ -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')
"""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
......
#registrations-form {
.required-field {
label::after {
content: '*';
margin-left: 2px;
color: $dark-grey;
}
}
}
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());
});
});
......@@ -9,39 +9,53 @@
<div class="container">
<h1 class="text-center section-title">{% trans "Become a Member" %}</h1>
<p>{% 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!" %}</p>
<p class="text-center">{% 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!" %}</p>
<h4>{% trans "How do I become a member?" %}</h4>
<div class="row">
<div class="col-12 col-lg-6 order-0">
<h4>{% trans "I'm a Computing Science or Information Sciences student at the Radboud University" %}</h4>
<p>
{% 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 %}
</p>
<p>
{% 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 %}
</p>
</div>
<p class="text-center my-4">
<a href="{{ member_form_url }}" class="btn btn-primary btn-lg btn-block">
{% trans "Register now" %}
</a>
</p>
<div class="col-12 col-lg-6 order-1 order-lg-3">
<p class="text-center my-4">
<a href="{% url 'registrations:register-member' %}" class="btn btn-primary btn-lg btn-block">
{% trans "Become a member" %}
</a>
</p>
</div>
<h4>{% 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?" %}</h4>
<div class="col-12 col-lg-6 h-100 order-2 order-lg-2">
<h4>{% trans "I'm not a Computing Science and Information Sciences student at the Radboud University" %}</h4>
<p>
{% 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 %}
</p>
<p>
{% 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 %}
</p>
</div>
<div class="col-12 col-lg-6 order-3">
<p class="text-center my-4">
<a href="{% url 'registrations:register-benefactor' %}" class="btn btn-primary btn-lg btn-block">
{% trans "Become a benefactor" %}
</a>
</p>
</div>
</div>
<p>
<p class="text-center">
{% 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
......
{% 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 %}
{% extends "base.html" %}
{% load i18n bootstrap4 alert %}
{% block title %}{% trans "reference"|capfirst %} — {{ block.super }}{% endblock %}
{% block body %}
<section class="page-section">
<div class="container">
<h1 class="text-center section-title">{% trans "give reference" %}</h1>
<p class="text-center">
{% 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.
<br /><br />
<strong>{{ name }}</strong> wants to become a benefactor of Thalia and
needs such a reference and has asked you to give it to them.
{% endblocktrans %}
</p>
<hr/>
{% 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 %}
<form method="post" class="">
{% csrf_token %}
<input type="submit"
value="{% trans 'give reference'|capfirst %}"
class="btn btn-primary col-12"/>
</form>
{% endif %}
</div>
</section>
{% endblock %}
{% extends "base.html" %}
{% load i18n bootstrap4 alert %}
{% block title %}{% trans "reference"|capfirst %} — {{ block.super }}{% endblock %}
{% block body %}
<section class="page-section">
<div class="container">
<h1 class="text-center section-title">{% trans "give reference" %}</h1>
<p class="text-center">
{% 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.
<br /><br />
<strong>{{ name }}</strong> wants to become a benefactor of Thalia and
needs such a reference and has asked you to give it to them.
{% endblocktrans %}
</p>
<hr/>
{% 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 %}
<form method="post" class="">
{% csrf_token %}
<input type="submit"
value="{% trans 'give reference'|capfirst %}"
class="btn btn-primary col-12"/>
</form>
{% endif %}
</div>
</section>
{% endblock %}
{% extends "base.html" %}
{% load i18n static compress bootstrap4 %}
{% block title %}{% trans "registration"|capfirst %} —
{{ block.super }}{% endblock %}
{% block body %}
<section class="page-section" id="registrations-form">
<div class="container">
<h1 class="text-center section-title">{% trans "registration" %} {% trans "Benefactor" %}</h1>
<p class="text-center">
{% 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 %}
</p>
<p class="text-center">
{% 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 %}
</p>
<p class="text-center">
{% blocktrans trimmed %}
If you have any other questions about Thalia and/or your membership, feel free to email
<a href="mailto:info@thalia.nu" rel="noopener" target="_blank">info@thalia.nu</a>!
{% endblocktrans %}
</p>
<hr/>
<form method="post" enctype="multipart/form-data" class="row">
{% csrf_token %}
<fieldset class="col-12 col-lg-6">
{% 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 %}
</fieldset>
<fieldset class="col-12 col-lg-6">
{% bootstrap_field form.email %}
<div class="form-group">
<div class="form-check">
<input name="optin_mailinglist" class="form-check-input" id="id_optin_mailinglist"
type="checkbox">
<label class="form-check-label"
for="id_optin_mailinglist">{% trans "Receive emails about (amongst others) job opportunities and in-house days from partners of Thalia." %}</label>
</div>
</div>
{% bootstrap_field form.phone_number %}
{% bootstrap_field form.birthday %}
<div class="form-group">
<div class="form-check">
<input name="optin_birthday"
class="form-check-input"
id="id_optin_birthday"
type="checkbox">
<label class="form-check-label"
for="id_optin_birthday">{% trans "Display birthday in calendar" %}</label>
</div>
</div>
{% bootstrap_field form.student_number %}
{% bootstrap_field form.icis_employee %}
{% bootstrap_field form.contribution %}
{% bootstrap_field form.privacy_policy %}
</fieldset>
<input type="submit" value="{% trans 'send'|capfirst %}" class="btn btn-primary col-6 offset-3 col-lg-2 offset-lg-10"/>
</form>
</div>
</section>
{% endblock %}
......@@ -3,12 +3,22 @@
{% block title %}{% trans "registration"|capfirst %} — {{ block.super }}{% endblock %}
{% block js_body %}
{{ block.super }}
<script type="text/javascript"
src="https://maps.googleapis.com/maps/api/js?key={{ google_api_key }}&libraries=places"></script>
{% compress js %}
<script type="text/javascript" src="{% static 'registrations/js/main.js' %}"></script>
{% endcompress %}
{% endblock %}
{% block body %}
<section class="page-section">
<section class="page-section" id="registrations-form">
<div class="container">
<h1 class="text-center section-title">{% trans "registration" %}</h1>
<h1 class="text-center section-title">{% trans "registration" %} {% trans "Member" %}</h1>
<p class="text-center">
{% url 'registrations:register-benefactor' as benefactor_register %}
{% blocktrans trimmed %}
A membership costs € {{ year_fees }} per year, or € {{ study_fees }} for your entire study duration.
<br/>
......@@ -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.<br/>
<em>Note that this form is only for member registration. Please visit the board room if you want to
become a benefactor.</em>
<em>
Note that this form is only for member registration.
Please use the <a href="{{ benefactor_register }}">benefactor registration page</a>
if you want to become a benefactor.
</em>
{% endblocktrans %}
</p>
......@@ -30,29 +43,32 @@
{% endblocktrans %}
</p>
<p class="text-center">
{% blocktrans trimmed %}
If you have any other questions about Thalia and/or your membership, feel free to email
<a href="mailto:info@thalia.nu" rel="noopener" target="_blank">info@thalia.nu</a>!
{% endblocktrans %}
</p>
<hr/>
<form method="post" enctype="multipart/form-data" class="col-lg-6 offset-lg-3">
<form method="post" enctype="multipart/form-data" class="row">
{% csrf_token %}
<fieldset>
<fieldset class="col-12 col-lg-6">
{% bootstrap_field form.length %}
</fieldset>
<fieldset>
{% bootstrap_field form.first_name %}
{% bootstrap_field form.last_name %}
{% bootstrap_field form.birthday %}
<div class="form-group">
<div class="form-check">
<input name="optin_birthday" class="form-check-input" id="id_optin_birthday"
type="checkbox">
<label class="form-check-label"
for="id_optin_birthday">{% trans "Display birthday in calendar" %}</label>
</div>
</div>
{% 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 %}
</fieldset>
<fieldset class="col-12 col-lg-6">
{% bootstrap_field form.email %}
<div class="form-group">
......@@ -65,36 +81,26 @@
</div>
{% bootstrap_field form.phone_number %}
</fieldset>
<fieldset>
{% 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 %}
</fieldset>
<fieldset>
{% bootstrap_field form.student_number %}
{% bootstrap_field form.programme %}
{% bootstrap_field form.starting_year %}
</fieldset>
{% bootstrap_field form.birthday %}
<fieldset>
<div class="form-group">
<div class="form-check">
<input name="privacy_policy" class="form-check-input" id="id_privacy_policy"
<input name="optin_birthday" class="form-check-input" id="id_optin_birthday"
type="checkbox">
<label class="form-check-label"
for="id_privacy_policy">{% blocktrans trimmed %}I accept the
<a target="_blank" href="{{ privacy_policy_url }}">privacy
policy</a>{% endblocktrans %}.</label>
for="id_optin_birthday">{% trans "Display birthday in calendar" %}</label>
</div>
</div>
{% bootstrap_field form.student_number %}
{% bootstrap_field form.programme %}
{% bootstrap_field form.starting_year %}
{% bootstrap_field form.privacy_policy %}
</fieldset>
<input type="submit" value="{% trans 'send'|capfirst %}" class="btn btn-primary float-right"/>
<input type="submit" value="{% trans 'send'|capfirst %}" class="btn btn-primary col-6 offset-3 col-lg-2 offset-lg-10"/>
</form>
</div>
</section>
......
{% 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 %}
<script type="text/javascript" src="{% static 'registrations/js/main.js' %}"></script>
{% endcompress %}
{% endblock %}
{% block body %}
<section class="page-section">
......@@ -109,13 +115,6 @@
have to renew your membership.
{% endblocktrans %}
</p>
{% elif latest_membership.type == 'benefactor' and not was_member %}
<p class="text-center">
{% blocktrans trimmed %}
You're a benefactor. Contact the board to renew your
membership.
{% endblocktrans %}
</p>
{% elif latest_membership.until is None %}
<p class="col-12 col-md-6 offset-md-3 text-center">
{% blocktrans trimmed %}
......@@ -132,44 +131,41 @@
{% endfor %}
{% endfor %}
<form method="post" enctype="multipart/form-data"
class="col-lg-6 offset-lg-3">
class="col-lg-6 offset-lg-3" data-benefactor-type="{{ benefactor_type }}">
{% csrf_token %}
<fieldset>
{% 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 %}
<div class="form-group">
<label
for="id_membership_type">{% trans 'membership type'|capfirst %}</label>
<input readonly disabled
value="{% trans 'Benefactor' %}"
type="text">
id="id_membership_type"
type="text" class="form-control" />
</div>
<div class="form-group">
<label
for="id_membership_length">{% trans 'membership length'|capfirst %}</label>
<input readonly disabled
value="{% trans 'One year' %}" type="text">
value="{% trans 'One year' %}"
type="text"
id="id_membership_length"
class="form-control" />
</div>
{% bootstrap_field form.contribution %}
{% endif %}
</fieldset>
<fieldset>
<div class="form-group">
<div class="form-check">
<input name="privacy_policy"
class="form-check-input"
id="id_privacy_policy"
type="checkbox">
<label class="form-check-label"
for="id_privacy_policy">
{% blocktrans trimmed %}I accept the
<a target="_blank"
href="{{ privacy_policy_url }}">privacy
policy</a>{% endblocktrans %}
.</label>
</div>
</div>
{% bootstrap_field form.privacy_policy %}
{% if latest_membership.type == 'benefactor' %}
{% bootstrap_field form.icis_employee %}
{% endif %}
</fieldset>
<input type="submit" value="{% trans 'send'|capfirst %}"
......
......@@ -211,8 +211,7 @@ class RegistrationAdminTest(TestCase):
fields = self.admin.get_readonly_fields(request, Registration(
status=Entry.STATUS_CONFIRM
))
self.assertEqual(fields, ['status', 'created_at',
'updated_at'])
self.assertEqual(fields, ['status', 'created_at', 'updated_at'])
fields = self.admin.get_readonly_fields(request, Registration(
status=Entry.STATUS_REJECTED
......@@ -227,7 +226,8 @@ class RegistrationAdminTest(TestCase):
'address_street', 'address_street2',
'address_postal_code', 'address_city',
'address_country', 'membership',
'optin_mailinglist', 'optin_birthday'])
'optin_mailinglist', 'optin_birthday',
'contribution'])
fields = self.admin.get_readonly_fields(request, Registration(
status=Entry.STATUS_ACCEPTED
......@@ -242,7 +242,8 @@ class RegistrationAdminTest(TestCase):
'address_street', 'address_street2',
'address_postal_code', 'address_city',
'address_country', 'membership',
'optin_mailinglist', 'optin_birthday'])
'optin_mailinglist', 'optin_birthday',
'contribution'])
fields = self.admin.get_readonly_fields(request, Registration(
status=Entry.STATUS_COMPLETED
......@@ -257,7 +258,8 @@ class RegistrationAdminTest(TestCase):
'address_street', 'address_street2',
'address_postal_code', 'address_city',
'address_country', 'membership',
'optin_mailinglist', 'optin_birthday'])
'optin_mailinglist', 'optin_birthday',
'contribution'])
def test_get_actions(self):
actions = self.admin.get_actions(_get_mock_request(
......@@ -419,7 +421,7 @@ class RenewalAdminTest(TestCase):
self.assertCountEqual(fields, ['created_at', 'updated_at', 'status',
'length', 'membership_type', 'remarks',
'entry_ptr', 'member',
'membership'])
'membership', 'contribution'])
fields = self.admin.get_readonly_fields(request, Renewal(
status=Entry.STATUS_ACCEPTED
......@@ -427,7 +429,7 @@ class RenewalAdminTest(TestCase):
self.assertCountEqual(fields, ['created_at', 'updated_at', 'status',
'length', 'membership_type', 'remarks',
'entry_ptr', 'member',
'membership'])
'membership', 'contribution'])
def test_get_actions(self):
actions = self.admin.get_actions(_get_mock_request(
......
......@@ -19,6 +19,9 @@ from registrations.models import Registration, Renewal
class EmailsTest(TestCase):
def setUp(self):
translation.activate('en')
@mock.patch('registrations.emails._send_email')
def test_send_registration_email_confirmation(self, send_email):
reg = Registration(
......@@ -97,34 +100,30 @@ class EmailsTest(TestCase):
@mock.patch('registrations.emails._send_email')
def test_send_new_registration_board_message(self, send_email):
entry = Registration(
registration = Registration(
language='en',
email='test@example.org',
first_name='John',
last_name='Doe',
pk=0,
)
entry.registration = entry
emails.send_new_registration_board_message(entry)
emails.send_new_registration_board_message(registration)
send_email.assert_called_once_with(
settings.BOARD_NOTIFICATION_ADDRESS,
'New registration',
'registrations/email/registration_board.txt',
{
'name': entry.registration.get_full_name(),
'name': registration.get_full_name(),
'url': (
'https://thalia.localhost'
+ reverse('admin:registrations_registration_change',
args=[entry.registration.pk])
args=[registration.pk])
)
}
)
entry.registration = None
emails.send_new_registration_board_message(entry)
@mock.patch('registrations.emails._send_email')
def test_send_renewal_accepted_message(self, send_email):
member = Member(
......@@ -246,6 +245,65 @@ class EmailsTest(TestCase):
}
)
@mock.patch('registrations.emails._send_email')
def test_send_references_information_message(self, send_email):
with self.subTest('Registrations'):
registration = Registration(
language='en',
email='test@example.org',
first_name='John',
last_name='Doe',
pk=uuid.uuid4(),
)
emails.send_references_information_message(registration)
send_email.assert_called_once_with(
'test@example.org',
'Information about references',
'registrations/email/references_information.txt',
{
'name': registration.get_full_name(),
'reference_link': (
'https://thalia.localhost' +
reverse('registrations:reference',
args=[registration.pk])
)
}
)
send_email.reset_mock()
with self.subTest('Renewals'):
member = Member(
email="test@example.org",
first_name='John',
last_name='Doe',
profile=Profile(
language='en'
)
)
renewal = Renewal(
pk=uuid.uuid4(),
member=member
)
emails.send_references_information_message(renewal)
send_email.assert_called_once_with(
'test@example.org',
'Information about references',
'registrations/email/references_information.txt',
{
'name': renewal.member.get_full_name(),
'reference_link': (
'https://thalia.localhost' +
reverse('registrations:reference', args=[renewal.pk])
)
}
)
def test_send_email(self):
_send_email(
subject='Subject',
......
......@@ -43,7 +43,45 @@ class MemberRegistrationFormTest(TestCase):
self.assertTrue(form.fields['privacy_policy'] is not None)
class MemberRenewalFormTest(TestCase):
class BenefactorRegistrationFormTest(TestCase):
def setUp(self):
self.data = {
'first_name': 'John',
'last_name': 'Doe',
'email': 'johndoe@example.com',
'programme': 'computingscience',
'student_number': 's1234567',
'starting_year': 2014,
'address_street': 'Heyendaalseweg 135',
'address_street2': '',
'address_postal_code': '6525AJ',
'address_city': 'Nijmegen',
'address_country': 'NL',
'phone_number': '06123456789',
'birthday': timezone.now().replace(year=1990, day=1),
'language': 'en',
'length': Entry.MEMBERSHIP_YEAR,
'membership_type': Membership.BENEFACTOR,
'privacy_policy': 1,
'icis_employee': 1,
}
def test_privacy_policy_checked(self):
with self.subTest("Form is valid"):
form = forms.BenefactorRegistrationForm(self.data)
self.assertTrue(form.is_valid(), msg=dict(form.errors))
with self.subTest("Form is not valid"):
self.data['privacy_policy'] = 0
form = forms.BenefactorRegistrationForm(self.data)
self.assertFalse(form.is_valid(), msg=dict(form.errors))
def test_has_privacy_policy_field(self):
form = forms.BenefactorRegistrationForm(self.data)
self.assertTrue(form.fields['privacy_policy'] is not None)
class RenewalFormTest(TestCase):
fixtures = ['members.json']
def setUp(self):
......@@ -58,13 +96,13 @@ class MemberRenewalFormTest(TestCase):
def test_is_valid(self):
self.member.membership_set.all().delete()
with self.subTest("Form is valid"):
form = forms.MemberRenewalForm(self.data)
form = forms.RenewalForm(self.data)
self.assertTrue(form.is_valid(), msg=dict(form.errors))
with self.subTest("Form is not valid"):
self.data['privacy_policy'] = 0
form = forms.MemberRenewalForm(self.data)
form = forms.RenewalForm(self.data)
self.assertFalse(form.is_valid(), msg=dict(form.errors))
def test_has_privacy_policy_field(self):
form = forms.MemberRenewalForm(self.data)
form = forms.RenewalForm(self.data)
self.assertTrue(form.fields['privacy_policy'] is not None)
from django.contrib.auth import get_user_model
from django.core import mail
from django.core.exceptions import ValidationError
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone, translation
from django.utils.translation import ugettext_lazy as _
from freezegun import freeze_time
from members.models import Member, Membership, Profile
from registrations.models import Entry, Registration, Renewal
from registrations.models import Entry, Registration, Renewal, Reference
class EntryTest(TestCase):
......@@ -51,6 +50,71 @@ class EntryTest(TestCase):
self.member.first_name, self.member.last_name,
self.member.email))
@freeze_time('2019-01-01')
def test_save(self):
entry = Entry(registration=self.registration)
entry.status = Entry.STATUS_ACCEPTED
test_value = timezone.now().replace(year=1996)
entry.updated_at = test_value
with self.subTest('Accepted should not update `updated_at`'):
entry.save()
self.assertEqual(entry.updated_at, test_value)
entry.status = Entry.STATUS_REJECTED
with self.subTest('Rejected should not update `updated_at`'):
entry.save()
self.assertEqual(entry.updated_at, test_value)
entry.status = Entry.STATUS_REVIEW
with self.subTest('Review should update `updated_at`'):
entry.save()
self.assertNotEqual(entry.updated_at, test_value)
entry.length = Entry.MEMBERSHIP_STUDY
with self.subTest('Type `Member` should not change length'):
entry.save()
self.assertEqual(entry.length, Entry.MEMBERSHIP_STUDY)
entry.membership_type = Membership.BENEFACTOR
with self.subTest('Type `Benefactor` should set length to year'):
entry.save()
self.assertEqual(entry.length, Entry.MEMBERSHIP_YEAR)
entry.contribution = 9
with self.subTest('Type `Benefactor` keeps contribution value'):
entry.save()
self.assertEqual(entry.contribution, 9)
entry.membership_type = Membership.MEMBER
with self.subTest('Type `Member` should clear contribution'):
entry.save()
self.assertEqual(entry.contribution, None)
def test_clean(self):
entry = Entry(registration=self.registration)
entry.membership_type = Membership.MEMBER
entry.contribution = None
with self.subTest('Type `Member` should not require contribution'):
entry.clean()
entry.membership_type = Membership.BENEFACTOR
with self.subTest('Type `Benefactor` should require contribution'):
with self.assertRaises(ValidationError):
entry.clean()
entry.contribution = 7.5
entry.clean()