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