mirror of https://github.com/zulip/zulip.git
custom_profile_fields: Add "display_in_profile_summary" field in model.
To allow `custom_profile_field` to display in user profile popover, added new boolean field "display_in_profile_summary" in its model class. In `custom_profile_fields.py`, functions are edited as per conditions, like currently we can display max 2 `custom_profile_fields` except `LONG_TEXT` and `USER` type fields. Default external account custom profile fields made updatable for only this new field, as previous they were not updatable. Fixes part of: #21215
This commit is contained in:
parent
2e9cd20380
commit
543f36b7da
|
@ -20,6 +20,14 @@ format used by the Zulip server that they are interacting with.
|
||||||
|
|
||||||
## Changes in Zulip 6.0
|
## Changes in Zulip 6.0
|
||||||
|
|
||||||
|
**Feature level 146**
|
||||||
|
|
||||||
|
* [`POST /realm/profile_fields`](/api/create-custom-profile-field),
|
||||||
|
[`GET /realm/profile_fields`](/api/get-custom-profile-fields): Added a
|
||||||
|
new parameter `display_in_profile_summary`, which clients use to
|
||||||
|
decide whether to display the field in a small/summary section of the
|
||||||
|
user's profile.
|
||||||
|
|
||||||
**Feature level 145**
|
**Feature level 145**
|
||||||
|
|
||||||
* [`DELETE users/me/subscriptions`](/api/unsubscribe): Normal users can
|
* [`DELETE users/me/subscriptions`](/api/unsubscribe): Normal users can
|
||||||
|
|
|
@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.4.3"
|
||||||
# Changes should be accompanied by documentation explaining what the
|
# Changes should be accompanied by documentation explaining what the
|
||||||
# new level means in templates/zerver/api/changelog.md, as well as
|
# new level means in templates/zerver/api/changelog.md, as well as
|
||||||
# "**Changes**" entries in the endpoint's documentation in `zulip.yaml`.
|
# "**Changes**" entries in the endpoint's documentation in `zulip.yaml`.
|
||||||
API_FEATURE_LEVEL = 145
|
API_FEATURE_LEVEL = 146
|
||||||
|
|
||||||
# Bump the minor PROVISION_VERSION to indicate that folks should provision
|
# Bump the minor PROVISION_VERSION to indicate that folks should provision
|
||||||
# only when going from an old version of the code to a newer version. Bump
|
# only when going from an old version of the code to a newer version. Bump
|
||||||
|
|
|
@ -26,7 +26,9 @@ def notify_realm_custom_profile_fields(realm: Realm) -> None:
|
||||||
|
|
||||||
|
|
||||||
def try_add_realm_default_custom_profile_field(
|
def try_add_realm_default_custom_profile_field(
|
||||||
realm: Realm, field_subtype: str
|
realm: Realm,
|
||||||
|
field_subtype: str,
|
||||||
|
display_in_profile_summary: bool = False,
|
||||||
) -> CustomProfileField:
|
) -> CustomProfileField:
|
||||||
field_data = DEFAULT_EXTERNAL_ACCOUNTS[field_subtype]
|
field_data = DEFAULT_EXTERNAL_ACCOUNTS[field_subtype]
|
||||||
custom_profile_field = CustomProfileField(
|
custom_profile_field = CustomProfileField(
|
||||||
|
@ -35,6 +37,7 @@ def try_add_realm_default_custom_profile_field(
|
||||||
field_type=CustomProfileField.EXTERNAL_ACCOUNT,
|
field_type=CustomProfileField.EXTERNAL_ACCOUNT,
|
||||||
hint=field_data.hint,
|
hint=field_data.hint,
|
||||||
field_data=orjson.dumps(dict(subtype=field_subtype)).decode(),
|
field_data=orjson.dumps(dict(subtype=field_subtype)).decode(),
|
||||||
|
display_in_profile_summary=display_in_profile_summary,
|
||||||
)
|
)
|
||||||
custom_profile_field.save()
|
custom_profile_field.save()
|
||||||
custom_profile_field.order = custom_profile_field.id
|
custom_profile_field.order = custom_profile_field.id
|
||||||
|
@ -49,8 +52,14 @@ def try_add_realm_custom_profile_field(
|
||||||
field_type: int,
|
field_type: int,
|
||||||
hint: str = "",
|
hint: str = "",
|
||||||
field_data: Optional[ProfileFieldData] = None,
|
field_data: Optional[ProfileFieldData] = None,
|
||||||
|
display_in_profile_summary: bool = False,
|
||||||
) -> CustomProfileField:
|
) -> CustomProfileField:
|
||||||
custom_profile_field = CustomProfileField(realm=realm, name=name, field_type=field_type)
|
custom_profile_field = CustomProfileField(
|
||||||
|
realm=realm,
|
||||||
|
name=name,
|
||||||
|
field_type=field_type,
|
||||||
|
display_in_profile_summary=display_in_profile_summary,
|
||||||
|
)
|
||||||
custom_profile_field.hint = hint
|
custom_profile_field.hint = hint
|
||||||
if (
|
if (
|
||||||
custom_profile_field.field_type == CustomProfileField.SELECT
|
custom_profile_field.field_type == CustomProfileField.SELECT
|
||||||
|
@ -95,9 +104,11 @@ def try_update_realm_custom_profile_field(
|
||||||
name: str,
|
name: str,
|
||||||
hint: str = "",
|
hint: str = "",
|
||||||
field_data: Optional[ProfileFieldData] = None,
|
field_data: Optional[ProfileFieldData] = None,
|
||||||
|
display_in_profile_summary: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
field.name = name
|
field.name = name
|
||||||
field.hint = hint
|
field.hint = hint
|
||||||
|
field.display_in_profile_summary = display_in_profile_summary
|
||||||
if (
|
if (
|
||||||
field.field_type == CustomProfileField.SELECT
|
field.field_type == CustomProfileField.SELECT
|
||||||
or field.field_type == CustomProfileField.EXTERNAL_ACCOUNT
|
or field.field_type == CustomProfileField.EXTERNAL_ACCOUNT
|
||||||
|
|
|
@ -171,6 +171,9 @@ custom_profile_field_type = DictType(
|
||||||
("field_data", str),
|
("field_data", str),
|
||||||
("order", int),
|
("order", int),
|
||||||
],
|
],
|
||||||
|
optional_keys=[
|
||||||
|
("display_in_profile_summary", bool),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
custom_profile_fields_event = event_dict_type(
|
custom_profile_fields_event = event_dict_type(
|
||||||
|
|
|
@ -29,11 +29,12 @@ RealmUserValidator = Callable[[int, object, bool], List[int]]
|
||||||
ProfileDataElementValue = Union[str, List[int]]
|
ProfileDataElementValue = Union[str, List[int]]
|
||||||
|
|
||||||
|
|
||||||
class ProfileDataElementBase(TypedDict):
|
class ProfileDataElementBase(TypedDict, total=False):
|
||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
type: int
|
type: int
|
||||||
hint: str
|
hint: str
|
||||||
|
display_in_profile_summary: bool
|
||||||
field_data: str
|
field_data: str
|
||||||
order: int
|
order: int
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 4.0.7 on 2022-09-19 17:28
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("zerver", "0411_alter_muteduser_muted_user_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="customprofilefield",
|
||||||
|
name="display_in_profile_summary",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
|
@ -4540,13 +4540,20 @@ class CustomProfileField(models.Model):
|
||||||
|
|
||||||
HINT_MAX_LENGTH = 80
|
HINT_MAX_LENGTH = 80
|
||||||
NAME_MAX_LENGTH = 40
|
NAME_MAX_LENGTH = 40
|
||||||
|
MAX_DISPLAY_IN_PROFILE_SUMMARY_FIELDS = 2
|
||||||
|
|
||||||
id: int = models.AutoField(auto_created=True, primary_key=True, verbose_name="ID")
|
id: int = models.AutoField(auto_created=True, primary_key=True, verbose_name="ID")
|
||||||
realm: Realm = models.ForeignKey(Realm, on_delete=CASCADE)
|
realm: Realm = models.ForeignKey(Realm, on_delete=CASCADE)
|
||||||
name: str = models.CharField(max_length=NAME_MAX_LENGTH)
|
name: str = models.CharField(max_length=NAME_MAX_LENGTH)
|
||||||
hint: str = models.CharField(max_length=HINT_MAX_LENGTH, default="")
|
hint: str = models.CharField(max_length=HINT_MAX_LENGTH, default="")
|
||||||
|
|
||||||
|
# Sort order for display of custom profile fields.
|
||||||
order: int = models.IntegerField(default=0)
|
order: int = models.IntegerField(default=0)
|
||||||
|
|
||||||
|
# Whether the field should be displayed in smaller summary
|
||||||
|
# sections of a page displaying custom profile fields.
|
||||||
|
display_in_profile_summary: bool = models.BooleanField(default=False)
|
||||||
|
|
||||||
SHORT_TEXT = 1
|
SHORT_TEXT = 1
|
||||||
LONG_TEXT = 2
|
LONG_TEXT = 2
|
||||||
SELECT = 3
|
SELECT = 3
|
||||||
|
@ -4619,7 +4626,7 @@ class CustomProfileField(models.Model):
|
||||||
unique_together = ("realm", "name")
|
unique_together = ("realm", "name")
|
||||||
|
|
||||||
def as_dict(self) -> ProfileDataElementBase:
|
def as_dict(self) -> ProfileDataElementBase:
|
||||||
return {
|
data_as_dict: ProfileDataElementBase = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"type": self.field_type,
|
"type": self.field_type,
|
||||||
|
@ -4627,6 +4634,10 @@ class CustomProfileField(models.Model):
|
||||||
"field_data": self.field_data,
|
"field_data": self.field_data,
|
||||||
"order": self.order,
|
"order": self.order,
|
||||||
}
|
}
|
||||||
|
if self.display_in_profile_summary:
|
||||||
|
data_as_dict["display_in_profile_summary"] = True
|
||||||
|
|
||||||
|
return data_as_dict
|
||||||
|
|
||||||
def is_renderable(self) -> bool:
|
def is_renderable(self) -> bool:
|
||||||
if self.field_type in [CustomProfileField.SHORT_TEXT, CustomProfileField.LONG_TEXT]:
|
if self.field_type in [CustomProfileField.SHORT_TEXT, CustomProfileField.LONG_TEXT]:
|
||||||
|
|
|
@ -1666,6 +1666,7 @@ paths:
|
||||||
"hint": "",
|
"hint": "",
|
||||||
"field_data": '{"0":{"text":"Vim","order":"1"},"1":{"text":"Emacs","order":"2"}}',
|
"field_data": '{"0":{"text":"Vim","order":"1"},"1":{"text":"Emacs","order":"2"}}',
|
||||||
"order": 4,
|
"order": 4,
|
||||||
|
"display_in_profile_summary": true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 5,
|
"id": 5,
|
||||||
|
@ -1682,6 +1683,7 @@ paths:
|
||||||
"hint": "Or your personal blog's URL",
|
"hint": "Or your personal blog's URL",
|
||||||
"field_data": "",
|
"field_data": "",
|
||||||
"order": 6,
|
"order": 6,
|
||||||
|
"display_in_profile_summary": true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 7,
|
"id": 7,
|
||||||
|
@ -8250,6 +8252,7 @@ paths:
|
||||||
"hint": "",
|
"hint": "",
|
||||||
"field_data": '{"0":{"text":"Vim","order":"1"},"1":{"text":"Emacs","order":"2"}}',
|
"field_data": '{"0":{"text":"Vim","order":"1"},"1":{"text":"Emacs","order":"2"}}',
|
||||||
"order": 4,
|
"order": 4,
|
||||||
|
"display_in_profile_summary": true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 5,
|
"id": 5,
|
||||||
|
@ -8266,6 +8269,7 @@ paths:
|
||||||
"hint": "Or your personal blog's URL",
|
"hint": "Or your personal blog's URL",
|
||||||
"field_data": "",
|
"field_data": "",
|
||||||
"order": 6,
|
"order": 6,
|
||||||
|
"display_in_profile_summary": true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 7,
|
"id": 7,
|
||||||
|
@ -8378,6 +8382,25 @@ paths:
|
||||||
"python": {"text": "Python", "order": "1"},
|
"python": {"text": "Python", "order": "1"},
|
||||||
"java": {"text": "Java", "order": "2"},
|
"java": {"text": "Java", "order": "2"},
|
||||||
}
|
}
|
||||||
|
- name: display_in_profile_summary
|
||||||
|
in: query
|
||||||
|
description: |
|
||||||
|
Whether clients should display this profile field in a summary section of a
|
||||||
|
user's profile (or in a more easily accessible "small profile").
|
||||||
|
|
||||||
|
At most 2 profile fields may have this property be true in a given
|
||||||
|
organization. The "Long text" [profile field types][profile-field-types]
|
||||||
|
profile field types cannot be selected to be displayed in profile summaries.
|
||||||
|
|
||||||
|
The "Person picker" profile field is also not supported, but that is likely to
|
||||||
|
be temporary.
|
||||||
|
|
||||||
|
[profile-field-types]: /help/add-custom-profile-fields#profile-field-types
|
||||||
|
|
||||||
|
**Changes**: New in Zulip 6.0 (feature level 146).
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
|
example: true
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Success.
|
description: Success.
|
||||||
|
@ -15418,6 +15441,15 @@ components:
|
||||||
dropdown UI for individual users to select an option.
|
dropdown UI for individual users to select an option.
|
||||||
|
|
||||||
The interface for field type 7 is not yet stabilized.
|
The interface for field type 7 is not yet stabilized.
|
||||||
|
display_in_profile_summary:
|
||||||
|
type: boolean
|
||||||
|
description: |
|
||||||
|
Whether the custom profile field, display or not in the user profile summary.
|
||||||
|
|
||||||
|
Currently it's value not allowed to be `true` of `Long text` and `Person picker`
|
||||||
|
[profile field types](/help/add-custom-profile-fields#profile-field-types).
|
||||||
|
|
||||||
|
**Changes**: New in Zulip 6.0 (feature level 146).
|
||||||
Hotspot:
|
Hotspot:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
|
|
@ -60,11 +60,19 @@ class CreateCustomProfileFieldTest(CustomProfileFieldTestCase):
|
||||||
data["hint"] = "*" * 81
|
data["hint"] = "*" * 81
|
||||||
data["field_type"] = CustomProfileField.SHORT_TEXT
|
data["field_type"] = CustomProfileField.SHORT_TEXT
|
||||||
result = self.client_post("/json/realm/profile_fields", info=data)
|
result = self.client_post("/json/realm/profile_fields", info=data)
|
||||||
msg = "hint is too long (limit: 80 characters)"
|
self.assert_json_error(result, "hint is too long (limit: 80 characters)")
|
||||||
self.assert_json_error(result, msg)
|
|
||||||
|
|
||||||
data["name"] = "Phone"
|
data["name"] = "Phone"
|
||||||
data["hint"] = "Contact number"
|
data["hint"] = "Contact number"
|
||||||
|
data["field_type"] = CustomProfileField.LONG_TEXT
|
||||||
|
data["display_in_profile_summary"] = "true"
|
||||||
|
result = self.client_post("/json/realm/profile_fields", info=data)
|
||||||
|
self.assert_json_error(result, "Field type not supported for display in profile summary.")
|
||||||
|
|
||||||
|
data["field_type"] = CustomProfileField.USER
|
||||||
|
result = self.client_post("/json/realm/profile_fields", info=data)
|
||||||
|
self.assert_json_error(result, "Field type not supported for display in profile summary.")
|
||||||
|
|
||||||
data["field_type"] = CustomProfileField.SHORT_TEXT
|
data["field_type"] = CustomProfileField.SHORT_TEXT
|
||||||
result = self.client_post("/json/realm/profile_fields", info=data)
|
result = self.client_post("/json/realm/profile_fields", info=data)
|
||||||
self.assert_json_success(result)
|
self.assert_json_success(result)
|
||||||
|
@ -75,12 +83,19 @@ class CreateCustomProfileFieldTest(CustomProfileFieldTestCase):
|
||||||
data["name"] = "Name "
|
data["name"] = "Name "
|
||||||
data["hint"] = "Some name"
|
data["hint"] = "Some name"
|
||||||
data["field_type"] = CustomProfileField.SHORT_TEXT
|
data["field_type"] = CustomProfileField.SHORT_TEXT
|
||||||
|
data["display_in_profile_summary"] = "true"
|
||||||
result = self.client_post("/json/realm/profile_fields", info=data)
|
result = self.client_post("/json/realm/profile_fields", info=data)
|
||||||
self.assert_json_success(result)
|
self.assert_json_success(result)
|
||||||
|
|
||||||
field = CustomProfileField.objects.get(name="Name", realm=realm)
|
field = CustomProfileField.objects.get(name="Name", realm=realm)
|
||||||
self.assertEqual(field.id, field.order)
|
self.assertEqual(field.id, field.order)
|
||||||
|
|
||||||
|
result = self.client_post("/json/realm/profile_fields", info=data)
|
||||||
|
self.assert_json_error(
|
||||||
|
result, "Only 2 custom profile fields can be displayed in the profile summary."
|
||||||
|
)
|
||||||
|
|
||||||
|
data["display_in_profile_summary"] = "false"
|
||||||
result = self.client_post("/json/realm/profile_fields", info=data)
|
result = self.client_post("/json/realm/profile_fields", info=data)
|
||||||
self.assert_json_error(result, "A field with that label already exists.")
|
self.assert_json_error(result, "A field with that label already exists.")
|
||||||
|
|
||||||
|
@ -202,7 +217,7 @@ class CreateCustomProfileFieldTest(CustomProfileFieldTestCase):
|
||||||
)
|
)
|
||||||
self.assert_json_success(result)
|
self.assert_json_success(result)
|
||||||
|
|
||||||
# Default external account field data cannot be updated
|
# Default external account field data cannot be updated except "display_in_profile_summary" field
|
||||||
field = CustomProfileField.objects.get(name="Twitter username", realm=realm)
|
field = CustomProfileField.objects.get(name="Twitter username", realm=realm)
|
||||||
result = self.client_patch(
|
result = self.client_patch(
|
||||||
f"/json/realm/profile_fields/{field.id}",
|
f"/json/realm/profile_fields/{field.id}",
|
||||||
|
@ -210,6 +225,18 @@ class CreateCustomProfileFieldTest(CustomProfileFieldTestCase):
|
||||||
)
|
)
|
||||||
self.assert_json_error(result, "Default custom field cannot be updated.")
|
self.assert_json_error(result, "Default custom field cannot be updated.")
|
||||||
|
|
||||||
|
result = self.client_patch(
|
||||||
|
f"/json/realm/profile_fields/{field.id}",
|
||||||
|
info={
|
||||||
|
"name": field.name,
|
||||||
|
"hint": field.hint,
|
||||||
|
"field_type": field_type,
|
||||||
|
"field_data": field_data,
|
||||||
|
"display_in_profile_summary": "true",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assert_json_success(result)
|
||||||
|
|
||||||
result = self.client_delete(f"/json/realm/profile_fields/{field.id}")
|
result = self.client_delete(f"/json/realm/profile_fields/{field.id}")
|
||||||
self.assert_json_success(result)
|
self.assert_json_success(result)
|
||||||
|
|
||||||
|
@ -470,6 +497,18 @@ class UpdateCustomProfileFieldTest(CustomProfileFieldTestCase):
|
||||||
info={
|
info={
|
||||||
"name": "New phone number",
|
"name": "New phone number",
|
||||||
"hint": "New contact number",
|
"hint": "New contact number",
|
||||||
|
"display_in_profile_summary": "invalid value",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
msg = 'Argument "display_in_profile_summary" is not valid JSON.'
|
||||||
|
self.assert_json_error(result, msg)
|
||||||
|
|
||||||
|
result = self.client_patch(
|
||||||
|
f"/json/realm/profile_fields/{field.id}",
|
||||||
|
info={
|
||||||
|
"name": "New phone number",
|
||||||
|
"hint": "New contact number",
|
||||||
|
"display_in_profile_summary": "true",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assert_json_success(result)
|
self.assert_json_success(result)
|
||||||
|
@ -479,14 +518,16 @@ class UpdateCustomProfileFieldTest(CustomProfileFieldTestCase):
|
||||||
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)
|
||||||
|
self.assertEqual(field.display_in_profile_summary, True)
|
||||||
|
|
||||||
result = self.client_patch(
|
result = self.client_patch(
|
||||||
f"/json/realm/profile_fields/{field.id}",
|
f"/json/realm/profile_fields/{field.id}",
|
||||||
info={"name": "Name "},
|
info={"name": "Name ", "display_in_profile_summary": "true"},
|
||||||
)
|
)
|
||||||
self.assert_json_success(result)
|
self.assert_json_success(result)
|
||||||
field.refresh_from_db()
|
field.refresh_from_db()
|
||||||
self.assertEqual(field.name, "Name")
|
self.assertEqual(field.name, "Name")
|
||||||
|
self.assertEqual(field.display_in_profile_summary, True)
|
||||||
|
|
||||||
field = CustomProfileField.objects.get(name="Favorite editor", realm=realm)
|
field = CustomProfileField.objects.get(name="Favorite editor", realm=realm)
|
||||||
result = self.client_patch(
|
result = self.client_patch(
|
||||||
|
@ -516,10 +557,27 @@ class UpdateCustomProfileFieldTest(CustomProfileFieldTestCase):
|
||||||
).decode()
|
).decode()
|
||||||
result = self.client_patch(
|
result = self.client_patch(
|
||||||
f"/json/realm/profile_fields/{field.id}",
|
f"/json/realm/profile_fields/{field.id}",
|
||||||
info={"name": "Favorite editor", "field_data": field_data},
|
info={
|
||||||
|
"name": "Favorite editor",
|
||||||
|
"field_data": field_data,
|
||||||
|
"display_in_profile_summary": "true",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
self.assert_json_success(result)
|
self.assert_json_success(result)
|
||||||
|
|
||||||
|
field = CustomProfileField.objects.get(name="Birthday", realm=realm)
|
||||||
|
result = self.client_patch(
|
||||||
|
f"/json/realm/profile_fields/{field.id}",
|
||||||
|
info={
|
||||||
|
"name": field.name,
|
||||||
|
"hint": field.hint,
|
||||||
|
"display_in_profile_summary": "true",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assert_json_error(
|
||||||
|
result, "Only 2 custom profile fields can be displayed in the profile summary."
|
||||||
|
)
|
||||||
|
|
||||||
def test_update_is_aware_of_uniqueness(self) -> None:
|
def test_update_is_aware_of_uniqueness(self) -> None:
|
||||||
self.login("iago")
|
self.login("iago")
|
||||||
realm = get_realm("zulip")
|
realm = get_realm("zulip")
|
||||||
|
|
|
@ -985,9 +985,12 @@ class NormalActionsTest(BaseAction):
|
||||||
field = realm.customprofilefield_set.get(realm=realm, name="Biography")
|
field = realm.customprofilefield_set.get(realm=realm, name="Biography")
|
||||||
name = field.name
|
name = field.name
|
||||||
hint = "Biography of the user"
|
hint = "Biography of the user"
|
||||||
|
display_in_profile_summary = False
|
||||||
|
|
||||||
events = self.verify_action(
|
events = self.verify_action(
|
||||||
lambda: try_update_realm_custom_profile_field(realm, field, name, hint=hint)
|
lambda: try_update_realm_custom_profile_field(
|
||||||
|
realm, field, name, hint=hint, display_in_profile_summary=display_in_profile_summary
|
||||||
|
)
|
||||||
)
|
)
|
||||||
check_custom_profile_fields("events[0]", events[0])
|
check_custom_profile_fields("events[0]", events[0])
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import List, cast
|
from typing import List, Optional, cast
|
||||||
|
|
||||||
import orjson
|
import orjson
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
@ -23,6 +23,7 @@ from zerver.lib.response import json_success
|
||||||
from zerver.lib.types import ProfileDataElementUpdateDict, ProfileFieldData, Validator
|
from zerver.lib.types import ProfileDataElementUpdateDict, ProfileFieldData, Validator
|
||||||
from zerver.lib.users import validate_user_custom_profile_data
|
from zerver.lib.users import validate_user_custom_profile_data
|
||||||
from zerver.lib.validator import (
|
from zerver.lib.validator import (
|
||||||
|
check_bool,
|
||||||
check_capped_string,
|
check_capped_string,
|
||||||
check_dict,
|
check_dict,
|
||||||
check_dict_only,
|
check_dict_only,
|
||||||
|
@ -70,6 +71,19 @@ def validate_custom_field_data(field_type: int, field_data: ProfileFieldData) ->
|
||||||
raise JsonableError(error.message)
|
raise JsonableError(error.message)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_display_in_profile_summary_field(
|
||||||
|
field_type: int, display_in_profile_summary: bool
|
||||||
|
) -> None:
|
||||||
|
if not display_in_profile_summary:
|
||||||
|
return
|
||||||
|
|
||||||
|
# The LONG_TEXT field type doesn't make sense visually for profile
|
||||||
|
# field summaries. The USER field type will require some further
|
||||||
|
# client support.
|
||||||
|
if field_type == CustomProfileField.LONG_TEXT or field_type == CustomProfileField.USER:
|
||||||
|
raise JsonableError(_("Field type not supported for display in profile summary."))
|
||||||
|
|
||||||
|
|
||||||
def is_default_external_field(field_type: int, field_data: ProfileFieldData) -> bool:
|
def is_default_external_field(field_type: int, field_data: ProfileFieldData) -> bool:
|
||||||
if field_type != CustomProfileField.EXTERNAL_ACCOUNT:
|
if field_type != CustomProfileField.EXTERNAL_ACCOUNT:
|
||||||
return False
|
return False
|
||||||
|
@ -79,7 +93,11 @@ def is_default_external_field(field_type: int, field_data: ProfileFieldData) ->
|
||||||
|
|
||||||
|
|
||||||
def validate_custom_profile_field(
|
def validate_custom_profile_field(
|
||||||
name: str, hint: str, field_type: int, field_data: ProfileFieldData
|
name: str,
|
||||||
|
hint: str,
|
||||||
|
field_type: int,
|
||||||
|
field_data: ProfileFieldData,
|
||||||
|
display_in_profile_summary: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
# Validate field data
|
# Validate field data
|
||||||
validate_custom_field_data(field_type, field_data)
|
validate_custom_field_data(field_type, field_data)
|
||||||
|
@ -94,12 +112,36 @@ def validate_custom_profile_field(
|
||||||
if field_type not in field_types:
|
if field_type not in field_types:
|
||||||
raise JsonableError(_("Invalid field type."))
|
raise JsonableError(_("Invalid field type."))
|
||||||
|
|
||||||
|
validate_display_in_profile_summary_field(field_type, display_in_profile_summary)
|
||||||
|
|
||||||
|
|
||||||
check_profile_field_data: Validator[ProfileFieldData] = check_dict(
|
check_profile_field_data: Validator[ProfileFieldData] = check_dict(
|
||||||
value_validator=check_union([check_dict(value_validator=check_string), check_string])
|
value_validator=check_union([check_dict(value_validator=check_string), check_string])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_only_display_in_profile_summary(
|
||||||
|
requested_name: str,
|
||||||
|
requested_hint: str,
|
||||||
|
requested_field_data: ProfileFieldData,
|
||||||
|
existing_field: CustomProfileField,
|
||||||
|
) -> bool:
|
||||||
|
if (
|
||||||
|
requested_name != existing_field.name
|
||||||
|
or requested_hint != existing_field.hint
|
||||||
|
or requested_field_data != orjson.loads(existing_field.field_data)
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def display_in_profile_summary_limit_reached(profile_field_id: Optional[int] = None) -> bool:
|
||||||
|
query = CustomProfileField.objects.filter(display_in_profile_summary=True)
|
||||||
|
if profile_field_id is not None:
|
||||||
|
query = query.exclude(id=profile_field_id)
|
||||||
|
return query.count() >= CustomProfileField.MAX_DISPLAY_IN_PROFILE_SUMMARY_FIELDS
|
||||||
|
|
||||||
|
|
||||||
@require_realm_admin
|
@require_realm_admin
|
||||||
@has_request_variables
|
@has_request_variables
|
||||||
def create_realm_custom_profile_field(
|
def create_realm_custom_profile_field(
|
||||||
|
@ -109,8 +151,14 @@ def create_realm_custom_profile_field(
|
||||||
hint: str = REQ(default=""),
|
hint: str = REQ(default=""),
|
||||||
field_data: ProfileFieldData = REQ(default={}, json_validator=check_profile_field_data),
|
field_data: ProfileFieldData = REQ(default={}, json_validator=check_profile_field_data),
|
||||||
field_type: int = REQ(json_validator=check_int),
|
field_type: int = REQ(json_validator=check_int),
|
||||||
|
display_in_profile_summary: bool = REQ(default=False, json_validator=check_bool),
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
validate_custom_profile_field(name, hint, field_type, field_data)
|
if display_in_profile_summary and display_in_profile_summary_limit_reached():
|
||||||
|
raise JsonableError(
|
||||||
|
_("Only 2 custom profile fields can be displayed in the profile summary.")
|
||||||
|
)
|
||||||
|
|
||||||
|
validate_custom_profile_field(name, hint, field_type, field_data, display_in_profile_summary)
|
||||||
try:
|
try:
|
||||||
if is_default_external_field(field_type, field_data):
|
if is_default_external_field(field_type, field_data):
|
||||||
field_subtype = field_data["subtype"]
|
field_subtype = field_data["subtype"]
|
||||||
|
@ -118,6 +166,7 @@ def create_realm_custom_profile_field(
|
||||||
field = try_add_realm_default_custom_profile_field(
|
field = try_add_realm_default_custom_profile_field(
|
||||||
realm=user_profile.realm,
|
realm=user_profile.realm,
|
||||||
field_subtype=field_subtype,
|
field_subtype=field_subtype,
|
||||||
|
display_in_profile_summary=display_in_profile_summary,
|
||||||
)
|
)
|
||||||
return json_success(request, data={"id": field.id})
|
return json_success(request, data={"id": field.id})
|
||||||
else:
|
else:
|
||||||
|
@ -127,6 +176,7 @@ def create_realm_custom_profile_field(
|
||||||
field_data=field_data,
|
field_data=field_data,
|
||||||
field_type=field_type,
|
field_type=field_type,
|
||||||
hint=hint,
|
hint=hint,
|
||||||
|
display_in_profile_summary=display_in_profile_summary,
|
||||||
)
|
)
|
||||||
return json_success(request, data={"id": field.id})
|
return json_success(request, data={"id": field.id})
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
|
@ -155,6 +205,7 @@ def update_realm_custom_profile_field(
|
||||||
name: str = REQ(default="", converter=lambda var_name, x: x.strip()),
|
name: str = REQ(default="", converter=lambda var_name, x: x.strip()),
|
||||||
hint: str = REQ(default=""),
|
hint: str = REQ(default=""),
|
||||||
field_data: ProfileFieldData = REQ(default={}, json_validator=check_profile_field_data),
|
field_data: ProfileFieldData = REQ(default={}, json_validator=check_profile_field_data),
|
||||||
|
display_in_profile_summary: bool = REQ(default=False, json_validator=check_bool),
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
realm = user_profile.realm
|
realm = user_profile.realm
|
||||||
try:
|
try:
|
||||||
|
@ -162,13 +213,34 @@ def update_realm_custom_profile_field(
|
||||||
except CustomProfileField.DoesNotExist:
|
except CustomProfileField.DoesNotExist:
|
||||||
raise JsonableError(_("Field id {id} not found.").format(id=field_id))
|
raise JsonableError(_("Field id {id} not found.").format(id=field_id))
|
||||||
|
|
||||||
|
if display_in_profile_summary and display_in_profile_summary_limit_reached(field.id):
|
||||||
|
raise JsonableError(
|
||||||
|
_("Only 2 custom profile fields can be displayed in the profile summary.")
|
||||||
|
)
|
||||||
|
|
||||||
if field.field_type == CustomProfileField.EXTERNAL_ACCOUNT:
|
if field.field_type == CustomProfileField.EXTERNAL_ACCOUNT:
|
||||||
if is_default_external_field(field.field_type, orjson.loads(field.field_data)):
|
# HACK: Allow changing the display_in_profile_summary property
|
||||||
|
# of default external account types, but not any others.
|
||||||
|
#
|
||||||
|
# TODO: Make the name/hint/field_data parameters optional, and
|
||||||
|
# just require that None was passed for all of them for this case.
|
||||||
|
if is_default_external_field(
|
||||||
|
field.field_type, orjson.loads(field.field_data)
|
||||||
|
) and not update_only_display_in_profile_summary(name, hint, field_data, field):
|
||||||
raise JsonableError(_("Default custom field cannot be updated."))
|
raise JsonableError(_("Default custom field cannot be updated."))
|
||||||
|
|
||||||
validate_custom_profile_field(name, hint, field.field_type, field_data)
|
validate_custom_profile_field(
|
||||||
|
name, hint, field.field_type, field_data, display_in_profile_summary
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
try_update_realm_custom_profile_field(realm, field, name, hint=hint, field_data=field_data)
|
try_update_realm_custom_profile_field(
|
||||||
|
realm,
|
||||||
|
field,
|
||||||
|
name,
|
||||||
|
hint=hint,
|
||||||
|
field_data=field_data,
|
||||||
|
display_in_profile_summary=display_in_profile_summary,
|
||||||
|
)
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
raise JsonableError(_("A field with that label already exists."))
|
raise JsonableError(_("A field with that label already exists."))
|
||||||
return json_success(request)
|
return json_success(request)
|
||||||
|
|
Loading…
Reference in New Issue