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 %}
+
+ {% translate 'Progress' %}
+
{% 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,
)
)