Skip to content
Open
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
9 changes: 9 additions & 0 deletions tapir/accounts/tests/factories/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ def is_employee(self: TapirUser, create, is_employee):

set_group_membership([self], settings.GROUP_EMPLOYEES, is_employee)

@factory.post_generation
def is_welcome_desk_account(self: TapirUser, create, is_welcome_desk_account):
if not create:
return

set_group_membership(
[self], settings.GROUP_WELCOME_DESK, is_welcome_desk_account
)

@factory.post_generation
def shift_capabilities(self: TapirUser, create, shift_capabilities, **kwargs):
if not create:
Expand Down
4 changes: 2 additions & 2 deletions tapir/coop/tests/membership_resignation/test_create_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@ def setUp(self) -> None:
self.given_feature_flag_value(feature_flag_membership_resignation, True)
mock_timezone_now(self, self.NOW)

def get_allowed_groups(self):
def permission_test_get_allowed_groups(self):
return [
settings.GROUP_VORSTAND,
settings.GROUP_EMPLOYEES,
settings.GROUP_MEMBER_OFFICE,
]

def do_request(self):
def permission_test_do_request(self):
return self.client.get(reverse("coop:membership_resignation_create"))

def test_membershipResignationCreateView_featureFlagDisabled_accessDenied(self):
Expand Down
4 changes: 2 additions & 2 deletions tapir/coop/tests/membership_resignation/test_delete_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ def setUp(self) -> None:
super().setUp()
self.given_feature_flag_value(feature_flag_membership_resignation, True)

def get_allowed_groups(self):
def permission_test_get_allowed_groups(self):
return [
settings.GROUP_VORSTAND,
settings.GROUP_EMPLOYEES,
settings.GROUP_MEMBER_OFFICE,
]

def do_request(self):
def permission_test_do_request(self):
resignation: MembershipResignation = MembershipResignationFactory.create()
return self.client.post(
reverse("coop:membership_resignation_delete", args=[resignation.id]),
Expand Down
4 changes: 2 additions & 2 deletions tapir/coop/tests/membership_resignation/test_detail_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ class TestMembershipResignationDetailView(
PermissionTestMixin, FeatureFlagTestMixin, TapirFactoryTestBase
):

def get_allowed_groups(self):
def permission_test_get_allowed_groups(self):
return [
settings.GROUP_VORSTAND,
settings.GROUP_EMPLOYEES,
settings.GROUP_MEMBER_OFFICE,
settings.GROUP_ACCOUNTING,
]

def do_request(self):
def permission_test_do_request(self):
self.given_feature_flag_value(feature_flag_membership_resignation, True)
resignation: MembershipResignation = MembershipResignationFactory.create()
return self.client.get(
Expand Down
4 changes: 2 additions & 2 deletions tapir/coop/tests/membership_resignation/test_edit_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ def setUp(self) -> None:
self.given_feature_flag_value(feature_flag_membership_resignation, True)
mock_timezone_now(self, self.NOW)

def get_allowed_groups(self):
def permission_test_get_allowed_groups(self):
return [
settings.GROUP_VORSTAND,
settings.GROUP_EMPLOYEES,
settings.GROUP_MEMBER_OFFICE,
]

def do_request(self):
def permission_test_do_request(self):
resignation: MembershipResignation = MembershipResignationFactory.create()
return self.client.get(
reverse("coop:membership_resignation_edit", args=[resignation.id])
Expand Down
4 changes: 2 additions & 2 deletions tapir/coop/tests/membership_resignation/test_list_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ class TestMembershipResignationListView(
PermissionTestMixin, FeatureFlagTestMixin, TapirFactoryTestBase
):

def get_allowed_groups(self):
def permission_test_get_allowed_groups(self):
return [
settings.GROUP_VORSTAND,
settings.GROUP_EMPLOYEES,
settings.GROUP_MEMBER_OFFICE,
settings.GROUP_ACCOUNTING,
]

def do_request(self):
def permission_test_do_request(self):
self.given_feature_flag_value(feature_flag_membership_resignation, True)
return self.client.get(reverse("coop:membership_resignation_list"))

Expand Down
30 changes: 0 additions & 30 deletions tapir/shifts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,6 @@ class Shift(models.Model):
)

NB_DAYS_FOR_SELF_UNREGISTER = 7
NB_DAYS_FOR_SELF_LOOK_FOR_STAND_IN = 2

def __str__(self):
display_name = "%s: %s %s-%s" % (
Expand Down Expand Up @@ -716,35 +715,6 @@ def user_can_attend(self, user):
and user.share_owner.is_active(self.shift.start_time)
)

def user_can_self_unregister(self, user: TapirUser) -> bool:
user_is_registered_to_slot = (
self.get_valid_attendance() is not None
and self.get_valid_attendance().user == user
)
user_is_not_registered_to_slot_template = (
self.slot_template is None
or not hasattr(self.slot_template, "attendance_template")
or not self.slot_template.attendance_template.user == user
)
early_enough = (
self.shift.start_time.date() - timezone.now().date()
).days >= Shift.NB_DAYS_FOR_SELF_UNREGISTER
return (
user_is_registered_to_slot
and user_is_not_registered_to_slot_template
and early_enough
)

def user_can_look_for_standin(self, user: TapirUser) -> bool:
user_is_registered_to_slot = (
self.get_valid_attendance() is not None
and self.get_valid_attendance().user == user
)
early_enough = (
self.shift.start_time - timezone.now()
).days >= Shift.NB_DAYS_FOR_SELF_LOOK_FOR_STAND_IN
return user_is_registered_to_slot and early_enough

def update_attendance_from_template(self):
"""Updates the attendance of this slot.

Expand Down
9 changes: 9 additions & 0 deletions tapir/shifts/services/can_look_for_standin_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.utils import timezone

from tapir.shifts.models import ShiftSlot


class CanLookForStandinService:
@staticmethod
def can_look_for_a_standin(slot: ShiftSlot):
return slot.shift.start_time > timezone.now()
64 changes: 64 additions & 0 deletions tapir/shifts/services/self_unregister_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from django.utils import timezone
from django.utils.translation import gettext_lazy as _

from tapir.accounts.models import TapirUser
from tapir.shifts.models import (
ShiftAttendance,
ShiftAttendanceTemplate,
Shift,
)


class SelfUnregisterService:
@classmethod
def should_show_is_abcd_attendance_reason(
cls, user: TapirUser, attendance: ShiftAttendance
):
if not attendance.slot.slot_template:
return False

return ShiftAttendanceTemplate.objects.filter(
user=user, slot_template=attendance.slot.slot_template
).exists()

@classmethod
def should_show_not_enough_days_before_shift_reason(
cls, attendance: ShiftAttendance, **_
):
return (
attendance.slot.shift.start_time.date() - timezone.now().date()
).days <= Shift.NB_DAYS_FOR_SELF_UNREGISTER

@classmethod
def build_reasons_why_cant_self_unregister(
cls, user: TapirUser, attendance: ShiftAttendance
):
return [
message
for check, message in cls.get_check_to_message_map().items()
if check(
user=user,
attendance=attendance,
)
]

@classmethod
def user_can_self_unregister(
cls, user: TapirUser, attendance: ShiftAttendance
) -> bool:
for check in cls.get_check_to_message_map().keys():
if check(user=user, attendance=attendance):
return False

return True

@classmethod
def get_check_to_message_map(cls):
return {
cls.should_show_is_abcd_attendance_reason: _(
"It is not possible to unregister from a shift that comes from your ABCD shift."
),
cls.should_show_not_enough_days_before_shift_reason: _(
"It is only possible to unregister from a shift at least 7 days before the shift."
),
}
102 changes: 102 additions & 0 deletions tapir/shifts/templates/shifts/cant_attend.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
{% extends "core/base.html" %}
{% load django_bootstrap5 %}
{% load i18n %}
{% load static %}
{% load core %}
{% block title %}
{% translate "Can't attend:" %} {{ attendance }}
{% endblock title %}
{% block content %}
<div class="container">
<div class="row mb-3">
<h3 class="col text-center">
{% translate "If you can't attend" %}
<br />
<a href="{{ attendance.slot.shift.get_absolute_url }}">{{ attendance.slot }}</a>
</h3>
</div>
<div class="row">
<div class="col-xl mb-2">
<div class="card">
<h5 class="card-header">{% translate "Option 1: unregister" %}</h5>
<div class="card-body">
<div>
{% if can_unregister %}
{% blocktranslate %}
You can unregister from this shift.
You will be removed from the attendance list and won't get any negative
point.
{% endblocktranslate %}
{% else %}
{% blocktranslate %}
You cannot unregister from this shift. <br />
The reasons are:
{% endblocktranslate %}
<ul>
{% for reason in reasons_why_cant_self_unregister %}<li>{{ reason }}</li>{% endfor %}
</ul>
{% endif %}
</div>
<form role="form"
method="post"
action="{% url 'shifts:update_shift_attendance_state' attendance.id attendance.State.CANCELLED %}">
{% csrf_token %}
<div class="d-flex justify-content-end">
<button type="submit"
class="{% tapir_button_action %} {% if not can_unregister %}disabled{% endif %}">
<span class="material-icons">event_busy</span>
{% translate 'Unregister' %}
</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-xl">
<div class="card">
<h5 class="card-header">{% translate "Option 2: Enable looking for a stand-in" %}</h5>
<div class="card-body">
{% blocktranslate %}
<p>
If you cannot unregister from the shift, your other option is to look for a replacement ("stand-in").
This will notify the team that you can't come, and will allow other members to take over your slot.
Please enable this as soon as you know you won't be able to come to your shift.
</p>
<p>
This is always possible up to the time of the shift.
</p>
<p>
After enabling this, it is still absolutely necessary that you contact the member office
(<a href="mailto:{{ email_address_member_office }}">{{ email_address_member_office }}</a>)
as soon as possible.
</p>
<p>
If you have a valid reason for not coming (see the
<a href="https://docs.google.com/document/d/19v91mj2MAJTtrPLCw54VMS6qlaiJqBqUAuiC6z_AKpw/edit?tab=t.0#heading=h.vxvgxkgmyf1w">Member Manual</a>),
the member office will unregister you from the shift and you won't get any negative points.
</p>
<p>
If you don't have a valid reason for not coming, you may still get lucky:
someone may take over your shift. The shift calendar shows where someone is looking for a stand-in. <br />
If that happens, you will be notified by email and will not get any negative points.
You are still obliged to sign up for an alternative shift in this shift cycle. <br />
There is no guarantee that this happens. If your slot is not taken over, you will get negative points.
</p>
{% endblocktranslate %}
<form role="form"
method="post"
action="{% url 'shifts:update_shift_attendance_state' attendance.id attendance.State.LOOKING_FOR_STAND_IN %}">
{% csrf_token %}
<div class="d-flex justify-content-end">
<button type="submit" class="{% tapir_button_action %}">
<span class="material-icons">sync</span>
{% translate 'Look for a stand-in' %}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}
49 changes: 5 additions & 44 deletions tapir/shifts/templates/shifts/shift_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -163,51 +163,12 @@ <h5>#{{ forloop.counter }}</h5>
</button>
</form>
{% else %}
<form style="display: inline"
method="post"
action="{% url 'shifts:update_shift_attendance_state' attendance.pk attendance_states.LOOKING_FOR_STAND_IN %}">
{% csrf_token %}
{% blocktranslate asvar stand_in_tooltip %}You can only look for
a
stand-in
{{ NB_DAYS_FOR_SELF_LOOK_FOR_STAND_IN }} days before the
shift. If
you can't
attend, contact your shift leader as soon as
possible.{% endblocktranslate %}
<span {% if not slot.can_look_for_stand_in %}data-bs-toggle="tooltip" title="{{ stand_in_tooltip }}"{% endif %}>
<button type="submit"
class="{% tapir_button_custom 'warning' %} btn-sm {% if not slot.can_look_for_stand_in %}disabled{% endif %}"
id="self_look_for_stand_in_button">
<span class="material-icons">sync</span>
{% translate "Look for a stand-in" %}
</button>
</span>
</form>
<a class="{% tapir_button_link_to_action %}"
href="{% url "shifts:attendance_cant_attend" attendance.id %}">
<span class="material-icons">event_busy</span>
{% translate "I can't attend this shift" %}
</a>
Copy link
Copy Markdown
Contributor

@kerstenkenan kerstenkenan Jan 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a little few words for this button {% translate "Cannot attend" %}? Better readable.

{% endif %}
<form style="display: inline"
method="post"
action="{% url 'shifts:update_shift_attendance_state' attendance.pk|default:1 attendance_states.CANCELLED %}">
{% csrf_token %}
{% blocktranslate asvar self_unregister_tooltip %}You can only
unregister
yourself
{{ NB_DAYS_FOR_SELF_UNREGISTER }} days before the shift. Also,
ABCD-Shifts
can't be unregistered from. If you can't
attend, look for a stand-in or contact your shift leader as soon
as
possible.{% endblocktranslate %}
<span {% if not slot.can_self_unregister %}data-bs-toggle="tooltip" title="{{ self_unregister_tooltip }}"{% endif %}>
<button type="submit"
class="{% tapir_button_custom 'danger' %} btn-sm"
id="unregister_self_button"
{% if not slot.can_self_unregister %}disabled style="pointer-events: none;"{% endif %}>
<span class="material-icons">person_remove</span>
{% translate "Unregister myself" %}
</button>
</span>
</form>
{% elif not slot.is_occupied %}
{% blocktranslate asvar self_register_tooltip %}You can only register
yourself
Expand Down
Loading
Loading