From 13af8646d0e2b359297035e177323445f3de9708 Mon Sep 17 00:00:00 2001
From: Todd Dembrey <todd.dembrey@torchbox.com>
Date: Thu, 2 Aug 2018 09:15:58 +0100
Subject: [PATCH] Refactor to group the models

---
 opentech/apply/funds/models/__init__.py       | 500 +++++++++++++++++
 .../{models.py => models/submissions.py}      | 506 +-----------------
 2 files changed, 508 insertions(+), 498 deletions(-)
 create mode 100644 opentech/apply/funds/models/__init__.py
 rename opentech/apply/funds/{models.py => models/submissions.py} (55%)

diff --git a/opentech/apply/funds/models/__init__.py b/opentech/apply/funds/models/__init__.py
new file mode 100644
index 000000000..b0ecc8ab3
--- /dev/null
+++ b/opentech/apply/funds/models/__init__.py
@@ -0,0 +1,500 @@
+from datetime import date
+
+from django.conf import settings
+from django.core.exceptions import ValidationError
+from django.db import models
+from django.db.models import Q
+from django.http import Http404
+from django.urls import reverse
+from django.utils.text import mark_safe
+from django.utils.translation import ugettext_lazy as _
+
+from modelcluster.fields import ParentalKey, ParentalManyToManyField
+
+from wagtail.admin.edit_handlers import (
+    FieldPanel,
+    FieldRowPanel,
+    InlinePanel,
+    MultiFieldPanel,
+    ObjectList,
+    StreamFieldPanel,
+    TabbedInterface,
+)
+from wagtail.core.fields import StreamField
+from wagtail.core.models import Orderable
+from wagtail.contrib.forms.models import AbstractEmailForm
+
+from opentech.apply.activity.messaging import messenger, MESSAGES
+from opentech.apply.stream_forms.models import AbstractStreamForm
+from opentech.apply.users.groups import REVIEWER_GROUP_NAME, STAFF_GROUP_NAME
+
+from ..admin_forms import WorkflowFormAdminForm
+from ..blocks import ApplicationCustomFormFieldsBlock
+from ..edit_handlers import FilteredFieldPanel, ReadOnlyPanel, ReadOnlyInlinePanel
+from ..workflow import WORKFLOWS
+
+LIMIT_TO_STAFF = {'groups__name': STAFF_GROUP_NAME}
+LIMIT_TO_REVIEWERS = {'groups__name': REVIEWER_GROUP_NAME}
+LIMIT_TO_STAFF_AND_REVIEWERS = {'groups__name__in': [STAFF_GROUP_NAME, REVIEWER_GROUP_NAME]}
+
+
+class WorkflowHelpers(models.Model):
+    """
+    Defines the common methods and fields for working with Workflows within Django models
+    """
+    class Meta:
+        abstract = True
+
+    WORKFLOW_CHOICES = {
+        name: workflow.name
+        for name, workflow in WORKFLOWS.items()
+    }
+
+    workflow_name = models.CharField(choices=WORKFLOW_CHOICES.items(), max_length=100, default='single', verbose_name="Workflow")
+
+    @property
+    def workflow(self):
+        return WORKFLOWS[self.workflow_name]
+
+
+from .submissions import ApplicationSubmission, ApplicationRevision
+
+
+__all__ = ['ApplicationSubmission', 'ApplicationRevision']
+
+
+def admin_url(page):
+    return reverse('wagtailadmin_pages:edit', args=(page.id,))
+
+
+class SubmittableStreamForm(AbstractStreamForm):
+    """
+    Controls how stream forms are submitted. Any Page allowing submissions should inherit from here.
+    """
+    class Meta:
+        abstract = True
+
+    def get_submission_class(self):
+        return ApplicationSubmission
+
+    def process_form_submission(self, form):
+        if not form.user.is_authenticated:
+            form.user = None
+        return self.get_submission_class().objects.create(
+            form_data=form.cleaned_data,
+            form_fields=self.get_defined_fields(),
+            **self.get_submit_meta_data(user=form.user),
+        )
+
+    def get_submit_meta_data(self, **kwargs):
+        return kwargs
+
+
+class WorkflowStreamForm(WorkflowHelpers, AbstractStreamForm):  # type: ignore
+    """
+    Defines the common methods and fields for working with Workflows within Wagtail pages
+    """
+    class Meta:
+        abstract = True
+
+    def get_defined_fields(self, stage=None):
+        if not stage:
+            form_index = 0
+        else:
+            form_index = self.workflow.stages.index(stage)
+        return self.forms.all()[form_index].fields
+
+    def render_landing_page(self, request, form_submission=None, *args, **kwargs):
+        # We only reach this page after creation of a new submission
+        # Hook in to notify about new applications
+        messenger(
+            MESSAGES.NEW_SUBMISSION,
+            request=request,
+            user=form_submission.user,
+            submission=form_submission,
+        )
+        return super().render_landing_page(request, form_submission=None, *args, **kwargs)
+
+    content_panels = AbstractStreamForm.content_panels + [
+        FieldPanel('workflow_name'),
+        InlinePanel('forms', label="Forms"),
+        InlinePanel('review_forms', label="Review Forms")
+    ]
+
+
+class EmailForm(AbstractEmailForm):
+    """
+    Defines the behaviour for pages that hold information about emailing applicants
+
+    Email Confirmation Panel should be included to allow admins to make changes.
+    """
+    class Meta:
+        abstract = True
+
+    confirmation_text_extra = models.TextField(blank=True, help_text="Additional text for the application confirmation message.")
+
+    def send_mail(self, submission):
+        # Make sure we don't send emails to users here. Messaging handles that
+        pass
+
+    email_confirmation_panels = [
+        MultiFieldPanel(
+            [
+                FieldRowPanel([
+                    FieldPanel('from_address', classname="col6"),
+                    FieldPanel('to_address', classname="col6"),
+                ]),
+                FieldPanel('subject'),
+                FieldPanel('confirmation_text_extra'),
+            ],
+            heading="Confirmation email",
+        )
+    ]
+
+    email_tab = ObjectList(email_confirmation_panels, heading='Confirmation email')
+
+
+class ApplicationBase(EmailForm, WorkflowStreamForm):  # type: ignore
+    is_createable = False
+
+    # Adds validation around forms & workflows. Isn't on Workflow class due to not displaying workflow field on Round
+    base_form_class = WorkflowFormAdminForm
+
+    reviewers = ParentalManyToManyField(
+        settings.AUTH_USER_MODEL,
+        related_name='%(class)s_reviewers',
+        limit_choices_to=LIMIT_TO_REVIEWERS,
+        blank=True,
+    )
+
+    parent_page_types = ['apply_home.ApplyHomePage']
+
+    def detail(self):
+        # The location to find out more information
+        return self.fund_public.first()
+
+    def _open_for(self, round_type):
+        rounds = round_type.objects.child_of(self).live().public().specific()
+        return rounds.filter(
+            Q(start_date__lte=date.today()) &
+            Q(Q(end_date__isnull=True) | Q(end_date__gte=date.today()))
+        ).first()
+
+    @property
+    def open_round(self):
+        open_round = self._open_for(Round)
+        open_sealed_round = self._open_for(SealedRound)
+        return open_round or open_sealed_round
+
+    def next_deadline(self):
+        try:
+            return self.open_round.end_date
+        except AttributeError:
+            # There isn't an open round
+            return None
+
+    def serve(self, request):
+        if hasattr(request, 'is_preview') or not self.open_round:
+            return super().serve(request)
+
+        # delegate to the open_round to use the latest form instances
+        request.show_round = True
+        return self.open_round.serve(request)
+
+    content_panels = WorkflowStreamForm.content_panels + [
+        FieldPanel('reviewers'),
+    ]
+
+    edit_handler = TabbedInterface([
+        ObjectList(content_panels, heading='Content'),
+        EmailForm.email_tab,
+        ObjectList(WorkflowStreamForm.promote_panels, heading='Promote'),
+    ])
+
+
+class FundType(ApplicationBase):
+    subpage_types = ['funds.Round']
+
+    class Meta:
+        verbose_name = _("Fund")
+
+
+class RequestForPartners(ApplicationBase):
+    subpage_types = ['funds.Round', 'funds.SealedRound']
+
+    class Meta:
+        verbose_name = _("RFP")
+
+
+class ApplicationForm(models.Model):
+    name = models.CharField(max_length=255)
+    form_fields = StreamField(ApplicationCustomFormFieldsBlock())
+
+    panels = [
+        FieldPanel('name'),
+        StreamFieldPanel('form_fields'),
+    ]
+
+    def __str__(self):
+        return self.name
+
+
+class AbstractRelatedForm(Orderable):
+    form = models.ForeignKey('ApplicationForm', on_delete=models.PROTECT)
+
+    panels = [
+        FilteredFieldPanel('form', filter_query={'roundform__isnull': True})
+    ]
+
+    @property
+    def fields(self):
+        return self.form.form_fields
+
+    class Meta(Orderable.Meta):
+        abstract = True
+
+    def __eq__(self, other):
+        try:
+            return self.fields == other.fields
+        except AttributeError:
+            return False
+
+    def __str__(self):
+        return self.form.name
+
+
+class ApplicationBaseForm(AbstractRelatedForm):
+    application = ParentalKey('ApplicationBase', related_name='forms')
+
+
+class RoundBaseForm(AbstractRelatedForm):
+    round = ParentalKey('RoundBase', related_name='forms')
+
+
+class AbstractRelatedReviewForm(Orderable):
+    form = models.ForeignKey('review.ReviewForm', on_delete=models.PROTECT)
+
+    panels = [
+        FieldPanel('form')
+    ]
+
+    @property
+    def fields(self):
+        return self.form.form_fields
+
+    class Meta(Orderable.Meta):
+        abstract = True
+
+    def __eq__(self, other):
+        try:
+            return self.fields == other.fields
+        except AttributeError:
+            return False
+
+    def __str__(self):
+        return self.form.name
+
+
+class ApplicationBaseReviewForm(AbstractRelatedReviewForm):
+    fund = ParentalKey('ApplicationBase', related_name='review_forms')
+
+
+class RoundBase(WorkflowStreamForm, SubmittableStreamForm):  # type: ignore
+    is_creatable = False
+
+    subpage_types = []  # type: ignore
+
+    lead = models.ForeignKey(
+        settings.AUTH_USER_MODEL,
+        limit_choices_to=LIMIT_TO_STAFF,
+        related_name='%(class)s_lead',
+        on_delete=models.PROTECT,
+    )
+    reviewers = ParentalManyToManyField(
+        settings.AUTH_USER_MODEL,
+        related_name='%(class)s_reviewer',
+        limit_choices_to=LIMIT_TO_REVIEWERS,
+        blank=True,
+    )
+    start_date = models.DateField(default=date.today)
+    end_date = models.DateField(
+        blank=True,
+        null=True,
+        default=date.today,
+        help_text='When no end date is provided the round will remain open indefinitely.'
+    )
+    sealed = models.BooleanField(default=False)
+
+    content_panels = SubmittableStreamForm.content_panels + [
+        FieldPanel('lead'),
+        MultiFieldPanel([
+            FieldRowPanel([
+                FieldPanel('start_date'),
+                FieldPanel('end_date'),
+            ]),
+        ], heading="Dates"),
+        FieldPanel('reviewers'),
+        ReadOnlyPanel('get_workflow_name_display', heading="Workflow"),
+        ReadOnlyInlinePanel('forms', help_text="Are copied from the parent fund."),
+    ]
+
+    edit_handler = TabbedInterface([
+        ObjectList(content_panels, heading='Content'),
+        ObjectList(SubmittableStreamForm.promote_panels, heading='Promote'),
+    ])
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        # We attached the parent page as part of the before_create_hook
+        if hasattr(self, 'parent_page'):
+            self.workflow_name = self.parent_page.workflow_name
+            self.reviewers = self.parent_page.reviewers.all()
+
+    def save(self, *args, **kwargs):
+        is_new = not self.id
+        if is_new and hasattr(self, 'parent_page'):
+            # Ensure that the workflow hasn't changed
+            self.workflow_name = self.parent_page.workflow_name
+
+        super().save(*args, **kwargs)
+
+        if is_new and hasattr(self, 'parent_page'):
+            # Would be nice to do this using model clusters as part of the __init__
+            for form in self.parent_page.forms.all():
+                # Create a copy of the existing form object
+                new_form = form.form
+                new_form.id = None
+                new_form.name = '{} for {} ({})'.format(new_form.name, self.title, self.get_parent().title)
+                new_form.save()
+                RoundBaseForm.objects.create(round=self, form=new_form)
+
+    def get_submit_meta_data(self, **kwargs):
+        return super().get_submit_meta_data(
+            page=self.get_parent(),
+            round=self,
+            **kwargs,
+        )
+
+    def clean(self):
+        super().clean()
+
+        if self.end_date and self.start_date > self.end_date:
+            raise ValidationError({
+                'end_date': 'End date must come after the start date',
+            })
+
+        if self.end_date:
+            conflict_query = (
+                Q(start_date__range=[self.start_date, self.end_date]) |
+                Q(end_date__range=[self.start_date, self.end_date]) |
+                Q(start_date__lte=self.start_date, end_date__gte=self.end_date)
+            )
+        else:
+            conflict_query = (
+                Q(start_date__lte=self.start_date, end_date__isnull=True) |
+                Q(end_date__gte=self.start_date)
+            )
+
+        if hasattr(self, 'parent_page'):
+            # Check if the create hook has added the parent page, we aren't an object yet.
+            # Ensures we can access related objects during the clean phase instead of save.
+            base_query = Round.objects.child_of(self.parent_page)
+        else:
+            # don't need parent page, we are an actual object now.
+            base_query = Round.objects.sibling_of(self)
+
+        conflicting_rounds = base_query.filter(
+            conflict_query
+        ).exclude(id=self.id)
+
+        if conflicting_rounds.exists():
+            error_message = mark_safe('Overlaps with the following rounds:<br> {}'.format(
+                '<br>'.join([
+                    f'<a href="{admin_url(round)}">{round.title}</a>: {round.start_date} - {round.end_date}'
+                    for round in conflicting_rounds]
+                )
+            ))
+            error = {
+                'start_date': error_message,
+            }
+            if self.end_date:
+                error['end_date'] = error_message
+
+            raise ValidationError(error)
+
+    def serve(self, request):
+        if hasattr(request, 'is_preview') or hasattr(request, 'show_round'):
+            return super().serve(request)
+
+        # We hide the round as only the open round is used which is displayed through the
+        # fund page
+        raise Http404()
+
+
+class Round(RoundBase):
+    parent_page_types = ['funds.FundType', 'funds.RequestForPartners']
+
+
+class SealedRound(RoundBase):
+    parent_page_types = ['funds.RequestForPartners']
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.sealed = True
+
+
+class LabType(EmailForm, WorkflowStreamForm, SubmittableStreamForm):  # type: ignore
+    class Meta:
+        verbose_name = _("Lab")
+
+    # Adds validation around forms & workflows.
+    base_form_class = WorkflowFormAdminForm
+
+    lead = models.ForeignKey(
+        settings.AUTH_USER_MODEL,
+        limit_choices_to=LIMIT_TO_STAFF,
+        related_name='lab_lead',
+        on_delete=models.PROTECT,
+    )
+    reviewers = ParentalManyToManyField(
+        settings.AUTH_USER_MODEL,
+        related_name='labs_reviewer',
+        limit_choices_to=LIMIT_TO_REVIEWERS,
+        blank=True,
+    )
+
+    parent_page_types = ['apply_home.ApplyHomePage']
+    subpage_types = []  # type: ignore
+
+    content_panels = WorkflowStreamForm.content_panels + [
+        FieldPanel('lead'),
+        FieldPanel('reviewers'),
+    ]
+
+    edit_handler = TabbedInterface([
+        ObjectList(content_panels, heading='Content'),
+        EmailForm.email_tab,
+        ObjectList(WorkflowStreamForm.promote_panels, heading='Promote'),
+    ])
+
+    def detail(self):
+        # The location to find out more information
+        return self.lab_public.first()
+
+    def get_submit_meta_data(self, **kwargs):
+        return super().get_submit_meta_data(
+            page=self,
+            round=None,
+            **kwargs,
+        )
+
+    def open_round(self):
+        return self.live
+
+
+class LabForm(AbstractRelatedForm):
+    lab = ParentalKey('LabType', related_name='forms')
+
+
+class LabReviewForm(AbstractRelatedReviewForm):
+    lab = ParentalKey('LabType', related_name='review_forms')
diff --git a/opentech/apply/funds/models.py b/opentech/apply/funds/models/submissions.py
similarity index 55%
rename from opentech/apply/funds/models.py
rename to opentech/apply/funds/models/submissions.py
index 83abe9c9c..811bbc845 100644
--- a/opentech/apply/funds/models.py
+++ b/opentech/apply/funds/models/submissions.py
@@ -1,48 +1,33 @@
-from datetime import date
 import os
 
 from django.conf import settings
 from django.contrib.auth import get_user_model
 from django.contrib.postgres.fields import JSONField
-from django.core.exceptions import ValidationError, ObjectDoesNotExist, PermissionDenied
+from django.core.exceptions import PermissionDenied
 from django.core.files.storage import default_storage
 from django.core.serializers.json import DjangoJSONEncoder
 from django.db import models
-from django.db.models import Q
+from django.db.models import ObjectDoesNotExist
 from django.db.models.expressions import RawSQL, OrderBy
 from django.dispatch import receiver
-from django.http import Http404
 from django.urls import reverse
 from django.utils.text import mark_safe, slugify
-from django.utils.translation import ugettext_lazy as _
 
 from django_fsm import can_proceed, FSMField, transition, RETURN_VALUE
 from django_fsm.signals import post_transition
-from modelcluster.fields import ParentalKey, ParentalManyToManyField
-from wagtail.admin.edit_handlers import (
-    FieldPanel,
-    FieldRowPanel,
-    InlinePanel,
-    MultiFieldPanel,
-    ObjectList,
-    StreamFieldPanel,
-    TabbedInterface,
-)
 
 from wagtail.core.fields import StreamField
-from wagtail.core.models import Orderable
-from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormSubmission
+from wagtail.contrib.forms.models import AbstractFormSubmission
 
 from opentech.apply.activity.messaging import messenger, MESSAGES
 from opentech.apply.stream_forms.blocks import UploadableMediaBlock
-from opentech.apply.stream_forms.models import AbstractStreamForm, BaseStreamForm
-from opentech.apply.users.groups import REVIEWER_GROUP_NAME, STAFF_GROUP_NAME
+from opentech.apply.stream_forms.models import BaseStreamForm
 from opentech.apply.utils.blocks import MustIncludeFieldBlock
 
-from .admin_forms import WorkflowFormAdminForm
-from .blocks import ApplicationCustomFormFieldsBlock, REQUIRED_BLOCK_NAMES
-from .edit_handlers import FilteredFieldPanel, ReadOnlyPanel, ReadOnlyInlinePanel
-from .workflow import (
+
+from . import LIMIT_TO_STAFF, LIMIT_TO_STAFF_AND_REVIEWERS, WorkflowHelpers
+from ..blocks import ApplicationCustomFormFieldsBlock, REQUIRED_BLOCK_NAMES
+from ..workflow import (
     active_statuses,
     DETERMINATION_PHASES,
     DETERMINATION_RESPONSE_PHASES,
@@ -53,481 +38,6 @@ from .workflow import (
     WORKFLOWS,
 )
 
-LIMIT_TO_STAFF = {'groups__name': STAFF_GROUP_NAME}
-LIMIT_TO_REVIEWERS = {'groups__name': REVIEWER_GROUP_NAME}
-LIMIT_TO_STAFF_AND_REVIEWERS = {'groups__name__in': [STAFF_GROUP_NAME, REVIEWER_GROUP_NAME]}
-
-
-def admin_url(page):
-    return reverse('wagtailadmin_pages:edit', args=(page.id,))
-
-
-class SubmittableStreamForm(AbstractStreamForm):
-    """
-    Controls how stream forms are submitted. Any Page allowing submissions should inherit from here.
-    """
-    class Meta:
-        abstract = True
-
-    def get_submission_class(self):
-        return ApplicationSubmission
-
-    def process_form_submission(self, form):
-        if not form.user.is_authenticated:
-            form.user = None
-        return self.get_submission_class().objects.create(
-            form_data=form.cleaned_data,
-            form_fields=self.get_defined_fields(),
-            **self.get_submit_meta_data(user=form.user),
-        )
-
-    def get_submit_meta_data(self, **kwargs):
-        return kwargs
-
-
-class WorkflowHelpers(models.Model):
-    """
-    Defines the common methods and fields for working with Workflows within Django models
-    """
-    class Meta:
-        abstract = True
-
-    WORKFLOW_CHOICES = {
-        name: workflow.name
-        for name, workflow in WORKFLOWS.items()
-    }
-
-    workflow_name = models.CharField(choices=WORKFLOW_CHOICES.items(), max_length=100, default='single', verbose_name="Workflow")
-
-    @property
-    def workflow(self):
-        return WORKFLOWS[self.workflow_name]
-
-
-class WorkflowStreamForm(WorkflowHelpers, AbstractStreamForm):  # type: ignore
-    """
-    Defines the common methods and fields for working with Workflows within Wagtail pages
-    """
-    class Meta:
-        abstract = True
-
-    def get_defined_fields(self, stage=None):
-        if not stage:
-            form_index = 0
-        else:
-            form_index = self.workflow.stages.index(stage)
-        return self.forms.all()[form_index].fields
-
-    def render_landing_page(self, request, form_submission=None, *args, **kwargs):
-        # We only reach this page after creation of a new submission
-        # Hook in to notify about new applications
-        messenger(
-            MESSAGES.NEW_SUBMISSION,
-            request=request,
-            user=form_submission.user,
-            submission=form_submission,
-        )
-        return super().render_landing_page(request, form_submission=None, *args, **kwargs)
-
-    content_panels = AbstractStreamForm.content_panels + [
-        FieldPanel('workflow_name'),
-        InlinePanel('forms', label="Forms"),
-        InlinePanel('review_forms', label="Review Forms")
-    ]
-
-
-class EmailForm(AbstractEmailForm):
-    """
-    Defines the behaviour for pages that hold information about emailing applicants
-
-    Email Confirmation Panel should be included to allow admins to make changes.
-    """
-    class Meta:
-        abstract = True
-
-    confirmation_text_extra = models.TextField(blank=True, help_text="Additional text for the application confirmation message.")
-
-    def send_mail(self, submission):
-        # Make sure we don't send emails to users here. Messaging handles that
-        pass
-
-    email_confirmation_panels = [
-        MultiFieldPanel(
-            [
-                FieldRowPanel([
-                    FieldPanel('from_address', classname="col6"),
-                    FieldPanel('to_address', classname="col6"),
-                ]),
-                FieldPanel('subject'),
-                FieldPanel('confirmation_text_extra'),
-            ],
-            heading="Confirmation email",
-        )
-    ]
-
-    email_tab = ObjectList(email_confirmation_panels, heading='Confirmation email')
-
-
-class RoundBasedParent(EmailForm, WorkflowStreamForm):  # type: ignore
-    class Meta:
-        abstract = True
-
-    # Adds validation around forms & workflows. Isn't on Workflow class due to not displaying workflow field on Round
-    base_form_class = WorkflowFormAdminForm
-
-    reviewers = ParentalManyToManyField(
-        settings.AUTH_USER_MODEL,
-        related_name='%(class)s_reviewers',
-        limit_choices_to=LIMIT_TO_REVIEWERS,
-        blank=True,
-    )
-
-    parent_page_types = ['apply_home.ApplyHomePage']
-
-    def detail(self):
-        # The location to find out more information
-        return self.fund_public.first()
-
-    def _open_for(self, round_type):
-        rounds = round_type.objects.child_of(self).live().public().specific()
-        return rounds.filter(
-            Q(start_date__lte=date.today()) &
-            Q(Q(end_date__isnull=True) | Q(end_date__gte=date.today()))
-        ).first()
-
-    @property
-    def open_round(self):
-        open_round = self._open_for(Round)
-        open_sealed_round = self._open_for(SealedRound)
-        return open_round or open_sealed_round
-
-    def next_deadline(self):
-        try:
-            return self.open_round.end_date
-        except AttributeError:
-            # There isn't an open round
-            return None
-
-    def serve(self, request):
-        if hasattr(request, 'is_preview') or not self.open_round:
-            return super().serve(request)
-
-        # delegate to the open_round to use the latest form instances
-        request.show_round = True
-        return self.open_round.serve(request)
-
-    content_panels = WorkflowStreamForm.content_panels + [
-        FieldPanel('reviewers'),
-    ]
-
-    edit_handler = TabbedInterface([
-        ObjectList(content_panels, heading='Content'),
-        EmailForm.email_tab,
-        ObjectList(WorkflowStreamForm.promote_panels, heading='Promote'),
-    ])
-
-
-class FundType(RoundBasedParent):
-    subpage_types = ['funds.Round']
-
-    class Meta:
-        verbose_name = _("Fund")
-
-
-class RequestForPartners(RoundBasedParent):
-    subpage_types = ['funds.Round', 'funds.SealedRound']
-
-    class Meta:
-        verbose_name = _("RFP")
-
-
-class AbstractRelatedForm(Orderable):
-    form = models.ForeignKey('ApplicationForm', on_delete=models.PROTECT)
-
-    panels = [
-        FilteredFieldPanel('form', filter_query={'roundform__isnull': True})
-    ]
-
-    @property
-    def fields(self):
-        return self.form.form_fields
-
-    class Meta(Orderable.Meta):
-        abstract = True
-
-    def __eq__(self, other):
-        try:
-            return self.fields == other.fields
-        except AttributeError:
-            return False
-
-    def __str__(self):
-        return self.form.name
-
-
-class FundForm(AbstractRelatedForm):
-    fund = ParentalKey('FundType', related_name='forms')
-
-
-class RoundForm(AbstractRelatedForm):
-    round = ParentalKey('Round', related_name='forms')
-
-
-class SealedRoundForn(AbstractRelatedForm):
-    round = ParentalKey('SealedRound', related_name='forms')
-
-
-class RFPForm(AbstractRelatedForm):
-    rfp = ParentalKey('RequestForPartners', related_name='forms')
-
-
-class AbstractRelatedReviewForm(Orderable):
-    form = models.ForeignKey('review.ReviewForm', on_delete=models.PROTECT)
-
-    panels = [
-        FieldPanel('form')
-    ]
-
-    @property
-    def fields(self):
-        return self.form.form_fields
-
-    class Meta(Orderable.Meta):
-        abstract = True
-
-    def __eq__(self, other):
-        try:
-            return self.fields == other.fields
-        except AttributeError:
-            return False
-
-    def __str__(self):
-        return self.form.name
-
-
-class FundReviewForm(AbstractRelatedReviewForm):
-    fund = ParentalKey('FundType', related_name='review_forms')
-
-
-class RFPReviewForm(AbstractRelatedReviewForm):
-    rfp = ParentalKey('RequestForPartners', related_name='review_forms')
-
-
-class ApplicationForm(models.Model):
-    name = models.CharField(max_length=255)
-    form_fields = StreamField(ApplicationCustomFormFieldsBlock())
-
-    panels = [
-        FieldPanel('name'),
-        StreamFieldPanel('form_fields'),
-    ]
-
-    def __str__(self):
-        return self.name
-
-
-class AbstractRound(WorkflowStreamForm, SubmittableStreamForm):  # type: ignore
-    class Meta:
-        abstract = True
-
-    parent_page_types = ['funds.FundType', 'funds.RequestForPartners']
-    subpage_types = []  # type: ignore
-
-    lead = models.ForeignKey(
-        settings.AUTH_USER_MODEL,
-        limit_choices_to=LIMIT_TO_STAFF,
-        related_name='%(class)s_lead',
-        on_delete=models.PROTECT,
-    )
-    reviewers = ParentalManyToManyField(
-        settings.AUTH_USER_MODEL,
-        related_name='%(class)s_reviewer',
-        limit_choices_to=LIMIT_TO_REVIEWERS,
-        blank=True,
-    )
-    start_date = models.DateField(default=date.today)
-    end_date = models.DateField(
-        blank=True,
-        null=True,
-        default=date.today,
-        help_text='When no end date is provided the round will remain open indefinitely.'
-    )
-    sealed = models.BooleanField(default=False)
-
-    content_panels = SubmittableStreamForm.content_panels + [
-        FieldPanel('lead'),
-        MultiFieldPanel([
-            FieldRowPanel([
-                FieldPanel('start_date'),
-                FieldPanel('end_date'),
-            ]),
-        ], heading="Dates"),
-        FieldPanel('reviewers'),
-        ReadOnlyPanel('get_workflow_name_display', heading="Workflow"),
-        ReadOnlyInlinePanel('forms', help_text="Are copied from the parent fund."),
-    ]
-
-    edit_handler = TabbedInterface([
-        ObjectList(content_panels, heading='Content'),
-        ObjectList(SubmittableStreamForm.promote_panels, heading='Promote'),
-    ])
-
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        # We attached the parent page as part of the before_create_hook
-        if hasattr(self, 'parent_page'):
-            self.workflow_name = self.parent_page.workflow_name
-            self.reviewers = self.parent_page.reviewers.all()
-
-    def save(self, *args, **kwargs):
-        is_new = not self.id
-        if is_new and hasattr(self, 'parent_page'):
-            # Ensure that the workflow hasn't changed
-            self.workflow_name = self.parent_page.workflow_name
-
-        super().save(*args, **kwargs)
-
-        if is_new and hasattr(self, 'parent_page'):
-            # Would be nice to do this using model clusters as part of the __init__
-            for form in self.parent_page.forms.all():
-                # Create a copy of the existing form object
-                new_form = form.form
-                new_form.id = None
-                new_form.name = '{} for {} ({})'.format(new_form.name, self.title, self.get_parent().title)
-                new_form.save()
-                RoundForm.objects.create(round=self, form=new_form)
-
-    def get_submit_meta_data(self, **kwargs):
-        return super().get_submit_meta_data(
-            page=self.get_parent(),
-            round=self,
-            **kwargs,
-        )
-
-    def clean(self):
-        super().clean()
-
-        if self.end_date and self.start_date > self.end_date:
-            raise ValidationError({
-                'end_date': 'End date must come after the start date',
-            })
-
-        if self.end_date:
-            conflict_query = (
-                Q(start_date__range=[self.start_date, self.end_date]) |
-                Q(end_date__range=[self.start_date, self.end_date]) |
-                Q(start_date__lte=self.start_date, end_date__gte=self.end_date)
-            )
-        else:
-            conflict_query = (
-                Q(start_date__lte=self.start_date, end_date__isnull=True) |
-                Q(end_date__gte=self.start_date)
-            )
-
-        if hasattr(self, 'parent_page'):
-            # Check if the create hook has added the parent page, we aren't an object yet.
-            # Ensures we can access related objects during the clean phase instead of save.
-            base_query = Round.objects.child_of(self.parent_page)
-        else:
-            # don't need parent page, we are an actual object now.
-            base_query = Round.objects.sibling_of(self)
-
-        conflicting_rounds = base_query.filter(
-            conflict_query
-        ).exclude(id=self.id)
-
-        if conflicting_rounds.exists():
-            error_message = mark_safe('Overlaps with the following rounds:<br> {}'.format(
-                '<br>'.join([
-                    f'<a href="{admin_url(round)}">{round.title}</a>: {round.start_date} - {round.end_date}'
-                    for round in conflicting_rounds]
-                )
-            ))
-            error = {
-                'start_date': error_message,
-            }
-            if self.end_date:
-                error['end_date'] = error_message
-
-            raise ValidationError(error)
-
-    def serve(self, request):
-        if hasattr(request, 'is_preview') or hasattr(request, 'show_round'):
-            return super().serve(request)
-
-        # We hide the round as only the open round is used which is displayed through the
-        # fund page
-        raise Http404()
-
-
-class Round(AbstractRound):
-    parent_page_types = ['funds.FundType', 'funds.RequestForPartners']
-
-
-class SealedRound(AbstractRound):
-    parent_page_types = ['funds.RequestForPartners']
-
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        self.sealed = True
-
-
-class LabType(EmailForm, WorkflowStreamForm, SubmittableStreamForm):  # type: ignore
-    class Meta:
-        verbose_name = _("Lab")
-
-    # Adds validation around forms & workflows.
-    base_form_class = WorkflowFormAdminForm
-
-    lead = models.ForeignKey(
-        settings.AUTH_USER_MODEL,
-        limit_choices_to=LIMIT_TO_STAFF,
-        related_name='lab_lead',
-        on_delete=models.PROTECT,
-    )
-    reviewers = ParentalManyToManyField(
-        settings.AUTH_USER_MODEL,
-        related_name='labs_reviewer',
-        limit_choices_to=LIMIT_TO_REVIEWERS,
-        blank=True,
-    )
-
-    parent_page_types = ['apply_home.ApplyHomePage']
-    subpage_types = []  # type: ignore
-
-    content_panels = WorkflowStreamForm.content_panels + [
-        FieldPanel('lead'),
-        FieldPanel('reviewers'),
-    ]
-
-    edit_handler = TabbedInterface([
-        ObjectList(content_panels, heading='Content'),
-        EmailForm.email_tab,
-        ObjectList(WorkflowStreamForm.promote_panels, heading='Promote'),
-    ])
-
-    def detail(self):
-        # The location to find out more information
-        return self.lab_public.first()
-
-    def get_submit_meta_data(self, **kwargs):
-        return super().get_submit_meta_data(
-            page=self,
-            round=None,
-            **kwargs,
-        )
-
-    def open_round(self):
-        return self.live
-
-
-class LabForm(AbstractRelatedForm):
-    lab = ParentalKey('LabType', related_name='forms')
-
-
-class LabReviewForm(AbstractRelatedReviewForm):
-    lab = ParentalKey('LabType', related_name='review_forms')
-
 
 class JSONOrderable(models.QuerySet):
     json_field = ''
-- 
GitLab