diff --git a/zerver/lib/actions.py b/zerver/lib/actions.py index 8196225963..15b59db643 100644 --- a/zerver/lib/actions.py +++ b/zerver/lib/actions.py @@ -4,6 +4,7 @@ from typing import ( ) from mypy_extensions import TypedDict +import django.db.utils from django.contrib.contenttypes.models import ContentType from django.utils.html import escape from django.utils.translation import ugettext as _ @@ -48,6 +49,7 @@ from zerver.lib.topic_mutes import ( add_topic_mute, remove_topic_mute, ) +from zerver.lib.user_groups import create_user_group from zerver.models import Realm, RealmEmoji, Stream, UserProfile, UserActivity, \ RealmDomain, \ Subscription, Recipient, Message, Attachment, UserMessage, RealmAuditLog, \ @@ -4183,3 +4185,10 @@ def do_update_user_custom_profile_data(user_profile, data): update_or_create(user_profile=user_profile, field_id=field['id'], defaults={'value': field['value']}) + +def check_add_user_group(realm, name, initial_members, description): + # type: (Realm, Text, List[UserProfile], Text) -> None + try: + create_user_group(name, initial_members, realm, description=description) + except django.db.utils.IntegrityError: + raise JsonableError(_("User group '%s' already exists." % (name,))) diff --git a/zerver/lib/user_groups.py b/zerver/lib/user_groups.py index 3ef1906ffb..5f4a24e6d4 100644 --- a/zerver/lib/user_groups.py +++ b/zerver/lib/user_groups.py @@ -1,9 +1,18 @@ from __future__ import absolute_import from django.db import transaction +from django.utils.translation import ugettext as _ +from zerver.lib.exceptions import JsonableError from zerver.models import UserProfile, Realm, UserGroupMembership, UserGroup from typing import Dict, Iterable, List, Text +def access_user_group_by_id(user_group_id: int, realm: Realm) -> UserGroup: + try: + user_group = UserGroup.objects.get(id=user_group_id, realm=realm) + except UserGroup.DoesNotExist: + raise JsonableError(_("Invalid user group")) + return user_group + def user_groups_in_realm(realm): # type: (Realm) -> List[UserGroup] user_groups = UserGroup.objects.filter(realm=realm) diff --git a/zerver/lib/users.py b/zerver/lib/users.py index c3a5c40aa1..37d8362aa5 100644 --- a/zerver/lib/users.py +++ b/zerver/lib/users.py @@ -1,10 +1,11 @@ -from typing import Text +from typing import List, Text from django.utils.translation import ugettext as _ from zerver.lib.actions import do_change_full_name from zerver.lib.request import JsonableError -from zerver.models import UserProfile, Service +from zerver.models import UserProfile, Service, Realm, \ + get_user_profile_by_id def check_full_name(full_name_raw): # type: (Text) -> Text @@ -43,3 +44,20 @@ def check_valid_interface_type(interface_type): # type: (int) -> None if interface_type not in Service.ALLOWED_INTERFACE_TYPES: raise JsonableError(_('Invalid interface type')) + +def user_ids_to_users(user_ids: List[int], realm: Realm) -> List[UserProfile]: + # TODO: Change this to do a single bulk query with + # generic_bulk_cached_fetch; it'll be faster. + # + # TODO: Consider adding a flag to control whether deactivated + # users should be included. + user_profiles = [] + for user_id in user_ids: + try: + user_profile = get_user_profile_by_id(user_id) + except UserProfile.DoesNotExist: + raise JsonableError(_("Invalid user ID: %s" % (user_id,))) + if user_profile.realm != realm: + raise JsonableError(_("Invalid user ID: %s" % (user_id,))) + user_profiles.append(user_profile) + return user_profiles diff --git a/zerver/tests/test_user_groups.py b/zerver/tests/test_user_groups.py index f11ae8737e..ec2826694d 100644 --- a/zerver/tests/test_user_groups.py +++ b/zerver/tests/test_user_groups.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from typing import Any, List, Optional, Text +import ujson import django import mock @@ -55,3 +56,39 @@ class UserGroupTestCase(ZulipTestCase): with mock.patch('zerver.lib.user_groups.remove_user_from_user_group', side_effect=Exception): self.assertFalse(check_remove_user_from_user_group(othello, user_group)) + +class UserGroupAPITestCase(ZulipTestCase): + def test_user_group_create(self): + # type: () -> None + hamlet = self.example_user('hamlet') + + # Test success + self.login(self.example_email("hamlet")) + params = { + 'name': 'support', + 'members': ujson.dumps([hamlet.id]), + 'description': 'Support team', + } + result = self.client_post('/json/user_groups/create', info=params) + self.assert_json_success(result) + self.assert_length(UserGroup.objects.all(), 1) + + # Test invalid member error + params = { + 'name': 'backend', + 'members': ujson.dumps([1111]), + 'description': 'Backend team', + } + result = self.client_post('/json/user_groups/create', info=params) + self.assert_json_error(result, "Invalid user ID: 1111") + self.assert_length(UserGroup.objects.all(), 1) + + # Test we cannot add hamlet again + params = { + 'name': 'support', + 'members': ujson.dumps([hamlet.id]), + 'description': 'Support team', + } + result = self.client_post('/json/user_groups/create', info=params) + self.assert_json_error(result, "User group 'support' already exists.") + self.assert_length(UserGroup.objects.all(), 1) diff --git a/zerver/tests/test_users.py b/zerver/tests/test_users.py index da1b23efb2..37777a5781 100644 --- a/zerver/tests/test_users.py +++ b/zerver/tests/test_users.py @@ -24,6 +24,7 @@ from zerver.models import UserProfile, Recipient, \ from zerver.lib.avatar import avatar_url from zerver.lib.email_mirror import create_missed_message_address +from zerver.lib.exceptions import JsonableError from zerver.lib.send_email import send_future_email from zerver.lib.actions import ( get_emails_from_user_ids, @@ -35,6 +36,7 @@ from zerver.lib.actions import ( ) from zerver.lib.topic_mutes import add_topic_mute from zerver.lib.stream_topic import StreamTopicTarget +from zerver.lib.users import user_ids_to_users from django.conf import settings @@ -265,6 +267,18 @@ class UserProfileTest(ZulipTestCase): self.assertFalse(m.called) + def test_user_ids_to_users(self) -> None: + real_user_ids = [ + self.example_user('hamlet').id, + self.example_user('cordelia').id, + ] + + user_ids_to_users(real_user_ids, get_realm("zulip")) + with self.assertRaises(JsonableError): + user_ids_to_users([1234], get_realm("zephyr")) + with self.assertRaises(JsonableError): + user_ids_to_users(real_user_ids, get_realm("zephyr")) + class ActivateTest(ZulipTestCase): def test_basics(self) -> None: user = self.example_user('hamlet') diff --git a/zerver/views/user_groups.py b/zerver/views/user_groups.py new file mode 100644 index 0000000000..60e35133e5 --- /dev/null +++ b/zerver/views/user_groups.py @@ -0,0 +1,22 @@ +from django.http import HttpResponse, HttpRequest + +from typing import List, Text + +from zerver.context_processors import get_realm_from_request +from zerver.lib.actions import check_add_user_group +from zerver.lib.request import has_request_variables, REQ +from zerver.lib.response import json_success, json_error +from zerver.lib.users import user_ids_to_users +from zerver.lib.validator import check_list, check_string, check_int, \ + check_short_string +from zerver.models import UserProfile + +@has_request_variables +def add_user_group(request, user_profile, + name=REQ(), + members=REQ(validator=check_list(check_int), default=[]), + description=REQ()): + # type: (HttpRequest, UserProfile, Text, List[int], Text) -> HttpResponse + user_profiles = user_ids_to_users(members, user_profile.realm) + check_add_user_group(user_profile.realm, name, user_profiles, description) + return json_success() diff --git a/zproject/urls.py b/zproject/urls.py index 0a9c37ab1b..0ee7eca882 100644 --- a/zproject/urls.py +++ b/zproject/urls.py @@ -27,6 +27,7 @@ import zerver.views.zephyr import zerver.views.users import zerver.views.unsubscribe import zerver.views.integrations +import zerver.views.user_groups import zerver.views.user_settings import zerver.views.muting import zerver.views.streams @@ -214,6 +215,10 @@ v1_api_and_json_patterns = [ {'POST': 'zerver.views.push_notifications.add_android_reg_id', 'DELETE': 'zerver.views.push_notifications.remove_android_reg_id'}), + # user_groups -> zerver.views.user_groups + url(r'^user_groups/create$', rest_dispatch, + {'POST': 'zerver.views.user_groups.add_user_group'}), + # users/me -> zerver.views.user_settings url(r'^users/me/api_key/regenerate$', rest_dispatch, {'POST': 'zerver.views.user_settings.regenerate_api_key'}),