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")