From f5446f2e6bec2987acbfabef7da187f6e08b847b Mon Sep 17 00:00:00 2001 From: George Hickman <ghickman@users.noreply.github.com> Date: Fri, 9 Aug 2019 14:36:05 +0100 Subject: [PATCH] Add removing files (#1392) Ref #1347 --- opentech/apply/activity/messaging.py | 1 + .../migrations/0039_add_remove_document.py | 18 +++++++++++ opentech/apply/activity/options.py | 1 + opentech/apply/projects/forms.py | 11 +++++++ opentech/apply/projects/models.py | 12 +++++++ .../includes/supporting_documents.html | 13 +++++++- opentech/apply/projects/tests/test_views.py | 32 ++++++++++++++++++- opentech/apply/projects/views.py | 26 +++++++++++++-- 8 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 opentech/apply/activity/migrations/0039_add_remove_document.py diff --git a/opentech/apply/activity/messaging.py b/opentech/apply/activity/messaging.py index ecc7a64f0..3dd9a1b44 100644 --- a/opentech/apply/activity/messaging.py +++ b/opentech/apply/activity/messaging.py @@ -743,6 +743,7 @@ class DjangoMessagesAdapter(AdapterBase): MESSAGES.BATCH_TRANSITION: 'batch_transition', MESSAGES.BATCH_DETERMINATION_OUTCOME: 'batch_determinations', MESSAGES.UPLOAD_DOCUMENT: 'Successfully uploaded document "{title}"', + MESSAGES.REMOVE_DOCUMENT: 'Successfully removed document "{title}"', } def batch_reviewers_updated(self, added, sources, **kwargs): diff --git a/opentech/apply/activity/migrations/0039_add_remove_document.py b/opentech/apply/activity/migrations/0039_add_remove_document.py new file mode 100644 index 000000000..c3014c711 --- /dev/null +++ b/opentech/apply/activity/migrations/0039_add_remove_document.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.13 on 2019-08-08 14:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('activity', '0038_auto_20190808_1142'), + ] + + 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'), ('REQUEST_PROJECT_CHANGE', 'Project change requested'), ('UPLOAD_DOCUMENT', 'Document was Uploaded to Project'), ('REMOVE_DOCUMENT', 'Document was Removed from Project')], max_length=50), + ), + ] diff --git a/opentech/apply/activity/options.py b/opentech/apply/activity/options.py index 5780d17cf..1dacb5fe6 100644 --- a/opentech/apply/activity/options.py +++ b/opentech/apply/activity/options.py @@ -33,6 +33,7 @@ class MESSAGES(Enum): APPROVE_PROJECT = 'Project was Approved' REQUEST_PROJECT_CHANGE = 'Project change requested' UPLOAD_DOCUMENT = 'Document was Uploaded to Project' + REMOVE_DOCUMENT = 'Document was Removed from Project' @classmethod def choices(cls): diff --git a/opentech/apply/projects/forms.py b/opentech/apply/projects/forms.py index 4fdf55a19..c7e51daf6 100644 --- a/opentech/apply/projects/forms.py +++ b/opentech/apply/projects/forms.py @@ -73,6 +73,17 @@ class RejectionForm(forms.Form): super().__init__(*args, **kwargs) +class RemoveDocumentForm(forms.ModelForm): + id = forms.IntegerField(widget=forms.HiddenInput()) + + class Meta: + fields = ['id'] + model = PacketFile + + def __init__(self, user=None, *args, **kwargs): + super().__init__(*args, **kwargs) + + class SetPendingForm(forms.ModelForm): class Meta: fields = ['id'] diff --git a/opentech/apply/projects/models.py b/opentech/apply/projects/models.py index e2055aa7b..441070ab4 100644 --- a/opentech/apply/projects/models.py +++ b/opentech/apply/projects/models.py @@ -37,6 +37,18 @@ class PacketFile(models.Model): def __str__(self): return f'Project file: {self.title}' + def get_remove_form(self): + """ + Get an instantiated RemoveDocumentForm with this class as `instance`. + + This allows us to build instances of the RemoveDocumentForm for each + instance of PacketFile in the supporting documents template. The + standard Delegated View flow makes it difficult to create these forms + in the view or template. + """ + from .forms import RemoveDocumentForm + return RemoveDocumentForm(instance=self) + COMMITTED = 'committed' CONTRACTING = 'contracting' diff --git a/opentech/apply/projects/templates/application_projects/includes/supporting_documents.html b/opentech/apply/projects/templates/application_projects/includes/supporting_documents.html index bb52d2879..e308cdb7e 100644 --- a/opentech/apply/projects/templates/application_projects/includes/supporting_documents.html +++ b/opentech/apply/projects/templates/application_projects/includes/supporting_documents.html @@ -73,7 +73,18 @@ </div> <div class="docs-block__document-inner"> <a class="docs-block__document-link" href="{{ document.document.url }}">Download</a> - <a class="docs-block__document-link" href="#">Remove</a> + <form method="POST" id="{{ remove_document_form.name }}"> + {% csrf_token %} + {{ document.get_remove_form }} + <input + class="button button--primary button--top-space" + id="{{ remove_document_form.name }}-submit" + name="{{ form_prefix }}{{ remove_document_form.name }}" + type="submit" + form="{{ remove_document_form.name }}" + value="Remove" /> + </form> + </input> </div> </li> {% endfor %} diff --git a/opentech/apply/projects/tests/test_views.py b/opentech/apply/projects/tests/test_views.py index f30bdd547..70fc10611 100644 --- a/opentech/apply/projects/tests/test_views.py +++ b/opentech/apply/projects/tests/test_views.py @@ -10,7 +10,8 @@ from opentech.apply.utils.testing.tests import BaseViewTestCase from ..forms import SetPendingForm from ..views import ProjectDetailView -from .factories import DocumentCategoryFactory, ProjectFactory +from .factories import (DocumentCategoryFactory, PacketFileFactory, + ProjectFactory) class TestCreateApprovalView(BaseViewTestCase): @@ -72,6 +73,35 @@ class TestProjectDetailView(TestCase): self.assertEqual(response.status_code, 200) +class TestRemoveDocumentView(BaseViewTestCase): + base_view_name = 'detail' + url_name = 'funds:projects:{}' + user_factory = StaffFactory + + def get_kwargs(self, instance): + return {'pk': instance.id} + + def test_remove_document(self): + project = ProjectFactory() + document = PacketFileFactory() + + response = self.post_page(project, { + 'form-submitted-remove_document_form': '', + 'id': document.id, + }) + project.refresh_from_db() + + self.assertEqual(response.status_code, 200) + self.assertNotIn(document.pk, project.packet_files.values_list('pk', flat=True)) + + def test_remove_non_existent_document(self): + response = self.post_page(ProjectFactory(), { + 'form-submitted-remove_document_form': '', + 'id': 1, + }) + self.assertEqual(response.status_code, 200) + + class TestSendForApprovalView(BaseViewTestCase): base_view_name = 'detail' url_name = 'funds:projects:{}' diff --git a/opentech/apply/projects/views.py b/opentech/apply/projects/views.py index 38059fa38..a97746f82 100644 --- a/opentech/apply/projects/views.py +++ b/opentech/apply/projects/views.py @@ -3,7 +3,7 @@ from copy import copy from django.db import transaction from django.shortcuts import redirect from django.utils.decorators import method_decorator -from django.views.generic import CreateView, DetailView, UpdateView +from django.views.generic import CreateView, DetailView, FormView, UpdateView from opentech.apply.activity.messaging import MESSAGES, messenger from opentech.apply.activity.views import ActivityContextMixin, CommentFormView @@ -12,8 +12,9 @@ from opentech.apply.utils.views import (DelegateableView, DelegatedViewMixin, ViewDispatcher) from .forms import (CreateApprovalForm, ProjectEditForm, RejectionForm, - SetPendingForm, UpdateProjectLeadForm, UploadDocumentForm) -from .models import CONTRACTING, Approval, Project + RemoveDocumentForm, SetPendingForm, UpdateProjectLeadForm, + UploadDocumentForm) +from .models import CONTRACTING, Approval, PacketFile, Project @method_decorator(staff_required, name='dispatch') @@ -63,6 +64,24 @@ class RejectionView(DelegatedViewMixin, UpdateView): return redirect(self.object) +@method_decorator(staff_required, name='dispatch') +class RemoveDocumentView(DelegatedViewMixin, FormView): + context_name = 'remove_document_form' + form_class = RemoveDocumentForm + model = Project + + def form_valid(self, form): + document_id = form.cleaned_data["id"] + project = self.kwargs['object'] + + try: + project.packet_files.get(pk=document_id).delete() + except PacketFile.DoesNotExist: + pass + + return redirect(project) + + @method_decorator(staff_required, name='dispatch') class SendForApprovalView(DelegatedViewMixin, UpdateView): context_name = 'request_approval_form' @@ -133,6 +152,7 @@ class AdminProjectDetailView(ActivityContextMixin, DelegateableView, DetailView) CommentFormView, CreateApprovalView, RejectionView, + RemoveDocumentView, SendForApprovalView, UpdateLeadView, UploadDocumentView, -- GitLab