mirror of
https://github.com/zulip/zulip.git
synced 2025-11-07 07:23:22 +00:00
@@ -64,6 +64,8 @@ function add_custom_profile_fields_to_settings() {
|
|||||||
// 1 & 2 type represent textual data.
|
// 1 & 2 type represent textual data.
|
||||||
if (field.type === 1 || field.type === 2) {
|
if (field.type === 1 || field.type === 2) {
|
||||||
type = "text";
|
type = "text";
|
||||||
|
} else if (field.type === 3) {
|
||||||
|
type = "choice";
|
||||||
} else {
|
} else {
|
||||||
blueslip.error("Undefined field type.");
|
blueslip.error("Undefined field type.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ from zerver.lib.upload import attachment_url_re, attachment_url_to_path_id, \
|
|||||||
claim_attachment, delete_message_image, upload_emoji_image
|
claim_attachment, delete_message_image, upload_emoji_image
|
||||||
from zerver.lib.str_utils import NonBinaryStr, force_str
|
from zerver.lib.str_utils import NonBinaryStr, force_str
|
||||||
from zerver.tornado.event_queue import request_event_queue, send_event
|
from zerver.tornado.event_queue import request_event_queue, send_event
|
||||||
|
from zerver.lib.types import ProfileFieldData
|
||||||
|
|
||||||
from analytics.models import StreamCount
|
from analytics.models import StreamCount
|
||||||
|
|
||||||
@@ -4528,9 +4529,13 @@ def notify_realm_custom_profile_fields(realm: Realm, operation: str) -> None:
|
|||||||
send_event(event, active_user_ids(realm.id))
|
send_event(event, active_user_ids(realm.id))
|
||||||
|
|
||||||
def try_add_realm_custom_profile_field(realm: Realm, name: Text, field_type: int,
|
def try_add_realm_custom_profile_field(realm: Realm, name: Text, field_type: int,
|
||||||
hint: Text='') -> CustomProfileField:
|
hint: Text='',
|
||||||
|
field_data: 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:
|
||||||
|
field.field_data = ujson.dumps(field_data or {})
|
||||||
|
|
||||||
field.save()
|
field.save()
|
||||||
notify_realm_custom_profile_fields(realm, 'add')
|
notify_realm_custom_profile_fields(realm, 'add')
|
||||||
return field
|
return field
|
||||||
@@ -4544,10 +4549,13 @@ def do_remove_realm_custom_profile_field(realm: Realm, field: CustomProfileField
|
|||||||
notify_realm_custom_profile_fields(realm, 'delete')
|
notify_realm_custom_profile_fields(realm, 'delete')
|
||||||
|
|
||||||
def try_update_realm_custom_profile_field(realm: Realm, field: CustomProfileField,
|
def try_update_realm_custom_profile_field(realm: Realm, field: CustomProfileField,
|
||||||
name: Text, hint: Text='') -> None:
|
name: Text, hint: Text='',
|
||||||
|
field_data: ProfileFieldData=None) -> None:
|
||||||
field.name = name
|
field.name = name
|
||||||
field.hint = hint
|
field.hint = hint
|
||||||
field.save(update_fields=['name', 'hint'])
|
if field.field_type == CustomProfileField.CHOICE:
|
||||||
|
field.field_data = ujson.dumps(field_data or {})
|
||||||
|
field.save()
|
||||||
notify_realm_custom_profile_fields(realm, 'update')
|
notify_realm_custom_profile_fields(realm, 'update')
|
||||||
|
|
||||||
def do_update_user_custom_profile_data(user_profile: UserProfile,
|
def do_update_user_custom_profile_data(user_profile: UserProfile,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import TypeVar, Callable, Optional, List, Dict, Union
|
from typing import TypeVar, Callable, Optional, List, Dict, Union, Tuple, Any
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
|
||||||
ViewFuncT = TypeVar('ViewFuncT', bound=Callable[..., HttpResponse])
|
ViewFuncT = TypeVar('ViewFuncT', bound=Callable[..., HttpResponse])
|
||||||
@@ -6,6 +6,14 @@ ViewFuncT = TypeVar('ViewFuncT', bound=Callable[..., HttpResponse])
|
|||||||
# See zerver/lib/validator.py for more details of Validators,
|
# See zerver/lib/validator.py for more details of Validators,
|
||||||
# including many examples
|
# including many examples
|
||||||
Validator = Callable[[str, object], Optional[str]]
|
Validator = Callable[[str, object], Optional[str]]
|
||||||
|
ExtendedValidator = Callable[[str, str, object], Optional[str]]
|
||||||
|
|
||||||
ProfileDataElement = Dict[str, Union[int, float, Optional[str]]]
|
ProfileDataElement = Dict[str, Union[int, float, Optional[str]]]
|
||||||
ProfileData = List[ProfileDataElement]
|
ProfileData = List[ProfileDataElement]
|
||||||
|
|
||||||
|
FieldElement = Tuple[int, str, Validator, Callable[[Any], Any]]
|
||||||
|
ExtendedFieldElement = Tuple[int, str, ExtendedValidator, Callable[[Any], Any]]
|
||||||
|
|
||||||
|
FieldTypeData = List[Union[FieldElement, ExtendedFieldElement]]
|
||||||
|
|
||||||
|
ProfileFieldData = Dict[str, Dict[str, str]]
|
||||||
|
|||||||
@@ -25,19 +25,32 @@ A simple example of composition is this:
|
|||||||
To extend this concept, it's simply a matter of writing your own validator
|
To extend this concept, it's simply a matter of writing your own validator
|
||||||
for any particular type of object.
|
for any particular type of object.
|
||||||
'''
|
'''
|
||||||
|
import ujson
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import validate_email, URLValidator
|
from django.core.validators import validate_email, URLValidator
|
||||||
from typing import Callable, Iterable, Optional, Tuple, TypeVar, Text
|
from typing import Callable, Iterable, Optional, Tuple, TypeVar, Text, cast, \
|
||||||
|
Dict
|
||||||
|
|
||||||
from zerver.lib.request import JsonableError
|
from zerver.lib.request import JsonableError
|
||||||
from zerver.lib.types import Validator
|
from zerver.lib.types import Validator, ProfileFieldData
|
||||||
|
|
||||||
def check_string(var_name: str, val: object) -> Optional[str]:
|
def check_string(var_name: str, val: object) -> Optional[str]:
|
||||||
if not isinstance(val, str):
|
if not isinstance(val, str):
|
||||||
return _('%s is not a string') % (var_name,)
|
return _('%s is not a string') % (var_name,)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def check_required_string(var_name: str, val: object) -> Optional[str]:
|
||||||
|
error = check_string(var_name, val)
|
||||||
|
if error:
|
||||||
|
return error
|
||||||
|
|
||||||
|
val = cast(str, val)
|
||||||
|
if not val.strip():
|
||||||
|
return _("{item} cannot be blank.").format(item=var_name)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def check_short_string(var_name: str, val: object) -> Optional[str]:
|
def check_short_string(var_name: str, val: object) -> Optional[str]:
|
||||||
return check_capped_string(50)(var_name, val)
|
return check_capped_string(50)(var_name, val)
|
||||||
|
|
||||||
@@ -174,3 +187,33 @@ def check_url(var_name: str, val: object) -> Optional[str]:
|
|||||||
return None
|
return None
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
return _('%s is not a URL') % (var_name,)
|
return _('%s is not a URL') % (var_name,)
|
||||||
|
|
||||||
|
def validate_field_data(field_data: ProfileFieldData) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
This function is used to validate the data sent to the server while
|
||||||
|
creating/editing choices of the choice field in Organization settings.
|
||||||
|
"""
|
||||||
|
validator = check_dict_only([
|
||||||
|
('text', check_required_string),
|
||||||
|
('order', check_required_string),
|
||||||
|
])
|
||||||
|
|
||||||
|
for key, value in field_data.items():
|
||||||
|
if not key.strip():
|
||||||
|
return _("'{item}' cannot be blank.").format(item='value')
|
||||||
|
|
||||||
|
error = validator('field_data', value)
|
||||||
|
if error:
|
||||||
|
return error
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def validate_choice_field(var_name: str, field_data: str, value: object) -> None:
|
||||||
|
"""
|
||||||
|
This function is used to validate the value selected by the user against a
|
||||||
|
choice field. This is not used to validate admin data.
|
||||||
|
"""
|
||||||
|
field_data_dict = ujson.loads(field_data)
|
||||||
|
if value not in field_data_dict:
|
||||||
|
msg = _("'{value}' is not a valid choice for '{field_name}'.")
|
||||||
|
return msg.format(value=value, field_name=var_name)
|
||||||
|
|||||||
25
zerver/migrations/0160_add_choice_field.py
Normal file
25
zerver/migrations/0160_add_choice_field.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.11 on 2018-04-10 04:57
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('zerver', '0159_realm_google_hangouts_domain'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customprofilefield',
|
||||||
|
name='field_data',
|
||||||
|
field=models.TextField(default='', null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customprofilefield',
|
||||||
|
name='field_type',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[(1, 'Short Text'), (2, 'Long Text'), (3, 'Choice')], default=1),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -32,9 +32,10 @@ from django.db.models.signals import pre_save, post_save, post_delete
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from zerver.lib import cache
|
from zerver.lib import cache
|
||||||
from zerver.lib.validator import check_int, check_float, \
|
from zerver.lib.validator import check_int, check_float, \
|
||||||
check_short_string, check_long_string
|
check_short_string, check_long_string, validate_choice_field
|
||||||
from zerver.lib.name_restrictions import is_disposable_domain
|
from zerver.lib.name_restrictions import is_disposable_domain
|
||||||
from zerver.lib.types import Validator, ProfileDataElement, ProfileData
|
from zerver.lib.types import Validator, ExtendedValidator, \
|
||||||
|
ProfileDataElement, ProfileData, FieldTypeData
|
||||||
|
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
|
|
||||||
@@ -1891,19 +1892,35 @@ class CustomProfileField(models.Model):
|
|||||||
realm = models.ForeignKey(Realm, on_delete=CASCADE) # type: Realm
|
realm = models.ForeignKey(Realm, on_delete=CASCADE) # type: Realm
|
||||||
name = models.CharField(max_length=100) # type: Text
|
name = models.CharField(max_length=100) # type: Text
|
||||||
hint = models.CharField(max_length=HINT_MAX_LENGTH, default='', null=True) # type: Optional[Text]
|
hint = models.CharField(max_length=HINT_MAX_LENGTH, default='', null=True) # type: Optional[Text]
|
||||||
|
# There is no performance overhead of using TextField in PostGreSQL.
|
||||||
|
# See https://www.postgresql.org/docs/9.0/static/datatype-character.html
|
||||||
|
field_data = models.TextField(default='', null=True) # type: Optional[Text]
|
||||||
|
|
||||||
SHORT_TEXT = 1
|
SHORT_TEXT = 1
|
||||||
LONG_TEXT = 2
|
LONG_TEXT = 2
|
||||||
|
CHOICE = 3
|
||||||
|
|
||||||
|
# These are the fields whose validators require field_data
|
||||||
|
# argument as well.
|
||||||
|
EXTENDED_FIELD_TYPE_DATA = [
|
||||||
|
(CHOICE, 'Choice', validate_choice_field, str),
|
||||||
|
] # type: FieldTypeData
|
||||||
|
|
||||||
|
EXTENDED_FIELD_VALIDATORS = {
|
||||||
|
item[0]: item[2] for item in EXTENDED_FIELD_TYPE_DATA
|
||||||
|
} # type: Dict[int, ExtendedValidator]
|
||||||
|
|
||||||
FIELD_TYPE_DATA = [
|
FIELD_TYPE_DATA = [
|
||||||
# Type, Name, Validator, Converter
|
# Type, Name, Validator, Converter
|
||||||
(SHORT_TEXT, u'Short Text', check_short_string, str),
|
(SHORT_TEXT, u'Short Text', check_short_string, str),
|
||||||
(LONG_TEXT, u'Long Text', check_long_string, str),
|
(LONG_TEXT, u'Long Text', check_long_string, str),
|
||||||
] # type: List[Tuple[int, Text, Validator, Callable[[Any], Any]]]
|
] # type: FieldTypeData
|
||||||
|
|
||||||
|
ALL_FIELD_TYPES = FIELD_TYPE_DATA + EXTENDED_FIELD_TYPE_DATA
|
||||||
|
|
||||||
FIELD_VALIDATORS = {item[0]: item[2] for item in FIELD_TYPE_DATA} # type: Dict[int, Validator]
|
FIELD_VALIDATORS = {item[0]: item[2] for item in FIELD_TYPE_DATA} # type: Dict[int, Validator]
|
||||||
FIELD_CONVERTERS = {item[0]: item[3] for item in FIELD_TYPE_DATA} # type: Dict[int, Callable[[Any], Any]]
|
FIELD_CONVERTERS = {item[0]: item[3] for item in ALL_FIELD_TYPES} # type: Dict[int, Callable[[Any], Any]]
|
||||||
FIELD_TYPE_CHOICES = [(item[0], item[1]) for item in FIELD_TYPE_DATA] # type: List[Tuple[int, Text]]
|
FIELD_TYPE_CHOICES = [(item[0], item[1]) for item in ALL_FIELD_TYPES] # type: List[Tuple[int, Text]]
|
||||||
|
|
||||||
field_type = models.PositiveSmallIntegerField(choices=FIELD_TYPE_CHOICES,
|
field_type = models.PositiveSmallIntegerField(choices=FIELD_TYPE_CHOICES,
|
||||||
default=SHORT_TEXT) # type: int
|
default=SHORT_TEXT) # type: int
|
||||||
@@ -1917,6 +1934,7 @@ class CustomProfileField(models.Model):
|
|||||||
'name': self.name,
|
'name': self.name,
|
||||||
'type': self.field_type,
|
'type': self.field_type,
|
||||||
'hint': self.hint,
|
'hint': self.hint,
|
||||||
|
'field_data': self.field_data,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class CustomProfileFieldTest(ZulipTestCase):
|
|||||||
self.assert_json_success(result)
|
self.assert_json_success(result)
|
||||||
self.assertEqual(200, result.status_code)
|
self.assertEqual(200, result.status_code)
|
||||||
content = result.json()
|
content = result.json()
|
||||||
self.assertEqual(len(content["custom_fields"]), 3)
|
self.assertEqual(len(content["custom_fields"]), 4)
|
||||||
|
|
||||||
def test_create(self) -> None:
|
def test_create(self) -> None:
|
||||||
self.login(self.example_email("iago"))
|
self.login(self.example_email("iago"))
|
||||||
@@ -53,6 +53,59 @@ class CustomProfileFieldTest(ZulipTestCase):
|
|||||||
self.assert_json_error(result,
|
self.assert_json_error(result,
|
||||||
u'A field with that name already exists.')
|
u'A field with that name already exists.')
|
||||||
|
|
||||||
|
def test_create_choice_field(self) -> None:
|
||||||
|
self.login(self.example_email("iago"))
|
||||||
|
data = {} # type: Dict[str, Union[str, int]]
|
||||||
|
data["name"] = "Favorite programming language"
|
||||||
|
data["field_type"] = CustomProfileField.CHOICE
|
||||||
|
|
||||||
|
data['field_data'] = 'invalid'
|
||||||
|
result = self.client_post("/json/realm/profile_fields", info=data)
|
||||||
|
error_msg = "Bad value for 'field_data': invalid"
|
||||||
|
self.assert_json_error(result, error_msg)
|
||||||
|
|
||||||
|
data["field_data"] = ujson.dumps({
|
||||||
|
'python': ['1'],
|
||||||
|
'java': ['2'],
|
||||||
|
})
|
||||||
|
result = self.client_post("/json/realm/profile_fields", info=data)
|
||||||
|
self.assert_json_error(result, 'field_data is not a dict')
|
||||||
|
|
||||||
|
data["field_data"] = ujson.dumps({
|
||||||
|
'python': {'text': 'Python'},
|
||||||
|
'java': {'text': 'Java'},
|
||||||
|
})
|
||||||
|
result = self.client_post("/json/realm/profile_fields", info=data)
|
||||||
|
self.assert_json_error(result, "order key is missing from field_data")
|
||||||
|
|
||||||
|
data["field_data"] = ujson.dumps({
|
||||||
|
'python': {'text': 'Python', 'order': ''},
|
||||||
|
'java': {'text': 'Java', 'order': '2'},
|
||||||
|
})
|
||||||
|
result = self.client_post("/json/realm/profile_fields", info=data)
|
||||||
|
self.assert_json_error(result, 'field_data["order"] cannot be blank.')
|
||||||
|
|
||||||
|
data["field_data"] = ujson.dumps({
|
||||||
|
'': {'text': 'Python', 'order': '1'},
|
||||||
|
'java': {'text': 'Java', 'order': '2'},
|
||||||
|
})
|
||||||
|
result = self.client_post("/json/realm/profile_fields", info=data)
|
||||||
|
self.assert_json_error(result, "'value' cannot be blank.")
|
||||||
|
|
||||||
|
data["field_data"] = ujson.dumps({
|
||||||
|
'python': {'text': 'Python', 'order': 1},
|
||||||
|
'java': {'text': 'Java', 'order': '2'},
|
||||||
|
})
|
||||||
|
result = self.client_post("/json/realm/profile_fields", info=data)
|
||||||
|
self.assert_json_error(result, 'field_data["order"] is not a string')
|
||||||
|
|
||||||
|
data["field_data"] = ujson.dumps({
|
||||||
|
'python': {'text': 'Python', 'order': '1'},
|
||||||
|
'java': {'text': 'Java', 'order': '2'},
|
||||||
|
})
|
||||||
|
result = self.client_post("/json/realm/profile_fields", info=data)
|
||||||
|
self.assert_json_success(result)
|
||||||
|
|
||||||
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")
|
||||||
@@ -67,11 +120,11 @@ class CustomProfileFieldTest(ZulipTestCase):
|
|||||||
result = self.client_delete("/json/realm/profile_fields/100")
|
result = self.client_delete("/json/realm/profile_fields/100")
|
||||||
self.assert_json_error(result, 'Field id 100 not found.')
|
self.assert_json_error(result, 'Field id 100 not found.')
|
||||||
|
|
||||||
self.assertEqual(CustomProfileField.objects.count(), 3)
|
self.assertEqual(CustomProfileField.objects.count(), 4)
|
||||||
result = self.client_delete(
|
result = self.client_delete(
|
||||||
"/json/realm/profile_fields/{}".format(field.id))
|
"/json/realm/profile_fields/{}".format(field.id))
|
||||||
self.assert_json_success(result)
|
self.assert_json_success(result)
|
||||||
self.assertEqual(CustomProfileField.objects.count(), 2)
|
self.assertEqual(CustomProfileField.objects.count(), 3)
|
||||||
|
|
||||||
def test_update(self) -> None:
|
def test_update(self) -> None:
|
||||||
self.login(self.example_email("iago"))
|
self.login(self.example_email("iago"))
|
||||||
@@ -92,14 +145,14 @@ class CustomProfileFieldTest(ZulipTestCase):
|
|||||||
|
|
||||||
field = CustomProfileField.objects.get(name="Phone number", realm=realm)
|
field = CustomProfileField.objects.get(name="Phone number", realm=realm)
|
||||||
|
|
||||||
self.assertEqual(CustomProfileField.objects.count(), 3)
|
self.assertEqual(CustomProfileField.objects.count(), 4)
|
||||||
result = self.client_patch(
|
result = self.client_patch(
|
||||||
"/json/realm/profile_fields/{}".format(field.id),
|
"/json/realm/profile_fields/{}".format(field.id),
|
||||||
info={'name': 'New phone number',
|
info={'name': 'New phone number',
|
||||||
'field_type': CustomProfileField.SHORT_TEXT})
|
'field_type': CustomProfileField.SHORT_TEXT})
|
||||||
self.assert_json_success(result)
|
self.assert_json_success(result)
|
||||||
field = CustomProfileField.objects.get(id=field.id, realm=realm)
|
field = CustomProfileField.objects.get(id=field.id, realm=realm)
|
||||||
self.assertEqual(CustomProfileField.objects.count(), 3)
|
self.assertEqual(CustomProfileField.objects.count(), 4)
|
||||||
self.assertEqual(field.name, 'New phone number')
|
self.assertEqual(field.name, 'New phone number')
|
||||||
self.assertIs(field.hint, '')
|
self.assertIs(field.hint, '')
|
||||||
self.assertEqual(field.field_type, CustomProfileField.SHORT_TEXT)
|
self.assertEqual(field.field_type, CustomProfileField.SHORT_TEXT)
|
||||||
@@ -120,11 +173,39 @@ class CustomProfileFieldTest(ZulipTestCase):
|
|||||||
self.assert_json_success(result)
|
self.assert_json_success(result)
|
||||||
|
|
||||||
field = CustomProfileField.objects.get(id=field.id, realm=realm)
|
field = CustomProfileField.objects.get(id=field.id, realm=realm)
|
||||||
self.assertEqual(CustomProfileField.objects.count(), 3)
|
self.assertEqual(CustomProfileField.objects.count(), 4)
|
||||||
self.assertEqual(field.name, 'New phone number')
|
self.assertEqual(field.name, 'New phone number')
|
||||||
self.assertEqual(field.hint, 'New contact number')
|
self.assertEqual(field.hint, 'New contact number')
|
||||||
self.assertEqual(field.field_type, CustomProfileField.SHORT_TEXT)
|
self.assertEqual(field.field_type, CustomProfileField.SHORT_TEXT)
|
||||||
|
|
||||||
|
field = CustomProfileField.objects.get(name="Favorite editor", realm=realm)
|
||||||
|
result = self.client_patch(
|
||||||
|
"/json/realm/profile_fields/{}".format(field.id),
|
||||||
|
info={'name': 'Favorite editor',
|
||||||
|
'field_data': 'invalid'})
|
||||||
|
self.assert_json_error(result, "Bad value for 'field_data': invalid")
|
||||||
|
|
||||||
|
field_data = ujson.dumps({
|
||||||
|
'vim': 'Vim',
|
||||||
|
'emacs': {'order': '2', 'text': 'Emacs'},
|
||||||
|
})
|
||||||
|
result = self.client_patch(
|
||||||
|
"/json/realm/profile_fields/{}".format(field.id),
|
||||||
|
info={'name': 'Favorite editor',
|
||||||
|
'field_data': field_data})
|
||||||
|
self.assert_json_error(result, "field_data is not a dict")
|
||||||
|
|
||||||
|
field_data = ujson.dumps({
|
||||||
|
'vim': {'order': '1', 'text': 'Vim'},
|
||||||
|
'emacs': {'order': '2', 'text': 'Emacs'},
|
||||||
|
'notepad': {'order': '3', 'text': 'Notepad'},
|
||||||
|
})
|
||||||
|
result = self.client_patch(
|
||||||
|
"/json/realm/profile_fields/{}".format(field.id),
|
||||||
|
info={'name': 'Favorite editor',
|
||||||
|
'field_data': field_data})
|
||||||
|
self.assert_json_success(result)
|
||||||
|
|
||||||
def test_update_is_aware_of_uniqueness(self) -> None:
|
def test_update_is_aware_of_uniqueness(self) -> None:
|
||||||
self.login(self.example_email("iago"))
|
self.login(self.example_email("iago"))
|
||||||
realm = get_realm('zulip')
|
realm = get_realm('zulip')
|
||||||
@@ -137,7 +218,7 @@ class CustomProfileFieldTest(ZulipTestCase):
|
|||||||
CustomProfileField.SHORT_TEXT
|
CustomProfileField.SHORT_TEXT
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(CustomProfileField.objects.count(), 5)
|
self.assertEqual(CustomProfileField.objects.count(), 6)
|
||||||
result = self.client_patch(
|
result = self.client_patch(
|
||||||
"/json/realm/profile_fields/{}".format(field.id),
|
"/json/realm/profile_fields/{}".format(field.id),
|
||||||
info={'name': 'Phone', 'field_type': CustomProfileField.SHORT_TEXT})
|
info={'name': 'Phone', 'field_type': CustomProfileField.SHORT_TEXT})
|
||||||
@@ -179,6 +260,7 @@ class CustomProfileDataTest(ZulipTestCase):
|
|||||||
('Phone number', 'short text data'),
|
('Phone number', 'short text data'),
|
||||||
('Biography', 'long text data'),
|
('Biography', 'long text data'),
|
||||||
('Favorite food', 'short text data'),
|
('Favorite food', 'short text data'),
|
||||||
|
('Favorite editor', 'vim'),
|
||||||
]
|
]
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
@@ -200,10 +282,10 @@ class CustomProfileDataTest(ZulipTestCase):
|
|||||||
|
|
||||||
for field_dict in iago.profile_data:
|
for field_dict in iago.profile_data:
|
||||||
self.assertEqual(field_dict['value'], expected_value[field_dict['id']])
|
self.assertEqual(field_dict['value'], expected_value[field_dict['id']])
|
||||||
for k in ['id', 'type', 'name']:
|
for k in ['id', 'type', 'name', 'field_data']:
|
||||||
self.assertIn(k, field_dict)
|
self.assertIn(k, field_dict)
|
||||||
|
|
||||||
self.assertEqual(len(iago.profile_data), 3)
|
self.assertEqual(len(iago.profile_data), 4)
|
||||||
|
|
||||||
# Update value of one field.
|
# Update value of one field.
|
||||||
field = CustomProfileField.objects.get(name='Biography', realm=realm)
|
field = CustomProfileField.objects.get(name='Biography', realm=realm)
|
||||||
@@ -219,6 +301,30 @@ class CustomProfileDataTest(ZulipTestCase):
|
|||||||
if f['id'] == field.id:
|
if f['id'] == field.id:
|
||||||
self.assertEqual(f['value'], 'foobar')
|
self.assertEqual(f['value'], 'foobar')
|
||||||
|
|
||||||
|
def test_update_choice_field(self) -> None:
|
||||||
|
self.login(self.example_email("iago"))
|
||||||
|
realm = get_realm('zulip')
|
||||||
|
field = CustomProfileField.objects.get(name='Favorite editor',
|
||||||
|
realm=realm)
|
||||||
|
data = [{
|
||||||
|
'id': field.id,
|
||||||
|
'value': 'foobar',
|
||||||
|
}]
|
||||||
|
|
||||||
|
result = self.client_patch("/json/users/me/profile_data",
|
||||||
|
{'data': ujson.dumps(data)})
|
||||||
|
self.assert_json_error(result,
|
||||||
|
"'foobar' is not a valid choice for 'value[4]'.")
|
||||||
|
|
||||||
|
data = [{
|
||||||
|
'id': field.id,
|
||||||
|
'value': 'emacs',
|
||||||
|
}]
|
||||||
|
|
||||||
|
result = self.client_patch("/json/users/me/profile_data",
|
||||||
|
{'data': ujson.dumps(data)})
|
||||||
|
self.assert_json_success(result)
|
||||||
|
|
||||||
def test_delete(self) -> None:
|
def test_delete(self) -> None:
|
||||||
user_profile = self.example_user('iago')
|
user_profile = self.example_user('iago')
|
||||||
realm = user_profile.realm
|
realm = user_profile.realm
|
||||||
@@ -226,10 +332,10 @@ class CustomProfileDataTest(ZulipTestCase):
|
|||||||
data = [{'id': field.id, 'value': u'123456'}] # type: List[Dict[str, Union[int, Text]]]
|
data = [{'id': field.id, 'value': u'123456'}] # type: List[Dict[str, Union[int, Text]]]
|
||||||
do_update_user_custom_profile_data(user_profile, data)
|
do_update_user_custom_profile_data(user_profile, data)
|
||||||
|
|
||||||
self.assertEqual(len(custom_profile_fields_for_realm(realm.id)), 3)
|
self.assertEqual(len(custom_profile_fields_for_realm(realm.id)), 4)
|
||||||
self.assertEqual(user_profile.customprofilefieldvalue_set.count(), 3)
|
self.assertEqual(user_profile.customprofilefieldvalue_set.count(), 4)
|
||||||
|
|
||||||
do_remove_realm_custom_profile_field(realm, field)
|
do_remove_realm_custom_profile_field(realm, field)
|
||||||
|
|
||||||
self.assertEqual(len(custom_profile_fields_for_realm(realm.id)), 2)
|
self.assertEqual(len(custom_profile_fields_for_realm(realm.id)), 3)
|
||||||
self.assertEqual(user_profile.customprofilefieldvalue_set.count(), 2)
|
self.assertEqual(user_profile.customprofilefieldvalue_set.count(), 3)
|
||||||
|
|||||||
@@ -917,6 +917,7 @@ class EventsRegisterTest(ZulipTestCase):
|
|||||||
('type', check_int),
|
('type', check_int),
|
||||||
('name', check_string),
|
('name', check_string),
|
||||||
('hint', check_string),
|
('hint', check_string),
|
||||||
|
('field_data', check_string),
|
||||||
]))),
|
]))),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
from typing import Union, List, Dict, Optional
|
from typing import Union, List, Dict, Optional
|
||||||
import logging
|
import logging
|
||||||
|
import ujson
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import IntegrityError, connection
|
from django.db import IntegrityError, connection
|
||||||
@@ -14,7 +15,9 @@ from zerver.lib.actions import (try_add_realm_custom_profile_field,
|
|||||||
try_update_realm_custom_profile_field,
|
try_update_realm_custom_profile_field,
|
||||||
do_update_user_custom_profile_data)
|
do_update_user_custom_profile_data)
|
||||||
from zerver.lib.response import json_success, json_error
|
from zerver.lib.response import json_success, json_error
|
||||||
from zerver.lib.validator import check_dict, check_list, check_int, check_capped_string
|
from zerver.lib.types import ProfileFieldData
|
||||||
|
from zerver.lib.validator import (check_dict, check_list, check_int,
|
||||||
|
validate_field_data, check_capped_string)
|
||||||
|
|
||||||
from zerver.models import (custom_profile_fields_for_realm, UserProfile,
|
from zerver.models import (custom_profile_fields_for_realm, UserProfile,
|
||||||
CustomProfileField, custom_profile_fields_for_realm)
|
CustomProfileField, custom_profile_fields_for_realm)
|
||||||
@@ -30,6 +33,8 @@ hint_validator = check_capped_string(CustomProfileField.HINT_MAX_LENGTH)
|
|||||||
def create_realm_custom_profile_field(request: HttpRequest,
|
def create_realm_custom_profile_field(request: HttpRequest,
|
||||||
user_profile: UserProfile, name: str=REQ(),
|
user_profile: UserProfile, name: str=REQ(),
|
||||||
hint: str=REQ(default=''),
|
hint: str=REQ(default=''),
|
||||||
|
field_data: ProfileFieldData=REQ(default={},
|
||||||
|
converter=ujson.loads),
|
||||||
field_type: int=REQ(validator=check_int)) -> HttpResponse:
|
field_type: int=REQ(validator=check_int)) -> HttpResponse:
|
||||||
if not name.strip():
|
if not name.strip():
|
||||||
return json_error(_("Name cannot be blank."))
|
return json_error(_("Name cannot be blank."))
|
||||||
@@ -42,10 +47,15 @@ def create_realm_custom_profile_field(request: HttpRequest,
|
|||||||
if field_type not in field_types:
|
if field_type not in field_types:
|
||||||
return json_error(_("Invalid field type."))
|
return json_error(_("Invalid field type."))
|
||||||
|
|
||||||
|
error = validate_field_data(field_data)
|
||||||
|
if error:
|
||||||
|
return json_error(error)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
field = try_add_realm_custom_profile_field(
|
field = try_add_realm_custom_profile_field(
|
||||||
realm=user_profile.realm,
|
realm=user_profile.realm,
|
||||||
name=name,
|
name=name,
|
||||||
|
field_data=field_data,
|
||||||
field_type=field_type,
|
field_type=field_type,
|
||||||
hint=hint,
|
hint=hint,
|
||||||
)
|
)
|
||||||
@@ -69,7 +79,9 @@ def delete_realm_custom_profile_field(request: HttpRequest, user_profile: UserPr
|
|||||||
@has_request_variables
|
@has_request_variables
|
||||||
def update_realm_custom_profile_field(request: HttpRequest, user_profile: UserProfile,
|
def update_realm_custom_profile_field(request: HttpRequest, user_profile: UserProfile,
|
||||||
field_id: int, name: str=REQ(),
|
field_id: int, name: str=REQ(),
|
||||||
hint: str=REQ(default='')
|
hint: str=REQ(default=''),
|
||||||
|
field_data: ProfileFieldData=REQ(default={},
|
||||||
|
converter=ujson.loads),
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
if not name.strip():
|
if not name.strip():
|
||||||
return json_error(_("Name cannot be blank."))
|
return json_error(_("Name cannot be blank."))
|
||||||
@@ -78,6 +90,10 @@ def update_realm_custom_profile_field(request: HttpRequest, user_profile: UserPr
|
|||||||
if error:
|
if error:
|
||||||
return json_error(error, data={'field': 'hint'})
|
return json_error(error, data={'field': 'hint'})
|
||||||
|
|
||||||
|
error = validate_field_data(field_data)
|
||||||
|
if error:
|
||||||
|
return json_error(error)
|
||||||
|
|
||||||
realm = user_profile.realm
|
realm = user_profile.realm
|
||||||
try:
|
try:
|
||||||
field = CustomProfileField.objects.get(realm=realm, id=field_id)
|
field = CustomProfileField.objects.get(realm=realm, id=field_id)
|
||||||
@@ -85,7 +101,8 @@ def update_realm_custom_profile_field(request: HttpRequest, user_profile: UserPr
|
|||||||
return json_error(_('Field id {id} not found.').format(id=field_id))
|
return json_error(_('Field id {id} not found.').format(id=field_id))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
try_update_realm_custom_profile_field(realm, field, name, hint=hint)
|
try_update_realm_custom_profile_field(realm, field, name, hint=hint,
|
||||||
|
field_data=field_data)
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
return json_error(_('A field with that name already exists.'))
|
return json_error(_('A field with that name already exists.'))
|
||||||
return json_success()
|
return json_success()
|
||||||
@@ -104,8 +121,21 @@ def update_user_custom_profile_data(
|
|||||||
except CustomProfileField.DoesNotExist:
|
except CustomProfileField.DoesNotExist:
|
||||||
return json_error(_('Field id {id} not found.').format(id=field_id))
|
return json_error(_('Field id {id} not found.').format(id=field_id))
|
||||||
|
|
||||||
validator = CustomProfileField.FIELD_VALIDATORS[field.field_type]
|
validators = CustomProfileField.FIELD_VALIDATORS
|
||||||
result = validator('value[{}]'.format(field_id), item['value'])
|
extended_validators = CustomProfileField.EXTENDED_FIELD_VALIDATORS
|
||||||
|
field_type = field.field_type
|
||||||
|
value = item['value']
|
||||||
|
if field_type in validators:
|
||||||
|
validator = validators[field_type]
|
||||||
|
var_name = 'value[{}]'.format(field_id)
|
||||||
|
result = validator(var_name, value)
|
||||||
|
else:
|
||||||
|
# Check extended validators.
|
||||||
|
extended_validator = extended_validators[field_type]
|
||||||
|
field_data = field.field_data
|
||||||
|
var_name = 'value[{}]'.format(field_id)
|
||||||
|
result = extended_validator(var_name, field_data, value)
|
||||||
|
|
||||||
if result is not None:
|
if result is not None:
|
||||||
return json_error(result)
|
return json_error(result)
|
||||||
|
|
||||||
|
|||||||
@@ -266,6 +266,14 @@ class Command(BaseCommand):
|
|||||||
favorite_food = try_add_realm_custom_profile_field(zulip_realm, "Favorite food",
|
favorite_food = try_add_realm_custom_profile_field(zulip_realm, "Favorite food",
|
||||||
CustomProfileField.SHORT_TEXT,
|
CustomProfileField.SHORT_TEXT,
|
||||||
hint="Or drink, if you'd prefer")
|
hint="Or drink, if you'd prefer")
|
||||||
|
field_data = {
|
||||||
|
'vim': {'text': 'Vim', 'order': '1'},
|
||||||
|
'emacs': {'text': 'Emacs', 'order': '2'},
|
||||||
|
}
|
||||||
|
favorite_editor = try_add_realm_custom_profile_field(zulip_realm,
|
||||||
|
"Favorite editor",
|
||||||
|
CustomProfileField.CHOICE,
|
||||||
|
field_data=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)
|
||||||
@@ -273,11 +281,13 @@ class Command(BaseCommand):
|
|||||||
{"id": phone_number.id, "value": "+1-234-567-8901"},
|
{"id": phone_number.id, "value": "+1-234-567-8901"},
|
||||||
{"id": biography.id, "value": "Betrayer of Othello."},
|
{"id": biography.id, "value": "Betrayer of Othello."},
|
||||||
{"id": favorite_food.id, "value": "Apples"},
|
{"id": favorite_food.id, "value": "Apples"},
|
||||||
|
{"id": favorite_editor.id, "value": "emacs"},
|
||||||
])
|
])
|
||||||
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"},
|
||||||
{"id": biography.id, "value": "Prince of Denmark, and other things!"},
|
{"id": biography.id, "value": "Prince of Denmark, and other things!"},
|
||||||
{"id": favorite_food.id, "value": "Dark chocolate"},
|
{"id": favorite_food.id, "value": "Dark chocolate"},
|
||||||
|
{"id": favorite_editor.id, "value": "vim"},
|
||||||
])
|
])
|
||||||
else:
|
else:
|
||||||
zulip_realm = get_realm("zulip")
|
zulip_realm = get_realm("zulip")
|
||||||
|
|||||||
Reference in New Issue
Block a user