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