diff --git a/castellum/appointments/models.py b/castellum/appointments/models.py index 86e7c10709012476fc2edf0dc0ecab5def23f4e8..7bcc61ab4f0fc765502bd17554efa9c6d28ec201 100644 --- a/castellum/appointments/models.py +++ b/castellum/appointments/models.py @@ -47,6 +47,8 @@ class Appointment(models.Model): (NOSHOW, _('no show')), ] + SHOWUP_OK = [SHOWUP, LATE] + session = models.ForeignKey(StudySession, verbose_name=_('Session'), on_delete=models.CASCADE) start = DateTimeField(_('Start')) reminded = models.BooleanField(_('Reminded'), default=False) diff --git a/castellum/execution/templates/execution/study_base.html b/castellum/execution/templates/execution/study_base.html index 0d5088a024caaddc22b2a6ba3f5289fff4fb9374..5a0dad110df1772a00c1796ca8bc6e0e181b81a3 100644 --- a/castellum/execution/templates/execution/study_base.html +++ b/castellum/execution/templates/execution/study_base.html @@ -38,6 +38,9 @@ {% translate 'Criteria' %} {% endif %} + {% endblock %} diff --git a/castellum/execution/templates/execution/study_progress.html b/castellum/execution/templates/execution/study_progress.html new file mode 100644 index 0000000000000000000000000000000000000000..589901fe560f4e282f3be9a7287a4a3f698d87e5 --- /dev/null +++ b/castellum/execution/templates/execution/study_progress.html @@ -0,0 +1,25 @@ +{% extends "execution/study_base.html" %} +{% load i18n auth %} + +{% block title %}{% translate "Progress" %} · {{ block.super }}{% endblock %} + +{% block content %} +

{% translate 'Participating' %}

+

+ {{ invited }}/{{ total }} + +

+ + {% for session in sessions %} +

{{ session.name }}

+
+
{% translate 'took place' %}
+
{{ session.took_place }}/{{ total }}
+
+ +
{% translate 'scheduled' %}
+
{{ session.scheduled }}/{{ total }}
+
+
+ {% endfor %} +{% endblock %} diff --git a/castellum/execution/urls.py b/castellum/execution/urls.py index 0dc51438614f297eb4c73bd62d8d580aa1c3a7f6..1461643d5670798a000eab3d5f44df95f8cf0674 100644 --- a/castellum/execution/urls.py +++ b/castellum/execution/urls.py @@ -37,6 +37,7 @@ from .views import ParticipationDetailView from .views import ParticipationNewsView from .views import ParticipationPseudonymsView from .views import ParticipationRemoveView +from .views import ProgressView from .views import ResolveView from .views import ShowUpUpdateView from .views import StudyDetailView @@ -51,6 +52,7 @@ urlpatterns = [ path('/calendar/feed/', AppointmentFeedForStudy(), name='calendar-feed'), path('/criteria/', ExclusionCriteriaView.as_view(), name='criteria'), path('/export/', ExportView.as_view(), name='export'), + path('/progress/', ProgressView.as_view(), name='progress'), path( '//', ParticipationDetailView.as_view(), diff --git a/castellum/execution/views.py b/castellum/execution/views.py index c787c7baec07a2da056a1f239860b1f9819e5dad..9f6945734a7f51ec1720fc5f004a83a73f713b53 100644 --- a/castellum/execution/views.py +++ b/castellum/execution/views.py @@ -30,9 +30,11 @@ from django.http import HttpResponse from django.shortcuts import get_object_or_404 from django.shortcuts import redirect from django.urls import reverse +from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django.views.generic import DetailView from django.views.generic import FormView +from django.views.generic import ListView from django.views.generic import UpdateView from castellum.appointments.forms import AppointmentsForm @@ -485,7 +487,7 @@ class CalendarView(StudyMixin, PermissionRequiredMixin, BaseCalendarView): title = '{} ({})'.format(title, appointment.get_show_up_display()) class_names = [] - if appointment.show_up not in [Appointment.SHOWUP, Appointment.LATE]: + if appointment.show_up not in Appointment.SHOWUP_OK: class_names.append('fullcalendar-muted') return { @@ -500,3 +502,38 @@ class CalendarView(StudyMixin, PermissionRequiredMixin, BaseCalendarView): def get_appointments(self): return super().get_appointments().filter(session__study=self.object) + + +class ProgressView(StudyMixin, PermissionRequiredMixin, ListView): + model = Study + template_name = 'execution/study_progress.html' + permission_required = 'recruitment.conduct_study' + study_status = [Study.EXECUTION] + tab = 'progress' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + context['invited'] = ( + self.study.participation_set + .filter(status=Participation.INVITED) + .count() + ) + context['total'] = max(self.study.min_subject_count, context['invited']) + context['sessions'] = self.study.studysession_set.annotate( + scheduled=models.Count( + 'appointment', + filter=models.Q(appointment__show_up__in=Appointment.SHOWUP_OK), + distinct=True, + ), + took_place=models.Count( + 'appointment', + filter=models.Q( + appointment__show_up__in=Appointment.SHOWUP_OK, + appointment__start__lte=timezone.now(), + ), + distinct=True, + ), + ) + + return context diff --git a/castellum/recruitment/models/participations.py b/castellum/recruitment/models/participations.py index 38d11374a6087f2103be1c1a220b65ab4112e4dc..1004fd2ab75a788c56c586951943eae3e6792d88 100644 --- a/castellum/recruitment/models/participations.py +++ b/castellum/recruitment/models/participations.py @@ -87,7 +87,7 @@ class ParticipationManager(models.Manager): return self.get_queryset().annotate( appointment_count=models.Count( 'appointment', - filter=models.Q(appointment__show_up__in=[Appointment.SHOWUP, Appointment.LATE]), + filter=models.Q(appointment__show_up__in=Appointment.SHOWUP_OK), distinct=True, ) )