From 7e6813a15f8b759e05cfc1631beabc3a0c61a184 Mon Sep 17 00:00:00 2001
From: Todd Dembrey <todd.dembrey@torchbox.com>
Date: Mon, 4 Nov 2019 09:12:17 +0000
Subject: [PATCH] Feature/gh 1625 staff edit (#1637)

* correct end date and dont change submitted reports
* Allow staff editing of the submitted report with no notify
* Prevent staff editing the privacy setting once the report is submitted
---
 opentech/apply/projects/forms.py              |  8 ++++
 .../migrations/0029_report_submitted.py       | 18 ++++++++
 opentech/apply/projects/models.py             | 12 +++--
 .../includes/reports.html                     |  3 ++
 opentech/apply/projects/tests/factories.py    |  3 +-
 opentech/apply/projects/tests/test_models.py  | 45 ++++++++++++++++++-
 opentech/apply/projects/tests/test_views.py   | 37 +++++++++++++--
 opentech/apply/projects/views/report.py       | 11 ++++-
 8 files changed, 126 insertions(+), 11 deletions(-)
 create mode 100644 opentech/apply/projects/migrations/0029_report_submitted.py

diff --git a/opentech/apply/projects/forms.py b/opentech/apply/projects/forms.py
index e5e84899d..dbbf3125d 100644
--- a/opentech/apply/projects/forms.py
+++ b/opentech/apply/projects/forms.py
@@ -399,6 +399,10 @@ class ReportEditForm(forms.ModelForm):
         self.fields['file_list'].queryset = self.report_files
         self.user = user
 
+        # Cant change the privacy of a submitted report
+        if self.instance.current:
+            del self.fields['public']
+
     @transaction.atomic
     def save(self, commit=True):
         is_draft = 'save' in self.data
@@ -414,6 +418,10 @@ class ReportEditForm(forms.ModelForm):
         if is_draft:
             self.instance.draft = version
         else:
+            # If this is the first submission of the report we track that as the
+            # submitted date of the report
+            if not self.instance.submitted:
+                self.instance.submitted = version.submitted
             self.instance.current = version
             self.instance.draft = None
 
diff --git a/opentech/apply/projects/migrations/0029_report_submitted.py b/opentech/apply/projects/migrations/0029_report_submitted.py
new file mode 100644
index 000000000..635f0b796
--- /dev/null
+++ b/opentech/apply/projects/migrations/0029_report_submitted.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.1.11 on 2019-10-31 09:10
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('application_projects', '0028_report_draft'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='report',
+            name='submitted',
+            field=models.DateTimeField(null=True),
+        ),
+    ]
diff --git a/opentech/apply/projects/models.py b/opentech/apply/projects/models.py
index b792ddcf9..36388f874 100644
--- a/opentech/apply/projects/models.py
+++ b/opentech/apply/projects/models.py
@@ -601,6 +601,7 @@ class ReportConfig(models.Model):
         report, _ = self.project.reports.update_or_create(
             project=self.project,
             end_date__gte=today,
+            current__isnull=True,
             defaults={'end_date': next_due_date}
         )
         return report
@@ -621,6 +622,7 @@ class Report(models.Model):
     public = models.BooleanField(default=True)
     end_date = models.DateField()
     project = models.ForeignKey("Project", on_delete=models.CASCADE, related_name="reports")
+    submitted = models.DateTimeField(null=True)
     current = models.OneToOneField(
         "ReportVersion",
         on_delete=models.CASCADE,
@@ -645,7 +647,9 @@ class Report(models.Model):
 
     @property
     def is_very_late(self):
-        more_than_one_report_late = self.project.reports.filter(end_date__gt=self.end_date).count() >= 2
+        more_than_one_report_late = self.project.reports.filter(
+            Q(end_date__gt=self.end_date) & Q(end_date__lt=timezone.now().date())
+        ).count() >= 1
         not_submitted = not self.current
         return not_submitted and more_than_one_report_late
 
@@ -655,12 +659,12 @@ class Report(models.Model):
 
     @property
     def submitted_date(self):
-        if self.current:
-            return self.current.submitted.date()
+        if self.submitted:
+            return self.submitted.date()
 
     @cached_property
     def start_date(self):
-        last_report = self.project.reports.order_by('current__submitted').filter(end_date__lt=self.end_date).first()
+        last_report = self.project.reports.filter(end_date__lt=self.end_date).first()
         if last_report:
             return last_report.end_date + relativedelta(days=1)
 
diff --git a/opentech/apply/projects/templates/application_projects/includes/reports.html b/opentech/apply/projects/templates/application_projects/includes/reports.html
index be0b115cb..81d83f5c2 100644
--- a/opentech/apply/projects/templates/application_projects/includes/reports.html
+++ b/opentech/apply/projects/templates/application_projects/includes/reports.html
@@ -33,6 +33,9 @@
                     <span class="payment-block__mobile-label">Privacy: </span>{% if report.public %}Public{% else %}Private{% endif %}
                 </td>
                 <td>
+                    {% if request.user.is_apply_staff %}
+                        <a href="{% url "apply:projects:reports:edit" pk=report.pk %}">edit</a>
+                    {% endif %}
                 </td>
             </tr>
             {% empty %}
diff --git a/opentech/apply/projects/tests/factories.py b/opentech/apply/projects/tests/factories.py
index 994bfc331..bdca5a9a5 100644
--- a/opentech/apply/projects/tests/factories.py
+++ b/opentech/apply/projects/tests/factories.py
@@ -175,6 +175,7 @@ class ReportVersionFactory(factory.DjangoModelFactory):
                 obj.report.draft = obj
             else:
                 obj.report.current = obj
+                obj.report.submitted = obj.submitted
             obj.report.save()
 
 
@@ -189,7 +190,7 @@ class ReportFactory(factory.DjangoModelFactory):
         past_due = factory.Trait(
             end_date=factory.LazyFunction(lambda: timezone.now() - relativedelta(days=1))
         )
-        submitted = factory.Trait(
+        is_submitted = factory.Trait(
             version=factory.RelatedFactory(ReportVersionFactory, 'report', draft=False, relate=True)
         )
         is_draft = factory.Trait(
diff --git a/opentech/apply/projects/tests/test_models.py b/opentech/apply/projects/tests/test_models.py
index 91e2540fb..15abe8986 100644
--- a/opentech/apply/projects/tests/test_models.py
+++ b/opentech/apply/projects/tests/test_models.py
@@ -177,7 +177,6 @@ class TestPaymentRequestsQueryset(TestCase):
 
 
 class TestReportConfigCalculations(TestCase):
-
     @property
     def today(self):
         return timezone.now().date()
@@ -254,3 +253,47 @@ class TestReportConfigCalculations(TestCase):
         report = config.current_due_report()
         self.assertEqual(Report.objects.count(), 2)
         self.assertEqual(report.end_date, self.today + relativedelta(days=3))
+
+    def test_submitted_report_unaffected(self):
+        config = ReportConfigFactory()
+        report = ReportFactory(is_submitted=True, project=config.project, end_date=self.today + relativedelta(days=1))
+        next_report = config.current_due_report()
+        self.assertNotEqual(report, next_report)
+
+
+class TestReport(TestCase):
+    @property
+    def today(self):
+        return timezone.now().date()
+
+    def from_today(self, days):
+        return self.today + relativedelta(days=days)
+
+    def test_not_late_if_one_ahead(self):
+        report = ReportFactory(end_date=self.from_today(-3))
+        ReportFactory(project=report.project)
+        self.assertFalse(report.is_very_late)
+
+    def test_late_if_two_ahead(self):
+        report = ReportFactory(end_date=self.from_today(-3))
+        ReportFactory(end_date=self.from_today(-2), project=report.project)
+        ReportFactory(project=report.project)
+        self.assertTrue(report.is_very_late)
+
+    def test_not_late_if_two_ahead_but_one_in_future(self):
+        report = ReportFactory(end_date=self.from_today(-3))
+        ReportFactory(project=report.project)
+        ReportFactory(end_date=self.from_today(2), project=report.project)
+        self.assertFalse(report.is_very_late)
+
+    def test_start_date(self):
+        yesterday = self.from_today(-1)
+        ReportFactory(end_date=yesterday)
+        report = ReportFactory(end_date=self.from_today(1))
+        self.assertEqual(report.start_date, self.today)
+
+    def test_start_date_with_submitted(self):
+        yesterday = self.from_today(-1)
+        ReportFactory(end_date=yesterday)
+        report = ReportFactory(end_date=self.from_today(1), is_submitted=True)
+        self.assertEqual(report.start_date, self.today)
diff --git a/opentech/apply/projects/tests/test_views.py b/opentech/apply/projects/tests/test_views.py
index 3cebdb7cf..ff727e84d 100644
--- a/opentech/apply/projects/tests/test_views.py
+++ b/opentech/apply/projects/tests/test_views.py
@@ -37,6 +37,7 @@ from .factories import (
     PaymentRequestFactory,
     ProjectFactory,
     ReportFactory,
+    ReportVersionFactory,
 )
 
 
@@ -1284,7 +1285,7 @@ class TestStaffSubmitReport(BaseViewTestCase):
         self.assertIsNone(report.draft)
 
     def test_edit_submitted_report(self):
-        report = ReportFactory(submitted=True)
+        report = ReportFactory(is_submitted=True)
         self.assertEqual(report.versions.first(), report.current)
         response = self.post_page(report, {'content': 'Some text', 'public': True, 'save': ' Save'})
         report.refresh_from_db()
@@ -1293,10 +1294,28 @@ class TestStaffSubmitReport(BaseViewTestCase):
         self.assertEqual(report.versions.last(), report.draft)
         self.assertEqual(report.versions.first(), report.current)
 
+    def test_resubmit_submitted_report(self):
+        yesterday = timezone.now() - relativedelta(days=1)
+        version = ReportVersionFactory(submitted=yesterday)
+        report = version.report
+        report.current = version
+        report.submitted = version.submitted
+        report.save()
+        self.assertEqual(report.submitted, yesterday)
+        self.assertEqual(report.versions.first(), report.current)
+        response = self.post_page(report, {'content': 'Some text', 'public': True})
+        report.refresh_from_db()
+        self.assertRedirects(response, self.absolute_url(report.project.get_absolute_url()))
+        self.assertEqual(report.versions.last().content, 'Some text')
+        self.assertEqual(report.versions.last(), report.current)
+        self.assertIsNone(report.draft)
+        self.assertEqual(report.submitted.date(), yesterday.date())
+        self.assertEqual(report.current.submitted.date(), timezone.now().date())
+
     def test_can_submit_future_report(self):
         submitted_report = ReportFactory(
             end_date=timezone.now() + relativedelta(days=1),
-            submitted=True,
+            is_submitted=True,
         )
         future_report = ReportFactory(
             end_date=timezone.now() + relativedelta(days=3),
@@ -1306,12 +1325,22 @@ class TestStaffSubmitReport(BaseViewTestCase):
         self.assertEqual(response.status_code, 404)
 
     def test_change_privacy(self):
-        report = ReportFactory()
+        report = ReportFactory(public=True)
         response = self.post_page(report, {'content': 'Some text', 'public': False})
         report.refresh_from_db()
         self.assertRedirects(response, self.absolute_url(report.project.get_absolute_url()))
         self.assertFalse(report.public)
 
+    def test_cant_change_privacy_submitted(self):
+        report = ReportFactory(
+            is_submitted=True,
+            public=True,
+        )
+        response = self.post_page(report, {'content': 'Some text', 'public': False})
+        report.refresh_from_db()
+        self.assertRedirects(response, self.absolute_url(report.project.get_absolute_url()))
+        self.assertTrue(report.public)
+
 
 class TestApplicantSubmitReport(BaseViewTestCase):
     base_view_name = 'edit'
@@ -1369,7 +1398,7 @@ class TestApplicantSubmitReport(BaseViewTestCase):
         self.assertIsNone(report.draft)
 
     def test_cant_edit_submitted_report(self):
-        report = ReportFactory(submitted=True, project__user=self.user)
+        report = ReportFactory(is_submitted=True, project__user=self.user)
         self.assertEqual(report.versions.first(), report.current)
         response = self.post_page(report, {'content': 'Some text', 'public': True, 'save': ' Save'})
         self.assertEqual(response.status_code, 404)
diff --git a/opentech/apply/projects/views/report.py b/opentech/apply/projects/views/report.py
index af02271ac..f728f49a2 100644
--- a/opentech/apply/projects/views/report.py
+++ b/opentech/apply/projects/views/report.py
@@ -73,7 +73,16 @@ class ReportUpdateView(ReportAccessMixin, UpdateView):
     def form_valid(self, form):
         response = super().form_valid(form)
 
-        if not form.instance.draft:
+        should_notify = True
+        if self.object.draft:
+            # It was a draft submission
+            should_notify = False
+        else:
+            if self.object.submitted != self.object.current.submitted:
+                # It was a staff edit - post submission
+                should_notify = False
+
+        if should_notify:
             messenger(
                 MESSAGES.SUBMIT_REPORT,
                 request=self.request,
-- 
GitLab