mirror of
https://github.com/zulip/zulip.git
synced 2025-11-05 06:23:38 +00:00
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:
committed by
Tim Abbott
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
|
||||||
|
|||||||
39
zerver/lib/external_accounts.py
Normal file
39
zerver/lib/external_accounts.py
Normal file
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user