mirror of https://github.com/zulip/zulip.git
models: Add `external_account` in custom profile field types.
Add new custom profile field type, External account. External account field links user's social media profile with account. e.g. GitHub, Twitter, etc. Fixes part of #12302
This commit is contained in:
parent
3368589df2
commit
d7ee2aced1
|
@ -7,17 +7,27 @@ set_global('Sortable', {create: () => {}});
|
||||||
|
|
||||||
const SHORT_TEXT_ID = 1;
|
const SHORT_TEXT_ID = 1;
|
||||||
const CHOICE_ID = 3;
|
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.custom_profile_fields = {};
|
||||||
|
page_params.realm_default_external_accounts = JSON.stringify({});
|
||||||
|
|
||||||
page_params.custom_profile_field_types = {
|
page_params.custom_profile_field_types = {
|
||||||
SHORT_TEXT: {
|
SHORT_TEXT: {
|
||||||
id: SHORT_TEXT_ID,
|
id: SHORT_TEXT_ID,
|
||||||
name: "Short Text",
|
name: SHORT_TEXT_NAME,
|
||||||
},
|
},
|
||||||
CHOICE: {
|
CHOICE: {
|
||||||
id: CHOICE_ID,
|
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 = [
|
const expected_template_data = [
|
||||||
{
|
{
|
||||||
|
@ -85,25 +114,59 @@ run_test('populate_profile_fields', () => {
|
||||||
id: 10,
|
id: 10,
|
||||||
name: 'favorite color',
|
name: 'favorite color',
|
||||||
hint: 'blue?',
|
hint: 'blue?',
|
||||||
type: 'Short Text',
|
type: SHORT_TEXT_NAME,
|
||||||
choices: [],
|
choices: [],
|
||||||
is_choice_field: false,
|
is_choice_field: false,
|
||||||
|
is_external_account_field: false,
|
||||||
},
|
},
|
||||||
can_modify: true,
|
can_modify: true,
|
||||||
|
realm_default_external_accounts:
|
||||||
|
page_params.realm_default_external_accounts,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
profile_field: {
|
profile_field: {
|
||||||
id: 30,
|
id: 30,
|
||||||
name: 'meal',
|
name: 'meal',
|
||||||
hint: 'lunch',
|
hint: 'lunch',
|
||||||
type: 'Choice',
|
type: CHOICE_NAME,
|
||||||
choices: [
|
choices: [
|
||||||
{order: 0, value: 0, text: 'lunch'},
|
{order: 0, value: 0, text: 'lunch'},
|
||||||
{order: 1, value: 1, text: 'dinner'},
|
{order: 1, value: 1, text: 'dinner'},
|
||||||
],
|
],
|
||||||
is_choice_field: true,
|
is_choice_field: true,
|
||||||
|
is_external_account_field: false,
|
||||||
},
|
},
|
||||||
can_modify: true,
|
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,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -71,6 +71,7 @@ exports.build_page = function () {
|
||||||
plan_includes_wide_organization_logo: page_params.plan_includes_wide_organization_logo,
|
plan_includes_wide_organization_logo: page_params.plan_includes_wide_organization_logo,
|
||||||
upgrade_text_for_wide_organization_logo:
|
upgrade_text_for_wide_organization_logo:
|
||||||
page_params.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;
|
options.admin_settings_label = admin_settings_label;
|
||||||
|
|
|
@ -104,6 +104,7 @@ function get_custom_profile_field_data(user, field, field_types, dateFormat) {
|
||||||
profile_field.name = field.name;
|
profile_field.name = field.name;
|
||||||
profile_field.is_user_field = false;
|
profile_field.is_user_field = false;
|
||||||
profile_field.is_link = field_type === field_types.URL.id;
|
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;
|
profile_field.type = field_type;
|
||||||
|
|
||||||
switch (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.value = field_value.value;
|
||||||
profile_field.rendered_value = field_value.rendered_value;
|
profile_field.rendered_value = field_value.rendered_value;
|
||||||
break;
|
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:
|
default:
|
||||||
profile_field.value = field_value.value;
|
profile_field.value = field_value.value;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.CHOICE.id] = "choice";
|
||||||
all_field_template_types[all_field_types.USER.id] = "user";
|
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.DATE.id] = "date";
|
||||||
|
all_field_template_types[all_field_types.EXTERNAL_ACCOUNT.id] = "text";
|
||||||
|
|
||||||
all_custom_fields.forEach(function (field) {
|
all_custom_fields.forEach(function (field) {
|
||||||
var field_value = people.get_custom_profile_data(user_id, field.id);
|
var field_value = people.get_custom_profile_data(user_id, field.id);
|
||||||
|
|
|
@ -76,6 +76,15 @@ function read_choice_field_data_from_form(field_elem) {
|
||||||
return field_data;
|
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) {
|
function update_choice_delete_btn(container, display_flag) {
|
||||||
var no_of_choice_row = container.find(".choice-row").length;
|
var no_of_choice_row = container.find(".choice-row").length;
|
||||||
|
|
||||||
|
@ -106,12 +115,20 @@ function clear_form_data() {
|
||||||
create_choice_row($("#profile_field_choices"));
|
create_choice_row($("#profile_field_choices"));
|
||||||
update_choice_delete_btn($("#profile_field_choices"), false);
|
update_choice_delete_btn($("#profile_field_choices"), false);
|
||||||
$("#profile_field_choices_row").hide();
|
$("#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) {
|
function read_field_data_from_form(field_type_id, field_elem) {
|
||||||
// Only read field data if we are creating a choice field
|
// Only read field data if we are creating a choice field
|
||||||
|
// or external account field.
|
||||||
if (field_type_id === field_types.CHOICE.id) {
|
if (field_type_id === field_types.CHOICE.id) {
|
||||||
return read_choice_field_data_from_form(field_elem);
|
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_data = {};
|
||||||
var field_type = $('#profile_field_type').val();
|
var field_type = $('#profile_field_type').val();
|
||||||
|
|
||||||
var opts = {
|
var opts = {
|
||||||
success_continuation: clear_form_data,
|
success_continuation: clear_form_data,
|
||||||
};
|
};
|
||||||
|
@ -186,6 +202,15 @@ exports.parse_field_choices_from_field_data = function (field_data) {
|
||||||
return choices;
|
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) {
|
function set_up_choices_field_edit_form(profile_field, field_data) {
|
||||||
// Re-render field choices in edit form to load initial choice 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');
|
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);
|
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.find('.reset').on("click", function () {
|
||||||
profile_field.form.hide();
|
profile_field.form.hide();
|
||||||
profile_field.row.show();
|
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("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.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 () {
|
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),
|
type: exports.field_type_id_to_string(profile_field.type),
|
||||||
choices: choices,
|
choices: choices,
|
||||||
is_choice_field: profile_field.type === field_types.CHOICE.id,
|
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,
|
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);
|
$("#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.set_up = function () {
|
||||||
exports.build_page();
|
exports.build_page();
|
||||||
exports.maybe_disable_widgets();
|
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);
|
$("#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);
|
$("#admin_profile_fields_table").on("click", ".open-edit-form", open_edit_form);
|
||||||
set_up_choices_field();
|
set_up_choices_field();
|
||||||
|
set_up_external_account_field();
|
||||||
clear_form_data();
|
clear_form_data();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,21 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if is_external_account_field}}
|
||||||
|
<div class="input-group profile_field_external_accounts_edit">
|
||||||
|
<label for="external_acc_field_type">{{t "External account type" }}</label>
|
||||||
|
<select name="external_acc_field_type">
|
||||||
|
{{#each ../realm_default_external_accounts}}
|
||||||
|
<option value='{{@key}}'>{{this.text}}</option>
|
||||||
|
{{/each}}
|
||||||
|
<option value="custom">{{t 'Custom' }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="input-group custom_external_account_detail">
|
||||||
|
<label for="url_pattern">{{t "URL pattern" }}</label>
|
||||||
|
<input type="url" name="url_pattern" autocomplete="off" maxlength="80" />
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<button type="button" class="button rounded sea-green submit">
|
<button type="button" class="button rounded sea-green submit">
|
||||||
{{t 'Save changes' }}
|
{{t 'Save changes' }}
|
||||||
|
|
|
@ -41,6 +41,19 @@
|
||||||
<tbody id="profile_field_choices" class="profile-field-choices"></tbody>
|
<tbody id="profile_field_choices" class="profile-field-choices"></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="control-group" id="profile_field_external_accounts">
|
||||||
|
<label for="profile_field_external_accounts_type" class="control-label">{{t "External account type" }}</label>
|
||||||
|
<select id="profile_field_external_accounts_type" name="external_acc_field_type">
|
||||||
|
{{#each realm_default_external_accounts}}
|
||||||
|
<option value='{{@key}}'>{{this.text}}</option>
|
||||||
|
{{/each}}
|
||||||
|
<option value="custom">{{t 'Custom' }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="control-group" id="custom_external_account_url_pattern">
|
||||||
|
<label for="custom_field_url_pattern" class="control-label">{{t "URL pattern" }}</label>
|
||||||
|
<input type="url" id="custom_field_url_pattern" name="url_pattern" autocomplete="off" maxlength="80" />
|
||||||
|
</div>
|
||||||
<button type="submit" class="button rounded sea-green" id="add-custom-profile-field-btn">
|
<button type="submit" class="button rounded sea-green" id="add-custom-profile-field-btn">
|
||||||
{{t 'Add profile field' }}
|
{{t 'Add profile field' }}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -51,6 +51,8 @@
|
||||||
</div>
|
</div>
|
||||||
{{else if this.is_link}}
|
{{else if this.is_link}}
|
||||||
<a href={{this.value}} target="_blank" class="value">{{this.value}}</a>
|
<a href={{this.value}} target="_blank" class="value">{{this.value}}</a>
|
||||||
|
{{else if this.is_external_account}}
|
||||||
|
<a href={{this.link}} target="_blank" class="value">{{this.value}}</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#if this.rendered_value}}
|
{{#if this.rendered_value}}
|
||||||
<div class="value rendered_markdown">{{{this.rendered_value}}}</div>
|
<div class="value rendered_markdown">{{{this.rendered_value}}}</div>
|
||||||
|
|
|
@ -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_data: Optional[ProfileFieldData]=None) -> CustomProfileField:
|
||||||
field = CustomProfileField(realm=realm, name=name, field_type=field_type)
|
field = CustomProfileField(realm=realm, name=name, field_type=field_type)
|
||||||
field.hint = hint
|
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.field_data = ujson.dumps(field_data or {})
|
||||||
|
|
||||||
field.save()
|
field.save()
|
||||||
|
@ -5410,7 +5411,8 @@ def try_update_realm_custom_profile_field(realm: Realm, field: CustomProfileFiel
|
||||||
field_data: Optional[ProfileFieldData]=None) -> None:
|
field_data: Optional[ProfileFieldData]=None) -> None:
|
||||||
field.name = name
|
field.name = name
|
||||||
field.hint = hint
|
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.field_data = ujson.dumps(field_data or {})
|
||||||
field.save()
|
field.save()
|
||||||
notify_realm_custom_profile_fields(realm, 'update')
|
notify_realm_custom_profile_fields(realm, 'update')
|
||||||
|
|
|
@ -53,7 +53,7 @@ from zerver.models import Client, Message, Realm, UserPresence, UserProfile, Cus
|
||||||
get_default_stream_groups, CustomProfileField, Stream
|
get_default_stream_groups, CustomProfileField, Stream
|
||||||
from zproject.backends import email_auth_enabled, password_auth_enabled
|
from zproject.backends import email_auth_enabled, password_auth_enabled
|
||||||
from version import ZULIP_VERSION
|
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]]:
|
def get_raw_user_data(realm: Realm, client_gravatar: bool) -> Dict[int, Dict[str, str]]:
|
||||||
user_dicts = get_realm_user_dicts(realm.id)
|
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['realm_plan_type'] = realm.plan_type
|
||||||
state['plan_includes_wide_organization_logo'] = realm.plan_type != Realm.LIMITED
|
state['plan_includes_wide_organization_logo'] = realm.plan_type != Realm.LIMITED
|
||||||
state['upgrade_text_for_wide_organization_logo'] = str(Realm.UPGRADE_TEXT_STANDARD)
|
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:
|
if realm.notifications_stream and not realm.notifications_stream.deactivated:
|
||||||
notifications_stream = realm.notifications_stream
|
notifications_stream = realm.notifications_stream
|
||||||
state['realm_notifications_stream_id'] = notifications_stream.id
|
state['realm_notifications_stream_id'] = notifications_stream.id
|
||||||
|
|
|
@ -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
|
|
@ -24,4 +24,4 @@ UserFieldElement = Tuple[int, str, RealmUserValidator, Callable[[Any], Any], str
|
||||||
|
|
||||||
FieldTypeData = List[Union[FieldElement, ExtendedFieldElement, UserFieldElement]]
|
FieldTypeData = List[Union[FieldElement, ExtendedFieldElement, UserFieldElement]]
|
||||||
|
|
||||||
ProfileFieldData = Dict[str, Dict[str, str]]
|
ProfileFieldData = Dict[str, Union[Dict[str, str], str]]
|
||||||
|
|
|
@ -228,6 +228,21 @@ def check_url(var_name: str, val: object) -> Optional[str]:
|
||||||
except ValidationError:
|
except ValidationError:
|
||||||
return _('%s is not a URL') % (var_name,)
|
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]:
|
def validate_choice_field_data(field_data: ProfileFieldData) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
This function is used to validate the data sent to the server while
|
This function is used to validate the data sent to the server while
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -2613,6 +2613,7 @@ class CustomProfileField(models.Model):
|
||||||
DATE = 4
|
DATE = 4
|
||||||
URL = 5
|
URL = 5
|
||||||
USER = 6
|
USER = 6
|
||||||
|
EXTERNAL_ACCOUNT = 7
|
||||||
|
|
||||||
# These are the fields whose validators require more than var_name
|
# These are the fields whose validators require more than var_name
|
||||||
# and value argument. i.e. CHOICE require field_data, USER require
|
# 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"),
|
(LONG_TEXT, str(_('Long text')), check_long_string, str, "LONG_TEXT"),
|
||||||
(DATE, str(_('Date picker')), check_date, str, "DATE"),
|
(DATE, str(_('Date picker')), check_date, str, "DATE"),
|
||||||
(URL, str(_('Link')), check_url, str, "URL"),
|
(URL, str(_('Link')), check_url, str, "URL"),
|
||||||
|
(EXTERNAL_ACCOUNT, str(_('External account')), check_short_string, str, "EXTERNAL_ACCOUNT"),
|
||||||
] # type: FieldTypeData
|
] # type: FieldTypeData
|
||||||
|
|
||||||
ALL_FIELD_TYPES = FIELD_TYPE_DATA + CHOICE_FIELD_TYPE_DATA + USER_FIELD_TYPE_DATA
|
ALL_FIELD_TYPES = FIELD_TYPE_DATA + CHOICE_FIELD_TYPE_DATA + USER_FIELD_TYPE_DATA
|
||||||
|
|
|
@ -141,6 +141,103 @@ class CustomProfileFieldTest(ZulipTestCase):
|
||||||
result = self.client_post("/json/realm/profile_fields", info=data)
|
result = self.client_post("/json/realm/profile_fields", info=data)
|
||||||
self.assert_json_success(result)
|
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:
|
def test_not_realm_admin(self) -> None:
|
||||||
self.login(self.example_email("hamlet"))
|
self.login(self.example_email("hamlet"))
|
||||||
result = self.client_post("/json/realm/profile_fields")
|
result = self.client_post("/json/realm/profile_fields")
|
||||||
|
@ -416,6 +513,7 @@ class CustomProfileFieldTest(ZulipTestCase):
|
||||||
('Birthday', '1909-3-5'),
|
('Birthday', '1909-3-5'),
|
||||||
('Favorite website', 'https://zulipchat.com'),
|
('Favorite website', 'https://zulipchat.com'),
|
||||||
('Mentor', [self.example_user("cordelia").id]),
|
('Mentor', [self.example_user("cordelia").id]),
|
||||||
|
('GitHub', 'zulip-mobile')
|
||||||
]
|
]
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
|
|
|
@ -129,6 +129,7 @@ class HomeTest(ZulipTestCase):
|
||||||
"realm_bot_domain",
|
"realm_bot_domain",
|
||||||
"realm_bots",
|
"realm_bots",
|
||||||
"realm_create_stream_policy",
|
"realm_create_stream_policy",
|
||||||
|
"realm_default_external_accounts",
|
||||||
"realm_default_language",
|
"realm_default_language",
|
||||||
"realm_default_stream_groups",
|
"realm_default_stream_groups",
|
||||||
"realm_default_streams",
|
"realm_default_streams",
|
||||||
|
|
|
@ -416,6 +416,7 @@ class PermissionTest(ZulipTestCase):
|
||||||
'Birthday': '1909-3-5',
|
'Birthday': '1909-3-5',
|
||||||
'Favorite website': 'https://zulipchat.com',
|
'Favorite website': 'https://zulipchat.com',
|
||||||
'Mentor': [cordelia.id],
|
'Mentor': [cordelia.id],
|
||||||
|
'GitHub': 'timabbott',
|
||||||
}
|
}
|
||||||
|
|
||||||
for field_name in fields:
|
for field_name in fields:
|
||||||
|
@ -500,7 +501,8 @@ class PermissionTest(ZulipTestCase):
|
||||||
'Favorite editor': None,
|
'Favorite editor': None,
|
||||||
'Birthday': None,
|
'Birthday': None,
|
||||||
'Favorite website': 'https://zulip.github.io',
|
'Favorite website': 'https://zulip.github.io',
|
||||||
'Mentor': [hamlet.id]
|
'Mentor': [hamlet.id],
|
||||||
|
'GitHub': 'timabbott',
|
||||||
}
|
}
|
||||||
new_profile_data = []
|
new_profile_data = []
|
||||||
for field_name in fields:
|
for field_name in fields:
|
||||||
|
|
|
@ -22,6 +22,7 @@ from zerver.models import (UserProfile,
|
||||||
CustomProfileField, custom_profile_fields_for_realm)
|
CustomProfileField, custom_profile_fields_for_realm)
|
||||||
from zerver.lib.exceptions import JsonableError
|
from zerver.lib.exceptions import JsonableError
|
||||||
from zerver.lib.users import validate_user_custom_profile_data
|
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:
|
def list_realm_custom_profile_fields(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:
|
||||||
fields = custom_profile_fields_for_realm(user_profile.realm_id)
|
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:
|
if len(field_data) < 1:
|
||||||
raise JsonableError(_("Field must have at least one choice."))
|
raise JsonableError(_("Field must have at least one choice."))
|
||||||
error = validate_choice_field_data(field_data)
|
error = validate_choice_field_data(field_data)
|
||||||
|
elif field_type == CustomProfileField.EXTERNAL_ACCOUNT:
|
||||||
|
error = validate_external_account_field_data(field_data)
|
||||||
|
|
||||||
if error:
|
if error:
|
||||||
raise JsonableError(error)
|
raise JsonableError(error)
|
||||||
|
|
||||||
|
|
||||||
@require_realm_admin
|
@require_realm_admin
|
||||||
@has_request_variables
|
@has_request_variables
|
||||||
def create_realm_custom_profile_field(request: HttpRequest,
|
def create_realm_custom_profile_field(request: HttpRequest,
|
||||||
|
|
|
@ -29,6 +29,7 @@ from zerver.models import CustomProfileField, DefaultStream, Message, Realm, Rea
|
||||||
UserMessage, UserPresence, UserProfile, clear_database, \
|
UserMessage, UserPresence, UserProfile, clear_database, \
|
||||||
email_to_username, get_client, get_huddle, get_realm, get_stream, \
|
email_to_username, get_client, get_huddle, get_realm, get_stream, \
|
||||||
get_system_bot, get_user, get_user_profile_by_id
|
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
|
from scripts.lib.zulip_tools import get_or_create_dev_uuid_var_path
|
||||||
|
|
||||||
|
@ -348,7 +349,7 @@ class Command(BaseCommand):
|
||||||
field_data = {
|
field_data = {
|
||||||
'vim': {'text': 'Vim', 'order': '1'},
|
'vim': {'text': 'Vim', 'order': '1'},
|
||||||
'emacs': {'text': 'Emacs', 'order': '2'},
|
'emacs': {'text': 'Emacs', 'order': '2'},
|
||||||
}
|
} # type: ProfileFieldData
|
||||||
favorite_editor = try_add_realm_custom_profile_field(zulip_realm,
|
favorite_editor = try_add_realm_custom_profile_field(zulip_realm,
|
||||||
"Favorite editor",
|
"Favorite editor",
|
||||||
CustomProfileField.CHOICE,
|
CustomProfileField.CHOICE,
|
||||||
|
@ -360,6 +361,12 @@ class Command(BaseCommand):
|
||||||
hint="Or your personal blog's URL")
|
hint="Or your personal blog's URL")
|
||||||
mentor = try_add_realm_custom_profile_field(zulip_realm, "Mentor",
|
mentor = try_add_realm_custom_profile_field(zulip_realm, "Mentor",
|
||||||
CustomProfileField.USER)
|
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
|
# Fill in values for Iago and Hamlet
|
||||||
hamlet = get_user("hamlet@zulip.com", zulip_realm)
|
hamlet = get_user("hamlet@zulip.com", zulip_realm)
|
||||||
|
@ -371,6 +378,7 @@ class Command(BaseCommand):
|
||||||
{"id": birthday.id, "value": "2000-1-1"},
|
{"id": birthday.id, "value": "2000-1-1"},
|
||||||
{"id": favorite_website.id, "value": "https://zulip.readthedocs.io/en/latest/"},
|
{"id": favorite_website.id, "value": "https://zulip.readthedocs.io/en/latest/"},
|
||||||
{"id": mentor.id, "value": [hamlet.id]},
|
{"id": mentor.id, "value": [hamlet.id]},
|
||||||
|
{"id": github_profile.id, "value": 'zulip'},
|
||||||
])
|
])
|
||||||
do_update_user_custom_profile_data(hamlet, [
|
do_update_user_custom_profile_data(hamlet, [
|
||||||
{"id": phone_number.id, "value": "+0-11-23-456-7890"},
|
{"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": birthday.id, "value": "1900-1-1"},
|
||||||
{"id": favorite_website.id, "value": "https://blog.zulig.org"},
|
{"id": favorite_website.id, "value": "https://blog.zulig.org"},
|
||||||
{"id": mentor.id, "value": [iago.id]},
|
{"id": mentor.id, "value": [iago.id]},
|
||||||
|
{"id": github_profile.id, "value": 'zulipbot'},
|
||||||
])
|
])
|
||||||
else:
|
else:
|
||||||
zulip_realm = get_realm("zulip")
|
zulip_realm = get_realm("zulip")
|
||||||
|
|
Loading…
Reference in New Issue