From ed988aeea86bd67b9ae4b9ac04a31b6b9a2c9c5b Mon Sep 17 00:00:00 2001
From: Todd Dembrey <todd.dembrey@torchbox.com>
Date: Mon, 4 Nov 2019 09:56:52 +0000
Subject: [PATCH] Feature/gh 1620 mark as skipped (#1641)

* Make reports skippable
* Add in activities for skip/unskip of files
---
 opentech/apply/activity/messaging.py          | 16 +++++++
 .../0050_add_report_skipping_activity.py      | 18 ++++++++
 opentech/apply/activity/options.py            |  1 +
 .../messages/email/report_skipped.html        | 13 ++++++
 .../messages/email/report_submitted.html      |  6 ++-
 .../migrations/0030_report_skipped.py         | 18 ++++++++
 opentech/apply/projects/models.py             |  7 ++-
 .../includes/report_line.html                 |  6 +++
 .../includes/reports.html                     | 10 ++++-
 .../application_projects/report_detail.html   |  4 ++
 opentech/apply/projects/tests/test_models.py  | 43 ++++++++++++++++++-
 opentech/apply/projects/tests/test_views.py   |  5 +++
 opentech/apply/projects/urls.py               |  2 +
 opentech/apply/projects/views/report.py       | 24 ++++++++++-
 14 files changed, 165 insertions(+), 8 deletions(-)
 create mode 100644 opentech/apply/activity/migrations/0050_add_report_skipping_activity.py
 create mode 100644 opentech/apply/activity/templates/messages/email/report_skipped.html
 create mode 100644 opentech/apply/projects/migrations/0030_report_skipped.py

diff --git a/opentech/apply/activity/messaging.py b/opentech/apply/activity/messaging.py
index c9eabcb59..b1dd270ed 100644
--- a/opentech/apply/activity/messaging.py
+++ b/opentech/apply/activity/messaging.py
@@ -65,6 +65,7 @@ neat_related = {
     MESSAGES.DELETE_PAYMENT_REQUEST: 'payment_request',
     MESSAGES.UPDATE_PAYMENT_REQUEST: 'payment_request',
     MESSAGES.SUBMIT_REPORT: 'report',
+    MESSAGES.SKIPPED_REPORT: 'report',
 }
 
 
@@ -240,6 +241,7 @@ class ActivityAdapter(AdapterBase):
         MESSAGES.UPDATE_PAYMENT_REQUEST_STATUS: 'Updated Payment Request status to: {payment_request.status_display}',
         MESSAGES.REQUEST_PAYMENT: 'Payment Request submitted',
         MESSAGES.SUBMIT_REPORT: 'Submitted a report',
+        MESSAGES.SKIPPED_REPORT: 'handle_skipped_report',
     }
 
     def recipients(self, message_type, **kwargs):
@@ -340,6 +342,12 @@ class ActivityAdapter(AdapterBase):
 
         return ' '.join(message)
 
+    def handle_skipped_report(self, report, **kwargs):
+        if report.skipped:
+            return "Skipped a Report"
+        else:
+            return "Marked a Report as required"
+
     def send_message(self, message, user, source, sources, **kwargs):
         from .models import Activity
         visibility = kwargs.get('visibility', ALL)
@@ -637,6 +645,7 @@ class EmailAdapter(AdapterBase):
         MESSAGES.UPDATE_PAYMENT_REQUEST: 'messages/email/payment_request_updated.html',
         MESSAGES.UPDATE_PAYMENT_REQUEST_STATUS: 'handle_payment_status_updated',
         MESSAGES.SUBMIT_REPORT: 'messages/email/report_submitted.html',
+        MESSAGES.SKIPPED_REPORT: 'messages/email/report_skipped.html',
     }
 
     def get_subject(self, message_type, source):
@@ -803,6 +812,7 @@ class DjangoMessagesAdapter(AdapterBase):
         MESSAGES.BATCH_DETERMINATION_OUTCOME: 'batch_determinations',
         MESSAGES.UPLOAD_DOCUMENT: 'Successfully uploaded document',
         MESSAGES.REMOVE_DOCUMENT: 'Successfully removed document',
+        MESSAGES.SKIPPED_REPORT: 'handle_skipped_report',
     }
 
     def batch_reviewers_updated(self, added, sources, **kwargs):
@@ -819,6 +829,12 @@ class DjangoMessagesAdapter(AdapterBase):
             ', '.join(['"{}"'.format(source.title) for source in sources])
         )
 
+    def handle_skipped_report(self, report, **kwargs):
+        if report.skipped:
+            return f"Successfully skipped a Report for {report.start_date} to {report.end_date}"
+        else:
+            return f"Successfully unskipped a Report for {report.start_date} to {report.end_date}"
+
     def batch_transition(self, sources, transitions, **kwargs):
         base_message = 'Successfully updated:'
         transition = '{submission} [{old_display} → {new_display}].'
diff --git a/opentech/apply/activity/migrations/0050_add_report_skipping_activity.py b/opentech/apply/activity/migrations/0050_add_report_skipping_activity.py
new file mode 100644
index 000000000..7f78e2da6
--- /dev/null
+++ b/opentech/apply/activity/migrations/0050_add_report_skipping_activity.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.1.11 on 2019-10-31 16:53
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('activity', '0049_add_submit_report'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='event',
+            name='type',
+            field=models.CharField(choices=[('UPDATE_LEAD', 'Update Lead'), ('BATCH_UPDATE_LEAD', 'Batch Update Lead'), ('EDIT', 'Edit'), ('APPLICANT_EDIT', 'Applicant Edit'), ('NEW_SUBMISSION', 'New Submission'), ('SCREENING', 'Screening'), ('TRANSITION', 'Transition'), ('BATCH_TRANSITION', 'Batch Transition'), ('DETERMINATION_OUTCOME', 'Determination Outcome'), ('BATCH_DETERMINATION_OUTCOME', 'Batch Determination Outcome'), ('INVITED_TO_PROPOSAL', 'Invited To Proposal'), ('REVIEWERS_UPDATED', 'Reviewers Updated'), ('BATCH_REVIEWERS_UPDATED', 'Batch Reviewers Updated'), ('PARTNERS_UPDATED', 'Partners Updated'), ('PARTNERS_UPDATED_PARTNER', 'Partners Updated Partner'), ('READY_FOR_REVIEW', 'Ready For Review'), ('BATCH_READY_FOR_REVIEW', 'Batch Ready For Review'), ('NEW_REVIEW', 'New Review'), ('COMMENT', 'Comment'), ('PROPOSAL_SUBMITTED', 'Proposal Submitted'), ('OPENED_SEALED', 'Opened Sealed Submission'), ('REVIEW_OPINION', 'Review Opinion'), ('DELETE_SUBMISSION', 'Delete Submission'), ('DELETE_REVIEW', 'Delete Review'), ('CREATED_PROJECT', 'Created Project'), ('UPDATE_PROJECT_LEAD', 'Update Project Lead'), ('EDIT_REVIEW', 'Edit Review'), ('SEND_FOR_APPROVAL', 'Send for Approval'), ('APPROVE_PROJECT', 'Project was Approved'), ('PROJECT_TRANSITION', 'Project was Transitioned'), ('REQUEST_PROJECT_CHANGE', 'Project change requested'), ('UPLOAD_DOCUMENT', 'Document was Uploaded to Project'), ('REMOVE_DOCUMENT', 'Document was Removed from Project'), ('UPLOAD_CONTRACT', 'Contract was Uploaded to Project'), ('APPROVE_CONTRACT', 'Contract was Approved'), ('REQUEST_PAYMENT', 'Payment was requested for Project'), ('UPDATE_PAYMENT_REQUEST_STATUS', 'Updated Payment Request Status'), ('DELETE_PAYMENT_REQUEST', 'Delete Payment Request'), ('SENT_TO_COMPLIANCE', 'Project was sent to Compliance'), ('UPDATE_PAYMENT_REQUEST', 'Updated Payment Request'), ('SUBMIT_REPORT', 'Submit Report'), ('SKIPPED_REPORT', 'Skipped Report')], max_length=50),
+        ),
+    ]
diff --git a/opentech/apply/activity/options.py b/opentech/apply/activity/options.py
index e65b5f484..34bd2a901 100644
--- a/opentech/apply/activity/options.py
+++ b/opentech/apply/activity/options.py
@@ -43,6 +43,7 @@ class MESSAGES(Enum):
     SENT_TO_COMPLIANCE = 'Project was sent to Compliance'
     UPDATE_PAYMENT_REQUEST = 'Updated Payment Request'
     SUBMIT_REPORT = 'Submit Report'
+    SKIPPED_REPORT = 'Skipped Report'
 
     @classmethod
     def choices(cls):
diff --git a/opentech/apply/activity/templates/messages/email/report_skipped.html b/opentech/apply/activity/templates/messages/email/report_skipped.html
new file mode 100644
index 000000000..36584004a
--- /dev/null
+++ b/opentech/apply/activity/templates/messages/email/report_skipped.html
@@ -0,0 +1,13 @@
+{% extends "messages/email/applicant_base.html" %}
+
+{% block content %}
+
+An {{ ORG_SHORT_NAME }} staff member has marked a report as {% if report.skipped %}no longer required{% else %}required{% endif %} for {{ source.title }} for period {{ report.end_date }} to {{ report.end_date }}.
+
+{% if not report.skipped %}
+This report was previously not required. Please ensure you now complete the report.
+{% endif %}
+
+Title: {{ source.title }}
+Link: {{ request.scheme }}://{{ request.get_host }}{{ source.get_absolute_url }}
+{% endblock %}
diff --git a/opentech/apply/activity/templates/messages/email/report_submitted.html b/opentech/apply/activity/templates/messages/email/report_submitted.html
index eb5296424..dc9869a83 100644
--- a/opentech/apply/activity/templates/messages/email/report_submitted.html
+++ b/opentech/apply/activity/templates/messages/email/report_submitted.html
@@ -1,8 +1,10 @@
 {% extends "messages/email/applicant_base.html" %}
 
 {% block content %}
-An {{ ORG_SHORT_NAME }} staff member has submitted a report for {{ source.title }} for period ending {{ report.end_date }}.
+An {{ ORG_SHORT_NAME }} staff member has submitted a report for {{ source.title }} for period {{ report.end_date }} to {{ report.end_date }}.
 
-Title: {{ source.title }}
+You can review the report here: {{ request.scheme }}://{{ request.get_host }}{{ report.get_absolute_url }}
+
+Project: {{ source.title }}
 Link: {{ request.scheme }}://{{ request.get_host }}{{ source.get_absolute_url }}
 {% endblock %}
diff --git a/opentech/apply/projects/migrations/0030_report_skipped.py b/opentech/apply/projects/migrations/0030_report_skipped.py
new file mode 100644
index 000000000..103f0df3f
--- /dev/null
+++ b/opentech/apply/projects/migrations/0030_report_skipped.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.1.11 on 2019-10-31 14:01
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('application_projects', '0029_report_submitted'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='report',
+            name='skipped',
+            field=models.BooleanField(default=False),
+        ),
+    ]
diff --git a/opentech/apply/projects/models.py b/opentech/apply/projects/models.py
index b99e4a0e4..5dfd57455 100644
--- a/opentech/apply/projects/models.py
+++ b/opentech/apply/projects/models.py
@@ -561,7 +561,7 @@ class ReportConfig(models.Model):
 
     def past_due_reports(self):
         return self.project.reports.filter(
-            current__isnull=True,
+            Q(current__isnull=True) & Q(skipped=False),
             end_date__lt=timezone.now().date(),
         ).order_by('end_date')
 
@@ -615,11 +615,14 @@ class ReportConfig(models.Model):
 
 class ReportQueryset(models.QuerySet):
     def done(self):
-        return self.filter(current__isnull=False)
+        return self.filter(
+            Q(current__isnull=False) | Q(skipped=True),
+        )
 
 
 class Report(models.Model):
     public = models.BooleanField(default=True)
+    skipped = models.BooleanField(default=False)
     end_date = models.DateField()
     project = models.ForeignKey("Project", on_delete=models.CASCADE, related_name="reports")
     submitted = models.DateTimeField(null=True)
diff --git a/opentech/apply/projects/templates/application_projects/includes/report_line.html b/opentech/apply/projects/templates/application_projects/includes/report_line.html
index 61fe9a82d..ef6f6d9c4 100644
--- a/opentech/apply/projects/templates/application_projects/includes/report_line.html
+++ b/opentech/apply/projects/templates/application_projects/includes/report_line.html
@@ -16,4 +16,10 @@
         {% if report.draft %}Continue Editing{% else %}Add Report{% endif %}
     </a>
     {% endif %}
+    {% if request.user.is_apply_staff and report.can_submit %}
+        <form action="{% url "apply:projects:reports:skip" pk=report.pk %}" method="post">
+            {% csrf_token %}
+            <input type="submit" value="Skip" class="btn"></input>
+        </form>
+    {% endif %}
 </p>
diff --git a/opentech/apply/projects/templates/application_projects/includes/reports.html b/opentech/apply/projects/templates/application_projects/includes/reports.html
index ae2af0600..bc7844b97 100644
--- a/opentech/apply/projects/templates/application_projects/includes/reports.html
+++ b/opentech/apply/projects/templates/application_projects/includes/reports.html
@@ -27,14 +27,20 @@
                     <span class="payment-block__mobile-label">Period End: </span>{{ report.end_date }}
                 </td>
                 <td>
-                    <span class="payment-block__mobile-label">Submitted: </span>{{ report.submitted_date|default:"-" }}
+                    <span class="payment-block__mobile-label">Submitted: </span>{{ report.submitted_date|default:"Skipped" }}
                 </td>
                 <td>
                     <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>
+                        <a href="{% url "apply:projects:reports:edit" pk=report.pk %}">Edit</a>
+                        {% if report.skipped %}
+                            <form action="{% url "apply:projects:reports:skip" pk=report.pk %}" method="post">
+                                {% csrf_token %}
+                                <input type="submit" value="Unskip" class="btn"></input>
+                            </form>
+                        {% endif %}
                     {% endif %}
                     <a href="{% url "apply:projects:reports:detail" pk=report.pk %}">view</a>
                 </td>
diff --git a/opentech/apply/projects/templates/application_projects/report_detail.html b/opentech/apply/projects/templates/application_projects/report_detail.html
index 249c07e8c..d145729ed 100644
--- a/opentech/apply/projects/templates/application_projects/report_detail.html
+++ b/opentech/apply/projects/templates/application_projects/report_detail.html
@@ -15,6 +15,9 @@
 
 <div class="wrapper wrapper--light-grey-bg wrapper--form wrapper--sidebar">
     <div class="wrapper--sidebar--inner">
+        {% if report.skipped %}
+            <h2>Report Skipped</h2>
+        {% else%}
         <h3>
             {% if object.public %}
                 Public
@@ -35,6 +38,7 @@
                 </ul>
             {% endif %}
         {% endfor %}
+        {% endif %}
     </div>
 </div>
 {% endblock %}
diff --git a/opentech/apply/projects/tests/test_models.py b/opentech/apply/projects/tests/test_models.py
index 15abe8986..52e656587 100644
--- a/opentech/apply/projects/tests/test_models.py
+++ b/opentech/apply/projects/tests/test_models.py
@@ -176,7 +176,7 @@ class TestPaymentRequestsQueryset(TestCase):
         self.assertEqual(PaymentRequest.objects.unpaid_value(), 0)
 
 
-class TestReportConfigCalculations(TestCase):
+class TestReportConfig(TestCase):
     @property
     def today(self):
         return timezone.now().date()
@@ -260,6 +260,31 @@ class TestReportConfigCalculations(TestCase):
         next_report = config.current_due_report()
         self.assertNotEqual(report, next_report)
 
+    def test_past_due(self):
+        report = ReportFactory(past_due=True)
+        config = report.project.report_config
+        self.assertQuerysetEqual(config.past_due_reports(), [report], transform=lambda x: x)
+
+    def test_past_due_has_drafts(self):
+        report = ReportFactory(past_due=True, is_draft=True)
+        config = report.project.report_config
+        self.assertQuerysetEqual(config.past_due_reports(), [report], transform=lambda x: x)
+
+    def test_past_due_no_submitted(self):
+        report = ReportFactory(is_submitted=True, past_due=True)
+        config = report.project.report_config
+        self.assertQuerysetEqual(config.past_due_reports(), [], transform=lambda x: x)
+
+    def test_past_due_no_future(self):
+        report = ReportFactory(end_date=self.today + relativedelta(days=1))
+        config = report.project.report_config
+        self.assertQuerysetEqual(config.past_due_reports(), [], transform=lambda x: x)
+
+    def test_past_due_no_skipped(self):
+        report = ReportFactory(skipped=True, past_due=True)
+        config = report.project.report_config
+        self.assertQuerysetEqual(config.past_due_reports(), [], transform=lambda x: x)
+
 
 class TestReport(TestCase):
     @property
@@ -297,3 +322,19 @@ class TestReport(TestCase):
         ReportFactory(end_date=yesterday)
         report = ReportFactory(end_date=self.from_today(1), is_submitted=True)
         self.assertEqual(report.start_date, self.today)
+
+    def test_queryset_done_includes_submitted(self):
+        report = ReportFactory(is_submitted=True)
+        self.assertQuerysetEqual(Report.objects.done(), [report], transform=lambda x: x)
+
+    def test_queryset_done_includes_skipped(self):
+        report = ReportFactory(skipped=True)
+        self.assertQuerysetEqual(Report.objects.done(), [report], transform=lambda x: x)
+
+    def test_queryset_done_doesnt_includes_draft(self):
+        ReportFactory(is_draft=True)
+        self.assertQuerysetEqual(Report.objects.done(), [], transform=lambda x: x)
+
+    def test_queryset_done_doesnt_includes_to_do(self):
+        ReportFactory()
+        self.assertQuerysetEqual(Report.objects.done(), [], transform=lambda x: x)
diff --git a/opentech/apply/projects/tests/test_views.py b/opentech/apply/projects/tests/test_views.py
index c7e09c139..23b6f729c 100644
--- a/opentech/apply/projects/tests/test_views.py
+++ b/opentech/apply/projects/tests/test_views.py
@@ -1424,6 +1424,11 @@ class TestStaffReportDetail(BaseViewTestCase):
         response = self.get_page(report)
         self.assertEqual(response.status_code, 200)
 
+    def test_can_access_skipped_report(self):
+        report = ReportFactory(skipped=True)
+        response = self.get_page(report)
+        self.assertEqual(response.status_code, 200)
+
     def test_cant_access_draft_report(self):
         report = ReportFactory(is_draft=True)
         response = self.get_page(report)
diff --git a/opentech/apply/projects/urls.py b/opentech/apply/projects/urls.py
index 91d070649..d5f379881 100644
--- a/opentech/apply/projects/urls.py
+++ b/opentech/apply/projects/urls.py
@@ -16,6 +16,7 @@ from .views import (
     ProjectPrivateMediaView,
     ReportDetailView,
     ReportPrivateMedia,
+    ReportSkipView,
     ReportUpdateView,
 )
 
@@ -45,6 +46,7 @@ urlpatterns = [
     path('reports/', include(([
         path('<int:pk>/', include([
             path('', ReportDetailView.as_view(), name='detail'),
+            path('skip/', ReportSkipView.as_view(), name='skip'),
             path('edit/', ReportUpdateView.as_view(), name='edit'),
             path('documents/<int:file_pk>/', ReportPrivateMedia.as_view(), name="document"),
         ])),
diff --git a/opentech/apply/projects/views/report.py b/opentech/apply/projects/views/report.py
index 70c2603ca..5b3d76ce8 100644
--- a/opentech/apply/projects/views/report.py
+++ b/opentech/apply/projects/views/report.py
@@ -4,13 +4,16 @@ from django.core.exceptions import PermissionDenied
 from django.http import Http404
 from django.shortcuts import get_object_or_404, redirect
 from django.utils.decorators import method_decorator
+from django.views import View
 from django.views.generic import (
     DetailView,
     UpdateView,
 )
+from django.views.generic.detail import SingleObjectMixin
 
 from opentech.apply.activity.messaging import MESSAGES, messenger
 from opentech.apply.utils.storage import PrivateMediaView
+from opentech.apply.users.decorators import staff_required
 
 from ..models import Report, ReportConfig, ReportPrivateFiles
 from ..forms import ReportEditForm
@@ -42,7 +45,7 @@ class ReportDetailView(ReportAccessMixin, DetailView):
 
     def dispatch(self, *args, **kwargs):
         report = self.get_object()
-        if not report.current:
+        if not report.current and not report.skipped:
             raise Http404
         return super().dispatch(*args, **kwargs)
 
@@ -139,3 +142,22 @@ class ReportPrivateMedia(UserPassesTestMixin, PrivateMediaView):
             return True
 
         return False
+
+
+@method_decorator(staff_required, name='dispatch')
+class ReportSkipView(SingleObjectMixin, View):
+    model = Report
+
+    def post(self, *args, **kwargs):
+        report = self.get_object()
+        if not report.current:
+            report.skipped = not report.skipped
+            report.save()
+            messenger(
+                MESSAGES.SKIPPED_REPORT,
+                request=self.request,
+                user=self.request.user,
+                source=report.project,
+                related=report,
+            )
+        return redirect(report.project.get_absolute_url())
-- 
GitLab