diff --git a/zerver/lib/actions.py b/zerver/lib/actions.py index eff95c07f5..6aa6e1baa6 100644 --- a/zerver/lib/actions.py +++ b/zerver/lib/actions.py @@ -40,6 +40,7 @@ from zerver.lib.emoji import emoji_name_to_emoji_code, get_emoji_file_name from zerver.lib.exceptions import StreamDoesNotExistError, \ StreamWithIDDoesNotExistError from zerver.lib.export import get_realm_exports_serialized +from zerver.lib.external_accounts import DEFAULT_EXTERNAL_ACCOUNTS from zerver.lib.hotspots import get_next_hotspots from zerver.lib.message import ( access_message, @@ -5376,6 +5377,19 @@ def notify_realm_custom_profile_fields(realm: Realm, operation: str) -> None: fields=[f.as_dict() for f in fields]) send_event(realm, event, active_user_ids(realm.id)) +def try_add_realm_default_custom_profile_field(realm: Realm, + field_subtype: str) -> CustomProfileField: + field_data = DEFAULT_EXTERNAL_ACCOUNTS[field_subtype] + field = CustomProfileField(realm=realm, name=field_data['name'], + field_type=CustomProfileField.EXTERNAL_ACCOUNT, + hint=field_data['hint'], + field_data=ujson.dumps(dict(subtype=field_subtype))) + field.save() + field.order = field.id + field.save(update_fields=['order']) + notify_realm_custom_profile_fields(realm, 'add') + return field + def try_add_realm_custom_profile_field(realm: Realm, name: str, field_type: int, hint: str='', field_data: Optional[ProfileFieldData]=None) -> CustomProfileField: diff --git a/zerver/lib/external_accounts.py b/zerver/lib/external_accounts.py index be6102201b..1548da31fb 100644 --- a/zerver/lib/external_accounts.py +++ b/zerver/lib/external_accounts.py @@ -8,14 +8,26 @@ from zerver.lib.validator import check_required_string, \ check_external_account_url_pattern, check_dict_only from zerver.lib.types import ProfileFieldData +# Default external account fields are by default avaliable +# to realm admins, where realm admin only need to select +# the default field and other values(i.e. name, url) will be +# fetch from this dictionary. +# text: Field text for admins - custom profile field in org settngs view +# name: Field label or name - user profile in user settings view +# hint: Field hint for realm users +# url_patter: Field url linkifier DEFAULT_EXTERNAL_ACCOUNTS = { "twitter": { "text": "Twitter", - "url_pattern": "https://twitter.com/%(username)s" + "url_pattern": "https://twitter.com/%(username)s", + "name": "Twitter", + "hint": "Enter your Twitter username", }, "github": { "text": 'GitHub', - "url_pattern": "https://github.com/%(username)s" + "url_pattern": "https://github.com/%(username)s", + "name": "GitHub", + "hint": "Enter your GitHub username", }, } diff --git a/zerver/tests/test_custom_profile_data.py b/zerver/tests/test_custom_profile_data.py index dfb175d0f4..a1d1cc45ac 100644 --- a/zerver/tests/test_custom_profile_data.py +++ b/zerver/tests/test_custom_profile_data.py @@ -9,6 +9,7 @@ from zerver.lib.test_classes import ZulipTestCase from zerver.lib.bugdown import convert as bugdown_convert from zerver.models import CustomProfileField, \ custom_profile_fields_for_realm, CustomProfileFieldValue, get_realm +from zerver.lib.external_accounts import DEFAULT_EXTERNAL_ACCOUNTS import ujson class CustomProfileFieldTestCase(ZulipTestCase): @@ -120,6 +121,53 @@ class CreateCustomProfileFieldTest(CustomProfileFieldTestCase): result = self.client_post("/json/realm/profile_fields", info=data) self.assert_json_success(result) + def test_create_default_external_account_field(self) -> None: + self.login(self.example_email("iago")) + realm = get_realm("zulip") + field_type = CustomProfileField.EXTERNAL_ACCOUNT # type: int + field_data = ujson.dumps({ + 'subtype': 'twitter' + }) # type: str + invalid_field_name = "Not required field name" # type: str + invalid_field_hint = "Not required field hint" # type: str + + result = self.client_post("/json/realm/profile_fields", + info=dict( + field_type=field_type, + field_data=field_data, + hint=invalid_field_hint, + name=invalid_field_name, + )) + self.assert_json_success(result) + # Sliently overwrite name and hint with values set in default fields dict + # for default custom external account fields. + with self.assertRaises(CustomProfileField.DoesNotExist): + field = CustomProfileField.objects.get(name=invalid_field_name, realm=realm) + # The field is created with 'Twitter' name as per values in default fields dict + field = CustomProfileField.objects.get(name='Twitter') + self.assertEqual(field.name, DEFAULT_EXTERNAL_ACCOUNTS['twitter']['name']) + self.assertEqual(field.hint, DEFAULT_EXTERNAL_ACCOUNTS['twitter']['hint']) + + result = self.client_delete("/json/realm/profile_fields/{}".format(field.id)) + self.assert_json_success(result) + + # Should also work without name or hint and only external field type and subtype data + result = self.client_post("/json/realm/profile_fields", + info=dict(field_type=field_type, field_data=field_data)) + self.assert_json_success(result) + + # Default external account field data cannot be updated + field = CustomProfileField.objects.get(name="Twitter", realm=realm) + result = self.client_patch( + "/json/realm/profile_fields/{}".format(field.id), + info={'name': 'Twitter username', + 'field_type': CustomProfileField.EXTERNAL_ACCOUNT} + ) + self.assert_json_error(result, 'Default custom field cannot be updated.') + + result = self.client_delete("/json/realm/profile_fields/{}".format(field.id)) + self.assert_json_success(result) + def test_create_external_account_field(self) -> None: self.login(self.example_email("iago")) realm = get_realm('zulip') @@ -297,7 +345,6 @@ class UpdateCustomProfileFieldTest(CustomProfileFieldTestCase): def test_update(self) -> None: self.login(self.example_email("iago")) realm = get_realm('zulip') - result = self.client_patch( "/json/realm/profile_fields/100", info={'name': 'Phone Number', diff --git a/zerver/views/custom_profile_fields.py b/zerver/views/custom_profile_fields.py index 20fe677616..c611e60d1b 100644 --- a/zerver/views/custom_profile_fields.py +++ b/zerver/views/custom_profile_fields.py @@ -12,6 +12,7 @@ from zerver.lib.actions import (try_add_realm_custom_profile_field, try_update_realm_custom_profile_field, do_update_user_custom_profile_data, try_reorder_realm_custom_profile_fields, + try_add_realm_default_custom_profile_field, check_remove_custom_profile_field_value) from zerver.lib.response import json_success, json_error from zerver.lib.types import ProfileFieldData @@ -57,13 +58,24 @@ def validate_custom_field_data(field_type: int, if error: raise JsonableError(error) +def is_default_external_field(field_type: int, field_data: ProfileFieldData) -> bool: + if field_type != CustomProfileField.EXTERNAL_ACCOUNT: + return False + if field_data['subtype'] == 'custom': + return False + return True + def validate_custom_profile_field(name: str, hint: str, field_type: int, field_data: ProfileFieldData) -> None: # Validate field data validate_custom_field_data(field_type, field_data) - # Validate field name, hint and type - validate_field_name_and_hint(name, hint) + if not is_default_external_field(field_type, field_data): + # If field is default external field then we will fetch all data + # from our default field dictionary, so no need to validate name or hint + # Validate field name, hint if not default external account field + validate_field_name_and_hint(name, hint) + field_types = [i[0] for i in CustomProfileField.FIELD_TYPE_CHOICES] if field_type not in field_types: raise JsonableError(_("Invalid field type.")) @@ -72,22 +84,30 @@ def validate_custom_profile_field(name: str, hint: str, field_type: int, @has_request_variables def create_realm_custom_profile_field(request: HttpRequest, user_profile: UserProfile, - name: str=REQ(), + name: str=REQ(default=''), hint: str=REQ(default=''), field_data: ProfileFieldData=REQ(default={}, converter=ujson.loads), field_type: int=REQ(validator=check_int)) -> HttpResponse: - validate_custom_profile_field(name, hint, field_type, field_data) try: - field = try_add_realm_custom_profile_field( - realm=user_profile.realm, - name=name, - field_data=field_data, - field_type=field_type, - hint=hint, - ) - return json_success({'id': field.id}) + if is_default_external_field(field_type, field_data): + field_subtype = '' # type: str + field_subtype = field_data['subtype'] # type: ignore # key for "Union[Dict[str, str], str]" can be str + field = try_add_realm_default_custom_profile_field( + realm=user_profile.realm, + field_subtype=field_subtype, + ) + return json_success({'id': field.id}) + else: + field = try_add_realm_custom_profile_field( + realm=user_profile.realm, + name=name, + field_data=field_data, + field_type=field_type, + hint=hint, + ) + return json_success({'id': field.id}) except IntegrityError: return json_error(_("A field with that label already exists.")) @@ -107,7 +127,7 @@ def delete_realm_custom_profile_field(request: HttpRequest, user_profile: UserPr @has_request_variables def update_realm_custom_profile_field(request: HttpRequest, user_profile: UserProfile, field_id: int, - name: str=REQ(), + name: str=REQ(default=''), hint: str=REQ(default=''), field_data: ProfileFieldData=REQ(default={}, converter=ujson.loads), @@ -118,6 +138,10 @@ def update_realm_custom_profile_field(request: HttpRequest, user_profile: UserPr except CustomProfileField.DoesNotExist: return json_error(_('Field id {id} not found.').format(id=field_id)) + if field.field_type == CustomProfileField.EXTERNAL_ACCOUNT: + if is_default_external_field(field.field_type, ujson.loads(field.field_data)): + return json_error(_("Default custom field cannot be updated.")) + validate_custom_profile_field(name, hint, field.field_type, field_data) try: try_update_realm_custom_profile_field(realm, field, name, hint=hint, diff --git a/zilencer/management/commands/populate_db.py b/zilencer/management/commands/populate_db.py index 436dd5e146..f6161a6034 100644 --- a/zilencer/management/commands/populate_db.py +++ b/zilencer/management/commands/populate_db.py @@ -15,7 +15,7 @@ from django.utils.timezone import timedelta as timezone_timedelta from zerver.lib.actions import STREAM_ASSIGNMENT_COLORS, check_add_realm_emoji, \ do_change_is_admin, do_send_messages, do_update_user_custom_profile_data, \ - try_add_realm_custom_profile_field + try_add_realm_custom_profile_field, try_add_realm_default_custom_profile_field from zerver.lib.bulk_create import bulk_create_streams, bulk_create_users from zerver.lib.cache import cache_set from zerver.lib.generate_test_data import create_test_data @@ -360,12 +360,7 @@ class Command(BaseCommand): hint="Or your personal blog's URL") mentor = try_add_realm_custom_profile_field(zulip_realm, "Mentor", CustomProfileField.USER) - external_account_field_data = { - 'subtype': 'github' - } # type: ProfileFieldData - github_profile = try_add_realm_custom_profile_field(zulip_realm, "GitHub", - CustomProfileField.EXTERNAL_ACCOUNT, - field_data=external_account_field_data) + github_profile = try_add_realm_default_custom_profile_field(zulip_realm, "github") # Fill in values for Iago and Hamlet hamlet = get_user("hamlet@zulip.com", zulip_realm)