From b5eaa6b69c7b92d62dc3fa6d9deb0d3c7d33cfad Mon Sep 17 00:00:00 2001 From: Flavio Tarsetti <flavio.tarsetti@idiap.ch> Date: Tue, 11 Jul 2017 17:37:30 +0200 Subject: [PATCH] [accounts/ui-registration/ui-views-urls/utils-management] added view to handle blocked users requests for supervision and management to cleanup expired requests --- beat/web/accounts/api.py | 17 ++- .../clean_blocked_users_expired_requests.py | 102 ++++++++++++++++ beat/web/ui/registration/forms.py | 69 +++++++++++ .../registration/blocked_user_reactivate.html | 82 +++++++++++++ .../templates/registration/login.html | 2 +- ...idation_supervisee_add_request.message.txt | 2 +- ...cked_state_wait_for_activation.message.txt | 14 +++ ...rvisee_blocked_validation_wait.subject.txt | 1 + beat/web/ui/urls.py | 5 + beat/web/ui/views.py | 114 ++++++++++++++++++ .../management/commands/daily_cron_actions.py | 3 + 11 files changed, 403 insertions(+), 8 deletions(-) create mode 100644 beat/web/accounts/management/commands/clean_blocked_users_expired_requests.py create mode 100644 beat/web/ui/registration/templates/registration/blocked_user_reactivate.html create mode 100644 beat/web/ui/registration/templates/registration/mail.supervisee_blocked_state_wait_for_activation.message.txt create mode 100644 beat/web/ui/registration/templates/registration/mail.supervisee_blocked_validation_wait.subject.txt diff --git a/beat/web/accounts/api.py b/beat/web/accounts/api.py index e60396eb4..01cd2f427 100644 --- a/beat/web/accounts/api.py +++ b/beat/web/accounts/api.py @@ -93,8 +93,10 @@ class GodfatherListView(generics.ListAPIView): #A godfather can validate an account of: #1) a new user requesting validation #2) an existing validated user rejected by a previous supervisor - #On both cases check the current key in supervisee profile match the supervisiontrack key as this is the current supervision request/track from the supervisee - queryset = SupervisionTrack.objects.filter(godfather=request.user).filter(Q(supervisee__profile__status=Profile.WAITINGVALIDATION)|Q(supervisee__profile__status=Profile.REJECTED)|Q(supervisee__profile__status=Profile.ACCEPTED)).filter(Q(supervisee__profile__supervision_key=models.F('supervision_key'))) + #3) an existing validated user requesting a change of supervisor + #4) a blocked user requesting a supervision + #On all cases check the current key in supervisee profile match the supervisiontrack key as this is the current supervision request/track from the supervisee + queryset = SupervisionTrack.objects.filter(godfather=request.user).filter(Q(supervisee__profile__status=Profile.WAITINGVALIDATION)|Q(supervisee__profile__status=Profile.REJECTED)|Q(supervisee__profile__status=Profile.ACCEPTED)|Q(supervisee__profile__status=Profile.BLOCKED)).filter(Q(supervisee__profile__supervision_key=models.F('supervision_key'))) serializer = FullSupervisionTrackSerializer(queryset, many=True, context ={'request': request}) return Response(serializer.data) @@ -215,14 +217,17 @@ class GodfatherRemoveSuperviseeView(BaseUpdateSupervisionTrackView): now = datetime.datetime.now() expiration_date_delta = datetime.timedelta(days=settings.ACCOUNT_BLOCKAGE_AFTER_FIRST_REJECTION_DAYS) - supervisiontrack.expiration_date = now supervisiontrack.is_valid = False - supervisee.profile.status = Profile.REJECTED + if supervisee.profile.status != Profile.BLOCKED: + supervisee.profile.status = Profile.REJECTED + if supervisee.profile.rejection_date == None: + supervisee.profile.rejection_date = now + expiration_date_delta + else: + supervisee.profile.rejection_date = None + supervisee.profile.supervision_key = None - if supervisee.profile.rejection_date == None: - supervisee.profile.rejection_date = now + expiration_date_delta supervisiontrack.save() supervisee.profile.save() diff --git a/beat/web/accounts/management/commands/clean_blocked_users_expired_requests.py b/beat/web/accounts/management/commands/clean_blocked_users_expired_requests.py new file mode 100644 index 000000000..98cdeb8fc --- /dev/null +++ b/beat/web/accounts/management/commands/clean_blocked_users_expired_requests.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 : +# encoding: utf-8 + +############################################################################### +# # +# Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ # +# Contact: beat.support@idiap.ch # +# # +# This file is part of the beat.web module of the BEAT platform. # +# # +# Commercial License Usage # +# Licensees holding valid commercial BEAT licenses may use this file in # +# accordance with the terms contained in a written agreement between you # +# and Idiap. For further information contact tto@idiap.ch # +# # +# Alternatively, this file may be used under the terms of the GNU Affero # +# Public License version 3 as published by the Free Software and appearing # +# in the file LICENSE.AGPL included in the packaging of this file. # +# The BEAT platform is distributed in the hope that it will be useful, but # +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # +# or FITNESS FOR A PARTICULAR PURPOSE. # +# # +# You should have received a copy of the GNU Affero Public License along # +# with the BEAT platform. If not, see http://www.gnu.org/licenses/. # +# # +############################################################################### + + +from django.core.management.base import BaseCommand, CommandError + +from datetime import date +import datetime + +from django.contrib.auth.models import User +from django.conf import settings + +from ...models import SupervisionTrack +from ...models import Profile +from ....ui.registration.models import RegistrationProfile + +import sys +import random + +class Command(BaseCommand): + + help = 'Clean blocked users expired requests for supervisor validation' + + def add_arguments(self, parser): + parser.add_argument('--noinput', action='store_false', dest='interactive', default=False, + help=('Tells Django to NOT prompt the user for input of any kind.')) + + + def handle(self, *args, **options): + block = True + + if options['interactive']: + try: + answer = self.get_input_data('Clean blocked user(s) that have not been validated by a supervisor? (y/n)? ', 'y').lower() + except KeyboardInterrupt: + self.stderr.write("\nOperation canceled.") + sys.exit(1) + + if answer != 'y': + self.stdout.write('Clean blocked users operation canceled') + sys.exit(1) + + if block: + blocked_profiles = Profile.objects.filter(status=Profile.BLOCKED) + count = 0 + for blocked_profile in blocked_profiles: + user = blocked_profile.user + if user.profile.rejection_date is not None: + if user.profile.rejection_date < datetime.datetime.now(): + count +=1 + user.profile.status = Profile.BLOCKED + user.profile.rejection_date = None + user.is_active = False + + if user.profile.supervision_key != None: + supervisiontrack = SupervisionTrack.objects.get(supervision_key=blocked_profile.supervision_key) + if supervisiontrack.is_valid == False and supervisiontrack.start_date is None: + user.profile.supervision_key = None + supervisiontrack.delete() + + user.profile.save() + user.save() + + self.stdout.write('{} Blocked user(s) successfully cleaned from expired supervision/'.format(count) + '{} Total user(s) checked'.format(blocked_profiles.count())) + + + def get_input_data(self, message, default=None): + """ + Override this method if you want to customize data inputs or + validation exceptions. + """ + raw_value = raw_input(message) + + if default and raw_value == '': + raw_value = default + + return raw_value diff --git a/beat/web/ui/registration/forms.py b/beat/web/ui/registration/forms.py index a2ed0c97e..26e7390d4 100644 --- a/beat/web/ui/registration/forms.py +++ b/beat/web/ui/registration/forms.py @@ -250,3 +250,72 @@ class RegistrationFormNoFreeEmail(RegistrationForm): if email_domain in self.bad_domains: raise forms.ValidationError(_(u'Registration using free email addresses is prohibited. Please supply a different email address.')) return self.cleaned_data['email'] + + +class BlockedUserRevalidationForm(forms.Form): + """ + Form for registering a new user account. + + Validates that the requested username is not already in use, and + requires the password to be entered twice to catch typos. + + Subclasses should feel free to add any additional validation they + need, but should either preserve the base ``save()`` or implement + a ``save()`` method which returns a ``User``. + + """ + + username = forms.RegexField(regex=r'^\w+$', + max_length=30, + widget=forms.TextInput(attrs=attrs_dict), + label=_(u'Username')) + password = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict, render_value=False), + label=_(u'Password')) + godfather = forms.RegexField(regex=r'^\w+$', + max_length=30, + widget=forms.TextInput(attrs=attrs_dict), + label=_(u'Godfather Username (Your account needs to be re-validated by a recognized person)')) + + + def clean_username(self): + """ + Validate that the username is alphanumeric and user exists + + """ + try: + user = User.objects.get(username__iexact=self.cleaned_data['username']) + return self.cleaned_data['username'] + except User.DoesNotExist: + raise forms.ValidationError(_(u'This username has not been recognized')) + + def clean_godfather(self): + """ + Validate that the username is alphanumeric and exists. + + """ + try: + user = User.objects.get(username__iexact=self.cleaned_data['godfather']) + except User.DoesNotExist: + raise forms.ValidationError(_(u'This godfather username does not exist. Please choose another.')) + + if not user.profile.is_godfather: + raise forms.ValidationError(_(u'This user is not a recognized godfather. Please choose another.')) + + return self.cleaned_data['godfather'] + + + def clean(self): + """ + Verifiy that the values entered into the two password fields + match. Note that an error here will end up in + ``non_field_errors()`` because it doesn't apply to a single + field. + + """ + if 'password1' in self.cleaned_data and 'password2' in self.cleaned_data: + if self.cleaned_data['password1'] != self.cleaned_data['password2']: + self._errors["password2"] = self.error_class([_(u'You must type the same password each time')]) + del self.cleaned_data["password1"] + del self.cleaned_data["password2"] + + return self.cleaned_data diff --git a/beat/web/ui/registration/templates/registration/blocked_user_reactivate.html b/beat/web/ui/registration/templates/registration/blocked_user_reactivate.html new file mode 100644 index 000000000..91e7f88c4 --- /dev/null +++ b/beat/web/ui/registration/templates/registration/blocked_user_reactivate.html @@ -0,0 +1,82 @@ +{% extends "base.html" %} +{% comment %} + * Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ + * Contact: beat.support@idiap.ch + * + * This file is part of the beat.web module of the BEAT platform. + * + * Commercial License Usage + * Licensees holding valid commercial BEAT licenses may use this file in + * accordance with the terms contained in a written agreement between you + * and Idiap. For further information contact tto@idiap.ch + * + * Alternatively, this file may be used under the terms of the GNU Affero + * Public License version 3 as published by the Free Software and appearing + * in the file LICENSE.AGPL included in the packaging of this file. + * The BEAT platform is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero Public License along + * with the BEAT platform. If not, see http://www.gnu.org/licenses/. +{% endcomment %} + +{% load registration_tags %} + +{% block content %} + +<div class="row"> + <div class="col-sm-4 col-sm-offset-4"> + + <div class="panel panel-default login-panel"> + + <div class="panel-heading">Account Reactivation</div> + + <div class="panel-body"> + + <form method="post" action="" class="form"> + {% csrf_token %} + + {% for error in form.non_field_errors %} + <div class="alert alert-danger" role="alert">{{ error }}</div> + {% endfor %} + + <div class="form-group{% if form.username.errors %} has-error{% endif %}"> + {{ form.username.label_tag }} + {{ form.username|tabindex:1|addclass:"form-control" }} + {% for error in form.username.errors %} + <div class="alert alert-danger" role="alert">{{ error }}</div> + {% endfor %} + </div> + + <div class="form-group{% if form.password.errors %} has-error{% endif %}"> + {{ form.password.label_tag }} + {{ form.password|tabindex:2|addclass:"form-control" }} + {% for error in form.password.errors %} + <div class="alert alert-danger" role="alert">{{ error }}</div> + {% endfor %} + </div> + + <div class="form-group{% if form.godfather.errors %} has-error{% endif %}"> + {{ form.godfather.label_tag }} + {{ form.godfather|tabindex:2|addclass:"form-control" }} + {% for error in form.godfather.errors %} + <div class="alert alert-danger" role="alert">{{ error }}</div> + {% endfor %} + </div> + + <div class="form-group"> + <button type="submit" class="btn btn-success" tabindex="3"><i class="fa fa-plug fa-fw fa-lg"></i> Reactivation request</button> + </div> + + </form> + + <p class="comment">In order to sign in, you must be <a href="{% url 'registration' %}">registered</a></p> + <p class="comment">Already registered and not blocked? <a href="{% url 'login' %}">login</a></p> + </div> + </div> + + </div> +</div> + +{% endblock content %} diff --git a/beat/web/ui/registration/templates/registration/login.html b/beat/web/ui/registration/templates/registration/login.html index bf265cdad..a08be74b7 100644 --- a/beat/web/ui/registration/templates/registration/login.html +++ b/beat/web/ui/registration/templates/registration/login.html @@ -64,7 +64,7 @@ </form> <p class="comment">In order to sign in, you must be <a href="{% url 'registration' %}">registered</a></p> - + <p class="comment">Inactive/Blocked user need to get their account reactivated <a href="{% url 'blocked_user_reactivation' %}">reactivation</a></p> </div> </div> diff --git a/beat/web/ui/registration/templates/registration/mail.godfather_validation_supervisee_add_request.message.txt b/beat/web/ui/registration/templates/registration/mail.godfather_validation_supervisee_add_request.message.txt index d490fbc6e..5c9eb2cd7 100644 --- a/beat/web/ui/registration/templates/registration/mail.godfather_validation_supervisee_add_request.message.txt +++ b/beat/web/ui/registration/templates/registration/mail.godfather_validation_supervisee_add_request.message.txt @@ -2,7 +2,7 @@ Dear {{ supervisor.first_name }}, Thank you in advance for validating the request of one your supervisee at the Idiap Research Institute's Biometric Evaluation and Testing (BEAT) platform. Before we can activate this -supervision requesti, you must login to your account and under supervision tab validate the following supervisee: +supervision request, you must login to your account and under supervision tab validate the following supervisee: First Name: {{ supervisee.first_name }} Last Name: {{ supervisee.last_name }} diff --git a/beat/web/ui/registration/templates/registration/mail.supervisee_blocked_state_wait_for_activation.message.txt b/beat/web/ui/registration/templates/registration/mail.supervisee_blocked_state_wait_for_activation.message.txt new file mode 100644 index 000000000..0473236bd --- /dev/null +++ b/beat/web/ui/registration/templates/registration/mail.supervisee_blocked_state_wait_for_activation.message.txt @@ -0,0 +1,14 @@ +Dear {{ supervisee.first_name }}, + +Your account is currently blocked due to a lack of supervisor. + +We acknowledge your request made to re-activate your account via a supervision request to {{ supervisor.username }}. + +You will be informed if your supervisor has accepted or rejected your request. + +If no information is given by your supervisor, you will be allowed to make a new supervision request after a week. + +If you are having problems to activate your account, contact a member of our +support staff at {{ prefix }}{% url 'contact' %}. + +BEAT Administrators at the Idiap Research Institute diff --git a/beat/web/ui/registration/templates/registration/mail.supervisee_blocked_validation_wait.subject.txt b/beat/web/ui/registration/templates/registration/mail.supervisee_blocked_validation_wait.subject.txt new file mode 100644 index 000000000..1c53371c0 --- /dev/null +++ b/beat/web/ui/registration/templates/registration/mail.supervisee_blocked_validation_wait.subject.txt @@ -0,0 +1 @@ +Blocked Account re-validation - Supervision request confirmation diff --git a/beat/web/ui/urls.py b/beat/web/ui/urls.py index 75dcf8a9e..960b0c3ad 100644 --- a/beat/web/ui/urls.py +++ b/beat/web/ui/urls.py @@ -48,6 +48,11 @@ urlpatterns = [ name='login', ), + url(r'^blocked_user_reactivation/$', + views.blocked_user_reactivation, + name='blocked_user_reactivation', + ), + url(r'^events/(?P<author_name>\w+)/$', views.activity_stream, name='activity-stream', diff --git a/beat/web/ui/views.py b/beat/web/ui/views.py index 07bbef7f5..5f4500fd8 100644 --- a/beat/web/ui/views.py +++ b/beat/web/ui/views.py @@ -29,6 +29,8 @@ from django.shortcuts import get_object_or_404 from django.shortcuts import render_to_response from django.template import RequestContext +from django.template import loader +from django.template import Context from django.contrib.auth.views import login as django_login from django.contrib.auth.forms import PasswordChangeForm from django.contrib.auth.models import User @@ -45,6 +47,11 @@ from rest_framework.authtoken.models import Token from ..import __version__ from ..common.models import Shareable +from registration.forms import BlockedUserRevalidationForm +from ..accounts.models import Profile, SupervisionTrack + +import datetime +from urlparse import urlparse import logging logger = logging.getLogger(__name__) @@ -71,6 +78,113 @@ def login(request): return response +def blocked_user_reactivation(request): + '''Reactivation page''' + + if request.method == "POST": + form = BlockedUserRevalidationForm(request.POST) + if form.is_valid(): + try: + user = User.objects.get(username=request.POST["username"]) + if user.check_password(request.POST["password"]): + # Check if user is a blocked user + if user.profile.status == Profile.BLOCKED: + godfather = User.objects.get(username=request.POST["godfather"]) + # Check the godfather + if godfather.profile.status == Profile.ACCEPTED: + # Check if supervision track already exists + if user.profile.supervision_key is None: + # Create and assign key + supervisee = user + supervisee.profile.supervision_key = supervisee.profile._generate_current_supervision_key() + supervisiontrack = SupervisionTrack.objects.create( + supervisee = supervisee, + godfather = godfather, + is_valid = False, + ) + + # Assign key to supervision track + supervisiontrack.supervision_key = supervisee.profile.supervision_key + supervisiontrack.save() + supervisee.profile.supervision.add(supervisiontrack) + # Add a rejection date to the supervisee profile + now = datetime.datetime.now() + expiration_date_delta = datetime.timedelta(days=settings.ACCOUNT_ACTIVATION_DAYS_FROM_GODFATHER) + if supervisee.profile.rejection_date == None: + supervisee.profile.rejection_date = now + expiration_date_delta + + supervisee.profile.save() + supervisee.save() + + #Inform by email the supervisor that he has a new supervisee request + from django.core.mail import send_mail + + parsed_url = urlparse(settings.URL_PREFIX) + server_address = '%s://%s' % (parsed_url.scheme, parsed_url.hostname) + + c = Context({ 'supervisor': godfather, + 'supervisee': supervisee, + 'prefix': server_address, + }) + + try: + t = loader.get_template('registration/mail.godfather_validation.subject.txt') + subject = t.render(c) + + # Note: e-mail subject *must not* contain newlines + subject = settings.EMAIL_SUBJECT_PREFIX + ''.join(subject.splitlines()) + + t = loader.get_template('registration/mail.godfather_validation_supervisee_add_request.message.txt') + message = t.render(c) + + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [godfather.email]) + except: + pass + + # inform the supervisee of his request + c = Context({ 'supervisor': godfather, + 'supervisee': supervisee, + 'prefix': server_address, + }) + + try: + t = loader.get_template('registration/mail.supervisee_blocked_validation_wait.subject.txt') + subject = t.render(c) + + # Note: e-mail subject *must not* contain newlines + subject = settings.EMAIL_SUBJECT_PREFIX + ''.join(subject.splitlines()) + + t = loader.get_template('registration/mail.supervisee_blocked_state_wait_for_activation.message.txt') + message = t.render(c) + + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [supervisee.email]) + except: + pass + + messages.success(request, "Your supervision request has been successfully processed.") + else: + messages.error(request, "A supervision request already exists for this account, you need to wait for your supervisor's decision.") + else: + messages.error(request, "The selected godfather is not valid.") + else: + path = request.GET.get('next', '/') + messages.error(request, "Your profile is not blocked, you can go to the login page instead.") + else: + # Don't give too much details though we know the problem is the password only at this step + messages.error(request, "Wrong user or password combination!") + except User.DoesNotExist: + # Don't give too much details though we know the problem is the password only at this step + messages.error(request, "Wrong user or password combination!") + pass + + #return redirect('blocked_user_reactivation', pk=post.pk) + else: + form = BlockedUserRevalidationForm() + + return render_to_response('registration/blocked_user_reactivate.html', {'form': form}, + context_instance=RequestContext(request)) + + def gather_contributions(requestor, author): '''Gather contributions that are accessible to a certain requestor''' diff --git a/beat/web/utils/management/commands/daily_cron_actions.py b/beat/web/utils/management/commands/daily_cron_actions.py index b2d7a7baa..dcbcf13a5 100644 --- a/beat/web/utils/management/commands/daily_cron_actions.py +++ b/beat/web/utils/management/commands/daily_cron_actions.py @@ -58,3 +58,6 @@ class Command(BaseCommand): # Block rejected users with no supervision after specific rejection date call_command('block_rejected_users') + + # Clean blocked users with no supervision after specific rejection date + call_command('clean_blocked_users_expired_requests') -- GitLab