diff --git a/opentech/apply/activity/messaging.py b/opentech/apply/activity/messaging.py index bfa925a2eb87c47bfe2e551df9d86535f8cbbd87..fce7c7bbc8cb10bd0b13412b0095b550a4958249 100644 --- a/opentech/apply/activity/messaging.py +++ b/opentech/apply/activity/messaging.py @@ -67,6 +67,7 @@ neat_related = { MESSAGES.SUBMIT_REPORT: 'report', MESSAGES.SKIPPED_REPORT: 'report', MESSAGES.REPORT_FREQUENCY_CHANGED: 'config', + MESSAGES.REPORT_NOTIFY: 'report', } @@ -653,6 +654,7 @@ class EmailAdapter(AdapterBase): MESSAGES.SUBMIT_REPORT: 'messages/email/report_submitted.html', MESSAGES.SKIPPED_REPORT: 'messages/email/report_skipped.html', MESSAGES.REPORT_FREQUENCY_CHANGED: 'messages/email/report_frequency.html', + MESSAGES.REPORT_NOTIFY: 'messages/email/report_notify.html', } def get_subject(self, message_type, source): diff --git a/opentech/apply/activity/migrations/0052_nullable_by_report_notify.py b/opentech/apply/activity/migrations/0052_nullable_by_report_notify.py new file mode 100644 index 0000000000000000000000000000000000000000..5f21463d7157fac539ddad71ca938002140a5678 --- /dev/null +++ b/opentech/apply/activity/migrations/0052_nullable_by_report_notify.py @@ -0,0 +1,25 @@ +# Generated by Django 2.1.11 on 2019-11-05 16:20 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('activity', '0051_report_frequency_change'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + ), + 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'), ('REPORT_FREQUENCY_CHANGED', 'Report Frequency Changed'), ('REPORT_NOTIFY', 'Report Notify')], max_length=50), + ), + ] diff --git a/opentech/apply/activity/models.py b/opentech/apply/activity/models.py index cf37ef0c242564a3c388305568f8686af32938c2..82b87a65445c2510d139a0696a9e6243a5ccd588 100644 --- a/opentech/apply/activity/models.py +++ b/opentech/apply/activity/models.py @@ -148,7 +148,7 @@ class Event(models.Model): when = models.DateTimeField(auto_now_add=True) type = models.CharField(choices=MESSAGES.choices(), max_length=50) - by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) + by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, null=True) content_type = models.ForeignKey(ContentType, blank=True, null=True, on_delete=models.CASCADE) object_id = models.PositiveIntegerField(blank=True, null=True) source = GenericForeignKey('content_type', 'object_id') diff --git a/opentech/apply/activity/options.py b/opentech/apply/activity/options.py index 4e6aea6eb8a6c6e0593a9eba8462b940b46f9631..c4d891f483c2cfc853580ea5193df5e310a9c949 100644 --- a/opentech/apply/activity/options.py +++ b/opentech/apply/activity/options.py @@ -45,6 +45,7 @@ class MESSAGES(Enum): SUBMIT_REPORT = 'Submit Report' SKIPPED_REPORT = 'Skipped Report' REPORT_FREQUENCY_CHANGED = 'Report Frequency Changed' + REPORT_NOTIFY= 'Report Notify' @classmethod def choices(cls): diff --git a/opentech/apply/activity/templates/messages/email/report_notify.html b/opentech/apply/activity/templates/messages/email/report_notify.html new file mode 100644 index 0000000000000000000000000000000000000000..104285b756e07a53ac11b581c380d72ce4d4834c --- /dev/null +++ b/opentech/apply/activity/templates/messages/email/report_notify.html @@ -0,0 +1,10 @@ +{% extends "messages/email/applicant_base.html" %} + +{% block content %} + +A report is due for {{ source.title }} on {{ report.end_date }}. + +More information can be found on the project page: +{{ request.scheme }}://{{ request.get_host }}{{ source.get_absolute_url }} + +{% endblock %} diff --git a/opentech/apply/projects/management/commands/notify_report_due.py b/opentech/apply/projects/management/commands/notify_report_due.py index d0162caed2ce85338bd212f546daaff1f0ee59f0..a74e3c572c946a6ad28a28a8c18ec46a57c7e2ee 100644 --- a/opentech/apply/projects/management/commands/notify_report_due.py +++ b/opentech/apply/projects/management/commands/notify_report_due.py @@ -1,8 +1,13 @@ from dateutil.relativedelta import relativedelta +from django.conf import settings +from django.contrib.messages.storage.fallback import FallbackStorage from django.core.management.base import BaseCommand +from django.http import HttpRequest from django.utils import timezone +from opentech.apply.activity.messaging import MESSAGES, messenger +from opentech.apply.home.models import ApplyHomePage from opentech.apply.projects.models import Project @@ -13,11 +18,37 @@ class Command(BaseCommand): parser.add_argument('days', type=int) def handle(self, *args, **options): - due_date = timezone.now().date() + relativedelta(days=options['days']) + site = ApplyHomePage.objects.first().get_site() + + # Mock a HTTPRequest in order to pass the site settings into the + # templates + request = HttpRequest() + request.META['SERVER_NAME'] = site.hostname + request.META['SERVER_PORT'] = site.port + request.META[settings.SECURE_PROXY_SSL_HEADER] = 'https' + request.session = {} + request._messages = FallbackStorage(request) + + today = timezone.now().date() + due_date = today + relativedelta(days=options['days']) for project in Project.objects.in_progress(): next_report = project.report_config.current_due_report() - if next_report.end_date == due_date: + due_soon = next_report.end_date == due_date + not_notified_today = ( + not next_report.notified or + next_report.notified.date() != today + ) + if due_soon and not_notified_today: + messenger( + MESSAGES.REPORT_NOTIFY, + request=request, + user=None, + source=project, + related=next_report, + ) # Notify about the due report + next_report.notified = timezone.now() + next_report.save() self.stdout.write( self.style.SUCCESS(f'Notified project: {project.id}') ) diff --git a/opentech/apply/projects/migrations/0032_report_notified.py b/opentech/apply/projects/migrations/0032_report_notified.py new file mode 100644 index 0000000000000000000000000000000000000000..ec7ae7bf0c846eb290df9950923434cc01a237bc --- /dev/null +++ b/opentech/apply/projects/migrations/0032_report_notified.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.11 on 2019-11-05 15:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('application_projects', '0031_add_public_private_content'), + ] + + operations = [ + migrations.AddField( + model_name='report', + name='notified', + field=models.DateTimeField(null=True), + ), + ] diff --git a/opentech/apply/projects/models.py b/opentech/apply/projects/models.py index c652a2a24c6f86a6b2198f19c6a388bce48b08e5..87993c947a3acc4199eb10db0405248498040bc5 100644 --- a/opentech/apply/projects/models.py +++ b/opentech/apply/projects/models.py @@ -746,6 +746,7 @@ class Report(models.Model): end_date = models.DateField() project = models.ForeignKey("Project", on_delete=models.CASCADE, related_name="reports") submitted = models.DateTimeField(null=True) + notified = models.DateTimeField(null=True) current = models.OneToOneField( "ReportVersion", on_delete=models.CASCADE, diff --git a/opentech/apply/projects/tests/factories.py b/opentech/apply/projects/tests/factories.py index a585a8b0b9d772df05f3d187b48e83f9a4615083..05a63a64d916310fc0569e805e9c2b1d81e8e481 100644 --- a/opentech/apply/projects/tests/factories.py +++ b/opentech/apply/projects/tests/factories.py @@ -8,6 +8,7 @@ from django.utils import timezone from opentech.apply.funds.tests.factories import ApplicationSubmissionFactory from opentech.apply.projects.models import ( + COMPLETE, Contract, DocumentCategory, IN_PROGRESS, @@ -101,6 +102,9 @@ class ProjectFactory(factory.DjangoModelFactory): in_progress = factory.Trait( status=IN_PROGRESS, ) + is_complete = factory.Trait( + status=COMPLETE, + ) class ContractFactory(factory.DjangoModelFactory): diff --git a/opentech/apply/projects/tests/test_commands.py b/opentech/apply/projects/tests/test_commands.py index b0ee1023a533aab5e979bdeee137f09d3cbe1121..5897e1c1be0c6a543e2072504377e497cc4b6cc0 100644 --- a/opentech/apply/projects/tests/test_commands.py +++ b/opentech/apply/projects/tests/test_commands.py @@ -3,9 +3,11 @@ from io import StringIO from dateutil.relativedelta import relativedelta from django.core.management import call_command -from django.test import TestCase +from django.test import override_settings, TestCase from django.utils import timezone +from opentech.apply.home.models import ApplyHomePage + from .factories import ( ProjectFactory, ReportConfigFactory, @@ -13,6 +15,8 @@ from .factories import ( ) +@override_settings(ALLOWED_HOSTS=[ApplyHomePage.objects.first().get_site().hostname]) +@override_settings(ROOT_URLCONF='opentech.apply.urls') class TestNotifyReportDue(TestCase): def test_notify_report_due_in_7_days(self): in_a_week = timezone.now() + relativedelta(days=7) @@ -23,7 +27,7 @@ class TestNotifyReportDue(TestCase): def test_dont_notify_report_due_in_7_days_already_submitted(self): in_a_week = timezone.now() + relativedelta(days=7) - config = ReportConfigFactory(schedule_start=in_a_week) + config = ReportConfigFactory(schedule_start=in_a_week, project__in_progress=True) ReportFactory( project=config.project, is_submitted=True, @@ -33,14 +37,26 @@ class TestNotifyReportDue(TestCase): call_command('notify_report_due', 7, stdout=out) self.assertNotIn('Notified project', out.getvalue()) + def test_dont_notify_already_notified(self): + in_a_week = timezone.now() + relativedelta(days=7) + config = ReportConfigFactory(schedule_start=in_a_week, project__in_progress=True) + ReportFactory( + project=config.project, + end_date=config.schedule_start, + notified=timezone.now(), + ) + out = StringIO() + call_command('notify_report_due', 7, stdout=out) + self.assertNotIn('Notified project', out.getvalue()) + def test_dont_notify_project_not_in_progress(self): ProjectFactory() out = StringIO() call_command('notify_report_due', 7, stdout=out) self.assertNotIn('Notified project', out.getvalue()) - def test_dont_notify_project_closed(self): - ProjectFactory() + def test_dont_notify_project_complete(self): + ProjectFactory(is_complete=True) out = StringIO() call_command('notify_report_due', 7, stdout=out) self.assertNotIn('Notified project', out.getvalue())