diff --git a/frontend_tests/node_tests/settings_profile_fields.js b/frontend_tests/node_tests/settings_profile_fields.js index 38de1e5678..f130afa2a6 100644 --- a/frontend_tests/node_tests/settings_profile_fields.js +++ b/frontend_tests/node_tests/settings_profile_fields.js @@ -7,17 +7,27 @@ set_global('Sortable', {create: () => {}}); const SHORT_TEXT_ID = 1; const CHOICE_ID = 3; +const EXTERNAL_ACCOUNT_ID = 7; + +const SHORT_TEXT_NAME = "Short Text"; +const CHOICE_NAME = "Choice"; +const EXTERNAL_ACCOUNT_NAME = "External account"; page_params.custom_profile_fields = {}; +page_params.realm_default_external_accounts = JSON.stringify({}); page_params.custom_profile_field_types = { SHORT_TEXT: { id: SHORT_TEXT_ID, - name: "Short Text", + name: SHORT_TEXT_NAME, }, CHOICE: { id: CHOICE_ID, - name: "Choice", + name: CHOICE_NAME, + }, + EXTERNAL_ACCOUNT: { + id: EXTERNAL_ACCOUNT_ID, + name: EXTERNAL_ACCOUNT_NAME, }, }; @@ -78,6 +88,25 @@ run_test('populate_profile_fields', () => { }, ]), }, + { + type: EXTERNAL_ACCOUNT_ID, + id: 20, + name: 'github profile', + hint: 'username only', + field_data: JSON.stringify({ + subtype: 'github', + }), + }, + { + type: EXTERNAL_ACCOUNT_ID, + id: 21, + name: 'zulip profile', + hint: 'username only', + field_data: JSON.stringify({ + subtype: 'custom', + url_pattern: 'https://chat.zulip.com/%(username)s', + }), + }, ]; const expected_template_data = [ { @@ -85,25 +114,59 @@ run_test('populate_profile_fields', () => { id: 10, name: 'favorite color', hint: 'blue?', - type: 'Short Text', + type: SHORT_TEXT_NAME, choices: [], is_choice_field: false, + is_external_account_field: false, }, can_modify: true, + realm_default_external_accounts: + page_params.realm_default_external_accounts, }, { profile_field: { id: 30, name: 'meal', hint: 'lunch', - type: 'Choice', + type: CHOICE_NAME, choices: [ {order: 0, value: 0, text: 'lunch'}, {order: 1, value: 1, text: 'dinner'}, ], is_choice_field: true, + is_external_account_field: false, }, can_modify: true, + realm_default_external_accounts: + page_params.realm_default_external_accounts, + }, + { + profile_field: { + id: 20, + name: 'github profile', + hint: 'username only', + type: EXTERNAL_ACCOUNT_NAME, + choices: [], + is_choice_field: false, + is_external_account_field: true, + }, + can_modify: true, + realm_default_external_accounts: + page_params.realm_default_external_accounts, + }, + { + profile_field: { + id: 21, + name: 'zulip profile', + hint: 'username only', + type: EXTERNAL_ACCOUNT_NAME, + choices: [], + is_choice_field: false, + is_external_account_field: true, + }, + can_modify: true, + realm_default_external_accounts: + page_params.realm_default_external_accounts, }, ]; diff --git a/static/js/admin.js b/static/js/admin.js index 8e057ed063..f7f60a2b3f 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -71,6 +71,7 @@ exports.build_page = function () { plan_includes_wide_organization_logo: page_params.plan_includes_wide_organization_logo, upgrade_text_for_wide_organization_logo: page_params.upgrade_text_for_wide_organization_logo, + realm_default_external_accounts: page_params.realm_default_external_accounts, }; options.admin_settings_label = admin_settings_label; diff --git a/static/js/popovers.js b/static/js/popovers.js index 7e136b85cf..73b50af6f6 100644 --- a/static/js/popovers.js +++ b/static/js/popovers.js @@ -104,6 +104,7 @@ function get_custom_profile_field_data(user, field, field_types, dateFormat) { profile_field.name = field.name; profile_field.is_user_field = false; profile_field.is_link = field_type === field_types.URL.id; + profile_field.is_external_account = field_type === field_types.EXTERNAL_ACCOUNT.id; profile_field.type = field_type; switch (field_type) { @@ -124,6 +125,11 @@ function get_custom_profile_field_data(user, field, field_types, dateFormat) { profile_field.value = field_value.value; profile_field.rendered_value = field_value.rendered_value; break; + case field_types.EXTERNAL_ACCOUNT.id: + profile_field.value = field_value.value; + profile_field.field_data = JSON.parse(field.field_data); + profile_field.link = settings_profile_fields.get_external_account_link(profile_field); + break; default: profile_field.value = field_value.value; } diff --git a/static/js/settings_account.js b/static/js/settings_account.js index 83a5fcd89e..349ddb10d2 100644 --- a/static/js/settings_account.js +++ b/static/js/settings_account.js @@ -90,6 +90,7 @@ exports.append_custom_profile_fields = function (element_id, user_id) { all_field_template_types[all_field_types.CHOICE.id] = "choice"; all_field_template_types[all_field_types.USER.id] = "user"; all_field_template_types[all_field_types.DATE.id] = "date"; + all_field_template_types[all_field_types.EXTERNAL_ACCOUNT.id] = "text"; all_custom_fields.forEach(function (field) { var field_value = people.get_custom_profile_data(user_id, field.id); diff --git a/static/js/settings_profile_fields.js b/static/js/settings_profile_fields.js index ced3939acd..ecd84a2eb7 100644 --- a/static/js/settings_profile_fields.js +++ b/static/js/settings_profile_fields.js @@ -76,6 +76,15 @@ function read_choice_field_data_from_form(field_elem) { return field_data; } +function read_external_account_field_data(field_elem) { + var field_data = {}; + field_data.subtype = $(field_elem).find('select[name=external_acc_field_type]').val(); + if (field_data.subtype === "custom") { + field_data.url_pattern = $(field_elem).find('input[name=url_pattern]').val(); + } + return field_data; +} + function update_choice_delete_btn(container, display_flag) { var no_of_choice_row = container.find(".choice-row").length; @@ -106,12 +115,20 @@ function clear_form_data() { create_choice_row($("#profile_field_choices")); update_choice_delete_btn($("#profile_field_choices"), false); $("#profile_field_choices_row").hide(); + // Clear external account field form + $("#custom_field_url_pattern").val(""); + $("#custom_external_account_url_pattern").hide(); + $("#profile_field_external_accounts").hide(); + $("#profile_field_external_accounts_type").val(0); } function read_field_data_from_form(field_type_id, field_elem) { // Only read field data if we are creating a choice field + // or external account field. if (field_type_id === field_types.CHOICE.id) { return read_choice_field_data_from_form(field_elem); + } else if (field_type_id === field_types.EXTERNAL_ACCOUNT.id) { + return read_external_account_field_data(field_elem); } } @@ -121,7 +138,6 @@ function create_profile_field(e) { var field_data = {}; var field_type = $('#profile_field_type').val(); - var opts = { success_continuation: clear_form_data, }; @@ -186,6 +202,15 @@ exports.parse_field_choices_from_field_data = function (field_data) { return choices; }; +function set_up_external_account_field_edit_form(field_elem, url_pattern_val) { + if (field_elem.form.find('select[name=external_acc_field_type]').val() === 'custom') { + field_elem.form.find('input[name=url_pattern]').val(url_pattern_val); + field_elem.form.find('.custom_external_account_detail').show(); + } else { + field_elem.form.find('.custom_external_account_detail').hide(); + } +} + function set_up_choices_field_edit_form(profile_field, field_data) { // Re-render field choices in edit form to load initial choice data var choice_list = profile_field.form.find('.edit_profile_field_choices_container'); @@ -229,6 +254,11 @@ function open_edit_form(e) { set_up_choices_field_edit_form(profile_field, field_data); } + if (parseInt(field.type, 10) === field_types.EXTERNAL_ACCOUNT.id) { + profile_field.form.find('select[name=external_acc_field_type]').val(field_data.subtype); + set_up_external_account_field_edit_form(profile_field, field_data.url_pattern); + } + profile_field.form.find('.reset').on("click", function () { profile_field.form.hide(); profile_field.row.show(); @@ -254,6 +284,11 @@ function open_edit_form(e) { profile_field.form.find(".edit_profile_field_choices_container").on("input", ".choice-row input", add_choice_row); profile_field.form.find(".edit_profile_field_choices_container").on("click", "button.delete-choice", delete_choice_row); + $(".profile_field_external_accounts_edit select").on('change', function (e) { + var field_id = $(e.target).closest('.profile-field-form').attr('data-profile-field-id'); + var field_form = get_profile_field_info(field_id); + set_up_external_account_field_edit_form(field_form, ""); + }); } exports.reset = function () { @@ -307,8 +342,11 @@ exports.do_populate_profile_fields = function (profile_fields_data) { type: exports.field_type_id_to_string(profile_field.type), choices: choices, is_choice_field: profile_field.type === field_types.CHOICE.id, + is_external_account_field: profile_field.type === + field_types.EXTERNAL_ACCOUNT.id, }, can_modify: page_params.is_admin, + realm_default_external_accounts: page_params.realm_default_external_accounts, } ) ); @@ -355,6 +393,46 @@ function set_up_choices_field() { $("#profile_field_choices").on("click", "button.delete-choice", delete_choice_row); } +function set_up_external_account_field() { + var field_elem = $("#profile_field_external_accounts"); + var field_url_pattern_elem = $("#custom_external_account_url_pattern"); + + $('#profile_field_type').on('change', function (e) { + var selected_field_id = parseInt($(e.target).val(), 10); + if (selected_field_id === field_types.EXTERNAL_ACCOUNT.id) { + field_elem.show(); + if ($("#profile_field_external_accounts_type").val() === 'custom') { + field_url_pattern_elem.show(); + } else { + field_url_pattern_elem.hide(); + } + } else { + field_url_pattern_elem.hide(); + field_elem.hide(); + } + }); + $("#profile_field_external_accounts_type").on("change", function () { + if ($("#profile_field_external_accounts_type").val() === 'custom') { + field_url_pattern_elem.show(); + } else { + field_url_pattern_elem.hide(); + } + }); +} + +exports.get_external_account_link = function (field) { + var field_subtype = field.field_data.subtype; + var field_url_pattern; + + if (field_subtype === 'custom') { + field_url_pattern = field.field_data.url_pattern; + } else { + field_url_pattern = + page_params.realm_default_external_accounts[field_subtype].url_pattern; + } + return field_url_pattern.replace('%(username)s', field.value); +}; + exports.set_up = function () { exports.build_page(); exports.maybe_disable_widgets(); @@ -371,6 +449,7 @@ exports.build_page = function () { $("#profile-field-settings").on("click", "#add-custom-profile-field-btn", create_profile_field); $("#admin_profile_fields_table").on("click", ".open-edit-form", open_edit_form); set_up_choices_field(); + set_up_external_account_field(); clear_form_data(); }; diff --git a/static/templates/admin_profile_field_list.handlebars b/static/templates/admin_profile_field_list.handlebars index 01032be962..0fd8fa0d04 100644 --- a/static/templates/admin_profile_field_list.handlebars +++ b/static/templates/admin_profile_field_list.handlebars @@ -48,6 +48,21 @@ {{/if}} + {{#if is_external_account_field}} +
+ + +
+
+ + +
+ {{/if}}
+
+ + +
+
+ + +
diff --git a/static/templates/user_profile_modal.handlebars b/static/templates/user_profile_modal.handlebars index 248d80443a..14e2e18cbf 100644 --- a/static/templates/user_profile_modal.handlebars +++ b/static/templates/user_profile_modal.handlebars @@ -51,6 +51,8 @@ {{else if this.is_link}} {{this.value}} + {{else if this.is_external_account}} + {{this.value}} {{else}} {{#if this.rendered_value}}
{{{this.rendered_value}}}
diff --git a/zerver/lib/actions.py b/zerver/lib/actions.py index a30d46a1bd..9390439c5f 100644 --- a/zerver/lib/actions.py +++ b/zerver/lib/actions.py @@ -5385,7 +5385,8 @@ def try_add_realm_custom_profile_field(realm: Realm, name: str, field_type: int, field_data: Optional[ProfileFieldData]=None) -> CustomProfileField: field = CustomProfileField(realm=realm, name=name, field_type=field_type) field.hint = hint - if field.field_type == CustomProfileField.CHOICE: + if (field.field_type == CustomProfileField.CHOICE or + field.field_type == CustomProfileField.EXTERNAL_ACCOUNT): field.field_data = ujson.dumps(field_data or {}) field.save() @@ -5410,7 +5411,8 @@ def try_update_realm_custom_profile_field(realm: Realm, field: CustomProfileFiel field_data: Optional[ProfileFieldData]=None) -> None: field.name = name field.hint = hint - if field.field_type == CustomProfileField.CHOICE: + if (field.field_type == CustomProfileField.CHOICE or + field.field_type == CustomProfileField.EXTERNAL_ACCOUNT): field.field_data = ujson.dumps(field_data or {}) field.save() notify_realm_custom_profile_fields(realm, 'update') diff --git a/zerver/lib/events.py b/zerver/lib/events.py index c9b2678b9d..5b90f56ef9 100644 --- a/zerver/lib/events.py +++ b/zerver/lib/events.py @@ -53,7 +53,7 @@ from zerver.models import Client, Message, Realm, UserPresence, UserProfile, Cus get_default_stream_groups, CustomProfileField, Stream from zproject.backends import email_auth_enabled, password_auth_enabled from version import ZULIP_VERSION - +from zerver.lib.external_accounts import DEFAULT_EXTERNAL_ACCOUNTS def get_raw_user_data(realm: Realm, client_gravatar: bool) -> Dict[int, Dict[str, str]]: user_dicts = get_realm_user_dicts(realm.id) @@ -218,6 +218,8 @@ def fetch_initial_state_data(user_profile: UserProfile, state['realm_plan_type'] = realm.plan_type state['plan_includes_wide_organization_logo'] = realm.plan_type != Realm.LIMITED state['upgrade_text_for_wide_organization_logo'] = str(Realm.UPGRADE_TEXT_STANDARD) + state['realm_default_external_accounts'] = DEFAULT_EXTERNAL_ACCOUNTS + if realm.notifications_stream and not realm.notifications_stream.deactivated: notifications_stream = realm.notifications_stream state['realm_notifications_stream_id'] = notifications_stream.id diff --git a/zerver/lib/external_accounts.py b/zerver/lib/external_accounts.py new file mode 100644 index 0000000000..1cf87cac6b --- /dev/null +++ b/zerver/lib/external_accounts.py @@ -0,0 +1,39 @@ +""" + This module stores data for "External Account" custom profile field. +""" +from typing import Optional +from django.utils.translation import ugettext as _ + +from zerver.lib.validator import check_required_string, \ + check_url_pattern, check_dict_only +from zerver.lib.types import ProfileFieldData + +DEFAULT_EXTERNAL_ACCOUNTS = { + "twitter": { + "text": "Twitter", + "url_pattern": "https://twitter.com/%(username)s" + }, + "github": { + "text": 'GitHub', + "url_pattern": "https://github.com/%(username)s" + }, +} + +def validate_external_account_field_data(field_data: ProfileFieldData) -> Optional[str]: + field_validator = check_dict_only( + [('subtype', check_required_string)], + [('url_pattern', check_url_pattern)], + ) + error = field_validator('field_data', field_data) + if error: + return error + + field_subtype = field_data.get('subtype') + if field_subtype not in DEFAULT_EXTERNAL_ACCOUNTS.keys(): + if field_subtype == "custom": + if 'url_pattern' not in field_data.keys(): + return _("Custom external account must define url pattern") + else: + return _("Invalid external account type") + + return None diff --git a/zerver/lib/types.py b/zerver/lib/types.py index 05db55c653..a0d1bdec96 100644 --- a/zerver/lib/types.py +++ b/zerver/lib/types.py @@ -24,4 +24,4 @@ UserFieldElement = Tuple[int, str, RealmUserValidator, Callable[[Any], Any], str FieldTypeData = List[Union[FieldElement, ExtendedFieldElement, UserFieldElement]] -ProfileFieldData = Dict[str, Dict[str, str]] +ProfileFieldData = Dict[str, Union[Dict[str, str], str]] diff --git a/zerver/lib/validator.py b/zerver/lib/validator.py index 6d3bfcffca..5e4e962714 100644 --- a/zerver/lib/validator.py +++ b/zerver/lib/validator.py @@ -228,6 +228,21 @@ def check_url(var_name: str, val: object) -> Optional[str]: except ValidationError: return _('%s is not a URL') % (var_name,) +def check_url_pattern(var_name: str, val: object) -> Optional[str]: + error = check_string(var_name, val) + if error: + return error + val = cast(str, val) + + if val.count('%(username)s') != 1: + return _('username should appear exactly once in pattern.') + url_val = val.replace('%(username)s', 'username') + + error = check_url(var_name, url_val) + if error: + return error + return None + def validate_choice_field_data(field_data: ProfileFieldData) -> Optional[str]: """ This function is used to validate the data sent to the server while diff --git a/zerver/migrations/0234_add_external_account_custom_profile_field.py b/zerver/migrations/0234_add_external_account_custom_profile_field.py new file mode 100644 index 0000000000..014d3f2f00 --- /dev/null +++ b/zerver/migrations/0234_add_external_account_custom_profile_field.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-05-30 08:18 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('zerver', '0233_userprofile_avatar_hash'), + ] + + operations = [ + migrations.AlterField( + model_name='customprofilefield', + name='field_type', + field=models.PositiveSmallIntegerField(choices=[(1, 'Short text'), (2, 'Long text'), (4, 'Date picker'), (5, 'Link'), (7, 'External account'), (3, 'List of options'), (6, 'Person picker')], default=1), + ), + ] diff --git a/zerver/models.py b/zerver/models.py index b258a044bc..c87e17e6bb 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -2613,6 +2613,7 @@ class CustomProfileField(models.Model): DATE = 4 URL = 5 USER = 6 + EXTERNAL_ACCOUNT = 7 # These are the fields whose validators require more than var_name # and value argument. i.e. CHOICE require field_data, USER require @@ -2637,6 +2638,7 @@ class CustomProfileField(models.Model): (LONG_TEXT, str(_('Long text')), check_long_string, str, "LONG_TEXT"), (DATE, str(_('Date picker')), check_date, str, "DATE"), (URL, str(_('Link')), check_url, str, "URL"), + (EXTERNAL_ACCOUNT, str(_('External account')), check_short_string, str, "EXTERNAL_ACCOUNT"), ] # type: FieldTypeData ALL_FIELD_TYPES = FIELD_TYPE_DATA + CHOICE_FIELD_TYPE_DATA + USER_FIELD_TYPE_DATA diff --git a/zerver/tests/test_custom_profile_data.py b/zerver/tests/test_custom_profile_data.py index 3c7cc8a7e9..8bbfc37a2e 100644 --- a/zerver/tests/test_custom_profile_data.py +++ b/zerver/tests/test_custom_profile_data.py @@ -141,6 +141,103 @@ class CustomProfileFieldTest(ZulipTestCase): result = self.client_post("/json/realm/profile_fields", info=data) self.assert_json_success(result) + def test_create_external_account_field(self) -> None: + self.login(self.example_email("iago")) + realm = get_realm('zulip') + data = {} # type: Dict[str, Union[str, int, Dict[str, str]]] + data["name"] = "Twitter" + data["field_type"] = CustomProfileField.EXTERNAL_ACCOUNT + + data['field_data'] = 'invalid' + result = self.client_post("/json/realm/profile_fields", info=data) + self.assert_json_error(result, "Bad value for 'field_data': invalid") + + data['field_data'] = ujson.dumps({}) + result = self.client_post("/json/realm/profile_fields", info=data) + self.assert_json_error(result, "subtype key is missing from field_data") + + data["field_data"] = ujson.dumps({ + 'subtype': '' + }) + result = self.client_post("/json/realm/profile_fields", info=data) + self.assert_json_error(result, 'field_data["subtype"] cannot be blank.') + + data["field_data"] = ujson.dumps({ + 'subtype': '123' + }) + result = self.client_post("/json/realm/profile_fields", info=data) + self.assert_json_error(result, 'Invalid external account type') + + non_default_external_account = 'linkedin' + data["field_data"] = ujson.dumps({ + 'subtype': non_default_external_account + }) + result = self.client_post("/json/realm/profile_fields", info=data) + self.assert_json_error(result, 'Invalid external account type') + + data["field_data"] = ujson.dumps({ + 'subtype': 'twitter' + }) + result = self.client_post("/json/realm/profile_fields", info=data) + self.assert_json_success(result) + + twitter_field = CustomProfileField.objects.get(name="Twitter", realm=realm) + self.assertEqual(twitter_field.field_type, CustomProfileField.EXTERNAL_ACCOUNT) + self.assertEqual(twitter_field.name, "Twitter") + self.assertEqual(ujson.loads(twitter_field.field_data)['subtype'], 'twitter') + + data['name'] = 'Reddit' + data["field_data"] = ujson.dumps({ + 'subtype': 'custom' + }) + result = self.client_post("/json/realm/profile_fields", info=data) + self.assert_json_error(result, 'Custom external account must define url pattern') + + data["field_data"] = ujson.dumps({ + 'subtype': 'custom', + 'url_pattern': 123, + }) + result = self.client_post("/json/realm/profile_fields", info=data) + self.assert_json_error(result, 'field_data["url_pattern"] is not a string') + + data["field_data"] = ujson.dumps({ + 'subtype': 'custom', + 'url_pattern': 'invalid', + }) + result = self.client_post("/json/realm/profile_fields", info=data) + self.assert_json_error(result, 'username should appear exactly once in pattern.') + + data["field_data"] = ujson.dumps({ + 'subtype': 'custom', + 'url_pattern': 'https://www.reddit.com/%(username)s/user/%(username)s', + }) + result = self.client_post("/json/realm/profile_fields", info=data) + self.assert_json_error(result, 'username should appear exactly once in pattern.') + + data["field_data"] = ujson.dumps({ + 'subtype': 'custom', + 'url_pattern': 'reddit.com/%(username)s', + }) + result = self.client_post("/json/realm/profile_fields", info=data) + self.assert_json_error(result, 'field_data["url_pattern"] is not a URL') + + data["field_data"] = ujson.dumps({ + 'subtype': 'custom', + 'url_pattern': 'https://www.reddit.com/user/%(username)s', + }) + result = self.client_post("/json/realm/profile_fields", info=data) + self.assert_json_success(result) + + custom_field = CustomProfileField.objects.get(name="Reddit", realm=realm) + self.assertEqual(custom_field.field_type, CustomProfileField.EXTERNAL_ACCOUNT) + self.assertEqual(custom_field.name, "Reddit") + field_data = ujson.loads(custom_field.field_data) + self.assertEqual(field_data['subtype'], 'custom') + self.assertEqual(field_data['url_pattern'], 'https://www.reddit.com/user/%(username)s') + + result = self.client_post("/json/realm/profile_fields", info=data) + self.assert_json_error(result, "A field with that name already exists.") + def test_not_realm_admin(self) -> None: self.login(self.example_email("hamlet")) result = self.client_post("/json/realm/profile_fields") @@ -416,6 +513,7 @@ class CustomProfileFieldTest(ZulipTestCase): ('Birthday', '1909-3-5'), ('Favorite website', 'https://zulipchat.com'), ('Mentor', [self.example_user("cordelia").id]), + ('GitHub', 'zulip-mobile') ] data = [] diff --git a/zerver/tests/test_home.py b/zerver/tests/test_home.py index 99d9b2f3cc..5a60b6e977 100644 --- a/zerver/tests/test_home.py +++ b/zerver/tests/test_home.py @@ -129,6 +129,7 @@ class HomeTest(ZulipTestCase): "realm_bot_domain", "realm_bots", "realm_create_stream_policy", + "realm_default_external_accounts", "realm_default_language", "realm_default_stream_groups", "realm_default_streams", diff --git a/zerver/tests/test_users.py b/zerver/tests/test_users.py index a9de4e9a0c..220f1cd374 100644 --- a/zerver/tests/test_users.py +++ b/zerver/tests/test_users.py @@ -416,6 +416,7 @@ class PermissionTest(ZulipTestCase): 'Birthday': '1909-3-5', 'Favorite website': 'https://zulipchat.com', 'Mentor': [cordelia.id], + 'GitHub': 'timabbott', } for field_name in fields: @@ -500,7 +501,8 @@ class PermissionTest(ZulipTestCase): 'Favorite editor': None, 'Birthday': None, 'Favorite website': 'https://zulip.github.io', - 'Mentor': [hamlet.id] + 'Mentor': [hamlet.id], + 'GitHub': 'timabbott', } new_profile_data = [] for field_name in fields: diff --git a/zerver/views/custom_profile_fields.py b/zerver/views/custom_profile_fields.py index 9cbe85860e..2f179f24cf 100644 --- a/zerver/views/custom_profile_fields.py +++ b/zerver/views/custom_profile_fields.py @@ -22,6 +22,7 @@ from zerver.models import (UserProfile, CustomProfileField, custom_profile_fields_for_realm) from zerver.lib.exceptions import JsonableError from zerver.lib.users import validate_user_custom_profile_data +from zerver.lib.external_accounts import validate_external_account_field_data def list_realm_custom_profile_fields(request: HttpRequest, user_profile: UserProfile) -> HttpResponse: fields = custom_profile_fields_for_realm(user_profile.realm_id) @@ -50,11 +51,12 @@ def validate_custom_field_data(field_type: int, if len(field_data) < 1: raise JsonableError(_("Field must have at least one choice.")) error = validate_choice_field_data(field_data) + elif field_type == CustomProfileField.EXTERNAL_ACCOUNT: + error = validate_external_account_field_data(field_data) if error: raise JsonableError(error) - @require_realm_admin @has_request_variables def create_realm_custom_profile_field(request: HttpRequest, diff --git a/zilencer/management/commands/populate_db.py b/zilencer/management/commands/populate_db.py index 2ee099e781..bcf64d0308 100644 --- a/zilencer/management/commands/populate_db.py +++ b/zilencer/management/commands/populate_db.py @@ -29,6 +29,7 @@ from zerver.models import CustomProfileField, DefaultStream, Message, Realm, Rea UserMessage, UserPresence, UserProfile, clear_database, \ email_to_username, get_client, get_huddle, get_realm, get_stream, \ get_system_bot, get_user, get_user_profile_by_id +from zerver.lib.types import ProfileFieldData from scripts.lib.zulip_tools import get_or_create_dev_uuid_var_path @@ -348,7 +349,7 @@ class Command(BaseCommand): field_data = { 'vim': {'text': 'Vim', 'order': '1'}, 'emacs': {'text': 'Emacs', 'order': '2'}, - } + } # type: ProfileFieldData favorite_editor = try_add_realm_custom_profile_field(zulip_realm, "Favorite editor", CustomProfileField.CHOICE, @@ -360,6 +361,12 @@ 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) # Fill in values for Iago and Hamlet hamlet = get_user("hamlet@zulip.com", zulip_realm) @@ -371,6 +378,7 @@ class Command(BaseCommand): {"id": birthday.id, "value": "2000-1-1"}, {"id": favorite_website.id, "value": "https://zulip.readthedocs.io/en/latest/"}, {"id": mentor.id, "value": [hamlet.id]}, + {"id": github_profile.id, "value": 'zulip'}, ]) do_update_user_custom_profile_data(hamlet, [ {"id": phone_number.id, "value": "+0-11-23-456-7890"}, @@ -383,6 +391,7 @@ class Command(BaseCommand): {"id": birthday.id, "value": "1900-1-1"}, {"id": favorite_website.id, "value": "https://blog.zulig.org"}, {"id": mentor.id, "value": [iago.id]}, + {"id": github_profile.id, "value": 'zulipbot'}, ]) else: zulip_realm = get_realm("zulip")