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