profile: Support custom profile data.

Implements backend for #1760.
This commit is contained in:
Umair Khan 2017-03-17 14:07:22 +05:00 committed by Tim Abbott
parent d0f907f9da
commit cf3b6c6ca9
9 changed files with 573 additions and 7 deletions

View File

@ -37,9 +37,11 @@ from zerver.models import Realm, RealmEmoji, Stream, UserProfile, UserActivity,
email_allowed_for_realm, email_to_username, display_recipient_cache_key, \ email_allowed_for_realm, email_to_username, display_recipient_cache_key, \
get_user_profile_by_email, get_stream_cache_key, \ get_user_profile_by_email, get_stream_cache_key, \
UserActivityInterval, get_active_user_dicts_in_realm, get_active_streams, \ UserActivityInterval, get_active_user_dicts_in_realm, get_active_streams, \
realm_filters_for_realm, RealmFilter, ScheduledJob, get_owned_bot_dicts, \ realm_filters_for_realm, RealmFilter, receives_offline_notifications, \
ScheduledJob, get_owned_bot_dicts, \
get_old_unclaimed_attachments, get_cross_realm_emails, \ get_old_unclaimed_attachments, get_cross_realm_emails, \
Reaction, EmailChangeStatus Reaction, EmailChangeStatus, CustomProfileField, custom_profile_fields_for_realm, \
CustomProfileFieldValue
from zerver.lib.alert_words import alert_words_in_realm from zerver.lib.alert_words import alert_words_in_realm
from zerver.lib.avatar import avatar_url from zerver.lib.avatar import avatar_url
@ -3303,3 +3305,41 @@ def check_attachment_reference_change(prev_content, message):
to_add = list(new_attachments - prev_attachments) to_add = list(new_attachments - prev_attachments)
if len(to_add) > 0: if len(to_add) > 0:
do_claim_attachments(message) do_claim_attachments(message)
def notify_realm_custom_profile_fields(realm):
# type: (Realm) -> None
fields = custom_profile_fields_for_realm(realm.id)
event = dict(type="custom_profile_fields",
fields=[f.as_dict() for f in fields])
send_event(event, active_user_ids(realm))
def try_add_realm_custom_profile_field(realm, name, field_type):
# type: (Realm, Text, int) -> CustomProfileField
field = CustomProfileField(realm=realm, name=name, field_type=field_type)
field.save()
notify_realm_custom_profile_fields(realm)
return field
def do_remove_realm_custom_profile_field(realm, field):
# type: (Realm, CustomProfileField) -> None
"""
Deleting a field will also delete the user profile data
associated with it in CustomProfileFieldValue model.
"""
field.delete()
notify_realm_custom_profile_fields(realm)
def try_update_realm_custom_profile_field(realm, field, name):
# type: (Realm, CustomProfileField, Text) -> None
field.name = name
field.save(update_fields=['name'])
notify_realm_custom_profile_fields(realm)
def do_update_user_custom_profile_data(user_profile, data):
# type: (UserProfile, List[Dict[str, Union[int, Text]]]) -> None
with transaction.atomic():
update_or_create = CustomProfileFieldValue.objects.update_or_create
for field in data:
update_or_create(user_profile=user_profile,
field_id=field['id'],
defaults={'value': field['value']})

View File

@ -32,7 +32,7 @@ from zerver.tornado.event_queue import request_event_queue, get_user_events
from zerver.models import Client, Message, UserProfile, \ from zerver.models import Client, Message, UserProfile, \
get_user_profile_by_email, get_user_profile_by_id, \ get_user_profile_by_email, get_user_profile_by_id, \
get_active_user_dicts_in_realm, realm_filters_for_realm, \ get_active_user_dicts_in_realm, realm_filters_for_realm, \
get_owned_bot_dicts get_owned_bot_dicts, custom_profile_fields_for_realm
from version import ZULIP_VERSION from version import ZULIP_VERSION
@ -71,6 +71,10 @@ def fetch_initial_state_data(user_profile, event_types, queue_id,
if want('alert_words'): if want('alert_words'):
state['alert_words'] = user_alert_words(user_profile) state['alert_words'] = user_alert_words(user_profile)
if want('custom_profile_fields'):
fields = custom_profile_fields_for_realm(user_profile.realm.id)
state['custom_profile_fields'] = [f.as_dict() for f in fields]
if want('attachments'): if want('attachments'):
state['attachments'] = user_attachments(user_profile) state['attachments'] = user_attachments(user_profile)
@ -188,6 +192,8 @@ def apply_event(state, event, user_profile, include_subscribers):
state['max_message_id'] = max(state['max_message_id'], event['message']['id']) state['max_message_id'] = max(state['max_message_id'], event['message']['id'])
elif event['type'] == "hotspots": elif event['type'] == "hotspots":
state['hotspots'] = event['hotspots'] state['hotspots'] = event['hotspots']
elif event['type'] == "custom_profile_fields":
state['custom_profile_fields'] = event['fields']
elif event['type'] == "pointer": elif event['type'] == "pointer":
state['pointer'] = max(state['pointer'], event['pointer']) state['pointer'] = max(state['pointer'], event['pointer'])
elif event['type'] == "realm_user": elif event['type'] == "realm_user":

View File

@ -42,6 +42,14 @@ def check_string(var_name, val):
return _('%s is not a string') % (var_name,) return _('%s is not a string') % (var_name,)
return None return None
def check_short_string(var_name, val):
# type: (str, Any) -> Optional[str]
max_length = 200
if len(val) >= max_length:
return _("{var_name} is longer than {max_length}.".format(
var_name=var_name, max_length=max_length))
return check_string(var_name, val)
def check_int(var_name, val): def check_int(var_name, val):
# type: (str, Any) -> Optional[str] # type: (str, Any) -> Optional[str]
if not isinstance(val, int): if not isinstance(val, int):

View File

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-04-17 06:49
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('zerver', '0072_realmauditlog_add_index_event_time'),
]
operations = [
migrations.CreateModel(
name='CustomProfileField',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('field_type', models.PositiveSmallIntegerField(choices=[(1, 'Integer'), (2, 'Float'), (3, 'Short Text'), (4, 'Long Text')], default=3)),
('realm', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='zerver.Realm')),
],
),
migrations.CreateModel(
name='CustomProfileFieldValue',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('value', models.TextField()),
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='zerver.CustomProfileField')),
('user_profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.AlterUniqueTogether(
name='customprofilefieldvalue',
unique_together=set([('user_profile', 'field')]),
),
migrations.AlterUniqueTogether(
name='customprofilefield',
unique_together=set([('realm', 'name')]),
),
]

View File

@ -1,18 +1,19 @@
from __future__ import absolute_import from __future__ import absolute_import
from typing import Any, DefaultDict, Dict, List, Set, Tuple, TypeVar, Text, \ from typing import Any, DefaultDict, Dict, List, Set, Tuple, TypeVar, Text, \
Union, Optional, Sequence, AbstractSet, Pattern, AnyStr Union, Optional, Sequence, AbstractSet, Pattern, AnyStr, Callable
from typing.re import Match from typing.re import Match
from zerver.lib.str_utils import NonBinaryStr from zerver.lib.str_utils import NonBinaryStr
from django.db import models from django.db import models
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.db.models import Manager from django.db.models import Manager, CASCADE
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AbstractBaseUser, UserManager, \ from django.contrib.auth.models import AbstractBaseUser, UserManager, \
PermissionsMixin PermissionsMixin
import django.contrib.auth import django.contrib.auth
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import URLValidator from django.core.validators import URLValidator, MinLengthValidator, \
RegexValidator
from django.dispatch import receiver from django.dispatch import receiver
from zerver.lib.cache import cache_with_key, flush_user_profile, flush_realm, \ from zerver.lib.cache import cache_with_key, flush_user_profile, flush_realm, \
user_profile_by_id_cache_key, user_profile_by_email_cache_key, \ user_profile_by_id_cache_key, user_profile_by_email_cache_key, \
@ -29,9 +30,10 @@ from django.utils.timezone import now as timezone_now
from django.contrib.sessions.models import Session from django.contrib.sessions.models import Session
from zerver.lib.timestamp import datetime_to_timestamp from zerver.lib.timestamp import datetime_to_timestamp
from django.db.models.signals import pre_save, post_save, post_delete from django.db.models.signals import pre_save, post_save, post_delete
from django.core.validators import MinLengthValidator, RegexValidator
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, check_string, \
check_short_string
from bitfield import BitField from bitfield import BitField
from bitfield.types import BitHandler from bitfield.types import BitHandler
@ -642,6 +644,27 @@ class UserProfile(ModelReprMixin, AbstractBaseUser, PermissionsMixin):
twenty_four_hour_time=bool, twenty_four_hour_time=bool,
) )
@property
def profile_data(self):
# type: () -> List[Dict[str, Union[int, float, Text]]]
values = CustomProfileFieldValue.objects.filter(user_profile=self)
user_data = {v.field_id: v.value for v in values}
data = [] # type: List[Dict[str, Union[int, float, Text]]]
for field in custom_profile_fields_for_realm(self.realm_id):
value = user_data.get(field.id, None)
field_type = field.field_type
if value is not None:
converter = field.FIELD_CONVERTERS[field_type]
value = converter(value)
field_data = {} # type: Dict[str, Union[int, float, Text]]
for k, v in field.as_dict().items():
field_data[k] = v
field_data['value'] = value
data.append(field_data)
return data
def can_admin_user(self, target_user): def can_admin_user(self, target_user):
# type: (UserProfile) -> bool # type: (UserProfile) -> bool
"""Returns whether this user has permission to modify target_user""" """Returns whether this user has permission to modify target_user"""
@ -1605,3 +1628,50 @@ class UserHotspot(models.Model):
class Meta(object): class Meta(object):
unique_together = ("user", "hotspot") unique_together = ("user", "hotspot")
class CustomProfileField(models.Model):
realm = models.ForeignKey(Realm, on_delete=CASCADE) # type: Realm
name = models.CharField(max_length=100) # type: Text
INTEGER = 1
FLOAT = 2
SHORT_TEXT = 3
LONG_TEXT = 4
FIELD_TYPE_DATA = [
# Type, Name, Validator, Converter
(INTEGER, u'Integer', check_int, int),
(FLOAT, u'Float', check_float, float),
(SHORT_TEXT, u'Short Text', check_short_string, str),
(LONG_TEXT, u'Long Text', check_string, str),
] # type: List[Tuple[int, Text, Callable[[str, Any], str], Callable[[Any], Any]]]
FIELD_VALIDATORS = {item[0]: item[2] for item in FIELD_TYPE_DATA} # type: Dict[int, Callable[[str, Any], str]]
FIELD_CONVERTERS = {item[0]: item[3] for item in FIELD_TYPE_DATA} # 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 = models.PositiveSmallIntegerField(choices=FIELD_TYPE_CHOICES,
default=SHORT_TEXT) # type: int
class Meta(object):
unique_together = ('realm', 'name')
def as_dict(self):
# type: () -> Dict[str, Union[int, Text]]
return {
'id': self.id,
'name': self.name,
'type': self.field_type,
}
def custom_profile_fields_for_realm(realm_id):
# type: (int) -> List[CustomProfileField]
return CustomProfileField.objects.filter(realm=realm_id).order_by('name')
class CustomProfileFieldValue(models.Model):
user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) # type: UserProfile
field = models.ForeignKey(CustomProfileField, on_delete=CASCADE) # type: CustomProfileField
value = models.TextField() # type: Text
class Meta(object):
unique_together = ('user_profile', 'field')

View File

@ -0,0 +1,267 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from typing import Union, List, Dict, Text, Any
from mock import patch
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
from zerver.lib.test_classes import ZulipTestCase
from zerver.models import CustomProfileField, get_user_profile_by_email, \
custom_profile_fields_for_realm
import ujson
class CustomProfileFieldTest(ZulipTestCase):
def test_list(self):
# type: () -> None
self.login(u"iago@zulip.com")
realm = get_realm('zulip')
try_add_realm_custom_profile_field(realm, u"Phone",
CustomProfileField.SHORT_TEXT)
result = self.client_get("/json/realm/profile_fields")
self.assert_json_success(result)
self.assertEqual(200, result.status_code)
content = result.json()
self.assertEqual(len(content["custom_fields"]), 1)
def test_create(self):
# type: () -> None
self.login(u"iago@zulip.com")
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.')
data["name"] = ""
data["field_type"] = 100
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_error(result, u'Name cannot be blank.')
data["name"] = "Phone"
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_error(result, u'Invalid field type.')
data["name"] = "Phone"
data["field_type"] = CustomProfileField.SHORT_TEXT
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_success(result)
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_error(result,
u'A field with that name already exists.')
def test_not_realm_admin(self):
# type: () -> None
self.login("hamlet@zulip.com")
result = self.client_post("/json/realm/profile_fields")
self.assert_json_error(result, u'Must be a realm administrator')
result = self.client_delete("/json/realm/profile_fields/1")
self.assert_json_error(result, 'Must be a realm administrator')
def test_delete(self):
# type: () -> None
self.login("iago@zulip.com")
realm = get_realm('zulip')
field = try_add_realm_custom_profile_field(
realm,
"Phone",
CustomProfileField.SHORT_TEXT
)
result = self.client_delete("/json/realm/profile_fields/100")
self.assert_json_error(result, 'Field id 100 not found.')
self.assertEqual(CustomProfileField.objects.count(), 1)
result = self.client_delete(
"/json/realm/profile_fields/{}".format(field.id))
self.assert_json_success(result)
self.assertEqual(CustomProfileField.objects.count(), 0)
def test_update(self):
# type: () -> None
self.login("iago@zulip.com")
realm = get_realm('zulip')
result = self.client_patch(
"/json/realm/profile_fields/100",
info={'name': '',
'field_type': CustomProfileField.SHORT_TEXT}
)
self.assert_json_error(result, u'Name cannot be blank.')
result = self.client_patch(
"/json/realm/profile_fields/100",
info={'name': 'Phone Number',
'field_type': CustomProfileField.SHORT_TEXT}
)
self.assert_json_error(result, u'Field id 100 not found.')
field = try_add_realm_custom_profile_field(
realm,
u"Phone",
CustomProfileField.SHORT_TEXT
)
self.assertEqual(CustomProfileField.objects.count(), 1)
result = self.client_patch(
"/json/realm/profile_fields/{}".format(field.id),
info={'name': 'Phone Number',
'field_type': CustomProfileField.SHORT_TEXT})
self.assert_json_success(result)
self.assertEqual(CustomProfileField.objects.count(), 1)
field = CustomProfileField.objects.first()
self.assertEqual(field.name, 'Phone Number')
self.assertEqual(field.field_type, CustomProfileField.SHORT_TEXT)
def test_update_is_aware_of_uniqueness(self):
# type: () -> None
self.login(u"iago@zulip.com")
realm = get_realm('zulip')
try_add_realm_custom_profile_field(realm, u"Phone",
CustomProfileField.SHORT_TEXT)
field = try_add_realm_custom_profile_field(
realm,
u"Phone 1",
CustomProfileField.SHORT_TEXT
)
self.assertEqual(CustomProfileField.objects.count(), 2)
result = self.client_patch(
"/json/realm/profile_fields/{}".format(field.id),
info={'name': 'Phone', 'field_type': CustomProfileField.SHORT_TEXT})
self.assert_json_error(
result, u'A field with that name already exists.')
class CustomProfileDataTest(ZulipTestCase):
def test_update_invalid(self):
# type: () -> None
self.login(u"iago@zulip.com")
data = [{'id': 1234, 'value': '12'}]
result = self.client_patch("/json/users/me/profile_data", {
'data': ujson.dumps(data)
})
self.assert_json_error(result,
u"Field id 1234 not found.")
def test_update_invalid_value(self):
# type: () -> None
self.login(u"iago@zulip.com")
realm = get_realm('zulip')
age_field = try_add_realm_custom_profile_field(
realm,
u"age",
CustomProfileField.INTEGER
)
data = [{'id': age_field.id, 'value': 'text'}]
result = self.client_patch("/json/users/me/profile_data", {
'data': ujson.dumps(data)
})
self.assert_json_error(
result,
u"value[4] is not an integer".format(age_field.id))
def test_update_invalid_double(self):
# type: () -> None
self.login(u"iago@zulip.com")
realm = get_realm('zulip')
field = try_add_realm_custom_profile_field(
realm,
u"distance",
CustomProfileField.FLOAT
)
data = [{'id': field.id, 'value': 'text'}]
result = self.client_patch("/json/users/me/profile_data", {
'data': ujson.dumps(data)
})
self.assert_json_error(
result,
u"value[{}] is not a float".format(field.id))
def test_update_invalid_short_text(self):
# type: () -> None
self.login(u"iago@zulip.com")
realm = get_realm('zulip')
field = try_add_realm_custom_profile_field(
realm,
u"description",
CustomProfileField.SHORT_TEXT
)
data = [{'id': field.id, 'value': 't' * 201}]
result = self.client_patch("/json/users/me/profile_data", {
'data': ujson.dumps(data)
})
self.assert_json_error(
result,
u"value[{}] is longer than 200.".format(field.id))
def test_update_profile_data(self):
# type: () -> None
self.login(u"iago@zulip.com")
realm = get_realm('zulip')
fields = [
(CustomProfileField.SHORT_TEXT, 'name 1', 'short text data'),
(CustomProfileField.LONG_TEXT, 'name 2', 'long text data'),
(CustomProfileField.INTEGER, 'name 3', 1),
(CustomProfileField.FLOAT, 'name 4', 2.0),
]
data = []
for i, field_value in enumerate(fields):
field_type, name, value = field_value
field = try_add_realm_custom_profile_field(realm, name, field_type)
data.append({
'id': field.id,
'value': value,
})
result = self.client_patch("/json/users/me/profile_data",
{'data': ujson.dumps(data)})
self.assert_json_success(result)
iago = get_user_profile_by_email('iago@zulip.com')
expected_value = {f['id']: f['value'] for f in data}
for field in iago.profile_data:
self.assertEqual(field['value'], expected_value[field['id']])
for k in ['id', 'type', 'name']:
self.assertIn(k, field)
self.assertEqual(len(iago.profile_data), 4)
# Update value of field
field = CustomProfileField.objects.get(name='name 1', realm=realm)
data = [{
'id': field.id,
'value': 'foobar',
}]
result = self.client_patch("/json/users/me/profile_data",
{'data': ujson.dumps(data)})
self.assert_json_success(result)
for f in iago.profile_data:
if f['id'] == field.id:
self.assertEqual(f['value'], 'foobar')
def test_delete(self):
# type: () -> None
user_profile = get_user_profile_by_email('iago@zulip.com')
realm = user_profile.realm
field = try_add_realm_custom_profile_field(
realm,
u"Phone",
CustomProfileField.SHORT_TEXT
)
data = [{'id': field.id, 'value': u'123456'}] # type: List[Dict[str, Union[int, Text]]]
do_update_user_custom_profile_data(user_profile, data)
self.assertEqual(len(custom_profile_fields_for_realm(realm.id)), 1)
self.assertEqual(user_profile.customprofilefieldvalue_set.count(), 1)
do_remove_realm_custom_profile_field(realm, field)
self.assertEqual(len(custom_profile_fields_for_realm(realm.id)), 0)
self.assertEqual(user_profile.customprofilefieldvalue_set.count(), 0)

View File

@ -22,6 +22,7 @@ from zerver.lib.actions import (
do_add_alert_words, do_add_alert_words,
check_add_realm_emoji, check_add_realm_emoji,
check_send_typing_notification, check_send_typing_notification,
notify_realm_custom_profile_fields,
do_add_realm_filter, do_add_realm_filter,
do_add_reaction, do_add_reaction,
do_remove_reaction, do_remove_reaction,
@ -576,6 +577,25 @@ class EventsRegisterTest(ZulipTestCase):
error = schema_checker('events[0]', events[0]) error = schema_checker('events[0]', events[0])
self.assert_on_error(error) self.assert_on_error(error)
def test_custom_profile_fields_events(self):
# type: () -> None
schema_checker = check_dict([
('type', equals('custom_profile_fields')),
('fields', check_list(check_dict([
('id', check_int),
('type', check_int),
('name', check_string),
]))),
])
events = self.do_test(
lambda: notify_realm_custom_profile_fields(
self.user_profile.realm),
state_change_expected=False,
)
error = schema_checker('events[0]', events[0])
self.assert_on_error(error)
def test_presence_events(self): def test_presence_events(self):
# type: () -> None # type: () -> None
schema_checker = check_dict([ schema_checker = check_dict([

View File

@ -0,0 +1,100 @@
from __future__ import absolute_import
from typing import Text, Union, List, Dict
import logging
from django.core.exceptions import ValidationError
from django.db import IntegrityError, connection
from django.http import HttpRequest, HttpResponse
from django.utils.translation import ugettext as _
from zerver.decorator import has_request_variables, REQ, require_realm_admin
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)
from zerver.lib.response import json_success, json_error
from zerver.lib.validator import check_dict, check_list, check_int
from zerver.models import (custom_profile_fields_for_realm, UserProfile,
CustomProfileField, custom_profile_fields_for_realm)
def list_realm_custom_profile_fields(request, user_profile):
# type: (HttpRequest, UserProfile) -> HttpResponse
fields = custom_profile_fields_for_realm(user_profile.realm_id)
return json_success({'custom_fields': [f.as_dict() for f in fields]})
@require_realm_admin
@has_request_variables
def create_realm_custom_profile_field(request, user_profile, name=REQ(),
field_type=REQ(validator=check_int)):
# type: (HttpRequest, UserProfile, Text, int) -> HttpResponse
if not name.strip():
return json_error(_("Name cannot be blank."))
if field_type not in CustomProfileField.FIELD_VALIDATORS:
return json_error(_("Invalid field type."))
try:
field = try_add_realm_custom_profile_field(
realm=user_profile.realm,
name=name,
field_type=field_type,
)
return json_success({'id': field.id})
except IntegrityError:
return json_error(_("A field with that name already exists."))
@require_realm_admin
def delete_realm_custom_profile_field(request, user_profile, field_id):
# type: (HttpRequest, UserProfile, int) -> HttpResponse
try:
field = CustomProfileField.objects.get(id=field_id)
except CustomProfileField.DoesNotExist:
return json_error(_('Field id {id} not found.').format(id=field_id))
do_remove_realm_custom_profile_field(realm=user_profile.realm,
field=field)
return json_success()
@require_realm_admin
@has_request_variables
def update_realm_custom_profile_field(request, user_profile, field_id,
name=REQ()):
# type: (HttpRequest, UserProfile, int, Text) -> HttpResponse
if not name.strip():
return json_error(_("Name cannot be blank."))
realm = user_profile.realm
try:
field = CustomProfileField.objects.get(realm=realm, id=field_id)
except CustomProfileField.DoesNotExist:
return json_error(_('Field id {id} not found.').format(id=field_id))
try:
try_update_realm_custom_profile_field(realm, field, name)
except IntegrityError:
return json_error(_('A field with that name already exists.'))
return json_success()
@has_request_variables
def update_user_custom_profile_data(
request,
user_profile,
data=REQ(validator=check_list(check_dict([('id', check_int)])))):
# type: (HttpRequest, UserProfile, List[Dict[str, Union[int, Text]]]) -> HttpResponse
for item in data:
field_id = item['id']
try:
field = CustomProfileField.objects.get(id=field_id)
except CustomProfileField.DoesNotExist:
return json_error(_('Field id {id} not found.').format(id=field_id))
validator = CustomProfileField.FIELD_VALIDATORS[field.field_type]
result = validator('value[{}]'.format(field_id), item['value'])
if result is not None:
return json_error(result)
do_update_user_custom_profile_data(user_profile, data)
# We need to call this explicitly otherwise constraints are not check
return json_success()

View File

@ -200,6 +200,14 @@ v1_api_and_json_patterns = [
url(r'^realm/filters/(?P<filter_id>\d+)$', rest_dispatch, url(r'^realm/filters/(?P<filter_id>\d+)$', rest_dispatch,
{'DELETE': 'zerver.views.realm_filters.delete_filter'}), {'DELETE': 'zerver.views.realm_filters.delete_filter'}),
# 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',
'POST': 'zerver.views.custom_profile_fields.create_realm_custom_profile_field'}),
url(r'^realm/profile_fields/(?P<field_id>\d+)$', rest_dispatch,
{'PATCH': 'zerver.views.custom_profile_fields.update_realm_custom_profile_field',
'DELETE': 'zerver.views.custom_profile_fields.delete_realm_custom_profile_field'}),
# users -> zerver.views.users # users -> zerver.views.users
# #
# Since some of these endpoints do something different if used on # Since some of these endpoints do something different if used on
@ -317,6 +325,10 @@ v1_api_and_json_patterns = [
'PUT': 'zerver.views.alert_words.add_alert_words', 'PUT': 'zerver.views.alert_words.add_alert_words',
'DELETE': 'zerver.views.alert_words.remove_alert_words'}), 'DELETE': 'zerver.views.alert_words.remove_alert_words'}),
# users/me/custom_profile_data -> zerver.views.custom_profile_data
url(r'^users/me/profile_data$', rest_dispatch,
{'PATCH': 'zerver.views.custom_profile_fields.update_user_custom_profile_data'}),
url(r'^users/me/(?P<stream_id>\d+)/topics$', rest_dispatch, url(r'^users/me/(?P<stream_id>\d+)/topics$', rest_dispatch,
{'GET': 'zerver.views.streams.get_topics_backend'}), {'GET': 'zerver.views.streams.get_topics_backend'}),