Skip to content

Commit

Permalink
Merge 09b3571 into d6f34b0
Browse files Browse the repository at this point in the history
  • Loading branch information
felixrindt authored Nov 4, 2021
2 parents d6f34b0 + 09b3571 commit daf0bde
Show file tree
Hide file tree
Showing 47 changed files with 976 additions and 601 deletions.
6 changes: 3 additions & 3 deletions ephios/core/forms/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
from guardian.shortcuts import assign_perm, get_objects_for_user, get_users_with_perms, remove_perm
from recurrence.forms import RecurrenceField

from ephios.core import signup
from ephios.core.dynamic_preferences_registry import event_type_preference_registry
from ephios.core.models import Event, EventType, LocalParticipation, Shift, UserProfile
from ephios.core.signup.methods import enabled_signup_methods
from ephios.core.widgets import MultiUserProfileWidget
from ephios.extra.crispy import AbortLink
from ephios.extra.permissions import get_groups_with_perms
Expand Down Expand Up @@ -171,7 +171,7 @@ class Meta:

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
signup_methods = list(signup.enabled_signup_methods())
signup_methods = list(enabled_signup_methods())
if self.instance and (method_slug := self.instance.signup_method_slug):
if method_slug not in map(operator.attrgetter("slug"), signup_methods):
signup_methods.append(self.instance.signup_method)
Expand Down Expand Up @@ -221,7 +221,7 @@ class EventDuplicationForm(forms.Form):
class EventTypeForm(forms.ModelForm):
class Meta:
model = EventType
fields = ["title", "can_grant_qualification", "color"]
fields = ["title", "color"]
widgets = {"color": ColorInput()}

def clean_color(self):
Expand Down
16 changes: 14 additions & 2 deletions ephios/core/ical.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db.models import Prefetch
from django.shortcuts import get_object_or_404
from django_ical.views import ICalFeed
from guardian.shortcuts import get_users_with_perms
Expand Down Expand Up @@ -48,9 +49,20 @@ def __init__(self, user):
super().__init__()
self.user = user

def item_start_datetime(self, item):
return item.participations.all()[0].start_time

def item_end_datetime(self, item):
return item.participations.all()[0].end_time

def items(self):
return self.user.get_shifts(
with_participation_state_in=[AbstractParticipation.States.CONFIRMED]
shift_ids = self.user.participations.filter(
state=AbstractParticipation.States.CONFIRMED
).values_list("shift", flat=True)
return (
Shift.objects.filter(pk__in=shift_ids)
.select_related("event")
.prefetch_related(Prefetch("participations", queryset=self.user.participations.all()))
)


Expand Down
47 changes: 47 additions & 0 deletions ephios/core/migrations/0014_auto_20211028_1125.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Generated by Django 3.2.8 on 2021-10-28 09:25

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("core", "0013_auto_20211027_2203"),
]

operations = [
migrations.RemoveField(
model_name="eventtype",
name="can_grant_qualification",
),
migrations.AddField(
model_name="abstractparticipation",
name="comment",
field=models.CharField(blank=True, max_length=255, verbose_name="Comment"),
),
migrations.AddField(
model_name="abstractparticipation",
name="individual_end_time",
field=models.DateTimeField(null=True, verbose_name="individual end time"),
),
migrations.AddField(
model_name="abstractparticipation",
name="individual_start_time",
field=models.DateTimeField(null=True, verbose_name="individual start time"),
),
migrations.AlterField(
model_name="abstractparticipation",
name="state",
field=models.IntegerField(
choices=[
(0, "requested"),
(1, "confirmed"),
(2, "declined by user"),
(3, "rejected by responsible"),
(4, "getting dispatched"),
],
default=4,
verbose_name="state",
),
),
]
105 changes: 79 additions & 26 deletions ephios/core/models/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@
SlugField,
TextField,
)
from django.db.models.functions import Coalesce
from django.utils import formats
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from dynamic_preferences.models import PerInstancePreferenceModel
from guardian.shortcuts import assign_perm
from polymorphic.managers import PolymorphicManager
from polymorphic.models import PolymorphicModel

from ephios.extra.json import CustomJSONDecoder, CustomJSONEncoder
Expand All @@ -31,7 +33,8 @@

if TYPE_CHECKING:
from ephios.core.models import UserProfile
from ephios.core.signup import AbstractParticipant, SignupStats
from ephios.core.signup import SignupStats
from ephios.core.signup.participants import AbstractParticipant


class ActiveManager(Manager):
Expand All @@ -41,7 +44,6 @@ def get_queryset(self):

class EventType(Model):
title = CharField(_("title"), max_length=254)
can_grant_qualification = BooleanField(_("can grant qualification"))
color = CharField(_("color"), max_length=7, default="#343a40")

class Meta:
Expand Down Expand Up @@ -102,7 +104,7 @@ def is_multi_day(self):

def get_signup_stats(self) -> "SignupStats":
"""Return a SignupStats object aggregated over all shifts of this event, or a default"""
from ephios.core.signup import SignupStats
from ephios.core.signup.methods import SignupStats

default_for_no_shifts = SignupStats.ZERO

Expand Down Expand Up @@ -138,7 +140,38 @@ def activate(self):
register_model_for_logging(Event, ModelFieldsLogConfig())


class AbstractParticipation(PolymorphicModel):
class ParticipationManager(PolymorphicManager):
def get_queryset(self):
return (
super()
.get_queryset()
.annotate(
start_time=Coalesce("individual_start_time", "shift__start_time"),
end_time=Coalesce("individual_end_time", "shift__end_time"),
)
)


class DatetimeDisplayMixin:
"""
Date and time formatting utilities used for start_time and end_time attributes
in AbstractParticipation and Shift.
"""

def get_date_display(self):
tz = pytz.timezone(settings.TIME_ZONE)
start_time = self.start_time.astimezone(tz)
return f"{formats.date_format(start_time, 'l')}, {formats.date_format(start_time, 'SHORT_DATE_FORMAT')}"

def get_time_display(self):
tz = pytz.timezone(settings.TIME_ZONE)
return f"{formats.time_format(self.start_time.astimezone(tz))} - {formats.time_format(self.end_time.astimezone(tz))}"

def get_datetime_display(self):
return f"{self.get_date_display()}, {self.get_time_display()}"


class AbstractParticipation(DatetimeDisplayMixin, PolymorphicModel):
class States(models.IntegerChoices):
REQUESTED = 0, _("requested")
CONFIRMED = 1, _("confirmed")
Expand All @@ -153,15 +186,29 @@ def labels_dict(cls):
shift = ForeignKey(
"Shift", on_delete=models.CASCADE, verbose_name=_("shift"), related_name="participations"
)
state = IntegerField(_("state"), choices=States.choices)
state = IntegerField(_("state"), choices=States.choices, default=States.GETTING_DISPATCHED)
data = models.JSONField(default=dict, verbose_name=_("Signup data"))

"""
Overwrites shift time. Use `start_time` and `end_time` to get the applicable time (implemented with a custom manager).
"""
individual_start_time = DateTimeField(_("individual start time"), null=True)
individual_end_time = DateTimeField(_("individual end time"), null=True)

# human readable comment
comment = models.CharField(_("Comment"), max_length=255, blank=True)

"""
The finished flag is used to make sure the participation_finished signal is only sent out once, even
if the shift time is changed afterwards.
"""
finished = models.BooleanField(default=False, verbose_name=_("finished"))

objects = ParticipationManager()

def has_customized_signup(self):
return bool(self.individual_start_time or self.individual_end_time or self.comment)

@property
def hours_value(self):
td = self.shift.end_time - self.shift.start_time
Expand All @@ -180,14 +227,17 @@ def __str__(self):
except NotImplementedError:
return super().__str__()

def is_in_positive_state(self):
return self.state in {self.States.CONFIRMED, self.States.REQUESTED}


PARTICIPATION_LOG_CONFIG = ModelFieldsLogConfig(
unlogged_fields=["id", "data", "abstractparticipation_ptr"],
attach_to_func=lambda instance: (Event, instance.shift.event_id),
)


class Shift(Model):
class Shift(DatetimeDisplayMixin, Model):
event = ForeignKey(
Event, on_delete=models.CASCADE, related_name="shifts", verbose_name=_("event")
)
Expand All @@ -207,21 +257,17 @@ class Meta:

@property
def signup_method(self):
from ephios.core.signup import signup_method_from_slug
from ephios.core.signup.methods import signup_method_from_slug

try:
return signup_method_from_slug(self.signup_method_slug, self)
event = self.event
except Event.DoesNotExist:
event = None
try:
return signup_method_from_slug(self.signup_method_slug, self, event=event)
except ValueError:
return None

def get_start_end_time_display(self):
tz = pytz.timezone(settings.TIME_ZONE)
start_time = self.start_time.astimezone(tz)
return (
f"{formats.date_format(start_time, 'l')}, {formats.date_format(start_time, 'SHORT_DATE_FORMAT')}, "
+ f"{formats.time_format(start_time)} - {formats.time_format(self.end_time.astimezone(tz))}"
)

def get_participants(self, with_state_in=frozenset({AbstractParticipation.States.CONFIRMED})):
for participation in self.participations.filter(state__in=with_state_in):
yield participation.participant
Expand All @@ -230,7 +276,7 @@ def get_absolute_url(self):
return f"{self.event.get_absolute_url()}#shift-{self.pk}"

def __str__(self):
return f"{self.event.title} ({self.get_start_end_time_display()})"
return f"{self.event.title} ({self.get_datetime_display()})"


class ShiftLogConfig(ModelFieldsLogConfig):
Expand All @@ -243,21 +289,28 @@ def object_to_attach_logentries_to(self, instance):
def initial_log_recorders(self, instance):
# pylint: disable=undefined-variable
yield from super().initial_log_recorders(instance)
yield DerivedFieldsLogRecorder(
lambda shift: {
_("Signup method"): str(method.verbose_name)
if (method := shift.signup_method)
else None
}
)

def get_signup_method_name_mapping(shift):
from ephios.core.signup.methods import installed_signup_methods

v = None
for method in installed_signup_methods():
if method.slug == shift.signup_method_slug:
v = str(method.verbose_name)
return {_("Signup method"): v}

yield DerivedFieldsLogRecorder(get_signup_method_name_mapping)


register_model_for_logging(Shift, ShiftLogConfig())


class LocalParticipation(AbstractParticipation):
user: "UserProfile" = ForeignKey(
"UserProfile", on_delete=models.CASCADE, verbose_name=_("Participant")
"UserProfile",
on_delete=models.CASCADE,
verbose_name=_("Participant"),
related_name="participations",
)

def save(self, *args, **kwargs):
Expand Down Expand Up @@ -289,7 +342,7 @@ class PlaceholderParticipation(AbstractParticipation):
@property
def participant(self) -> "AbstractParticipant":
from ephios.core.models.users import Qualification
from ephios.core.signup.methods import PlaceholderParticipant
from ephios.core.signup.participants import PlaceholderParticipant

return PlaceholderParticipant(
first_name=self.first_name,
Expand Down
16 changes: 4 additions & 12 deletions ephios/core/models/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ def is_minor(self):
return self.age < 18

def as_participant(self):
from ephios.core.signup import LocalUserParticipant
from ephios.core.signup.participants import LocalUserParticipant

return LocalUserParticipant(
first_name=self.first_name,
Expand All @@ -150,25 +150,17 @@ def qualifications(self):
expires=Max(F("grants__expires"), filter=Q(grants__user=self)),
)

def get_shifts(self, with_participation_state_in):
from ephios.core.models import Shift

shift_ids = self.localparticipation_set.filter(
state__in=with_participation_state_in
).values_list("shift", flat=True)
return Shift.objects.filter(pk__in=shift_ids).select_related("event")

def get_workhour_items(self):
from ephios.core.models import AbstractParticipation

participations = (
self.localparticipation_set.filter(state=AbstractParticipation.States.CONFIRMED)
self.participations.filter(state=AbstractParticipation.States.CONFIRMED)
.annotate(
duration=ExpressionWrapper(
(F("shift__end_time") - F("shift__start_time")),
(F("end_time") - F("start_time")),
output_field=models.DurationField(),
),
date=ExpressionWrapper(TruncDate(F("shift__start_time")), output_field=DateField()),
date=ExpressionWrapper(TruncDate(F("start_time")), output_field=DateField()),
reason=F("shift__event__title"),
)
.values("duration", "date", "reason")
Expand Down
2 changes: 1 addition & 1 deletion ephios/core/pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def get_story(self):

for shift in self.event.shifts.all():
story.append(Spacer(height=1 * cm, width=19 * cm))
story.append(Paragraph(shift.get_start_end_time_display(), self.style["Heading2"]))
story.append(Paragraph(shift.get_datetime_display(), self.style["Heading2"]))
data = [
[_("Meeting time"), formats.time_format(shift.meeting_time.astimezone(tz))],
] + [
Expand Down
2 changes: 1 addition & 1 deletion ephios/core/services/notifications/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from ephios.core.models import AbstractParticipation, Event, LocalParticipation, UserProfile
from ephios.core.models.users import Consequence, Notification
from ephios.core.signals import register_notification_types
from ephios.core.signup import LocalUserParticipant
from ephios.core.signup.participants import LocalUserParticipant


def installed_notification_types():
Expand Down
1 change: 0 additions & 1 deletion ephios/core/signup/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
from .methods import * # no-qa
Loading

0 comments on commit daf0bde

Please sign in to comment.