diff --git a/static/js/settings_profile_fields.js b/static/js/settings_profile_fields.js index c0b9b33d24..fdf12413f3 100644 --- a/static/js/settings_profile_fields.js +++ b/static/js/settings_profile_fields.js @@ -6,6 +6,8 @@ var meta = { loaded: false, }; +var order = []; + function field_type_id_to_string(type_id) { var name = _.find(page_params.custom_profile_field_types, function (type) { return type[0] === type_id; @@ -67,6 +69,26 @@ function delete_choice_row(e) { row.remove(); } +function move_field(e, btn, direction) { + e.preventDefault(); + e.stopPropagation(); + var button_id = parseInt(btn.attr('data-profile-field-id'), 10); + var button_index = order.indexOf(button_id); + order[button_index] = order[button_index + direction]; + order[button_index + direction] = button_id; + settings_ui.do_settings_change(channel.patch, "/json/realm/profile_fields", + {order: JSON.stringify(order)}, + $('#admin-profile-field-status').expectOne()); +} + +function move_field_up(e) { + move_field(e, $(this), -1); +} + +function move_field_down(e) { + move_field(e, $(this), 1); +} + function get_profile_field_info(id) { var info = {}; info.row = $("tr.profile-field-row[data-profile-field-id='" + id + "']"); @@ -123,7 +145,9 @@ exports.populate_profile_fields = function (profile_fields_data) { var profile_fields_table = $("#admin_profile_fields_table").expectOne(); profile_fields_table.find("tr.profile-field-row").remove(); // Clear all rows. profile_fields_table.find("tr.profile-field-form").remove(); // Clear all rows. - _.each(profile_fields_data, function (profile_field) { + order = []; + _.each(profile_fields_data, function (profile_field, index) { + order.push(profile_field.id); var field_data = {}; if (profile_field.field_data !== "") { field_data = JSON.parse(profile_field.field_data); @@ -161,6 +185,8 @@ exports.populate_profile_fields = function (profile_fields_data) { is_choice_field: is_choice_field, }, can_modify: page_params.is_admin, + first: index === 0, + last: index === _.size(profile_fields_data) - 1, } ) ); @@ -199,7 +225,8 @@ exports.set_up = function () { $('#admin_profile_fields_table').on('click', '.delete', delete_profile_field); $(".organization").on("submit", "form.admin-profile-field-form", create_profile_field); $("#admin_profile_fields_table").on("click", ".open-edit-form", open_edit_form); - + $("#admin_profile_fields_table").on("click", ".move-field-up", move_field_up); + $("#admin_profile_fields_table").on("click", ".move-field-down", move_field_down); set_up_choices_field(); }; diff --git a/static/templates/admin_profile_field_list.handlebars b/static/templates/admin_profile_field_list.handlebars index b7e554ff23..1669b5dd50 100644 --- a/static/templates/admin_profile_field_list.handlebars +++ b/static/templates/admin_profile_field_list.handlebars @@ -17,6 +17,12 @@ + + {{/if}} diff --git a/zerver/lib/actions.py b/zerver/lib/actions.py index 9dd10b6633..9752248063 100644 --- a/zerver/lib/actions.py +++ b/zerver/lib/actions.py @@ -4577,6 +4577,8 @@ def try_add_realm_custom_profile_field(realm: Realm, name: Text, field_type: int field.field_data = ujson.dumps(field_data or {}) field.save() + field.order = field.id + field.save(update_fields=['order']) notify_realm_custom_profile_fields(realm, 'add') return field @@ -4598,6 +4600,17 @@ def try_update_realm_custom_profile_field(realm: Realm, field: CustomProfileFiel field.save() notify_realm_custom_profile_fields(realm, 'update') +def try_reorder_realm_custom_profile_fields(realm: Realm, order: List[int]) -> None: + order_mapping = dict((_[1], _[0]) for _ in enumerate(order)) + fields = CustomProfileField.objects.filter(realm=realm) + for field in fields: + if field.id not in order_mapping: + raise JsonableError(_("Invalid order mapping.")) + for field in fields: + field.order = order_mapping[field.id] + field.save(update_fields=['order']) + notify_realm_custom_profile_fields(realm, 'update') + def do_update_user_custom_profile_data(user_profile: UserProfile, data: List[Dict[str, Union[int, Text]]]) -> None: with transaction.atomic(): diff --git a/zerver/migrations/0167_custom_profile_fields_sort_order.py b/zerver/migrations/0167_custom_profile_fields_sort_order.py new file mode 100644 index 0000000000..a9f54425ad --- /dev/null +++ b/zerver/migrations/0167_custom_profile_fields_sort_order.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.11 on 2018-04-08 15:49 +from __future__ import unicode_literals + +from django.db import migrations, models +from django.db.models import F +from django.db.backends.postgresql_psycopg2.schema import DatabaseSchemaEditor +from django.db.migrations.state import StateApps + +def migrate_set_order_value(apps: StateApps, schema_editor: DatabaseSchemaEditor) -> None: + CustomProfileField = apps.get_model('zerver', 'CustomProfileField') + CustomProfileField.objects.all().update(order=F('id')) + +class Migration(migrations.Migration): + + dependencies = [ + ('zerver', '0166_add_url_to_profile_field'), + ] + + operations = [ + migrations.AddField( + model_name='customprofilefield', + name='order', + field=models.IntegerField(default=0), + ), + migrations.RunPython(migrate_set_order_value), + ] diff --git a/zerver/models.py b/zerver/models.py index b7fda4aa8c..a6c10a41ad 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -1927,6 +1927,7 @@ class CustomProfileField(models.Model): field_type = models.PositiveSmallIntegerField(choices=FIELD_TYPE_CHOICES, default=SHORT_TEXT) # type: int + order = models.IntegerField(default=0) # type: int class Meta: unique_together = ('realm', 'name') @@ -1938,13 +1939,14 @@ class CustomProfileField(models.Model): 'type': self.field_type, 'hint': self.hint, 'field_data': self.field_data, + 'order': self.order, } def __str__(self) -> str: - return "" % (self.realm, self.name, self.field_type) + return "" % (self.realm, self.name, self.field_type, self.order) def custom_profile_fields_for_realm(realm_id: int) -> List[CustomProfileField]: - return CustomProfileField.objects.filter(realm=realm_id).order_by('name') + return CustomProfileField.objects.filter(realm=realm_id).order_by('order') class CustomProfileFieldValue(models.Model): user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) # type: UserProfile diff --git a/zerver/tests/test_custom_profile_data.py b/zerver/tests/test_custom_profile_data.py index 11ecd57659..59f7298070 100644 --- a/zerver/tests/test_custom_profile_data.py +++ b/zerver/tests/test_custom_profile_data.py @@ -3,8 +3,9 @@ from typing import Union, List, Dict, Text, Any from mock import patch -from zerver.lib.actions import try_add_realm_custom_profile_field, \ - do_update_user_custom_profile_data, do_remove_realm_custom_profile_field +from zerver.lib.actions import get_realm, try_add_realm_custom_profile_field, \ + do_update_user_custom_profile_data, do_remove_realm_custom_profile_field, \ + try_reorder_realm_custom_profile_fields from zerver.lib.test_classes import ZulipTestCase from zerver.models import CustomProfileField, \ custom_profile_fields_for_realm, get_realm @@ -22,8 +23,23 @@ class CustomProfileFieldTest(ZulipTestCase): content = result.json() self.assertEqual(len(content["custom_fields"]), self.original_count) + def test_list_order(self) -> None: + self.login(self.example_email("iago")) + realm = get_realm('zulip') + order = ( + CustomProfileField.objects.filter(realm=realm) + .order_by('-order') + .values_list('order', flat=True) + ) + try_reorder_realm_custom_profile_fields(realm, order) + result = self.client_get("/json/realm/profile_fields") + content = result.json() + self.assertListEqual(content["custom_fields"], + sorted(content["custom_fields"], key=lambda x: -x["id"])) + def test_create(self) -> None: self.login(self.example_email("iago")) + realm = get_realm('zulip') data = {"name": u"Phone", "field_type": "text id"} # type: Dict[str, Any] result = self.client_post("/json/realm/profile_fields", info=data) self.assert_json_error(result, u'Argument "field_type" is not valid JSON.') @@ -50,6 +66,9 @@ class CustomProfileFieldTest(ZulipTestCase): result = self.client_post("/json/realm/profile_fields", info=data) self.assert_json_success(result) + field = CustomProfileField.objects.get(name="Phone", realm=realm) + self.assertEqual(field.id, field.order) + result = self.client_post("/json/realm/profile_fields", info=data) self.assert_json_error(result, u'A field with that name already exists.') @@ -236,6 +255,63 @@ class CustomProfileFieldTest(ZulipTestCase): {'data': ujson.dumps([{"id": field.id, "value": new_value}])}) self.assert_json_error(result, error_msg) + def test_reorder(self) -> None: + self.login(self.example_email("iago")) + realm = get_realm('zulip') + order = ( + CustomProfileField.objects.filter(realm=realm) + .order_by('-order') + .values_list('order', flat=True) + ) + result = self.client_patch("/json/realm/profile_fields", + info={'order': ujson.dumps(order)}) + self.assert_json_success(result) + fields = CustomProfileField.objects.filter(realm=realm).order_by('order') + for field in fields: + self.assertEqual(field.id, order[field.order]) + + def test_reorder_duplicates(self) -> None: + self.login(self.example_email("iago")) + realm = get_realm('zulip') + order = ( + CustomProfileField.objects.filter(realm=realm) + .order_by('-order') + .values_list('order', flat=True) + ) + order = list(order) + order.append(4) + result = self.client_patch("/json/realm/profile_fields", + info={'order': ujson.dumps(order)}) + self.assert_json_success(result) + fields = CustomProfileField.objects.filter(realm=realm).order_by('order') + for field in fields: + self.assertEqual(field.id, order[field.order]) + + def test_reorder_unauthorized(self) -> None: + self.login(self.example_email("hamlet")) + realm = get_realm('zulip') + order = ( + CustomProfileField.objects.filter(realm=realm) + .order_by('-order') + .values_list('order', flat=True) + ) + result = self.client_patch("/json/realm/profile_fields", + info={'order': ujson.dumps(order)}) + self.assert_json_error(result, "Must be an organization administrator") + + def test_reorder_invalid(self) -> None: + self.login(self.example_email("iago")) + order = [100, 200, 300] + result = self.client_patch("/json/realm/profile_fields", + info={'order': ujson.dumps(order)}) + self.assert_json_error( + result, u'Invalid order mapping.') + order = [1, 2] + result = self.client_patch("/json/realm/profile_fields", + info={'order': ujson.dumps(order)}) + self.assert_json_error( + result, u'Invalid order mapping.') + def test_update_invalid_field(self) -> None: self.login(self.example_email("iago")) data = [{'id': 1234, 'value': '12'}] diff --git a/zerver/tests/test_events.py b/zerver/tests/test_events.py index 45bb4dadd7..6abf06b533 100644 --- a/zerver/tests/test_events.py +++ b/zerver/tests/test_events.py @@ -918,6 +918,7 @@ class EventsRegisterTest(ZulipTestCase): ('name', check_string), ('hint', check_string), ('field_data', check_string), + ('order', check_int), ]))), ]) diff --git a/zerver/views/custom_profile_fields.py b/zerver/views/custom_profile_fields.py index 088310f254..750e2833d9 100644 --- a/zerver/views/custom_profile_fields.py +++ b/zerver/views/custom_profile_fields.py @@ -13,7 +13,8 @@ from zerver.lib.request import has_request_variables, REQ from zerver.lib.actions import (try_add_realm_custom_profile_field, do_remove_realm_custom_profile_field, try_update_realm_custom_profile_field, - do_update_user_custom_profile_data) + do_update_user_custom_profile_data, + try_reorder_realm_custom_profile_fields) from zerver.lib.response import json_success, json_error from zerver.lib.types import ProfileFieldData from zerver.lib.validator import (check_dict, check_list, check_int, @@ -107,6 +108,14 @@ def update_realm_custom_profile_field(request: HttpRequest, user_profile: UserPr return json_error(_('A field with that name already exists.')) return json_success() +@require_realm_admin +@has_request_variables +def reorder_realm_custom_profile_fields(request: HttpRequest, user_profile: UserProfile, + order: List[int]=REQ(validator=check_list( + check_int))) -> HttpResponse: + try_reorder_realm_custom_profile_fields(user_profile.realm, order) + return json_success() + @human_users_only @has_request_variables def update_user_custom_profile_data( diff --git a/zproject/urls.py b/zproject/urls.py index dc6b186574..57686e9d26 100644 --- a/zproject/urls.py +++ b/zproject/urls.py @@ -105,6 +105,7 @@ v1_api_and_json_patterns = [ # realm/profile_fields -> zerver.views.custom_profile_fields url(r'^realm/profile_fields$', rest_dispatch, {'GET': 'zerver.views.custom_profile_fields.list_realm_custom_profile_fields', + 'PATCH': 'zerver.views.custom_profile_fields.reorder_realm_custom_profile_fields', 'POST': 'zerver.views.custom_profile_fields.create_realm_custom_profile_field'}), url(r'^realm/profile_fields/(?P\d+)$', rest_dispatch, {'PATCH': 'zerver.views.custom_profile_fields.update_realm_custom_profile_field',