Repository: akvo/akvo-rsr

Issue:
Show all user information in one row (estimate: 6)
On the user management page, we should:
- [ ] Show all user information (all employments, etc) in one row;
- [ ] Also show users without an employment;
- [ ] Easily select multiple roles for one user.

The current implementation shows one employment per row, and therefore the users without an employment will not be shown.



Discussion:
Agreed at the planning poker session today that we go with the most straightforward solution for this iteration. We sort by the user name (and possibly also the organisation). The bug with "ghost" users with no employment should be dealt with as well. The estimate for both actions is 6.

There are whole bunch (5-6x of users with at least one employment) of users with spammy email ids with no employments in the database currently.  Showing them in the user management interface would make the interface pretty useless.  

Given that users can be invited from the user management page, I vote for this being the standard way to add users and to get rid of the "Register" button on the site.  The new user creation form is being spammed. 
Agreed with @punchagan that sorting by username would complicate user experience for partners as they might want to see new users first for user management purposes. So we don't make any changes there.

Following our discussion on bot users in RSR database - I created a new issue https://github.com/akvo/akvo-rsr/issues/2454 about adding a captcha to the registration page. 

Also we need to check if we have any mechanism for deleting new users if they haven't activated their account in a certain timeframe. If we don't have that - that might become a new github issue too.

Resolving patch:
diff --git a/akvo/rsr/views/my_rsr.py b/akvo/rsr/views/my_rsr.py
--- a/akvo/rsr/views/my_rsr.py
+++ b/akvo/rsr/views/my_rsr.py
@@ -12,6 +12,7 @@
 from django.contrib.auth.models import Group
 from django.core.exceptions import PermissionDenied
 from django.core.urlresolvers import reverse
+from django.db.models import Max
 from django.forms.models import model_to_dict
 from django.http import HttpResponseRedirect, Http404
 from django.shortcuts import get_object_or_404, render
@@ -416,7 +417,7 @@ def user_management(request):
 
     if user.is_admin or user.is_superuser:
         # Superusers or RSR Admins can manage and invite someone for any organisation
-        employments = Employment.objects.select_related().prefetch_related('group').order_by('-id')
+        employments = Employment.objects.select_related().prefetch_related('group')
         organisations = Organisation.objects.all()
         roles = Group.objects.filter(name__in=groups)
     else:
@@ -430,10 +431,10 @@ def user_management(request):
             content_owned_organisations()
         if org_admin:
             roles = Group.objects.filter(name__in=groups)
-            employments = organisations.employments().order_by('-id')
+            employments = organisations.employments()
         else:
             roles = Group.objects.filter(name__in=groups[:-1])
-            employments = organisations.employments().exclude(user=user).order_by('-id')
+            employments = organisations.employments().exclude(user=user)
 
     q = request.GET.get('q')
     if q:
@@ -445,6 +446,11 @@ def user_management(request):
                 employments.filter(organisation__name__icontains=q_item) | \
                 employments.filter(organisation__long_name__icontains=q_item)
 
+    # Order employments in reverse chronological order, but also group
+    # employments by the user.
+    employments = employments.annotate(max_id=Max('user__employers__id'))
+    employments = employments.order_by('-max_id', '-id')
+
     qs = remove_empty_querydict_items(request.GET)
     page = request.GET.get('page')
     page, paginator, page_range = pagination(page, employments, 10)


Test patch:
diff --git a/akvo/rsr/tests/views/__init__.py b/akvo/rsr/tests/views/__init__.py
new file mode 100644
--- /dev/null
+++ b/akvo/rsr/tests/views/__init__.py
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+
+"""Akvo RSR is covered by the GNU Affero General Public License.
+
+See more details in the license.txt file located at the root folder of the
+Akvo RSR module. For additional details on the GNU license please
+see < http://www.gnu.org/licenses/agpl.html >.
+"""
diff --git a/akvo/rsr/tests/views/test_my_rsr.py b/akvo/rsr/tests/views/test_my_rsr.py
new file mode 100644
--- /dev/null
+++ b/akvo/rsr/tests/views/test_my_rsr.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+
+"""
+Akvo RSR is covered by the GNU Affero General Public License.
+
+See more details in the license.txt file located at the root folder of the Akvo RSR module.
+For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >.
+"""
+
+from __future__ import print_function
+
+import json
+
+from django.conf import settings
+from django.contrib.auth.models import Group
+from django.test import TestCase, Client
+from lxml import html
+
+from akvo.rsr.models import Employment, Organisation, User
+from akvo.utils import check_auth_groups
+
+
+class MyRSRTestCase(TestCase):
+    """Test my_rsr views."""
+
+    def setUp(self):
+        check_auth_groups(settings.REQUIRED_AUTH_GROUPS)
+        self.c = Client(HTTP_HOST=settings.RSR_DOMAIN)
+        self.password = 'password'
+        self.user1 = self._create_user('user1@example.com', self.password, is_admin=True)
+        self.user2 = self._create_user('user2@example.com', self.password, is_admin=True)
+        self.org = Organisation.objects.create(name='akvo', long_name='akvo foundation')
+        self.c.login(username=self.user1.username, password=self.password)
+        self.admin_group = Group.objects.get(name='Admins')
+        self.user_group = Group.objects.get(name='Users')
+
+    def test_user_management_employments_ordering(self):
+        # Given
+        Employment.objects.create(user=self.user1, organisation=self.org, group=self.user_group)
+        Employment.objects.create(user=self.user2, organisation=self.org, group=self.user_group)
+        Employment.objects.create(user=self.user1, organisation=self.org, group=self.admin_group)
+        Employment.objects.create(user=self.user2, organisation=self.org, group=self.admin_group)
+
+        # When
+        response = self.c.get('/myrsr/user_management', follow=True)
+
+        # Then
+        self.assertEqual(response.status_code, 200)
+        doc = html.fromstring(response.content)
+        script = doc.get_element_by_id('initial-employment-data')
+        employments = json.loads(script.text)
+        self.assertEqual(4, len(employments))
+        self.assertEqual([(e['user']['id'], e['group']['name']) for e in employments],
+                         [(self.user2.id, u'Admins'),
+                          (self.user2.id, u'Users'),
+                          (self.user1.id, u'Admins'),
+                          (self.user1.id, u'Users')])
+
+    def _create_user(self, email, password, is_active=True, is_admin=False):
+        """Create a user with the given email and password."""
+
+        user = User.objects.create(
+            email=email,
+            username=email,
+            is_active=is_active,
+            is_admin=is_admin
+        )
+        user.set_password(password)
+        user.save()
+
+        return user