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