-
Notifications
You must be signed in to change notification settings - Fork 24
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
2170 introduce gender model #2485
base: main
Are you sure you want to change the base?
Conversation
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.
See comments in migration code.
With help of the openslides-datastore-service pull request 368 it will be easier to write the migration.
openslides_backend/migrations/migrations/0054_introduce_flexible_gender_model.py
Outdated
Show resolved
Hide resolved
openslides_backend/migrations/migrations/0054_introduce_flexible_gender_model.py
Outdated
Show resolved
Hide resolved
openslides_backend/migrations/migrations/0054_introduce_flexible_gender_model.py
Outdated
Show resolved
Hide resolved
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.
Please change the permission from can_manage_organization
to can_manage_accounts
to edit/delete or update a gender
I will use |
openslides_backend/migrations/migrations/0054_introduce_flexible_gender_model.py
Show resolved
Hide resolved
# update users | ||
for user_id, user in users.items(): | ||
if user.get("gender"): | ||
if gender_id := gender_strings.index(user.get("gender")) + 1: |
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.
The index-method on a list will raise an exception, if no string matches. Use it with a check before (user.get("gender") in gender_strings) or with a try/except-construct.
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.
Done
events.append( | ||
RequestUpdateEvent( | ||
fqid_from_collection_and_id("user", user_id), | ||
{"gender_id": gender_id, "gender": None}, |
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.
Even if there was no gender_id found for the gender
you must create an event to set gender to None.
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.
Done
@@ -216,6 +216,7 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: | |||
"last_email_sent", | |||
"last_login", | |||
"committee_ids", | |||
"gender", |
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.
Removing is a concern of the migration. Please remove the deletion here and integrate it in the migration.
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.
We can do that but then we would have to ensure, that the client will not export the gender in future migration indices. Also we would have to rewrite the tests for the meeting import to not include the gender per default since the majority of tests uses the backend migration index, which I then did.
"gender/1": {"name": "male"}, | ||
"gender/2": {"name": "female"}, | ||
"gender/3": {"name": "diverse"}, | ||
"gender/4": {"name": "non-binary"}, |
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.
Amend an existing test: explicit test for the field gender being empty after importing a new user
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 used test_all_migrations for this.
) -> dict[str, int] | None: | ||
"""returns gender as a dict of "name" and "id" if it exists, otherwise raises ActionException""" | ||
if gender_id := instance.get("gender_id"): | ||
gender_dict = datastore.get_all("gender", ["id", "name"], lock_result=False) |
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.
use a simple self.datastore.get("gender", gender_id,...)
to read the gender instance.
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 use the fqid to get the gender now.
) | ||
def check_gender_exists( | ||
datastore: DatastoreService, instance: dict[str, Any] | ||
) -> dict[str, int] | None: |
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.
don't return anything, it is never used
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.
Done
@@ -29,7 +29,7 @@ def setUp(self) -> None: | |||
"first_name": "firstName", | |||
"last_name": "lastName", | |||
"email": "email", | |||
"gender": "gender", | |||
"gender_id": "gender_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.
revert to gender:gender
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.
Done
@@ -28,7 +28,7 @@ class UserMergeTogether(CreateAction, CheckForArchivedMeetingMixin): | |||
"is_active", | |||
"is_physical_person", | |||
"default_password", | |||
"gender", | |||
"gender_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.
revert to gender
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.
As discussed with @Elblinator only the backend and client will use this and we can expect the client to use the 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.
Please change the documentation or the implementation
@@ -12,7 +12,7 @@ class OrganizationUpdateActionTest(BaseActionTestCase): | |||
"first_name": "firstName", | |||
"last_name": "lastName", | |||
"email": "email", | |||
"gender": "gender", | |||
"gender_id": "gender_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.
revert to gender:gender
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.
Done
docs/actions/meeting.import.md
Outdated
@@ -16,10 +16,11 @@ Import one meeting from a file. The file must only contain exactly one meeting. | |||
- The request user is assigned to the admin group. | |||
- meeting.is_active_in_organization_id is set. | |||
- It has to be checked, whether the organization.limit_of_meetings is unlimited(=0) or lower than the active meetings in organization.active_meeting_ids, if the new meeting is not archived (`is_active_in_organization_id` is set) | |||
- Search for users and if username, first-name, last-name and email are identical use this user instead creating a duplicate. Keep the data, including password, of the exiating user. | |||
- Search for users and if username, first-name, last-name and email are identical use this existing user instead of creating a duplicate. Keep the data, including password, of the existing user. Relavent relations such as to the meeting will be updated though. |
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.
- Search for users and if username, first-name, last-name and email are identical use this existing user instead of creating a duplicate. Keep the data, including password, of the existing user. Relavent relations such as to the meeting will be updated though. | |
- Search for users and if username, first-name, last-name and email are identical use this existing user instead of creating a duplicate. Keep the data, including password, of the existing user. Relevant relations such as to the meeting will be updated though. |
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.
Done
@@ -28,7 +28,7 @@ class UserMergeTogether(CreateAction, CheckForArchivedMeetingMixin): | |||
"is_active", | |||
"is_physical_person", | |||
"default_password", | |||
"gender", | |||
"gender_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.
Please change the documentation or the implementation
|
||
def test_create(self) -> None: | ||
self.set_models(self.test_models) | ||
self.set_models({"gender/1": {"organization_id": 1, "name": "male"}}) |
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.
You are using the test_models only once. Then please join organization and user setting with the gender/1 setting in line 18 and remove it from setUp-method.
Additionally you should add the opposite relation (field organization/gender_ids) to the organization/1-record and assert the gender/1 and gender/2 instances in organization.
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.
Done
def create_data(self) -> None: | ||
self.set_models( | ||
{ | ||
ONE_ORGANIZATION_FQID: {"gender_ids": [self.gender_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.
Also add gender/1 to the gender_ids.
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.
Done
ONE_ORGANIZATION_FQID: {"gender_ids": [1, self.gender_id, 6]}, | ||
} | ||
) | ||
# user = self.request("user.update", {"id": 21, "gender_id": self.gender_id}) #error? |
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.
Please remove the 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.
Done
@@ -2140,6 +2139,7 @@ def test_with_listfields_from_migration(self) -> None: | |||
data["meeting"]["meeting"]["1"]["motion_ids"] = [5, 6] | |||
data["meeting"]["meeting"]["1"]["list_of_speakers_ids"] = [1, 2] | |||
data["meeting"]["motion_state"]["1"]["motion_ids"] = [5, 6] | |||
data["meeting"]["user"]["1"]["gender"] = "male" |
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.
Concerning gender
:
- set a different gender in the user that will become user/1 from import (match an existing user)
- Write an assert for the imported user/1 gender/gender_id field after the request, showing that the gender not changed
In contrast to the other changed test make one with matching user and the other with new user
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 hope I understood this correctly. I created another user to check both: creating a user with a new gender and not updating a matching user.
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.
let's talk about it
@@ -2225,6 +2224,10 @@ def test_all_migrations(self) -> None: | |||
"organization/1", {"user_ids": [1, 2], "active_meeting_ids": [1, 2]} | |||
) | |||
|
|||
self._assert_fields( |
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.
Don't use the private
- method _assert_fields, but instead use self.assert_model_assists
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.
Done
@@ -109,7 +124,16 @@ def validate_fields(self, instance_old: dict[str, Any]) -> dict[str, Any]: | |||
if isinstance((tx := instance_old[payload_field]), list) and len(tx) | |||
else tx | |||
) | |||
if value not in (None, []): | |||
if payload_field == "gender": |
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.
The requirement for the save_saml_account action is to create a gender-instance, if the value of the gender
is not in the collection.
IMO this is the most convenient way to add new gender-entries to the collection and the same behavior of accepting all gender-values in save_saml_account as before.
Please update the action documentation and implement these feature as described.
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.
Done
…ate gender for saml accounts
for user in users.values(): | ||
if gender := user.get("gender"): | ||
del user["gender"] | ||
# if user is to be created, convert to gender_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.
IMO the comment at this place is misleading, because you don't convert gender to gender_id at this place
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.
Done
if gender := user.get("gender"): | ||
del user["gender"] | ||
# if user is to be created, convert to gender_id | ||
if gender != "": |
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.
No need for this if
-statement, because the if
in line 169 results in False, if gender is an empty string
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.
Done. Also considered that an empty string should be deleted, plus writing a test for it.
users = instance["meeting"].get("user", {}) | ||
for user_id, gender in self.user_id_to_gender.items(): | ||
if user_id not in self.merge_user_map: | ||
gender_dict = self.datastore.filter( |
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.
IMO you should read all existing gender-names to a dict before the loop. Then you don't need to ask the database for each user.
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.
Done
GenderCreate, [{"name": gender}] | ||
) | ||
gender_id = action_result[0].get("id", 0) # type: ignore | ||
self.execute_other_action( |
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.
IMO you shouldn't update one field with an action, but update it in the users data. I hope I remember correctly, that finally user's will be update or created.
@@ -2140,6 +2139,7 @@ def test_with_listfields_from_migration(self) -> None: | |||
data["meeting"]["meeting"]["1"]["motion_ids"] = [5, 6] | |||
data["meeting"]["meeting"]["1"]["list_of_speakers_ids"] = [1, 2] | |||
data["meeting"]["motion_state"]["1"]["motion_ids"] = [5, 6] | |||
data["meeting"]["user"]["1"]["gender"] = "male" |
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.
let's talk about it
|
||
def migrate_models(self) -> list[BaseRequestEvent] | None: | ||
events: list[BaseRequestEvent] = [] | ||
users = self.reader.get_all("user", ["gender"]) |
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.
You don't need to read the users, if it is a in-memory-migration. Therefore you read them only, if it is no in-memory-migration
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.
Done
openslides_backend/migrations/migrations/0054_introduce_flexible_gender_model.py
Show resolved
Hide resolved
… in saml, export, import and migration plus additional test for empty string.
docs/actions/gender.update.md
Outdated
@@ -0,0 +1,15 @@ | |||
## Payload | |||
```js |
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.
Why do you put the js-tag to the page?
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 used it to see the highlighting changes as it is being done in some other md files. Since this is mostly not used in our docs I agreed with @rrenkert to delete these tags from the files I'm editing.
docs/actions/user.create.md
Outdated
@@ -57,7 +57,7 @@ Creates a user. | |||
* If `username` is given, it has to be unique within all users. If there already exists a user with the same username, an error must be returned. If the `username` is not given, 1. the saml_id will be used or 2. it has to be generated (see [user.create#generate-a-username](user.create.md#generate-a-username) below). Also the username may not contain spaces. | |||
* The `organization_management_level` as restring can be taken from the enum of this user field. | |||
* Remove starting and trailing spaces from `username`, `first_name` and `last_name` | |||
* The given `gender` must be present in `organization/genders` | |||
* The given `gender_id` must be present in `organization/gender_ids` |
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.
Better: ... must be present in database
. The gender lived only in organization/genders
-field, the gender_id is presented by an gender-instance in the database.
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.
Done.
@@ -157,11 +157,16 @@ def add_users( | |||
) | |||
|
|||
for user in users.values(): | |||
# convert to gender string |
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.
Please remove the comment. It is just the description of the following variable, but that would be to much to maintain
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.
Done
"organization_id": 1, | ||
} | ||
} | ||
other_user_request_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.
Before this line you have to update your dict meeting["meeting"]["user"] with the data for user/2. Write something like
data["meeting"]["user"].update(other_user_request_data)
or put the user/2-data directly into the update-bracket.
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 prefer to use both in one curly brace instead.
"organization_id": 1, | ||
} | ||
} | ||
data["meeting"]["user"]["1"]["gender"] = "needs_to_be_created" |
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.
Please put the change of user/1 in front of the changes of user/2. IMO it is easier to understand, that user/1 in data is not the request user user/1.
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.
Done
} | ||
} | ||
data["meeting"]["user"]["1"]["gender"] = "needs_to_be_created" | ||
data["meeting"]["user"] = data["meeting"]["user"] | other_user_request_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.
This part is missing for user/2 data, see above.
I would prefer the dict.update-notation for easier understanding. Use the same way as you will for user/2
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.
Done
@@ -12,7 +12,7 @@ def setUp(self) -> None: | |||
"first_name": "Max", | |||
"last_name": "Mustermann", | |||
"email": "[email protected]", | |||
"gender": "male", | |||
"gender_id": 1, |
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.
Seem that the whole structure self.result isn't used anymore. Than you could delete it.
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.
Done
@@ -83,11 +84,26 @@ def validate_instance(self, instance: dict[str, Any]) -> None: | |||
] | |||
} | |||
for model_field, payload_field in self.saml_attr_mapping.items() | |||
if model_field in allowed_user_fields | |||
# handle only allowed fields. handle gender seperatly since it needs conversion to 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.
typo: separately
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.
Done
self.user_id_to_gender = {} | ||
users = instance["meeting"].get("user", {}) | ||
for user in users.values(): | ||
if gender := user.get("gender"): |
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 do you think about following code:
if "gender" in user:
if gender := user["gender"]:
self.user_id_to_gender[user["id"]] = gender
del user["gender"]
Your way you could get None
if gender was set to it or if gender doesn't exist in 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 like the new way even better, because it's leaner.
if gender in gender_dict: | ||
gender_id = gender_dict[gender] | ||
else: | ||
action_result = self.execute_other_action( |
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.
You have to add a new key to your gender_dict, otherwise you'll execute the create_gender action for all users with the same gender
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.
Done.
Please fix the error and merge the main branch into the pull request |
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.
Just 2 little things in documentation.
I'll try to solve the user update problem.
docs/actions/gender.delete.md
Outdated
``` | ||
|
||
## Action | ||
Deletes the gender. |
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.
Add some text like The first 4 gender names are fixed and cannot be changed or delete
.
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.
Done
docs/actions/gender.update.md
Outdated
``` | ||
|
||
## Action | ||
Updates the gender if a gender with that name doesn't exist. |
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.
Also add some text like The first 4 gender names are fixed and cannot be changed or delete
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.
Done
…e import without any new genders would result in exception.
Thank you. That was really helpful. I didn't think of adding it to the org like this. |
Notice: Needs to be updated, to work with my pr in client. |
Closes #2170
Themes to check:
gender
-field may change nothing, agender
-field with an empty string should be interpreted as "no gender value wanted" and the user's gender_id should be set to NULL. @rrenkert @Elblinator hope you agree