mirror of https://github.com/zulip/zulip.git
parent
d0f907f9da
commit
cf3b6c6ca9
|
@ -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']})
|
||||||
|
|
|
@ -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":
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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')]),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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')
|
||||||
|
|
|
@ -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)
|
|
@ -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([
|
||||||
|
|
|
@ -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()
|
|
@ -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'}),
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue