From 7d0928c729fb63571706a8edb3a298350de02915 Mon Sep 17 00:00:00 2001
From: Flavio Tarsetti <flavio.tarsetti@idiap.ch>
Date: Thu, 8 Jun 2017 15:11:00 +0200
Subject: [PATCH] [accounts/ui/settings/navigation] added first registration
 step for supervisee

---
 .../migrations/0003_auto_20170518_1232.py     | 39 ++++++++++
 .../migrations/0004_auto_20170523_1736.py     | 25 +++++++
 .../migrations/0005_auto_20170608_0935.py     | 25 +++++++
 .../migrations/0006_auto_20170608_1451.py     | 22 ++++++
 beat/web/accounts/models.py                   | 43 +++++++++--
 beat/web/navigation/admin.py                  | 72 ++++++++++++++++++-
 beat/web/settings/settings.py                 |  1 +
 beat/web/ui/registration/forms.py             | 44 ++++++++++++
 beat/web/ui/registration/models.py            | 40 +++++++++++
 .../templates/registration/activate.html      | 22 +++---
 .../registration/registration_form.html       | 16 +++--
 11 files changed, 330 insertions(+), 19 deletions(-)
 create mode 100644 beat/web/accounts/migrations/0003_auto_20170518_1232.py
 create mode 100644 beat/web/accounts/migrations/0004_auto_20170523_1736.py
 create mode 100644 beat/web/accounts/migrations/0005_auto_20170608_0935.py
 create mode 100644 beat/web/accounts/migrations/0006_auto_20170608_1451.py

diff --git a/beat/web/accounts/migrations/0003_auto_20170518_1232.py b/beat/web/accounts/migrations/0003_auto_20170518_1232.py
new file mode 100644
index 000000000..f14663979
--- /dev/null
+++ b/beat/web/accounts/migrations/0003_auto_20170518_1232.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.5 on 2017-05-18 12:32
+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 = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('accounts', '0002_profile'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='SupervisionTrack',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('is_valid', models.BooleanField(default=False)),
+                ('start_date', models.DateTimeField(blank=True, null=True)),
+                ('expiration_date', models.DateTimeField(blank=True, null=True)),
+                ('last_validation_date', models.DateTimeField(blank=True, null=True)),
+                ('godfather', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='supervisees', to=settings.AUTH_USER_MODEL)),
+                ('supervisee', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='supervisors', to=settings.AUTH_USER_MODEL)),
+            ],
+        ),
+        migrations.RemoveField(
+            model_name='profile',
+            name='supervisees',
+        ),
+        migrations.AddField(
+            model_name='profile',
+            name='supervision',
+            field=models.ManyToManyField(blank=True, related_name='profiles', to='accounts.SupervisionTrack'),
+        ),
+    ]
diff --git a/beat/web/accounts/migrations/0004_auto_20170523_1736.py b/beat/web/accounts/migrations/0004_auto_20170523_1736.py
new file mode 100644
index 000000000..bfe5e0b23
--- /dev/null
+++ b/beat/web/accounts/migrations/0004_auto_20170523_1736.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.5 on 2017-05-23 17:36
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('accounts', '0003_auto_20170518_1232'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='profile',
+            name='supervision_key',
+            field=models.CharField(blank=True, max_length=40, null=True),
+        ),
+        migrations.AddField(
+            model_name='supervisiontrack',
+            name='supervision_key',
+            field=models.CharField(blank=True, max_length=40, null=True),
+        ),
+    ]
diff --git a/beat/web/accounts/migrations/0005_auto_20170608_0935.py b/beat/web/accounts/migrations/0005_auto_20170608_0935.py
new file mode 100644
index 000000000..60270a7b4
--- /dev/null
+++ b/beat/web/accounts/migrations/0005_auto_20170608_0935.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.5 on 2017-06-08 09:35
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('accounts', '0004_auto_20170523_1736'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='profile',
+            name='registration_date',
+            field=models.DateTimeField(blank=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='profile',
+            name='status',
+            field=models.CharField(choices=[(b'N', b'New User'), (b'W', b'Waiting Validation'), (b'A', b'Accepted'), (b'R', b'Rejected'), (b'Y', b'Yearly revalidation'), (b'B', b'Blocked no supervisor')], default=b'B', max_length=1),
+        ),
+    ]
diff --git a/beat/web/accounts/migrations/0006_auto_20170608_1451.py b/beat/web/accounts/migrations/0006_auto_20170608_1451.py
new file mode 100644
index 000000000..a0e80203a
--- /dev/null
+++ b/beat/web/accounts/migrations/0006_auto_20170608_1451.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.5 on 2017-06-08 14:51
+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', '0005_auto_20170608_0935'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='supervisiontrack',
+            name='godfather',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='supervisees', to=settings.AUTH_USER_MODEL),
+        ),
+    ]
diff --git a/beat/web/accounts/models.py b/beat/web/accounts/models.py
index a61ed582a..88cf26c31 100644
--- a/beat/web/accounts/models.py
+++ b/beat/web/accounts/models.py
@@ -30,6 +30,9 @@ from django.contrib.auth.models import User
 from django.db.models.signals import post_save
 from django.dispatch import receiver
 
+import random
+import string
+
 class AccountSettings(models.Model):
 
     class Meta:
@@ -42,19 +45,44 @@ class AccountSettings(models.Model):
     database_notifications_enabled = models.BooleanField(default=True)
     environment_notifications_enabled = models.BooleanField(default=True)
 
+class SupervisionTrack(models.Model):
+
+    #_____ Fields __________
+
+    supervisee           = models.OneToOneField(User, on_delete=models.CASCADE,
+        related_name='supervisors')
+    godfather            = models.ForeignKey(User, on_delete=models.CASCADE,
+        related_name='supervisees')
+    is_valid             = models.BooleanField(default=False)
+    start_date           = models.DateTimeField(null=True, blank=True)
+    expiration_date      = models.DateTimeField(null=True, blank=True)
+    last_validation_date = models.DateTimeField(null=True, blank=True)
+    supervision_key      = models.CharField(max_length=40, null=True, blank=True)
+
+    def __unicode__(self):
+      return u'Godfather: %s, Supervisee, %s, Validity: %s' % (self.godfather.username, self.supervisee.username, self.is_valid)
+
+
 class Profile(models.Model):
     #_____ Constants __________
 
+    #New account creation 'N'/'W'
+    #Godfather acceptance/rejection 'A'/'R'
+    #Yearly revalidation/blockage  'A'/'B'
+    NEWUSER           = 'N'
+    WAITINGVALIDATION = 'W'
     ACCEPTED          = 'A'
     REJECTED          = 'R'
-    WAITINGVALIDATION = 'W'
+    YEARREVALIDATION  = 'Y'
     BLOCKED           = 'B'
 
     PROFILE_STATUS = (
+        (NEWUSER, 'New User'),
+        (WAITINGVALIDATION, 'Waiting Validation'),
         (ACCEPTED,    'Accepted'),
         (REJECTED,    'Rejected'),
-        (WAITINGVALIDATION, 'Waiting Validation'),
-        (BLOCKED, 'Blocked'),
+        (YEARREVALIDATION, 'Yearly revalidation'),
+        (BLOCKED, 'Blocked no supervisor'),
     )
 
     #_____ Fields __________
@@ -65,11 +93,18 @@ class Profile(models.Model):
     # Other fields here
     status       = models.CharField(max_length=1, choices=PROFILE_STATUS, default=BLOCKED)
     is_godfather = models.BooleanField(default=False)
-    supervisees  = models.ManyToManyField(User, related_name='users', blank=True)
+    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)
 
     def __unicode__(self):
       return u'User: %s' % self.user.username
 
+    def _generate_current_supervision_key(self):
+      length = 40
+      return ''.join(random.choice(string.ascii_letters + string.digits) for _
+          in range(length))
+
 @receiver(post_save, sender=User)
 def create_user_profile(sender, instance, created, **kwargs):
     if created:
diff --git a/beat/web/navigation/admin.py b/beat/web/navigation/admin.py
index 23cf9bafa..726b02598 100644
--- a/beat/web/navigation/admin.py
+++ b/beat/web/navigation/admin.py
@@ -31,6 +31,7 @@ from django.contrib.auth.models import User
 
 from .models import Agreement
 from ..accounts.models import AccountSettings
+from ..accounts.models import SupervisionTrack
 from ..accounts.models import Profile
 
 
@@ -51,6 +52,14 @@ class AccountSettingsInline(admin.StackedInline):
 #----------------------------------------------------------
 
 
+class SupervisionTrackInline(admin.StackedInline):
+    model = SupervisionTrack
+    fk_name = 'godfather'
+
+
+#----------------------------------------------------------
+
+
 class ProfileInline(admin.StackedInline):
     model = Profile
 
@@ -76,8 +85,19 @@ class UserAdmin(UserAdmin):
     def status(self, obj):
         return obj.profile.status
 
-    def supervisees(self, obj):
-        return obj.profile.supervisees
+    def supervision(self, obj):
+        if obj.is_staff:
+            return "Staff account"
+        if obj.profile.is_godfather:
+            return "Godfather account"
+        else:
+            supervisiontrack = SupervisionTrack.objects.get(supervisee=obj,
+                is_valid=True)
+            godfather = supervisiontrack.godfather
+            return godfather
+
+    def supervision_key(self, obj):
+        return obj.profile.supervision_key
 
 
 
@@ -90,7 +110,8 @@ class UserAdmin(UserAdmin):
         'last_login',
         'godfather',
         'status',
-        'supervisees',
+        'supervision',
+        'supervision_key',
     )
 
     ordering = (
@@ -102,8 +123,53 @@ class UserAdmin(UserAdmin):
     inlines = (
         AgreementInline,
         AccountSettingsInline,
+        SupervisionTrackInline,
         ProfileInline,
     )
 
 admin.site.unregister(User)
 admin.site.register(User, UserAdmin)
+
+
+#----------------------------------------------------------
+
+
+class SupervisionTrackAdmin(admin.ModelAdmin):
+
+
+    list_display = (
+        'godfather',
+        'supervisee',
+        'is_valid',
+        'start_date',
+        'expiration_date',
+        'last_validation_date',
+        'supervision_key',
+    )
+
+    ordering = (
+        'godfather',
+        )
+
+admin.site.register(SupervisionTrack, SupervisionTrackAdmin)
+
+
+#----------------------------------------------------------
+
+
+class ProfileAdmin(admin.ModelAdmin):
+
+
+    list_display = (
+        'user',
+        'status',
+        'registration_date',
+        'is_godfather',
+        'supervision_key',
+    )
+
+    ordering = (
+        'user',
+        )
+
+admin.site.register(Profile, ProfileAdmin)
diff --git a/beat/web/settings/settings.py b/beat/web/settings/settings.py
index a5c324d86..0dc550136 100755
--- a/beat/web/settings/settings.py
+++ b/beat/web/settings/settings.py
@@ -216,6 +216,7 @@ DATASETS_ROOT_PATH = None
 ##############################################################################
 
 ACCOUNT_ACTIVATION_DAYS  = 2
+ACCOUNT_ACTIVATION_DAYS_FROM_GODFATHER  = 7
 LOGIN_REDIRECT_URL       = '/'
 LOGIN_URL                = '/login/'
 SYSTEM_ACCOUNT           = 'system'
diff --git a/beat/web/ui/registration/forms.py b/beat/web/ui/registration/forms.py
index b41f5b997..a2ed0c97e 100644
--- a/beat/web/ui/registration/forms.py
+++ b/beat/web/ui/registration/forms.py
@@ -30,6 +30,7 @@ Forms and validation code for user registration.
 
 """
 
+import datetime
 
 from django.contrib.auth.models import User
 from django import forms
@@ -39,6 +40,8 @@ from django.utils.translation import ugettext_lazy as _
 from .models import RegistrationProfile
 from .models import PreregistrationProfile
 
+from ...accounts.models import SupervisionTrack
+from ...accounts.models import Profile
 
 # I put this on all required fields, because it's easier to pick up
 # on them with CSS or JavaScript if they have a class of "required"
@@ -79,6 +82,11 @@ class RegistrationForm(forms.Form):
                                 label=_(u'Password'))
     password2 = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict, render_value=False),
                                 label=_(u'Password (again)'))
+    godfather = forms.RegexField(regex=r'^\w+$',
+                                max_length=30,
+                                widget=forms.TextInput(attrs=attrs_dict),
+                                label=_(u'Godfather Username (Your account needs to be validated by a recognized person)'))
+
 
     def clean_username(self):
         """
@@ -92,6 +100,22 @@ class RegistrationForm(forms.Form):
             return self.cleaned_data['username']
         raise forms.ValidationError(_(u'This username is already taken. Please choose another.'))
 
+    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
@@ -123,6 +147,26 @@ class RegistrationForm(forms.Form):
                 password=self.cleaned_data['password1'],
                 email=self.cleaned_data['email'],
                 )
+
+        #Create and assign key
+        new_user.profile.supervision_key = new_user.profile._generate_current_supervision_key()
+        godfather = User.objects.get(username = self.cleaned_data['godfather'])
+        supervisiontrack = SupervisionTrack.objects.create(
+            supervisee = new_user,
+            godfather = godfather,
+            is_valid = False,
+            )
+
+        #Assign key to supervision track
+        supervisiontrack.supervision_key = new_user.profile.supervision_key
+        supervisiontrack.save()
+        new_user.profile.is_godfather = False
+        new_user.profile.status = Profile.NEWUSER
+        new_user.profile.registration_date = datetime.datetime.now()
+        new_user.profile.supervision.add(supervisiontrack)
+        new_user.save()
+
+
         return new_user
 
 
diff --git a/beat/web/ui/registration/models.py b/beat/web/ui/registration/models.py
index 546669521..405c31daf 100644
--- a/beat/web/ui/registration/models.py
+++ b/beat/web/ui/registration/models.py
@@ -39,6 +39,9 @@ from django.template import loader
 from django.template import Context
 from django.utils.translation import ugettext_lazy as _
 
+from ...accounts.models import SupervisionTrack
+from ...accounts.models import Profile
+
 SHA1_RE = re.compile('^[a-f0-9]{40}$')
 
 
@@ -93,6 +96,43 @@ class RegistrationManager(models.Manager):
                 profile.activation_key = self.model.ACTIVATED
                 profile.save()
                 user_activated.send(sender=self.model, user=user)
+                # The user activated via the link but activation from godfather or admin
+                # is requested now to have access to the platform (so set it
+                # inactive again)
+                user.is_active = False
+                user.profile.status = Profile.WAITINGVALIDATION
+                user.profile.registration_date = datetime.datetime.now()
+                user.save()
+
+                # Send email to godfather
+                # Fetch Godfather user from supervision track
+                supervisiontrack = SupervisionTrack.objects.get(supervision_key=user.profile.supervision_key)
+                godfather_user = supervisiontrack.godfather
+
+                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_user,
+                              'supervisee': user,
+                              '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.message.txt')
+                    message = t.render(c)
+
+                    send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [godfather_user.email])
+                except:
+                    pass
+
                 return user
         return False
 
diff --git a/beat/web/ui/registration/templates/registration/activate.html b/beat/web/ui/registration/templates/registration/activate.html
index 54c1d444c..deeaed58a 100644
--- a/beat/web/ui/registration/templates/registration/activate.html
+++ b/beat/web/ui/registration/templates/registration/activate.html
@@ -2,21 +2,21 @@
 {% 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 %}
@@ -26,13 +26,19 @@
 <div class="row">
   <div class="col-sm-4 col-sm-offset-4">
 
-    <h4>{% if account %}Account activated{% else %}Error{% endif %}</h4>
+    <h4>{% if account %}Account almost activated{% else %}Error{% endif %}</h4>
 
     {% if account %}
-    <p class="text">Your account was successfully activated. Welcome {{ account.username }}.
-    You can now <a href="{% url 'login' %}">sign in</a> and start your own experiments!</p>
+    <p class="text">Your account is almost activated. Welcome {{ account.username }}.
+    We have asked your "godfather" to validate your account and accept you as a
+    supervisee. You will be informed by email once this is done.
+    In the meantime you can remind your "godfather"(supervisor) to validate
+    your account. You will then be able to  <a href="{% url 'login' %}">sign in</a> and start your own experiments!</p>
     {% else %}
-    <p class="text">Your account wasn't activated. Check that the URL you entered is correct. If you still have problems to activate your account, <a href="{% url 'contact' %}">contact a member of our support staff</a>.</p>
+    <p class="text">Your account wasn't activated. Check that the URL you entered is correct.
+    If you already clicked on the link sent to you by email, you need to wait
+    for your "godfather"(supervisor) to accept your account activation request.
+    If you still have problems to activate your account, <a href="{% url 'contact' %}">contact a member of our support staff</a>.</p>
     {% endif %}
 
   </div>
diff --git a/beat/web/ui/registration/templates/registration/registration_form.html b/beat/web/ui/registration/templates/registration/registration_form.html
index eb3cc436c..13702df70 100644
--- a/beat/web/ui/registration/templates/registration/registration_form.html
+++ b/beat/web/ui/registration/templates/registration/registration_form.html
@@ -2,21 +2,21 @@
 {% 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 %}
@@ -91,6 +91,14 @@
             {% endfor %}
           </div>
 
+          <div class="form-group{% if form.godfather.errors %} has-error{% endif %}">
+            {{ form.godfather.label_tag }}
+            {{ form.godfather|tabindex:7|addclass:"form-control" }}
+            {% for error in form.godfather.errors %}
+            <div class="alert alert-danger" role="alert">{{ error }}</div>
+            {% endfor %}
+          </div>
+
           <div class="form-group{% if form.tos.errors %} has-error{% endif %}">
             {{ form.tos }} I have carefully read the <a href="{% url 'terms-of-service' %}">Terms of Service</a>, which include the Privacy and Data Protection Terms of Use, and fully agree and undertake to comply with all provisions therein by checking this box.
             {% for error in form.tos.errors %}
-- 
GitLab