-
-
Notifications
You must be signed in to change notification settings - Fork 9.3k
custom_profile_fields: Add 'Select multiple' field type #37018
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
custom_profile_fields: Add 'Select multiple' field type #37018
Conversation
|
Hello @zulip/server-settings members, this pull request was labeled with the "area: settings (admin/org)" label, so you may want to check it out! |
fa04c95 to
56f7c05
Compare
| (SELECT, gettext_lazy("List of options"), validate_select_field, str, "SELECT"), | ||
| ( | ||
| SELECT_MULTIPLE, | ||
| gettext_lazy("List of options (multiple)"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if "multiple" or something like "multiselect" would feel clearer to end users for this.
zerver/tests/test_users.py
Outdated
| try: | ||
| field_type_id = CustomProfileField.SELECT_MULTIPLE | ||
| except AttributeError: | ||
| field_type_id = 9 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This exception should never happen in a test; how did you end up writing this test code?
(I think you only use this in one place, so it should be inlined?)
zerver/lib/users.py
Outdated
| try: | ||
| val_to_validate = orjson.loads(value) | ||
| except orjson.JSONDecodeError: | ||
| except orjson.JSONDecodeError: # nocoverage |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These comments should be in the previous commit that adds the code with the linter issue. Or better, just add coverage in that commit.
(Similarly for all the other backend changes in this second commit).
web/src/user_profile.ts
Outdated
| readable_values.push(option.text); | ||
| } | ||
| } | ||
| const final_text = readable_values.join(", "); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you want to use one of the helpers from util.ts that use Intl.ListFormat to do this in an internationalized fashion. (Japanese doesn't use , as the separator, for example).
| return undefined; | ||
| } | ||
| profile_field.value = final_text; | ||
| } catch { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the situation when this catch clause would fire?
Generally it's quite risky to write code that catches ANY exception, since if the code above grew a syntax error such that it always failed, it'd just use this fallback code here.
| ); | ||
| } catch { | ||
| choices_data = []; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly, why did you add this try/catch?
| selected_values.add(item); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a lot of parsing code; can you add comments explaining what possible failures you're worried about here?
|
On the UI side:
|
|
This looks like a great first PR @PedroSimas4! FYI #api design > custom profile fields @ 💬 will need to be resolved before we can finalize the server part of the implementation. . |
5c1315b to
f98686f
Compare
|
@timabbott @alya Thank you for the detailed feedback! I have updated the PR addressing all the points:
Changed the field label to "Multi-select" as suggested. Moved the inline styles to web/styles/settings.css and cleaned up the HBS template. Fixed the flexbox wrapping and spacing for long values. (Added screenshots of the Admin UI and Profile View).
Switched to util.format_array_as_list for proper internationalization. Replaced generic try/catch blocks with specific z.safeParse checks to handle data integrity safely without swallowing logic errors.
Removed # nocoverage tags. Added a regression test case in zerver/tests/test_users.py covering both invalid JSON input and corrupted field definitions. Refactored the test flow to avoid the unnecessary try/except block for the field type constant. Ready for another look! |
|
Hm, I still don't see a screenshot of the dropdown? |
|
"Multi-select" is more technical-sounding than I like. Let's just call it "Checkboxes" (following, e.g., Google Forms and SurveyMonkey). Flagged for discussion in #design > custom profile field types, in case there's feedback. |
|
Tagging @sahil839 for maintainer review once the renaming feedback above has been completed. |
f98686f to
2e1f1b6
Compare
This commit introduces a new custom profile field type, SELECT_MULTIPLE (ID 9). This field allows administrators to define a fixed set of options, similar to the existing SELECT type, but enables users to choose multiple answers. Key implementation details: * Storage: Selected values are stored in the database as a JSON-encoded list of strings (e.g., '["1", "2"]'). * Validation: Added logic to `validator.py` to ensure that saved values are valid lists and that every selected item exists in the field's defined options. * Tests: Added backend tests in `test_users.py` to verify creation, validation, and storage logic.
This updates the web application to support the display and editing of the new 'Select multiple' custom profile field type. Changes include: * Settings UI: Admins can now create and edit multi-select fields in the organization settings. * Profile Display: The user profile now renders these fields as a comma-separated list of selected values. If no value is selected, the field is hidden to maintain a clean UI. * Tests: Updated `settings_profile_fields.test.cjs` and mocks to support the new field type ID. Fixes zulip#17495.
2e1f1b6 to
9735e8b
Compare
|
Renamed the field to 'Checkboxes' as suggested |
sahil839
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have only done review for the first commit currently. Will do the complete review tommorrow.
| for item in items: | ||
| if item not in field_data_dict: | ||
| msg = _("'{value}' is not a valid choice for '{field_name}'.") | ||
| raise ValidationError(msg.format(value=item, field_name=var_name)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think some code is missing like validating the data when creating a SELECT_MULTIPLE type custom field. This is for validating the value set by user for the custom field but there is no validation when an admin creates a SELECT_MULTIPLE type custom profile field.
| val_to_validate = orjson.loads(value) | ||
| except orjson.JSONDecodeError: | ||
| raise ValidationError(_("Invalid value (not valid JSON).")) | ||
| multiselect_validator = cast( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't why casting to a type is needed here. I think this is because SELECT_FIELD_VALIDATORS dict is being used to store validators for both SELECT and SELECT_MULTIPLE type fields when they have different return types.
I think we can store them differently if that is the case instead of using cast here.
| raise ValidationError(_("{var_name} is not a string or integer").format(var_name=var_name)) | ||
|
|
||
|
|
||
| def validate_select_multiple_field(var_name: str, field_data: str, value: object) -> Any: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can use an explicit type here instead of Any.
| ) | ||
|
|
||
| field.field_data = "invalid_json_definition" | ||
| field.save() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure if it is required to test such a case where DB needs to be updated manually to set invalid data and it is not possible to do so using API.
sahil839
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Posted some comments in frontend code as well. I feel the try-catch blocks are not needed since we can expect valid JSON value from server where we already validate the data. Or would be good if you could highlight cases where JSON value can be invalid except for modifying the DB value manually.
| $container.find("input:checked").each(function (this: HTMLElement) { | ||
| const val = $(this).val(); | ||
| if (val !== undefined) { | ||
| values.push(String(val)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of using String here, above call to val() can be modified as const val = $<HTMLInputELement>(this).val()!.
| ); | ||
| } catch { | ||
| field_choice_dict = {}; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need this try-catch block. We expect server to validate field_data when creating and updating the field so we can expect field.field_data to always be a valid json.
| // the expected schema (array of strings). | ||
| try { | ||
| parsed = JSON.parse(current_val); | ||
| } catch { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly for the field value as well, the server would validate the value when setting or updating it.
| const result = z.array(z.string()).safeParse(parsed); | ||
| if (result.success) { | ||
| for (const item of result.data) { | ||
| selected_values.add(item); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This breaks the behavior for single select type field where the selected value is not shown in UI. I guess would be good to just have separate if-blocks for single and multiple select fields, i.e. have is_select_field false for multiple select fields and instead have a different variable like is_multi_select_field.
| // We wrap this in a try/catch so that if the field data is malformed | ||
| // (e.g. invalid JSON), the edit form still opens (showing empty choices) | ||
| // rather than crashing. This allows the admin to recover by re-entering choices. | ||
| try { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any case that you can reproduce with invalid JSON without manually modifying the DB? If not we can remove this as well.
| profile_field.field_data && | ||
| (profile_field.type === field_types.SELECT.id || | ||
| profile_field.type === field_types.SELECT_MULTIPLE.id) | ||
| ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is choices field computed here used in the below render_admin_profile_field_list? I don't think so because we do not show choices in the list.
|
Heads up @PedroSimas4, we just merged some commits that conflict with the changes you made in this pull request! You can review this repository's recent commits to see where the conflicts occur. Please rebase your feature branch against the |
This PR adds support for a new custom profile field type: SELECT_MULTIPLE (ID 9). This allows administrators to define a list of fixed options (similar to the standard Select field), but enables users to select multiple answers via checkboxes.
Key Changes:
Backend: Added SELECT_MULTIPLE to models.py, added validation logic in validator.py (ensuring values are valid lists of option keys), and updated users.py to handle storage.
Frontend: Updated settings UI to allow creating these fields and the Profile UI to display selected values as a comma-separated list.
Storage: Data is stored in the database as a JSON-encoded list of strings (e.g., ["1", "2"]).
Fixes: #17495
How changes were tested:
Automated Tests:
Added backend tests in zerver/tests/test_users.py to verify creation, validation (invalid choices, malformed JSON), and storage logic.
Added frontend unit tests in web/tests/settings_profile_fields.test.cjs to verify the UI configuration logic.
Ran ./tools/test-all locally and all tests passed.
Manual Verification:
Verified the end-to-end flow:
Created a "Programming Languages" multi-select field in Organization Settings.
Edited a user profile to select multiple options (e.g., Python, Rust).
Verified that the profile displays "Python, Rust" correctly.
Verified that deselecting all options hides the field from the profile display.
Screenshots and screen captures:
Self-review checklist
[x] [Self-reviewed]
Communicate decisions, questions, and potential concerns.
[x] Explains differences from previous plans (e.g., issue description).
[x] Highlights technical choices and bugs encountered.
[x] Calls out remaining decisions and concerns.
[x] Automated tests verify logic where appropriate.
Individual commits are ready for review.
[x] Each commit is a coherent idea.
[x] Commit message(s) explain reasoning and motivation for changes.
Completed manual review and testing of the following:
[x] Visual appearance of the changes.
[x] Responsiveness and internationalization.
[x] Strings and tooltips.
[x] End-to-end functionality of buttons, interactions and flows.
[x] Corner cases, error conditions, and easily imagined bugs.