mirror of
				https://github.com/zulip/zulip.git
				synced 2025-10-30 19:43:47 +00:00 
			
		
		
		
	Removed `is_billing_admin` user property as it is no longer used since billing permissions are now determined by `can_manage_billing_group` realm setting.
		
			
				
	
	
		
			1300 lines
		
	
	
		
			49 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			1300 lines
		
	
	
		
			49 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| from collections.abc import Iterable
 | |
| from typing import Any, cast
 | |
| from unittest import mock
 | |
| 
 | |
| import orjson
 | |
| from typing_extensions import override
 | |
| 
 | |
| from zerver.actions.custom_profile_fields import (
 | |
|     do_remove_realm_custom_profile_field,
 | |
|     do_update_user_custom_profile_data_if_changed,
 | |
|     try_add_realm_custom_profile_field,
 | |
|     try_reorder_realm_custom_profile_fields,
 | |
| )
 | |
| from zerver.actions.user_settings import do_change_user_setting
 | |
| from zerver.lib.external_accounts import DEFAULT_EXTERNAL_ACCOUNTS
 | |
| from zerver.lib.markdown import markdown_convert
 | |
| from zerver.lib.test_classes import ZulipTestCase
 | |
| from zerver.lib.types import ProfileDataElementUpdateDict, ProfileDataElementValue
 | |
| from zerver.models import CustomProfileField, CustomProfileFieldValue, UserProfile
 | |
| from zerver.models.custom_profile_fields import custom_profile_fields_for_realm
 | |
| from zerver.models.realms import get_realm
 | |
| 
 | |
| 
 | |
| class CustomProfileFieldTestCase(ZulipTestCase):
 | |
|     @override
 | |
|     def setUp(self) -> None:
 | |
|         super().setUp()
 | |
|         self.realm = get_realm("zulip")
 | |
|         self.original_count = len(custom_profile_fields_for_realm(self.realm.id))
 | |
| 
 | |
|     def custom_field_exists_in_realm(self, field_id: int) -> bool:
 | |
|         fields = custom_profile_fields_for_realm(self.realm.id)
 | |
|         field_ids = [field.id for field in fields]
 | |
|         return field_id in field_ids
 | |
| 
 | |
| 
 | |
| class CreateCustomProfileFieldTest(CustomProfileFieldTestCase):
 | |
|     def test_create(self) -> None:
 | |
|         self.login("iago")
 | |
|         realm = get_realm("zulip")
 | |
|         data: dict[str, Any] = {"name": "Phone", "field_type": "text id"}
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_error(result, "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, "Label cannot be blank.")
 | |
| 
 | |
|         data["name"] = "*" * 41
 | |
|         data["field_type"] = 100
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_error(result, "name is too long (limit: 40 characters)")
 | |
| 
 | |
|         data["name"] = "Phone"
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_error(result, "Invalid field type.")
 | |
| 
 | |
|         data["name"] = "Phone"
 | |
|         data["hint"] = "*" * 81
 | |
|         data["field_type"] = CustomProfileField.SHORT_TEXT
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_error(result, "hint is too long (limit: 80 characters)")
 | |
| 
 | |
|         data["name"] = "Phone"
 | |
|         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
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_success(result)
 | |
| 
 | |
|         field = CustomProfileField.objects.get(name="Phone", realm=realm)
 | |
|         self.assertEqual(field.id, field.order)
 | |
| 
 | |
|         data["name"] = "Name "
 | |
|         data["hint"] = "Some name"
 | |
|         data["field_type"] = CustomProfileField.SHORT_TEXT
 | |
|         data["display_in_profile_summary"] = "true"
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_success(result)
 | |
| 
 | |
|         field = CustomProfileField.objects.get(name="Name", realm=realm)
 | |
|         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)
 | |
|         self.assert_json_error(result, "A field with that label already exists.")
 | |
| 
 | |
|     def test_create_select_field(self) -> None:
 | |
|         self.login("iago")
 | |
|         data: dict[str, str | int] = {}
 | |
|         data["name"] = "Favorite programming language"
 | |
|         data["field_type"] = CustomProfileField.SELECT
 | |
| 
 | |
|         data["field_data"] = "invalid"
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         error_msg = "field_data is not valid JSON"
 | |
|         self.assert_json_error(result, error_msg)
 | |
| 
 | |
|         data["field_data"] = orjson.dumps(
 | |
|             {
 | |
|                 "python": ["1"],
 | |
|                 "java": ["2"],
 | |
|             }
 | |
|         ).decode()
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_error(result, 'field_data["python"]["dict[str,str]"] is not a dict')
 | |
| 
 | |
|         data["field_data"] = orjson.dumps(
 | |
|             {
 | |
|                 "0": {"text": "Python"},
 | |
|                 "1": {"text": "Java"},
 | |
|             }
 | |
|         ).decode()
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_error(result, "order key is missing from field_data")
 | |
| 
 | |
|         data["field_data"] = orjson.dumps(
 | |
|             {
 | |
|                 "0": {"text": "Python", "order": ""},
 | |
|                 "1": {"text": "Java", "order": "2"},
 | |
|             }
 | |
|         ).decode()
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_error(result, 'field_data["order"] cannot be blank.')
 | |
| 
 | |
|         data["field_data"] = orjson.dumps(
 | |
|             {
 | |
|                 "": {"text": "Python", "order": "1"},
 | |
|                 "1": {"text": "Java", "order": "2"},
 | |
|             }
 | |
|         ).decode()
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_error(result, "'value' cannot be blank.")
 | |
| 
 | |
|         data["field_data"] = orjson.dumps(
 | |
|             {
 | |
|                 "0": {"text": "Python", "order": 1},
 | |
|                 "1": {"text": "Java", "order": "2"},
 | |
|             }
 | |
|         ).decode()
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_error(result, 'field_data["0"]["dict[str,str]"]["order"] is not a string')
 | |
| 
 | |
|         data["field_data"] = orjson.dumps({}).decode()
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_error(result, "Field must have at least one choice.")
 | |
| 
 | |
|         data["field_data"] = orjson.dumps(
 | |
|             {
 | |
|                 "0": {"text": "Duplicate", "order": "1"},
 | |
|                 "1": {"text": "Duplicate", "order": "2"},
 | |
|             }
 | |
|         ).decode()
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_error(result, "Field must not have duplicate choices.")
 | |
| 
 | |
|         data["field_data"] = orjson.dumps(
 | |
|             {
 | |
|                 "0": {"text": "Python", "order": "1"},
 | |
|                 "1": {"text": "Java", "order": "2"},
 | |
|             }
 | |
|         ).decode()
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_success(result)
 | |
| 
 | |
|     def test_create_default_external_account_field(self) -> None:
 | |
|         self.login("iago")
 | |
|         realm = get_realm("zulip")
 | |
|         field_type: int = CustomProfileField.EXTERNAL_ACCOUNT
 | |
|         field_data: str = orjson.dumps(
 | |
|             {
 | |
|                 "subtype": "twitter",
 | |
|             }
 | |
|         ).decode()
 | |
|         invalid_field_name: str = "Not required field name"
 | |
|         invalid_field_hint: str = "Not required field hint"
 | |
| 
 | |
|         result = self.client_post(
 | |
|             "/json/realm/profile_fields",
 | |
|             info=dict(
 | |
|                 field_type=field_type,
 | |
|                 field_data=field_data,
 | |
|                 hint=invalid_field_hint,
 | |
|                 name=invalid_field_name,
 | |
|             ),
 | |
|         )
 | |
|         self.assert_json_success(result)
 | |
|         # Silently overwrite name and hint with values set in default fields dict
 | |
|         # for default custom external account fields.
 | |
|         with self.assertRaises(CustomProfileField.DoesNotExist):
 | |
|             field = CustomProfileField.objects.get(name=invalid_field_name, realm=realm)
 | |
|         # The field is created with 'Twitter username' name as per values in default fields dict
 | |
|         field = CustomProfileField.objects.get(name="Twitter username")
 | |
|         self.assertEqual(field.name, DEFAULT_EXTERNAL_ACCOUNTS["twitter"].name)
 | |
|         self.assertEqual(field.hint, DEFAULT_EXTERNAL_ACCOUNTS["twitter"].hint)
 | |
| 
 | |
|         result = self.client_delete(f"/json/realm/profile_fields/{field.id}")
 | |
|         self.assert_json_success(result)
 | |
| 
 | |
|         # Should also work without name or hint and only external field type and subtype data
 | |
|         result = self.client_post(
 | |
|             "/json/realm/profile_fields", info=dict(field_type=field_type, field_data=field_data)
 | |
|         )
 | |
|         self.assert_json_success(result)
 | |
| 
 | |
|         # Default external account field data cannot be updated except "display_in_profile_summary" field
 | |
|         field = CustomProfileField.objects.get(name="Twitter username", realm=realm)
 | |
|         result = self.client_patch(
 | |
|             f"/json/realm/profile_fields/{field.id}",
 | |
|             info={"name": "Twitter", "field_type": CustomProfileField.EXTERNAL_ACCOUNT},
 | |
|         )
 | |
|         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_data": field_data,
 | |
|                 "display_in_profile_summary": "true",
 | |
|             },
 | |
|         )
 | |
|         self.assert_json_success(result)
 | |
| 
 | |
|         result = self.client_delete(f"/json/realm/profile_fields/{field.id}")
 | |
|         self.assert_json_success(result)
 | |
| 
 | |
|     def test_create_external_account_field(self) -> None:
 | |
|         self.login("iago")
 | |
|         realm = get_realm("zulip")
 | |
|         data: dict[str, str | int | dict[str, str]] = {}
 | |
|         data["name"] = "Twitter username"
 | |
|         data["field_type"] = CustomProfileField.EXTERNAL_ACCOUNT
 | |
| 
 | |
|         data["field_data"] = "invalid"
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_error(result, "field_data is not valid JSON")
 | |
| 
 | |
|         data["field_data"] = orjson.dumps({}).decode()
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_error(result, "subtype key is missing from field_data")
 | |
| 
 | |
|         data["field_data"] = orjson.dumps(
 | |
|             {
 | |
|                 "subtype": "",
 | |
|             }
 | |
|         ).decode()
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_error(result, 'field_data["subtype"] cannot be blank.')
 | |
| 
 | |
|         data["field_data"] = orjson.dumps(
 | |
|             {
 | |
|                 "subtype": "123",
 | |
|             }
 | |
|         ).decode()
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_error(result, "Invalid external account type")
 | |
| 
 | |
|         non_default_external_account = "linkedin"
 | |
|         data["field_data"] = orjson.dumps(
 | |
|             {
 | |
|                 "subtype": non_default_external_account,
 | |
|             }
 | |
|         ).decode()
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_error(result, "Invalid external account type")
 | |
| 
 | |
|         data["field_data"] = orjson.dumps(
 | |
|             {
 | |
|                 "subtype": "twitter",
 | |
|             }
 | |
|         ).decode()
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_success(result)
 | |
| 
 | |
|         twitter_field = CustomProfileField.objects.get(name="Twitter username", realm=realm)
 | |
|         self.assertEqual(twitter_field.field_type, CustomProfileField.EXTERNAL_ACCOUNT)
 | |
|         self.assertEqual(twitter_field.name, "Twitter username")
 | |
|         self.assertEqual(orjson.loads(twitter_field.field_data)["subtype"], "twitter")
 | |
| 
 | |
|         data["name"] = "Reddit"
 | |
|         data["field_data"] = orjson.dumps(
 | |
|             {
 | |
|                 "subtype": "custom",
 | |
|             }
 | |
|         ).decode()
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_error(result, "Custom external account must define URL pattern")
 | |
| 
 | |
|         data["field_data"] = orjson.dumps(
 | |
|             {
 | |
|                 "subtype": "custom",
 | |
|                 "url_pattern": 123,
 | |
|             }
 | |
|         ).decode()
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_error(result, 'field_data["url_pattern"]["dict[str,str]"] is not a dict')
 | |
| 
 | |
|         data["field_data"] = orjson.dumps(
 | |
|             {
 | |
|                 "subtype": "custom",
 | |
|                 "url_pattern": "invalid",
 | |
|             }
 | |
|         ).decode()
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_error(result, "URL pattern must contain '%(username)s'.")
 | |
| 
 | |
|         data["field_data"] = orjson.dumps(
 | |
|             {
 | |
|                 "subtype": "custom",
 | |
|                 "url_pattern": "https://www.reddit.com/%(username)s/user/%(username)s",
 | |
|             }
 | |
|         ).decode()
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_error(result, "URL pattern must contain '%(username)s'.")
 | |
| 
 | |
|         data["field_data"] = orjson.dumps(
 | |
|             {
 | |
|                 "subtype": "custom",
 | |
|                 "url_pattern": "reddit.com/%(username)s",
 | |
|             }
 | |
|         ).decode()
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_error(result, 'field_data["url_pattern"] is not a URL')
 | |
| 
 | |
|         data["field_data"] = orjson.dumps(
 | |
|             {
 | |
|                 "subtype": "custom",
 | |
|                 "url_pattern": "https://www.reddit.com/user/%(username)s",
 | |
|             }
 | |
|         ).decode()
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_success(result)
 | |
| 
 | |
|         custom_field = CustomProfileField.objects.get(name="Reddit", realm=realm)
 | |
|         self.assertEqual(custom_field.field_type, CustomProfileField.EXTERNAL_ACCOUNT)
 | |
|         self.assertEqual(custom_field.name, "Reddit")
 | |
|         field_data = orjson.loads(custom_field.field_data)
 | |
|         self.assertEqual(field_data["subtype"], "custom")
 | |
|         self.assertEqual(field_data["url_pattern"], "https://www.reddit.com/user/%(username)s")
 | |
| 
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_error(result, "A field with that label already exists.")
 | |
| 
 | |
|     def test_create_field_of_type_user(self) -> None:
 | |
|         self.login("iago")
 | |
|         data = {
 | |
|             "name": "Your mentor",
 | |
|             "field_type": CustomProfileField.USER,
 | |
|         }
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_success(result)
 | |
| 
 | |
|     def test_create_field_of_type_pronouns(self) -> None:
 | |
|         self.login("iago")
 | |
|         data = {
 | |
|             "name": "Pronouns for you",
 | |
|             "hint": "What pronouns should people use to refer to you?",
 | |
|             "field_type": CustomProfileField.PRONOUNS,
 | |
|         }
 | |
|         result = self.client_post("/json/realm/profile_fields", info=data)
 | |
|         self.assert_json_success(result)
 | |
| 
 | |
|     def test_not_realm_admin(self) -> None:
 | |
|         self.login("hamlet")
 | |
|         result = self.client_post("/json/realm/profile_fields")
 | |
|         self.assert_json_error(result, "Must be an organization administrator")
 | |
|         result = self.client_delete("/json/realm/profile_fields/1")
 | |
|         self.assert_json_error(result, "Must be an organization administrator")
 | |
| 
 | |
| 
 | |
| class DeleteCustomProfileFieldTest(CustomProfileFieldTestCase):
 | |
|     def test_delete(self) -> None:
 | |
|         self.login("iago")
 | |
|         realm = get_realm("zulip")
 | |
|         field = CustomProfileField.objects.get(name="Phone number", realm=realm)
 | |
|         result = self.client_delete("/json/realm/profile_fields/100")
 | |
|         self.assert_json_error(result, "Field id 100 not found.")
 | |
| 
 | |
|         self.assertTrue(self.custom_field_exists_in_realm(field.id))
 | |
|         result = self.client_delete(f"/json/realm/profile_fields/{field.id}")
 | |
|         self.assert_json_success(result)
 | |
|         self.assertFalse(self.custom_field_exists_in_realm(field.id))
 | |
| 
 | |
|     def test_delete_field_value(self) -> None:
 | |
|         iago = self.example_user("iago")
 | |
|         self.login_user(iago)
 | |
|         realm = get_realm("zulip")
 | |
| 
 | |
|         invalid_field_id = 1234
 | |
|         result = self.client_delete(
 | |
|             "/json/users/me/profile_data",
 | |
|             {
 | |
|                 "data": orjson.dumps([invalid_field_id]).decode(),
 | |
|             },
 | |
|         )
 | |
|         self.assert_json_error(result, f"Field id {invalid_field_id} not found.")
 | |
| 
 | |
|         field = CustomProfileField.objects.get(name="Mentor", realm=realm)
 | |
|         data: list[ProfileDataElementUpdateDict] = [
 | |
|             {"id": field.id, "value": [self.example_user("aaron").id]},
 | |
|         ]
 | |
|         do_update_user_custom_profile_data_if_changed(iago, data)
 | |
| 
 | |
|         iago_value = CustomProfileFieldValue.objects.get(user_profile=iago, field=field)
 | |
|         converter = field.FIELD_CONVERTERS[field.field_type]
 | |
|         self.assertEqual([self.example_user("aaron").id], converter(iago_value.value))
 | |
| 
 | |
|         result = self.client_delete(
 | |
|             "/json/users/me/profile_data",
 | |
|             {
 | |
|                 "data": orjson.dumps([field.id]).decode(),
 | |
|             },
 | |
|         )
 | |
|         self.assert_json_success(result)
 | |
| 
 | |
|         # Don't throw an exception here
 | |
|         result = self.client_delete(
 | |
|             "/json/users/me/profile_data",
 | |
|             {
 | |
|                 "data": orjson.dumps([field.id]).decode(),
 | |
|             },
 | |
|         )
 | |
|         self.assert_json_success(result)
 | |
| 
 | |
|     def test_delete_internals(self) -> None:
 | |
|         user_profile = self.example_user("iago")
 | |
|         realm = user_profile.realm
 | |
|         field = CustomProfileField.objects.get(name="Phone number", realm=realm)
 | |
|         data: list[ProfileDataElementUpdateDict] = [
 | |
|             {"id": field.id, "value": "123456"},
 | |
|         ]
 | |
|         do_update_user_custom_profile_data_if_changed(user_profile, data)
 | |
| 
 | |
|         self.assertTrue(self.custom_field_exists_in_realm(field.id))
 | |
|         self.assertEqual(user_profile.customprofilefieldvalue_set.count(), self.original_count)
 | |
| 
 | |
|         do_remove_realm_custom_profile_field(realm, field)
 | |
| 
 | |
|         self.assertFalse(self.custom_field_exists_in_realm(field.id))
 | |
|         self.assertEqual(user_profile.customprofilefieldvalue_set.count(), self.original_count - 1)
 | |
| 
 | |
|     def test_delete_value_with_editable_by_user(self) -> None:
 | |
|         iago = self.example_user("iago")
 | |
|         hamlet = self.example_user("hamlet")
 | |
|         realm = iago.realm
 | |
|         self.login("hamlet")
 | |
| 
 | |
|         biography_custom_field = CustomProfileField.objects.get(name="Biography", realm=realm)
 | |
|         birthday_custom_field = CustomProfileField.objects.get(name="Birthday", realm=realm)
 | |
| 
 | |
|         # Set and assert our initial state.
 | |
|         data = {}
 | |
|         data["editable_by_user"] = "false"
 | |
|         result = self.api_patch(
 | |
|             iago, f"/api/v1/realm/profile_fields/{birthday_custom_field.id}", info=data
 | |
|         )
 | |
|         self.assert_json_success(result)
 | |
| 
 | |
|         birthday_custom_field.refresh_from_db()
 | |
|         self.assertFalse(birthday_custom_field.editable_by_user)
 | |
|         self.assertTrue(biography_custom_field.editable_by_user)
 | |
| 
 | |
|         self.assertTrue(
 | |
|             CustomProfileFieldValue.objects.filter(
 | |
|                 user_profile=iago, field=birthday_custom_field
 | |
|             ).exists()
 | |
|         )
 | |
|         self.assertTrue(
 | |
|             CustomProfileFieldValue.objects.filter(
 | |
|                 user_profile=hamlet, field=birthday_custom_field
 | |
|             ).exists()
 | |
|         )
 | |
|         self.assertTrue(
 | |
|             CustomProfileFieldValue.objects.filter(
 | |
|                 user_profile=hamlet, field=biography_custom_field
 | |
|             ).exists()
 | |
|         )
 | |
| 
 | |
|         # Users can only delete fields where editable_by_user is true.
 | |
|         result = self.client_delete(
 | |
|             "/json/users/me/profile_data",
 | |
|             {"data": orjson.dumps([biography_custom_field.id]).decode()},
 | |
|         )
 | |
|         self.assert_json_success(result)
 | |
|         self.assertFalse(
 | |
|             CustomProfileFieldValue.objects.filter(
 | |
|                 user_profile=hamlet, field=biography_custom_field
 | |
|             ).exists()
 | |
|         )
 | |
| 
 | |
|         result = self.client_delete(
 | |
|             "/json/users/me/profile_data",
 | |
|             {"data": orjson.dumps([birthday_custom_field.id]).decode()},
 | |
|         )
 | |
|         self.assert_json_error(
 | |
|             result,
 | |
|             "You are not allowed to change this field. Contact an administrator to update it.",
 | |
|         )
 | |
|         self.assertTrue(
 | |
|             CustomProfileFieldValue.objects.filter(
 | |
|                 user_profile=hamlet, field=birthday_custom_field
 | |
|             ).exists()
 | |
|         )
 | |
| 
 | |
|         # Admins can always delete field values regardless of editable_by_user.
 | |
|         result = self.api_delete(
 | |
|             iago,
 | |
|             "/api/v1/users/me/profile_data",
 | |
|             {"data": orjson.dumps([birthday_custom_field.id]).decode()},
 | |
|         )
 | |
|         self.assert_json_success(result)
 | |
|         self.assertFalse(
 | |
|             CustomProfileFieldValue.objects.filter(
 | |
|                 user_profile=iago, field=birthday_custom_field
 | |
|             ).exists()
 | |
|         )
 | |
| 
 | |
| 
 | |
| class UpdateCustomProfileFieldTest(CustomProfileFieldTestCase):
 | |
|     def test_update(self) -> None:
 | |
|         self.login("iago")
 | |
|         realm = get_realm("zulip")
 | |
|         result = self.client_patch(
 | |
|             "/json/realm/profile_fields/100",
 | |
|             info={"name": "Phone number"},
 | |
|         )
 | |
|         self.assert_json_error(result, "Field id 100 not found.")
 | |
| 
 | |
|         field = CustomProfileField.objects.get(name="Phone number", realm=realm)
 | |
|         result = self.client_patch(
 | |
|             f"/json/realm/profile_fields/{field.id}",
 | |
|             info={"name": ""},
 | |
|         )
 | |
|         self.assert_json_error(result, "Label cannot be blank.")
 | |
| 
 | |
|         result = self.client_patch(
 | |
|             f"/json/realm/profile_fields/{field.id}",
 | |
|             info={"name": "*" * 41},
 | |
|         )
 | |
|         msg = "name is too long (limit: 40 characters)"
 | |
|         self.assert_json_error(result, msg)
 | |
| 
 | |
|         result = self.client_patch(
 | |
|             f"/json/realm/profile_fields/{field.id}",
 | |
|             info={
 | |
|                 "hint": "*" * 81,
 | |
|             },
 | |
|         )
 | |
|         msg = "hint is too long (limit: 80 characters)"
 | |
|         self.assert_json_error(result, msg)
 | |
| 
 | |
|         result = self.client_patch(
 | |
|             f"/json/realm/profile_fields/{field.id}",
 | |
|             info={
 | |
|                 "required": "invalid value",
 | |
|             },
 | |
|         )
 | |
|         msg = "required is not valid JSON"
 | |
|         self.assert_json_error(result, msg)
 | |
| 
 | |
|         result = self.client_patch(
 | |
|             f"/json/realm/profile_fields/{field.id}",
 | |
|             info={
 | |
|                 "editable_by_user": "invalid value",
 | |
|             },
 | |
|         )
 | |
|         msg = "editable_by_user 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",
 | |
|                 "required": "true",
 | |
|                 "editable_by_user": "false",
 | |
|             },
 | |
|         )
 | |
|         self.assert_json_success(result)
 | |
|         field.refresh_from_db()
 | |
|         self.assertEqual(CustomProfileField.objects.count(), self.original_count)
 | |
|         self.assertEqual(field.name, "New phone number")
 | |
|         self.assertEqual(field.hint, "New contact number")
 | |
|         self.assertEqual(field.field_type, CustomProfileField.SHORT_TEXT)
 | |
|         self.assertEqual(field.display_in_profile_summary, True)
 | |
|         self.assertEqual(field.required, True)
 | |
|         self.assertEqual(field.editable_by_user, False)
 | |
| 
 | |
|         # Not sending required or editable_by_user should not reset their value to default.
 | |
|         result = self.client_patch(
 | |
|             f"/json/realm/profile_fields/{field.id}",
 | |
|             info={
 | |
|                 "hint": "New hint",
 | |
|             },
 | |
|         )
 | |
|         self.assert_json_success(result)
 | |
|         field.refresh_from_db()
 | |
|         self.assertEqual(field.hint, "New hint")
 | |
|         self.assertEqual(field.required, True)
 | |
|         self.assertEqual(field.editable_by_user, False)
 | |
| 
 | |
|         result = self.client_patch(
 | |
|             f"/json/realm/profile_fields/{field.id}",
 | |
|             info={"name": "Name "},
 | |
|         )
 | |
|         self.assert_json_success(result)
 | |
|         field.refresh_from_db()
 | |
|         self.assertEqual(field.name, "Name")
 | |
| 
 | |
|         # Empty string for hint should set it to an empty string
 | |
|         result = self.client_patch(
 | |
|             f"/json/realm/profile_fields/{field.id}",
 | |
|             info={"hint": ""},
 | |
|         )
 | |
|         self.assert_json_success(result)
 | |
|         field.refresh_from_db()
 | |
|         self.assertEqual(field.hint, "")
 | |
| 
 | |
|     def test_update_display_in_profile_summary(self) -> None:
 | |
|         self.login("iago")
 | |
|         realm = get_realm("zulip")
 | |
| 
 | |
|         field = CustomProfileField.objects.get(name="Phone number", realm=realm)
 | |
| 
 | |
|         result = self.client_patch(
 | |
|             f"/json/realm/profile_fields/{field.id}",
 | |
|             info={
 | |
|                 "display_in_profile_summary": "invalid value",
 | |
|             },
 | |
|         )
 | |
|         msg = "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={
 | |
|                 "display_in_profile_summary": "true",
 | |
|             },
 | |
|         )
 | |
|         self.assert_json_success(result)
 | |
|         field.refresh_from_db()
 | |
|         self.assertEqual(field.display_in_profile_summary, True)
 | |
| 
 | |
|         # Not sending display_in_profile_summary should not set it to false.
 | |
|         result = self.client_patch(
 | |
|             f"/json/realm/profile_fields/{field.id}",
 | |
|             info={
 | |
|                 "hint": "New hint",
 | |
|             },
 | |
|         )
 | |
|         field.refresh_from_db()
 | |
|         self.assertEqual(field.hint, "New hint")
 | |
|         self.assertEqual(field.display_in_profile_summary, True)
 | |
|         self.assert_json_success(result)
 | |
| 
 | |
|         # Setting display_in_profile_summary to True for the 2nd field. This
 | |
|         # will be useful in the next test where we will test that maximum 2
 | |
|         # fields can be displayed in the profile summary.
 | |
|         field = CustomProfileField.objects.get(name="Pronouns", realm=realm)
 | |
|         result = self.client_patch(
 | |
|             f"/json/realm/profile_fields/{field.id}",
 | |
|             info={
 | |
|                 "display_in_profile_summary": "true",
 | |
|             },
 | |
|         )
 | |
|         self.assert_json_success(result)
 | |
|         field.refresh_from_db()
 | |
|         self.assertEqual(field.display_in_profile_summary, True)
 | |
| 
 | |
|         field = CustomProfileField.objects.get(name="Birthday", realm=realm)
 | |
|         result = self.client_patch(
 | |
|             f"/json/realm/profile_fields/{field.id}",
 | |
|             info={
 | |
|                 "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_field_data(self) -> None:
 | |
|         self.login("iago")
 | |
|         realm = get_realm("zulip")
 | |
|         field = CustomProfileField.objects.get(name="Favorite editor", realm=realm)
 | |
|         result = self.client_patch(
 | |
|             f"/json/realm/profile_fields/{field.id}",
 | |
|             info={"field_data": "invalid"},
 | |
|         )
 | |
|         self.assert_json_error(result, "field_data is not valid JSON")
 | |
| 
 | |
|         field_data = orjson.dumps(
 | |
|             {
 | |
|                 "0": "Vim",
 | |
|                 "1": {"order": "2", "text": "Emacs"},
 | |
|             }
 | |
|         ).decode()
 | |
|         result = self.client_patch(
 | |
|             f"/json/realm/profile_fields/{field.id}",
 | |
|             info={"field_data": field_data},
 | |
|         )
 | |
|         self.assert_json_error(result, "field_data is not a dict")
 | |
| 
 | |
|         field_data = orjson.dumps(
 | |
|             {
 | |
|                 "0": {"order": "1", "text": "Vim"},
 | |
|                 "1": {"order": "2", "text": "Emacs"},
 | |
|                 "2": {"order": "3", "text": "Notepad"},
 | |
|             }
 | |
|         ).decode()
 | |
|         result = self.client_patch(
 | |
|             f"/json/realm/profile_fields/{field.id}",
 | |
|             info={
 | |
|                 "field_data": field_data,
 | |
|             },
 | |
|         )
 | |
|         self.assert_json_success(result)
 | |
| 
 | |
|         # We need this test to add coverage for the case where field_data
 | |
|         # is loaded from existing field info for validation.
 | |
|         result = self.client_patch(
 | |
|             f"/json/realm/profile_fields/{field.id}",
 | |
|             info={
 | |
|                 "hint": "new hint",
 | |
|             },
 | |
|         )
 | |
|         self.assert_json_success(result)
 | |
|         field.refresh_from_db()
 | |
|         self.assertEqual(field.hint, "new hint")
 | |
| 
 | |
|     def test_update_is_aware_of_uniqueness(self) -> None:
 | |
|         self.login("iago")
 | |
|         realm = get_realm("zulip")
 | |
|         field_1 = try_add_realm_custom_profile_field(realm, "Phone", CustomProfileField.SHORT_TEXT)
 | |
| 
 | |
|         field_2 = try_add_realm_custom_profile_field(
 | |
|             realm, "Phone 1", CustomProfileField.SHORT_TEXT
 | |
|         )
 | |
| 
 | |
|         self.assertTrue(self.custom_field_exists_in_realm(field_1.id))
 | |
|         self.assertTrue(self.custom_field_exists_in_realm(field_2.id))
 | |
|         result = self.client_patch(
 | |
|             f"/json/realm/profile_fields/{field_2.id}",
 | |
|             info={"name": "Phone"},
 | |
|         )
 | |
|         self.assert_json_error(result, "A field with that label already exists.")
 | |
| 
 | |
|     def assert_error_update_invalid_value(
 | |
|         self, field_name: str, new_value: object, error_msg: str
 | |
|     ) -> None:
 | |
|         self.login("iago")
 | |
|         realm = get_realm("zulip")
 | |
|         field = CustomProfileField.objects.get(name=field_name, realm=realm)
 | |
| 
 | |
|         # Update value of field
 | |
|         result = self.client_patch(
 | |
|             "/json/users/me/profile_data",
 | |
|             {"data": orjson.dumps([{"id": field.id, "value": new_value}]).decode()},
 | |
|         )
 | |
|         self.assert_json_error(result, error_msg)
 | |
| 
 | |
|     def test_update_invalid_field(self) -> None:
 | |
|         self.login("iago")
 | |
|         data = [{"id": 1234, "value": "12"}]
 | |
|         result = self.client_patch(
 | |
|             "/json/users/me/profile_data",
 | |
|             {
 | |
|                 "data": orjson.dumps(data).decode(),
 | |
|             },
 | |
|         )
 | |
|         self.assert_json_error(result, "Field id 1234 not found.")
 | |
| 
 | |
|     def test_update_invalid_short_text(self) -> None:
 | |
|         field_name = "Phone number"
 | |
|         self.assert_error_update_invalid_value(
 | |
|             field_name, "t" * 201, f"{field_name} is too long (limit: 50 characters)"
 | |
|         )
 | |
| 
 | |
|     def test_update_invalid_date(self) -> None:
 | |
|         field_name = "Birthday"
 | |
|         self.assert_error_update_invalid_value(field_name, "a-b-c", f"{field_name} is not a date")
 | |
|         self.assert_error_update_invalid_value(
 | |
|             field_name, "1909-3-5", f"{field_name} is not a date"
 | |
|         )
 | |
|         self.assert_error_update_invalid_value(field_name, [123], f"{field_name} is not a string")
 | |
| 
 | |
|     def test_update_invalid_url(self) -> None:
 | |
|         field_name = "Favorite website"
 | |
|         self.assert_error_update_invalid_value(field_name, "not URL", f"{field_name} is not a URL")
 | |
| 
 | |
|     def test_update_invalid_user_field(self) -> None:
 | |
|         field_name = "Mentor"
 | |
|         invalid_user_id = 1000
 | |
|         self.assert_error_update_invalid_value(
 | |
|             field_name, [invalid_user_id], f"Invalid user IDs: {invalid_user_id}"
 | |
|         )
 | |
| 
 | |
|     def test_update_profile_data_successfully(self) -> None:
 | |
|         self.login("iago")
 | |
|         realm = get_realm("zulip")
 | |
|         fields: list[tuple[str, str | list[int]]] = [
 | |
|             ("Phone number", "*short* text data"),
 | |
|             ("Biography", "~~short~~ **long** text data"),
 | |
|             ("Favorite food", "long short text data"),
 | |
|             ("Favorite editor", "0"),
 | |
|             ("Birthday", "1909-03-05"),
 | |
|             ("Favorite website", "https://zulip.com"),
 | |
|             ("Mentor", [self.example_user("cordelia").id]),
 | |
|             ("GitHub username", "zulip-mobile"),
 | |
|             ("Pronouns", "he/him"),
 | |
|         ]
 | |
| 
 | |
|         data: list[ProfileDataElementUpdateDict] = []
 | |
|         expected_value: dict[int, ProfileDataElementValue] = {}
 | |
|         expected_rendered_value: dict[int, str | None] = {}
 | |
|         for name, value in fields:
 | |
|             field = CustomProfileField.objects.get(name=name, realm=realm)
 | |
|             data.append(
 | |
|                 {
 | |
|                     "id": field.id,
 | |
|                     "value": value,
 | |
|                 }
 | |
|             )
 | |
|             expected_value[field.id] = value
 | |
|             expected_rendered_value[field.id] = (
 | |
|                 markdown_convert(value).rendered_content
 | |
|                 if field.is_renderable() and isinstance(value, str)
 | |
|                 else None
 | |
|             )
 | |
| 
 | |
|         # Update value of field
 | |
|         result = self.client_patch(
 | |
|             "/json/users/me/profile_data",
 | |
|             {"data": orjson.dumps([{"id": f["id"], "value": f["value"]} for f in data]).decode()},
 | |
|         )
 | |
|         self.assert_json_success(result)
 | |
| 
 | |
|         iago = self.example_user("iago")
 | |
| 
 | |
|         for field_dict in iago.profile_data():
 | |
|             self.assertEqual(field_dict["value"], expected_value[field_dict["id"]])
 | |
|             self.assertEqual(
 | |
|                 field_dict["rendered_value"], expected_rendered_value[field_dict["id"]]
 | |
|             )
 | |
|             for k in ["id", "type", "name", "field_data"]:
 | |
|                 self.assertIn(k, field_dict)
 | |
| 
 | |
|         # Update value of one field.
 | |
|         field = CustomProfileField.objects.get(name="Biography", realm=realm)
 | |
|         data = [
 | |
|             {
 | |
|                 "id": field.id,
 | |
|                 "value": "foobar",
 | |
|             }
 | |
|         ]
 | |
| 
 | |
|         result = self.client_patch(
 | |
|             "/json/users/me/profile_data", {"data": orjson.dumps(data).decode()}
 | |
|         )
 | |
|         self.assert_json_success(result)
 | |
|         for field_dict in iago.profile_data():
 | |
|             if field_dict["id"] == field.id:
 | |
|                 self.assertEqual(field_dict["value"], "foobar")
 | |
| 
 | |
|     def test_update_invalid_select_field(self) -> None:
 | |
|         field_name = "Favorite editor"
 | |
|         self.assert_error_update_invalid_value(
 | |
|             field_name, "foobar", f"'foobar' is not a valid choice for '{field_name}'."
 | |
|         )
 | |
| 
 | |
|     def test_update_select_field_successfully(self) -> None:
 | |
|         self.login("iago")
 | |
|         realm = get_realm("zulip")
 | |
|         field = CustomProfileField.objects.get(name="Favorite editor", realm=realm)
 | |
|         data = [
 | |
|             {
 | |
|                 "id": field.id,
 | |
|                 "value": "1",
 | |
|             }
 | |
|         ]
 | |
| 
 | |
|         result = self.client_patch(
 | |
|             "/json/users/me/profile_data", {"data": orjson.dumps(data).decode()}
 | |
|         )
 | |
|         self.assert_json_success(result)
 | |
| 
 | |
|     def test_null_value_and_rendered_value(self) -> None:
 | |
|         self.login("iago")
 | |
|         realm = get_realm("zulip")
 | |
| 
 | |
|         quote = try_add_realm_custom_profile_field(
 | |
|             realm=realm,
 | |
|             name="Quote",
 | |
|             hint="Saying or phrase which you known for.",
 | |
|             field_type=CustomProfileField.SHORT_TEXT,
 | |
|         )
 | |
| 
 | |
|         iago = self.example_user("iago")
 | |
|         iago_profile_quote = iago.profile_data()[-1]
 | |
|         value = iago_profile_quote["value"]
 | |
|         rendered_value = iago_profile_quote["rendered_value"]
 | |
|         self.assertIsNone(value)
 | |
|         self.assertIsNone(rendered_value)
 | |
| 
 | |
|         update_dict: ProfileDataElementUpdateDict = {
 | |
|             "id": quote.id,
 | |
|             "value": "***beware*** of jealousy...",
 | |
|         }
 | |
|         do_update_user_custom_profile_data_if_changed(iago, [update_dict])
 | |
| 
 | |
|         iago_profile_quote = self.example_user("iago").profile_data()[-1]
 | |
|         value = iago_profile_quote["value"]
 | |
|         rendered_value = iago_profile_quote["rendered_value"]
 | |
|         self.assertIsNotNone(value)
 | |
|         self.assertIsNotNone(rendered_value)
 | |
|         self.assertEqual("<p><strong><em>beware</em></strong> of jealousy...</p>", rendered_value)
 | |
| 
 | |
|     def test_do_update_value_not_changed(self) -> None:
 | |
|         iago = self.example_user("iago")
 | |
|         self.login_user(iago)
 | |
|         realm = get_realm("zulip")
 | |
| 
 | |
|         # Set field value:
 | |
|         field = CustomProfileField.objects.get(name="Mentor", realm=realm)
 | |
|         data: list[ProfileDataElementUpdateDict] = [
 | |
|             {"id": field.id, "value": [self.example_user("aaron").id]},
 | |
|         ]
 | |
|         do_update_user_custom_profile_data_if_changed(iago, data)
 | |
| 
 | |
|         with mock.patch(
 | |
|             "zerver.actions.custom_profile_fields.notify_user_update_custom_profile_data"
 | |
|         ) as mock_notify:
 | |
|             # Attempting to "update" the field value, when it wouldn't actually change,
 | |
|             # shouldn't trigger notify.
 | |
|             do_update_user_custom_profile_data_if_changed(iago, data)
 | |
|             mock_notify.assert_not_called()
 | |
| 
 | |
|     def test_removing_option_from_select_field(self) -> None:
 | |
|         self.login("iago")
 | |
|         realm = get_realm("zulip")
 | |
|         field = CustomProfileField.objects.get(name="Favorite editor", realm=realm)
 | |
|         self.assertTrue(
 | |
|             CustomProfileFieldValue.objects.filter(field_id=field.id, value="0").exists()
 | |
|         )
 | |
|         self.assertTrue(
 | |
|             CustomProfileFieldValue.objects.filter(field_id=field.id, value="1").exists()
 | |
|         )
 | |
| 
 | |
|         new_options = {"1": {"text": "Emacs", "order": "1"}}
 | |
|         result = self.client_patch(
 | |
|             f"/json/realm/profile_fields/{field.id}",
 | |
|             info={"name": "Favorite editor", "field_data": orjson.dumps(new_options).decode()},
 | |
|         )
 | |
|         self.assert_json_success(result)
 | |
|         self.assertFalse(
 | |
|             CustomProfileFieldValue.objects.filter(field_id=field.id, value="0").exists()
 | |
|         )
 | |
|         self.assertTrue(
 | |
|             CustomProfileFieldValue.objects.filter(field_id=field.id, value="1").exists()
 | |
|         )
 | |
| 
 | |
|     def test_default_external_account_type_field(self) -> None:
 | |
|         self.login("iago")
 | |
|         realm = get_realm("zulip")
 | |
|         field_data = orjson.dumps(
 | |
|             {
 | |
|                 "subtype": "twitter",
 | |
|             }
 | |
|         ).decode()
 | |
| 
 | |
|         field = CustomProfileField.objects.get(name="GitHub username", realm=realm)
 | |
|         # Attempting to change subtype here.
 | |
|         result = self.client_patch(
 | |
|             f"/json/realm/profile_fields/{field.id}",
 | |
|             info={
 | |
|                 "name": "GitHub username",
 | |
|                 "field_type": CustomProfileField.EXTERNAL_ACCOUNT,
 | |
|                 "field_data": field_data,
 | |
|             },
 | |
|         )
 | |
|         self.assert_json_error(result, "Default custom field cannot be updated.")
 | |
| 
 | |
|         field_data = orjson.dumps(
 | |
|             {
 | |
|                 "subtype": "github",
 | |
|             }
 | |
|         ).decode()
 | |
| 
 | |
|         # Attempting to change name here.
 | |
|         result = self.client_patch(
 | |
|             f"/json/realm/profile_fields/{field.id}",
 | |
|             info={
 | |
|                 "name": "GitHub",
 | |
|                 "field_type": CustomProfileField.EXTERNAL_ACCOUNT,
 | |
|                 "field_data": field_data,
 | |
|             },
 | |
|         )
 | |
|         self.assert_json_error(result, "Default custom field cannot be updated.")
 | |
| 
 | |
|         field_data = orjson.dumps(
 | |
|             {
 | |
|                 "subtype": "github",
 | |
|                 "url_pattern": "invalid",
 | |
|             }
 | |
|         ).decode()
 | |
| 
 | |
|         # Verify cannot change URL pattern
 | |
|         result = self.client_patch(
 | |
|             f"/json/realm/profile_fields/{field.id}",
 | |
|             info={
 | |
|                 "name": "GitHub username",
 | |
|                 "field_type": CustomProfileField.EXTERNAL_ACCOUNT,
 | |
|                 "field_data": field_data,
 | |
|             },
 | |
|         )
 | |
|         self.assert_json_error(result, "Default custom field cannot be updated.")
 | |
| 
 | |
|     def assert_profile_field_value(
 | |
|         self, user: UserProfile, field_id: int, field_value: str | None
 | |
|     ) -> None:
 | |
|         for field_dict in user.profile_data():
 | |
|             if field_dict["id"] == field_id:
 | |
|                 self.assertEqual(field_dict["value"], field_value)
 | |
| 
 | |
|     def test_update_with_editable_by_user(self) -> None:
 | |
|         iago = self.example_user("iago")
 | |
|         aaron = self.example_user("aaron")
 | |
|         self.login("aaron")
 | |
| 
 | |
|         # Create field with editable_by_user = false
 | |
|         realm_profile_field_data: dict[str, Any] = {}
 | |
|         realm_profile_field_data["name"] = "Dummy field"
 | |
|         realm_profile_field_data["field_type"] = CustomProfileField.SHORT_TEXT
 | |
|         realm_profile_field_data["editable_by_user"] = "false"
 | |
|         result = self.api_post(iago, "/api/v1/realm/profile_fields", info=realm_profile_field_data)
 | |
|         result_json = self.assert_json_success(result)
 | |
|         restricted_field_id = result_json["id"]
 | |
| 
 | |
|         field_data = [
 | |
|             {
 | |
|                 "id": restricted_field_id,
 | |
|                 "value": "test",
 | |
|             }
 | |
|         ]
 | |
| 
 | |
|         # Admins can always change their own fields
 | |
|         self.assert_profile_field_value(iago, restricted_field_id, None)
 | |
|         result = self.api_patch(
 | |
|             iago, "/api/v1/users/me/profile_data", {"data": orjson.dumps(field_data).decode()}
 | |
|         )
 | |
|         self.assert_json_success(result)
 | |
|         self.assert_profile_field_value(iago, restricted_field_id, "test")
 | |
| 
 | |
|         # Admins can always change fields of others
 | |
|         self.assert_profile_field_value(aaron, restricted_field_id, None)
 | |
|         result = self.api_patch(
 | |
|             iago, f"/api/v1/users/{aaron.id}", {"profile_data": orjson.dumps(field_data).decode()}
 | |
|         )
 | |
|         self.assert_json_success(result)
 | |
|         self.assert_profile_field_value(aaron, restricted_field_id, "test")
 | |
| 
 | |
|         # Users cannot update field value when editable_by_user is false.
 | |
|         self.assert_profile_field_value(aaron, restricted_field_id, "test")
 | |
|         result = self.client_patch(
 | |
|             "/json/users/me/profile_data", {"data": orjson.dumps(field_data).decode()}
 | |
|         )
 | |
|         self.assert_json_error(
 | |
|             result,
 | |
|             "You are not allowed to change this field. Contact an administrator to update it.",
 | |
|         )
 | |
|         self.assert_profile_field_value(aaron, restricted_field_id, "test")
 | |
| 
 | |
|         # Change editable_by_user to true.
 | |
|         data = {}
 | |
|         data["editable_by_user"] = "true"
 | |
|         result = self.api_patch(
 | |
|             iago, f"/api/v1/realm/profile_fields/{restricted_field_id}", info=data
 | |
|         )
 | |
|         self.assert_json_success(result)
 | |
| 
 | |
|         # Users can update field value when editable_by_user is true
 | |
|         self.assert_profile_field_value(aaron, restricted_field_id, "test")
 | |
|         field_data[0]["value"] = "test2"
 | |
|         result = self.client_patch(
 | |
|             "/json/users/me/profile_data", {"data": orjson.dumps(field_data).decode()}
 | |
|         )
 | |
|         self.assert_json_success(result)
 | |
|         self.assert_profile_field_value(aaron, restricted_field_id, "test2")
 | |
| 
 | |
| 
 | |
| class ListCustomProfileFieldTest(CustomProfileFieldTestCase):
 | |
|     def test_list(self) -> None:
 | |
|         self.login("iago")
 | |
|         result = self.client_get("/json/realm/profile_fields")
 | |
|         content = self.assert_json_success(result)
 | |
|         self.assertEqual(200, result.status_code)
 | |
|         self.assert_length(content["custom_fields"], self.original_count)
 | |
| 
 | |
|     def test_list_order(self) -> None:
 | |
|         self.login("iago")
 | |
|         realm = get_realm("zulip")
 | |
|         order = (
 | |
|             CustomProfileField.objects.filter(realm=realm)
 | |
|             .order_by("-order")
 | |
|             .values_list("order", flat=True)
 | |
|         )
 | |
|         # Until https://github.com/typeddjango/django-stubs/issues/444 gets resolved,
 | |
|         # we need the cast here to ensure the value list is correctly typed.
 | |
|         assert all(isinstance(item, int) for item in order)
 | |
|         try_reorder_realm_custom_profile_fields(realm, cast(Iterable[int], order))
 | |
|         result = self.client_get("/json/realm/profile_fields")
 | |
|         content = self.assert_json_success(result)
 | |
|         self.assertListEqual(
 | |
|             content["custom_fields"], sorted(content["custom_fields"], key=lambda x: -x["id"])
 | |
|         )
 | |
| 
 | |
|     def test_get_custom_profile_fields_from_api(self) -> None:
 | |
|         iago = self.example_user("iago")
 | |
|         test_bot = self.create_test_bot("foo-bot", iago)
 | |
|         self.login_user(iago)
 | |
| 
 | |
|         with self.assert_database_query_count(5):
 | |
|             response = self.client_get(
 | |
|                 "/json/users", {"client_gravatar": "false", "include_custom_profile_fields": "true"}
 | |
|             )
 | |
| 
 | |
|         raw_users_data = self.assert_json_success(response)["members"]
 | |
| 
 | |
|         iago_raw_data = None
 | |
|         test_bot_raw_data = None
 | |
| 
 | |
|         for user_dict in raw_users_data:
 | |
|             if user_dict["user_id"] == iago.id:
 | |
|                 iago_raw_data = user_dict
 | |
|                 continue
 | |
|             if user_dict["user_id"] == test_bot.id:
 | |
|                 test_bot_raw_data = user_dict
 | |
|                 continue
 | |
| 
 | |
|         if (not iago_raw_data) or (not test_bot_raw_data):
 | |
|             raise AssertionError("Could not find required data from the response.")
 | |
| 
 | |
|         expected_keys_for_iago = {
 | |
|             "delivery_email",
 | |
|             "email",
 | |
|             "user_id",
 | |
|             "avatar_url",
 | |
|             "avatar_version",
 | |
|             "is_admin",
 | |
|             "is_guest",
 | |
|             "is_bot",
 | |
|             "is_owner",
 | |
|             "role",
 | |
|             "full_name",
 | |
|             "timezone",
 | |
|             "is_active",
 | |
|             "date_joined",
 | |
|             "profile_data",
 | |
|         }
 | |
|         self.assertEqual(set(iago_raw_data.keys()), expected_keys_for_iago)
 | |
|         self.assertNotEqual(iago_raw_data["profile_data"], {})
 | |
| 
 | |
|         expected_keys_for_test_bot = {
 | |
|             "delivery_email",
 | |
|             "email",
 | |
|             "user_id",
 | |
|             "avatar_url",
 | |
|             "avatar_version",
 | |
|             "is_admin",
 | |
|             "is_guest",
 | |
|             "is_bot",
 | |
|             "is_owner",
 | |
|             "role",
 | |
|             "full_name",
 | |
|             "timezone",
 | |
|             "is_active",
 | |
|             "date_joined",
 | |
|             "bot_type",
 | |
|             "bot_owner_id",
 | |
|         }
 | |
|         self.assertEqual(set(test_bot_raw_data.keys()), expected_keys_for_test_bot)
 | |
|         self.assertEqual(test_bot_raw_data["bot_type"], 1)
 | |
|         self.assertEqual(test_bot_raw_data["bot_owner_id"], iago_raw_data["user_id"])
 | |
| 
 | |
|         response = self.client_get("/json/users", {"client_gravatar": "false"})
 | |
|         raw_users_data = self.assert_json_success(response)["members"]
 | |
|         for user_dict in raw_users_data:
 | |
|             with self.assertRaises(KeyError):
 | |
|                 user_dict["profile_data"]
 | |
| 
 | |
|     def test_get_custom_profile_fields_from_api_for_single_user(self) -> None:
 | |
|         self.login("iago")
 | |
|         do_change_user_setting(
 | |
|             self.example_user("iago"),
 | |
|             "email_address_visibility",
 | |
|             UserProfile.EMAIL_ADDRESS_VISIBILITY_ADMINS,
 | |
|             acting_user=None,
 | |
|         )
 | |
|         expected_keys = {
 | |
|             "result",
 | |
|             "msg",
 | |
|             "max_message_id",
 | |
|             "user_id",
 | |
|             "avatar_url",
 | |
|             "full_name",
 | |
|             "email",
 | |
|             "is_bot",
 | |
|             "is_admin",
 | |
|             "is_owner",
 | |
|             "role",
 | |
|             "profile_data",
 | |
|             "avatar_version",
 | |
|             "timezone",
 | |
|             "delivery_email",
 | |
|             "is_active",
 | |
|             "is_guest",
 | |
|             "date_joined",
 | |
|         }
 | |
| 
 | |
|         url = "/json/users/me"
 | |
|         response = self.client_get(url)
 | |
|         raw_user_data = self.assert_json_success(response)
 | |
|         self.assertEqual(set(raw_user_data.keys()), expected_keys)
 | |
| 
 | |
| 
 | |
| class ReorderCustomProfileFieldTest(CustomProfileFieldTestCase):
 | |
|     def test_reorder(self) -> None:
 | |
|         self.login("iago")
 | |
|         realm = get_realm("zulip")
 | |
|         order = list(
 | |
|             CustomProfileField.objects.filter(realm=realm)
 | |
|             .order_by("-order")
 | |
|             .values_list("order", flat=True)
 | |
|         )
 | |
|         result = self.client_patch(
 | |
|             "/json/realm/profile_fields", info={"order": orjson.dumps(order).decode()}
 | |
|         )
 | |
|         self.assert_json_success(result)
 | |
|         fields = CustomProfileField.objects.filter(realm=realm).order_by("order")
 | |
|         for field in fields:
 | |
|             self.assertEqual(field.id, order[field.order])
 | |
| 
 | |
|     def test_reorder_duplicates(self) -> None:
 | |
|         self.login("iago")
 | |
|         realm = get_realm("zulip")
 | |
|         order = list(
 | |
|             CustomProfileField.objects.filter(realm=realm)
 | |
|             .order_by("-order")
 | |
|             .values_list("order", flat=True)
 | |
|         )
 | |
|         order.append(4)
 | |
|         result = self.client_patch(
 | |
|             "/json/realm/profile_fields", info={"order": orjson.dumps(order).decode()}
 | |
|         )
 | |
|         self.assert_json_success(result)
 | |
|         fields = CustomProfileField.objects.filter(realm=realm).order_by("order")
 | |
|         for field in fields:
 | |
|             self.assertEqual(field.id, order[field.order])
 | |
| 
 | |
|     def test_reorder_unauthorized(self) -> None:
 | |
|         self.login("hamlet")
 | |
|         realm = get_realm("zulip")
 | |
|         order = list(
 | |
|             CustomProfileField.objects.filter(realm=realm)
 | |
|             .order_by("-order")
 | |
|             .values_list("order", flat=True)
 | |
|         )
 | |
|         result = self.client_patch(
 | |
|             "/json/realm/profile_fields", info={"order": orjson.dumps(order).decode()}
 | |
|         )
 | |
|         self.assert_json_error(result, "Must be an organization administrator")
 | |
| 
 | |
|     def test_reorder_invalid(self) -> None:
 | |
|         self.login("iago")
 | |
|         order = [100, 200, 300]
 | |
|         result = self.client_patch(
 | |
|             "/json/realm/profile_fields", info={"order": orjson.dumps(order).decode()}
 | |
|         )
 | |
|         self.assert_json_error(result, "Invalid order mapping.")
 | |
|         order = [1, 2]
 | |
|         result = self.client_patch(
 | |
|             "/json/realm/profile_fields", info={"order": orjson.dumps(order).decode()}
 | |
|         )
 | |
|         self.assert_json_error(result, "Invalid order mapping.")
 |