diff --git a/jdav_web/mailer/models.py b/jdav_web/mailer/models.py index 6965c7c..0f8324a 100644 --- a/jdav_web/mailer/models.py +++ b/jdav_web/mailer/models.py @@ -140,11 +140,11 @@ class Message(CommonModel): recipients_without_reminder = [m for m in filtered if m.registered] emails_rem = [member.email for member in recipients_with_reminder] - emails_rem.extend([member.email_parents for member in recipients_with_reminder - if member.email_parents and member.cc_email_parents]) + emails_rem.extend([member.alternative_email for member in recipients_with_reminder + if member.alternative_email]) emails_no_rem = [member.email for member in recipients_without_reminder] - emails_no_rem.extend([member.email_parents for member in recipients_without_reminder - if member.email_parents and member.cc_email_parents]) + emails_no_rem.extend([member.alternative_email for member in recipients_without_reminder + if member.alternative_email]) # remove any underscores from subject to prevent Arne from using # terrible looking underscores in subjects self.subject = self.subject.replace('_', ' ') diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py index d3b41d0..fb2f534 100644 --- a/jdav_web/members/admin.py +++ b/jdav_web/members/admin.py @@ -35,7 +35,7 @@ import nested_admin from .models import (Member, Group, Freizeit, MemberNoteList, NewMemberOnList, Klettertreff, MemberWaitingList, LJPProposal, Intervention, PermissionMember, PermissionGroup, MemberTraining, TrainingCategory, - KlettertreffAttendee, ActivityCategory, + KlettertreffAttendee, ActivityCategory, EmergencyContact, annotate_activity_score, RegistrationPassword, MemberUnconfirmedProxy) from finance.models import Statement, BillOnExcursionProxy from mailer.mailutils import send as send_mail, get_echo_link @@ -102,6 +102,15 @@ class TrainingOnMemberInline(CommonAdminInlineMixin, admin.TabularInline): extra = 0 +class EmergencyContactInline(CommonAdminInlineMixin, admin.TabularInline): + model = EmergencyContact + formfield_overrides = { + TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})} + } + fields = ['prename', 'lastname', 'email', 'phone_number'] + extra = 0 + + class TrainingCategoryAdmin(admin.ModelAdmin): list_display = ('name', 'permission_needed') ordering = ('name', ) @@ -109,7 +118,7 @@ class TrainingCategoryAdmin(admin.ModelAdmin): class RegistrationFilter(admin.SimpleListFilter): title = _('Registration complete') - parameter_name = 'registered' + parameter_name = 'registration_complete' default_value = ('All', None) def lookups(self, request, model_admin): @@ -121,14 +130,14 @@ class RegistrationFilter(admin.SimpleListFilter): def queryset(self, request, queryset): if self.value() == 'True': - return queryset.filter(registered=True) + return queryset.filter(registration_complete=True) elif self.value() == 'False': - return queryset.filter(registered=False) + return queryset.filter(registration_complete=False) elif self.value() is None: if self.default_value[1] is None: return queryset else: - return queryset.filter(registered=self.default_value[1]) + return queryset.filter(registration_complete=self.default_value[1]) elif self.value() == 'All': return queryset @@ -149,27 +158,25 @@ class RegistrationFilter(admin.SimpleListFilter): # Register your models here. class MemberAdmin(CommonAdminMixin, admin.ModelAdmin): fields = ['prename', 'lastname', 'email', - ('email_parents', 'cc_email_parents'), - 'street', 'plz', 'town', 'address_extra', 'country', 'nationality', - 'phone_number_private', 'phone_number_mobile', - 'phone_number_parents', 'birth_date', 'gender', 'civil_status', + 'alternative_email', + 'street', 'plz', 'town', 'address_extra', 'country', + 'phone_number', 'birth_date', 'gender', 'dav_badge_no', 'group', - 'swimming_badge', 'climbing_badge', 'rock_experience', 'allergies', + 'swimming_badge', 'climbing_badge', 'alpine_experience', 'allergies', 'medication', 'tetanus_vaccination', 'photos_may_be_taken', 'legal_guardians', ('good_conduct_certificate_presented_date', 'good_conduct_certificate_presentation_needed'), - 'iban', 'has_key', 'has_free_ticket_gym', 'gets_newsletter', 'registered', 'registration_form', - 'image', + 'iban', 'has_key', 'has_free_ticket_gym', 'gets_newsletter', + 'registration_form', 'image', 'active', 'echoed', ('join_date', 'leave_date'), - 'comments', 'technical_comments', - 'user'] + 'comments', 'user'] list_display = ('name_text_or_link', 'birth_date', 'age', 'get_group', 'gets_newsletter', - 'registered', 'active', 'echoed', 'comments', 'activity_score') + 'registration_complete', 'active', 'echoed', 'comments', 'activity_score') search_fields = ('prename', 'lastname', 'email') list_filter = ('group', 'gets_newsletter', RegistrationFilter, 'active') list_display_links = None - inlines = [TrainingOnMemberInline, PermissionOnMemberInline] + inlines = [EmergencyContactInline, TrainingOnMemberInline, PermissionOnMemberInline] #formfield_overrides = { # ManyToManyField: {'widget': forms.CheckboxSelectMultiple}, # ForeignKey: {'widget': apply_select2(forms.Select)} @@ -213,11 +220,8 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin): for member in queryset: if not member.gets_newsletter: continue - send_mail(_("Echo required"), - settings.ECHO_TEXT.format(name=member.prename, link=get_echo_link(member)), - settings.DEFAULT_SENDING_MAIL, - [member.email, member.email_parents] if member.email_parents and member.cc_email_parents - else member.email) + member.send_mail(_("Echo required"), + settings.ECHO_TEXT.format(name=member.prename, link=get_echo_link(member))) messages.success(request, _("Successfully requested echo from selected members.")) request_echo.short_description = _('Request echo from selected members') @@ -251,12 +255,12 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin): class MemberUnconfirmedAdmin(admin.ModelAdmin): - fields = ['prename', 'lastname', 'email', 'email_parents', 'cc_email_parents', 'street', 'plz', - 'town', 'phone_number_mobile', 'phone_number_private','phone_number_parents', 'birth_date', 'group', - 'registered', 'registration_form', 'active', 'comments'] - list_display = ('name', 'birth_date', 'age', 'get_group', 'confirmed_mail', 'confirmed_mail_parents') + fields = ['prename', 'lastname', 'email', 'alternative_email', 'street', 'plz', + 'town', 'phone_number', 'birth_date', 'gender', 'group', + 'registration_form', 'comments'] + list_display = ('name', 'birth_date', 'age', 'get_group', 'confirmed_mail', 'confirmed_alternative_mail') search_fields = ('prename', 'lastname', 'email') - list_filter = ('group', 'confirmed_mail', 'confirmed_mail_parents') + list_filter = ('group', 'confirmed_mail', 'confirmed_alternative_mail') actions = ['request_mail_confirmation', 'confirm', 'demote_to_waiter'] change_form_template = "members/change_member_unconfirmed.html" @@ -306,14 +310,10 @@ class MemberUnconfirmedAdmin(admin.ModelAdmin): waiter = MemberWaitingList(prename=member.prename, lastname=member.lastname, email=member.email, - email_parents=member.email_parents, - cc_email_parents=member.cc_email_parents, birth_date=member.birth_date, comments=member.comments, confirmed_mail=member.confirmed_mail, - confirmed_mail_parents=member.confirmed_mail_parents, - confirm_mail_key=member.confirm_mail_key, - confirm_mail_parents_key=member.confirm_mail_parents_key) + confirm_mail_key=member.confirm_mail_key) waiter.save() member.delete() messages.success(request, _("Successfully demoted %(name)s to waiter.") % {'name': waiter.name}) @@ -336,13 +336,14 @@ class WaiterInviteForm(forms.Form): class MemberWaitingListAdmin(CommonAdminMixin, admin.ModelAdmin): - fields = ['prename', 'lastname', 'email', 'email_parents', 'birth_date', 'application_text', 'application_date', 'comments', 'invited_for_group'] - list_display = ('name', 'birth_date', 'age', 'application_date', 'confirmed_mail', 'confirmed_mail_parents', + fields = ['prename', 'lastname', 'email', 'birth_date', 'gender', 'application_text', + 'application_date', 'comments', 'invited_for_group'] + list_display = ('name', 'birth_date', 'age', 'application_date', 'confirmed_mail', 'waiting_confirmed') search_fields = ('prename', 'lastname', 'email') - list_filter = ('confirmed_mail', 'confirmed_mail_parents') + list_filter = ('confirmed_mail',) actions = ['ask_for_registration', 'ask_for_wait_confirmation'] - readonly_fields= ('invited_for_group',) + readonly_fields= ('invited_for_group', 'application_date') def has_add_permission(self, request, obj=None): return False diff --git a/jdav_web/members/migrations/0014_remove_fields_alternative_email.py b/jdav_web/members/migrations/0014_remove_fields_alternative_email.py new file mode 100644 index 0000000..ae1be0b --- /dev/null +++ b/jdav_web/members/migrations/0014_remove_fields_alternative_email.py @@ -0,0 +1,156 @@ +# Generated by Django 4.0.1 on 2024-10-13 19:02 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import rules.contrib.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('members', '0013_memberwaitinglist_add_application_text_and_date'), + ] + + operations = [ + migrations.RemoveField( + model_name='member', + name='civil_status', + ), + migrations.RemoveField( + model_name='member', + name='nationality', + ), + migrations.RemoveField( + model_name='member', + name='registered', + ), + migrations.RemoveField( + model_name='member', + name='rock_experience', + ), + migrations.RemoveField( + model_name='member', + name='technical_comments', + ), + migrations.AddField( + model_name='member', + name='alpine_experience', + field=models.TextField(blank=True, default='', verbose_name='Alpine experience'), + ), + migrations.RemoveField( + model_name='member', + name='gender', + ), + migrations.AddField( + model_name='member', + name='gender', + field=models.IntegerField(choices=[(0, 'Männlich'), (1, 'Weiblich'), (2, 'Divers')], default=2, verbose_name='Gender'), + ), + migrations.RemoveField( + model_name='member', + name='swimming_badge', + ), + migrations.AddField( + model_name='member', + name='swimming_badge', + field=models.BooleanField(default=False, verbose_name='Knows how to swim'), + ), + migrations.RenameField( + model_name='member', + old_name='confirm_mail_parents_key', + new_name='confirm_alternative_mail_key', + ), + migrations.RemoveField( + model_name='member', + name='cc_email_parents', + ), + migrations.RemoveField( + model_name='member', + name='confirmed_mail_parents', + ), + migrations.RemoveField( + model_name='member', + name='email_parents', + ), + migrations.RemoveField( + model_name='member', + name='phone_number_mobile', + ), + migrations.RemoveField( + model_name='member', + name='phone_number_parents', + ), + migrations.RemoveField( + model_name='member', + name='phone_number_private', + ), + migrations.RemoveField( + model_name='memberwaitinglist', + name='cc_email_parents', + ), + migrations.RemoveField( + model_name='memberwaitinglist', + name='confirm_mail_parents_key', + ), + migrations.RemoveField( + model_name='memberwaitinglist', + name='confirmed_mail_parents', + ), + migrations.RemoveField( + model_name='memberwaitinglist', + name='email_parents', + ), + migrations.AddField( + model_name='member', + name='alternative_email', + field=models.EmailField(blank=True, default=None, max_length=100), + ), + migrations.AddField( + model_name='member', + name='confirmed_alternative_mail', + field=models.BooleanField(default=True, verbose_name='Alternative email confirmed'), + ), + migrations.AddField( + model_name='member', + name='phone_number', + field=models.CharField(default='', max_length=100, verbose_name='phone number'), + preserve_default=False, + ), + migrations.CreateModel( + name='EmergencyContact', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='members.member', verbose_name='Member')), + ('confirm_mail_key', models.CharField(default='', max_length=32)), + ('confirmed_mail', models.BooleanField(default=True, verbose_name='Email confirmed')), + ('email', models.EmailField(default='', max_length=100)), + ('lastname', models.CharField(default='', max_length=20, verbose_name='last name')), + ('phone_number', models.CharField(default='', max_length=100, verbose_name='phone number')), + ('prename', models.CharField(default='', max_length=20, verbose_name='prename')), + ], + options={ + 'verbose_name': 'Emergency contact', + 'verbose_name_plural': 'Emergency contacts', + 'abstract': False, + 'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'), + }, + bases=(models.Model, rules.contrib.models.RulesModelMixin), + ), + migrations.AlterField( + model_name='memberwaitinglist', + name='application_text', + field=models.TextField(blank=True, default='', verbose_name='Do you want to tell us something else?'), + ), + migrations.AddField( + model_name='memberwaitinglist', + name='gender', + field=models.IntegerField(choices=[(0, 'Männlich'), (1, 'Weiblich'), (2, 'Divers')], default=2, verbose_name='Gender'), + ), + migrations.AlterField( + model_name='memberwaitinglist', + name='application_date', + field=models.DateTimeField(auto_now=True, default=django.utils.timezone.now, verbose_name='application date'), + preserve_default=False, + ), + ] diff --git a/jdav_web/members/migrations/0015_alter_emergencycontact_lastname_and_more.py b/jdav_web/members/migrations/0015_alter_emergencycontact_lastname_and_more.py new file mode 100644 index 0000000..02be80b --- /dev/null +++ b/jdav_web/members/migrations/0015_alter_emergencycontact_lastname_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.0.1 on 2024-10-13 19:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('members', '0014_remove_fields_alternative_email'), + ] + + operations = [ + migrations.AlterField( + model_name='emergencycontact', + name='lastname', + field=models.CharField(max_length=20, verbose_name='last name'), + ), + migrations.AlterField( + model_name='emergencycontact', + name='phone_number', + field=models.CharField(max_length=100, verbose_name='phone number'), + ), + migrations.AlterField( + model_name='emergencycontact', + name='prename', + field=models.CharField(max_length=20, verbose_name='prename'), + ), + ] diff --git a/jdav_web/members/models.py b/jdav_web/members/models.py index eed858b..c81af89 100644 --- a/jdav_web/members/models.py +++ b/jdav_web/members/models.py @@ -30,9 +30,9 @@ def generate_random_key(): return uuid.uuid4().hex -GEMEINSCHAFTS_TOUR = MUSKELKRAFT_ANREISE = 0 -FUEHRUNGS_TOUR = OEFFENTLICHE_ANREISE = 1 -AUSBILDUNGS_TOUR = FAHRGEMEINSCHAFT_ANREISE = 2 +GEMEINSCHAFTS_TOUR = MUSKELKRAFT_ANREISE = MALE = 0 +FUEHRUNGS_TOUR = OEFFENTLICHE_ANREISE = FEMALE = 1 +AUSBILDUNGS_TOUR = FAHRGEMEINSCHAFT_ANREISE = DIVERSE = 2 class ActivityCategory(models.Model): @@ -77,25 +77,16 @@ class MemberManager(models.Manager): return super().get_queryset().filter(confirmed=True) -class Person(CommonModel): +class Contact(CommonModel): """ - Represents an abstract person. Not necessarily a member of any group. + Represents an abstract person with only absolutely necessary contact information. """ prename = models.CharField(max_length=20, verbose_name=_('prename')) lastname = models.CharField(max_length=20, verbose_name=_('last name')) - email = models.EmailField(max_length=100, default="") - email_parents = models.EmailField(max_length=100, default="", blank=True, - verbose_name=_("Parents' Email")) - cc_email_parents = models.BooleanField(default=True, verbose_name=_('Also send mails to parents')) - - birth_date = models.DateField(_('birth date'), null=True, blank=True) # to determine the age - - comments = models.TextField(_('comments'), default='', blank=True) + email = models.EmailField(max_length=100, default="") confirmed_mail = models.BooleanField(default=True, verbose_name=_('Email confirmed')) - confirmed_mail_parents = models.BooleanField(default=True, verbose_name=_('Parents email confirmed')) confirm_mail_key = models.CharField(max_length=32, default="") - confirm_mail_parents_key = models.CharField(max_length=32, default="") class Meta(CommonModel.Meta): abstract = True @@ -109,6 +100,85 @@ class Person(CommonModel): """Returning whole name (prename + lastname)""" return "{0} {1}".format(self.prename, self.lastname) + @property + def email_fields(self): + """Returns all tuples of emails and confirmation data related to this contact. + By default, this is only the principal email field, but extending classes can add + more email fields and then override this method.""" + return [('email', 'confirmed_mail', 'confirm_mail_key')] + + def request_mail_confirmation(self, rerequest=True): + """Request mail confirmation for every mail field. If `rerequest` is false, then only + confirmation is requested for currently unconfirmed emails. + + Returns true if any mail confirmation was requested, false otherwise.""" + requested_confirmation = False + for email_fd, confirmed_email_fd, confirm_mail_key_fd in self.email_fields: + if getattr(self, confirmed_email_fd) and not rerequest: + continue + requested_confirmation = True + setattr(self, confirmed_email_fd, False) + confirm_mail_key = uuid.uuid4().hex + setattr(self, confirm_mail_key_fd, confirm_mail_key) + send_mail(_('Email confirmation needed'), + settings.CONFIRM_MAIL_TEXT.format(name=self.prename, + link=get_mail_confirmation_link(confirm_mail_key), + whattoconfirm='deiner Emailadresse'), + settings.DEFAULT_SENDING_MAIL, + getattr(self, email_fd)) + self.save() + return requested_confirmation + + def confirm_mail(self, key): + for email_fd, confirmed_email_fd, confirm_mail_key_fd in self.email_fields: + if getattr(self, confirm_mail_key_fd) == key: + setattr(self, confirmed_email_fd, True) + setattr(self, confirm_mail_key_fd, "") + self.save() + return getattr(self, email_fd) + return None + + def send_mail(self, subject, content): + send_mail(subject, content, settings.DEFAULT_SENDING_MAIL, + [getattr(self, email_fd) for email_fd, _, _ in self.email_fields]) + + +def confirm_mail_by_key(key): + matching_unconfirmed = MemberUnconfirmedProxy.objects.filter(confirm_mail_key=key) \ + | MemberUnconfirmedProxy.objects.filter(confirm_alternative_mail_key=key) + matching_waiter = MemberWaitingList.objects.filter(confirm_mail_key=key) + if len(matching_unconfirmed) + len(matching_waiter) != 1: + return None + person = matching_unconfirmed[0] if len(matching_unconfirmed) == 1 else matching_waiter[0] + return person, person.confirm_mail(key) + + +class ContactWithPhoneNumber(Contact): + """ + A contact with a phone number. + """ + phone_number = models.CharField(max_length=100, verbose_name=_('phone number')) + + class Meta(CommonModel.Meta): + abstract = True + + +class Person(Contact): + """ + Represents an abstract person. Not necessarily a member of any group. + """ + birth_date = models.DateField(_('birth date'), null=True, blank=True) # to determine the age + gender_choices = ((MALE, 'Männlich'), + (FEMALE, 'Weiblich'), + (DIVERSE, 'Divers')) + gender = models.IntegerField(choices=gender_choices, + default=DIVERSE, + verbose_name=_('Gender')) + comments = models.TextField(_('comments'), default='', blank=True) + + class Meta(CommonModel.Meta): + abstract = True + @property def age(self): """Age of member""" @@ -120,53 +190,18 @@ class Person(CommonModel): return "---" return self.birth_date.strftime("%d.%m.%Y") - def request_mail_confirmation(self): - self.confirmed_mail = False - self.confirm_mail_key = uuid.uuid4().hex - send_mail(_('Email confirmation needed'), - settings.CONFIRM_MAIL_TEXT.format(name=self.prename, - link=get_mail_confirmation_link(self.confirm_mail_key), - whattoconfirm='deiner Emailadresse'), - settings.DEFAULT_SENDING_MAIL, - self.email) - if self.email_parents: - self.confirmed_mail_parents = False - self.confirm_mail_parents_key = uuid.uuid4().hex - send_mail(_('Email confirmation needed'), - settings.CONFIRM_MAIL_TEXT.format(name=self.prename, - link=get_mail_confirmation_link(self.confirm_mail_parents_key), - whattoconfirm='der Emailadresse deiner Eltern'), - settings.DEFAULT_SENDING_MAIL, - self.email_parents) - else: - self.confirmed_mail_parents = True - self.save() - - def confirm_mail(self, key): - parents = False - email = None - if self.confirm_mail_key == key: - self.confirm_mail_key, self.confirmed_mail = "", True - email, parents = self.email, False - elif self.confirm_mail_parents_key == key: - self.confirm_mail_parents_key, self.confirmed_mail_parents = "", True - email, parents = self.email_parents, True - self.save() - return (email, parents) - - def send_mail(self, subject, content): - send_mail(subject, - content, - settings.DEFAULT_SENDING_MAIL, - [self.email, self.email_parents] if self.email_parents and self.cc_email_parents - else self.email) - class Member(Person): """ Represents a member of the association Might be a member of different groups: e.g. J1, J2, Jugendleiter, etc. """ + alternative_email = models.EmailField(max_length=100, default=None, blank=True) + confirmed_alternative_mail = models.BooleanField(default=True, + verbose_name=_('Alternative email confirmed')) + confirm_alternative_mail_key = models.CharField(max_length=32, default="") + + phone_number = models.CharField(max_length=100, verbose_name=_('phone number')) street = models.CharField(max_length=30, verbose_name=_('street and house number'), default='', blank=True) plz = models.CharField(max_length=10, verbose_name=_('Postcode'), default='', blank=True) @@ -176,39 +211,30 @@ class Member(Person): good_conduct_certificate_presentation_needed = models.BooleanField(_('Good conduct certificate presentation needed'), default=False) good_conduct_certificate_presented_date = models.DateField(_('Good conduct certificate presented on'), default=None, blank=True, null=True) - gender = models.CharField(max_length=30, verbose_name=_('Gender'), default='', blank=True) - nationality = models.CharField(max_length=30, verbose_name=_('Nationality'), default='', blank=True) join_date = models.DateField(_('Joined on'), default=None, blank=True, null=True) leave_date = models.DateField(_('Left on'), default=None, blank=True, null=True) - civil_status = models.CharField(_('Civil status'), max_length=30, default='', blank=True) has_key = models.BooleanField(_('Has key'), default=False) has_free_ticket_gym = models.BooleanField(_('Has a free ticket for the climbing gym'), default=False) dav_badge_no = models.CharField(max_length=20, verbose_name=_('DAV badge number'), default='', blank=True) - swimming_badge = models.CharField(max_length=20, verbose_name=_('Swimming badge'), default='', blank=True) + swimming_badge = models.BooleanField(verbose_name=_('Knows how to swim'), default=False) climbing_badge = models.CharField(max_length=100, verbose_name=_('Climbing badge'), default='', blank=True) - rock_experience = models.CharField(max_length=50, verbose_name=_('Rock experience'), default='', blank=True) + alpine_experience = models.TextField(verbose_name=_('Alpine experience'), default='', blank=True) allergies = models.CharField(max_length=100, verbose_name=_('Allergies'), default='', blank=True) medication = models.CharField(max_length=100, verbose_name=_('Medication'), default='', blank=True) tetanus_vaccination = models.CharField(max_length=50, verbose_name=_('Tetanus vaccination'), default='', blank=True) photos_may_be_taken = models.BooleanField(verbose_name=_('Photos may be taken'), default=False) legal_guardians = models.CharField(max_length=100, verbose_name=_('Legal guardians'), default='', blank=True) - phone_number_private = models.CharField(max_length=100, verbose_name=_('phone number private'), default='', blank=True) - phone_number_mobile = models.CharField(max_length=100, verbose_name=_('phone number mobile'), default='', blank=True) - phone_number_parents = models.CharField(max_length=200, verbose_name=_('parents phone number'), default='', blank=True) group = models.ManyToManyField(Group, verbose_name=_('group')) iban = models.CharField(max_length=30, blank=True, verbose_name='IBAN') - technical_comments = models.TextField(verbose_name=_('Technical comments'), default='', blank=True) gets_newsletter = models.BooleanField(_('receives newsletter'), default=True) unsubscribe_key = models.CharField(max_length=32, default="") unsubscribe_expire = models.DateTimeField(default=timezone.now) created = models.DateField(auto_now=True, verbose_name=_('created')) - registered = models.BooleanField(default=False, verbose_name=_('Registration complete')) active = models.BooleanField(default=True, verbose_name=_('Active')) - #not_waiting = models.BooleanField(default=True, verbose_name=_('Not waiting')) registration_form = RestrictedFileField(verbose_name=_('registration form'), upload_to='registration_forms', blank=True, @@ -232,6 +258,11 @@ class Member(Person): objects = MemberManager() + @property + def email_fields(self): + return [('email', 'confirmed_mail', 'confirm_mail_key'), + ('alternative_email', 'confirmed_alternative_mail', 'confirm_alternative_mail_key')] + @property def place(self): """Returning the whole place (plz + town)""" @@ -259,7 +290,7 @@ class Member(Person): return self.echo_key def confirm(self): - if not self.confirmed_mail or not self.confirmed_mail_parents: + if not self.confirmed_mail or not self.confirmed_alternative_mail: return False self.confirmed = True self.save() @@ -281,23 +312,16 @@ class Member(Person): @property def contact_phone_number(self): - """Returning, if available phone number of parents, else member's phone number""" - if self.phone_number_parents: - return str(self.phone_number_parents) - elif self.phone_number_mobile: - return str(self.phone_number_mobile) - elif self.phone_number_private: - return str(self.phone_number_private) + """Synonym for phone number field.""" + if self.phone_number: + return str(self.phone_number) else: return "---" @property def contact_email(self): - """Returning, if available email of parents, else member's email""" - if self.email_parents: - return self.email_parents - else: - return self.email + """A synonym for the email field.""" + return self.email @property def association_email(self): @@ -305,6 +329,13 @@ class Member(Person): raw = "{0}.{1}@{2}".format(self.prename.lower(), self.lastname.lower(), settings.DOMAIN) return raw.replace('ö', 'oe').replace('ä', 'ae').replace('ü', 'ue') + def registration_complete(self): + """Check if all necessary fields are set.""" + # TODO: implement a proper predicate here + return True + registration_complete.boolean = True + registration_complete.short_description = _('Registration complete') + def get_group(self): """Returns a string of groups in which the member is.""" groupstring = ''.join(g.name + ',\n' for g in self.group.all()) @@ -342,9 +373,29 @@ class Member(Person): # get activity overview return Freizeit.objects.filter(membersonlist__member=self) + def create_from_registration(self, waiter, group): + """Given a member, a corresponding waiting-list object and a group, this completes + the registration and requests email confirmations if necessary. + Returns if any mail confirmation requests have been sent out.""" + self.group.add(group) + self.confirmed = False + if waiter: + if self.email == waiter.email: + self.confirmed_mail = waiter.confirmed_mail + self.confirm_mail_key = waiter.confirm_mail_key + if self.alternative_email: + self.confirmed_alternative_mail = False + self.save() + + if self.confirmed_alternative_mail and self.confirmed_mail and not self.confirmed: + self.notify_jugendleiters_about_confirmed_mail() + if waiter: + waiter.delete() + return self.request_mail_confirmation(rerequest=False) + def confirm_mail(self, key): ret = super().confirm_mail(key) - if self.confirmed_mail_parents and self.confirmed_mail and not self.confirmed: + if self.confirmed_alternative_mail and self.confirmed_mail and not self.confirmed: self.notify_jugendleiters_about_confirmed_mail() return ret @@ -556,6 +607,26 @@ class Member(Person): return False +class EmergencyContact(ContactWithPhoneNumber): + """ + Emergency contact of a member + """ + member = models.ForeignKey(Member, verbose_name=_('Member'), on_delete=models.CASCADE) + + def __str__(self): + return str(self.member) + + class Meta(CommonModel.Meta): + verbose_name = _('Emergency contact') + verbose_name_plural = _('Emergency contacts') + rules_permissions = { + 'add_obj': may_change | has_global_perm('members.change_global_member'), + 'view_obj': may_view | has_global_perm('members.view_global_member'), + 'change_obj': may_change | has_global_perm('members.change_global_member'), + 'delete_obj': may_delete | has_global_perm('members.delete_global_member'), + } + + class MemberUnconfirmedManager(models.Manager): def get_queryset(self): return super().get_queryset().filter(confirmed=False) @@ -584,8 +655,8 @@ class MemberWaitingList(Person): WAITING_CONFIRMATION_EXPIRED = 1 WAITING_CONFIRMED = 2 - application_text = models.TextField(_('application text'), default='', blank=True) - application_date = models.DateTimeField(verbose_name=_('application date'), null=True, blank=True) + application_text = models.TextField(_('Do you want to tell us something else?'), default='', blank=True) + application_date = models.DateTimeField(verbose_name=_('application date'), auto_now=True) last_wait_confirmation = models.DateField(auto_now=True, verbose_name=_('Last wait confirmation')) wait_confirmation_key = models.CharField(max_length=32, default="") @@ -599,7 +670,7 @@ class MemberWaitingList(Person): blank=True, default=None, verbose_name=_('Invited for group'), - on_delete=models.SET_NULL) + on_delete=models.SET_NULL) class Meta(CommonModel.Meta): verbose_name = _('Waiter') verbose_name_plural = _('Waiters') @@ -670,12 +741,9 @@ class MemberWaitingList(Person): return self.registration_key == key and timezone.now() < self.registration_expire def invite_to_group(self): - send_mail(_("Good news"), - settings.INVITE_TEXT.format(name=self.prename, - link=get_registration_link(self)), - settings.DEFAULT_SENDING_MAIL, - [self.email, self.email_parents] if self.email_parents and self.cc_email_parents - else self.email) + self.send_mail(_("Invitation to trial group meeting"), + settings.INVITE_TEXT.format(name=self.prename, + link=get_registration_link(self))) class NewMemberOnList(CommonModel): @@ -1302,12 +1370,10 @@ CLUBDESK_TO_KOMPASS = { 'Land': 'country', 'Nationalität': 'nationality', 'E-Mail': 'email', - 'E-Mail Alternativ': 'email_parents', + 'E-Mail Alternativ': 'alternative_email', 'Status': ('active', parse_status), 'Eintritt': ('join_date', parse_date), 'Austritt': ('leave_date', parse_date), - 'Zivilstand': 'civil_status', - 'Geschlecht': 'gender', 'Geburtsdatum': ('birth_date', parse_date), 'Geburtstag': ('birth_date', parse_date), 'Bemerkungen': 'comments', @@ -1323,12 +1389,11 @@ CLUBDESK_TO_KOMPASS = { 'DAV Ausweis Nr.': 'dav_badge_no', 'Schwimmabzeichen': 'swimming_badge', 'Kletterschein': 'climbing_badge', - 'Felserfahrung': 'rock_experience', + 'Felserfahrung': 'alpine_experience', 'Allergien': 'allergies', 'Medikamente': 'medication', 'Tetanusimpfung': 'tetanus_vaccination', 'Fotoerlaubnis': ('photos_may_be_taken', parse_boolean), - 'Kommentar': 'technical_comments', 'Erziehungsberechtigte': 'legal_guardians', 'Mobil Eltern': 'phone_number_parents', 'Sonstiges': 'application_text', diff --git a/jdav_web/members/templates/members/register.html b/jdav_web/members/templates/members/register.html index bd5efa9..92b5608 100644 --- a/jdav_web/members/templates/members/register.html +++ b/jdav_web/members/templates/members/register.html @@ -35,22 +35,4 @@

- - {% endblock %} diff --git a/jdav_web/members/templates/members/register_waiting_list.html b/jdav_web/members/templates/members/register_waiting_list.html index a9668e6..cf7f62f 100644 --- a/jdav_web/members/templates/members/register_waiting_list.html +++ b/jdav_web/members/templates/members/register_waiting_list.html @@ -39,22 +39,4 @@ Die Anmeldung für die Gruppen

- - {% endblock %} diff --git a/jdav_web/members/views.py b/jdav_web/members/views.py index 158095a..722415d 100644 --- a/jdav_web/members/views.py +++ b/jdav_web/members/views.py @@ -2,7 +2,8 @@ from startpage.views import render from django.utils.translation import gettext_lazy as _ from django.http import HttpResponseRedirect from django.forms import ModelForm, TextInput, DateInput -from members.models import Member, RegistrationPassword, MemberUnconfirmedProxy, MemberWaitingList, Group +from members.models import Member, RegistrationPassword, MemberUnconfirmedProxy, MemberWaitingList, Group,\ + confirm_mail_by_key from django.urls import reverse from django.utils import timezone from django.conf import settings @@ -12,7 +13,7 @@ class MemberForm(ModelForm): class Meta: model = Member fields = ['prename', 'lastname', 'street', 'plz', 'town', 'address_extra', 'country', - 'phone_number_private', 'phone_number_mobile', 'phone_number_parents', 'birth_date'] + 'phone_number', 'birth_date'] widgets = { 'birth_date': DateInput(format='%d.%m.%Y', attrs={'class': 'datepicker'}) } @@ -24,13 +25,10 @@ class MemberRegistrationForm(ModelForm): for field in self.Meta.required: self.fields[field].required = True - self.fields['cc_email_parents'].initial = False - class Meta: model = Member fields = ['prename', 'lastname', 'street', 'plz', 'town', 'address_extra', 'country', - 'phone_number_private', 'phone_number_mobile', 'phone_number_parents', - 'birth_date', 'email', 'email_parents', 'cc_email_parents', + 'phone_number', 'birth_date', 'gender', 'email', 'alternative_email', 'registration_form'] widgets = { 'birth_date': DateInput(format='%d.%m.%Y', attrs={'class': 'datepicker'}) @@ -45,11 +43,9 @@ class MemberRegistrationWaitingListForm(ModelForm): for field in self.Meta.required: self.fields[field].required = True - self.fields['cc_email_parents'].initial = False - class Meta: model = MemberWaitingList - fields = ['prename', 'lastname', 'birth_date', 'email', 'email_parents', 'cc_email_parents'] + fields = ['prename', 'lastname', 'birth_date', 'gender', 'email', 'application_text'] widgets = { 'birth_date': DateInput(format='%d.%m.%Y', attrs={'class': 'datepicker'}) } @@ -162,7 +158,7 @@ def register(request): group = pwd.group except RegistrationPassword.DoesNotExist: return render_register_wrong_password(request) - elif waiter_key: + elif waiter_key: try: waiter = MemberWaitingList.objects.get(registration_key=waiter_key) group = waiter.invited_for_group @@ -178,20 +174,7 @@ def register(request): form = MemberRegistrationForm(request.POST, request.FILES) try: new_member = form.save() - new_member.group.add(group) - new_member.confirmed = False - needs_mail_confirmation = True - if waiter: - if new_member.email == waiter.email and new_member.email_parents == waiter.email_parents: - new_member.confirmed_mail = True - new_member.confirmed_mail_parents = True - needs_mail_confirmation = False - new_member.notify_jugendleiters_about_confirmed_mail() - waiter.delete() - - new_member.save() - if needs_mail_confirmation: - new_member.request_mail_confirmation() + needs_mail_confirmation = new_member.create_from_registration(waiter, group) return render_register_success(request, group.name, new_member.prename, needs_mail_confirmation) except ValueError: # when input is invalid @@ -202,16 +185,11 @@ def register(request): def confirm_mail(request): if request.method == 'GET' and 'key' in request.GET: - key = request.GET['key'] - matching_unconfirmed = MemberUnconfirmedProxy.objects.filter(confirm_mail_key=key) \ - | MemberUnconfirmedProxy.objects.filter(confirm_mail_parents_key=key) - matching_waiter = MemberWaitingList.objects.filter(confirm_mail_key=key) \ - | MemberWaitingList.objects.filter(confirm_mail_parents_key=key) - if len(matching_unconfirmed) + len(matching_waiter) != 1: + res = confirm_mail_by_key(request.GET['key']) + if res: + return render_mail_confirmation_success(request, res[1], res[0].prename, False) + else: return render_mail_confirmation_invalid(request) - person = matching_unconfirmed[0] if len(matching_unconfirmed) == 1 else matching_waiter[0] - email, parents = person.confirm_mail(key) - return render_mail_confirmation_success(request, email, person.prename, parents) return HttpResponseRedirect(reverse('startpage:index')) @@ -272,7 +250,7 @@ def invited_registration(request): return render_invited_registration_failed(request, _("invalid")) except KeyError: return render_invited_registration_failed(request, _("expired")) - + # if its a POST request return register(request)