diff --git a/interventions/views.py b/interventions/views.py
index 41a4470c7..1c4c81cca 100644
--- a/interventions/views.py
+++ b/interventions/views.py
@@ -4,6 +4,8 @@
from main.views import CreateView, UpdateView, AllowErrorsOnBackbuttonMixin
from studies.models import Study
from studies.utils import get_study_progress
+from studies.mixins import StudyFromURLMixin
+from proposals.mixins import StepperContextMixin
from .forms import InterventionForm
from .models import Intervention
@@ -12,7 +14,7 @@
##############################
# CRUD actions on Intervention
##############################
-class InterventionMixin(object):
+class InterventionMixin(StepperContextMixin):
"""Mixin for an Intervention, to use in both InterventionCreate and InterventionUpdate below"""
model = Intervention
@@ -48,6 +50,9 @@ def get_next_url(self):
next_url = "tasks:session_start"
return reverse(next_url, args=(pk,))
+ def get_proposal(self):
+ return self.get_object().study.proposal
+
def get_back_url(self):
return reverse("studies:design", args=(self.get_study().pk,))
@@ -55,7 +60,12 @@ def get_study(self):
raise NotImplementedError
-class InterventionCreate(InterventionMixin, AllowErrorsOnBackbuttonMixin, CreateView):
+class InterventionCreate(
+ StudyFromURLMixin,
+ InterventionMixin,
+ AllowErrorsOnBackbuttonMixin,
+ CreateView,
+):
"""Creates a Intervention from a InterventionForm"""
def form_valid(self, form):
@@ -63,10 +73,6 @@ def form_valid(self, form):
form.instance.study = self.get_study()
return super(InterventionCreate, self).form_valid(form)
- def get_study(self):
- """Retrieves the Study from the pk kwarg"""
- return Study.objects.get(pk=self.kwargs["pk"])
-
class InterventionUpdate(InterventionMixin, AllowErrorsOnBackbuttonMixin, UpdateView):
"""Updates a Intervention from an InterventionForm"""
diff --git a/main/templates/base/fetc_form_base.html b/main/templates/base/fetc_form_base.html
index 3d5a0d97d..fe713b8a2 100644
--- a/main/templates/base/fetc_form_base.html
+++ b/main/templates/base/fetc_form_base.html
@@ -9,7 +9,7 @@
{# todo: responsive design #}
{% block sidebar %}
- {% include "base/stepper.html" %}
+ {% include stepper %}
{% endblock %}
diff --git a/main/templates/base/stepper.html b/main/templates/base/stepper.html
index b16334bbe..3895fc871 100644
--- a/main/templates/base/stepper.html
+++ b/main/templates/base/stepper.html
@@ -3,21 +3,17 @@
{% counter counter create 1 %}
- {% for url in proposal.available_urls %}
+ {% for item in stepper.build_stepper %}
-
-
- {% counter counter value %}
- {{ url.title }}
+
+ {% counter counter value %}
+ {{ item.title }}
- {% if url.children %}
+ {% counter depth create 1 %}
+ {% if item.children %}
- {% for child in url.children %}
- -
-
-
- {{ child.title }}
-
-
+ {% for child in item.children %}
+ {% include child with bubble_size=bubble_size|slice:"1:" %}
{% endfor %}
{% endif %}
diff --git a/main/templates/base/stepper_item.html b/main/templates/base/stepper_item.html
new file mode 100644
index 000000000..76b240be4
--- /dev/null
+++ b/main/templates/base/stepper_item.html
@@ -0,0 +1,13 @@
+ -
+
+
+ {{ item.title }}
+
+ {% if item.children %}
+
+ {% for child in item.children %}
+ {% include "base/stepper_item.html" with item=child bubble_size=bubble_size|slice:"1:" %}
+ {% endfor %}
+
+ {% endif %}
+
diff --git a/main/utils.py b/main/utils.py
index 6ff581545..763f4714c 100644
--- a/main/utils.py
+++ b/main/utils.py
@@ -5,6 +5,7 @@
from django.db.models import Q
from django.db.models.fields.files import FieldFile
from django.utils.translation import gettext_lazy as _
+from django.template import loader, Template, Context
import magic # whoooooo
import pdftotext
@@ -162,3 +163,16 @@ def can_view_archive(user):
# If our tests are inconclusive,
# check for Humanities affiliation
return is_member_of_humanities(user)
+
+
+class renderable:
+
+ def get_context_data(self):
+ context = Context()
+ return context
+
+ def render(self, extra_context={}):
+ context = self.get_context_data()
+ template = loader.get_template(self.template_name)
+ context.update(extra_context)
+ return template.render(context.flatten())
diff --git a/observations/views.py b/observations/views.py
index 7c4c2e85d..d72f41f37 100644
--- a/observations/views.py
+++ b/observations/views.py
@@ -3,8 +3,9 @@
from main.views import CreateView, UpdateView, AllowErrorsOnBackbuttonMixin
from fetc import settings
-from studies.models import Study
from studies.utils import get_study_progress
+from studies.mixins import StudyFromURLMixin
+from proposals.mixins import StepperContextMixin
from .forms import ObservationForm, ObservationUpdateAttachmentsForm
from .models import Observation
@@ -13,7 +14,7 @@
#############################
# CRUD actions on Observation
#############################
-class ObservationMixin(object):
+class ObservationMixin(StepperContextMixin):
"""Mixin for a Observation, to use in both ObservationCreate and ObservationUpdate below"""
model = Observation
@@ -53,8 +54,12 @@ def get_back_url(self):
return reverse(next_url, args=(pk,))
def get_study(self):
+ # Um.... what?
raise NotImplementedError
+ def get_proposal(self):
+ return self.get_object().study.proposal
+
class AttachmentsUpdate(UpdateView):
model = Observation
@@ -63,7 +68,12 @@ class AttachmentsUpdate(UpdateView):
group_required = settings.GROUP_SECRETARY
-class ObservationCreate(ObservationMixin, AllowErrorsOnBackbuttonMixin, CreateView):
+class ObservationCreate(
+ StudyFromURLMixin,
+ ObservationMixin,
+ AllowErrorsOnBackbuttonMixin,
+ CreateView,
+):
"""Creates an Observation from a ObservationForm"""
def form_valid(self, form):
@@ -71,10 +81,6 @@ def form_valid(self, form):
form.instance.study = self.get_study()
return super(ObservationCreate, self).form_valid(form)
- def get_study(self):
- """Retrieves the Study from the pk kwarg"""
- return Study.objects.get(pk=self.kwargs["pk"])
-
class ObservationUpdate(ObservationMixin, AllowErrorsOnBackbuttonMixin, UpdateView):
"""Updates a Observation from a ObservationForm"""
diff --git a/proposals/mixins.py b/proposals/mixins.py
index d08f393e3..385fc31ec 100644
--- a/proposals/mixins.py
+++ b/proposals/mixins.py
@@ -15,25 +15,64 @@
from .utils.proposal_utils import pdf_link_callback
-class ProposalMixin(UserFormKwargsMixin):
- model = Proposal
- form_class = ProposalForm
+class StepperContextMixin:
+ """
+ Includes a stepper object in the view's context
+ """
- def get_next_url(self):
- return reverse("proposals:researcher", args=(self.object.pk,))
+ def get_context_data(self, *args, **kwargs):
+ # Importing here to prevent circular import
+ from .utils.stepper import Stepper
+
+ context = super().get_context_data(*args, **kwargs)
+ # Try to determine proposal
+ proposal = Proposal()
+ if hasattr(self, "get_proposal"):
+ proposal = self.get_proposal()
+ # Initialize and insert stepper object
+ stepper = Stepper(
+ proposal,
+ request=self.request,
+ )
+ context["stepper"] = stepper
+ return context
-class ProposalContextMixin:
+class ProposalContextMixin(
+ StepperContextMixin,
+):
def current_user_is_supervisor(self):
- return self.object.supervisor == self.request.user
+ return self.get_proposal().supervisor == self.request.user
+
+ def get_proposal(
+ self,
+ ):
+ try:
+ if self.model is Proposal:
+ return self.get_object()
+ except AttributeError:
+ raise RuntimeError(
+ "Couldn't find proposal object for ProposalContextMixin",
+ )
def get_context_data(self, **kwargs):
context = super(ProposalContextMixin, self).get_context_data(**kwargs)
context["is_supervisor"] = self.current_user_is_supervisor()
- context["is_practice"] = self.object.is_practice()
+ context["is_practice"] = self.get_proposal().is_practice()
return context
+class ProposalMixin(
+ ProposalContextMixin,
+):
+ model = Proposal
+
+ def get_proposal(
+ self,
+ ):
+ return self.get_object()
+
+
class PDFTemplateResponseMixin(TemplateResponseMixin):
"""
A mixin class that implements PDF rendering and Django response construction.
diff --git a/proposals/templates/proposals/funding_form.html b/proposals/templates/proposals/funding_form.html
index 95739f1f1..17f389cdc 100644
--- a/proposals/templates/proposals/funding_form.html
+++ b/proposals/templates/proposals/funding_form.html
@@ -19,4 +19,4 @@
{% block pre-form-text %}
{% trans "Informatie over financiering" %}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/proposals/templates/proposals/other_researchers_form.html b/proposals/templates/proposals/other_researchers_form.html
index 1e3701d09..969f7a01f 100644
--- a/proposals/templates/proposals/other_researchers_form.html
+++ b/proposals/templates/proposals/other_researchers_form.html
@@ -19,4 +19,4 @@
{% block pre-form-text %}
{% trans "Informatie over betrokken onderzoekers" %}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/proposals/templates/proposals/practice_or_supervisor_warning.html b/proposals/templates/proposals/practice_or_supervisor_warning.html
index 682ce15df..5a2d9ce9b 100644
--- a/proposals/templates/proposals/practice_or_supervisor_warning.html
+++ b/proposals/templates/proposals/practice_or_supervisor_warning.html
@@ -17,4 +17,4 @@
{% trans "Je bewerkt op het moment een oefenaanvraag. Deze kan niet ter beoordeling door de FETC-GW worden ingediend." %}
-{% endif %}
\ No newline at end of file
+{% endif %}
diff --git a/proposals/templates/proposals/pre_approved_form.html b/proposals/templates/proposals/pre_approved_form.html
index 75aec84e5..676ab1b44 100644
--- a/proposals/templates/proposals/pre_approved_form.html
+++ b/proposals/templates/proposals/pre_approved_form.html
@@ -9,4 +9,4 @@
{% block pre-form-text %}
{% trans "Informatie over eerdere toesting" %}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/proposals/templates/proposals/researcher_form.html b/proposals/templates/proposals/researcher_form.html
index f9c0bf2af..588bff4b3 100644
--- a/proposals/templates/proposals/researcher_form.html
+++ b/proposals/templates/proposals/researcher_form.html
@@ -22,4 +22,4 @@
{% block pre-form-text %}
{% trans "Informatie over de onderzoeker" %}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/proposals/utils/checkers.py b/proposals/utils/checkers.py
new file mode 100644
index 000000000..d0ba93ce6
--- /dev/null
+++ b/proposals/utils/checkers.py
@@ -0,0 +1,828 @@
+from django.utils.translation import gettext as _
+from django.urls import reverse
+
+from proposals import forms as proposal_forms
+from studies import forms as study_forms
+from interventions import forms as intervention_forms
+from observations import forms as observation_forms
+from tasks import forms as tasks_forms
+
+from tasks.views import task_views, session_views
+from tasks.models import Task, Session
+
+from .stepper_helpers import (
+ Checker,
+ PlaceholderItem,
+ StepperItem,
+ ContainerItem,
+ ModelFormChecker,
+ ModelFormItem,
+ UpdateOrCreateChecker,
+)
+
+
+class ProposalTypeChecker(
+ Checker,
+):
+
+ def check(self):
+ # TODO: check stepper.proposal_type_hint
+ # and proposal.is_pre_approved etc. for non-standard layouts
+ return self.regular_proposal()
+
+ def regular_proposal(self):
+ from .stepper import RegularProposalLayout
+
+ self.stepper.layout = RegularProposalLayout
+ return [ProposalCreateChecker]
+
+
+class BasicDetailsItem(
+ ContainerItem,
+):
+ title = _("Basisgegevens")
+ location = "create"
+
+
+class ProposalCreateChecker(
+ ModelFormChecker,
+):
+ title = _("Start")
+ form_class = proposal_forms.ProposalForm
+
+ def get_url(self):
+ if self.proposal.pk:
+ return reverse(
+ "proposals:update",
+ args=[self.proposal.pk],
+ )
+ return reverse(
+ "proposals:create",
+ )
+
+ def check(self):
+ self.parent = BasicDetailsItem(self.stepper)
+ self.stepper.items.append(self.parent)
+ if self.proposal.pk:
+ return self.proposal_exists()
+ return self.new_proposal()
+
+ def new_proposal(self):
+ self.stepper.items.append(self.make_stepper_item())
+ placeholders = [
+ PlaceholderItem(
+ self.stepper,
+ title=_("Onderzoeker"),
+ parent=self.parent,
+ ),
+ PlaceholderItem(
+ self.stepper,
+ title=_("Andere onderzoekers"),
+ parent=self.parent,
+ ),
+ PlaceholderItem(
+ self.stepper,
+ title=_("Financiering"),
+ parent=self.parent,
+ ),
+ PlaceholderItem(
+ self.stepper,
+ title=_("Onderzoeksdoel"),
+ parent=self.parent,
+ ),
+ ]
+ self.stepper.items += placeholders
+ return []
+
+ def proposal_exists(self):
+ stepper_item = ModelFormItem(
+ self.stepper,
+ title=self.title,
+ parent=self.parent,
+ form_object=self.proposal,
+ form_class=self.form_class,
+ url_func=self.get_url,
+ )
+ self.stepper.items.append(
+ stepper_item,
+ )
+ return [
+ ResearcherChecker(
+ self.stepper,
+ parent=self.parent,
+ )
+ ]
+
+
+class ResearcherChecker(
+ ModelFormChecker,
+):
+ title = _("Onderzoeker")
+ form_class = proposal_forms.ResearcherForm
+
+ def check(self):
+ self.stepper.items.append(self.make_stepper_item())
+ return [OtherResearchersChecker(self.stepper, parent=self.parent)]
+
+ def get_url(self):
+ return reverse(
+ "proposals:researcher",
+ args=(self.proposal.pk,),
+ )
+
+
+class OtherResearchersChecker(
+ ModelFormChecker,
+):
+ title = _("Andere onderzoekers")
+ form_class = proposal_forms.OtherResearchersForm
+
+ def check(self):
+ self.stepper.items.append(self.make_stepper_item())
+ return [
+ FundingChecker(
+ self.stepper,
+ parent=self.parent,
+ )
+ ]
+
+ def get_url(self):
+ return reverse(
+ "proposals:other_researchers",
+ args=(self.proposal.pk,),
+ )
+
+
+class FundingChecker(
+ ModelFormChecker,
+):
+ title = _("Financiering")
+ form_class = proposal_forms.FundingForm
+
+ def check(self):
+ self.stepper.items.append(self.make_stepper_item())
+ return [
+ GoalChecker(
+ self.stepper,
+ parent=self.parent,
+ )
+ ]
+
+ def get_url(self):
+ return reverse(
+ "proposals:funding",
+ args=(self.proposal.pk,),
+ )
+
+
+class GoalChecker(
+ ModelFormChecker,
+):
+ title = _("Onderzoeksdoel")
+ form_class = proposal_forms.ResearchGoalForm
+
+ def check(self):
+ self.stepper.items.append(self.make_stepper_item())
+ return [WMOChecker]
+
+ def get_url(self):
+ return reverse(
+ "proposals:research_goal",
+ args=(self.proposal.pk,),
+ )
+
+
+class WMOItem(
+ StepperItem,
+):
+ location = "wmo"
+ title = _("WMO")
+
+ def __init__(
+ self,
+ *args,
+ **kwargs,
+ ):
+ super().__init__(*args, **kwargs)
+ self.proposal = self.stepper.proposal
+ self.wmo = self.get_wmo()
+
+ def get_wmo(
+ self,
+ ):
+ if hasattr(
+ self.proposal,
+ "wmo",
+ ):
+ return self.proposal.wmo
+ return None
+
+ def get_url(
+ self,
+ ):
+ if self.wmo:
+ return reverse(
+ "proposals:wmo_update",
+ args=[self.wmo.pk],
+ )
+ else:
+ return reverse(
+ "proposals:wmo_create",
+ args=[self.proposal.pk],
+ )
+
+ def wmo_exists(
+ self,
+ ):
+ return hasattr(
+ self.proposal,
+ "wmo",
+ )
+
+
+class WMOChecker(
+ Checker,
+):
+
+ def check(
+ self,
+ ):
+ self.item = WMOItem(self.stepper)
+ self.stepper.items.append(self.item)
+ if self.item.wmo:
+ return self.check_wmo()
+ else:
+ return []
+
+ def check_wmo(
+ self,
+ ):
+ """
+ This method should check the correctness of the
+ WMO object.
+ """
+ # Just assume any WMO is correct as long as it exists
+ if self.item.wmo:
+ return [TrajectoriesChecker]
+ return [] # TODO next item
+
+
+class TrajectoriesChecker(
+ ModelFormChecker,
+):
+ form_class = proposal_forms.StudyStartForm
+ title = _("Trajecten")
+ location = "studies"
+
+ def check(
+ self,
+ ):
+ self.item = self.make_stepper_item()
+ self.stepper.items.append(self.item)
+ sub_items = [self.make_study_checker(s) for s in self.get_studies()]
+ return sub_items + self.remaining_checkers()
+
+ def make_study_checker(self, study):
+ return StudyChecker(
+ self.stepper,
+ parent=self.item,
+ study=study,
+ )
+
+ def remaining_checkers(
+ self,
+ ):
+ return [
+ DocumentsChecker,
+ DataManagementChecker,
+ SubmitChecker,
+ ]
+
+ def get_studies(
+ self,
+ ):
+ proposal = self.stepper.proposal
+ return list(
+ proposal.study_set.all(),
+ )
+
+ def get_url(
+ self,
+ ):
+ return reverse(
+ "proposals:study_start",
+ args=[self.proposal.pk],
+ )
+
+
+class StudyChecker(
+ Checker,
+):
+ def __init__(self, *args, **kwargs):
+ self.study = kwargs.pop("study")
+ return super().__init__(*args, **kwargs)
+
+ def check(
+ self,
+ ):
+ self.current_parent = self.parent
+ checkers = []
+ if self.stepper.has_multiple_studies():
+ # Create an intermediate container item per study,
+ # if we have more than one
+ self.current_parent = ContainerItem(
+ stepper=self.stepper,
+ title=self.study.name,
+ parent=self.parent,
+ )
+ self.stepper.items.append(self.current_parent)
+ # We always have a Participants and StudyDesign item
+ checkers = [
+ ParticipantsChecker(
+ self.stepper,
+ study=self.study,
+ parent=self.current_parent,
+ ),
+ DesignChecker(
+ self.stepper,
+ study=self.study,
+ parent=self.current_parent,
+ ),
+ ]
+ end_checker = StudyEndChecker(
+ self.stepper,
+ study=self.study,
+ parent=self.current_parent,
+ )
+ return checkers + self.determine_study_checkers(self.study) + [end_checker]
+
+ def determine_study_checkers(self, study):
+ tests = {
+ self.make_intervention: lambda s: s.has_intervention,
+ self.make_observation: lambda s: s.has_observation,
+ self.make_sessions: lambda s: s.has_sessions,
+ }
+ optional_checkers = [
+ maker(study) for maker, test in tests.items() if test(study)
+ ]
+ return optional_checkers
+
+ def make_intervention(self, study):
+ return InterventionChecker(
+ self.stepper,
+ study=study,
+ parent=self.current_parent,
+ )
+
+ def make_observation(self, study):
+ return ObservationChecker(
+ self.stepper,
+ study=study,
+ parent=self.current_parent,
+ )
+
+ def make_sessions(self, study):
+ return SessionsChecker(
+ self.stepper,
+ study=study,
+ parent=self.current_parent,
+ )
+
+
+class ParticipantsChecker(
+ ModelFormChecker,
+):
+ title = _("Deelnemers")
+ form_class = study_forms.StudyForm
+
+ def __init__(
+ self,
+ *args,
+ **kwargs,
+ ):
+ self.study = kwargs.pop("study")
+ return super().__init__(*args, **kwargs)
+
+ def check(
+ self,
+ ):
+ self.stepper.items.append(self.make_stepper_item())
+ return []
+
+ def get_url(
+ self,
+ ):
+ return reverse(
+ "studies:update",
+ args=[
+ self.study.pk,
+ ],
+ )
+
+
+class DesignChecker(
+ ModelFormChecker,
+):
+ title = _("Ontwerp")
+ form_class = study_forms.StudyDesignForm
+
+ def __init__(
+ self,
+ *args,
+ **kwargs,
+ ):
+ self.study = kwargs.pop("study")
+ return super().__init__(*args, **kwargs)
+
+ def check(
+ self,
+ ):
+ self.stepper.items.append(self.make_stepper_item())
+ return []
+
+ def get_url(
+ self,
+ ):
+ return reverse(
+ "studies:design",
+ args=[
+ self.study.pk,
+ ],
+ )
+
+
+class StudyEndChecker(
+ ModelFormChecker,
+):
+ title = _("Afronding")
+ form_class = study_forms.StudyEndForm
+
+ def __init__(
+ self,
+ *args,
+ **kwargs,
+ ):
+ self.study = kwargs.pop("study")
+ return super().__init__(*args, **kwargs)
+
+ def check(
+ self,
+ ):
+ self.stepper.items.append(self.make_stepper_item())
+ return []
+
+ def get_url(
+ self,
+ ):
+ return reverse(
+ "studies:design_end",
+ args=[
+ self.study.pk,
+ ],
+ )
+
+
+class InterventionChecker(
+ UpdateOrCreateChecker,
+):
+ form_class = intervention_forms.InterventionForm
+ title = _("Interventie")
+
+ def __init__(
+ self,
+ *args,
+ **kwargs,
+ ):
+ self.study = kwargs.pop("study")
+ return super().__init__(*args, **kwargs)
+
+ def check(
+ self,
+ ):
+ self.stepper.items.append(
+ self.make_stepper_item(),
+ )
+ return []
+
+ def object_exists(
+ self,
+ ):
+ return hasattr(
+ self.study,
+ "intervention",
+ )
+
+ def get_form_object(
+ self,
+ ):
+ return self.study.intervention
+
+ def get_create_url(
+ self,
+ ):
+ return reverse(
+ "interventions:create",
+ args=[self.study.pk],
+ )
+
+ def get_update_url(
+ self,
+ ):
+ return reverse(
+ "interventions:update",
+ args=[self.study.intervention.pk],
+ )
+
+
+class ObservationChecker(
+ UpdateOrCreateChecker,
+):
+
+ form_class = observation_forms.ObservationForm
+ title = _("Observatie")
+
+ def __init__(
+ self,
+ *args,
+ **kwargs,
+ ):
+ self.study = kwargs.pop("study")
+ return super().__init__(*args, **kwargs)
+
+ def check(
+ self,
+ ):
+ self.stepper.items.append(
+ self.make_stepper_item(),
+ )
+ return []
+
+ def object_exists(
+ self,
+ ):
+ return hasattr(
+ self.study,
+ "observation",
+ )
+
+ def get_create_url(
+ self,
+ ):
+ return reverse(
+ "observations:create",
+ args=[self.study.pk],
+ )
+
+ def get_update_url(
+ self,
+ ):
+ return reverse(
+ "observations:update",
+ args=[self.study.observation.pk],
+ )
+
+ def get_form_object(
+ self,
+ ):
+ return self.study.observation
+
+
+class SessionsChecker(
+ ModelFormChecker,
+):
+
+ form_class = tasks_forms.SessionOverviewForm
+ title = _("Sessies")
+
+ def __init__(
+ self,
+ *args,
+ **kwargs,
+ ):
+ self.study = kwargs.pop("study")
+ return super().__init__(*args, **kwargs)
+
+ def check(
+ self,
+ ):
+ self.stepper.items.append(
+ self.make_stepper_item(),
+ )
+ return []
+
+ def make_stepper_item(
+ self,
+ ):
+ """
+ An absolutely ridiculous piece of work just to have the right
+ stepper item be bold for all underlying task/session views.
+ Don't ask me how much time I spent on this.
+ """
+ item = super().make_stepper_item()
+
+ def modified_is_current(self, request):
+ if request.path_info == self.get_url():
+ return True
+ # Strip beginning of request path
+ subpath = request.path_info.replace(
+ "/tasks/",
+ "",
+ 1,
+ )
+
+ # Define function to match PK
+ def pk_matches_study(view, given_pk, study):
+ # These views have a Study object
+ if view in [
+ session_views.SessionCreate,
+ session_views.SessionOverview,
+ ]:
+ return given_pk == study.pk
+ # These have a Task object
+ if view in [
+ task_views.TaskUpdate,
+ task_views.TaskDelete,
+ ]:
+ return Task.objects.filter(
+ pk=given_pk,
+ session__in=study.session_set.all(),
+ ).exists()
+ # Everything else has a Session object
+ else:
+ return Session.objects.filter(
+ pk=given_pk,
+ study__pk=study.pk,
+ ).exists()
+
+ # Check tasks URL patterns
+ from tasks.urls import urlpatterns
+
+ for pat in urlpatterns:
+ # If any of them match, check the PK
+ if match := pat.resolve(subpath):
+ view = match.func.view_class
+ pk = match.kwargs["pk"]
+ return pk_matches_study(view, pk, self.study)
+ # If all else fails, guess we're not current
+ return False
+
+ # Overwrite method at the instance level
+ # Strategy taken from types.MethodType and copied verbatim
+ # here for clarity
+ class _C: # NoQA
+ def _m(
+ self,
+ ):
+ pass
+
+ MethodType = type(_C()._m)
+ # The type of _m is a method, bound to class instance _C, and
+ # MethodType is its constructor. MethodType takes a function
+ # and a class instance and returns a method bound to that instance,
+ # allowing for self-reference as expected.
+ item.is_current = MethodType(modified_is_current, item)
+ # Provide study to the modified is_current
+ item.study = self.study
+ return item
+
+ def get_form_object(
+ self,
+ ):
+ return self.study
+
+ def get_url(
+ self,
+ ):
+ return reverse(
+ "tasks:session_overview",
+ args=[self.study.pk],
+ )
+
+
+class DocumentsChecker(
+ Checker,
+):
+ def make_stepper_item(
+ self,
+ ):
+ return ContainerItem(
+ self.stepper,
+ title=_("Documenten"),
+ location="attachments",
+ )
+
+ def check(
+ self,
+ ):
+ item = self.make_stepper_item()
+ self.stepper.items.append(item)
+ return [
+ TranslationChecker(
+ self.stepper,
+ parent=item,
+ ),
+ AttachmentsChecker(
+ self.stepper,
+ parent=item,
+ ),
+ ]
+
+
+class TranslationChecker(
+ ModelFormChecker,
+):
+ form_class = proposal_forms.TranslatedConsentForms
+ title = _("Vertalingen")
+ location = "data_management"
+
+ def check(
+ self,
+ ):
+ self.stepper.items.append(self.make_stepper_item())
+ return []
+
+ def get_url(
+ self,
+ ):
+ return reverse(
+ "proposals:translated",
+ args=[
+ self.stepper.proposal.pk,
+ ],
+ )
+
+
+class AttachmentsChecker(
+ Checker,
+):
+
+ def check(
+ self,
+ ):
+ self.stepper.items.append(self.make_stepper_item())
+ return []
+
+ def make_stepper_item(self):
+ url = reverse(
+ "proposals:consent",
+ args=[self.stepper.proposal.pk],
+ )
+ item = PlaceholderItem(
+ self.stepper,
+ title=_("Documenten beheren"),
+ parent=self.parent,
+ )
+ item.get_url = lambda: url
+ return item
+
+
+class DataManagementChecker(
+ ModelFormChecker,
+):
+ title = _("Data management")
+ form_class = proposal_forms.ProposalDataManagementForm
+ location = "data_management"
+
+ def check(
+ self,
+ ):
+ self.stepper.items.append(
+ self.make_stepper_item(),
+ )
+ return []
+
+ def get_url(
+ self,
+ ):
+ return reverse(
+ "proposals:data_management",
+ args=[
+ self.stepper.proposal.pk,
+ ],
+ )
+
+
+class SubmitChecker(
+ ModelFormChecker,
+):
+ title = _("Indienen")
+ form_class = proposal_forms.ProposalSubmitForm
+ location = "submit"
+
+ def check(
+ self,
+ ):
+ self.stepper.items.append(
+ self.make_stepper_item(),
+ )
+ return []
+
+ def get_url(
+ self,
+ ):
+ return reverse(
+ "proposals:submit",
+ args=[
+ self.stepper.proposal.pk,
+ ],
+ )
diff --git a/proposals/utils/stepper.py b/proposals/utils/stepper.py
new file mode 100644
index 000000000..723c69a20
--- /dev/null
+++ b/proposals/utils/stepper.py
@@ -0,0 +1,163 @@
+from copy import copy
+
+from django.utils.translation import gettext as _
+
+from main.utils import renderable
+
+from .stepper_helpers import (
+ PlaceholderItem,
+ StepperItem,
+)
+
+from .checkers import ProposalTypeChecker
+
+
+class Stepper(renderable):
+
+ template_name = "base/stepper.html"
+
+ def __init__(
+ self,
+ proposal,
+ proposal_type_hint=None,
+ request=None,
+ ):
+ self.proposal = proposal
+ self.starting_checkers = [
+ ProposalTypeChecker,
+ ]
+ # The stepper keeps track of the request to determine
+ # which item is current
+ self.request = request
+ # The type can be provided by the view for the case in which
+ # the proposal has not yet been created but we need to determine
+ # its type, i.e. the ProposalCreateViews
+ self.proposal_type_hint = proposal_type_hint
+ self.items = []
+ self.check_all(self.starting_checkers)
+
+ def get_context_data(self):
+ context = super().get_context_data()
+ # Provide the stepper bubble classes in order
+ # of descending size
+ bubble_list = [
+ "stepper-bubble-largest",
+ "stepper-bubble-large",
+ "stepper-bubble-medium",
+ "stepper-bubble-small",
+ "stepper-bubble-smallest",
+ ]
+ context.update(
+ {
+ "stepper": self,
+ "bubble_size": bubble_list,
+ }
+ )
+ return context
+
+ def get_resume_url(self):
+ """
+ Returns the url of the first page that requires attention,
+ that being either a page with an error or an incomplete page.
+ """
+ for item in self.items:
+ if not item.is_complete:
+ return item.get_url()
+
+ def build_stepper(
+ self,
+ ):
+ """
+ The meat and potatoes of the stepper. Returns a list of top-level
+ StepperItems to be rendered in the template.
+ """
+ # In building the stepper we will be editing this layout in-place,
+ # which means we need to make a copy. Otherwise we're editing the
+ # original RegularProposalLayout which causes strange behaviour
+ # when it is in use by any other steppers.
+ layout = copy(getattr(self, "layout", False))
+ if not layout:
+ # Layout should be set before building the stepper
+ # by something like ProposalTypeChecker
+ raise RuntimeError(
+ "Base layout was never defined for this stepper",
+ )
+ # First, insert all items into the layout
+ for item in self.items:
+ self._insert_item(layout, item)
+ # Second, replace all remaining empty slots in the layout
+ # by PlaceholderItems
+ self._insert_placeholders(layout)
+ return layout
+
+ def _insert_item(self, layout, new_item):
+ """
+ Inserts a stepper item into a layout, in-place.
+ """
+ # We're only concerned with top-level items, children can sort
+ # themselves out
+ if new_item.parent:
+ return new_item.parent.children.append(
+ new_item,
+ )
+ # Step through the layout looking for empty slots, which are
+ # represented by tuples of locations and titles
+ for index, slot in enumerate(layout):
+ # If the slot is already filled with an actual item, just
+ # skip it
+ if type(slot) is not tuple:
+ continue
+ # If the slot location matches that of our new_item, replace
+ # this slot with the item
+ if new_item.location == slot[0]:
+ layout.insert(index, new_item)
+ layout.remove(slot)
+
+ def _insert_placeholders(self, layout):
+ # Step through the remaining slots in the layout
+ for index, slot in enumerate(layout):
+ # Skip slots that are already items
+ if isinstance(slot, StepperItem):
+ continue
+ # Remaining empty slots are replaced by placeholders
+ placeholder = PlaceholderItem(
+ self,
+ title=slot[1],
+ )
+ layout.insert(index, placeholder)
+ layout.remove(slot)
+ return layout
+
+ def check_all(self, next_checkers):
+ # No more checkers means we are done
+ if next_checkers == []:
+ return True
+ # Instantiate next checker
+ # and give it access to the stepper
+ current = next_checkers.pop(0)(self)
+ # Run the check method
+ # and gather new checkers from its output
+ new_checkers = current.check()
+ # Combine the lists, new checkers come first
+ next_checkers = new_checkers + next_checkers
+ # Recurse until next_checkers is empty
+ return self.check_all(next_checkers)
+
+ def has_multiple_studies(
+ self,
+ ):
+ """
+ Returns True if the proposal has more than one trajectory (study).
+ """
+ num_studies = self.proposal.study_set.count()
+ return num_studies > 1
+
+
+RegularProposalLayout = [
+ ("create", _("Basisgegevens")),
+ ("wmo", _("WMO")),
+ ("studies", _("Trajecten")),
+ ("attachments", _("Documenten")),
+ ("data_management", _("Datamanagement")),
+ ("submit", _("Indienen")),
+]
diff --git a/proposals/utils/stepper_helpers.py b/proposals/utils/stepper_helpers.py
new file mode 100644
index 000000000..418113239
--- /dev/null
+++ b/proposals/utils/stepper_helpers.py
@@ -0,0 +1,288 @@
+from django.core.exceptions import ImproperlyConfigured
+from braces.forms import UserKwargModelFormMixin
+
+from main.utils import renderable
+
+
+class BaseStepperComponent:
+
+ def __init__(self, stepper, parent=None):
+ self.stepper = stepper
+ self.proposal = stepper.proposal
+ self.parent = parent
+
+
+class Checker(
+ BaseStepperComponent,
+):
+
+ def __call__(self, *args, **kwargs):
+ """
+ This class may be called to initialize it when it is
+ already initialized. For now, we don't do anything with this
+ and pretend we just got initialized.
+ """
+ return self
+
+ def check(self):
+ """
+ This method gets called to process an item in the proposal creation
+ process. It finally returns a list of checkers with which to continue
+ the checking process. This list can be empty.
+ """
+ return []
+
+
+class StepperItem(
+ renderable,
+):
+ """
+ Represents an item in the stepper
+ """
+
+ template_name = "base/stepper_item.html"
+ title = "Stepper item"
+ location = None
+
+ def __init__(self, stepper, parent=None, disabled=False, title=None, location=None):
+ self.stepper = stepper
+ self.proposal = stepper.proposal
+ self.children = []
+ self.parent = parent
+ self.available = False
+ self.disabled = disabled
+ # Don't override default location if not provided explicitly
+ if location:
+ self.location = location
+ if title:
+ self.title = title
+
+ def get_context_data(self, *args, **kwargs):
+ context = super().get_context_data(*args, **kwargs)
+ context.update(
+ {
+ "item": self,
+ }
+ )
+ return context
+
+ def get_url(self):
+ return "#"
+
+ def get_errors(self):
+ return []
+
+ def css_classes(
+ self,
+ ):
+ classes = []
+ if self.is_current(self.stepper.request):
+ classes.append(
+ "active",
+ )
+ if self.disabled:
+ classes.append(
+ "disabled",
+ )
+ return " ".join(classes)
+
+ def is_current(self, request):
+ """
+ Returns True if this item represents the page the user
+ is currently on.
+ """
+ if request.path == self.get_url():
+ return True
+ return False
+
+
+class PlaceholderItem(StepperItem):
+
+ def __init__(self, *args, **kwargs):
+ return super().__init__(*args, **kwargs)
+
+ def get_url(
+ self,
+ ):
+ return ""
+
+
+class ContainerItem(
+ StepperItem,
+):
+ """
+ A basic stepper item that is nothing more than a parent for its
+ children. Its url will try to redirect to its first child.
+ """
+
+ def get_url(self):
+ try:
+ url = self.children[0].get_url()
+ return url
+ except:
+ return ""
+
+ def is_current(self, request):
+ """
+ Because container items by default refer to their first child,
+ we say they are never current. The child is.
+ """
+ return False
+
+
+class ModelFormChecker(
+ Checker,
+):
+ """
+ A checker base that makes it as easy as possible to go from checking
+ a ModelForm to making a stepper item.
+ """
+
+ form_class = None
+ title = None
+ location = None
+
+ def __init__(self, *args, **kwargs):
+ if not self.form_class:
+ raise ImproperlyConfigured("form_class must be defined")
+ return super().__init__(*args, **kwargs)
+
+ def make_stepper_item(self):
+ if not self.title:
+ raise ImproperlyConfigured("title must be defined")
+ stepper_item = ModelFormItem(
+ self.stepper,
+ title=self.title,
+ parent=self.parent,
+ form_object=self.get_form_object(),
+ form_class=self.form_class,
+ url_func=self.get_url,
+ location=self.location,
+ )
+ return stepper_item
+
+ def get_form_object(
+ self,
+ ):
+ # Overwrite method for other objects
+ return self.proposal
+
+
+class ModelFormItem(
+ StepperItem,
+):
+
+ def __init__(self, *args, **kwargs):
+ self.form_class = kwargs.pop(
+ "form_class",
+ )
+ self.form_object = kwargs.pop(
+ "form_object",
+ )
+ get_url = kwargs.pop(
+ "url_func",
+ None,
+ )
+ self.errors = []
+ if get_url:
+ self.get_url = get_url
+ return super().__init__(*args, **kwargs)
+
+ @property
+ def model_form(self):
+ """
+ This property is used to access the bound ModelForm and its errors.
+ self.instantiated_form can be set by hand (e.g. by a checker) to bypass
+ default form instantiation.
+ """
+ if not hasattr(self, "instantiated_form"):
+ self.instantiated_form = self.instantiate_form()
+ return self.instantiated_form
+
+ def get_form_object(self):
+ """
+ Override this for modelforms that don't relate to a
+ Proposal model.
+ """
+ return self.proposal
+
+ def get_form_kwargs(self):
+ """
+ This method can be overidden to provide extra kwargs to the form. But
+ kwargs that pop up often can also be added to this base class.
+ """
+ kwargs = {}
+ if issubclass(self.form_class, UserKwargModelFormMixin):
+ kwargs["user"] = self.stepper.request.user
+ return kwargs
+
+ def instantiate_form(self):
+ if not self.form_class:
+ raise ImproperlyConfigured("form_class must be defined")
+ kwargs = self.get_form_kwargs()
+ model_form = self.form_class(
+ instance=self.get_form_object(),
+ **kwargs,
+ )
+ return model_form
+
+ def get_errors(self):
+ """
+ This is a placeholder that just returns the form errors for now.
+ But once we've figured out what we want exactly this method should
+ return both the form errors and any extra errors that a Checker
+ might insert into this item.
+ """
+ return self.model_form.errors
+
+
+class UpdateOrCreateChecker(
+ ModelFormChecker,
+):
+ """
+ A variation on the ModelFormChecker designed for
+ forms like the InterventionForm, which link either
+ to a create or update view depending on if they exist
+ already.
+
+ If the object in question exists, this class acts like
+ a normal ModelFormChecker. Otherwise it provides a factory
+ for a PlaceholderItem that links to the CreateView.
+ """
+
+ def make_stepper_item(
+ self,
+ ):
+ if self.object_exists():
+ return super().make_stepper_item()
+ return self.make_placeholder_item()
+
+ def make_placeholder_item(
+ self,
+ ):
+ item = PlaceholderItem(
+ self.stepper,
+ title=self.title,
+ parent=self.parent,
+ )
+ item.get_url = self.get_create_url
+ return item
+
+ def object_exists(
+ self,
+ ):
+ # By default, assume the object exists
+ return True
+
+ def get_url(self):
+ return self.get_update_url()
+
+ def get_create_url(
+ self,
+ ):
+ return ""
+
+ def get_update_url(
+ self,
+ ):
+ return ""
diff --git a/proposals/views/proposal_views.py b/proposals/views/proposal_views.py
index 9f5a84bb2..4949a7f68 100644
--- a/proposals/views/proposal_views.py
+++ b/proposals/views/proposal_views.py
@@ -47,6 +47,7 @@
ResearchGoalForm,
PreApprovedForm,
TranslatedConsentForms,
+ ProposalForm,
)
from ..models import Proposal, Wmo
from ..utils import generate_pdf, generate_ref_number
@@ -241,6 +242,13 @@ class ProposalCreate(ProposalMixin, AllowErrorsOnBackbuttonMixin, CreateView):
# Note: template_name is auto-generated to proposal_form.html
success_message = _("Aanvraag %(title)s aangemaakt")
+ proposal_type_hint = "regular"
+ form_class = ProposalForm
+
+ def get_proposal(
+ self,
+ ):
+ return self.get_form().instance
def form_valid(self, form):
"""
@@ -264,10 +272,15 @@ def get_context_data(self, **kwargs):
context["no_back"] = True
return context
+ def get_next_url(self):
+ return reverse("proposals:researcher", args=(self.object.pk,))
+
class ProposalUpdate(
ProposalMixin, ProposalContextMixin, AllowErrorsOnBackbuttonMixin, UpdateView
):
+ form_class = ProposalForm
+
def form_valid(self, form):
"""Sets created_by to current user and generates a reference number"""
form.instance.reviewing_committee = form.instance.institution.reviewing_chamber
@@ -281,6 +294,9 @@ def get_context_data(self, **kwargs):
return context
+ def get_next_url(self):
+ return reverse("proposals:researcher", args=(self.object.pk,))
+
class ProposalDelete(DeleteView):
model = Proposal
@@ -372,7 +388,11 @@ def get_context_data(self, **kwargs):
class ProposalResearcherFormView(
- UserFormKwargsMixin, ProposalContextMixin, AllowErrorsOnBackbuttonMixin, UpdateView
+ ProposalMixin,
+ UserFormKwargsMixin,
+ ProposalContextMixin,
+ AllowErrorsOnBackbuttonMixin,
+ UpdateView,
):
model = Proposal
form_class = ResearcherForm
@@ -386,7 +406,10 @@ def get_back_url(self):
class ProposalOtherResearchersFormView(
- UserFormKwargsMixin, ProposalContextMixin, AllowErrorsOnBackbuttonMixin, UpdateView
+ UserFormKwargsMixin,
+ AllowErrorsOnBackbuttonMixin,
+ ProposalMixin,
+ UpdateView,
):
model = Proposal
form_class = OtherResearchersForm
@@ -462,7 +485,7 @@ def get_back_url(self):
return reverse("proposals:research_goal", args=(self.object.pk,))
-class TranslatedConsentFormsView(UpdateView):
+class TranslatedConsentFormsView(ProposalContextMixin, UpdateView):
model = Proposal
form_class = TranslatedConsentForms
template_name = "proposals/translated_consent_forms.html"
@@ -476,7 +499,7 @@ def get_back_url(self):
return reverse("studies:design_end", args=(self.object.last_study().pk,))
-class ProposalDataManagement(UpdateView):
+class ProposalDataManagement(ProposalContextMixin, UpdateView):
model = Proposal
form_class = ProposalDataManagementForm
template_name = "proposals/proposal_data_management.html"
diff --git a/proposals/views/study_views.py b/proposals/views/study_views.py
index 9d35f404c..79b428d53 100644
--- a/proposals/views/study_views.py
+++ b/proposals/views/study_views.py
@@ -4,6 +4,7 @@
from django.utils.translation import gettext_lazy as _
from main.views import AllowErrorsOnBackbuttonMixin, UpdateView, FormSetUpdateView
+from proposals.mixins import ProposalContextMixin
from studies.models import Documents, Study
from studies.forms import StudyConsentForm
from studies.utils import create_documents_for_study
@@ -11,7 +12,11 @@
from ..models import Proposal
-class StudyStart(AllowErrorsOnBackbuttonMixin, UpdateView):
+class StudyStart(
+ ProposalContextMixin,
+ AllowErrorsOnBackbuttonMixin,
+ UpdateView,
+):
model = Proposal
form_class = StudyStartForm
template_name = "proposals/study_start.html"
diff --git a/proposals/views/wmo_views.py b/proposals/views/wmo_views.py
index 6a1db57aa..098614745 100644
--- a/proposals/views/wmo_views.py
+++ b/proposals/views/wmo_views.py
@@ -9,6 +9,7 @@
from main.models import YesNoDoubt
from main.views import CreateView, UpdateView, AllowErrorsOnBackbuttonMixin
from main.utils import get_secretary
+from proposals.mixins import StepperContextMixin
from ..models import Proposal, Wmo
from ..forms import WmoForm, WmoApplicationForm, WmoCheckForm
@@ -52,7 +53,11 @@ def get_proposal(self):
raise NotImplementedError
-class WmoCreate(WmoMixin, CreateView):
+class WmoCreate(
+ StepperContextMixin,
+ WmoMixin,
+ CreateView,
+):
success_message = _("WMO-gegevens opgeslagen")
def form_valid(self, form):
@@ -65,7 +70,11 @@ def get_proposal(self):
return Proposal.objects.get(pk=self.kwargs["pk"])
-class WmoUpdate(WmoMixin, UpdateView):
+class WmoUpdate(
+ StepperContextMixin,
+ WmoMixin,
+ UpdateView,
+):
success_message = _("WMO-gegevens bewerkt")
def get_proposal(self):
@@ -76,7 +85,10 @@ def get_proposal(self):
######################
# Other actions on WMO
######################
-class WmoApplication(UpdateView):
+class WmoApplication(
+ UpdateView,
+ StepperContextMixin,
+):
model = Wmo
form_class = WmoApplicationForm
template_name = "proposals/wmo_application.html"
diff --git a/studies/mixins.py b/studies/mixins.py
new file mode 100644
index 000000000..7f00ba24f
--- /dev/null
+++ b/studies/mixins.py
@@ -0,0 +1,24 @@
+from .models import Study
+from proposals.mixins import StepperContextMixin
+
+
+class StudyMixin(
+ StepperContextMixin,
+):
+
+ def get_proposal(
+ self,
+ ):
+ return self.get_object().proposal
+
+
+class StudyFromURLMixin:
+
+ def get_study(self):
+ """Retrieves the Study from the pk kwarg"""
+ return Study.objects.get(pk=self.kwargs["pk"])
+
+ def get_proposal(
+ self,
+ ):
+ return self.get_study().proposal
diff --git a/studies/views/study_views.py b/studies/views/study_views.py
index c9efe4e65..2e7dcc3c6 100644
--- a/studies/views/study_views.py
+++ b/studies/views/study_views.py
@@ -23,12 +23,17 @@
)
from ..models import Study, Documents
from ..utils import check_has_adults, check_necessity_required, get_study_progress
+from ..mixins import StudyMixin
#######################
# CRUD actions on Study
#######################
-class StudyUpdate(AllowErrorsOnBackbuttonMixin, UpdateView):
+class StudyUpdate(
+ StudyMixin,
+ AllowErrorsOnBackbuttonMixin,
+ UpdateView,
+):
"""Updates a Study from a StudyForm"""
model = Study
@@ -39,7 +44,7 @@ def get_context_data(self, **kwargs):
"""Setting the progress on the context"""
context = super(StudyUpdate, self).get_context_data(**kwargs)
context["progress"] = get_study_progress(self.object)
- context["proposal"] = self.object.proposal
+ context["proposal"] = self.get_proposal()
return context
def get_form_kwargs(self):
@@ -65,7 +70,9 @@ def get_next_url(self):
###############
# Other actions
###############
-class StudyDesign(AllowErrorsOnBackbuttonMixin, UpdateView, generic.edit.FormMixin):
+class StudyDesign(
+ StudyMixin, AllowErrorsOnBackbuttonMixin, UpdateView, generic.edit.FormMixin
+):
model = Study
form_class = StudyDesignForm
success_message = _("Traject opgeslagen")
@@ -134,7 +141,11 @@ def get_back_url(self):
return reverse("studies:update", args=(self.kwargs["pk"],))
-class StudyEnd(AllowErrorsOnBackbuttonMixin, UpdateView):
+class StudyEnd(
+ StudyMixin,
+ AllowErrorsOnBackbuttonMixin,
+ UpdateView,
+):
"""
Completes a Study
"""
diff --git a/tasks/views/session_views.py b/tasks/views/session_views.py
index 631c5a067..2a7ec30d2 100644
--- a/tasks/views/session_views.py
+++ b/tasks/views/session_views.py
@@ -6,6 +6,8 @@
from django.utils.translation import gettext_lazy as _
from main.views import AllowErrorsOnBackbuttonMixin, UpdateView, DeleteView, CreateView
+from proposals.mixins import StepperContextMixin
+from studies.mixins import StudyFromURLMixin, StudyMixin
from ..forms import SessionUpdateForm, SessionEndForm, SessionOverviewForm
from ..models import Session, Study
@@ -44,7 +46,10 @@ def form_valid(self, form):
##################
-class SessionMixin(AllowErrorsOnBackbuttonMixin):
+class SessionMixin(
+ StepperContextMixin,
+ AllowErrorsOnBackbuttonMixin,
+):
model = Session
form_class = SessionUpdateForm
@@ -63,8 +68,13 @@ def get_study(self):
def get_next_url(self):
return reverse("tasks:session_end", args=(self.object.pk,))
+ def get_proposal(
+ self,
+ ):
+ return self.get_object().study.proposal
-class SessionStart(SessionMixin, UpdateView):
+
+class SessionStart(StudyMixin, UpdateView):
model = Study
# This form is just a placeholder to make navigation work. It does not do
@@ -87,11 +97,12 @@ def get_back_url(self):
pk = study.intervention.pk
return reverse(next_url, args=(pk,))
- def get_study(self):
- return self.object
-
-class SessionCreate(SessionMixin, CreateView):
+class SessionCreate(
+ StudyFromURLMixin,
+ SessionMixin,
+ CreateView,
+):
def get_form_kwargs(self):
"""Sets the Study as a form kwarg"""
@@ -106,10 +117,6 @@ def form_valid(self, form):
form.instance.order = study.session_set.count() + 1
return super(SessionCreate, self).form_valid(form)
- def get_study(self):
- """Retrieves the Study from the pk kwarg"""
- return Study.objects.get(pk=self.kwargs["pk"])
-
def get_back_url(self):
return reverse("tasks:session_overview", args=(self.object.study.pk,))
@@ -153,7 +160,7 @@ def get_back_url(self):
return reverse("tasks:session_overview", args=(self.object.study.pk,))
-class SessionOverview(UpdateView):
+class SessionOverview(StudyMixin, UpdateView):
model = Study
form_class = SessionOverviewForm
diff --git a/tasks/views/task_views.py b/tasks/views/task_views.py
index fc746d515..82a86969c 100644
--- a/tasks/views/task_views.py
+++ b/tasks/views/task_views.py
@@ -8,6 +8,7 @@
from main.views import AllowErrorsOnBackbuttonMixin, UpdateView, DeleteView, CreateView
from ..forms import TaskForm
from ..models import Task, Session
+from proposals.mixins import StepperContextMixin
######################
@@ -15,7 +16,10 @@
######################
-class TaskMixin(AllowErrorsOnBackbuttonMixin):
+class TaskMixin(
+ StepperContextMixin,
+ AllowErrorsOnBackbuttonMixin,
+):
model = Task
form_class = TaskForm
template_name = "tasks/task_update.html"
@@ -25,7 +29,7 @@ def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
session = self.get_session()
context["session"] = session
- context["proposal"] = session.study.proposal
+ context["proposal"] = self.get_proposal()
try:
context["order"] = self.object.order
except AttributeError:
@@ -37,7 +41,10 @@ def get_context_data(self, **kwargs):
return context
def get_session(self):
- return self.object.session
+ return self.get_object().session
+
+ def get_proposal(self):
+ return self.get_object().session.study.proposal
def get_next_url(self):
return reverse("tasks:session_end", args=(self.object.session.pk,))