diff --git a/src/memberdb/account.py b/src/memberdb/account.py new file mode 100644 index 0000000000000000000000000000000000000000..64d12a162b0d93237016195ac626a481698171da --- /dev/null +++ b/src/memberdb/account.py @@ -0,0 +1,84 @@ +from django.http import HttpResponse, HttpResponseRedirect +from django.shortcuts import render +from django.urls import reverse +from django.utils import timezone +from django import forms + +from .models import Member +from .forms import MyModelForm +from .views import MyUpdateView +from memberdb.account_backend import validate_username + +class AccountForm(MyModelForm): + + # form fields + user= forms.SlugField( + validators=[validate_username] + ) + forward_email = forms.EmailField( + label='Forwarding address(optional)', + required=False, + help_text="Your club email will be forwarded to this address. Leave blank if email forwarding is not required" + ) + + password = forms.CharField( + min_length=10, + max_length=127, + widget=forms.PasswordInput, + strip=False, + help_text="Password must be between 10 and 127 characters long") + + confirm_password = forms.CharField( + min_length=10, + max_length=127, + widget=forms.PasswordInput, + strip=False, + ) + + + class Meta: + model = Member + fields = ['first_name'] + 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: + user.clean() + if (self['password'].value() != self['confirm_password'].value()): + self.add_error('confirm_password', 'Passwords must match.') + if (self['forward_email'].value().split('@')[1] in ["ucc.asn.au", "ucc.gu.uwa.edu.au"]): + self.add_error('forward_email', 'Forwarding address cannot be the same as your account address.') + except: + pass + super().clean(); + + def save(self): + return + + + +class AccountView(MyUpdateView): + template_name = 'admin/memberdb/account_create.html' + form_class = AccountForm + model = Member + pk_url_kwarg = 'object_id' + admin = None + + def get_context_data(self, **kwargs): + m = self.get_object() + context = super().get_context_data(**kwargs) + context.update(self.admin.admin_site.each_context(self.request)) + context.update({ + 'opts': self.admin.model._meta, + 'member': m, + }) + return context + + def form_valid(self, form): + m, ms = form.save() + messages.success(self.request, 'Your membership renewal has been submitted.') + return HttpResponseRedirect(reverse("admin:memberdb_membership_summary")) diff --git a/src/account/actions.py b/src/memberdb/account_backend.py similarity index 81% rename from src/account/actions.py rename to src/memberdb/account_backend.py index c4a73826b0d8082f291b3ba98180bdb808026320..4883b204b82536be9d86af0133fbddcdc46e0246 100644 --- a/src/account/actions.py +++ b/src/memberdb/account_backend.py @@ -2,7 +2,8 @@ import logging from django.conf import settings -from django.core.exceptions import ImproperlyConfigured +from django.core.exceptions import ImproperlyConfigured, ValidationError +from django.utils.translation import gettext_lazy as _ import ldap import re @@ -15,9 +16,9 @@ log = logging.getLogger('ldap') # load config ldap_uri = getattr(settings, 'AUTH_LDAP_SERVER_URI') -ldap_search_dn = getattr(settings, 'REPLACE_ME') -ldap_bind_dn = getattr() -ldap_bind_secret = getattr() +ldap_search_dn = getattr(settings, 'AUTH_LDAP_USER_DN_TEMPLATE') +#ldap_bind_dn = getattr() +#ldap_bind_secret = getattr() #initalise ldap instace @@ -40,6 +41,9 @@ def get_user_attrs(username, attrs): return None return result[0]; + except: + return None + def get_account_lock_status(username): ld = get_ldap_instance() try: @@ -49,6 +53,17 @@ def get_account_lock_status(username): ld.unbind() return bool(result[1]['userAccountControl'] & 0x002) +def validate_username(value): + # usernames can't begin with a numeric + if re.match(r"^\d.*", value): + log.info("test") + raise ValidationError( + _('Username cannot begin with a number'), + params={'value': value} + ) + else: + return value + # locks the specified User Account by performing the following actions: # 1. set UAC ACCOUNTDISABLE flag (0x002) via ldap # 2. set user shell to `/etc/locked20xx` via ldap @@ -104,7 +119,7 @@ def unlock_account(username): # Account creation steps: # def create_account(member): - username = + username = "changeme"; log.info("I: creating new account for %s (%s %s)") # prepend student numbers with 'sn' diff --git a/src/memberdb/admin.py b/src/memberdb/admin.py index 25d3698ee1fceae61b8ddd7b38dcb43d23460184..413fe0edfec2a2977d6261b3d204f20ed01d942b 100644 --- a/src/memberdb/admin.py +++ b/src/memberdb/admin.py @@ -10,122 +10,138 @@ from gms import admin from memberdb.models import Member, IncAssocMember, Membership from memberdb.actions import download_as_csv from memberdb.approve import MembershipApprovalForm, MembershipApprovalAdminView - +from memberdb.account import AccountForm, AccountView def get_model_url(pk, model_name): - return reverse('admin:memberdb_%s_change' % model_name, args=[pk]) + return reverse('admin:memberdb_%s_change' % model_name, args=[pk]) """ helper mixin to make the admin page display only "View" rather than "Change" or "Add" """ class ReadOnlyModelAdmin(admin.ModelAdmin): - def has_add_permission(self, request): - return False - - def has_delete_permission(self, request, obj=None): - return True + def has_add_permission(self, request): + return False + + def has_delete_permission(self, request, obj=None): + return True - def has_change_permission(self, request, obj=None): - return False + def has_change_permission(self, request, obj=None): + return False """ Define the administrative interface for viewing member details required under the Incorporations Act """ class IAMemberAdmin(ReadOnlyModelAdmin): - readonly_fields = ['__str__', 'updated', 'created'] - fields = ['first_name', 'last_name', 'email_address', 'updated', 'created'] - search_fields = ['first_name', 'last_name', 'email_address'] - list_display = readonly_fields - actions = [download_as_csv] - - # add a "go to member" URL into the template context data - def change_view(self, request, object_id, form_url='', extra_context={}): - extra_context['member_edit_url'] = get_model_url(object_id, 'member') - return super().change_view(request, object_id, form_url, extra_context=extra_context) - + readonly_fields = ['__str__', 'updated', 'created'] + fields = ['first_name', 'last_name', 'email_address', 'updated', 'created'] + search_fields = ['first_name', 'last_name', 'email_address'] + list_display = readonly_fields + actions = [download_as_csv] + + # add a "go to member" URL into the template context data + def change_view(self, request, object_id, form_url='', extra_context={}): + extra_context['member_edit_url'] = get_model_url(object_id, 'member') + return super().change_view(request, object_id, form_url, extra_context=extra_context) + class MembershipInline(admin.TabularInline): - model = Membership - readonly_fields = ['member', 'date_submitted'] - radio_fields = {'payment_method': admin.VERTICAL, 'membership_type': admin.VERTICAL} - extra = 0 - fk_name = 'member' + model = Membership + readonly_fields = ['member', 'date_submitted'] + radio_fields = {'payment_method': admin.VERTICAL, 'membership_type': admin.VERTICAL} + extra = 0 + fk_name = 'member' class MemberAdmin(admin.ModelAdmin): - list_display = ['first_name', 'last_name', 'display_name', 'username'] - list_filter = ['is_guild', 'is_student'] - readonly_fields = ['member_updated', 'updated', 'created'] - search_fields = list_display - actions = [download_as_csv] - inlines = [MembershipInline] - - # add a "go to member" URL into the template context data - def change_view(self, request, object_id, form_url='', extra_context={}): - extra_context['incassocmember_url'] = get_model_url(object_id, 'incassocmember') - return super().change_view(request, object_id, form_url, extra_context=extra_context) + list_display = ['first_name', 'last_name', 'display_name', 'username'] + list_filter = ['is_guild', 'is_student'] + readonly_fields = ['member_updated', 'updated', 'created'] + search_fields = list_display + actions = [download_as_csv] + inlines = [MembershipInline] + + # add custom URLs to this model in the admin site + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path('<object_id>/create/', self.admin_site.admin_view(self.process_account), name='create-account'), + ] + return custom_urls + urls + + + + # add a "go to member" URL into the template context data + def change_view(self, request, object_id, form_url='', extra_context={}): + extra_context['incassocmember_url'] = get_model_url(object_id, 'incassocmember') + return super().change_view(request, object_id, form_url, extra_context=extra_context) + + def process_account(self, request, *args, **kwargs): + return AccountView.as_view(admin=self)(request, *args, **kwargs) + + """ Define the admin page for viewing normal Member records (all details included) and approving them """ class MembershipAdmin(admin.ModelAdmin): - list_display = ['membership_info', 'member_actions', 'membership_type', 'payment_method', 'approved', 'date_submitted', ] - list_display_links = None - list_filter = ['approved'] - readonly_fields = ['date_submitted'] - radio_fields = {'payment_method': admin.VERTICAL, 'membership_type': admin.VERTICAL} - - # make the admin page queryset preload the parent records (Member) - def get_queryset(self, request): - qs = super().get_queryset(request) - return qs.select_related('member') - - # add custom URLs to this model in the admin site - def get_urls(self): - urls = super().get_urls() - custom_urls = [ - path('<object_id>/approve/', self.admin_site.admin_view(self.process_approve), name='membership-approve'), - ] - return custom_urls + urls - - # display a short summary of relevant member / membership info for pending memberships - def membership_info(self, ms): - context = { - 'ms': ms, - 'member': ms.member, - 'member_url': get_model_url(ms.member.pk, 'member'), - } - html = render_to_string('admin/memberdb/membership_summary.html', context) - return mark_safe(html) - - membership_info.short_description = 'Membership info' - membership_info.allow_tags = True - - # called per record, returns HTML to display under the "Actions" column - def member_actions(self, ms): - context = { - 'ms': ms, - 'member': ms.member, - 'member_url': get_model_url(ms.member.pk, 'member'), - 'member_approve': reverse('admin:membership-approve', args=[ms.pk]) - } - html = render_to_string('admin/memberdb/membership_actions.html', context) - return mark_safe(html) - - member_actions.short_description = 'Actions' - member_actions.allow_tags = True - - def process_approve(self, request, *args, **kwargs): - return MembershipApprovalAdminView.as_view(admin=self)(request, *args, **kwargs) - -""" + list_display = ['membership_info', 'membership_type', 'payment_method', 'approved', 'date_submitted', 'member_actions', ] + list_display_links = None + list_filter = ['approved'] + readonly_fields = ['date_submitted'] + radio_fields = {'payment_method': admin.VERTICAL, 'membership_type': admin.VERTICAL} + + # make the admin page queryset preload the parent records (Member) + def get_queryset(self, request): + qs = super().get_queryset(request) + return qs.select_related('member') + + # add custom URLs to this model in the admin site + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path('<object_id>/approve/', self.admin_site.admin_view(self.process_approve), name='membership-approve'), + ] + return custom_urls + urls + + # display a short summary of relevant member / membership info for pending memberships + def membership_info(self, ms): + context = { + 'ms': ms, + 'member': ms.member, + 'member_url': get_model_url(ms.member.pk, 'member'), + } + html = render_to_string('admin/memberdb/membership_summary.html', context) + return mark_safe(html) + + membership_info.short_description = 'Membership info' + membership_info.allow_tags = True + + # called per record, returns HTML to display under the "Actions" column + def member_actions(self, ms): + context = { + 'ms': ms, + 'member': ms.member, + 'member_url': get_model_url(ms.member.pk, 'member'), + 'member_approve': reverse('admin:membership-approve', args=[ms.pk]), + 'create_account': reverse('admin:create-account', args=[ms.member.pk]) + } + html = render_to_string('admin/memberdb/membership_actions.html', context) + return mark_safe(html) + + member_actions.short_description = 'Actions' + member_actions.allow_tags = True + + def process_approve(self, request, *args, **kwargs): + return MembershipApprovalAdminView.as_view(admin=self)(request, *args, **kwargs) + + """ Register multiple ModelAdmins per model. See https://stackoverflow.com/questions/2223375/multiple-modeladmins-views-for-same-model-in-django-admin/2228821 """ class ProxyMembership(Membership): - class Meta: - proxy = True + class Meta: + proxy = True class PendingMembershipAdmin(MembershipAdmin): - def get_queryset(self, request): - return self.model.objects.filter(approved__exact=False) + def get_queryset(self, request): + return self.model.objects.filter(approved__exact=False) # Register the other models with either default admin site pages or with optional customisations admin.site.register(Member, MemberAdmin) diff --git a/src/static/account_form.js b/src/static/account_form.js new file mode 100644 index 0000000000000000000000000000000000000000..149bd13edff15de187cb6e733f812902b7e41920 --- /dev/null +++ b/src/static/account_form.js @@ -0,0 +1 @@ +var conditional_fields = $("div"); \ No newline at end of file diff --git a/src/static/admin_custom.css b/src/static/admin_custom.css index 629d6c09b024c7598d1fecd17bd3941a52af0a9b..00ce3db898ca8871eb330fc763d319be52f92bc7 100644 --- a/src/static/admin_custom.css +++ b/src/static/admin_custom.css @@ -34,6 +34,7 @@ overflow: auto; } + .button { display: inline-block; } diff --git a/src/templates/admin/memberdb/account_create.html b/src/templates/admin/memberdb/account_create.html new file mode 100644 index 0000000000000000000000000000000000000000..c8e1724a0ccde01b8634e0f9f2cf9466c941f9c5 --- /dev/null +++ b/src/templates/admin/memberdb/account_create.html @@ -0,0 +1,40 @@ +{% extends "admin/change_form.html" %} +{% load i18n admin_static admin_modify %} + +{% block content %} +<div id="content-main"> +<h1>Create Account for <i>{{ member.first_name }} {{ member.last_name }}</i></h1> +<div class="ms-approve-summary"> +{% include "admin/memberdb/membership_summary.html" %} +</div> +<form action="" method="POST"> + {% csrf_token %} + + {% if form.non_field_errors|length > 0 %} + <p class="errornote"> + Please correct the errors below. + </p> + {{ form.non_field_errors }} + {% endif %} + + <fieldset class="module aligned"> + {% for field in form %} + <div class="form-row"> + {{ field.label_tag }} + {{ field }} + {{ field.errors }} + {% if field.field.help_text %} + <p class="help"> + {{ field.field.help_text|safe }} + </p> + {% endif %} + </div> + {% endfor %} + </fieldset> + <div class="submit-row"> + <input type="submit" class="default" value="Create"> + </div> +</form> +</div> + +{% endblock %} \ No newline at end of file diff --git a/src/templates/admin/memberdb/membership_actions.html b/src/templates/admin/memberdb/membership_actions.html index 4d5c55b6260f06697a7b52f2e1f54462d599a608..ce12e156bda0e7749ef0d39781dacf4def36a385 100644 --- a/src/templates/admin/memberdb/membership_actions.html +++ b/src/templates/admin/memberdb/membership_actions.html @@ -4,5 +4,5 @@ {% if not ms.approved %} <a class="button" href="{{ member_approve }}">Approve</a> {% endif %} - <a class="button" href="{{ account_create }}">Create Account</a> + <a class="button" href="{{ create_account }}">Create Account</a> </div>