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 responsible indicator #1452

Merged
merged 10 commits into from
Jan 5, 2025
Merged
27 changes: 18 additions & 9 deletions ephios/core/forms/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ class EventForm(forms.ModelForm):
queryset=Group.objects.none(),
label=_("Visible for"),
help_text=_(
"Select groups which the event shall be visible for. Regardless, the event will be visible for users that already signed up."
"Select groups which the event shall be visible for. Regardless, the event will be "
"visible for responsible groups and users that already signed up."
),
widget=Select2MultipleWidget,
required=False,
Expand All @@ -48,6 +49,7 @@ class EventForm(forms.ModelForm):
queryset=UserProfile.objects.all(),
required=False,
label=_("Responsible persons"),
help_text=_("Individuals can also be made responsible for an event."),
widget=MultiUserProfileWidget,
)
responsible_groups = forms.ModelMultipleChoiceField(
Expand All @@ -64,16 +66,17 @@ class Meta:
def __init__(self, **kwargs):
user = kwargs.pop("user")
can_publish_for_groups = get_objects_for_user(user, "publish_event_for_group", klass=Group)

if (event := kwargs.get("instance", None)) is not None:
self.eventtype = event.type
responsible_users = get_users_with_perms(
event, only_with_perms_in=["change_event"], with_group_users=False
)
responsible_groups = get_groups_with_perms(event, only_with_perms_in=["change_event"])
visible_for = get_groups_with_perms(event, only_with_perms_in=["view_event"]).exclude(
id__in=responsible_groups
responsible_groups = get_groups_with_perms(
event, only_with_perms_in=["change_event"], accept_global_perms=False
)
visible_for = get_groups_with_perms(
event, only_with_perms_in=["view_event"], accept_global_perms=False
).exclude(id__in=responsible_groups)

self.locked_visible_for_groups = set(visible_for.exclude(id__in=can_publish_for_groups))
kwargs["initial"] = {
Expand Down Expand Up @@ -101,13 +104,19 @@ def __init__(self, **kwargs):
self.fields["visible_for"].queryset = can_publish_for_groups
self.fields["visible_for"].disabled = not can_publish_for_groups
if self.locked_visible_for_groups:
self.fields["visible_for"].help_text = _(
"Select groups which the event shall be visible for. "
"This event is also visible for <b>{groups}</b>, "
"but you don't have the permission to change visibility "
self.fields["visible_for"].help_text += " " + _(
"Also, this event is visible to <b>{groups}</b>, "
"but you don't have permission to change visibility "
"for those groups."
).format(groups=", ".join(group.name for group in self.locked_visible_for_groups))

groups_with_global_change_permissions = get_groups_with_perms(
None, only_with_perms_in=["core.change_event"]
)
self.fields["responsible_groups"].help_text = _(
"This event is always editable by <b>{groups}</b>, because they manage ephios."
).format(groups=", ".join(group.name for group in groups_with_global_change_permissions))

def save(self, commit=True):
if not self.instance.pk:
self.instance.type = self.eventtype
Expand Down
6 changes: 5 additions & 1 deletion ephios/core/models/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from datetime import timedelta
from typing import TYPE_CHECKING

from django.contrib.contenttypes.fields import GenericRelation
from django.db import models, transaction
from django.db.models import (
BooleanField,
Expand All @@ -24,7 +25,7 @@
from django.utils.translation import gettext_lazy as _
from django.utils.translation import pgettext
from dynamic_preferences.models import PerInstancePreferenceModel
from guardian.shortcuts import assign_perm, get_objects_for_user
from guardian.shortcuts import GroupObjectPermission, assign_perm, get_objects_for_user
from polymorphic.managers import PolymorphicManager
from polymorphic.models import PolymorphicModel
from polymorphic.query import PolymorphicQuerySet
Expand Down Expand Up @@ -78,6 +79,9 @@ class Event(Model):
location = CharField(_("location"), max_length=254)
type = ForeignKey(EventType, on_delete=models.CASCADE, verbose_name=_("event type"))
active = BooleanField(default=False, verbose_name=_("active"))
group_object_permission_set = GenericRelation(
GroupObjectPermission, object_id_field="object_pk"
) # GenericRelation allows us to query Groups that have object permissions for this model in a prefetch

objects = ActiveManager()
all_objects = Manager()
Expand Down
11 changes: 11 additions & 0 deletions ephios/core/templates/core/event_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,17 @@ <h5 class="card-subtitle mb-2 text-body-secondary fw-bolder lh-base">
<div class="card-text text-body-secondary">
{{ event.description|rich_text:"h1,h2" }}
</div>
{% if can_change_event %}
<div class="card-text text-body-secondary mb-2">
{% if not visible_for %}
<i class="fas fa-eye-slash text-warning"></i>
{% translate "Event has not been made visible to any groups." %}
{% else %}
<i class="fas fa-eye"></i>
{% translate "Viewable by" %} {{ visible_for }}.
{% endif %}
</div>
{% endif %}
<div class="event-plugin-content">
{% event_plugin_content event request as plugin_content %}
{% for item in plugin_content %}
Expand Down
111 changes: 69 additions & 42 deletions ephios/core/templates/core/fragments/event_list_list_mode.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,25 +28,26 @@
{% with counter=event|event_list_signup_state_counts stats=event.get_signup_stats %}
<li class="list-group-item p-0 d-flex flex-row">
<div class="m-0 py-2 d-flex flex-column flex-lg-row-reverse justify-content-around flex-grow-0">
<div class="ps-lg-2 d-flex flex-row flex-lg-column justify-content-center event-list-status-icon"
{% if counter %}
data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-html="true"
title="{% for state in counter %}<div>
{% if event.shifts.all|length > 1 %}
{{ counter|get:state }} {% endif %}{{ ParticipationStates.labels_dict|get:state }}
</div>{% endfor %}"
{% endif %}>
{% if counter|get:ParticipationStates.CONFIRMED > 0 %}
<span class="text-success fa fa-user-check ps-2"></span>
{% elif counter|get:ParticipationStates.REQUESTED > 0 %}
<span class="text-warning fa fa-user-clock ps-2"></span>
{% elif counter|get:ParticipationStates.RESPONSIBLE_REJECTED > 0 %}
<span class="text-danger fa fa-user-times ps-2"></span>
{% elif counter|get:ParticipationStates.USER_DECLINED > 0 and counter|length == 1 %}
<span class="text-danger fa fa-user-minus ps-2"></span>
{% else %}
<span class="text-body-secondary far fa-user ps-2"></span>
{% endif %}
<div class="ps-lg-2 d-flex flex-column justify-content-center event-list-status-icon">
<span {% if counter %}
data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-html="true"
title="{% for state in counter %}<div>
{% if event.shifts.all|length > 1 %}
{{ counter|get:state }} {% endif %}{{ ParticipationStates.labels_dict|get:state }}
</div>{% endfor %}"
{% endif %}>
{% if counter|get:ParticipationStates.CONFIRMED > 0 %}
<i class="text-{% if event.can_change %}info{% else %}success{% endif %} fa fa-user-check ps-2"></i>
{% elif counter|get:ParticipationStates.REQUESTED > 0 %}
<i class="text-{% if event.can_change %}info{% else %}warning{% endif %} fa fa-user-clock ps-2"></i>
{% elif counter|get:ParticipationStates.RESPONSIBLE_REJECTED > 0 %}
<i class="text-{% if event.can_change %}info{% else %}danger{% endif %} fa fa-user-times ps-2"></i>
{% elif counter|get:ParticipationStates.USER_DECLINED > 0 and counter|length == 1 %}
<i class="text-{% if event.can_change %}info{% else %}danger{% endif %} fa fa-user-minus ps-2"></i>
{% else %}
<i class="text-{% if event.can_change %}info{% else %}body-secondary{% endif %} far fa-user ps-2"></i>
{% endif %}
</span>
</div>
{% if perms.core.add_event %}
<div class="ps-lg-2 d-flex flex-row flex-lg-column justify-content-center">
Expand All @@ -68,33 +69,59 @@ <h5 class="mb-0 text-break">
<a class="grid-badge event-list-item-link" href="{{ event.get_absolute_url }}">
<span class="badge badge-eventtype eventtype-{{ event.type.pk }}-color">{{ event.type }}</span>
</a>
<a class="grid-signup d-flex flex-column align-items-end justify-content-center event-list-item-link"
<a class="grid-signup d-flex flex-row justify-content-end event-list-item-link w-100"
href="{{ event.get_absolute_url }}">
<div class="position-relative">
<span class="fas fa-users"></span>
{% if event.pending_disposition_count %}
<span class="pending-indicator"></span>
{% endif %}
{{ stats.confirmed_count }}
</div>
<div>
{% include "core/fragments/signup_stats_indicator.html" with stats=stats %}
<div class="d-flex flex-column align-items-end justify-content-center">
<div>
<span data-bs-toggle="tooltip" data-bs-placement="top"
title="{% if event|viewable_by %}
{% translate "Viewable by" %} {{ event|viewable_by }}.
{% else %}
{% translate "Event has not been made visible to any groups." %}
{% endif %}
{% if event.pending_disposition_count %}
{% blocktranslate trimmed count waiting=event.pending_disposition_count %}
{{ waiting }} participation is awaiting disposition.
{% plural %}
{{ waiting }} participations are awaiting disposition.
{% endblocktranslate %}
{% endif %}
{% blocktranslate trimmed count confirmed=stats.confirmed_count %}
{{ confirmed }} confirmed participation.
{% plural %}
{{ confirmed }} confirmed participations.
{% endblocktranslate %}">
{% if not event|viewable_by %}
<i class="fas fa-eye-slash pe-1 text-warning"></i>
{% endif %}
{% if event.pending_disposition_count %}
<i class="fas fa-hourglass-half pe-1 text-warning"></i>
{% endif %}
<i class="fas fa-users"></i>
{{ stats.confirmed_count }}
</span>
</div>
<div>
{% include "core/fragments/signup_stats_indicator.html" with stats=stats %}
</div>
</div>
</a>
<a class="grid-time event-list-item-link" href="{{ event.get_absolute_url }}">
{{ event.start_time|date:"D" }},
{% if event.start_time.date == event.end_time.date %}
{{ event.start_time|date:"SHORT_DATE_FORMAT" }}
<span class="d-lg-none">,</span>
<span class="d-none d-lg-inline"><br></span>
{{ event.start_time|date:"TIME_FORMAT" }} –
{{ event.end_time|date:"TIME_FORMAT" }}
{% else %}
{{ event.start_time|date:"SHORT_DATE_FORMAT" }}
<span class="d-none d-lg-inline"><br></span>
{% translate "to" %}
{{ event.end_time|date:"SHORT_DATE_FORMAT" }}
{% endif %}
<div>
{{ event.start_time|date:"D" }},
{% if event.start_time.date == event.end_time.date %}
{{ event.start_time|date:"SHORT_DATE_FORMAT" }}
<span class="d-lg-none">,</span>
<span class="d-none d-lg-inline"><br></span>
{{ event.start_time|date:"TIME_FORMAT" }} –
{{ event.end_time|date:"TIME_FORMAT" }}
{% else %}
{{ event.start_time|date:"SHORT_DATE_FORMAT" }}
<span class="d-none d-lg-inline"><br></span>
{% translate "to" %}
{{ event.end_time|date:"SHORT_DATE_FORMAT" }}
{% endif %}
</div>
</a>
<div class="grid-action d-none d-lg-flex flex-column justify-content-center">
</div>
Expand Down
11 changes: 11 additions & 0 deletions ephios/core/templatetags/event_extras.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,14 @@ def can_do_disposition_for(user: UserProfile, shift):
user.has_perm("change_event", shift.event)
and shift.structure.disposition_participation_form_class is not None
)


@register.filter
def viewable_by(event, allow_db=False):
"""
For an event from the EventDetailView queryset, return a string of comma-seperated group names
of groups that are considered to be able to view the event like displayed in the event form.
"""
names_can_view = {perm.group.name for perm in event.view_permissions}
names_can_change = {perm.group.name for perm in event.change_permissions}
return ", ".join(names_can_view - names_can_change)
31 changes: 28 additions & 3 deletions ephios/core/views/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
UpdateView,
)
from django.views.generic.detail import SingleObjectMixin
from guardian.models import GroupObjectPermission
from guardian.shortcuts import assign_perm, get_objects_for_user, get_users_with_perms

from ephios.core.calendar import ShiftCalendar
Expand Down Expand Up @@ -210,7 +211,7 @@ def get_queryset(self):
),
default=False,
output_field=BooleanField(),
)
),
)
.annotate(
pending_disposition_count=Count(
Expand All @@ -232,14 +233,31 @@ def get_queryset(self):
},
)
.select_related("type")
.prefetch_related("shifts")
.prefetch_related(Prefetch("shifts__participations"))
)
if self.filter_form.is_valid():
qs = self.filter_form.filter_events(qs)
else:
# safeguard for not loading too many events
qs = qs.filter(end_time__gte=timezone.now()).order_by("start_time", "end_time")
qs = qs.prefetch_related("shifts__participations")

# annotate groups that can view the event by prefetching the object permission model
qs = qs.prefetch_related(
Prefetch(
"group_object_permission_set",
queryset=GroupObjectPermission.objects.filter(
permission__codename="view_event"
).select_related("group"),
to_attr="view_permissions",
),
Prefetch(
"group_object_permission_set",
queryset=GroupObjectPermission.objects.filter(
permission__codename="change_event"
).select_related("group"),
to_attr="change_permissions",
),
)
return qs

@cached_property
Expand Down Expand Up @@ -466,6 +484,13 @@ def get_queryset(self):

def get_context_data(self, **kwargs):
kwargs["can_change_event"] = self.request.user.has_perm("core.change_event", self.object)
responsible_groups = get_groups_with_perms(
self.object, only_with_perms_in=["change_event"], accept_global_perms=False
)
visible_for = get_groups_with_perms(
self.object, only_with_perms_in=["view_event"], accept_global_perms=False
).exclude(id__in=responsible_groups)
kwargs["visible_for"] = ", ".join(group.name for group in visible_for)
return super().get_context_data(**kwargs)


Expand Down
Loading
Loading