diff --git a/hypha/apply/activity/messaging.py b/hypha/apply/activity/messaging.py index d2c7b76cbd78218fdb53d1af1193f0f26d5124d8..2710b0c9172fd78d259bea77f6f37c8206bb6102 100644 --- a/hypha/apply/activity/messaging.py +++ b/hypha/apply/activity/messaging.py @@ -68,6 +68,9 @@ neat_related = { MESSAGES.SKIPPED_REPORT: 'report', MESSAGES.REPORT_FREQUENCY_CHANGED: 'config', MESSAGES.REPORT_NOTIFY: 'report', + MESSAGES.CREATE_REMINDER: 'reminder', + MESSAGES.DELETE_REMINDER: 'reminder', + MESSAGES.REVIEW_REMINDER: 'reminder', } @@ -670,12 +673,15 @@ class EmailAdapter(AdapterBase): 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', + MESSAGES.REVIEW_REMINDER: 'messages/email/ready_to_review.html', } def get_subject(self, message_type, source): if source: if is_ready_for_review(message_type): subject = 'Application ready to review: {source.title}'.format(source=source) + elif message_type in {MESSAGES.REVIEW_REMINDER}: + subject = 'Reminder: Application ready to review: {source.title}'.format(source=source) else: try: subject = source.page.specific.subject or 'Your application to {org_long_name}: {source.title}'.format(org_long_name=settings.ORG_LONG_NAME, source=source) @@ -764,6 +770,9 @@ class EmailAdapter(AdapterBase): if user.is_applicant: return [] + if message_type in {MESSAGES.REVIEW_REMINDER}: + return self.reviewers(source) + return [source.user.email] def batch_recipients(self, message_type, sources, **kwargs): @@ -838,6 +847,8 @@ class DjangoMessagesAdapter(AdapterBase): MESSAGES.REMOVE_DOCUMENT: 'Successfully removed document', MESSAGES.SKIPPED_REPORT: 'handle_skipped_report', MESSAGES.REPORT_FREQUENCY_CHANGED: 'handle_report_frequency', + MESSAGES.CREATE_REMINDER: 'Reminder created', + MESSAGES.DELETE_REMINDER: 'Reminder deleted', } def batch_reviewers_updated(self, added, sources, **kwargs): diff --git a/hypha/apply/activity/migrations/0054_add_create_reminder_delete_reminder_and_review_reminder.py b/hypha/apply/activity/migrations/0054_add_create_reminder_delete_reminder_and_review_reminder.py new file mode 100644 index 0000000000000000000000000000000000000000..34df18e2c618aab504d2a778a24ea5bde7b22c2c --- /dev/null +++ b/hypha/apply/activity/migrations/0054_add_create_reminder_delete_reminder_and_review_reminder.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.11 on 2020-03-19 10:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('activity', '0053_nullable_by_report_notify'), + ] + + 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'), ('REPORT_FREQUENCY_CHANGED', 'Report Frequency Changed'), ('REPORT_NOTIFY', 'Report Notify'), ('CREATE_REMINDER', 'Reminder Created'), ('DELETE_REMINDER', 'Reminder Deleted'), ('REVIEW_REMINDER', 'Reminde to Review')], max_length=50), + ), + ] diff --git a/hypha/apply/activity/options.py b/hypha/apply/activity/options.py index c6b54450a0f2574c210c78c1a020b0021c23d48d..a6c81fb10579295db430c0137f1b08c9c1905005 100644 --- a/hypha/apply/activity/options.py +++ b/hypha/apply/activity/options.py @@ -46,6 +46,9 @@ class MESSAGES(Enum): SKIPPED_REPORT = 'Skipped Report' REPORT_FREQUENCY_CHANGED = 'Report Frequency Changed' REPORT_NOTIFY = 'Report Notify' + CREATE_REMINDER = 'Reminder Created' + DELETE_REMINDER = 'Reminder Deleted' + REVIEW_REMINDER = 'Reminde to Review' @classmethod def choices(cls): diff --git a/hypha/apply/activity/templates/messages/email/ready_to_review.html b/hypha/apply/activity/templates/messages/email/ready_to_review.html index 98dabcef3f002a23eb79ce874ca2ccb236778fc4..d2713c6c51589162f2e345e586ed4638ba05b9ed 100644 --- a/hypha/apply/activity/templates/messages/email/ready_to_review.html +++ b/hypha/apply/activity/templates/messages/email/ready_to_review.html @@ -2,7 +2,7 @@ {% block salutation %}Dear Reviewer,{% endblock %} {% block content %} -A new proposal has been added to your review list. +This application is awaiting your review. Title: {{ source.title }} Link: {{ request.scheme }}://{{ request.get_host }}{{ source.get_absolute_url }} diff --git a/hypha/apply/funds/forms.py b/hypha/apply/funds/forms.py index 97a947897a4c93ec019a10b684795a2c024032fc..45d86240364fb97a6f02af20dbec127b4b686ece 100644 --- a/hypha/apply/funds/forms.py +++ b/hypha/apply/funds/forms.py @@ -11,7 +11,7 @@ from django_select2.forms import Select2Widget from hypha.apply.categories.models import MetaTerm from hypha.apply.users.models import User -from .models import ApplicationSubmission, AssignedReviewers, ReviewerRole +from .models import ApplicationSubmission, AssignedReviewers, Reminder, ReviewerRole from .utils import render_icon from .widgets import MetaTermSelect2Widget, Select2MultiCheckboxesWidget from .workflow import get_action_mapping @@ -408,3 +408,28 @@ class UpdateMetaTermsForm(ApplicationSubmissionModelForm): kwargs.pop('user') super().__init__(*args, **kwargs) self.fields['meta_terms'].queryset = MetaTerm.get_root_descendants().exclude(depth=2) + + +class CreateReminderForm(forms.ModelForm): + submission = forms.ModelChoiceField( + queryset=ApplicationSubmission.objects.filter(), + widget=forms.HiddenInput(), + ) + time = forms.DateTimeField() + + def __init__(self, instance=None, user=None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.user = user + + if instance: + self.fields['submission'].initial = instance.id + + def save(self, *args, **kwargs): + return Reminder.objects.create( + time=self.cleaned_data['time'], + submission=self.cleaned_data['submission'], + user=self.user) + + class Meta: + model = Reminder + fields = ['time', 'action'] diff --git a/hypha/apply/funds/management/commands/send_reminders.py b/hypha/apply/funds/management/commands/send_reminders.py new file mode 100644 index 0000000000000000000000000000000000000000..9309339cd8636d865461e7e4f1739f5d5fd49951 --- /dev/null +++ b/hypha/apply/funds/management/commands/send_reminders.py @@ -0,0 +1,41 @@ +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.urls import set_urlconf +from django.utils import timezone + +from hypha.apply.activity.messaging import messenger +from hypha.apply.funds.models import Reminder +from hypha.apply.home.models import ApplyHomePage + + +class Command(BaseCommand): + help = 'Send reminders' + + def handle(self, *args, **options): + site = ApplyHomePage.objects.first().get_site() + set_urlconf('hypha.apply.urls') + + # 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) + + for reminder in Reminder.objects.filter(sent=False, time__lte=timezone.now()): + messenger( + reminder.action_message, + request=request, + user=None, + source=reminder.submission, + related=reminder, + ) + self.stdout.write( + self.style.SUCCESS(f'Reminder sent: {reminder.id}') + ) + reminder.sent = True + reminder.save() diff --git a/hypha/apply/funds/migrations/0073_reminder.py b/hypha/apply/funds/migrations/0073_reminder.py new file mode 100644 index 0000000000000000000000000000000000000000..247398da79e669421e4d7b54b290fb47e3fc53b3 --- /dev/null +++ b/hypha/apply/funds/migrations/0073_reminder.py @@ -0,0 +1,30 @@ +# Generated by Django 2.2.11 on 2020-03-19 17:28 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('funds', '0072_add_guide_link_field'), + ] + + operations = [ + migrations.CreateModel( + name='Reminder', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('time', models.DateTimeField()), + ('action', models.CharField(choices=[('reviewers_review', 'Remind reviewers to Review')], default='reviewers_review', max_length=50)), + ('sent', models.BooleanField(default=False)), + ('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reminders', to='funds.ApplicationSubmission')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-time'], + }, + ), + ] diff --git a/hypha/apply/funds/models/__init__.py b/hypha/apply/funds/models/__init__.py index 9b9fb053cb7716418b4f0218e3f506e21bf46bb3..a7c7beabb39510a751dcd207bd5824a37f81fabe 100644 --- a/hypha/apply/funds/models/__init__.py +++ b/hypha/apply/funds/models/__init__.py @@ -2,11 +2,12 @@ from django.utils.translation import ugettext_lazy as _ from .applications import ApplicationBase, LabBase, RoundBase, RoundsAndLabs # NOQA from .forms import ApplicationForm +from .reminders import Reminder from .reviewer_role import ReviewerRole from .screening import ScreeningStatus from .submissions import ApplicationRevision, ApplicationSubmission, AssignedReviewers -__all__ = ['ApplicationSubmission', 'AssignedReviewers', 'ApplicationRevision', 'ApplicationForm', 'ScreeningStatus', 'ReviewerRole'] +__all__ = ['ApplicationSubmission', 'AssignedReviewers', 'ApplicationRevision', 'ApplicationForm', 'ScreeningStatus', 'ReviewerRole', 'Reminder'] class FundType(ApplicationBase): diff --git a/hypha/apply/funds/models/reminders.py b/hypha/apply/funds/models/reminders.py new file mode 100644 index 0000000000000000000000000000000000000000..14ff327fe9bc7e5de52c1fcb0fb7e07a1e1a4496 --- /dev/null +++ b/hypha/apply/funds/models/reminders.py @@ -0,0 +1,56 @@ +from django.conf import settings +from django.db import models +from django.utils import timezone + +from hypha.apply.activity.messaging import MESSAGES + + +class Reminder(models.Model): + REVIEW = 'reviewers_review' + ACTIONS = { + REVIEW: 'Remind reviewers to Review', + } + EMAIL = 'email' + MEDIUM = { + REVIEW: EMAIL + } + ACTION_MESSAGE = { + f'{REVIEW}-{EMAIL}': MESSAGES.REVIEW_REMINDER, + } + submission = models.ForeignKey( + 'funds.ApplicationSubmission', + on_delete=models.CASCADE, + related_name='reminders' + ) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.PROTECT, + ) + time = models.DateTimeField() + action = models.CharField( + choices=ACTIONS.items(), + default=REVIEW, + max_length=50, + ) + sent = models.BooleanField(default=False) + + def __str__(self): + return '{} at {}'.format( + self.ACTIONS[self.action], + self.time.strftime('%Y-%m-%d %I:%M %p') + ) + + class Meta: + ordering = ['-time'] + + @property + def is_expired(self): + return timezone.now() > self.time + + @property + def action_message(self): + return self.ACTION_MESSAGE[f'{self.action}-{self.medium}'] + + @property + def medium(self): + return self.MEDIUM[self.action] diff --git a/hypha/apply/funds/templates/funds/applicationsubmission_admin_detail.html b/hypha/apply/funds/templates/funds/applicationsubmission_admin_detail.html index 1b007bcbb2cb25b6a14d0efed3ce7fcb480a2cbd..4c601f75cbc5442d9ac925802b3d5519e3502934 100644 --- a/hypha/apply/funds/templates/funds/applicationsubmission_admin_detail.html +++ b/hypha/apply/funds/templates/funds/applicationsubmission_admin_detail.html @@ -26,6 +26,7 @@ {% include "funds/includes/create_project_form.html" %} {% endif %} {% include "funds/includes/update_meta_terms_form.html" %} + {% include "funds/includes/create_reminder_form.html" %} {% endblock %} {% block flags %} @@ -59,6 +60,10 @@ {% include 'determinations/includes/determination_block.html' with submission=object %} {% endblock %} +{% block reminders %} + {% include 'funds/includes/reminders_block.html' with submission=object %} +{% endblock %} + {% block extra_js %} {{ reviewer_form.media.js }} {{ comment_form.media.js }} diff --git a/hypha/apply/funds/templates/funds/applicationsubmission_detail.html b/hypha/apply/funds/templates/funds/applicationsubmission_detail.html index 09c04a170e811ef94abda9f11d513158ac8ee81a..491ad9c572174d7e915a45191d56a37b439b3156 100644 --- a/hypha/apply/funds/templates/funds/applicationsubmission_detail.html +++ b/hypha/apply/funds/templates/funds/applicationsubmission_detail.html @@ -111,6 +111,9 @@ {% block screening_status %} {% endblock %} + {% block reminders %} + {% endblock %} + {% block meta_terms %} {% endblock %} diff --git a/hypha/apply/funds/templates/funds/includes/actions.html b/hypha/apply/funds/templates/funds/includes/actions.html index 20f84c073537ee2acebafe912800dbe47eef856e..fef520d40b0baaa27c542576c4650e9e76ff4beb 100644 --- a/hypha/apply/funds/templates/funds/includes/actions.html +++ b/hypha/apply/funds/templates/funds/includes/actions.html @@ -28,3 +28,5 @@ <a class="button button--white button--full-width button--bottom-space" href="{% url 'funds:submissions:revisions:list' submission_pk=object.id %}">Revisions</a> <a data-fancybox data-src="#update-meta-terms" class="button button--white button--full-width button--bottom-space" href="#">Meta Terms</a> + +<a data-fancybox data-src="#create-reminder" class="button button--white button--full-width button--bottom-space" href="#">Create Reminder</a> diff --git a/hypha/apply/funds/templates/funds/includes/create_reminder_form.html b/hypha/apply/funds/templates/funds/includes/create_reminder_form.html new file mode 100644 index 0000000000000000000000000000000000000000..6e74066c408cce5cb2c58473f677bb9762c2d7f7 --- /dev/null +++ b/hypha/apply/funds/templates/funds/includes/create_reminder_form.html @@ -0,0 +1,4 @@ +<div class="modal" id="create-reminder"> + <h4 class="modal__header-bar">Create Reminder</h4> + {% include 'funds/includes/delegated_form_base.html' with form=reminder_form value='Create' %} +</div> diff --git a/hypha/apply/funds/templates/funds/includes/reminders_block.html b/hypha/apply/funds/templates/funds/includes/reminders_block.html new file mode 100644 index 0000000000000000000000000000000000000000..2cb7144206ea96467671c76a763b99aacd9c46e3 --- /dev/null +++ b/hypha/apply/funds/templates/funds/includes/reminders_block.html @@ -0,0 +1,23 @@ +<div class="sidebar__inner"> + <h5>Reminders</h5> + {% regroup object.reminders.all by get_action_display as action_list %} + <ul> + {% for action in action_list %} + <li><strong>{{ action.grouper }}</strong> + <ul> + {% for reminder in action.list %} + <li class="{% if reminder.is_expired %}expired-reminder{% endif %}"> + {{ reminder.time|date:"SHORT_DATETIME_FORMAT" }} + <a class="link" href="{% url 'funds:submissions:reminders:delete' object.id reminder.id %}"> + <svg class="icon icon--delete"><use xlink:href="#delete"></use></svg> + </a> + </li> + {% endfor %} + </ul> + </li> + <br> + {% empty %} + <li>No reminders yet.</li> + {% endfor %} + </ul> +</div> diff --git a/hypha/apply/funds/templates/funds/reminder_confirm_delete.html b/hypha/apply/funds/templates/funds/reminder_confirm_delete.html new file mode 100644 index 0000000000000000000000000000000000000000..9fca2cfbd7551535a160ac2c5052a22d2631023f --- /dev/null +++ b/hypha/apply/funds/templates/funds/reminder_confirm_delete.html @@ -0,0 +1,23 @@ +{% extends "base-apply.html" %} +{% load static %} + +{% block title %}Deleting: {{ object }}{% endblock %} + +{% block content %} +<div class="admin-bar"> + <div class="admin-bar__inner"> + <h2 class="heading heading--no-margin">Deleting: {{ object }}</h2> + </div> +</div> + +<div class="wrapper wrapper--light-grey-bg wrapper--form wrapper--sidebar"> + <div class="wrapper--sidebar--inner"> + <form class="form" action="" method="post"> + {% csrf_token %} + <p><strong>Are you sure you want to delete "{{ object }}"?</strong></p> + <input class="button button--warning button--submit button--top-space" type="submit"value="Confirm" /> + </form> + </div> +</div> + +{% endblock %} diff --git a/hypha/apply/funds/tests/factories/models.py b/hypha/apply/funds/tests/factories/models.py index 87f7647c882350481cd4bd680fb43168605a69f0..b0c94dbc6f350243920940368cedf7787e5ce905 100644 --- a/hypha/apply/funds/tests/factories/models.py +++ b/hypha/apply/funds/tests/factories/models.py @@ -2,6 +2,7 @@ import datetime import factory import wagtail_factories +from django.utils import timezone from hypha.apply.funds.models import ( ApplicationRevision, @@ -9,6 +10,7 @@ from hypha.apply.funds.models import ( AssignedReviewers, FundType, LabType, + Reminder, RequestForPartners, ReviewerRole, Round, @@ -57,6 +59,7 @@ __all__ = [ 'ReviewerRoleFactory', 'TodayRoundFactory', 'workflow_for_stages', + 'ReminderFactory', ] @@ -351,3 +354,13 @@ class ScreeningStatusFactory(factory.DjangoModelFactory): model = ScreeningStatus title = factory.Iterator(["Bad", "Good"]) + + +class ReminderFactory(factory.DjangoModelFactory): + class Meta: + model = Reminder + + submission = factory.SubFactory('hypha.apply.funds.tests.factories.ApplicationSubmissionFactory') + user = factory.SubFactory(StaffFactory) + time = factory.Sequence(lambda n: timezone.now() + datetime.timedelta(days=7 * n + 1)) + action = factory.Iterator(["reviewers_review"]) diff --git a/hypha/apply/funds/tests/test_models.py b/hypha/apply/funds/tests/test_models.py index 4d91163377730d6035a2a7f30ccc4559da292f2b..a0486ff7f0bb8e331d7cc544d8599c3af0e212ab 100644 --- a/hypha/apply/funds/tests/test_models.py +++ b/hypha/apply/funds/tests/test_models.py @@ -11,7 +11,7 @@ from django.test import TestCase, override_settings from django.urls import reverse from hypha.apply.funds.blocks import EmailBlock, FullNameBlock -from hypha.apply.funds.models import ApplicationSubmission +from hypha.apply.funds.models import ApplicationSubmission, Reminder from hypha.apply.funds.workflow import Request from hypha.apply.review.options import MAYBE, NO from hypha.apply.review.tests.factories import ReviewFactory, ReviewOpinionFactory @@ -25,6 +25,7 @@ from .factories import ( FundTypeFactory, InvitedToProposalFactory, LabFactory, + ReminderFactory, RequestForPartnersFactory, RoundFactory, TodayRoundFactory, @@ -655,3 +656,20 @@ class TestForTableQueryset(TestCase): self.assertEqual(submission.review_count, 1) self.assertEqual(submission.review_submitted_count, 1) self.assertEqual(submission.review_recommendation, NO) + + +class TestReminderModel(TestCase): + + def test_can_save_reminder(self): + submission = ApplicationSubmissionFactory() + reminder = ReminderFactory(submission=submission) + self.assertEqual(submission, reminder.submission) + self.assertFalse(reminder.sent) + + def test_check_default_action(self): + reminder = ReminderFactory() + self.assertEqual(reminder.action, Reminder.REVIEW) + + def test_reminder_action_message(self): + reminder = ReminderFactory() + self.assertEqual(reminder.action_message, Reminder.ACTION_MESSAGE[f'{reminder.action}-{reminder.medium}']) diff --git a/hypha/apply/funds/tests/test_views.py b/hypha/apply/funds/tests/test_views.py index 5e5bd2cea53ed4486aedd4cb274fb3bfda88d8fb..dd09926fded0ac68663849e69c168ab2ba9e1ea5 100644 --- a/hypha/apply/funds/tests/test_views.py +++ b/hypha/apply/funds/tests/test_views.py @@ -17,6 +17,7 @@ from hypha.apply.funds.tests.factories import ( AssignedWithRoleReviewersFactory, InvitedToProposalFactory, LabSubmissionFactory, + ReminderFactory, ReviewerRoleFactory, ScreeningStatusFactory, SealedRoundFactory, @@ -735,3 +736,35 @@ class TestAnonSubmissionFileView(BaseSubmissionFileViewTestCase): self.assertEqual(len(response.redirect_chain), 2) for path, _ in response.redirect_chain: self.assertIn(reverse('users_public:login'), path) + + +class BaseProjectDeleteTestCase(BaseViewTestCase): + url_name = 'funds:submissions:reminders:{}' + base_view_name = 'delete' + + def get_kwargs(self, instance): + return {'pk': instance.id, 'submission_pk': instance.submission.id} + + +class TestStaffReminderDeleteView(BaseProjectDeleteTestCase): + user_factory = StaffFactory + + def test_has_access(self): + reminder = ReminderFactory() + response = self.get_page(reminder) + self.assertEqual(response.status_code, 200) + + def test_confirm_message(self): + reminder = ReminderFactory() + response = self.get_page(reminder) + self.assertContains(response, 'Are you sure you want to delete') + self.assertEqual(response.status_code, 200) + + +class TestUserReminderDeleteView(BaseProjectDeleteTestCase): + user_factory = ApplicantFactory + + def test_doesnt_has_access(self): + reminder = ReminderFactory() + response = self.get_page(reminder) + self.assertEqual(response.status_code, 403) diff --git a/hypha/apply/funds/urls.py b/hypha/apply/funds/urls.py index 2a089d7a065f2865355a36bb777feb62943dfa2f..b4ef599c53d3436b133c56e0463ecbcd53a153e6 100644 --- a/hypha/apply/funds/urls.py +++ b/hypha/apply/funds/urls.py @@ -3,6 +3,7 @@ from django.urls import include, path from hypha.apply.projects import urls as projects_urls from .views import ( + ReminderDeleteView, RevisionCompareView, RevisionListView, RoundListView, @@ -26,6 +27,10 @@ revision_urls = ([ path('compare/<int:to>/<int:from>/', RevisionCompareView.as_view(), name='compare'), ], 'revisions') +reminders_urls = ([ + path('<int:pk>/delete/', ReminderDeleteView.as_view(), name="delete"), +], 'reminders') + app_name = 'funds' @@ -51,6 +56,7 @@ submission_urls = ([ path('<int:submission_pk>/', include([ path('', include('hypha.apply.review.urls', namespace="reviews")), path('revisions/', include(revision_urls, namespace="revisions")), + path('reminders/', include(reminders_urls, namespace="reminders")), ])), path('', include('hypha.apply.determinations.urls', namespace="determinations")), path('', include('hypha.apply.flags.urls', namespace="flags")), diff --git a/hypha/apply/funds/views.py b/hypha/apply/funds/views.py index 96bb65f9f746f80d27bc7c319720d5fbd7ce9c1e..66d4aa8f2e93c429226c99dd9e6e87c0222133ba 100644 --- a/hypha/apply/funds/views.py +++ b/hypha/apply/funds/views.py @@ -53,6 +53,7 @@ from .forms import ( BatchProgressSubmissionForm, BatchUpdateReviewersForm, BatchUpdateSubmissionLeadForm, + CreateReminderForm, ProgressSubmissionForm, ScreeningSubmissionForm, UpdateMetaTermsForm, @@ -64,6 +65,7 @@ from .models import ( ApplicationRevision, ApplicationSubmission, LabBase, + Reminder, RoundBase, RoundsAndLabs, ) @@ -580,12 +582,54 @@ class UpdateMetaTermsView(DelegatedViewMixin, UpdateView): context_name = 'meta_terms_form' +@method_decorator(staff_required, name='dispatch') +class ReminderCreateView(DelegatedViewMixin, CreateView): + context_name = 'reminder_form' + form_class = CreateReminderForm + model = Reminder + + def form_valid(self, form): + response = super().form_valid(form) + + messenger( + MESSAGES.CREATE_REMINDER, + request=self.request, + user=self.request.user, + source=self.object.submission, + related=self.object, + ) + + return response + + +@method_decorator(staff_required, name='dispatch') +class ReminderDeleteView(DeleteView): + model = Reminder + + def get_success_url(self): + submission = get_object_or_404(ApplicationSubmission, id=self.kwargs['submission_pk']) + return reverse_lazy('funds:submissions:detail', args=(submission.id,)) + + def delete(self, request, *args, **kwargs): + reminder = self.get_object() + messenger( + MESSAGES.DELETE_REMINDER, + user=request.user, + request=request, + source=reminder.submission, + related=reminder, + ) + response = super().delete(request, *args, **kwargs) + return response + + class AdminSubmissionDetailView(ReviewContextMixin, ActivityContextMixin, DelegateableView, DetailView): template_name_suffix = '_admin_detail' model = ApplicationSubmission form_views = [ ProgressSubmissionView, ScreeningSubmissionView, + ReminderCreateView, CommentFormView, UpdateLeadView, UpdateReviewersView, diff --git a/hypha/settings/base.py b/hypha/settings/base.py index fd092667c19169e7123f05c4a4b0d009a8bfd56a..7f557cc118163256cffc8d51cc6ac3eace69949a 100644 --- a/hypha/settings/base.py +++ b/hypha/settings/base.py @@ -323,6 +323,23 @@ SHORT_DATE_FORMAT = 'Y-m-d' SHORT_DATETIME_FORMAT = 'Y-m-d H:i' +DATETIME_INPUT_FORMATS = [ + '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59' + '%Y-%m-%d %H:%M:%S.%f', # '2006-10-25 14:30:59.000200' + '%Y-%m-%d %H:%M', # '2006-10-25 14:30' + '%Y-%m-%dT%H:%M', # '2006-10-25T14:30 (this is extra)' + '%Y-%m-%d', # '2006-10-25' + '%m/%d/%Y %H:%M:%S', # '10/25/2006 14:30:59' + '%m/%d/%Y %H:%M:%S.%f', # '10/25/2006 14:30:59.000200' + '%m/%d/%Y %H:%M', # '10/25/2006 14:30' + '%m/%d/%Y', # '10/25/2006' + '%m/%d/%y %H:%M:%S', # '10/25/06 14:30:59' + '%m/%d/%y %H:%M:%S.%f', # '10/25/06 14:30:59.000200' + '%m/%d/%y %H:%M', # '10/25/06 14:30' + '%m/%d/%y', # '10/25/06' +] + + # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/stable/howto/static-files/ diff --git a/hypha/static_src/src/sass/apply/components/_reminder-sidebar.scss b/hypha/static_src/src/sass/apply/components/_reminder-sidebar.scss new file mode 100644 index 0000000000000000000000000000000000000000..520156dbe9d9781b4ce5422e22eaeb26cfd82b87 --- /dev/null +++ b/hypha/static_src/src/sass/apply/components/_reminder-sidebar.scss @@ -0,0 +1,3 @@ +.expired-reminder { + color: $color--mid-dark-grey; +} diff --git a/hypha/static_src/src/sass/apply/main.scss b/hypha/static_src/src/sass/apply/main.scss index cbba6b86e8c3fccff9dfdc25cd02e8b216e030d5..adc07cb39de5c2045a381c2721b588aba8dd6467 100644 --- a/hypha/static_src/src/sass/apply/main.scss +++ b/hypha/static_src/src/sass/apply/main.scss @@ -62,6 +62,7 @@ @import 'components/funding-block'; @import 'components/data-block'; @import 'components/invoice-block'; +@import 'components/reminder-sidebar'; // Layout @import 'layout/header';