From 527cb00069506623c8c1b902740bb58cc2f96e18 Mon Sep 17 00:00:00 2001
From: Todd Dembrey <todd.dembrey@torchbox.com>
Date: Fri, 19 Jan 2018 11:03:47 +0000
Subject: [PATCH] Prevent users creating overlapping rounds for the same fund

---
 opentech/apply/funds/models.py            | 17 ++++++++
 opentech/apply/funds/tests/factories.py   | 13 +++++-
 opentech/apply/funds/tests/test_models.py | 50 ++++++++++++++++++++++-
 3 files changed, 78 insertions(+), 2 deletions(-)

diff --git a/opentech/apply/funds/models.py b/opentech/apply/funds/models.py
index 55d6874e4..2b431ec15 100644
--- a/opentech/apply/funds/models.py
+++ b/opentech/apply/funds/models.py
@@ -2,6 +2,8 @@ from datetime import date
 
 from django.core.exceptions import ValidationError
 from django.db import models
+from django.db.models import Q
+from django.utils.text import mark_safe
 
 from modelcluster.fields import ParentalKey
 from wagtail.wagtailadmin.edit_handlers import (
@@ -110,3 +112,18 @@ class Round(AbstractStreamForm):
             raise ValidationError({
                 'end_date': 'End date must come after the start date',
             })
+
+        conflicting_rounds = Round.objects.sibling_of(self).filter(
+            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)
+        ).exclude(id=self.id)
+
+        if conflicting_rounds.exists():
+            error_message = mark_safe('Overlaps with the following rounds:<br> {}'.format(
+                '<br>'.join([f'{round.start_date} - {round.end_date}' for round in conflicting_rounds])
+            ))
+            raise ValidationError({
+                'start_date': error_message,
+                'end_date': error_message,
+            })
diff --git a/opentech/apply/funds/tests/factories.py b/opentech/apply/funds/tests/factories.py
index 59ce824cd..aa37d55a8 100644
--- a/opentech/apply/funds/tests/factories.py
+++ b/opentech/apply/funds/tests/factories.py
@@ -1,8 +1,10 @@
+import datetime
+
 from django.forms import Form
 import factory
 import wagtail_factories
 
-from opentech.apply.funds.models import ApplicationForm, FundType, FundForm
+from opentech.apply.funds.models import ApplicationForm, FundType, FundForm, Round
 from opentech.apply.funds.workflow import Action, Phase, Stage, Workflow
 
 
@@ -135,3 +137,12 @@ class ApplicationFormFactory(factory.DjangoModelFactory):
         model = ApplicationForm
 
     name = factory.Faker('word')
+
+
+class RoundFactory(wagtail_factories.PageFactory):
+    class Meta:
+        model = Round
+
+    title = factory.Sequence('Round {}'.format)
+    start_date = factory.LazyFunction(datetime.date.today)
+    end_date = factory.LazyFunction(lambda: datetime.date.today() + datetime.timedelta(days=7))
diff --git a/opentech/apply/funds/tests/test_models.py b/opentech/apply/funds/tests/test_models.py
index a1ba5de9e..d0a2305a8 100644
--- a/opentech/apply/funds/tests/test_models.py
+++ b/opentech/apply/funds/tests/test_models.py
@@ -1,8 +1,11 @@
+from datetime import date, timedelta
+
+from django.core.exceptions import ValidationError
 from django.test import TestCase
 
 from opentech.apply.funds.workflow import SingleStage
 
-from .factories import FundTypeFactory
+from .factories import FundTypeFactory, RoundFactory
 
 
 class TestFundModel(TestCase):
@@ -10,3 +13,48 @@ class TestFundModel(TestCase):
         fund = FundTypeFactory(parent=None)
         self.assertEqual(fund.workflow, 'single')
         self.assertEqual(fund.workflow_class, SingleStage)
+
+
+class TestRoundModel(TestCase):
+    def setUp(self):
+        self.fund = FundTypeFactory(parent=None)
+
+    def make_round(self, **kwargs):
+        data = {'parent': self.fund}
+        data.update(kwargs)
+        return RoundFactory(**data)
+
+    def test_normal_start_end_doesnt_error(self):
+        self.make_round()
+
+    def test_end_before_start(self):
+        yesterday = date.today() + timedelta(days=-1)
+        with self.assertRaises(ValidationError):
+            self.make_round(end_date=yesterday)
+
+    def test_end_overlaps(self):
+        existing_round = self.make_round()
+        overlapping_end = existing_round.end_date - timedelta(-1)
+        start = existing_round.start_date - timedelta(-1)
+        with self.assertRaises(ValidationError):
+            self.make_round(start_date=start, end_date=overlapping_end)
+
+    def test_start_overlaps(self):
+        existing_round = self.make_round()
+        overlapping_start = existing_round.start_date + timedelta(1)
+        end = existing_round.end_date + timedelta(1)
+        with self.assertRaises(ValidationError):
+            self.make_round(start_date=overlapping_start, end_date=end)
+
+    def test_inside_overlaps(self):
+        existing_round = self.make_round()
+        overlapping_start = existing_round.start_date + timedelta(1)
+        overlapping_end = existing_round.end_date - timedelta(1)
+        with self.assertRaises(ValidationError):
+            self.make_round(start_date=overlapping_start, end_date=overlapping_end)
+
+    def test_other_fund_not_impacting(self):
+        self.make_round()
+        new_fund = FundTypeFactory(parent=None)
+        # Will share the same start and end dates
+        self.make_round(parent=new_fund)
-- 
GitLab