From 696c1e14cdd47627c12f8485c423ab452b2d77e5 Mon Sep 17 00:00:00 2001
From: Jorran de Wit <jorrandewit@outlook.com>
Date: Mon, 19 Jun 2017 21:56:22 +0200
Subject: [PATCH] Improve registration vetting

Editorial Administrators are able to reset and resend the verification key
to a person who requested a key. All registrations are shown, so also
old ones. Should this view be extended to have some kind of cleanup function
to periodically remove (specific) old registrations?
---
 scipost/constants.py                          |  3 +-
 .../commands/add_groups_and_permissions.py    |  5 ++
 scipost/managers.py                           |  5 +-
 scipost/models.py                             |  1 -
 scipost/templates/scipost/personal_page.html  |  8 ++-
 .../scipost/registration_requests.html        | 67 +++++++++++++++++++
 scipost/urls.py                               |  3 +
 scipost/views.py                              | 37 +++++++++-
 8 files changed, 122 insertions(+), 7 deletions(-)
 create mode 100644 scipost/templates/scipost/registration_requests.html

diff --git a/scipost/constants.py b/scipost/constants.py
index aa19e9b47..3f8ef98b4 100644
--- a/scipost/constants.py
+++ b/scipost/constants.py
@@ -124,6 +124,7 @@ subject_areas_dict = {}
 for k in subject_areas_raw_dict.keys():
     subject_areas_dict.update(dict(subject_areas_raw_dict[k]))
 
+CONTRIBUTOR_NEWLY_REGISTERED = 0
 CONTRIBUTOR_NORMAL = 1
 CONTRIBUTOR_STATUS = (
     # status determine the type of Contributor:
@@ -135,7 +136,7 @@ CONTRIBUTOR_STATUS = (
     # -2: other account already exists for this person
     # -3: barred from SciPost (abusive behaviour)
     # -4: disabled account (deceased)
-    (0, 'newly registered'),
+    (CONTRIBUTOR_NEWLY_REGISTERED, 'newly registered'),
     (CONTRIBUTOR_NORMAL, 'normal user'),
     (-1, 'not a professional scientist'),
     (-2, 'other account already exists'),
diff --git a/scipost/management/commands/add_groups_and_permissions.py b/scipost/management/commands/add_groups_and_permissions.py
index 5dc95ea76..9889c3a6f 100644
--- a/scipost/management/commands/add_groups_and_permissions.py
+++ b/scipost/management/commands/add_groups_and_permissions.py
@@ -61,6 +61,10 @@ class Command(BaseCommand):
             codename='can_invite_Fellows',
             name='Can invite Fellows',
             content_type=content_type)
+        can_resend_registration_requests, created = Permission.objects.get_or_create(
+            codename='can_resend_registration_requests',
+            name='Can resend registration activation emails',
+            content_type=content_type)
 
         # Communications
         can_email_group_members, created = Permission.objects.get_or_create(
@@ -215,6 +219,7 @@ class Command(BaseCommand):
             can_view_production,
             can_publish_accepted_submission,
             can_attend_VGMs,
+            can_resend_registration_requests,
         ])
         EditorialCollege.permissions.set([
             can_view_pool,
diff --git a/scipost/managers.py b/scipost/managers.py
index 518ca5aaf..cd9344ef5 100644
--- a/scipost/managers.py
+++ b/scipost/managers.py
@@ -3,7 +3,7 @@ import datetime
 from django.db import models
 from django.db.models import Q
 
-from .constants import CONTRIBUTOR_NORMAL
+from .constants import CONTRIBUTOR_NORMAL, CONTRIBUTOR_NEWLY_REGISTERED
 
 
 class FellowManager(models.Manager):
@@ -20,3 +20,6 @@ class FellowManager(models.Manager):
 class ContributorManager(models.Manager):
     def active(self):
         return self.filter(user__is_active=True, status=CONTRIBUTOR_NORMAL)
+
+    def awaiting_validation(self):
+        return self.filter(user__is_active=False, status=CONTRIBUTOR_NEWLY_REGISTERED)
diff --git a/scipost/models.py b/scipost/models.py
index 9bd5f46d8..8c477ff6b 100644
--- a/scipost/models.py
+++ b/scipost/models.py
@@ -103,7 +103,6 @@ class Contributor(models.Model):
         salt = self.user.username.encode('utf8')
         self.activation_key = hashlib.sha1(salt+salt).hexdigest()
         self.key_expires = datetime.datetime.now() + datetime.timedelta(days=2)
-        self.save()
 
     def discipline_as_string(self):
         # Redundant, to be removed in future
diff --git a/scipost/templates/scipost/personal_page.html b/scipost/templates/scipost/personal_page.html
index 7956a555a..5468435a3 100644
--- a/scipost/templates/scipost/personal_page.html
+++ b/scipost/templates/scipost/personal_page.html
@@ -205,7 +205,13 @@
                         <ul>
                             {% if perms.scipost.can_vet_registration_requests %}
                                 <li><a href="{% url 'scipost:vet_registration_requests' %}">Vet Registration requests</a> ({{ nr_reg_to_vet }})</li>
-                                <li>Awaiting validation ({{ nr_reg_awaiting_validation }}) (no action necessary)</li>
+                                <li>
+                                    {% if perms.scipost.can_resend_registration_requests %}
+                                        <a href="{% url 'scipost:registration_requests' %}">Awaiting validation</a> ({{ nr_reg_awaiting_validation }})
+                                    {% else %}
+                                        Awaiting validation ({{ nr_reg_awaiting_validation }})
+                                    {% endif %}
+                                </li>
                             {% endif %}
                             {% if perms.scipost.can_draft_registration_invitations %}
                                 <li><a href="{% url 'scipost:draft_registration_invitation' %}">Draft a Registration Invitation</a></li>
diff --git a/scipost/templates/scipost/registration_requests.html b/scipost/templates/scipost/registration_requests.html
new file mode 100644
index 000000000..3402c5b1e
--- /dev/null
+++ b/scipost/templates/scipost/registration_requests.html
@@ -0,0 +1,67 @@
+{% extends 'scipost/_personal_page_base.html' %}
+
+{% block pagetitle %}: registration awaiting validation{% endblock pagetitle %}
+
+{% load bootstrap %}
+
+{% block breadcrumb_items %}
+    {{block.super}}
+    <span class="breadcrumb-item">Registration awaiting validation</span>
+{% endblock %}
+
+{% block content %}
+
+<div class="row">
+    <div class="col-12">
+        <h1 class="highlight">Registration awaiting validation</h1>
+        <p>
+            These Contributors did not yet activate their account. Sometimes, this link is never clicked on (email is either lost to spam, or not received).<br>
+            As per this page, you are able to send a reminder email to the as-yet-unconfirmed contributor.
+        </p>
+    </div>
+</div>
+
+<div class="row">
+    <div class="col-12">
+        <table class="table">
+            <thead>
+                <tr>
+                    <th>Name</th>
+                    <th>Email</th>
+                    <th>Date requested</th>
+                    <th>Key expires</th>
+                    <th>Actions</th>
+                </tr>
+            </thead>
+            <tbody>
+                {% for contributor in unactive_contributors %}
+                    <tr>
+                        <td>{{contributor.user.first_name}} {{contributor.user.last_name}}</td>
+                        <td>{{contributor.user.email}}</td>
+                        <td>{{contributor.user.date_joined|timesince}} ago</td>
+                        <td>
+                            {% if contributor.key_expires < now %}
+                                <span class="text-danger">Expired {{contributor.key_expires|timesince}} ago</span>
+                            {% else %}
+                                Expires in {{contributor.key_expires|timeuntil}}
+                            {% endif %}
+                        </td>
+                        <td>
+                            <form action="{% url 'scipost:registration_requests_reset' contributor.id %}" method="post">
+                                {% csrf_token %}
+                                <input type="submit" class="btn btn-warning" value="Reset and resend" />
+                            </form>
+                        </td>
+                    </tr>
+                {% empty %}
+                    <tr>
+                        <td colspan="5">All registrations have been activated.</td>
+                    </tr>
+                {% endfor %}
+            </tbody>
+        </table>
+    </div>
+</div>
+
+
+{% endblock content %}
diff --git a/scipost/urls.py b/scipost/urls.py
index 307e602b5..8eaf7b40e 100644
--- a/scipost/urls.py
+++ b/scipost/urls.py
@@ -84,6 +84,9 @@ urlpatterns = [
         views.vet_registration_requests, name='vet_registration_requests'),
     url(r'^vet_registration_request_ack/(?P<contributor_id>[0-9]+)$',
         views.vet_registration_request_ack, name='vet_registration_request_ack'),
+    url(r'^registration_requests$', views.registration_requests, name="registration_requests"),
+    url(r'^registration_requests/(?P<contributor_id>[0-9]+)/reset$',
+        views.registration_requests_reset, name="registration_requests_reset"),
     url(r'^registration_invitations/(?P<draft_id>[0-9]+)$',
         views.registration_invitations, name="registration_invitations_from_draft"),
     url(r'^registration_invitations$',
diff --git a/scipost/views.py b/scipost/views.py
index d7b30e9a4..0b7425b9e 100644
--- a/scipost/views.py
+++ b/scipost/views.py
@@ -278,6 +278,7 @@ def request_new_activation_link(request, contributor_id, key):
     if request.GET.get('confirm', False):
         # Generate a new email activation key and link
         contributor.generate_key()
+        contributor.save()
         Utils.load({'contributor': contributor}, request)
         Utils.send_new_activation_link_email()
 
@@ -385,6 +386,36 @@ def vet_registration_request_ack(request, contributor_id):
     return redirect(reverse('scipost:vet_registration_requests'))
 
 
+@permission_required('scipost.can_resend_registration_requests', return_403=True)
+def registration_requests(request):
+    '''
+    List all inactive users. These are users that have filled the registration form,
+    but did not yet activate their account using the validation email.
+    '''
+    unactive_contributors = (Contributor.objects.awaiting_validation()
+                             .prefetch_related('user')
+                             .order_by('-key_expires'))
+    context = {
+        'unactive_contributors': unactive_contributors,
+        'now': timezone.now()
+    }
+    return render(request, 'scipost/registration_requests.html', context)
+
+
+@require_POST
+@permission_required('scipost.can_resend_registration_requests', return_403=True)
+def registration_requests_reset(request, contributor_id):
+    '''
+    Reset specific activation_key for Contributor and resend activation mail.
+    '''
+    contributor = get_object_or_404(Contributor.objects.awaiting_validation(), id=contributor_id)
+    contributor.generate_key()
+    contributor.save()
+    Utils.load({'contributor': contributor}, request)
+    Utils.send_new_activation_link_email()
+    return redirect(reverse('scipost:registration_requests'))
+
+
 @permission_required('scipost.can_draft_registration_invitations', return_403=True)
 def draft_registration_invitation(request):
     """
@@ -758,9 +789,9 @@ def personal_page(request):
 
         # count the number of pending registration requests
         nr_reg_to_vet = Contributor.objects.filter(user__is_active=True, status=0).count()
-        nr_reg_awaiting_validation = Contributor.objects.filter(
-            user__is_active=False, key_expires__gte=now,
-            key_expires__lte=intwodays, status=0).count()
+        nr_reg_awaiting_validation = (Contributor.objects.awaiting_validation()
+                                    #   .filter(key_expires__gte=now, key_expires__lte=intwodays)
+                                      .count())
         nr_submissions_to_assign = Submission.objects.filter(status__in=['unassigned']).count()
         nr_recommendations_to_prepare_for_voting = EICRecommendation.objects.filter(
             submission__status__in=['voting_in_preparation']).count()
-- 
GitLab