From c8ed258f0605ecb651f2e8df44e7d501ddff7d95 Mon Sep 17 00:00:00 2001
From: Flavio Tarsetti <flavio.tarsetti@idiap.ch>
Date: Tue, 27 Jun 2017 16:52:23 +0200
Subject: [PATCH] [accounts/ui/utils/navigation] added remove supervisee
 endpoint for godfather + management to block users + patched key relation for
 supervisee + started new endpoint to make request for supervision

---
 beat/web/accounts/api.py                      | 194 +++++++++++++++++-
 beat/web/accounts/api_urls.py                 |  11 +
 .../commands/block_rejected_users.py          | 101 +++++++++
 .../commands/clean_invalid_users.py           |   2 +-
 .../0007_profile_first_rejection_date.py      |  20 ++
 .../migrations/0008_auto_20170626_1516.py     |  20 ++
 .../migrations/0009_auto_20170627_0956.py     |  22 ++
 beat/web/accounts/models.py                   |   3 +-
 beat/web/navigation/admin.py                  |   4 +
 beat/web/settings/settings.py                 |   1 +
 .../mail.godfather_rejected.message.txt       |  17 ++
 .../mail.godfather_rejected.subject.txt       |   1 +
 ...father_rejected_delete_account.message.txt |   9 +
 .../management/commands/daily_cron_actions.py |   3 +
 14 files changed, 405 insertions(+), 3 deletions(-)
 create mode 100644 beat/web/accounts/management/commands/block_rejected_users.py
 create mode 100644 beat/web/accounts/migrations/0007_profile_first_rejection_date.py
 create mode 100644 beat/web/accounts/migrations/0008_auto_20170626_1516.py
 create mode 100644 beat/web/accounts/migrations/0009_auto_20170627_0956.py
 create mode 100644 beat/web/ui/registration/templates/registration/mail.godfather_rejected.message.txt
 create mode 100644 beat/web/ui/registration/templates/registration/mail.godfather_rejected.subject.txt
 create mode 100644 beat/web/ui/registration/templates/registration/mail.godfather_rejected_delete_account.message.txt

diff --git a/beat/web/accounts/api.py b/beat/web/accounts/api.py
index ae449df78..cfa2c90db 100644
--- a/beat/web/accounts/api.py
+++ b/beat/web/accounts/api.py
@@ -52,12 +52,13 @@ from .models import SupervisionTrack, Profile
 from ..common.models import Shareable
 from ..common.exceptions import ShareError
 from ..common.mixins import CommonContextMixin
+from ..ui.registration.models import RegistrationProfile
 
 from itertools import chain
 from datetime import datetime, timedelta
 
 
-from .permissions import IsGodfatherAndAuthor
+from .permissions import IsGodfatherAndAuthor, IsAuthorAndNotGodfather
 
 from ..common.responses import BadRequestResponse, ForbiddenResponse
 
@@ -168,3 +169,194 @@ class GodfatherAddSuperviseeView(BaseUpdateSupervisionTrackView):
 
 
 #----------------------------------------------------------
+
+
+class GodfatherRemoveSuperviseeView(BaseUpdateSupervisionTrackView):
+    permission_classes = BaseUpdateSupervisionTrackView.permission_classes
+
+    def put(self, request, supervisee_name):
+        supervisee = User.objects.get(username=supervisee_name)
+        supervisiontrack = SupervisionTrack.objects.get(godfather=request.user, supervisee=supervisee, supervisee__profile__supervision_key=models.F('supervision_key'))
+
+        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': supervisiontrack.godfather,
+                      'supervisee': supervisee,
+                      'prefix': server_address,
+                    })
+
+        if supervisee.profile.status == Profile.WAITINGVALIDATION:
+            #New user account waiting validation, so delete this account and inform by email the user
+            try:
+                t = loader.get_template('registration/mail.godfather_rejected.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_rejected_delete_account.message.txt')
+                message = t.render(c)
+
+                send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [supervisee.email])
+            except:
+                pass
+
+            registration_profile = RegistrationProfile.objects.get(user=supervisee)
+            supervisee.profile.delete()
+            supervisee.delete()
+            supervisiontrack.delete()
+            registration_profile.delete()
+        else:
+            #Reject this account and inform by email the user
+            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
+            supervisee.profile.supervision_key = None
+            if supervisee.profile.rejection_date == None:
+                supervisee.profile.rejection_date = now + expiration_date_delta
+
+            supervisiontrack.save()
+            supervisee.profile.save()
+            supervisee.save()
+
+            try:
+                t = loader.get_template('registration/mail.godfather_rejected.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_rejected.message.txt')
+                message = t.render(c)
+
+                send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [supervisee.email])
+            except:
+                pass
+
+            #New supervision request refused
+            if supervisiontrack.start_date is None:
+                supervisiontrack.delete()
+
+        return Response(status=status.HTTP_204_NO_CONTENT)
+
+
+##----------------------------------------------------------
+#
+#
+#class BaseCreateSupervisionTrackViewSupervisee(generics.CreateAPIView):
+#    model = SupervisionTrack
+#    serializer_class = SupervisionTrackUpdateSerializer
+#
+#    def get_permissions(self):
+#        permission_classes = [permissions.IsAuthenticated, IsAuthorAndNotGodfather]
+#
+#        self.permission_classes = permission_classes
+#
+#        return super(BaseCreateSupervisionTrackViewSupervisee, self).get_permissions()
+#
+#
+##----------------------------------------------------------
+#
+#
+#class SuperviseeAddGodfatherView(BaseCreateSupervisionTrackViewSupervisee):
+#    permission_classes = BaseCreateSupervisionTrackViewSupervisee.permission_classes
+#
+#    def post(self, request, supervisor_name):
+#        godfather = User.objects.get(username=supervisor_name)
+#        supervisee = request.user
+#        print godfather
+#        print supervisee
+#        supervisee.profile.supervision_key = supervisee.profile._generate_current_supervision_key()
+#        #godfather = User.objects.get(username = self.cleaned_data['godfather'])
+#        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)
+#        supervisee.save()
+#
+#        #supervisiontrack = SupervisionTrack.objects.get(godfather=request.user, supervisee=supervisee, supervisee__profile__supervision_key=models.F('supervision_key'))
+#
+#        #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': supervisiontrack.godfather,
+#        #              'supervisee': supervisee,
+#        #              'prefix': server_address,
+#        #            })
+#
+#        #if supervisee.profile.status == Profile.WAITINGVALIDATION:
+#        #    #New user account waiting validation, so delete this account and inform by email the user
+#        #    try:
+#        #        t = loader.get_template('registration/mail.godfather_rejected.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_rejected_delete_account.message.txt')
+#        #        message = t.render(c)
+#
+#        #        send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [supervisee.email])
+#        #    except:
+#        #        pass
+#
+#        #    registration_profile = RegistrationProfile.objects.get(user=supervisee)
+#        #    supervisee.profile.delete()
+#        #    supervisee.delete()
+#        #    supervisiontrack.delete()
+#        #    registration_profile.delete()
+#        #else:
+#        #    #Reject this account and inform by email the user
+#        #    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
+#        #    supervisee.profile.supervision_key = None
+#        #    if supervisee.profile.rejection_date == None:
+#        #        supervisee.profile.rejection_date = now + expiration_date_delta
+#
+#        #    supervisiontrack.save()
+#        #    supervisee.profile.save()
+#        #    supervisee.save()
+#
+#        #    try:
+#        #        t = loader.get_template('registration/mail.godfather_rejected.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_rejected.message.txt')
+#        #        message = t.render(c)
+#
+#        #        send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [supervisee.email])
+#        #    except:
+#        #        pass
+#
+#        #    #if supervisiontrack.start_date is None:
+#        #    #    supervisiontrack.delete()
+#
+#        return Response(status=status.HTTP_204_NO_CONTENT)
+#
+#
+##----------------------------------------------------------
diff --git a/beat/web/accounts/api_urls.py b/beat/web/accounts/api_urls.py
index a912e700d..1b09dc243 100644
--- a/beat/web/accounts/api_urls.py
+++ b/beat/web/accounts/api_urls.py
@@ -41,4 +41,15 @@ urlpatterns = [
         name='validate_supervisee'
     ),
 
+    url(
+        r'^(?P<supervisee_name>[\w\W]+)/remove/$',
+        api.GodfatherRemoveSuperviseeView.as_view(),
+        name='remove_supervisee'
+    ),
+
+    url(
+        r'^(?P<supervisor_name>[\w\W]+)/add/$',
+        api.SuperviseeAddGodfatherView.as_view(),
+        name='add_supervisor'
+    ),
 ]
diff --git a/beat/web/accounts/management/commands/block_rejected_users.py b/beat/web/accounts/management/commands/block_rejected_users.py
new file mode 100644
index 000000000..a00ffddf7
--- /dev/null
+++ b/beat/web/accounts/management/commands/block_rejected_users.py
@@ -0,0 +1,101 @@
+#!/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 = 'Block rejected users after rejection date with no valid supervisor'
+
+    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('Block rejected 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('Block users operation canceled')
+                sys.exit(1)
+
+        if block:
+            rejected_profiles = Profile.objects.filter(status=Profile.REJECTED)
+            count = 0
+            for rejected_profile in rejected_profiles:
+                user = rejected_profile.user
+                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=rejected_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('{} Rejected user(s) successfully blocked/'.format(count) + '{} Total user(s) checked'.format(rejected_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/accounts/management/commands/clean_invalid_users.py b/beat/web/accounts/management/commands/clean_invalid_users.py
index 5b2308ba4..36c3059e2 100644
--- a/beat/web/accounts/management/commands/clean_invalid_users.py
+++ b/beat/web/accounts/management/commands/clean_invalid_users.py
@@ -95,7 +95,7 @@ class Command(BaseCommand):
                     supervisiontrack.delete()
                     registration_profile.delete()
 
-            self.stdout.write('{} Invalid user(s) successfully cleaned/'.format(count) + '{} Total user(s) checked:'.format(invalid_userprofiles_new_users.count()+invalid_userprofiles_waiting_validation.count()))
+            self.stdout.write('{} Invalid user(s) successfully cleaned/'.format(count) + '{} Total user(s) checked'.format(invalid_userprofiles_new_users.count()+invalid_userprofiles_waiting_validation.count()))
 
 
     def get_input_data(self, message, default=None):
diff --git a/beat/web/accounts/migrations/0007_profile_first_rejection_date.py b/beat/web/accounts/migrations/0007_profile_first_rejection_date.py
new file mode 100644
index 000000000..ff5cac206
--- /dev/null
+++ b/beat/web/accounts/migrations/0007_profile_first_rejection_date.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.5 on 2017-06-26 14:06
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('accounts', '0006_auto_20170608_1451'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='profile',
+            name='first_rejection_date',
+            field=models.DateTimeField(blank=True, null=True),
+        ),
+    ]
diff --git a/beat/web/accounts/migrations/0008_auto_20170626_1516.py b/beat/web/accounts/migrations/0008_auto_20170626_1516.py
new file mode 100644
index 000000000..f1ac03523
--- /dev/null
+++ b/beat/web/accounts/migrations/0008_auto_20170626_1516.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.5 on 2017-06-26 15:16
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('accounts', '0007_profile_first_rejection_date'),
+    ]
+
+    operations = [
+        migrations.RenameField(
+            model_name='profile',
+            old_name='first_rejection_date',
+            new_name='rejection_date',
+        ),
+    ]
diff --git a/beat/web/accounts/migrations/0009_auto_20170627_0956.py b/beat/web/accounts/migrations/0009_auto_20170627_0956.py
new file mode 100644
index 000000000..00e134aec
--- /dev/null
+++ b/beat/web/accounts/migrations/0009_auto_20170627_0956.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.5 on 2017-06-27 09:56
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('accounts', '0008_auto_20170626_1516'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='supervisiontrack',
+            name='supervisee',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='supervisors', to=settings.AUTH_USER_MODEL),
+        ),
+    ]
diff --git a/beat/web/accounts/models.py b/beat/web/accounts/models.py
index 8a3ddf0a5..e212ba512 100644
--- a/beat/web/accounts/models.py
+++ b/beat/web/accounts/models.py
@@ -50,7 +50,7 @@ class SupervisionTrack(models.Model):
 
     #_____ Fields __________
 
-    supervisee           = models.OneToOneField(User, on_delete=models.CASCADE,
+    supervisee           = models.ForeignKey(User, on_delete=models.CASCADE,
         related_name='supervisors')
     godfather            = models.ForeignKey(User, on_delete=models.CASCADE,
         related_name='supervisees')
@@ -97,6 +97,7 @@ class Profile(models.Model):
     supervision  = models.ManyToManyField(SupervisionTrack, related_name='profiles', blank=True)
     supervision_key = models.CharField(max_length=40, null=True, blank=True)
     registration_date = models.DateTimeField(null=True, blank=True)
+    rejection_date = models.DateTimeField(null=True, blank=True)
 
     def __unicode__(self):
       return u'User: %s' % self.user.username
diff --git a/beat/web/navigation/admin.py b/beat/web/navigation/admin.py
index 726b02598..86f7b76f1 100644
--- a/beat/web/navigation/admin.py
+++ b/beat/web/navigation/admin.py
@@ -99,6 +99,8 @@ class UserAdmin(UserAdmin):
     def supervision_key(self, obj):
         return obj.profile.supervision_key
 
+    def rejection_date(self, obj):
+        return obj.profile.rejection_date
 
 
     list_display = (
@@ -112,6 +114,7 @@ class UserAdmin(UserAdmin):
         'status',
         'supervision',
         'supervision_key',
+        'rejection_date',
     )
 
     ordering = (
@@ -166,6 +169,7 @@ class ProfileAdmin(admin.ModelAdmin):
         'registration_date',
         'is_godfather',
         'supervision_key',
+        'rejection_date',
     )
 
     ordering = (
diff --git a/beat/web/settings/settings.py b/beat/web/settings/settings.py
index 9c54130b0..6dfc77da8 100755
--- a/beat/web/settings/settings.py
+++ b/beat/web/settings/settings.py
@@ -218,6 +218,7 @@ DATASETS_ROOT_PATH = None
 ACCOUNT_ACTIVATION_DAYS  = 2
 ACCOUNT_ACTIVATION_DAYS_FROM_GODFATHER  = 7
 ACCOUNT_EXPIRATION_DAYS  = 365
+ACCOUNT_BLOCKAGE_AFTER_FIRST_REJECTION_DAYS  = 56
 
 LOGIN_REDIRECT_URL       = '/'
 LOGIN_URL                = '/login/'
diff --git a/beat/web/ui/registration/templates/registration/mail.godfather_rejected.message.txt b/beat/web/ui/registration/templates/registration/mail.godfather_rejected.message.txt
new file mode 100644
index 000000000..bb95d0708
--- /dev/null
+++ b/beat/web/ui/registration/templates/registration/mail.godfather_rejected.message.txt
@@ -0,0 +1,17 @@
+Dear {{ supervisee.first_name }},
+
+Your supervision request was not accepted and has been rejected by the supervisor {{ supervisor.username }}.
+
+Either you made a mistake or you are not a supervisee anymore for {{ supervisor.username }}
+
+Your account {{ supervisee.username }} is currently not fully validated.
+
+You can request to validate your account through a new supervision request
+either through an existing validated supervisor from the platform or by asking your current supervisor
+to register on the platform as a supervisor and when his account is validated,
+you can request a supervision from him and get a re-activation of your
+account.
+
+Your account will be blocked on {{ supervisee.profile.rejection_date }}. You have until this date to find a
+supervisor. Passed this date your account will be blocked and you will need to
+proceed with an unblock account request.
diff --git a/beat/web/ui/registration/templates/registration/mail.godfather_rejected.subject.txt b/beat/web/ui/registration/templates/registration/mail.godfather_rejected.subject.txt
new file mode 100644
index 000000000..09ffd3d8c
--- /dev/null
+++ b/beat/web/ui/registration/templates/registration/mail.godfather_rejected.subject.txt
@@ -0,0 +1 @@
+Account not confirmed - Supervision Rejected
diff --git a/beat/web/ui/registration/templates/registration/mail.godfather_rejected_delete_account.message.txt b/beat/web/ui/registration/templates/registration/mail.godfather_rejected_delete_account.message.txt
new file mode 100644
index 000000000..ca0d45a1a
--- /dev/null
+++ b/beat/web/ui/registration/templates/registration/mail.godfather_rejected_delete_account.message.txt
@@ -0,0 +1,9 @@
+Dear {{ supervisee.first_name }},
+
+Your supervision request was not accepted and has been rejected by the supervisor {{ supervisor.username }}.
+
+Either you made a mistake or you are not a supervisee of {{ supervisor.username }}
+
+Your newly created account {{ supervisee.username }} has therefore currently been deleted.
+
+You can re-create a new account and make a proper supervision request.
diff --git a/beat/web/utils/management/commands/daily_cron_actions.py b/beat/web/utils/management/commands/daily_cron_actions.py
index 34bcd0140..b2d7a7baa 100644
--- a/beat/web/utils/management/commands/daily_cron_actions.py
+++ b/beat/web/utils/management/commands/daily_cron_actions.py
@@ -55,3 +55,6 @@ class Command(BaseCommand):
 
         # Clean and remove invalid users
         call_command('clean_invalid_users')
+
+        # Block rejected users with no supervision after specific rejection date
+        call_command('block_rejected_users')
-- 
GitLab