2020-06-11 00:54:34 +02:00
|
|
|
from typing import Dict, List, Union
|
2017-03-17 10:07:22 +01:00
|
|
|
|
2020-06-11 00:54:34 +02:00
|
|
|
import ujson
|
2019-02-02 23:53:22 +01:00
|
|
|
from django.db import IntegrityError
|
2017-03-17 10:07:22 +01:00
|
|
|
from django.http import HttpRequest, HttpResponse
|
|
|
|
from django.utils.translation import ugettext as _
|
|
|
|
|
2020-06-11 00:54:34 +02:00
|
|
|
from zerver.decorator import human_users_only, require_realm_admin
|
|
|
|
from zerver.lib.actions import (
|
|
|
|
check_remove_custom_profile_field_value,
|
|
|
|
do_remove_realm_custom_profile_field,
|
|
|
|
do_update_user_custom_profile_data_if_changed,
|
|
|
|
try_add_realm_custom_profile_field,
|
|
|
|
try_add_realm_default_custom_profile_field,
|
|
|
|
try_reorder_realm_custom_profile_fields,
|
|
|
|
try_update_realm_custom_profile_field,
|
|
|
|
)
|
2018-08-16 20:12:49 +02:00
|
|
|
from zerver.lib.exceptions import JsonableError
|
2019-05-27 10:59:55 +02:00
|
|
|
from zerver.lib.external_accounts import validate_external_account_field_data
|
2020-06-11 00:54:34 +02:00
|
|
|
from zerver.lib.request import REQ, has_request_variables
|
|
|
|
from zerver.lib.response import json_error, json_success
|
|
|
|
from zerver.lib.types import ProfileFieldData
|
|
|
|
from zerver.lib.users import validate_user_custom_profile_data
|
|
|
|
from zerver.lib.validator import (
|
|
|
|
check_capped_string,
|
|
|
|
check_dict,
|
|
|
|
check_int,
|
|
|
|
check_list,
|
2020-06-21 03:22:21 +02:00
|
|
|
check_string,
|
|
|
|
check_union,
|
2020-06-11 00:54:34 +02:00
|
|
|
validate_choice_field_data,
|
|
|
|
)
|
|
|
|
from zerver.models import CustomProfileField, UserProfile, custom_profile_fields_for_realm
|
|
|
|
|
2017-03-17 10:07:22 +01:00
|
|
|
|
2017-10-27 02:18:49 +02:00
|
|
|
def list_realm_custom_profile_fields(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:
|
2017-03-17 10:07:22 +01:00
|
|
|
fields = custom_profile_fields_for_realm(user_profile.realm_id)
|
|
|
|
return json_success({'custom_fields': [f.as_dict() for f in fields]})
|
|
|
|
|
2018-03-31 07:30:24 +02:00
|
|
|
hint_validator = check_capped_string(CustomProfileField.HINT_MAX_LENGTH)
|
2018-08-16 20:12:49 +02:00
|
|
|
name_validator = check_capped_string(CustomProfileField.NAME_MAX_LENGTH)
|
|
|
|
|
|
|
|
def validate_field_name_and_hint(name: str, hint: str) -> None:
|
|
|
|
if not name.strip():
|
2019-08-03 02:30:15 +02:00
|
|
|
raise JsonableError(_("Label cannot be blank."))
|
2018-08-16 20:12:49 +02:00
|
|
|
|
|
|
|
error = hint_validator('hint', hint)
|
|
|
|
if error:
|
|
|
|
raise JsonableError(error)
|
|
|
|
|
|
|
|
error = name_validator('name', name)
|
|
|
|
if error:
|
|
|
|
raise JsonableError(error)
|
2018-03-31 07:30:24 +02:00
|
|
|
|
2019-06-07 08:00:37 +02:00
|
|
|
def validate_custom_field_data(field_type: int,
|
|
|
|
field_data: ProfileFieldData) -> None:
|
|
|
|
error = None
|
|
|
|
if field_type == CustomProfileField.CHOICE:
|
|
|
|
# Choice type field must have at least have one choice
|
|
|
|
if len(field_data) < 1:
|
|
|
|
raise JsonableError(_("Field must have at least one choice."))
|
|
|
|
error = validate_choice_field_data(field_data)
|
2019-05-27 10:59:55 +02:00
|
|
|
elif field_type == CustomProfileField.EXTERNAL_ACCOUNT:
|
|
|
|
error = validate_external_account_field_data(field_data)
|
2019-06-07 08:00:37 +02:00
|
|
|
|
|
|
|
if error:
|
|
|
|
raise JsonableError(error)
|
|
|
|
|
2019-08-24 13:52:25 +02:00
|
|
|
def is_default_external_field(field_type: int, field_data: ProfileFieldData) -> bool:
|
|
|
|
if field_type != CustomProfileField.EXTERNAL_ACCOUNT:
|
|
|
|
return False
|
|
|
|
if field_data['subtype'] == 'custom':
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
2019-08-24 13:13:48 +02:00
|
|
|
def validate_custom_profile_field(name: str, hint: str, field_type: int,
|
|
|
|
field_data: ProfileFieldData) -> None:
|
|
|
|
# Validate field data
|
|
|
|
validate_custom_field_data(field_type, field_data)
|
|
|
|
|
2019-08-24 13:52:25 +02:00
|
|
|
if not is_default_external_field(field_type, field_data):
|
|
|
|
# If field is default external field then we will fetch all data
|
|
|
|
# from our default field dictionary, so no need to validate name or hint
|
|
|
|
# Validate field name, hint if not default external account field
|
|
|
|
validate_field_name_and_hint(name, hint)
|
|
|
|
|
2019-08-24 13:13:48 +02:00
|
|
|
field_types = [i[0] for i in CustomProfileField.FIELD_TYPE_CHOICES]
|
|
|
|
if field_type not in field_types:
|
|
|
|
raise JsonableError(_("Invalid field type."))
|
|
|
|
|
2017-03-17 10:07:22 +01:00
|
|
|
@require_realm_admin
|
|
|
|
@has_request_variables
|
2017-12-24 16:06:24 +01:00
|
|
|
def create_realm_custom_profile_field(request: HttpRequest,
|
2018-08-16 20:12:49 +02:00
|
|
|
user_profile: UserProfile,
|
2019-08-24 13:52:25 +02:00
|
|
|
name: str=REQ(default=''),
|
2018-04-24 03:47:28 +02:00
|
|
|
hint: str=REQ(default=''),
|
2018-04-08 09:50:05 +02:00
|
|
|
field_data: ProfileFieldData=REQ(default={},
|
|
|
|
converter=ujson.loads),
|
2017-12-24 16:06:24 +01:00
|
|
|
field_type: int=REQ(validator=check_int)) -> HttpResponse:
|
2019-08-24 13:13:48 +02:00
|
|
|
validate_custom_profile_field(name, hint, field_type, field_data)
|
2017-03-17 10:07:22 +01:00
|
|
|
try:
|
2019-08-24 13:52:25 +02:00
|
|
|
if is_default_external_field(field_type, field_data):
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
field_subtype: str = ''
|
2020-04-22 04:13:37 +02:00
|
|
|
field_subtype = field_data['subtype'] # type: ignore[assignment] # key for "Union[Dict[str, str], str]" can be str
|
2019-08-24 13:52:25 +02:00
|
|
|
field = try_add_realm_default_custom_profile_field(
|
|
|
|
realm=user_profile.realm,
|
|
|
|
field_subtype=field_subtype,
|
|
|
|
)
|
|
|
|
return json_success({'id': field.id})
|
|
|
|
else:
|
|
|
|
field = try_add_realm_custom_profile_field(
|
|
|
|
realm=user_profile.realm,
|
|
|
|
name=name,
|
|
|
|
field_data=field_data,
|
|
|
|
field_type=field_type,
|
|
|
|
hint=hint,
|
|
|
|
)
|
|
|
|
return json_success({'id': field.id})
|
2017-03-17 10:07:22 +01:00
|
|
|
except IntegrityError:
|
2019-08-03 02:30:15 +02:00
|
|
|
return json_error(_("A field with that label already exists."))
|
2017-03-17 10:07:22 +01:00
|
|
|
|
|
|
|
@require_realm_admin
|
2017-10-27 02:18:49 +02:00
|
|
|
def delete_realm_custom_profile_field(request: HttpRequest, user_profile: UserProfile,
|
|
|
|
field_id: int) -> HttpResponse:
|
2017-03-17 10:07:22 +01:00
|
|
|
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
|
2017-10-27 02:18:49 +02:00
|
|
|
def update_realm_custom_profile_field(request: HttpRequest, user_profile: UserProfile,
|
2018-08-16 20:12:49 +02:00
|
|
|
field_id: int,
|
2019-08-24 13:52:25 +02:00
|
|
|
name: str=REQ(default=''),
|
2018-04-08 09:50:05 +02:00
|
|
|
hint: str=REQ(default=''),
|
|
|
|
field_data: ProfileFieldData=REQ(default={},
|
|
|
|
converter=ujson.loads),
|
2018-03-31 07:30:24 +02:00
|
|
|
) -> HttpResponse:
|
2017-03-17 10:07:22 +01:00
|
|
|
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))
|
|
|
|
|
2019-08-24 13:52:25 +02:00
|
|
|
if field.field_type == CustomProfileField.EXTERNAL_ACCOUNT:
|
|
|
|
if is_default_external_field(field.field_type, ujson.loads(field.field_data)):
|
|
|
|
return json_error(_("Default custom field cannot be updated."))
|
|
|
|
|
2019-08-24 13:13:48 +02:00
|
|
|
validate_custom_profile_field(name, hint, field.field_type, field_data)
|
2017-03-17 10:07:22 +01:00
|
|
|
try:
|
2018-04-08 09:50:05 +02:00
|
|
|
try_update_realm_custom_profile_field(realm, field, name, hint=hint,
|
|
|
|
field_data=field_data)
|
2017-03-17 10:07:22 +01:00
|
|
|
except IntegrityError:
|
2019-08-03 02:30:15 +02:00
|
|
|
return json_error(_('A field with that label already exists.'))
|
2017-03-17 10:07:22 +01:00
|
|
|
return json_success()
|
|
|
|
|
2018-04-08 18:13:37 +02:00
|
|
|
@require_realm_admin
|
|
|
|
@has_request_variables
|
|
|
|
def reorder_realm_custom_profile_fields(request: HttpRequest, user_profile: UserProfile,
|
|
|
|
order: List[int]=REQ(validator=check_list(
|
|
|
|
check_int))) -> HttpResponse:
|
|
|
|
try_reorder_realm_custom_profile_fields(user_profile.realm, order)
|
|
|
|
return json_success()
|
|
|
|
|
2018-06-05 12:57:02 +02:00
|
|
|
@human_users_only
|
|
|
|
@has_request_variables
|
|
|
|
def remove_user_custom_profile_data(request: HttpRequest, user_profile: UserProfile,
|
|
|
|
data: List[int]=REQ(validator=check_list(
|
|
|
|
check_int))) -> HttpResponse:
|
|
|
|
for field_id in data:
|
2019-01-15 11:52:14 +01:00
|
|
|
check_remove_custom_profile_field_value(user_profile, field_id)
|
2018-06-05 12:57:02 +02:00
|
|
|
return json_success()
|
|
|
|
|
2017-07-31 20:30:08 +02:00
|
|
|
@human_users_only
|
2017-03-17 10:07:22 +01:00
|
|
|
@has_request_variables
|
|
|
|
def update_user_custom_profile_data(
|
2020-06-21 03:22:21 +02:00
|
|
|
request: HttpRequest,
|
|
|
|
user_profile: UserProfile,
|
|
|
|
data: List[Dict[str, Union[int, str, List[int]]]] = REQ(
|
|
|
|
validator=check_list(
|
|
|
|
check_dict(
|
|
|
|
[('id', check_int)],
|
|
|
|
value_validator=check_union([check_int, check_string, check_list(check_int)]),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
),
|
|
|
|
) -> HttpResponse:
|
2017-03-17 10:07:22 +01:00
|
|
|
|
2018-09-04 20:23:44 +02:00
|
|
|
validate_user_custom_profile_data(user_profile.realm.id, data)
|
2019-10-01 04:22:50 +02:00
|
|
|
do_update_user_custom_profile_data_if_changed(user_profile, data)
|
2017-03-17 10:07:22 +01:00
|
|
|
# We need to call this explicitly otherwise constraints are not check
|
|
|
|
return json_success()
|