Skip to content
Merged
14 changes: 12 additions & 2 deletions ephios/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
Shift,
UserProfile,
)
from ephios.core.models.events import ParticipationComment
from ephios.core.services.qualification import collect_all_included_qualifications
from ephios.core.templatetags.settings_extras import make_absolute

Expand Down Expand Up @@ -152,6 +153,14 @@ class ConfidentialParticipantSerializer(PublicParticipantSerializer):
age = serializers.IntegerField(source="get_age")


class CommentSerializer(ModelSerializer):
author = serializers.CharField(source="author.display_name", read_only=True)

class Meta:
model = ParticipationComment
fields = ["author", "text", "created_at"]


class UserinfoParticipationSerializer(ModelSerializer):
state = ChoiceDisplayField(choices=AbstractParticipation.States.choices)
duration = serializers.SerializerMethodField(label=_("Duration in seconds"))
Expand All @@ -162,6 +171,7 @@ class UserinfoParticipationSerializer(ModelSerializer):
event = serializers.PrimaryKeyRelatedField(source="shift.event", read_only=True)
user = serializers.PrimaryKeyRelatedField(read_only=True, allow_null=True)
participant = ConfidentialParticipantSerializer(read_only=True)
comments = CommentSerializer(many=True, read_only=True)

def build_unknown_field(self, field_name, model_class):
if field_name in {"start_time", "end_time"}:
Expand All @@ -180,7 +190,7 @@ class Meta:
"event_title",
"event_type",
"state",
"comment",
"comments",
"start_time",
"end_time",
"duration",
Expand All @@ -199,4 +209,4 @@ class ParticipationSerializer(UserinfoParticipationSerializer):

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
del self.fields["comment"]
del self.fields["comments"]
3 changes: 2 additions & 1 deletion ephios/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
UserProfile,
WorkingHours,
)
from ephios.core.models.events import PlaceholderParticipation
from ephios.core.models.events import ParticipationComment, PlaceholderParticipation
from ephios.core.models.users import IdentityProvider

admin.site.register(UserProfile)
Expand All @@ -31,3 +31,4 @@
admin.site.register(PlaceholderParticipation)
admin.site.register(Notification)
admin.site.register(IdentityProvider)
admin.site.register(ParticipationComment)
87 changes: 87 additions & 0 deletions ephios/core/migrations/0035_participationcomment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Generated by Django 5.0.9 on 2025-01-05 12:37

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


def migrate_comment(apps, schema_editor):
ParticipationComment = apps.get_model("core", "ParticipationComment")
AbstractParticipation = apps.get_model("core", "AbstractParticipation")
db_alias = schema_editor.connection.alias
comments = []
for participation in AbstractParticipation.objects.using(db_alias).all():
if participation.comment:
comments.append(
ParticipationComment(participation=participation, text=participation.comment)
)
ParticipationComment.objects.using(db_alias).bulk_create(comments)


def revert_comments(apps, schema_editor):
ParticipationComment = apps.get_model("core", "ParticipationComment")
db_alias = schema_editor.connection.alias
for comment in ParticipationComment.objects.using(db_alias).all():
comment.participation.comment = comment.text
comment.participation.save()


class Migration(migrations.Migration):

dependencies = [
("core", "0034_alter_eventtype_show_participant_data"),
]

operations = [
migrations.CreateModel(
name="ParticipationComment",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
(
"visible_for",
models.IntegerField(
choices=[
(0, "responsibles only"),
(1, "responsibles and corresponding participant"),
(2, "everyone"),
],
default=0,
verbose_name="visible for",
),
),
("text", models.CharField(max_length=255, verbose_name="Comment")),
(
"authored_by_responsible",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
(
"created_at",
models.DateTimeField(auto_now_add=True),
),
(
"participation",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="comments",
to="core.abstractparticipation",
),
),
],
),
migrations.RunPython(migrate_comment, revert_comments),
migrations.RemoveField(
model_name="abstractparticipation",
name="comment",
),
]
32 changes: 28 additions & 4 deletions ephios/core/models/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,9 +273,6 @@ def labels_dict(cls):
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.
Expand All @@ -288,7 +285,6 @@ def has_customized_signup(self):
return bool(
self.individual_start_time
or self.individual_end_time
or self.comment
or self.shift.structure.has_customized_signup(self)
)

Expand Down Expand Up @@ -320,6 +316,34 @@ def is_in_positive_state(self):
)


class ParticipationComment(Model):
class Visibility(models.IntegerChoices):
RESPONSIBLES_ONLY = 0, _("responsibles only")
PARTICIPANT = 1, _("responsibles and corresponding participant")
PUBLIC = 2, _("everyone")

participation = models.ForeignKey(
AbstractParticipation, on_delete=models.CASCADE, related_name="comments"
)
authored_by_responsible = models.ForeignKey(
"UserProfile", on_delete=models.SET_NULL, blank=True, null=True
)
created_at = models.DateTimeField(auto_now_add=True)
visible_for = IntegerField(
_("visible for"), choices=Visibility.choices, default=Visibility.RESPONSIBLES_ONLY
)
text = models.CharField(_("Comment"), max_length=255)

@property
def author(self):
return self.authored_by_responsible or self.participation.participant

def __str__(self):
return _("Participation comment for {participation}").format(
participation=self.participation
)


class Shift(DatetimeDisplayMixin, Model):
event = ForeignKey(
Event, on_delete=models.CASCADE, related_name="shifts", verbose_name=_("event")
Expand Down
15 changes: 13 additions & 2 deletions ephios/core/signup/disposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
Shift,
UserProfile,
)
from ephios.core.models.events import ParticipationComment
from ephios.core.services.notifications.types import (
ParticipationCustomizationNotification,
ParticipationStateChangeNotification,
Expand All @@ -31,6 +32,9 @@ class MissingParticipation(ValueError):

class BaseDispositionParticipationForm(BaseParticipationForm):
disposition_participation_template = "core/disposition/fragment_participation.html"
comment_is_internal = forms.BooleanField(
label=_("Hide comment for participant"), required=False, label_suffix=""
)

def __init__(self, **kwargs):
try:
Expand All @@ -40,10 +44,16 @@ def __init__(self, **kwargs):

super().__init__(**kwargs)
self.can_delete = self.instance.state == AbstractParticipation.States.GETTING_DISPATCHED
self.fields["comment"].disabled = True

def get_comment_visibility(self):
return (
ParticipationComment.Visibility.RESPONSIBLES_ONLY
if self.cleaned_data["comment_is_internal"]
else ParticipationComment.Visibility.PARTICIPANT
)

class Meta(BaseParticipationForm.Meta):
fields = ["state", "individual_start_time", "individual_end_time", "comment"]
fields = ["state", "individual_start_time", "individual_end_time"]
widgets = {"state": forms.HiddenInput(attrs={"class": "state-input"})}


Expand Down Expand Up @@ -215,6 +225,7 @@ def get_formset(self):
self.request.POST or None,
queryset=self.object.participations.all(),
prefix="participations",
form_kwargs={"acting_user": self.request.user},
)
return formset

Expand Down
49 changes: 48 additions & 1 deletion ephios/core/signup/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
from crispy_forms.helper import FormHelper
from crispy_forms.layout import HTML, Field, Layout
from django import forms
from django.db import transaction
from django.utils.formats import date_format
from django.utils.timezone import localtime
from django.utils.translation import gettext_lazy as _

from ephios.core.models import AbstractParticipation, Shift
from ephios.core.models.events import ParticipationComment
from ephios.core.signup.flow.participant_validation import get_conflicting_participations
from ephios.core.signup.participants import AbstractParticipant
from ephios.core.widgets import PreviousCommentWidget
from ephios.extra.widgets import CustomSplitDateTimeWidget


Expand All @@ -21,6 +24,10 @@ class BaseParticipationForm(forms.ModelForm):
widget=CustomSplitDateTimeWidget,
required=False,
)
comment = forms.CharField(label=_("Comment"), max_length=255, required=False, label_suffix="")
comment_is_public = forms.BooleanField(
label=_("Make comment visible for other participants"), required=False, label_suffix=""
)

def clean_individual_start_time(self):
if self.cleaned_data["individual_start_time"] == self.shift.start_time:
Expand All @@ -32,27 +39,67 @@ def clean_individual_end_time(self):
return None
return self.cleaned_data["individual_end_time"]

def get_comment_visibility(self):
return (
ParticipationComment.Visibility.PUBLIC
if self.cleaned_data["comment_is_public"]
else ParticipationComment.Visibility.PARTICIPANT
)

def clean(self):
cleaned_data = super().clean()
cleaned_data["comment_visibility"] = self.get_comment_visibility()
if not self.errors:
start = cleaned_data["individual_start_time"] or self.shift.start_time
end = cleaned_data["individual_end_time"] or self.shift.end_time
if end < start:
self.add_error("individual_end_time", _("End time must not be before start time."))
return cleaned_data

@transaction.atomic
def save(self, commit=True):
result = super().save()
if comment := self.cleaned_data["comment"]:
ParticipationComment.objects.create(
participation=result,
text=comment,
authored_by_responsible=self.acting_user,
visible_for=self.get_comment_visibility(),
)
return result

class Meta:
model = AbstractParticipation
fields = ["individual_start_time", "individual_end_time", "comment"]
fields = ["individual_start_time", "individual_end_time"]

def __init__(self, *args, **kwargs):
instance = kwargs["instance"]
self.acting_user = kwargs.pop("acting_user", None)
kwargs["initial"] = {
**kwargs.get("initial", {}),
"individual_start_time": instance.individual_start_time or self.shift.start_time,
"individual_end_time": instance.individual_end_time or self.shift.end_time,
}
super().__init__(*args, **kwargs)
if self.instance.pk and self.instance.comments.exists():
self.fields["previous_comments"] = forms.CharField(
widget=PreviousCommentWidget(
comments=(
self.instance.comments.all()
if self.acting_user
and self.acting_user.has_perm(
"core.change_event", obj=self.instance.shift.event
)
else self.instance.comments.filter(
visible_for__in=[
ParticipationComment.Visibility.PUBLIC,
ParticipationComment.Visibility.PARTICIPANT,
]
)
)
),
required=False,
)

def get_customization_notification_info(self):
"""
Expand Down
19 changes: 16 additions & 3 deletions ephios/core/templates/core/disposition/fragment_participation.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ <h6 class="d-inline">
<button class="btn" type="button" data-bs-toggle="collapse"
data-bs-target="#participation-collapse-{{ form.instance.id }}" aria-expanded="false"
aria-controls="collapseExample">
{% if form.instance.comments.exists %}
<span class="fa fa-comment"></span> <span class="pe-2">{{ form.instance.comments.count }}</span>
{% endif %}
<span class="{% if form.instance.has_customized_signup %}text-dark{% else %}text-secondary{% endif %}">
<i class="fas fa-cog"></i>
<i class="fas fa-exclamation{% if not form.instance.has_customized_signup %} invisible{% endif %}"></i>
Expand All @@ -57,13 +60,23 @@ <h6 class="d-inline">
<div class="card-body">
{{ form.individual_start_time|as_crispy_field }}
{{ form.individual_end_time|as_crispy_field }}
{% if form.comment.initial %}
{{ form.comment|as_crispy_field }}
{% if form.previous_comments %}
{{ form.previous_comments|as_crispy_field }}
{% endif %}
<div class="mb-3">
<div class="form-label">
{{ form.comment.label_tag }}
<span class="float-end form-check">
<input type="checkbox" id="{{ form.comment_is_internal.auto_id }}" name="{{ form.comment_is_internal.html_name }}" class="form-check-input">
{{ form.comment_is_internal.label_tag }}
</span>
</div>
<input class="form-control" id="{{ form.comment.auto_id }}" name="{{ form.comment.html_name }}">
</div>
{% block participation_form %}
{% endblock %}
<div class="mb-n3"></div>
</div>
</div>
</div>
{% endblock %}
{% endblock %}
Loading