Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
62ebc57
add individual startend time fields
felixrindt Oct 3, 2021
235583f
save start_time in perform_signup
felixrindt Oct 3, 2021
70fad83
implement signup logic
felixrindt Oct 9, 2021
a488427
refactor to participants.py
felixrindt Oct 9, 2021
9cfe343
let participants customize participation after signup
felixrindt Oct 10, 2021
e49939e
add comment field on participations
felixrindt Oct 10, 2021
0376511
reduce queries on event list
felixrindt Oct 10, 2021
2af49b6
refactor form save
felixrindt Oct 10, 2021
58cfb8b
use icon to customize initial signup
felixrindt Oct 10, 2021
734ba33
refactor signup method configuration to form classes
felixrindt Oct 27, 2021
0869cda
fix shift conflict detection
felixrindt Oct 27, 2021
5bf7fac
refactor Participation Forms
felixrindt Oct 27, 2021
b9cc40f
add form to disposition view
felixrindt Oct 27, 2021
e185b04
disallow deviating shift times
felixrindt Oct 27, 2021
d69e942
count workinghours correctly
felixrindt Oct 27, 2021
0a19b45
fix section-based signup
felixrindt Oct 27, 2021
b04eac2
fix no-selfservice
felixrindt Oct 27, 2021
258a1c6
refactor customization button
felixrindt Oct 27, 2021
5c23fae
rebase migrations
felixrindt Oct 28, 2021
6e61f8a
update ical view
felixrindt Oct 29, 2021
233c42c
show comment and individual times in event detail
felixrindt Oct 29, 2021
145e9cc
highlight individual times in shift box
felixrindt Oct 29, 2021
710ae57
update home view, refactoring
felixrindt Oct 30, 2021
0a9c617
fix postgres error
felixrindt Oct 31, 2021
6f0462a
update section based form
felixrindt Nov 6, 2021
8fdccef
Fix #524
felixrindt Nov 6, 2021
56ce2d7
Add responsible notification for customization of participation
felixrindt Nov 7, 2021
08cb538
move templates
felixrindt Nov 8, 2021
56dd935
basic individual times validation
felixrindt Nov 8, 2021
520aa00
add participation customization notification
felixrindt Nov 8, 2021
7b869c5
rework conflict checks
felixrindt Nov 20, 2021
ded0273
coupled signup renders simple participation pills
felixrindt Nov 20, 2021
c7b53d4
rework exception classes
felixrindt Nov 20, 2021
b563cd0
address review
felixrindt Nov 21, 2021
3378a3c
Refactor event detail templates
felixrindt Nov 26, 2021
a7b0e33
refactor shift state rendering
felixrindt Nov 26, 2021
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
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
84 changes: 84 additions & 0 deletions ephios/core/migrations/0014_auto_20211106_1852.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Generated by Django 3.2.9 on 2021-11-06 17:52

import django.db.models.deletion
import django.db.models.manager
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

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

operations = [
migrations.AlterModelOptions(
name="event",
options={
"base_manager_name": "all_objects",
"default_manager_name": "objects",
"permissions": [("view_past_event", "Can view past events")],
"verbose_name": "event",
"verbose_name_plural": "events",
},
),
migrations.AlterModelManagers(
name="event",
managers=[
("objects", django.db.models.manager.Manager()),
("all_objects", django.db.models.manager.Manager()),
],
),
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.AddField(
model_name="notification",
name="created_at",
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
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",
),
),
migrations.AlterField(
model_name="localparticipation",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="participations",
to=settings.AUTH_USER_MODEL,
verbose_name="Participant",
),
),
]
119 changes: 90 additions & 29 deletions ephios/core/models/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
from datetime import timedelta
from typing import TYPE_CHECKING

import pytz
from django.conf import settings
from django.db import models, transaction
from django.db.models import (
BooleanField,
Expand All @@ -18,11 +16,14 @@
SlugField,
TextField,
)
from django.db.models.functions import Coalesce
from django.utils import formats
from django.utils.text import slugify
from django.utils.timezone import localtime
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 +32,8 @@

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


class ActiveManager(Manager):
Expand All @@ -41,7 +43,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 +103,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 +139,42 @@ 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_start_time(self):
return self.start_time

def get_end_time(self):
return self.end_time

def get_date_display(self):
start_time = localtime(self.get_start_time())
return f"{formats.date_format(start_time, 'l')}, {formats.date_format(start_time, 'SHORT_DATE_FORMAT')}"

def get_time_display(self):
return f"{formats.time_format(localtime(self.get_start_time()))} - {formats.time_format(localtime(self.get_end_time()))}"

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 +189,34 @@ 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
or self.shift.signup_method.has_customized_signup(self)
)

@property
def hours_value(self):
td = self.shift.end_time - self.shift.start_time
Expand All @@ -180,14 +235,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 @@ -206,22 +264,18 @@ class Meta:
db_table = "shift"

@property
def signup_method(self):
from ephios.core.signup import signup_method_from_slug
def signup_method(self) -> "BaseSignupMethod":
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 +284,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 +297,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 +350,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
Loading