-
Notifications
You must be signed in to change notification settings - Fork 8
Add Session Points: opt-in attendance scoring per organization #83
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
base: master
Are you sure you want to change the base?
Changes from all commits
127a18f
cca72c6
15fc6a1
d1452ed
577a5e7
21e0866
120be26
4ea83fb
6ade37e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| from typing import Any | ||
|
|
||
| from django import forms | ||
| from django.forms import ModelChoiceField | ||
| from django.utils.translation import gettext_lazy as _ | ||
|
|
||
| from shiftings.accounts.models import User | ||
| from shiftings.shifts.models import Shift | ||
|
|
||
|
|
||
| class ExcuseOtherForm(forms.Form): | ||
| user = ModelChoiceField(queryset=User.objects.none(), label=_('User to excuse')) | ||
|
|
||
| shift: Shift | ||
|
|
||
| def __init__(self, shift: Shift, *args: Any, **kwargs: Any) -> None: | ||
| super().__init__(*args, **kwargs) | ||
| self.shift = shift | ||
| excused_ids = list(shift.excused_users.values_list('pk', flat=True)) | ||
| self.fields['user'].queryset = shift.organization.users.exclude(pk__in=excused_ids) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # Generated by Django 6.0.2 on 2026-06-23 12:17 | ||
|
|
||
| from decimal import Decimal | ||
|
|
||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ("shifts", "0006_recurringshift_auto_create_days"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name="shifttype", | ||
| name="is_mandatory", | ||
| field=models.BooleanField( | ||
| default=False, | ||
| help_text="When set, members who do not respond to shifts of this type incur the organisation-wide no-response penalty. Only takes effect when the organisation has Session Points enabled.", | ||
| verbose_name="Mandatory Shift Type", | ||
| ), | ||
| ), | ||
| migrations.AddField( | ||
| model_name="shifttype", | ||
| name="point_weight", | ||
| field=models.DecimalField( | ||
| decimal_places=2, | ||
| default=Decimal("1.00"), | ||
| help_text="Points awarded when attending a shift of this type. Only takes effect when the organisation has Session Points enabled.", | ||
| max_digits=4, | ||
| verbose_name="Session Points Weight", | ||
| ), | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| # Generated by Django 6.0.2 on 2026-06-23 12:18 | ||
|
|
||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ("accounts", "0001_initial"), | ||
| ("shifts", "0007_shifttype_attendance_points"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name="shift", | ||
| name="excused_users", | ||
| field=models.ManyToManyField( | ||
| blank=True, | ||
| related_name="excused_from_shifts", | ||
| to="accounts.baseuser", | ||
| verbose_name="Excused Users", | ||
| ), | ||
| ), | ||
| migrations.AddField( | ||
| model_name="shift", | ||
| name="is_mandatory_override", | ||
| field=models.BooleanField( | ||
| blank=True, | ||
| help_text="If set, overrides the shift type mandatory flag just for this shift. Leave empty to inherit from the shift type.", | ||
| null=True, | ||
| verbose_name="Mandatory Override", | ||
| ), | ||
| ), | ||
| migrations.AddField( | ||
| model_name="shift", | ||
| name="point_weight_override", | ||
| field=models.DecimalField( | ||
| blank=True, | ||
| decimal_places=2, | ||
| help_text="If set, overrides the shift type weight just for this shift. Leave empty to inherit from the shift type.", | ||
| max_digits=4, | ||
| null=True, | ||
| verbose_name="Session Points Weight Override", | ||
| ), | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # Generated by Django 6.0.2 on 2026-06-23 12:20 | ||
|
|
||
| from decimal import Decimal | ||
|
|
||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ("shifts", "0008_shift_attendance_points"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name="organizationsummarysettings", | ||
| name="attendance_points_enabled", | ||
| field=models.BooleanField( | ||
| default=False, | ||
| help_text="Enable per-member Session Points tracking on the shift summary and the shift detail page.", | ||
| verbose_name="Session Points enabled", | ||
| ), | ||
| ), | ||
| migrations.AddField( | ||
| model_name="organizationsummarysettings", | ||
| name="no_response_penalty", | ||
| field=models.DecimalField( | ||
| decimal_places=2, | ||
| default=Decimal("0.33"), | ||
| help_text="Points deducted when a member does not respond to a mandatory shift. Applies organisation-wide.", | ||
| max_digits=4, | ||
| verbose_name="No-Response Penalty", | ||
| ), | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -66,6 +66,18 @@ <h4>Update Shift "{{ name }} for {{ organization }}"</h4> | |
| </div> | ||
| </div> | ||
| {% bootstrap_field form.additional_infos %} | ||
| {% if form.point_weight_override %} | ||
| <hr> | ||
| <div class="text-center"><h5>{% trans "Session Points (Override)" %}</h5></div> | ||
| <div class="center-items"> | ||
| <div class="col-6 me-1"> | ||
| {% bootstrap_field form.point_weight_override %} | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be nicer to by default load the orgs default point weight here |
||
| </div> | ||
| <div class="col-6 ms-1"> | ||
| {% bootstrap_field form.is_mandatory_override %} | ||
| </div> | ||
| </div> | ||
| {% endif %} | ||
| </form> | ||
| </div> | ||
| </div> | ||
|
|
@@ -87,4 +99,4 @@ <h4>{% trans "Create from Template" %}</h4> | |
| </div> | ||
| {% endif %} | ||
| </div> | ||
| {% endblock %} | ||
| {% endblock %} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| {% load i18n %} | ||
| <div class="mx-3 mt-2 shift-slots shift-info overflow-auto"> | ||
| {% for excused_user in shift.excused_users.all %} | ||
| {% include "shifts/template/excused_display.html" with excused_user=excused_user shift=shift %} | ||
| {% endfor %} | ||
| {% if shift.start.date >= current_date and not shift.locked %} | ||
| {% url 'excuse_self_from_shift' shift.pk as excuse_url %} | ||
| {% url 'remove_excused' shift.pk request.user.pk as withdraw_url %} | ||
| {% is_request_user_excused shift as user_is_excused %} | ||
| {% if user_is_participant %} | ||
| <form method="post" action="{{ excuse_url }}" class="d-flex mt-2 mx-2"> | ||
| {% csrf_token %} | ||
| <button type="submit" class="btn border border-2 card-link link w-100"> | ||
| <i class="fa-solid fa-person-circle-xmark me-2"></i>{% trans "I can't come" %} | ||
| </button> | ||
| </form> | ||
| {% elif user_is_excused %} | ||
| <form method="post" action="{{ withdraw_url }}" class="d-flex mt-2 mx-2"> | ||
| {% csrf_token %} | ||
| <button type="submit" class="btn border border-2 card-link link w-100"> | ||
| <i class="fa-solid fa-rotate-left me-2"></i>{% trans 'Withdraw excuse' %} | ||
| </button> | ||
| </form> | ||
| {% else %} | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is excusing oneself when you're neither a participant nor excused intended? |
||
| <form method="post" action="{{ excuse_url }}" class="d-flex mt-2 mx-2"> | ||
| {% csrf_token %} | ||
| <button type="submit" class="btn border border-2 card-link link w-100"> | ||
| <i class="fa-solid fa-person-circle-xmark me-2"></i>{% trans 'Excuse me' %} | ||
| </button> | ||
| </form> | ||
| {% endif %} | ||
| {% endif %} | ||
| </div> | ||
Uh oh!
There was an error while loading. Please reload this page.