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

Add filter to UserProfileListView #1038

Merged
merged 25 commits into from
Sep 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
94fa2e0
add filter to UserProfileListView
felixrindt Aug 28, 2023
f74aa86
auto-overflow for qualification list
felixrindt Aug 28, 2023
2d0016e
fix test
felixrindt Aug 28, 2023
bab630c
add default pagination size
felixrindt Aug 29, 2023
6e19b91
deprecate relevant qualification categories preference
felixrindt Aug 29, 2023
4734051
add show_with_user to category form
felixrindt Aug 29, 2023
8669aa8
refactor getting qualification abbreviations
felixrindt Aug 29, 2023
11406d1
refactor qualification graph
felixrindt Aug 29, 2023
6521535
only show essential qualifications
felixrindt Aug 29, 2023
1a37cc7
refactor QualificationUniverse
felixrindt Aug 29, 2023
e172685
use contextvars
felixrindt Aug 29, 2023
53767ec
use QualificationUniverse in more places
felixrindt Aug 29, 2023
fafa0ec
userlist use qualification badges
felixrindt Aug 29, 2023
d277643
restyle group list
felixrindt Aug 30, 2023
bbe5334
fix DOM text reinterpreted as HTML
felixrindt Aug 30, 2023
8cd92c7
dont log permission changes on userprofile
felixrindt Aug 30, 2023
c9a7c4c
left align user labels
felixrindt Aug 30, 2023
f7edf4f
add userlist tests
felixrindt Aug 30, 2023
9b979ae
correct css order
felixrindt Aug 31, 2023
8e39f49
address review
felixrindt Aug 31, 2023
403b4dd
fix the graph
felixrindt Aug 31, 2023
7b1ca2f
user singlemodelchoice fields in user filter
felixrindt Sep 1, 2023
92bcbe1
trim user list in group list view
felixrindt Sep 1, 2023
90457d0
better differentiation of top_level vs essential qualification
felixrindt Sep 1, 2023
23ef745
update is_management_group label/help text
felixrindt Sep 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ephios/core/dynamic_preferences_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ class OrganizationName(StringPreference):

@global_preferences_registry.register
class RelevantQualificationCategories(ModelMultipleChoicePreference):
"""deprecated"""

name = "relevant_qualification_categories"
section = general_global_section
model = QualificationCategory
Expand Down
58 changes: 38 additions & 20 deletions ephios/core/forms/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,25 @@
from ephios.modellogging.log import add_log_recorder
from ephios.modellogging.recorders import DerivedFieldsLogRecorder

CORE_MANAGEMENT_PERMISSIONS = [
PLANNING_TEST_PERMISSION = "core.add_event"

PLANNING_PERMISSIONS = [
"core.add_event",
"core.delete_event",
]

HR_TEST_PERMISSION = "core.change_userprofile"

HR_PERMISSIONS = [
"core.add_userprofile",
"core.change_userprofile",
"core.delete_userprofile",
"core.view_userprofile",
]

MANAGEMENT_TEST_PERMISSION = "auth.change_group"

MANAGEMENT_PERMISSIONS = [
"auth.add_group",
"auth.change_group",
"auth.delete_group",
Expand Down Expand Up @@ -60,12 +78,15 @@ def get_group_permission_log_fields(group):
# This lives here because it is closely related to the fields on GroupForm below
if not group.pk:
return {}
perms = set(group.permissions.values_list("codename", flat=True))
perms = set(
f"{g[0]}.{g[1]}"
for g in group.permissions.values_list("content_type__app_label", "codename")
)

return {
_("Can add events"): "add_event" in perms,
_("Can edit users"): "change_userprofile" in perms,
_("Can manage ephios"): "change_group" in perms,
_("Can add events"): PLANNING_TEST_PERMISSION in perms,
_("Can edit users"): HR_TEST_PERMISSION in perms,
_("Can change permissions"): MANAGEMENT_TEST_PERMISSION in perms,
# force evaluation of querysets
_("Can publish events for groups"): set(
get_objects_for_group(group, "publish_event_for_group", klass=Group)
Expand All @@ -79,7 +100,7 @@ def get_group_permission_log_fields(group):
class GroupForm(PermissionFormMixin, ModelForm):
is_planning_group = PermissionField(
label=_("Can add events"),
permissions=["core.add_event", "core.delete_event"],
permissions=PLANNING_PERMISSIONS,
required=False,
)
publish_event_for_group = ModelMultipleChoiceField(
Expand All @@ -102,22 +123,19 @@ class GroupForm(PermissionFormMixin, ModelForm):
is_hr_group = PermissionField(
label=_("Can edit users"),
help_text=_(
"If checked, users in this group can view, add, edit and delete users. They can also manage group memberships for their own groups."
"If checked, users in this group can view, add, edit and delete users. "
"They can also manage group memberships for their own groups."
),
permissions=[
"core.add_userprofile",
"core.change_userprofile",
"core.delete_userprofile",
"core.view_userprofile",
],
permissions=HR_PERMISSIONS,
required=False,
)
is_management_group = PermissionField(
label=_("Can manage permissions and qualifications"),
label=_("Can change permissions and manage ephios"),
help_text=_(
"If checked, users in this group can manage users, groups, all group memberships, eventtypes and qualifications"
"If checked, users in this group can edit all users, change groups, their permissions and memberships "
"as well as define eventtypes and qualifications."
),
permissions=CORE_MANAGEMENT_PERMISSIONS,
permissions=MANAGEMENT_PERMISSIONS,
required=False,
)

Expand All @@ -143,7 +161,7 @@ def __init__(self, **kwargs):
}
self.permission_target = group
extra_fields = [
item for _, result in register_group_permission_fields.send(None) for item in result
item for __, result in register_group_permission_fields.send(None) for item in result
]
for field_name, field in extra_fields:
self.base_fields[field_name] = field
Expand Down Expand Up @@ -180,7 +198,7 @@ def clean_is_management_group(self):
is_management_group = self.cleaned_data["is_management_group"]
if self.fields["is_management_group"].initial and not is_management_group:
other_management_groups = get_groups_with_perms(
only_with_perms_in=CORE_MANAGEMENT_PERMISSIONS,
only_with_perms_in=MANAGEMENT_PERMISSIONS,
must_have_all_perms=True,
).exclude(pk=self.instance.pk)
if not other_management_groups.exists():
Expand Down Expand Up @@ -232,7 +250,7 @@ class UserProfileForm(PermissionFormMixin, ModelForm):
"If checked, this user can change technical ephios settings as well as edit all user profiles, "
"groups, qualifications, events and event types."
),
permissions=CORE_MANAGEMENT_PERMISSIONS,
permissions=MANAGEMENT_PERMISSIONS,
)

def __init__(self, *args, **kwargs):
Expand Down Expand Up @@ -350,7 +368,7 @@ def __init__(self, *args, **kwargs):

def clean(self):
management_groups = get_groups_with_perms(
only_with_perms_in=CORE_MANAGEMENT_PERMISSIONS, must_have_all_perms=True
only_with_perms_in=MANAGEMENT_PERMISSIONS, must_have_all_perms=True
)

if (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Generated by Django 4.2.4 on 2023-08-29 09:30
import logging

from django.db import migrations, models

logger = logging.getLogger(__name__)


def show_with_user_from_relevant_qualification_categories(apps, schema_editor):
from dynamic_preferences.registries import global_preferences_registry

global_preferences = global_preferences_registry.manager()
relevant_categories_pks = {
category.pk for category in global_preferences["general__relevant_qualification_categories"]
}

QualificationCategory = apps.get_model("core", "QualificationCategory")
for category in QualificationCategory.objects.all():
category.show_with_user = category.pk in relevant_categories_pks
logger.info(f"migrate {category.show_with_user=} for {category}")
category.save()


class Migration(migrations.Migration):
dependencies = [
("core", "0019_userprofile_user_email_ci_uniqueness"),
("dynamic_preferences", "0005_auto_20181120_0848"),
]

operations = [
migrations.AddField(
model_name="qualificationcategory",
name="show_with_user",
field=models.BooleanField(
default=True,
verbose_name="Show qualifications of this category everywhere a user is presented.",
),
),
migrations.AlterField(
model_name="userprofile",
name="is_staff",
field=models.BooleanField(default=False, verbose_name="Administrator"),
),
migrations.RunPython(
show_with_user_from_relevant_qualification_categories, migrations.RunPython.noop
),
]
56 changes: 33 additions & 23 deletions ephios/core/models/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@
from ephios.modellogging.recorders import FixedMessageLogRecorder, M2MLogRecorder


class UserProfileQuerySet(models.QuerySet):
felixrindt marked this conversation as resolved.
Show resolved Hide resolved
def visible(self):
return self.filter(is_visible=True)


class UserProfileManager(BaseUserManager):
def create_user(
self,
Expand Down Expand Up @@ -89,7 +94,7 @@ def get_by_natural_key(self, username):

class VisibleUserProfileManager(BaseUserManager):
def get_queryset(self):
return super().get_queryset().filter(is_visible=True)
return super().get_queryset().visible()

# mozilla-django-oidc looks here for a method to create users
def create_user(self, username, email):
Expand Down Expand Up @@ -120,8 +125,8 @@ class UserProfile(guardian.mixins.GuardianUserMixin, PermissionsMixin, AbstractB
"date_of_birth",
]

objects = VisibleUserProfileManager()
all_objects = UserProfileManager()
objects = VisibleUserProfileManager.from_queryset(UserProfileQuerySet)()
all_objects = UserProfileManager.from_queryset(UserProfileQuerySet)()

class Meta:
verbose_name = _("user profile")
Expand Down Expand Up @@ -173,10 +178,20 @@ def as_participant(self):

@property
def qualifications(self):
return Qualification.objects.filter(
pk__in=self.qualification_grants.unexpired().values_list("qualification_id", flat=True)
).annotate(
expires=Max(F("grants__expires"), filter=Q(grants__user=self)),
"""
Returns a queryset with all qualifications that are granted to this user and not expired.
Be careful to not use this in a loop, as it will perform a query for each iteration.
"""
return (
Qualification.objects.filter(
pk__in=self.qualification_grants.unexpired().values_list(
"qualification_id", flat=True
)
)
.annotate(
expires=Max(F("grants__expires"), filter=Q(grants__user=self)),
)
.select_related("category")
)

def get_workhour_items(self):
Expand Down Expand Up @@ -212,7 +227,7 @@ def get_workhour_items(self):
register_model_for_logging(
UserProfile,
ModelFieldsLogConfig(
unlogged_fields={"id", "password", "calendar_token", "last_login"},
unlogged_fields={"id", "password", "calendar_token", "last_login", "user_permissions"},
),
)

Expand All @@ -235,6 +250,10 @@ def get_by_natural_key(self, category_uuid, *args):
class QualificationCategory(Model):
uuid = models.UUIDField("UUID", unique=True, default=uuid.uuid4)
title = CharField(_("title"), max_length=254)
show_with_user = BooleanField(
default=True,
verbose_name=_("Show qualifications of this category everywhere a user is presented"),
)

objects = QualificationCategoryManager()

Expand Down Expand Up @@ -296,21 +315,6 @@ def natural_key(self):

natural_key.dependencies = ["core.QualificationCategory"]

@classmethod
def collect_all_included_qualifications(cls, given_qualifications) -> set:
"""We collect using breadth first search with one query for every layer of inclusion."""
all_qualifications = set(given_qualifications)
current = set(given_qualifications)
while current:
new = (
Qualification.objects.filter(included_by__in=current)
.exclude(id__in=(q.id for q in all_qualifications))
.distinct()
)
all_qualifications |= set(new)
current = new
return all_qualifications


class CustomQualificationGrantQuerySet(models.QuerySet):
# Available on both Manager and QuerySet.
Expand Down Expand Up @@ -350,6 +354,12 @@ class QualificationGrant(Model):

objects = CustomQualificationGrantQuerySet.as_manager()

def is_expired(self):
return self.expires and self.expires < timezone.now()

def is_valid(self):
return not self.is_expired()

def __str__(self):
return f"{self.qualification!s} {_('for')} {self.user!s}"

Expand Down
2 changes: 1 addition & 1 deletion ephios/core/services/mail/send.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging
from typing import List, Optional

from css_inline import css_inline
import css_inline
from django.conf import settings
from django.core.mail import SafeMIMEMultipart, SafeMIMEText
from django.template.loader import render_to_string
Expand Down
Loading