diff --git a/src/gms/settings.py b/src/gms/settings.py index 935e805fec6072609bd420d1cec5e7b11e90bb76..e4ce4ef8774f318df2a0d20883644735abd2dd9d 100644 --- a/src/gms/settings.py +++ b/src/gms/settings.py @@ -17,26 +17,27 @@ ALLOWED_HOSTS = ['127.0.0.1', 'localhost', "130.95.13.36"] # Application definition INSTALLED_APPS = ( - 'sslserver', - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'memberdb', - 'import_members', - 'squarepay', + 'sslserver', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'memberdb', + 'import_members', + 'squarepay', + 'formtools' ) MIDDLEWARE = [ - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'memberdb.views.MemberMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'memberdb.views.MemberMiddleware', ] ROOT_URLCONF = 'gms.urls' @@ -66,33 +67,33 @@ DATABASE_ROUTERS = ['import_members.db.MemberDbRouter'] # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.7/howto/static-files/ STATICFILES_DIRS = [ - os.path.join(BASE_DIR, 'static'), + os.path.join(BASE_DIR, 'static'), ] STATIC_URL = '/media/' STATIC_ROOT = os.path.join(ROOT_DIR, 'media') AUTHENTICATION_BACKENDS = [ - # see https://django-auth-ldap.readthedocs.io/en/latest for configuration info - 'django_auth_ldap.backend.LDAPBackend', - 'django.contrib.auth.backends.ModelBackend', + # see https://django-auth-ldap.readthedocs.io/en/latest for configuration info + 'django_auth_ldap.backend.LDAPBackend', + 'django.contrib.auth.backends.ModelBackend', ] # see settings_local.py for LDAP settings TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'templates')], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'templates')], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, ] TEMPLATE_DEBUG = DEBUG @@ -103,51 +104,51 @@ MESSAGE_LEVEL = message_constants.DEBUG ### Logging configuration ### import logging LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'standard': { - 'format' : "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s", - 'datefmt' : "%d/%b/%Y %H:%M:%S" - }, - }, - 'handlers': { - 'logfile': { - 'level': LOG_LEVEL, - 'class':'logging.handlers.RotatingFileHandler', - 'filename': LOG_FILENAME, - 'maxBytes': 500000, - 'backupCount': 2, - 'formatter': 'standard', - }, - 'console':{ - 'level': LOG_LEVEL, - 'class':'logging.StreamHandler', - 'formatter': 'standard' - }, - }, - 'loggers': { - 'django': { - 'handlers':['logfile', 'console'], - 'propagate': True, - 'level': LOG_LEVEL, - }, - 'django.db.backends': { - 'handlers': ['logfile', 'console'], - 'level': LOG_LEVEL, - 'propagate': False, - }, - 'django.contrib.auth': { - 'handlers': ['logfile', 'console'], - 'level': LOG_LEVEL, - }, - 'django_auth_ldap': { - 'level': LOG_LEVEL, - 'handlers': ['logfile', 'console'], - }, - 'squarepay': { - 'level': LOG_LEVEL, - 'handlers': ['logfile', 'console'], - } - }, + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'standard': { + 'format' : "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s", + 'datefmt' : "%d/%b/%Y %H:%M:%S" + }, + }, + 'handlers': { + 'logfile': { + 'level': LOG_LEVEL, + 'class':'logging.handlers.RotatingFileHandler', + 'filename': LOG_FILENAME, + 'maxBytes': 500000, + 'backupCount': 2, + 'formatter': 'standard', + }, + 'console':{ + 'level': LOG_LEVEL, + 'class':'logging.StreamHandler', + 'formatter': 'standard' + }, + }, + 'loggers': { + 'django': { + 'handlers':['logfile', 'console'], + 'propagate': True, + 'level': LOG_LEVEL, + }, + 'django.db.backends': { + 'handlers': ['logfile', 'console'], + 'level': LOG_LEVEL, + 'propagate': False, + }, + 'django.contrib.auth': { + 'handlers': ['logfile', 'console'], + 'level': LOG_LEVEL, + }, + 'django_auth_ldap': { + 'level': LOG_LEVEL, + 'handlers': ['logfile', 'console'], + }, + 'squarepay': { + 'level': LOG_LEVEL, + 'handlers': ['logfile', 'console'], + } + }, } diff --git a/src/memberdb/account.py b/src/memberdb/account.py index 64d12a162b0d93237016195ac626a481698171da..e94039e42f09f8634d123776a6d127d7126789a4 100644 --- a/src/memberdb/account.py +++ b/src/memberdb/account.py @@ -2,32 +2,31 @@ from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import render from django.urls import reverse from django.utils import timezone +from django.contrib import messages from django import forms +from formtools.wizard.views import SessionWizardView from .models import Member -from .forms import MyModelForm -from .views import MyUpdateView +from .forms import MyModelForm, MyForm +from .views import MyUpdateView, MyWizardView 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" + username = forms.SlugField( + validators=[validate_username], + max_length=19, ) - 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") - + help_text="Password must be between 10 and 127 characters long" + ) confirm_password = forms.CharField( min_length=10, max_length=127, @@ -35,41 +34,79 @@ class AccountForm(MyModelForm): 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' - } - } + fields = ['username'] + 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 EmailForm(MyModelForm): + forward = forms.BooleanField(required=False) + email_address = 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" + ) + + class Meta: + model = Member + fields = ['forward', 'email_address'] + + def clean(self): + if self['forward'].value() == True: + try: + 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: + pass + super().clean(); + +class DispenseForm(MyForm): + pin = forms.CharField( + min_length=0, + max_length=4, + widget=forms.PasswordInput, + strip=False, + help_text="PIN must be 4 digits long") + + confirm_pin = forms.CharField( + min_length=0, + max_length=4, + widget=forms.PasswordInput, + strip=False, + ) + def clean(self): + try: + if len(self['pin'].value()) != 4 : + self.add_error('pin', 'PIN must be excatly 4 digits.') + if not self['pin'].value().isdigit(): + self.add_error('pin', 'PIN can only contain numbers.') + if (self['pin'].value() != self['confirm_pin'].value()): + self.add_error('confirm_pin', 'PINs must match.') + except: + pass + super().clean(); + -class AccountView(MyUpdateView): +class AccountView(MyWizardView): + form_list = [AccountForm,EmailForm,DispenseForm] template_name = 'admin/memberdb/account_create.html' - form_class = AccountForm - model = Member - pk_url_kwarg = 'object_id' admin = None + def get_form_instance(self, step): + return self.object + def get_context_data(self, **kwargs): - m = self.get_object() + m = self.object context = super().get_context_data(**kwargs) context.update(self.admin.admin_site.each_context(self.request)) context.update({ @@ -78,7 +115,19 @@ class AccountView(MyUpdateView): }) 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")) + + def done(self, form_list, form_dict, **kwargs): + messages.success(self.request, 'Your membership renewal has been submitted.') + return HttpResponseRedirect(reverse("admin:memberdb_membership_changelist")) + + #return accountProgressView(self.request, m) + + +def accountProgressView(request, member): + return + + + + +def accountFinalView(): + return render(request, 'accountfinal.html', context) diff --git a/src/memberdb/account_backend.py b/src/memberdb/account_backend.py index 4883b204b82536be9d86af0133fbddcdc46e0246..3f795ed6c41d2a3a82bb054c021b326adf6637dd 100644 --- a/src/memberdb/account_backend.py +++ b/src/memberdb/account_backend.py @@ -7,6 +7,11 @@ from django.utils.translation import gettext_lazy as _ import ldap import re +import socket + +import subprocess +from subprocess import CalledProcessError, TimeoutExpired + import memberdb.models from datetime import date from squarepay import dispense @@ -16,53 +21,90 @@ log = logging.getLogger('ldap') # load config ldap_uri = getattr(settings, 'AUTH_LDAP_SERVER_URI') -ldap_search_dn = getattr(settings, 'AUTH_LDAP_USER_DN_TEMPLATE') -#ldap_bind_dn = getattr() -#ldap_bind_secret = getattr() +ldap_search_dn = getattr(settings, 'LDAP_USER_SEARCH_DN') +ldap_bind_dn = getattr(settings, 'LDAP_BIND_DN') +ldap_bind_secret = getattr(settings, 'LDAP_BIND_SECRET') +ldap_opts = getattr(settings, 'AUTH_LDAP_GLOBAL_OPTIONS') #initalise ldap instace _ldap_inst = ldap.initialize(ldap_uri) +for option,value in ldap_opts.items(): + _ldap_inst.set_option(option,value) + +_ldap_inst.set_option(ldap.OPT_X_TLS_NEWCTX, 0) + def get_ldap_instance(): + try: + _ldap_inst.bind(ldap_bind_dn, ldap_bind_secret) + except ldap.INVALID_CREDENTIALS: + log.error("LDAP: Invalid bind credentials") + except ldap.SERVER_DOWN: + log.error("LDAP: Cannot Contact LDAP server") + return _ldap_inst def get_user_attrs(username, attrs): # TODO verify bind ld = get_ldap_instance() filter = "cn=" + username - try: - result = ld.search_s(ldap_search_dn, ldap.SCOPE_SUBTREE, filter, attrs) - if result.size > 1: - # multiple accounts matched, this is a problem - return None - if result.size == 0: - # account does not exist - return None - return result[0]; - - except: - return None + + result = ld.search_s(ldap_search_dn, ldap.SCOPE_SUBTREE, filter, attrs) + if len(result) > 1: + # multiple accounts matched, this is a problem + return ldap.NO_UNIQUE_ENTRY + if len(result) == 0: + return ldap.NO_SUCH_OBJECT + return result[0]; + def get_account_lock_status(username): ld = get_ldap_instance() try: - ld.bind(ldap_bind_dn, ldap_bind_secret) result = get_user_attrs(username, ['userAccountControl']) finally: ld.unbind() return bool(result[1]['userAccountControl'] & 0x002) -def validate_username(value): +def validate_username(value : str): + # note: slug validator ensures that username only contains [a-z0-9_-] # usernames can't begin with a numeric - if re.match(r"^\d.*", value): - log.info("test") + if not value[0].isalpha(): + raise ValidationError( + _('Username must begin with a letter') + ) + # ensure username is lowercase + elif not value.islower(): raise ValidationError( - _('Username cannot begin with a number'), - params={'value': value} + _('Username cannot contain uppercase characters') ) - else: - return value + # check if the user exists, this test should catch *most* cases + if subprocess.call(["id", value]) == 0: + raise ValidationError(_('Username already taken (passwd)')) + + # usernames cannot conflict with hostnames + try: + socket.gethostbyname(value) + raise ValidationError( + _('Username already taken (CNAME)') + ) + except socket.gaierror: + pass + + # lookup user in ldap, required because not all users are mapped to *nix users + try: + if get_user_attrs(value, ['cn']) != ldap.NO_SUCH_OBJECT: + raise ValidationError( + _('Username already taken (AD)') + ) + except ldap.LDAPError: + log.error("Network error, cannot verify username") + raise ldap.OPERATIONS_ERROR + + + + # locks the specified User Account by performing the following actions: # 1. set UAC ACCOUNTDISABLE flag (0x002) via ldap @@ -73,7 +115,6 @@ def lock_account(username): ld = get_ldap_instance() today = date.today() try: - ld.bind(ldap_bind_dn, ldap_bind_secret) # fetch current uac result = get_user_attrs(username, ['userAccountControl']) @@ -97,7 +138,6 @@ def unlock_account(username): ld = get_ldap_instance() today = date.today() try: - ld.bind(ldap_bind_dn, ldap_bind_secret) # fetch current uac result = get_user_attrs(username, ['userAccountControl']) @@ -118,24 +158,21 @@ def unlock_account(username): # Account creation steps: # -def create_account(member): +def create_account(member, passwd): username = "changeme"; log.info("I: creating new account for %s (%s %s)") - - # prepend student numbers with 'sn' - if re.fullmatch(r"^2\d{7}$", username): - log.info("I: username is a student number, adding sn prefix") - username = sn + username - - # usernames can't begin with a numeric - if re.match(r"^\d", username): - log.error("E: The username %s cannot start with a digit." % username) - return; - return None; +def create_homes(member): + return +def set_email_forwarding(member, addr): + return +def subscribe_to_list(member): + return +def set_pin(member, pin): + return diff --git a/src/memberdb/admin.py b/src/memberdb/admin.py index 413fe0edfec2a2977d6261b3d204f20ed01d942b..060ce356b3d0547a6c5fd9c4ac2e3530f81362f9 100644 --- a/src/memberdb/admin.py +++ b/src/memberdb/admin.py @@ -73,8 +73,14 @@ class MemberAdmin(admin.ModelAdmin): 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) + inst = Member.objects.get(pk=kwargs['object_id']) + model_dict = { + '0': inst, + '1': inst + } + return AccountView.as_view(object=inst,admin=self)(request, *args, **kwargs) diff --git a/src/memberdb/forms.py b/src/memberdb/forms.py index 5f6657a42d5e55a05e43335673198313d537f80f..f9ff1d4bc546436633f348923d48a90a5f6c40d1 100644 --- a/src/memberdb/forms.py +++ b/src/memberdb/forms.py @@ -9,6 +9,13 @@ class MyModelForm(forms.ModelForm): # this must be passed by kwargs upon instantiating the form request = None + def __init__(self, *args, **kwargs): + self.request = kwargs.pop("request") + super().__init__(*args, **kwargs) +class MyForm(forms.Form): + # this must be passed by kwargs upon instantiating the form + request = None + def __init__(self, *args, **kwargs): self.request = kwargs.pop("request") super().__init__(*args, **kwargs) diff --git a/src/memberdb/views.py b/src/memberdb/views.py index a1c067039bec3e19244e17bb18f195a29961d801..ae6c507f96044fe8b16253b78a76fd9730f2e5b1 100644 --- a/src/memberdb/views.py +++ b/src/memberdb/views.py @@ -9,6 +9,7 @@ from django.views.generic.base import View from django.views.generic.edit import UpdateView from django.contrib.auth.mixins import AccessMixin from django.utils import timezone +from formtools.wizard.views import SessionWizardView from .models import Member, IncAssocMember, Membership, MEMBERSHIP_TYPES, TokenConfirmation from .forms import MemberHomeForm @@ -80,6 +81,25 @@ class MyUpdateView(UpdateView): kwargs.update({'request': self.request}) return kwargs +class MyWizardView(SessionWizardView): + object = None + + def get_object(self): + if (not self.object is None): + return self.object + try: + sobj = super().get_object() + if (not sobj is None): + return sobj + except: + pass + return None + + def get_form_kwargs(self, step, **kwargs): + kwargs.update(super().get_form_kwargs()) + kwargs.update({'request': self.request}) + return kwargs + class MemberHomeView(MemberAccessMixin, MyUpdateView): model = Member template_name = 'home.html' diff --git a/src/static/account_form.js b/src/static/account_form.js index 149bd13edff15de187cb6e733f812902b7e41920..4460b2b5312d059de71407396b026a03987f9148 100644 --- a/src/static/account_form.js +++ b/src/static/account_form.js @@ -1 +1,13 @@ -var conditional_fields = $("div"); \ No newline at end of file + +var conditional_fields = $("#1-email_address"); +if (!$("#id_1-forward").prop('checked') === true) { + conditional_fields.hide(); +} + +$("#id_1-forward").change(function() { + if ($(this).prop('checked') === true) { + conditional_fields.show(); + } else { + conditional_fields.hide(); + } +}); \ No newline at end of file diff --git a/src/static/shared.css b/src/static/shared.css index dc9e6ceb24e1fcbb0c03e83f3fa8938e690ae982..f138c8f769bbf51e74c5249e8de59a73337c504e 100644 --- a/src/static/shared.css +++ b/src/static/shared.css @@ -115,7 +115,7 @@ ul.messagelist li.error { /* FORM BUTTONS */ -.button, input[type=submit], input[type=button], .submit-row input, a.button { +.button, input[type=submit], input[type=button], .submit-row input,.submit-row button, a.button { background: #79aec8; padding: 10px 15px; border: none; diff --git a/src/templates/admin/memberdb/account_create.html b/src/templates/admin/memberdb/account_create.html index c8e1724a0ccde01b8634e0f9f2cf9466c941f9c5..bd13750cba83724f31e2559e17499a70be50eb71 100644 --- a/src/templates/admin/memberdb/account_create.html +++ b/src/templates/admin/memberdb/account_create.html @@ -1,39 +1,62 @@ {% extends "admin/change_form.html" %} -{% load i18n admin_static admin_modify %} +{% load i18n admin_static admin_modify admin_urls %} + +{% load static %} + +{% block extrahead %} +{{ block.super }} +{# This makes use of some hacky javascript to replace the default "actions" dropdown list with more user-friendly buttons for each action. #} +<script type="text/javascript" src="{% static 'admin/js/vendor/jquery/jquery.js' %}"></script> +{% endblock %} + {% 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> + <div class="ms-approve-summary"> + {% include "admin/memberdb/membership_summary.html" %} + </div> <form action="" method="POST"> - {% csrf_token %} + {{ wizard.management_form }} + {% csrf_token %} + + {% if form.non_field_errors|length > 0 %} + <p class="errornote"> + Please correct the errors below. + </p> + {{ form.non_field_errors }} + {% endif %} - {% if form.non_field_errors|length > 0 %} - <p class="errornote"> - Please correct the errors below. - </p> - {{ form.non_field_errors }} - {% endif %} + {{ wizard.form.management_form }} + <fieldset class="module aligned"> + {% for field in wizard.form %} + <div class="form-row" id="{{ field.html_name}}"> + {{ 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"> + {% if wizard.steps.prev %} + <button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}"> prev </button> + {% endif %} + + {% if wizard.steps.next %} + <input type="submit" value="{% trans "next" %}"/> + {% else %} + <input type="submit" value="{% trans "finish" %}"/> + {%endif%} + </div> - <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> + {% if wizard.steps.index == 1 %} + <script type="text/javascript" src="{% static 'account_form.js' %}"></script> + {% endif %} </form> </div>