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

allow multiple comments per participation #1457

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
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)
84 changes: 84 additions & 0 deletions ephios/core/migrations/0034_participationcomment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# 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")
AbstractParticipation = apps.get_model("core", "AbstractParticipation")
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", "0033_eventtype_show_participant_data"),
]

operations = [
migrations.CreateModel(
name="ParticipationComment",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
(
"visibile_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, default=django.utils.timezone.now),
),
(
"participation",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="comments",
to="core.abstractparticipation",
),
),
],
),
migrations.RunPython(migrate_comment, revert_comments),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.0.9 on 2025-01-05 12:52

from django.db import migrations


class Migration(migrations.Migration):

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

operations = [
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 @@ -239,9 +239,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 @@ -254,7 +251,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 @@ -286,6 +282,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)
visibile_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
9 changes: 7 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,7 @@ 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)

def __init__(self, **kwargs):
try:
Expand All @@ -40,10 +42,12 @@ 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 +219,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
25 changes: 23 additions & 2 deletions ephios/core/signup/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
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.extra.widgets import CustomSplitDateTimeWidget
from ephios.extra.widgets import CustomSplitDateTimeWidget, PreviousCommentWidget


class BaseParticipationForm(forms.ModelForm):
Expand All @@ -21,6 +22,9 @@ class BaseParticipationForm(forms.ModelForm):
widget=CustomSplitDateTimeWidget,
required=False,
)
comment = forms.CharField(label=_("Comment"), max_length=255, required=False)
comment_is_public = forms.BooleanField(label=_("Make comment visible for other participants"), required=False)
comment_visibility = forms.ChoiceField(choices=ParticipationComment.Visibility, required=False, widget=forms.HiddenInput)

def clean_individual_start_time(self):
if self.cleaned_data["individual_start_time"] == self.shift.start_time:
Expand All @@ -32,27 +36,44 @@ 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

def save(self, commit=True):
result = super().save(commit)
if comment := self.cleaned_data["comment"]:
ParticipationComment.objects.create(
participation=result, text=comment, authored_by_responsible=self.acting_user, visibile_for=self.cleaned_data["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 and self.instance.comments.exists():
self.fields["previous_comments"] = forms.CharField(
widget=PreviousCommentWidget(comments=self.instance.comments.all()), required=False
)

def get_customization_notification_info(self):
"""
Expand Down
18 changes: 15 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,22 @@ <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="form-row">
<div class="form-group col-md-10">
{{ form.comment|as_crispy_field }}
</div>
<div class="form-group col-md-2">
<input type="checkbox" class="btn-check" id="{{ form.comment_is_internal.auto_id }}" name="{{ form.comment_is_internal.html_name }}" checked autocomplete="off">
<label class="btn btn-secondary" for="btn-check-2"><span class="fa fa-eye-slash"></span></label>
</div>
</div>
{% block participation_form %}
{% endblock %}
<div class="mb-n3"></div>
</div>
</div>
</div>
{% endblock %}
{% endblock %}
32 changes: 18 additions & 14 deletions ephios/core/views/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,22 +446,26 @@ def get_queryset(self):
base = Event.objects.all()
if self.request.user.has_perm("core.add_event"):
base = Event.all_objects.all()
return base.prefetch_related("shifts").prefetch_related(
Prefetch(
"shifts__participations",
queryset=AbstractParticipation.objects.all().annotate(
show_participant_data=Case(
When(
id__in=AbstractParticipation.objects.all().viewable_by(
self.request.user
return (
base.prefetch_related("shifts")
.prefetch_related(
Prefetch(
"shifts__participations",
queryset=AbstractParticipation.objects.all().annotate(
show_participant_data=Case(
When(
id__in=AbstractParticipation.objects.all().viewable_by(
self.request.user
),
then=True,
),
then=True,
),
default=False,
output_field=BooleanField(),
)
),
default=False,
output_field=BooleanField(),
)
),
)
)
.prefetch_related("shifts__participations__comments")
)

def get_context_data(self, **kwargs):
Expand Down
10 changes: 10 additions & 0 deletions ephios/extra/templates/extra/widgets/previous_comments.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{% load i18n %}
<div>
{% for comment in comments %}
<p class="mt-0 mb-0 d-flex">
<span class="pe-1"><b>{{ comment.author }}</b></span>
<span class="flex-grow-1">{{ comment.text }}</span>
<span>{% blocktranslate with comment.created_at|timesince as time %}{{ time }} ago{% endblocktranslate %}</span>
</p>
{% endfor %}
</div>
15 changes: 14 additions & 1 deletion ephios/extra/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from django.core.exceptions import ValidationError
from django.forms import CharField, DateInput, MultiWidget, Textarea, TimeInput
from django.forms.utils import to_current_timezone
from django.forms.widgets import Input
from django.forms.widgets import Input, Widget
from django.utils.translation import gettext as _


Expand Down Expand Up @@ -72,3 +72,16 @@ def clean(self, value):
return rrulestr(value, ignoretz=True)
except (TypeError, KeyError, ValueError) as e:
raise ValidationError(_("Invalid recurrence rule: {error}").format(error=e)) from e


class PreviousCommentWidget(Widget):
template_name = "extra/widgets/previous_comments.html"

def __init__(self, *args, **kwargs):
self.comments = kwargs.pop("comments")
super().__init__(*args, **kwargs)

def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
context["comments"] = self.comments
return context
Loading
Loading