Skip to content
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

Open
wants to merge 35 commits into
base: main
Choose a base branch
from

Conversation

hjanott
Copy link
Member

@hjanott hjanott commented Jun 21, 2024

Closes #2170

Themes to check:

  • meeting export: export only the gender string per user.
  • meeting import: import the gender only for users that need to be created.
  • save_saml_account-Action: saml_attr_mapping field continue to use gender:gender mapping. In the action we have to transform the gender-string to an existing or new gender_id and update the user. Always create not existing gender-instances. An action-payload without gender-field may change nothing, a gender-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
  • user.create/update: now uses gender_id in payload instead of gender as string. Must be also changed in client.
  • set null in user and organization on deletion of gender-instances

@hjanott hjanott added feature migration Introduces a new migration labels Jun 21, 2024
@hjanott hjanott added this to the 4.2 milestone Jun 21, 2024
@hjanott hjanott requested a review from r-peschke June 21, 2024 13:18
@hjanott hjanott marked this pull request as ready for review June 21, 2024 13:57
@hjanott hjanott marked this pull request as draft June 21, 2024 14:17
Copy link
Member

@r-peschke r-peschke left a 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.

Copy link
Member

@Elblinator Elblinator left a 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

@hjanott
Copy link
Member Author

hjanott commented Jun 27, 2024

can_manage_accounts

I will use can_manage_users instead. ;)

# update users
for user_id, user in users.items():
if user.get("gender"):
if gender_id := gender_strings.index(user.get("gender")) + 1:
Copy link
Member

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.

Copy link
Member Author

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},
Copy link
Member

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.

Copy link
Member Author

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",
Copy link
Member

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.

Copy link
Member Author

@hjanott hjanott Jul 31, 2024

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"},
Copy link
Member

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

Copy link
Member Author

@hjanott hjanott Jul 30, 2024

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)
Copy link
Member

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.

Copy link
Member Author

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:
Copy link
Member

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

Copy link
Member Author

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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert to gender:gender

Copy link
Member Author

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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert to gender

Copy link
Member Author

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.

Copy link
Member

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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert to gender:gender

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- 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.

Copy link
Member Author

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",
Copy link
Member

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"}})
Copy link
Member

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.

Copy link
Member Author

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]},
Copy link
Member

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.

Copy link
Member Author

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?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove the comment.

Copy link
Member Author

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"
Copy link
Member

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

Copy link
Member Author

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.

Copy link
Member

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(
Copy link
Member

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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

tests/system/action/user/test_save_saml_account.py Outdated Show resolved Hide resolved
@@ -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":
Copy link
Member

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@hjanott hjanott requested a review from r-peschke August 21, 2024 07:31
docs/actions/gender.update.md Show resolved Hide resolved
for user in users.values():
if gender := user.get("gender"):
del user["gender"]
# if user is to be created, convert to gender_id
Copy link
Member

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

Copy link
Member Author

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 != "":
Copy link
Member

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

Copy link
Member Author

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(
Copy link
Member

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.

Copy link
Member Author

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(
Copy link
Member

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.

openslides_backend/shared/export_helper.py Show resolved Hide resolved
@@ -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"
Copy link
Member

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"])
Copy link
Member

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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@hjanott hjanott requested a review from r-peschke August 22, 2024 08:48
@@ -0,0 +1,15 @@
## Payload
```js
Copy link
Member

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?

Copy link
Member Author

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.

@@ -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`
Copy link
Member

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.

Copy link
Member Author

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
Copy link
Member

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

Copy link
Member Author

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 = {
Copy link
Member

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.

Copy link
Member Author

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"
Copy link
Member

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.

Copy link
Member Author

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
Copy link
Member

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

Copy link
Member Author

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,
Copy link
Member

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.

Copy link
Member Author

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: separately

Copy link
Member Author

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"):
Copy link
Member

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

Copy link
Member Author

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(
Copy link
Member

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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@hjanott hjanott requested a review from r-peschke August 27, 2024 12:40
@r-peschke
Copy link
Member

Please fix the error and merge the main branch into the pull request

@r-peschke r-peschke removed their assignment Aug 28, 2024
Copy link
Member

@r-peschke r-peschke left a 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.

```

## Action
Deletes the gender.
Copy link
Member

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

```

## Action
Updates the gender if a gender with that name doesn't exist.
Copy link
Member

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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@hjanott
Copy link
Member Author

hjanott commented Sep 5, 2024

I'll try to solve the user update problem.

Thank you. That was really helpful. I didn't think of adding it to the org like this.

@reiterl
Copy link
Member

reiterl commented Sep 11, 2024

Notice: Needs to be updated, to work with my pr in client.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature migration Introduces a new migration
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Improve organization/genders
4 participants