mirror of
https://github.com/zulip/zulip.git
synced 2025-10-24 00:23:49 +00:00
This new property allows organization administrators to specify whether users can modify the custom profile field value on their own account. This property is configurable for individual fields. By default, existing and newly created fields have this property set to true, that is, they allow users to edit the value of the fields. Fixes part of #22883. Co-Authored-By: Ujjawal Modi <umodi2003@gmail.com>
205 lines
6.8 KiB
Python
205 lines
6.8 KiB
Python
from collections.abc import Callable
|
|
from typing import Any
|
|
|
|
import orjson
|
|
from django.core.exceptions import ValidationError
|
|
from django.db import models
|
|
from django.db.models import CASCADE, QuerySet
|
|
from django.utils.translation import gettext as _
|
|
from django.utils.translation import gettext_lazy
|
|
from django_stubs_ext import StrPromise
|
|
from typing_extensions import override
|
|
|
|
from zerver.lib.types import (
|
|
ExtendedFieldElement,
|
|
ExtendedValidator,
|
|
FieldElement,
|
|
ProfileDataElementBase,
|
|
ProfileDataElementValue,
|
|
RealmUserValidator,
|
|
UserFieldElement,
|
|
Validator,
|
|
)
|
|
from zerver.lib.validator import (
|
|
check_date,
|
|
check_int,
|
|
check_list,
|
|
check_long_string,
|
|
check_short_string,
|
|
check_url,
|
|
validate_select_field,
|
|
)
|
|
from zerver.models.realms import Realm
|
|
from zerver.models.users import UserProfile
|
|
|
|
|
|
def check_valid_user_ids(realm_id: int, val: object, allow_deactivated: bool = False) -> list[int]:
|
|
user_ids = check_list(check_int)("User IDs", val)
|
|
user_profiles = UserProfile.objects.filter(realm_id=realm_id, id__in=user_ids)
|
|
|
|
valid_users_ids = set(user_profiles.values_list("id", flat=True))
|
|
invalid_users_ids = [invalid_id for invalid_id in user_ids if invalid_id not in valid_users_ids]
|
|
|
|
if invalid_users_ids:
|
|
raise ValidationError(
|
|
_("Invalid user IDs: {invalid_ids}").format(
|
|
invalid_ids=", ".join(map(str, invalid_users_ids))
|
|
)
|
|
)
|
|
|
|
for user in user_profiles:
|
|
if not allow_deactivated and not user.is_active:
|
|
raise ValidationError(
|
|
_("User with ID {user_id} is deactivated").format(user_id=user.id)
|
|
)
|
|
|
|
if user.is_bot:
|
|
raise ValidationError(_("User with ID {user_id} is a bot").format(user_id=user.id))
|
|
|
|
return user_ids
|
|
|
|
|
|
class CustomProfileField(models.Model):
|
|
"""Defines a form field for the per-realm custom profile fields feature.
|
|
|
|
See CustomProfileFieldValue for an individual user's values for one of
|
|
these fields.
|
|
"""
|
|
|
|
HINT_MAX_LENGTH = 80
|
|
NAME_MAX_LENGTH = 40
|
|
MAX_DISPLAY_IN_PROFILE_SUMMARY_FIELDS = 2
|
|
|
|
realm = models.ForeignKey(Realm, on_delete=CASCADE)
|
|
name = models.CharField(max_length=NAME_MAX_LENGTH)
|
|
hint = models.CharField(max_length=HINT_MAX_LENGTH, default="")
|
|
|
|
# Sort order for display of custom profile fields.
|
|
order = 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 = models.BooleanField(default=False)
|
|
required = models.BooleanField(default=False)
|
|
|
|
# Whether regular users can edit this field on their own account.
|
|
editable_by_user = models.BooleanField(default=True, db_default=True)
|
|
|
|
SHORT_TEXT = 1
|
|
LONG_TEXT = 2
|
|
SELECT = 3
|
|
DATE = 4
|
|
URL = 5
|
|
USER = 6
|
|
EXTERNAL_ACCOUNT = 7
|
|
PRONOUNS = 8
|
|
|
|
# These are the fields whose validators require more than var_name
|
|
# and value argument. i.e. SELECT require field_data, USER require
|
|
# realm as argument.
|
|
SELECT_FIELD_TYPE_DATA: list[ExtendedFieldElement] = [
|
|
(SELECT, gettext_lazy("List of options"), validate_select_field, str, "SELECT"),
|
|
]
|
|
USER_FIELD_TYPE_DATA: list[UserFieldElement] = [
|
|
(USER, gettext_lazy("Users"), check_valid_user_ids, orjson.loads, "USER"),
|
|
]
|
|
|
|
SELECT_FIELD_VALIDATORS: dict[int, ExtendedValidator] = {
|
|
item[0]: item[2] for item in SELECT_FIELD_TYPE_DATA
|
|
}
|
|
USER_FIELD_VALIDATORS: dict[int, RealmUserValidator] = {
|
|
item[0]: item[2] for item in USER_FIELD_TYPE_DATA
|
|
}
|
|
|
|
FIELD_TYPE_DATA: list[FieldElement] = [
|
|
# Type, display name, validator, converter, keyword
|
|
(SHORT_TEXT, gettext_lazy("Text (short)"), check_short_string, str, "SHORT_TEXT"),
|
|
(LONG_TEXT, gettext_lazy("Text (long)"), check_long_string, str, "LONG_TEXT"),
|
|
(DATE, gettext_lazy("Date"), check_date, str, "DATE"),
|
|
(URL, gettext_lazy("Link"), check_url, str, "URL"),
|
|
(
|
|
EXTERNAL_ACCOUNT,
|
|
gettext_lazy("External account"),
|
|
check_short_string,
|
|
str,
|
|
"EXTERNAL_ACCOUNT",
|
|
),
|
|
(PRONOUNS, gettext_lazy("Pronouns"), check_short_string, str, "PRONOUNS"),
|
|
]
|
|
|
|
ALL_FIELD_TYPES = sorted(
|
|
[*FIELD_TYPE_DATA, *SELECT_FIELD_TYPE_DATA, *USER_FIELD_TYPE_DATA], key=lambda x: x[1]
|
|
)
|
|
|
|
FIELD_VALIDATORS: dict[int, Validator[ProfileDataElementValue]] = {
|
|
item[0]: item[2] for item in FIELD_TYPE_DATA
|
|
}
|
|
FIELD_CONVERTERS: dict[int, Callable[[Any], Any]] = {
|
|
item[0]: item[3] for item in ALL_FIELD_TYPES
|
|
}
|
|
FIELD_TYPE_CHOICES: list[tuple[int, StrPromise]] = [
|
|
(item[0], item[1]) for item in ALL_FIELD_TYPES
|
|
]
|
|
|
|
field_type = models.PositiveSmallIntegerField(
|
|
choices=FIELD_TYPE_CHOICES,
|
|
default=SHORT_TEXT,
|
|
)
|
|
|
|
# A JSON blob of any additional data needed to define the field beyond
|
|
# type/name/hint.
|
|
#
|
|
# The format depends on the type. Field types SHORT_TEXT, LONG_TEXT,
|
|
# DATE, URL, and USER leave this empty. Fields of type SELECT store the
|
|
# choices' descriptions.
|
|
#
|
|
# Note: There is no performance overhead of using TextField in PostgreSQL.
|
|
# See https://www.postgresql.org/docs/9.0/static/datatype-character.html
|
|
field_data = models.TextField(default="")
|
|
|
|
class Meta:
|
|
unique_together = ("realm", "name")
|
|
|
|
@override
|
|
def __str__(self) -> str:
|
|
return f"{self.realm!r} {self.name} {self.field_type} {self.order}"
|
|
|
|
def as_dict(self) -> ProfileDataElementBase:
|
|
data_as_dict: ProfileDataElementBase = {
|
|
"id": self.id,
|
|
"name": self.name,
|
|
"type": self.field_type,
|
|
"hint": self.hint,
|
|
"field_data": self.field_data,
|
|
"order": self.order,
|
|
"required": self.required,
|
|
"editable_by_user": self.editable_by_user,
|
|
}
|
|
if self.display_in_profile_summary:
|
|
data_as_dict["display_in_profile_summary"] = True
|
|
|
|
return data_as_dict
|
|
|
|
def is_renderable(self) -> bool:
|
|
if self.field_type in [CustomProfileField.SHORT_TEXT, CustomProfileField.LONG_TEXT]:
|
|
return True
|
|
return False
|
|
|
|
|
|
def custom_profile_fields_for_realm(realm_id: int) -> QuerySet[CustomProfileField]:
|
|
return CustomProfileField.objects.filter(realm=realm_id).order_by("order")
|
|
|
|
|
|
class CustomProfileFieldValue(models.Model):
|
|
user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE)
|
|
field = models.ForeignKey(CustomProfileField, on_delete=CASCADE)
|
|
value = models.TextField()
|
|
rendered_value = models.TextField(null=True, default=None)
|
|
|
|
class Meta:
|
|
unique_together = ("user_profile", "field")
|
|
|
|
@override
|
|
def __str__(self) -> str:
|
|
return f"{self.user_profile!r} {self.field!r} {self.value}"
|