diff --git a/src/memberdb/account.py b/src/memberdb/account.py index 5f911c0f1cd3747cdce27e480f708ca32356af96..216358c0b7800039f28efb10d317affe828a34ff 100644 --- a/src/memberdb/account.py +++ b/src/memberdb/account.py @@ -45,8 +45,7 @@ class AccountForm(MyModelForm): pass super().clean(); - def save(self): - return + class EmailForm(MyModelForm): forward = forms.BooleanField(required=False) @@ -64,7 +63,7 @@ class EmailForm(MyModelForm): if self['forward'].value() == True: try: if (len(self['email_address'].value()) == 0): - self.add_error('email_address', 'Email field cannot be left blankL.') + self.add_error('email_address', 'Email field cannot be left blank.') if (self['email_address'].value().split('@')[1] in ["ucc.asn.au", "ucc.gu.uwa.edu.au"]): self.add_error('email_address', 'Forwarding address cannot be the same as your account address.') except: @@ -121,12 +120,22 @@ class AccountView(MyWizardView): def done(self, form_list, form_dict, **kwargs): - # create the user and save their username if successfull - if create_ad_user(self.get_cleaned_data_for_step('0'), self.object): - form_list[0].save() - - messages.success(self.request, 'Your membership renewal has been submitted.') + # create the user and save their username if successfull + try: + if create_ad_user(self.get_cleaned_data_for_step('0'), self.object): + form_dict['0'].save() + + make_home(self.get_cleaned_data_for_step('1'), self.object) + make_dispense_account(self.object.username, self.get_cleaned_data_for_step('2')['pin']) + subscribe_to_list(self.object) + except Exception as e: + messages.error(self.request,'Account creation failed for %s', self.object) + messages.error(self.request, e) + raise #DEBUG + + else: + messages.success(self.request, 'An account has been successfully created for %s.' % self.object) return HttpResponseRedirect(reverse("admin:memberdb_membership_changelist")) #return accountProgressView(self.request, m) diff --git a/src/memberdb/account_backend.py b/src/memberdb/account_backend.py index a66364c1eb36981ea2af36143cb650d641f97893..697f95a4a56939e9f8784e7915e36ef20a7f236d 100644 --- a/src/memberdb/account_backend.py +++ b/src/memberdb/account_backend.py @@ -31,7 +31,7 @@ ldap_user_dn = getattr(settings, 'LDAP_USER_SEARCH_DN') ldap_base_dn = getattr(settings, 'LDAP_BASE_DN') ldap_bind_dn = getattr(settings, 'LDAP_BIND_DN') ldap_bind_secret = getattr(settings, 'LDAP_BIND_SECRET') -make_home_cmd = "sudo python3 root_actions.py" +make_home_cmd = ["sudo", "python3", "root_actions.py"] make_mail_cmd = 'ssh -i %s root@mooneye "/usr/local/mailman/bin/add_members" -r- ucc-announce <<< %s@ucc.asn.au' make_mail_key = './mooneye.key' @@ -206,7 +206,8 @@ def create_ad_user(form_data, member): result = get_maxuid() except: log.error("LDAP: cannot find base uid") - return False + raise + maxuid = int(result.msSFU30MaxUidNumber.value) @@ -236,7 +237,7 @@ def create_ad_user(form_data, member): # sanity check: make sure the uid is free if subprocess.call(["id", newuid], stderr=subprocess.DEVNULL) == 0: log.error("LDAP: uid already taken") - return False + raise ValueError # create the new user struct objclass = ['top','posixAccount','person','organizationalPerson','user'] @@ -261,24 +262,31 @@ def create_ad_user(form_data, member): result = ld.add(dn, objclass, attrs) if not result: log.error("LDAP: user add failed") - return False + raise LDAPOperationsErrorResult # set maxuid result = ld.modify(maxuid_dn, {'msSFU30MaxUidNumber': [(MODIFY_REPLACE, newuid)]}) if not result: - log.warning("LDAP: user created but msSFU30MaxUidNumber not update") + log.warning("LDAP: user created but msSFU30MaxUidNumber not updated") ld.unbind(); return True; -def make_home(member,formdata): +def make_home(formdata, member): user = member.username mail = formdata['email_address'] if formdata['forward'] else "" - return subprocess.call(make_home_cmd, user, mail) - + result = subprocess.call(make_home_cmd + [user, mail]) + if result == 0: + return True + else: + raise CalledProcessError def subscribe_to_list(member): - return os.system(make_mail_cmd % (make_mail_key, member.username)) + result = os.system(make_mail_cmd % (make_mail_key, member.username)) + if result == 0: + return True + else: + raise CalledProcessError def set_pin(member, pin): return diff --git a/src/memberdb/forms.py b/src/memberdb/forms.py index f9ff1d4bc546436633f348923d48a90a5f6c40d1..340c30739fe878eb5518703ee94632ec3708aaba 100644 --- a/src/memberdb/forms.py +++ b/src/memberdb/forms.py @@ -23,4 +23,4 @@ class MyForm(forms.Form): class MemberHomeForm(MyModelForm): class Meta: model = Member - fields = ['display_name', 'email_address', 'phone_number'] + fields = [ 'email_address', 'phone_number'] diff --git a/src/memberdb/models.py b/src/memberdb/models.py index d5ba6f1484a5fffca285336dcb8a6d7155843b86..57dddd6000a3d874f0a9cc427a3d328490323812 100644 --- a/src/memberdb/models.py +++ b/src/memberdb/models.py @@ -5,6 +5,7 @@ from django.core.management.utils import get_random_string from django.urls import reverse from squarepay.dispense import get_item_price +import subprocess import ldap @@ -110,6 +111,13 @@ PAYMENT_METHODS = [ ('', 'No payment') ] +ID_TYPES = [ + ('student', 'Student ID'), + ('drivers', 'Drivers licence'), + ('passport', 'Passport'), + ('Other', 'Other ID'), +] + ACCOUNT_STATUS = [ 'enabled', 'disabled', @@ -124,15 +132,15 @@ class IncAssocMember (models.Model): Note: these data should only be changed administratively or with suitable validation since it must be up to date & accurate. """ - first_name = models.CharField ('First name', max_length=200) - last_name = models.CharField ('Surname', max_length=200) - email_address = models.EmailField ('Email address', blank=False) + first_name = models.CharField ('Given Name', max_length=200) + last_name = models.CharField ('Other Names', max_length=200) + email_address = models.EmailField ('Contact email', blank=False) updated = models.DateTimeField ('IncA. info last updated', auto_now=True) created = models.DateTimeField ('When created', auto_now_add=True) def __str__ (self): return "%s %s <%s>" % (self.first_name, self.last_name, self.email_address) - + class Meta: verbose_name = "Incorporations Act member data" verbose_name_plural = verbose_name @@ -145,13 +153,16 @@ class Member (IncAssocMember): Note: Privacy laws are a thing, unless people allow it then we cannot provide this info to members. """ + + # data to be entered by user and validated (mostly) manually display_name = models.CharField ('Display name', max_length=200) - username = models.SlugField ('Username', max_length=32, null=False, blank=False, unique=True, validators=[RegexValidator(regex='^[a-z0-9._-]+$')]) + username = models.SlugField ('Username', max_length=32, null=True, blank=True, unique=True, validators=[RegexValidator(regex='^[a-z0-9._-]*$')]) phone_number = models.CharField ('Phone number', max_length=20, blank=False, validators=[RegexValidator(regex='^\+?[0-9() -]+$')]) is_student = models.BooleanField ('Student', default=True, blank=True, help_text="Tick this box if you are a current student at a secondary or tertiary institution in WA") is_guild = models.BooleanField ('UWA Guild member', default=True, blank=True) - id_number = models.CharField ('Student email or Drivers License', max_length=255, blank=False, help_text="Student emails should end with '@student.*.edu.au' and drivers licences should be in the format '<AU state> 1234567'") + id_number = models.CharField ('Student number or other ID', max_length=255, blank=False, help_text="") + id_desc = models.CharField ('Form of ID provided', max_length=255, blank=False, help_text="Please describe the type of identification provided", choices=ID_TYPES, default="student") # data used internally by the system, not to be touched, seen or heard (except when it is) member_updated = models.DateTimeField ('Internal UCC info last updated', auto_now=True) @@ -160,9 +171,15 @@ class Member (IncAssocMember): studnt_confirm = models.BooleanField ('Student status confirmed', null=False, editable=False, default=False) guild_confirm = models.BooleanField ('Guild status confirmed', null=False, editable=False, default=False) + has_account = models.BooleanField ('Has AD account', null=False, editable=False, default=False) + # account info - def get_account_status(self): - return; + def get_uid(self): + result, uid = subprocess.getstatusoutput(["id", "-u", self.username]) + if (result == 0): + return uid; + else: + return None; def __str__ (self): if (self.display_name != "%s %s" % (self.first_name, self.last_name)): diff --git a/src/memberdb/register.py b/src/memberdb/register.py index 60f1ba38e8820a922afe2098a1089eea8baca7e0..5058358275c863646c400bb85d604e8ad3508f77 100644 --- a/src/memberdb/register.py +++ b/src/memberdb/register.py @@ -23,119 +23,147 @@ First step: enter an email address and some details (to fill at least a Member m see https://docs.djangoproject.com/en/2.1/ref/models/fields/#error-messages and https://docs.djangoproject.com/en/2.1/ref/forms/fields/#error-messages """ -class RegisterForm(MyModelForm): - confirm_email = forms.EmailField(label='Confirm your email address', required=False) - agree_tnc = forms.BooleanField(label='I agree to the terms & conditions', required=True, help_text=mark_safe( - "You agree to abide by the UCC Constitution, rulings of the UCC Committee, UCC and " - "UWA’s Network Usage Guidelines and that you will be subscribed to the UCC Mailing List. <br>" - '<b>Policies can be found <a href="https://www.ucc.asn.au/infobase/policies.ucc">here</a>.</b>')) - membership_type = forms.ChoiceField(label='Select your membership type', required=True, choices=get_membership_choices(is_renew=False)) - - class Meta: - model = Member - fields = ['first_name', 'last_name', 'username', 'phone_number', 'is_student', 'is_guild', 'id_number', 'email_address'] - error_messages = { - 'username': { - 'unique': 'This username is already taken, please pick another one.', - 'invalid': 'Please pick a username with only lowercase letters and numbers' - } - } - - def clean(self): - try: - if (self['email_address'].value() != self['confirm_email'].value()): - self.add_error('email_address', 'Email addresses must match.') - except: - pass - super().clean(); - - def save(self, commit=True): - # get the Member model instance (ie. a record in the Members table) based on submitted form data - m = super().save(commit=False) - if (m.display_name == ""): - m.display_name = "%s %s" % (m.first_name, m.last_name); - # must save otherwise membership creation will fail - m.save() - - # now create a corresponding Membership (marked as pending / not accepted, mostly default values) - ms = make_pending_membership(m) - - if (commit): - ms.save(); - return m, ms - -class RenewForm(RegisterForm): - confirm_email = None - membership_type = forms.ChoiceField(label='Select your membership type', required=True, choices=get_membership_choices(is_renew=True)) - - class Meta(RegisterForm.Meta): - fields = ['first_name', 'last_name', 'phone_number', 'is_student', 'is_guild', 'id_number', 'email_address'] - - def save(self, commit=True): - m, ms = super().save(commit=False) - m.username = self.request.user.username - if (commit): - m.save() - ms.save() - return m, ms +class RegisterRenewForm(MyModelForm): + confirm_email = forms.EmailField(label='Confirm your email address', required=False) + agree_tnc = forms.BooleanField(label='I agree to the terms & conditions', required=True, help_text=mark_safe( + "You agree to abide by the UCC Constitution, rulings of the UCC Committee, UCC and " + "UWA’s Network Usage Guidelines and that you will be subscribed to the UCC Mailing List. <br>" + '<b>Policies can be found <a href="https://www.ucc.asn.au/infobase/policies.ucc">here</a>.</b>')) + membership_type = forms.ChoiceField(label='Select your membership type', required=True, choices=get_membership_choices(is_renew=False)) + + class Meta: + model = Member + fields = ['first_name', 'last_name', 'phone_number', 'is_student', 'is_guild', 'id_number', 'id_desc', 'email_address'] + error_messages = { + 'username': { + 'unique': 'This username is already taken, please pick another one.', + 'invalid': 'Please pick a username with only lowercase letters and numbers' + } + } + + def clean(self): + try: + if (self['email_address'].value() != self['confirm_email'].value()): + self.add_error('email_address', 'Email addresses must match.') + if (self['email_address'].value().split('@')[1] in ["ucc.asn.au", "ucc.gu.uwa.edu.au"]): + self.add_error('email_address', 'Contact address cannot be an UCC address.') + except: + pass + super().clean(); + + def save(self, commit=True): + # get the Member model instance (ie. a record in the Members table) based on submitted form data + m = super().save(commit=False) + if (m.display_name == ""): + m.display_name = "%s %s" % (m.first_name, m.last_name); + # must save otherwise membership creation will fail + m.save() + + # now create a corresponding Membership (marked as pending / not accepted, mostly default values) + ms = make_pending_membership(m) + + if (commit): + ms.save(); + return m, ms + +class RegisterForm(RegisterRenewForm): + username = forms.CharField( + label='Preferred Username (optional)', + required=False, + help_text="This will be the username you use to access club systems. You may leave this blank to choose a username later" + ) + + class Meta(): + model = Member + fields = ['first_name', 'last_name', 'username', 'phone_number', 'is_student', 'is_guild', 'id_number', 'id_desc', 'email_address'] + + + def clean(self): + try: + if (self['email_address'].value() != self['confirm_email'].value()): + self.add_error('email_address', 'Email addresses must match.') + if (self['email_address'].value().split('@')[1] in ["ucc.asn.au", "ucc.gu.uwa.edu.au"]): + self.add_error('email_address', 'Contact address cannot be an UCC address.') + except: + pass + super().clean(); + + +class RenewForm(RegisterRenewForm): + confirm_email = None + membership_type = forms.ChoiceField(label='Select your membership type', required=True, choices=get_membership_choices(is_renew=True)) + + class Meta: + model = Member + fields = ['first_name', 'last_name', 'phone_number', 'is_student', 'is_guild', 'id_number', 'id_desc', 'email_address'] + exclude = ['username'] + + def save(self, commit=True): + m, ms = super().save(commit=False) + m.username = self.request.user.username + m.has_account = m.get_uid() != None + if (commit): + m.save() + ms.save() + return m, ms """ simple FormView which displays registration form and handles template rendering & form submission """ class RegisterView(MyUpdateView): - template_name = 'register.html' - form_class = RegisterForm - model = Member - can_create = False - - """ - called when valid form data has been POSTed - invalid form data simply redisplays the form with validation errors - """ - def form_valid(self, form): - # save the member data and get the Member instance - m, ms = form.save() - messages.success(self.request, 'Your registration has been submitted.') - - # don't set the member session info - user can click on the link - #self.request.session['member_id'] = m.id - return thanks_view(self.request, m, ms) + template_name = 'register.html' + form_class = RegisterForm + model = Member + can_create = False + + """ + called when valid form data has been POSTed + invalid form data simply redisplays the form with validation errors + """ + def form_valid(self, form): + # save the member data and get the Member instance + m, ms = form.save() + messages.success(self.request, 'Your registration has been submitted.') + + # don't set the member session info - user can click on the link + #self.request.session['member_id'] = m.id + return thanks_view(self.request, m, ms) def thanks_view(request, member, ms): - """ display a thankyou page after registration is completed """ - context = { - 'member': member, - 'ms': ms, - 'login_url': reverse('memberdb:login_member', kwargs={'username': member.username, 'member_token': member.login_token}), - } - return render(request, 'thanks.html', context) + """ display a thankyou page after registration is completed """ + context = { + 'member': member, + 'ms': ms, + 'login_url': reverse('memberdb:login_member', kwargs={'username': member.username, 'member_token': member.login_token}), + } + return render(request, 'thanks.html', context) class RenewView(LoginRequiredMixin, MyUpdateView): - template_name = 'renew.html' - form_class = RenewForm - model = Member - - def get_object(self): - u = self.request.user - - obj = Member.objects.filter(username__exact=u.username).first() - if (obj is None): - # make a new Member object and prefill some data - obj = Member(username=u.username) - obj.first_name = u.first_name - obj.last_name = u.last_name - obj.email_address = u.email - obj.login_token = None # renewing members won't need this - return obj - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context.update({ - 'is_new': Member.objects.filter(username__exact=self.request.user.username).count() == 0, - }) - return context - - def form_valid(self, form): - m, ms = form.save() - messages.success(self.request, 'Your membership renewal has been submitted.') - return HttpResponseRedirect(reverse("memberdb:home")) + template_name = 'renew.html' + form_class = RenewForm + model = Member + + def get_object(self): + u = self.request.user + + obj = Member.objects.filter(username__exact=u.username).first() + if (obj is None): + # make a new Member object and prefill some data + obj = Member(username=u.username) + obj.first_name = u.first_name + obj.last_name = u.last_name + obj.email_address = u.email + obj.login_token = None # renewing members won't need this + return obj + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context.update({ + 'is_new': Member.objects.filter(username__exact=self.request.user.username).count() == 0, + }) + return context + + def form_valid(self, form): + m, ms = form.save() + messages.success(self.request, 'Your membership renewal has been submitted.') + return HttpResponseRedirect(reverse("memberdb:home")) diff --git a/src/memberdb/root_actions.py b/src/memberdb/root_actions.py index f822c827c42a44cb4bf4669db34fd37f3d19fef2..4c6089e86f5dbbbc9a0d68704088b607a752c92a 100644 --- a/src/memberdb/root_actions.py +++ b/src/memberdb/root_actions.py @@ -2,22 +2,31 @@ import sys import os import shutil import subprocess -## WARNING ## -# this script runs with elevated permissions # + + + ### ###### ## ## ######## ## ## ## ## ###### + ## ## ## ## ## ## ## ## ## ### ## ## ## + ## ## ## ## ## ## ## ## #### ## ## +## ## ## ######### ## ## ## ## ## ## ## #### +######### ## ## ## ## ## ## ## #### ## ## +## ## ## ## ## ## ## ## ## ## ### ## ## +## ## ###### ## ## ## ####### ## ## ###### + + # this script runs with elevated permissions # + # be very careful with what you do # def main(): os.umask(0o077) - if len(sys.argv) != 2: - return 1 - user = sys.argv[0] - mail = sys.argv[1] + if len(sys.argv) != 3: + exit(1) + user = sys.argv[1] + mail = sys.argv[2] # abort if user does not exist if subprocess.call(["id", user], stderr=subprocess.DEVNULL) != 0: - return 1 - + exit(1) homes = { ('/home/ucc/%s' % user, '/home/wheel/bin/skel/ucc'), @@ -34,7 +43,7 @@ def main(): os.system('chmod a+x %s' % home) os.system('chmod a+rX %s/public-html' % home) except: - return 1 + exit(1) # write .forward try: @@ -46,10 +55,12 @@ def main(): shutil.chown(forward,user,"gumby") os.chmod(forward, 0o644) except: - return 1 + exit(1) + if __name__ == "__main__": main() + exit(0) diff --git a/src/squarepay/dispense.py b/src/squarepay/dispense.py index abd5a32312d371eb0ab3b361c219f41405576670..e32393578c39716e0fbff21c9ba9254b37a9caaa 100644 --- a/src/squarepay/dispense.py +++ b/src/squarepay/dispense.py @@ -55,7 +55,6 @@ def set_dispense_flag(user, flag, reason): log.warning("DISPENSE_BIN is not defined, user will not be disabled") return False - cmd = [DISPENSE_BIN] + args out = run_dispense('user', 'type', user, flag, reason) s = out.split() if s[2] != "updated": @@ -63,3 +62,23 @@ def set_dispense_flag(user, flag, reason): log.warning("set_dispense_flag: user was not updated with error: " + out) return False; return True; + +def make_dispense_account(user, pin): + if DISPENSE_BIN is None: + log.warning("DISPENSE_BIN is not defined") + return False + + cmdargs = [ + ("user","add", user), + ("acct",user,"+500","treasurer: new user"), + ("-u", user, "pinset", pin) + ] + + + for args in cmdargs: + cmd = [DISPENSE_BIN] + args + out = run_dispense('user', 'type', user, flag, reason) + if out == None: + raise CalledProcessError + + return True; diff --git a/src/templates/admin/memberdb/account_create.html b/src/templates/admin/memberdb/account_create.html index 0f76d5153c02d8a373a3863348279c05a8293ee6..cebe02f1209314d1045efb52c0f73f3d8485ee6e 100644 --- a/src/templates/admin/memberdb/account_create.html +++ b/src/templates/admin/memberdb/account_create.html @@ -44,8 +44,8 @@ </fieldset> <div class="submit-row"> {% if wizard.steps.prev %} - <button name="wizard_goto_step" class="button" type="submit" value="{{ wizard.steps.prev }}"> prev </button> - {% endif %} + <button name="wizard_goto_step" class="button" type="submit" value="{{ wizard.steps.prev }}"> prev </button> + {% endif %} {% if wizard.steps.next %} <input type="submit" value="{% trans "next" %}"/> diff --git a/src/templates/admin/memberdb/membership_actions.html b/src/templates/admin/memberdb/membership_actions.html index ce12e156bda0e7749ef0d39781dacf4def36a385..8bced93e869e7e28a0dc950bfff6e72455cd54a6 100644 --- a/src/templates/admin/memberdb/membership_actions.html +++ b/src/templates/admin/memberdb/membership_actions.html @@ -1,8 +1,10 @@ {% load static %}{# this template is used to generate the member action buttons, see memberdb/admin.py #} <div class="member-actions"> - <a class="button" href="{{ member_url }}">Edit</a> - {% if not ms.approved %} - <a class="button" href="{{ member_approve }}">Approve</a> - {% endif %} - <a class="button" href="{{ create_account }}">Create Account</a> + <a class="button" href="{{ member_url }}">Edit</a> + {% if not ms.approved %} + <a class="button" href="{{ member_approve }}">Approve</a> + {% endif %} + {% if not member.has_account %} + <a class="button" href="{{ create_account }}">Create Account</a> + {% endif %} </div> diff --git a/src/templates/membership_summary.html b/src/templates/membership_summary.html index aeffa35b3f30715546aab9fd453ebb67de50e41e..29d69251a8f0afc62df4c2cdd54132c969a19dd2 100644 --- a/src/templates/membership_summary.html +++ b/src/templates/membership_summary.html @@ -19,7 +19,7 @@ {% block email %} <tr class="{% cycle rcl %}"> - <th>Email</th> + <th>Contact email</th> <td> {% if member.email_address %} <a href="mailto:{{ member.email_address }}">{{ member.email_address }}</a> @@ -47,9 +47,13 @@ <td>{% if member.is_guild %}<img src="{% static 'admin/img/icon-yes.svg' %}" alt="yes">{% else %}<img src="{% static 'admin/img/icon-no.svg' %}" alt="no">{% endif %}</td> </tr> <tr class="{% cycle rcl %}"> - <th>Student email / Drivers license</th> + <th>Student number or Other ID</th> <td>{% if member.id_number %}{{ member.id_number }}{% else %}<i>not provided</i>{% endif %}</td> </tr> + <tr class="{% cycle rcl %}"> + <th>ID type</th> + <td>{% if member.id_desc %}{{ member.id_desc }}{% else %}<i>not provided</i>{% endif %}</td> + </tr> {% endblock %} </table> {% endblock %}