mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	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:
		
				
					committed by
					
						
						Tim Abbott
					
				
			
			
				
	
			
			
			
						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)
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user