You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by ma...@apache.org on 2019/12/12 23:18:40 UTC

[airavata-django-portal] branch airavata-3243 created (now 0a7ba8d)

This is an automated email from the ASF dual-hosted git repository.

machristie pushed a change to branch airavata-3243
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git.


      at 0a7ba8d  AIRAVATA-3243 Set Reply-To to PORTAL_ADMINS

This branch includes the following new commits:

     new 8c88a56  Ignore sqlite3 dbs
     new 06664cd  AIRAVATA-3243 Send email to user when added to group
     new ade0d51  AIRAVATA-3243 Wrap links in a tags
     new ee2c247  AIRAVATA-3243 auth: activate signal receivers on startup
     new ad4b2e4  AIRAVATA-3243 Add Save button to groups editor
     new a7452b4  AIRAVATA-3243 Send one email when user added to multiple groups at once
     new 0a7ba8d  AIRAVATA-3243 Set Reply-To to PORTAL_ADMINS

The 7 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[airavata-django-portal] 03/07: AIRAVATA-3243 Wrap links in a tags

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch airavata-3243
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit ade0d5168e81f2f3c3ef2bb2e9a4e26a5cbba7f8
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Thu Dec 12 15:11:32 2019 -0500

    AIRAVATA-3243 Wrap links in a tags
---
 django_airavata/apps/auth/migrations/0005_auto_20191211_2011.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/django_airavata/apps/auth/migrations/0005_auto_20191211_2011.py b/django_airavata/apps/auth/migrations/0005_auto_20191211_2011.py
index 7ab9bc0..c6533dc 100644
--- a/django_airavata/apps/auth/migrations/0005_auto_20191211_2011.py
+++ b/django_airavata/apps/auth/migrations/0005_auto_20191211_2011.py
@@ -27,12 +27,13 @@ def default_templates(apps, schema_editor):
         <p>
         You may have access to additional applications now that you are a
         member of {{group_name}}. To check what applications you have access
-        to, please check: {{dashboard_url}}.
+        to, please check: <a href="{{dashboard_url}}">{{dashboard_url}}</a>.
         </p>
 
         <p>
         You may also have access to additional experiments. To check what
-        experiments you have access to, please check: {{experiments_url}}.
+        experiments you have access to, please check: <a
+        href="{{experiments_url}}">{{experiments_url}}</a>.
         </p>
 
         <p>


[airavata-django-portal] 07/07: AIRAVATA-3243 Set Reply-To to PORTAL_ADMINS

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch airavata-3243
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit 0a7ba8ddba454f92ef575292956af50e8dcfcc73
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Thu Dec 12 18:01:01 2019 -0500

    AIRAVATA-3243 Set Reply-To to PORTAL_ADMINS
---
 django_airavata/apps/auth/tests.py | 64 ++++++++++++++++----------------------
 django_airavata/apps/auth/utils.py | 12 ++++---
 2 files changed, 34 insertions(+), 42 deletions(-)

diff --git a/django_airavata/apps/auth/tests.py b/django_airavata/apps/auth/tests.py
index 50d4faa..330a18c 100644
--- a/django_airavata/apps/auth/tests.py
+++ b/django_airavata/apps/auth/tests.py
@@ -11,82 +11,72 @@ from . import signals  # noqa
 GATEWAY_ID = "test-gateway"
 SERVER_EMAIL = "admin@test-gateway.com"
 PORTAL_TITLE = "Test Gateway"
-
+PORTAL_ADMINS = [('Portal Admin', 'admin@test-gateway.com')]
 
 @override_settings(
     GATEWAY_ID=GATEWAY_ID,
     SERVER_EMAIL=SERVER_EMAIL,
-    PORTAL_TITLE=PORTAL_TITLE
+    PORTAL_TITLE=PORTAL_TITLE,
+    PORTAL_ADMINS=PORTAL_ADMINS
 )
 class EmailUserAddedToGroupSignalReceiverTests(TestCase):
 
-    def test(self):
+    def setUp(self):
         factory = RequestFactory()
-        request = factory.get("/")
-        user = UserProfile(
+        self.request = factory.get("/")
+        self.user = UserProfile(
             airavataInternalUserId=f"testuser@{GATEWAY_ID}",
             userId="testuser",
             gatewayId=GATEWAY_ID,
             emails=["testuser@example.com"],
             firstName="Test",
             lastName="User")
+
+    def test(self):
         group = GroupModel(id="abc123", name="Test Group")
         user_added_to_group.send(None,
-                                 user=user,
+                                 user=self.user,
                                  groups=[group],
-                                 request=request)
+                                 request=self.request)
         self.assertEqual(len(mail.outbox), 1)
         msg = mail.outbox[0]
+        self._assert_common_email_attributes(msg)
         self.assertEqual(msg.subject,
                          f"You've been added to group "
                          f"[{group.name}] in {PORTAL_TITLE}")
-        self.assertEqual(msg.from_email,
-                         f"{PORTAL_TITLE} <{SERVER_EMAIL}>")
-        self.assertSequenceEqual(
-            msg.to, [f"{user.firstName} {user.lastName} <{user.emails[0]}>"])
-        self.assertIn(
-            request.build_absolute_uri(
-                reverse("django_airavata_workspace:dashboard")),
-            msg.body)
-        self.assertIn(
-            request.build_absolute_uri(
-                reverse("django_airavata_workspace:experiments")),
-            msg.body)
-        self.assertIn(user.userId, msg.body)
 
     def test_multiple_groups(self):
-        factory = RequestFactory()
-        request = factory.get("/")
-        user = UserProfile(
-            airavataInternalUserId=f"testuser@{GATEWAY_ID}",
-            userId="testuser",
-            gatewayId=GATEWAY_ID,
-            emails=["testuser@example.com"],
-            firstName="Test",
-            lastName="User")
         group1 = GroupModel(id="abc123", name="Test Group")
         group2 = GroupModel(id="group2", name="Group 2")
         user_added_to_group.send(None,
-                                 user=user,
+                                 user=self.user,
                                  groups=[group1, group2],
-                                 request=request)
+                                 request=self.request)
         self.assertEqual(len(mail.outbox), 1)
         msg = mail.outbox[0]
+        self._assert_common_email_attributes(msg)
         self.assertEqual(msg.subject,
                          f"You've been added to groups "
                          f"[{group1.name}] and [{group2.name}] "
                          f"in {PORTAL_TITLE}")
+        self.assertIn("groups Test Group and Group 2", msg.body)
+
+    def _assert_common_email_attributes(self, msg):
         self.assertEqual(msg.from_email,
-                         f"{PORTAL_TITLE} <{SERVER_EMAIL}>")
+                         f"\"{PORTAL_TITLE}\" <{SERVER_EMAIL}>")
+        self.assertEqual(
+            msg.reply_to,
+            [f"\"{PORTAL_ADMINS[0][0]}\" <{PORTAL_ADMINS[0][1]}>"])
         self.assertSequenceEqual(
-            msg.to, [f"{user.firstName} {user.lastName} <{user.emails[0]}>"])
+            msg.to, [f"\"{self.user.firstName} {self.user.lastName}\" "
+                     f"<{self.user.emails[0]}>"])
         self.assertIn(
-            request.build_absolute_uri(
+            self.request.build_absolute_uri(
                 reverse("django_airavata_workspace:dashboard")),
             msg.body)
         self.assertIn(
-            request.build_absolute_uri(
+            self.request.build_absolute_uri(
                 reverse("django_airavata_workspace:experiments")),
             msg.body)
-        self.assertIn(user.userId, msg.body)
-        self.assertIn("groups Test Group and Group 2", msg.body)
+        self.assertIn(self.user.userId, msg.body)
+
diff --git a/django_airavata/apps/auth/utils.py b/django_airavata/apps/auth/utils.py
index 50a1e16..0c35a3b 100644
--- a/django_airavata/apps/auth/utils.py
+++ b/django_airavata/apps/auth/utils.py
@@ -109,10 +109,12 @@ def send_email_to_user(template_id, context):
     msg = EmailMessage(
         subject=subject,
         body=body,
-        from_email="{} <{}>".format(settings.PORTAL_TITLE,
-                                    settings.SERVER_EMAIL),
-        to=["{} {} <{}>".format(context['first_name'],
-                                context['last_name'],
-                                context['email'])])
+        from_email="\"{}\" <{}>".format(settings.PORTAL_TITLE,
+                                        settings.SERVER_EMAIL),
+        to=["\"{} {}\" <{}>".format(context['first_name'],
+                                    context['last_name'],
+                                    context['email'])],
+        reply_to=[f"\"{a[0]}\" <{a[1]}>" for a in settings.PORTAL_ADMINS]
+    )
     msg.content_subtype = 'html'
     msg.send()


[airavata-django-portal] 02/07: AIRAVATA-3243 Send email to user when added to group

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch airavata-3243
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit 06664cd54e46b5102786aacb23b201448c61f7c8
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Thu Dec 12 14:36:36 2019 -0500

    AIRAVATA-3243 Send email to user when added to group
---
 .travis.yml                                        |   1 +
 django_airavata/apps/api/signals.py                |   9 +-
 django_airavata/apps/api/tests.py                  | 274 ++++++++++++++++++++-
 django_airavata/apps/api/views.py                  |  25 ++
 .../auth/migrations/0005_auto_20191211_2011.py     |  65 +++++
 django_airavata/apps/auth/models.py                |   2 +
 django_airavata/apps/auth/signals.py               |  25 ++
 django_airavata/apps/auth/tests.py                 |  52 +++-
 django_airavata/apps/auth/utils.py                 |  16 ++
 django_airavata/apps/auth/views.py                 |  24 +-
 django_airavata/settings.py                        |   1 +
 11 files changed, 469 insertions(+), 25 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 9b65b65..0029b02 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,6 +7,7 @@ script:
   - cp django_airavata/settings_local.py.sample django_airavata/settings_local.py
   - python manage.py migrate
   - python manage.py check
+  - python manage.py test
   # For now ignore long line endings
   - flake8 --ignore=E501 .
   - ./lint_js.sh
diff --git a/django_airavata/apps/api/signals.py b/django_airavata/apps/api/signals.py
index 3967e20..e9abfee 100644
--- a/django_airavata/apps/api/signals.py
+++ b/django_airavata/apps/api/signals.py
@@ -1,15 +1,20 @@
-"""Signal receivers for the api app."""
+"""Signal and receivers for the api app."""
 
 import logging
 
 from django.contrib.auth.signals import user_logged_in
-from django.dispatch import receiver
+from django.dispatch import Signal, receiver
 
 from . import data_products_helper
 
 log = logging.getLogger(__name__)
 
 
+# Signals
+user_added_to_group = Signal(providing_args=["user", "group", "request"])
+
+
+# Receivers
 @receiver(user_logged_in)
 def create_user_storage_dir(sender, request, user, **kwargs):
     """Create user's home direct in gateway storage."""
diff --git a/django_airavata/apps/api/tests.py b/django_airavata/apps/api/tests.py
index 4929020..b682ddb 100644
--- a/django_airavata/apps/api/tests.py
+++ b/django_airavata/apps/api/tests.py
@@ -1,2 +1,274 @@
+from unittest.mock import MagicMock, patch
 
-# Create your tests here.
+from django.contrib.auth.models import User
+from django.test import TestCase, override_settings
+from django.urls import reverse
+# from rest_framework import status
+from rest_framework.test import APIRequestFactory, force_authenticate
+
+from airavata.model.appcatalog.gatewaygroups.ttypes import GatewayGroups
+from airavata.model.group.ttypes import GroupModel
+from airavata.model.user.ttypes import UserProfile
+
+from . import signals, views
+
+GATEWAY_ID = "test-gateway"
+
+
+@override_settings(
+    GATEWAY_ID=GATEWAY_ID
+)
+class GroupViewSetTests(TestCase):
+
+    def setUp(self):
+        self.user = User.objects.create_user('testuser')
+        self.factory = APIRequestFactory()
+
+    def test_create_group_sends_user_added_to_group_signal(self):
+
+        url = reverse('django_airavata_api:group-list')
+        data = {
+            "id": None,
+            "name": "test",
+            "description": None,
+            "members": [
+                    f"{self.user.username}@{GATEWAY_ID}",  # owner
+                    f"testuser1@{GATEWAY_ID}"],
+            "admins": []
+        }
+        request = self.factory.post(url, data)
+        force_authenticate(request, self.user)
+
+        # Mock api clients
+        group_manager_mock = MagicMock(name='group_manager')
+        user_profile_mock = MagicMock(name='user_profile')
+        request.profile_service = {
+            'group_manager': group_manager_mock,
+            'user_profile': user_profile_mock,
+        }
+        request.airavata_client = MagicMock(name="airavata_client")
+        request.airavata_client.getGatewayGroups.return_value = GatewayGroups(
+            gatewayId=GATEWAY_ID,
+            adminsGroupId="adminsGroupId",
+            readOnlyAdminsGroupId="readOnlyAdminsGroupId",
+            defaultGatewayUsersGroupId="defaultGatewayUsersGroupId"
+        )
+        request.authz_token = "dummy"
+        request.session = {}
+        group_manager_mock.createGroup.return_value = "abc123"
+        user_profile = UserProfile(
+            airavataInternalUserId=f"testuser1@{GATEWAY_ID}",
+            userId="testuser1",
+            firstName="Test",
+            lastName="User1",
+            emails=["testuser1@example.com"]
+        )
+        user_profile_mock.getUserProfileById.return_value = user_profile
+
+        # Mock signal handler to verify 'user_added_to_group' signal is sent
+        user_added_to_group_handler = MagicMock()
+        signals.user_added_to_group.connect(
+            user_added_to_group_handler,
+            sender=views.GroupViewSet)
+        group_create = views.GroupViewSet.as_view({'post': 'create'})
+        response = group_create(request)
+        self.assertEquals(201, response.status_code)
+        self.assertEquals("abc123", response.data['id'])
+        user_added_to_group_handler.assert_called_once()
+        args, kwargs = user_added_to_group_handler.call_args
+        self.assertEquals("abc123", kwargs["group"].id)
+        self.assertIs(user_profile, kwargs["user"])
+
+    def test_update_group_sends_user_added_to_group_signal(self):
+        url = reverse('django_airavata_api:group-detail',
+                      kwargs={'group_id': 'abc123'})
+        data = {
+            "id": "abc123",
+            "name": "test",
+            "description": None,
+            "members": [
+                    f"{self.user.username}@{GATEWAY_ID}",  # owner
+                    f"testuser1@{GATEWAY_ID}",  # existing member
+                    f"testuser3@{GATEWAY_ID}"],  # new member
+            "admins": []
+        }
+        request = self.factory.put(url, data)
+        force_authenticate(request, self.user)
+
+        # Mock api clients
+        group_manager_mock = MagicMock(name='group_manager')
+        user_profile_mock = MagicMock(name='user_profile')
+        request.profile_service = {
+            'group_manager': group_manager_mock,
+            'user_profile': user_profile_mock,
+        }
+        request.airavata_client = MagicMock(name="airavata_client")
+        request.airavata_client.getGatewayGroups.return_value = GatewayGroups(
+            gatewayId=GATEWAY_ID,
+            adminsGroupId="adminsGroupId",
+            readOnlyAdminsGroupId="readOnlyAdminsGroupId",
+            defaultGatewayUsersGroupId="defaultGatewayUsersGroupId"
+        )
+        request.authz_token = "dummy"
+        request.session = {}
+
+        # mock getGroup
+        group = GroupModel(id="abc123", name="My Group",
+                           ownerId=f"{self.user.username}@{GATEWAY_ID}",
+                           members=[
+                               f"{self.user.username}@{GATEWAY_ID}",  # owner
+                               f"testuser1@{GATEWAY_ID}",  # existing member
+                               f"testuser2@{GATEWAY_ID}",  # new member
+                           ],
+                           admins=[])
+        group_manager_mock.getGroup.return_value = group
+
+        # Only user added is testuser3, so getUserProfileById will be called
+        # for that user
+        user_profile = UserProfile(
+            airavataInternalUserId=f"testuser3@{GATEWAY_ID}",
+            userId="testuser3",
+            firstName="Test",
+            lastName="User3",
+            emails=["testuser3@example.com"]
+        )
+        user_profile_mock.getUserProfileById.return_value = user_profile
+
+        # Mock signal handler to verify 'user_added_to_group' signal is sent
+        user_added_to_group_handler = MagicMock()
+        signals.user_added_to_group.connect(
+            user_added_to_group_handler,
+            sender=views.GroupViewSet)
+        group_update = views.GroupViewSet.as_view({'put': 'update'})
+        response = group_update(request, group_id="abc123")
+        self.assertEquals(200, response.status_code)
+        self.assertEquals("abc123", response.data['id'])
+
+        # verify addUsersToGroup
+        group_manager_mock.addUsersToGroup.assert_called_once()
+        args, kwargs = group_manager_mock.addUsersToGroup.call_args
+        self.assertEqual(args[1], [f"testuser3@{GATEWAY_ID}"])
+
+        # verify removeUsersFromGroup
+        group_manager_mock.removeUsersFromGroup.assert_called_once()
+        args, kwargs = group_manager_mock.removeUsersFromGroup.call_args
+        self.assertEqual(args[1], [f"testuser2@{GATEWAY_ID}"])
+
+        # verify updateGroup
+        group_manager_mock.updateGroup.assert_called_once()
+
+        user_added_to_group_handler.assert_called_once()
+        args, kwargs = user_added_to_group_handler.call_args
+        self.assertEquals("abc123", kwargs["group"].id)
+        self.assertIs(user_profile, kwargs["user"])
+
+
+@override_settings(
+    GATEWAY_ID=GATEWAY_ID
+)
+class IAMUserViewSetTests(TestCase):
+
+    def setUp(self):
+        self.user = User.objects.create_user('testuser')
+        self.factory = APIRequestFactory()
+
+    @patch("django_airavata.apps.api.views.iam_admin_client")
+    def test_update_that_adds_user_to_group_sends_user_added_to_group_signal(
+            self, iam_admin_client):
+
+        username = "testuser1"
+        url = reverse(
+            'django_airavata_api:iam-user-profile-detail',
+            kwargs={'user_id': username})
+        data = {
+            "airavataInternalUserId": f"{username}@{GATEWAY_ID}",
+            "userId": username,
+            "gatewayId": GATEWAY_ID,
+            "email": "testuser1@example.com",
+            "firstName": "Test",
+            "lastName": "User1",
+            "airavataUserProfileExists": True,
+            "enabled": True,
+            "emailVerified": True,
+            "groups": [
+                {"id": "group1", "name": "Group 1"},
+                {"id": "group2", "name": "Group 2"}
+            ]
+        }
+        request = self.factory.put(url, data)
+        force_authenticate(request, self.user)
+        request.is_gateway_admin = True
+
+        # Mock api clients
+        iam_user_profile = UserProfile(
+            airavataInternalUserId=f"testuser1@{GATEWAY_ID}",
+            userId="testuser1",
+            firstName="Test",
+            lastName="User1",
+            emails=["testuser1@example.com"]
+        )
+        iam_admin_client.get_user.return_value = iam_user_profile
+        group_manager_mock = MagicMock(name='group_manager')
+        user_profile_mock = MagicMock(name='user_profile')
+        request.profile_service = {
+            'group_manager': group_manager_mock,
+            'user_profile': user_profile_mock,
+        }
+        request.authz_token = "dummy"
+        user_profile_mock.doesUserExist.return_value = True
+        user_profile = UserProfile(
+            airavataInternalUserId=f"testuser1@{GATEWAY_ID}",
+            userId="testuser1",
+            firstName="Test",
+            lastName="User1",
+            emails=["testuser1@example.com"]
+        )
+        user_profile_mock.getUserProfileById.return_value = user_profile
+        group_manager_mock.getAllGroupsUserBelongs.return_value = [
+            GroupModel(id="group1")]
+        group = GroupModel(
+            id="group2", name="Group 2"
+        )
+        group_manager_mock.getGroup.return_value = group
+        request.airavata_client = MagicMock(name="airavata_client")
+        request.airavata_client.getGatewayGroups.return_value = GatewayGroups(
+            gatewayId=GATEWAY_ID,
+            adminsGroupId="adminsGroupId",
+            readOnlyAdminsGroupId="readOnlyAdminsGroupId",
+            defaultGatewayUsersGroupId="defaultGatewayUsersGroupId"
+        )
+        request.session = {}
+
+        # Mock signal handler to verify 'user_added_to_group' signal is sent
+        user_added_to_group_handler = MagicMock(
+            name="user_added_to_group_handler")
+        signals.user_added_to_group.connect(
+            user_added_to_group_handler,
+            sender=views.IAMUserViewSet)
+        iam_user_update = views.IAMUserViewSet.as_view({'put': 'update'})
+        response = iam_user_update(request, user_id=username)
+        self.assertEquals(200, response.status_code)
+
+        user_profile_mock.doesUserExist.assert_called_once()
+        group_manager_mock.getAllGroupsUserBelongs.assert_called_once()
+
+        user_profile_mock.getUserProfileById.assert_called_once()
+        args, kwargs = user_profile_mock.getUserProfileById.call_args
+        self.assertSequenceEqual(
+            args, [request.authz_token, "testuser1", GATEWAY_ID])
+
+        group_manager_mock.getGroup.assert_called_once()
+        args, kwargs = group_manager_mock.getGroup.call_args
+        self.assertSequenceEqual(args, [request.authz_token, "group2"])
+
+        group_manager_mock.addUsersToGroup.assert_called_once()
+        args, kwargs = group_manager_mock.addUsersToGroup.call_args
+        self.assertSequenceEqual(
+            args, [request.authz_token, [f"testuser1@{GATEWAY_ID}"], "group2"]
+        )
+
+        user_added_to_group_handler.assert_called_once()
+        args, kwargs = user_added_to_group_handler.call_args
+        self.assertEqual(kwargs["sender"], views.IAMUserViewSet)
+        self.assertEqual(kwargs["user"], user_profile)
+        self.assertEqual(kwargs["group"], group)
diff --git a/django_airavata/apps/api/views.py b/django_airavata/apps/api/views.py
index c5d22cb..f674139 100644
--- a/django_airavata/apps/api/views.py
+++ b/django_airavata/apps/api/views.py
@@ -50,6 +50,7 @@ from . import (
     models,
     output_views,
     serializers,
+    signals,
     thrift_utils,
     tus,
     view_utils
@@ -87,6 +88,8 @@ class GroupViewSet(APIBackedViewSet):
         group_id = self.request.profile_service['group_manager'].createGroup(
             self.authz_token, group)
         group.id = group_id
+        users_added_to_group = set(group.members) - {group.ownerId}
+        self._send_users_added_to_group(users_added_to_group, group)
 
     def perform_update(self, serializer):
         group = serializer.save()
@@ -94,6 +97,7 @@ class GroupViewSet(APIBackedViewSet):
         if len(group._added_members) > 0:
             group_manager_client.addUsersToGroup(
                 self.authz_token, group._added_members, group.id)
+            self._send_users_added_to_group(group._added_members, group)
         if len(group._removed_members) > 0:
             group_manager_client.removeUsersFromGroup(
                 self.authz_token, group._removed_members, group.id)
@@ -110,6 +114,17 @@ class GroupViewSet(APIBackedViewSet):
         group_manager_client.deleteGroup(
             self.authz_token, group.id, group.ownerId)
 
+    def _send_users_added_to_group(self, internal_user_ids, group):
+        for internal_user_id in internal_user_ids:
+            user_id, gateway_id = internal_user_id.rsplit("@", maxsplit=1)
+            user_profile = self.request.profile_service['user_profile'].getUserProfileById(
+                self.authz_token, user_id, gateway_id)
+            signals.user_added_to_group.send(
+                sender=self.__class__,
+                user=user_profile,
+                group=group,
+                request=self.request)
+
 
 class ProjectViewSet(APIBackedViewSet):
     serializer_class = serializers.ProjectSerializer
@@ -1596,10 +1611,20 @@ class IAMUserViewSet(mixins.RetrieveModelMixin,
     def perform_update(self, serializer):
         managed_user_profile = serializer.save()
         group_manager_client = self.request.profile_service['group_manager']
+        user_profile_client = self.request.profile_service['user_profile']
         user_id = managed_user_profile['airavataInternalUserId']
+        username = managed_user_profile['userId']
         for group_id in managed_user_profile['_added_group_ids']:
+            user_profile = user_profile_client.getUserProfileById(
+                self.authz_token, username, settings.GATEWAY_ID)
+            group = group_manager_client.getGroup(self.authz_token, group_id)
             group_manager_client.addUsersToGroup(
                 self.authz_token, [user_id], group_id)
+            signals.user_added_to_group.send(
+                sender=self.__class__,
+                user=user_profile,
+                group=group,
+                request=self.request)
         for group_id in managed_user_profile['_removed_group_ids']:
             group_manager_client.removeUsersFromGroup(
                 self.authz_token, [user_id], group_id)
diff --git a/django_airavata/apps/auth/migrations/0005_auto_20191211_2011.py b/django_airavata/apps/auth/migrations/0005_auto_20191211_2011.py
new file mode 100644
index 0000000..7ab9bc0
--- /dev/null
+++ b/django_airavata/apps/auth/migrations/0005_auto_20191211_2011.py
@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.21 on 2019-12-11 20:11
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+from django_airavata.apps.auth.models import USER_ADDED_TO_GROUP_TEMPLATE
+
+
+def default_templates(apps, schema_editor):
+
+    EmailTemplate = apps.get_model("django_airavata_auth", "EmailTemplate")
+    user_added_to_group_template = EmailTemplate(
+        template_type=USER_ADDED_TO_GROUP_TEMPLATE,
+        subject="You've been added to group [{{group_name}}] in {{portal_title}}",
+        body="""
+        <p>
+        Dear {{first_name}} {{last_name}},
+        </p>
+
+        <p>
+        Your user account (username {{username}}) has been added to the group
+        {{group_name}}. {{portal_title}} uses groups to share applications
+        and experiments.
+        </p>
+
+        <p>
+        You may have access to additional applications now that you are a
+        member of {{group_name}}. To check what applications you have access
+        to, please check: {{dashboard_url}}.
+        </p>
+
+        <p>
+        You may also have access to additional experiments. To check what
+        experiments you have access to, please check: {{experiments_url}}.
+        </p>
+
+        <p>
+        Please let us know if you have any questions.  Thanks.
+        </p>
+        """.strip())
+    user_added_to_group_template.save()
+
+
+def delete_default_templates(apps, schema_editor):
+    EmailTemplate = apps.get_model("django_airavata_auth", "EmailTemplate")
+    EmailTemplate.objects.filter(
+        template_type=USER_ADDED_TO_GROUP_TEMPLATE).delete()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('django_airavata_auth', '0004_password_reset_request'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='emailtemplate',
+            name='template_type',
+            field=models.IntegerField(choices=[(1, 'Verify Email Template'), (2, 'New User Email Template'), (3, 'Password Reset Email Template'), (4, 'User Added to Group Template')], primary_key=True, serialize=False),
+        ),
+        migrations.RunPython(default_templates,
+                             reverse_code=delete_default_templates)
+    ]
diff --git a/django_airavata/apps/auth/models.py b/django_airavata/apps/auth/models.py
index 5e4d109..cfea8c1 100644
--- a/django_airavata/apps/auth/models.py
+++ b/django_airavata/apps/auth/models.py
@@ -5,6 +5,7 @@ from django.db import models
 VERIFY_EMAIL_TEMPLATE = 1
 NEW_USER_EMAIL_TEMPLATE = 2
 PASSWORD_RESET_EMAIL_TEMPLATE = 3
+USER_ADDED_TO_GROUP_TEMPLATE = 4
 
 
 class EmailVerification(models.Model):
@@ -20,6 +21,7 @@ class EmailTemplate(models.Model):
         (VERIFY_EMAIL_TEMPLATE, 'Verify Email Template'),
         (NEW_USER_EMAIL_TEMPLATE, 'New User Email Template'),
         (PASSWORD_RESET_EMAIL_TEMPLATE, 'Password Reset Email Template'),
+        (USER_ADDED_TO_GROUP_TEMPLATE, 'User Added to Group Template'),
     )
     template_type = models.IntegerField(
         primary_key=True, choices=TEMPLATE_TYPE_CHOICES)
diff --git a/django_airavata/apps/auth/signals.py b/django_airavata/apps/auth/signals.py
new file mode 100644
index 0000000..9efe76a
--- /dev/null
+++ b/django_airavata/apps/auth/signals.py
@@ -0,0 +1,25 @@
+from django.conf import settings
+from django.dispatch import receiver
+from django.shortcuts import reverse
+from django.template import Context
+
+from django_airavata.apps.api.signals import user_added_to_group
+
+from . import models, utils
+
+
+@receiver(user_added_to_group, dispatch_uid="auth_email_user_added_to_group")
+def email_user_added_to_group(sender, user, group, request, **kwargs):
+    context = Context({
+        "email": user.emails[0],
+        "first_name": user.firstName,
+        "last_name": user.lastName,
+        "username": user.userId,
+        "portal_title": settings.PORTAL_TITLE,
+        "dashboard_url": request.build_absolute_uri(
+            reverse("django_airavata_workspace:dashboard")),
+        "experiments_url": request.build_absolute_uri(
+            reverse("django_airavata_workspace:experiments")),
+        "group_name": group.name
+    })
+    utils.send_email_to_user(models.USER_ADDED_TO_GROUP_TEMPLATE, context)
diff --git a/django_airavata/apps/auth/tests.py b/django_airavata/apps/auth/tests.py
index 4929020..6d7a033 100644
--- a/django_airavata/apps/auth/tests.py
+++ b/django_airavata/apps/auth/tests.py
@@ -1,2 +1,52 @@
+from django.core import mail
+from django.shortcuts import reverse
+from django.test import RequestFactory, TestCase, override_settings
 
-# Create your tests here.
+from airavata.model.group.ttypes import GroupModel
+from airavata.model.user.ttypes import UserProfile
+from django_airavata.apps.api.signals import user_added_to_group
+
+from . import signals  # noqa
+
+GATEWAY_ID = "test-gateway"
+SERVER_EMAIL = "admin@test-gateway.com"
+PORTAL_TITLE = "Test Gateway"
+
+
+@override_settings(
+    GATEWAY_ID=GATEWAY_ID,
+    SERVER_EMAIL=SERVER_EMAIL,
+    PORTAL_TITLE=PORTAL_TITLE
+)
+class EmailUserAddedToGroupSignalReceiverTests(TestCase):
+
+    def test(self):
+        factory = RequestFactory()
+        request = factory.get("/")
+        user = UserProfile(
+            airavataInternalUserId=f"testuser@{GATEWAY_ID}",
+            userId="testuser",
+            gatewayId=GATEWAY_ID,
+            emails=["testuser@example.com"],
+            firstName="Test",
+            lastName="User")
+        group = GroupModel(id="abc123", name="Test Group")
+        user_added_to_group.send(None, user=user, group=group, request=request)
+        self.assertEqual(len(mail.outbox), 1)
+        msg = mail.outbox[0]
+        self.assertEqual(msg.subject,
+                         f"You've been added to group "
+                         f"[{group.name}] in {PORTAL_TITLE}")
+        self.assertEqual(msg.from_email,
+                         f"{PORTAL_TITLE} <{SERVER_EMAIL}>")
+        self.assertSequenceEqual(
+            msg.to, [f"{user.firstName} {user.lastName} <{user.emails[0]}>"])
+        self.assertIn(
+            request.build_absolute_uri(
+                reverse("django_airavata_workspace:dashboard")),
+            msg.body)
+        self.assertIn(
+            request.build_absolute_uri(
+                reverse("django_airavata_workspace:experiments")),
+            msg.body)
+        self.assertIn(user.userId, msg.body)
diff --git a/django_airavata/apps/auth/utils.py b/django_airavata/apps/auth/utils.py
index e8ce6d0..50a1e16 100644
--- a/django_airavata/apps/auth/utils.py
+++ b/django_airavata/apps/auth/utils.py
@@ -100,3 +100,19 @@ def send_new_user_email(request, username, email, first_name, last_name):
                        to=[a[1] for a in settings.PORTAL_ADMINS])
     msg.content_subtype = 'html'
     msg.send()
+
+
+def send_email_to_user(template_id, context):
+    email_template = models.EmailTemplate.objects.get(pk=template_id)
+    subject = Template(email_template.subject).render(context)
+    body = Template(email_template.body).render(context)
+    msg = EmailMessage(
+        subject=subject,
+        body=body,
+        from_email="{} <{}>".format(settings.PORTAL_TITLE,
+                                    settings.SERVER_EMAIL),
+        to=["{} {} <{}>".format(context['first_name'],
+                                context['last_name'],
+                                context['email'])])
+    msg.content_subtype = 'html'
+    msg.send()
diff --git a/django_airavata/apps/auth/views.py b/django_airavata/apps/auth/views.py
index 47fddb9..4e8decd 100644
--- a/django_airavata/apps/auth/views.py
+++ b/django_airavata/apps/auth/views.py
@@ -7,11 +7,10 @@ from django.conf import settings
 from django.contrib import messages
 from django.contrib.auth import authenticate, login, logout
 from django.core.exceptions import ObjectDoesNotExist
-from django.core.mail import EmailMessage
 from django.forms import ValidationError
 from django.http import HttpResponseBadRequest, JsonResponse
 from django.shortcuts import redirect, render, resolve_url
-from django.template import Context, Template
+from django.template import Context
 from django.urls import reverse
 from requests_oauthlib import OAuth2Session
 
@@ -305,7 +304,7 @@ def _create_and_send_email_verification_link(
         "portal_title": settings.PORTAL_TITLE,
         "url": verification_uri,
     })
-    _send_email_to_user(models.VERIFY_EMAIL_TEMPLATE, context)
+    utils.send_email_to_user(models.VERIFY_EMAIL_TEMPLATE, context)
 
 
 def forgot_password(request):
@@ -371,7 +370,7 @@ def _create_and_send_password_reset_request_link(request, username):
         "portal_title": settings.PORTAL_TITLE,
         "url": verification_uri,
     })
-    _send_email_to_user(models.PASSWORD_RESET_EMAIL_TEMPLATE, context)
+    utils.send_email_to_user(models.PASSWORD_RESET_EMAIL_TEMPLATE, context)
 
 
 def reset_password(request, code):
@@ -423,23 +422,6 @@ def reset_password(request, code):
     })
 
 
-def _send_email_to_user(template_id, context):
-    email_template = models.EmailTemplate.objects.get(
-        pk=template_id)
-    subject = Template(email_template.subject).render(context)
-    body = Template(email_template.body).render(context)
-    msg = EmailMessage(
-        subject=subject,
-        body=body,
-        from_email="{} <{}>".format(settings.PORTAL_TITLE,
-                                    settings.SERVER_EMAIL),
-        to=["{} {} <{}>".format(context['first_name'],
-                                context['last_name'],
-                                context['email'])])
-    msg.content_subtype = 'html'
-    msg.send()
-
-
 def login_desktop(request):
     context = {
         'options': settings.AUTHENTICATION_OPTIONS,
diff --git a/django_airavata/settings.py b/django_airavata/settings.py
index 9efe750..4033af5 100644
--- a/django_airavata/settings.py
+++ b/django_airavata/settings.py
@@ -235,6 +235,7 @@ REST_FRAMEWORK = {
     ),
     'EXCEPTION_HANDLER':
         'django_airavata.apps.api.exceptions.custom_exception_handler',
+    'TEST_REQUEST_DEFAULT_FORMAT': 'json',
 }
 
 AUTHENTICATION_BACKENDS = [


[airavata-django-portal] 06/07: AIRAVATA-3243 Send one email when user added to multiple groups at once

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch airavata-3243
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit a7452b4261effc99005cfa7990a636c6e8397493
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Thu Dec 12 16:58:06 2019 -0500

    AIRAVATA-3243 Send one email when user added to multiple groups at once
---
 django_airavata/apps/api/signals.py                |   2 +-
 django_airavata/apps/api/tests.py                  | 119 ++++++++++++++++++++-
 django_airavata/apps/api/views.py                  |  21 ++--
 .../auth/migrations/0005_auto_20191211_2011.py     |  12 +--
 django_airavata/apps/auth/signals.py               |   4 +-
 django_airavata/apps/auth/tests.py                 |  42 +++++++-
 6 files changed, 177 insertions(+), 23 deletions(-)

diff --git a/django_airavata/apps/api/signals.py b/django_airavata/apps/api/signals.py
index e9abfee..f01df92 100644
--- a/django_airavata/apps/api/signals.py
+++ b/django_airavata/apps/api/signals.py
@@ -11,7 +11,7 @@ log = logging.getLogger(__name__)
 
 
 # Signals
-user_added_to_group = Signal(providing_args=["user", "group", "request"])
+user_added_to_group = Signal(providing_args=["user", "groups", "request"])
 
 
 # Receivers
diff --git a/django_airavata/apps/api/tests.py b/django_airavata/apps/api/tests.py
index b682ddb..bb52a9f 100644
--- a/django_airavata/apps/api/tests.py
+++ b/django_airavata/apps/api/tests.py
@@ -1,4 +1,4 @@
-from unittest.mock import MagicMock, patch
+from unittest.mock import MagicMock, call, patch
 
 from django.contrib.auth.models import User
 from django.test import TestCase, override_settings
@@ -76,7 +76,7 @@ class GroupViewSetTests(TestCase):
         self.assertEquals("abc123", response.data['id'])
         user_added_to_group_handler.assert_called_once()
         args, kwargs = user_added_to_group_handler.call_args
-        self.assertEquals("abc123", kwargs["group"].id)
+        self.assertEquals("abc123", kwargs["groups"][0].id)
         self.assertIs(user_profile, kwargs["user"])
 
     def test_update_group_sends_user_added_to_group_signal(self):
@@ -159,7 +159,7 @@ class GroupViewSetTests(TestCase):
 
         user_added_to_group_handler.assert_called_once()
         args, kwargs = user_added_to_group_handler.call_args
-        self.assertEquals("abc123", kwargs["group"].id)
+        self.assertEquals("abc123", kwargs["groups"][0].id)
         self.assertIs(user_profile, kwargs["user"])
 
 
@@ -271,4 +271,115 @@ class IAMUserViewSetTests(TestCase):
         args, kwargs = user_added_to_group_handler.call_args
         self.assertEqual(kwargs["sender"], views.IAMUserViewSet)
         self.assertEqual(kwargs["user"], user_profile)
-        self.assertEqual(kwargs["group"], group)
+        self.assertEqual(kwargs["groups"][0], group)
+
+    @patch("django_airavata.apps.api.views.iam_admin_client")
+    def test_update_that_adds_user_to_multiple_groups(
+            self, iam_admin_client):
+
+        username = "testuser1"
+        url = reverse(
+            'django_airavata_api:iam-user-profile-detail',
+            kwargs={'user_id': username})
+        data = {
+            "airavataInternalUserId": f"{username}@{GATEWAY_ID}",
+            "userId": username,
+            "gatewayId": GATEWAY_ID,
+            "email": "testuser1@example.com",
+            "firstName": "Test",
+            "lastName": "User1",
+            "airavataUserProfileExists": True,
+            "enabled": True,
+            "emailVerified": True,
+            "groups": [
+                {"id": "group1", "name": "Group 1"},
+                {"id": "group2", "name": "Group 2"},
+                {"id": "group3", "name": "Group 3"},
+            ]
+        }
+        request = self.factory.put(url, data)
+        force_authenticate(request, self.user)
+        request.is_gateway_admin = True
+
+        # Mock api clients
+        iam_user_profile = UserProfile(
+            airavataInternalUserId=f"testuser1@{GATEWAY_ID}",
+            userId="testuser1",
+            firstName="Test",
+            lastName="User1",
+            emails=["testuser1@example.com"]
+        )
+        iam_admin_client.get_user.return_value = iam_user_profile
+        group_manager_mock = MagicMock(name='group_manager')
+        user_profile_mock = MagicMock(name='user_profile')
+        request.profile_service = {
+            'group_manager': group_manager_mock,
+            'user_profile': user_profile_mock,
+        }
+        request.authz_token = "dummy"
+        user_profile_mock.doesUserExist.return_value = True
+        user_profile = UserProfile(
+            airavataInternalUserId=f"testuser1@{GATEWAY_ID}",
+            userId="testuser1",
+            firstName="Test",
+            lastName="User1",
+            emails=["testuser1@example.com"]
+        )
+        user_profile_mock.getUserProfileById.return_value = user_profile
+        group_manager_mock.getAllGroupsUserBelongs.return_value = [
+            GroupModel(id="group1")]
+
+        def side_effect(authz_token, group_id):
+            if group_id == "group2":
+                return GroupModel(id="group2", name="Group 2")
+            elif group_id == "group3":
+                return GroupModel(id="group3", name="Group 3")
+            else:
+                raise Exception("Unexpected group id: " + group_id)
+
+        group_manager_mock.getGroup.side_effect = side_effect
+        request.airavata_client = MagicMock(name="airavata_client")
+        request.airavata_client.getGatewayGroups.return_value = GatewayGroups(
+            gatewayId=GATEWAY_ID,
+            adminsGroupId="adminsGroupId",
+            readOnlyAdminsGroupId="readOnlyAdminsGroupId",
+            defaultGatewayUsersGroupId="defaultGatewayUsersGroupId"
+        )
+        request.session = {}
+
+        # Mock signal handler to verify 'user_added_to_group' signal is sent
+        user_added_to_group_handler = MagicMock(
+            name="user_added_to_group_handler")
+        signals.user_added_to_group.connect(
+            user_added_to_group_handler,
+            sender=views.IAMUserViewSet)
+        iam_user_update = views.IAMUserViewSet.as_view({'put': 'update'})
+        response = iam_user_update(request, user_id=username)
+        self.assertEquals(200, response.status_code)
+
+        user_profile_mock.doesUserExist.assert_called_once()
+        group_manager_mock.getAllGroupsUserBelongs.assert_called_once()
+
+        user_profile_mock.getUserProfileById.assert_called_once()
+        args, kwargs = user_profile_mock.getUserProfileById.call_args
+        self.assertSequenceEqual(
+            args, [request.authz_token, "testuser1", GATEWAY_ID])
+
+        group_manager_mock.getGroup.assert_has_calls([
+            call(request.authz_token, "group2"),
+            call(request.authz_token, "group3")
+        ], any_order=True)
+
+        group_manager_mock.addUsersToGroup.assert_has_calls([
+            call(request.authz_token, [f"testuser1@{GATEWAY_ID}"], "group2"),
+            call(request.authz_token, [f"testuser1@{GATEWAY_ID}"], "group3"),
+        ], any_order=True)
+
+        # user_added_to_group signal should only be called once, with both
+        # groups passed to it
+        user_added_to_group_handler.assert_called_once()
+        args, kwargs = user_added_to_group_handler.call_args
+        self.assertEqual(kwargs["sender"], views.IAMUserViewSet)
+        self.assertEqual(kwargs["user"], user_profile)
+        self.assertSetEqual({"group2", "group3"},
+                            {g.id for g in kwargs["groups"]})
diff --git a/django_airavata/apps/api/views.py b/django_airavata/apps/api/views.py
index f674139..02d6701 100644
--- a/django_airavata/apps/api/views.py
+++ b/django_airavata/apps/api/views.py
@@ -122,7 +122,7 @@ class GroupViewSet(APIBackedViewSet):
             signals.user_added_to_group.send(
                 sender=self.__class__,
                 user=user_profile,
-                group=group,
+                groups=[group],
                 request=self.request)
 
 
@@ -1613,18 +1613,21 @@ class IAMUserViewSet(mixins.RetrieveModelMixin,
         group_manager_client = self.request.profile_service['group_manager']
         user_profile_client = self.request.profile_service['user_profile']
         user_id = managed_user_profile['airavataInternalUserId']
-        username = managed_user_profile['userId']
+        user_profile = user_profile_client.getUserProfileById(
+            self.authz_token,
+            managed_user_profile['userId'],
+            settings.GATEWAY_ID)
+        added_groups = []
         for group_id in managed_user_profile['_added_group_ids']:
-            user_profile = user_profile_client.getUserProfileById(
-                self.authz_token, username, settings.GATEWAY_ID)
             group = group_manager_client.getGroup(self.authz_token, group_id)
             group_manager_client.addUsersToGroup(
                 self.authz_token, [user_id], group_id)
-            signals.user_added_to_group.send(
-                sender=self.__class__,
-                user=user_profile,
-                group=group,
-                request=self.request)
+            added_groups.append(group)
+        signals.user_added_to_group.send(
+            sender=self.__class__,
+            user=user_profile,
+            groups=added_groups,
+            request=self.request)
         for group_id in managed_user_profile['_removed_group_ids']:
             group_manager_client.removeUsersFromGroup(
                 self.authz_token, [user_id], group_id)
diff --git a/django_airavata/apps/auth/migrations/0005_auto_20191211_2011.py b/django_airavata/apps/auth/migrations/0005_auto_20191211_2011.py
index c6533dc..eeb4b99 100644
--- a/django_airavata/apps/auth/migrations/0005_auto_20191211_2011.py
+++ b/django_airavata/apps/auth/migrations/0005_auto_20191211_2011.py
@@ -12,22 +12,22 @@ def default_templates(apps, schema_editor):
     EmailTemplate = apps.get_model("django_airavata_auth", "EmailTemplate")
     user_added_to_group_template = EmailTemplate(
         template_type=USER_ADDED_TO_GROUP_TEMPLATE,
-        subject="You've been added to group [{{group_name}}] in {{portal_title}}",
+        subject="You've been added to group{{ group_names|length|pluralize }} [{{group_names|join:'] and ['}}] in {{portal_title}}",
         body="""
         <p>
         Dear {{first_name}} {{last_name}},
         </p>
 
         <p>
-        Your user account (username {{username}}) has been added to the group
-        {{group_name}}. {{portal_title}} uses groups to share applications
-        and experiments.
+        Your user account (username {{username}}) has been added to the
+        group{{ group_names|length|pluralize }} {{group_names|join:' and '}}.
+        {{portal_title}} uses groups to share applications and experiments.
         </p>
 
         <p>
         You may have access to additional applications now that you are a
-        member of {{group_name}}. To check what applications you have access
-        to, please check: <a href="{{dashboard_url}}">{{dashboard_url}}</a>.
+        member of {{group_names|join:' and '}}. To check what applications you
+        have access to, please check: <a href="{{dashboard_url}}">{{dashboard_url}}</a>.
         </p>
 
         <p>
diff --git a/django_airavata/apps/auth/signals.py b/django_airavata/apps/auth/signals.py
index 9efe76a..32f3cb0 100644
--- a/django_airavata/apps/auth/signals.py
+++ b/django_airavata/apps/auth/signals.py
@@ -9,7 +9,7 @@ from . import models, utils
 
 
 @receiver(user_added_to_group, dispatch_uid="auth_email_user_added_to_group")
-def email_user_added_to_group(sender, user, group, request, **kwargs):
+def email_user_added_to_group(sender, user, groups, request, **kwargs):
     context = Context({
         "email": user.emails[0],
         "first_name": user.firstName,
@@ -20,6 +20,6 @@ def email_user_added_to_group(sender, user, group, request, **kwargs):
             reverse("django_airavata_workspace:dashboard")),
         "experiments_url": request.build_absolute_uri(
             reverse("django_airavata_workspace:experiments")),
-        "group_name": group.name
+        "group_names": [g.name for g in groups]
     })
     utils.send_email_to_user(models.USER_ADDED_TO_GROUP_TEMPLATE, context)
diff --git a/django_airavata/apps/auth/tests.py b/django_airavata/apps/auth/tests.py
index 6d7a033..50d4faa 100644
--- a/django_airavata/apps/auth/tests.py
+++ b/django_airavata/apps/auth/tests.py
@@ -31,7 +31,10 @@ class EmailUserAddedToGroupSignalReceiverTests(TestCase):
             firstName="Test",
             lastName="User")
         group = GroupModel(id="abc123", name="Test Group")
-        user_added_to_group.send(None, user=user, group=group, request=request)
+        user_added_to_group.send(None,
+                                 user=user,
+                                 groups=[group],
+                                 request=request)
         self.assertEqual(len(mail.outbox), 1)
         msg = mail.outbox[0]
         self.assertEqual(msg.subject,
@@ -50,3 +53,40 @@ class EmailUserAddedToGroupSignalReceiverTests(TestCase):
                 reverse("django_airavata_workspace:experiments")),
             msg.body)
         self.assertIn(user.userId, msg.body)
+
+    def test_multiple_groups(self):
+        factory = RequestFactory()
+        request = factory.get("/")
+        user = UserProfile(
+            airavataInternalUserId=f"testuser@{GATEWAY_ID}",
+            userId="testuser",
+            gatewayId=GATEWAY_ID,
+            emails=["testuser@example.com"],
+            firstName="Test",
+            lastName="User")
+        group1 = GroupModel(id="abc123", name="Test Group")
+        group2 = GroupModel(id="group2", name="Group 2")
+        user_added_to_group.send(None,
+                                 user=user,
+                                 groups=[group1, group2],
+                                 request=request)
+        self.assertEqual(len(mail.outbox), 1)
+        msg = mail.outbox[0]
+        self.assertEqual(msg.subject,
+                         f"You've been added to groups "
+                         f"[{group1.name}] and [{group2.name}] "
+                         f"in {PORTAL_TITLE}")
+        self.assertEqual(msg.from_email,
+                         f"{PORTAL_TITLE} <{SERVER_EMAIL}>")
+        self.assertSequenceEqual(
+            msg.to, [f"{user.firstName} {user.lastName} <{user.emails[0]}>"])
+        self.assertIn(
+            request.build_absolute_uri(
+                reverse("django_airavata_workspace:dashboard")),
+            msg.body)
+        self.assertIn(
+            request.build_absolute_uri(
+                reverse("django_airavata_workspace:experiments")),
+            msg.body)
+        self.assertIn(user.userId, msg.body)
+        self.assertIn("groups Test Group and Group 2", msg.body)


[airavata-django-portal] 05/07: AIRAVATA-3243 Add Save button to groups editor

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch airavata-3243
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit ad4b2e4ea61105d424072444bea016162b92ad74
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Thu Dec 12 15:45:05 2019 -0500

    AIRAVATA-3243 Add Save button to groups editor
---
 .../src/components/users/UserDetailsContainer.vue  | 50 ++++++++++++++++++----
 1 file changed, 41 insertions(+), 9 deletions(-)

diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserDetailsContainer.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserDetailsContainer.vue
index 9bd2eba..77fab42 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserDetailsContainer.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserDetailsContainer.vue
@@ -1,14 +1,20 @@
 <template>
   <div>
-    <user-group-membership-editor
-      v-if="iamUserProfile.airavataUserProfileExists"
-      v-model="localIAMUserProfile.groups"
-      :editable-groups="editableGroups"
-      :airavata-internal-user-id="iamUserProfile.airavataInternalUserId"
-      @input="groupsUpdated"
-    />
+    <b-card header="Edit Groups">
+      <user-group-membership-editor
+        v-if="iamUserProfile.airavataUserProfileExists"
+        v-model="localIAMUserProfile.groups"
+        :editable-groups="editableGroups"
+        :airavata-internal-user-id="iamUserProfile.airavataInternalUserId"
+      />
+      <b-button @click="groupsUpdated" variant="primary" :disabled="!areGroupsUpdated">Save</b-button>
+    </b-card>
     <activate-user-panel
-      v-if="iamUserProfile.enabled && iamUserProfile.emailVerified && !iamUserProfile.airavataUserProfileExists"
+      v-if="
+        iamUserProfile.enabled &&
+          iamUserProfile.emailVerified &&
+          !iamUserProfile.airavataUserProfileExists
+      "
       :username="iamUserProfile.userId"
       @activate-user="$emit('enable-user', $event)"
     />
@@ -64,7 +70,33 @@ export default {
     groupsUpdated() {
       this.$emit("groups-updated", this.localIAMUserProfile);
     }
+  },
+  computed: {
+    currentGroupIds() {
+      const groupIds = this.iamUserProfile.groups.map(g => g.id);
+      groupIds.sort();
+      return groupIds;
+    },
+    updatedGroupIds() {
+      const groupIds = this.localIAMUserProfile.groups.map(g => g.id);
+      groupIds.sort();
+      return groupIds;
+    },
+    areGroupsUpdated() {
+      for (const groupId of this.currentGroupIds) {
+        // Check if a group was removed
+        if (this.updatedGroupIds.indexOf(groupId) < 0) {
+          return true;
+        }
+      }
+      for (const groupId of this.updatedGroupIds) {
+        // Check if a group was added
+        if (this.currentGroupIds.indexOf(groupId) < 0) {
+          return true;
+        }
+      }
+      return false;
+    }
   }
 };
 </script>
-


[airavata-django-portal] 01/07: Ignore sqlite3 dbs

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch airavata-3243
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit 8c88a56b9e8deb25fdd13471351055bea06eb336
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Thu Dec 12 14:37:41 2019 -0500

    Ignore sqlite3 dbs
---
 .gitignore | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.gitignore b/.gitignore
index 5124db2..65af29f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
 *.pyc
-db.sqlite3
+*.sqlite3
 settings_local.py
 settings_local*.py
 .DS_Store


[airavata-django-portal] 04/07: AIRAVATA-3243 auth: activate signal receivers on startup

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch airavata-3243
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit ee2c24786d4a4fe36eb8fc7b285e86067a28b3cd
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Thu Dec 12 15:12:11 2019 -0500

    AIRAVATA-3243 auth: activate signal receivers on startup
---
 django_airavata/apps/auth/apps.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/django_airavata/apps/auth/apps.py b/django_airavata/apps/auth/apps.py
index 1e103a4..cca02f9 100644
--- a/django_airavata/apps/auth/apps.py
+++ b/django_airavata/apps/auth/apps.py
@@ -4,3 +4,6 @@ from django.apps import AppConfig
 class AuthConfig(AppConfig):
     name = 'django_airavata.apps.auth'
     label = 'django_airavata_auth'
+
+    def ready(self):
+        from . import signals  # noqa