Skip to content

Commit

Permalink
Add filter to UserProfileListView (#1038)
Browse files Browse the repository at this point in the history
* add filter to UserProfileListView

* auto-overflow for qualification list

* fix test

* add default pagination size

* deprecate relevant qualification categories preference

* add show_with_user to category form

* refactor getting qualification abbreviations

* refactor qualification graph

* only show essential qualifications

* refactor QualificationUniverse

* use contextvars

* use QualificationUniverse in more places

* userlist use qualification badges

* restyle group list

* fix DOM text reinterpreted as HTML

* dont log permission changes on userprofile

* left align user labels

* add userlist tests

* correct css order

* address review

* fix the graph

* user singlemodelchoice fields in user filter

* trim user list in group list view

* better differentiation of top_level vs essential qualification

* update is_management_group label/help text
  • Loading branch information
felixrindt authored Sep 1, 2023
1 parent 4750409 commit 567ae17
Show file tree
Hide file tree
Showing 37 changed files with 1,040 additions and 303 deletions.
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):
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

0 comments on commit 567ae17

Please sign in to comment.