From f3c014d7be815ba09c8fe341cbc0c48f957d7755 Mon Sep 17 00:00:00 2001
From: Todd Dembrey <todd.dembrey@torchbox.com>
Date: Fri, 11 Oct 2019 14:09:15 +0100
Subject: [PATCH] Feature/tidy up applicant views (#1595)

* Ensure the applicant dashboard loads when they have projects

* Limit applicant visibility for entities on project page

* Track the progress of a project for the applicant's viewpoint

* Improve messaging to applicant around contracts

* Ensure the payment request works for the applicant

* Fixup the tests for the applicant changes
---
 opentech/apply/activity/messaging.py          |  6 +-
 .../migrations/0048_add_project_transition.py | 18 +++++
 opentech/apply/activity/options.py            |  1 +
 .../messages/email/contract_uploaded.html     |  7 ++
 .../activity/templatetags/activity_tags.py    |  3 +-
 opentech/apply/dashboard/views.py             |  4 +-
 opentech/apply/projects/forms.py              | 30 ++++----
 ...3_ensure_contracts_uses_private_storage.py | 20 ++++++
 .../0024_allow_no_comments_on_pr.py           | 18 +++++
 opentech/apply/projects/models.py             | 32 ++++++---
 .../includes/payment_requests.html            | 17 ++---
 .../includes/supporting_documents.html        | 11 +--
 .../paymentrequest_confirm_delete.html        | 37 ++++++++++
 .../paymentrequest_detail.html                |  6 ++
 .../paymentrequest_form.html                  |  6 +-
 .../application_projects/project_detail.html  | 11 +--
 .../projects/templatetags/approval_tools.py   |  5 ++
 opentech/apply/projects/tests/test_forms.py   | 10 +--
 opentech/apply/projects/tests/test_models.py  |  4 +-
 .../apply/projects/tests/test_templatetags.py |  4 +-
 opentech/apply/projects/tests/test_views.py   |  5 +-
 opentech/apply/projects/urls.py               |  4 ++
 opentech/apply/projects/views/payment.py      | 59 ++++++++--------
 opentech/apply/projects/views/project.py      | 68 ++++++++++++++++---
 .../sass/apply/components/_payment-block.scss | 12 ++--
 25 files changed, 286 insertions(+), 112 deletions(-)
 create mode 100644 opentech/apply/activity/migrations/0048_add_project_transition.py
 create mode 100644 opentech/apply/projects/migrations/0023_ensure_contracts_uses_private_storage.py
 create mode 100644 opentech/apply/projects/migrations/0024_allow_no_comments_on_pr.py
 create mode 100644 opentech/apply/projects/templates/application_projects/paymentrequest_confirm_delete.html

diff --git a/opentech/apply/activity/messaging.py b/opentech/apply/activity/messaging.py
index 74fb6c01a..77a92b8a1 100644
--- a/opentech/apply/activity/messaging.py
+++ b/opentech/apply/activity/messaging.py
@@ -55,8 +55,11 @@ neat_related = {
     MESSAGES.DELETE_REVIEW: 'review',
     MESSAGES.EDIT_REVIEW: 'review',
     MESSAGES.CREATED_PROJECT: 'submission',
+    MESSAGES.PROJECT_TRANSITION: 'old_stage',
     MESSAGES.UPDATE_PROJECT_LEAD: 'old_lead',
     MESSAGES.APPROVE_CONTRACT: 'contract',
+    MESSAGES.UPLOAD_CONTRACT: 'contract',
+    MESSAGES.REQUEST_PAYMENT: 'payment_request',
     MESSAGES.UPDATE_PAYMENT_REQUEST_STATUS: 'payment_request',
     MESSAGES.DELETE_PAYMENT_REQUEST: 'payment_request',
     MESSAGES.UPDATE_PAYMENT_REQUEST: 'payment_request',
@@ -219,11 +222,12 @@ class ActivityAdapter(AdapterBase):
         MESSAGES.SCREENING: 'Screening status from {old_status} to {source.screening_status}',
         MESSAGES.REVIEW_OPINION: '{user} {opinion.opinion_display}s with {opinion.review.author}''s review of {source}',
         MESSAGES.CREATED_PROJECT: 'Created',
+        MESSAGES.PROJECT_TRANSITION: 'Progressed from {old_stage} to {source.status_display}',
         MESSAGES.UPDATE_PROJECT_LEAD: 'Lead changed from {old_lead} to {source.lead}',
         MESSAGES.SEND_FOR_APPROVAL: 'Requested approval',
         MESSAGES.APPROVE_PROJECT: 'Approved',
         MESSAGES.REQUEST_PROJECT_CHANGE: 'Requested changes for acceptance: "{comment}"',
-        MESSAGES.UPLOAD_CONTRACT: 'Uploaded a contract',
+        MESSAGES.UPLOAD_CONTRACT: 'Uploaded a {contract.state} contract',
         MESSAGES.APPROVE_CONTRACT: 'Approved contract',
         MESSAGES.UPDATE_PAYMENT_REQUEST_STATUS: 'Updated Payment Request status to: {payment_request.status_display}',
         MESSAGES.REQUEST_PAYMENT: 'Payment Request submitted',
diff --git a/opentech/apply/activity/migrations/0048_add_project_transition.py b/opentech/apply/activity/migrations/0048_add_project_transition.py
new file mode 100644
index 000000000..b576b1ed8
--- /dev/null
+++ b/opentech/apply/activity/migrations/0048_add_project_transition.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.1.11 on 2019-09-05 05:58
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('activity', '0047_add_update_payment_request'),
+    ]
+
+    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')], max_length=50),
+        ),
+    ]
diff --git a/opentech/apply/activity/options.py b/opentech/apply/activity/options.py
index e04ddcf90..7d2f9bb58 100644
--- a/opentech/apply/activity/options.py
+++ b/opentech/apply/activity/options.py
@@ -31,6 +31,7 @@ class MESSAGES(Enum):
     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'
diff --git a/opentech/apply/activity/templates/messages/email/contract_uploaded.html b/opentech/apply/activity/templates/messages/email/contract_uploaded.html
index fdda12cc6..5a5aeed5b 100644
--- a/opentech/apply/activity/templates/messages/email/contract_uploaded.html
+++ b/opentech/apply/activity/templates/messages/email/contract_uploaded.html
@@ -5,4 +5,11 @@ A new contract has been added to your Project:
 
 Title: {{ source.title }}
 Link: {{ request.scheme }}://{{ request.get_host }}{{ source.get_absolute_url }}
+
+{% if contract.is_signed %}
+This contract has already been signed and there is no action for you to take.
+{% else %}
+Please review the contract and sign it before reuploading it to your Project page for the {{ ORG_SHORT_NAME }} Team to approve.
+{% endif %}
+
 {% endblock %}
diff --git a/opentech/apply/activity/templatetags/activity_tags.py b/opentech/apply/activity/templatetags/activity_tags.py
index 9a1aa5e4e..649e48387 100644
--- a/opentech/apply/activity/templatetags/activity_tags.py
+++ b/opentech/apply/activity/templatetags/activity_tags.py
@@ -3,6 +3,7 @@ import json
 from django import template
 
 from opentech.apply.determinations.models import Determination
+from opentech.apply.projects.models import Contract
 from opentech.apply.review.models import Review
 
 from ..models import TEAM, ALL, REVIEWER
@@ -25,7 +26,7 @@ def user_can_see_related(activity, user):
     if user.is_apply_staff:
         return True
 
-    if isinstance(activity.related_object, Determination):
+    if isinstance(activity.related_object, (Determination, Contract)):
         return True
 
     return False
diff --git a/opentech/apply/dashboard/views.py b/opentech/apply/dashboard/views.py
index 646141271..148bddc9b 100644
--- a/opentech/apply/dashboard/views.py
+++ b/opentech/apply/dashboard/views.py
@@ -328,7 +328,7 @@ class ApplicantDashboardView(MultiTableMixin, TemplateView):
         return context
 
     def get_active_project_data(self, user):
-        return Project.objects.filter(user=user).in_progress()
+        return Project.objects.filter(user=user).in_progress().for_table()
 
     def get_active_submissions(self, user):
         active_subs = ApplicationSubmission.objects.filter(
@@ -339,7 +339,7 @@ class ApplicantDashboardView(MultiTableMixin, TemplateView):
             yield submission.from_draft()
 
     def get_historical_project_data(self, user):
-        return Project.objects.filter(user=user).complete()
+        return Project.objects.filter(user=user).complete().for_table()
 
     def get_historical_submission_data(self, user):
         return ApplicationSubmission.objects.filter(
diff --git a/opentech/apply/projects/forms.py b/opentech/apply/projects/forms.py
index 693588845..5dc566f88 100644
--- a/opentech/apply/projects/forms.py
+++ b/opentech/apply/projects/forms.py
@@ -199,19 +199,21 @@ class RemoveDocumentForm(forms.ModelForm):
         super().__init__(*args, **kwargs)
 
 
-class RequestPaymentForm(forms.ModelForm):
-    receipts = MultiFileField()
-
+class PaymentRequestBaseForm(forms.ModelForm):
     class Meta:
-        fields = ['requested_value', 'invoice', 'date_from', 'date_to', 'receipts', 'comment']
+        fields = ['requested_value', 'invoice', 'date_from', 'date_to']
         model = PaymentRequest
         widgets = {
             'date_from': forms.DateInput,
             'date_to': forms.DateInput,
         }
+        labels = {
+            'requested_value': 'Requested Value ($)'
+        }
 
-    def __init__(self, user=None, instance=None, *args, **kwargs):
+    def __init__(self, user=None, *args, **kwargs):
         super().__init__(*args, **kwargs)
+        self.fields['requested_value'].widget.attrs['min'] = 0
 
     def clean(self):
         cleaned_data = super().clean()
@@ -223,6 +225,10 @@ class RequestPaymentForm(forms.ModelForm):
 
         return cleaned_data
 
+
+class CreatePaymentRequestForm(PaymentRequestBaseForm):
+    receipts = MultiFileField()
+
     def save(self, commit=True):
         request = super().save(commit=commit)
 
@@ -234,7 +240,7 @@ class RequestPaymentForm(forms.ModelForm):
         return request
 
 
-class EditPaymentRequestForm(forms.ModelForm):
+class EditPaymentRequestForm(PaymentRequestBaseForm):
     receipt_list = forms.ModelMultipleChoiceField(
         widget=forms.CheckboxSelectMultiple(attrs={'class': 'delete'}),
         queryset=PaymentReceipt.objects.all(),
@@ -243,14 +249,6 @@ class EditPaymentRequestForm(forms.ModelForm):
     )
     receipts = MultiFileField(label='', required=False)
 
-    class Meta:
-        fields = ['invoice', 'requested_value', 'date_from', 'date_to', 'receipt_list', 'receipts', 'comment']
-        model = PaymentRequest
-        widgets = {
-            'date_from': forms.DateInput,
-            'date_to': forms.DateInput,
-        }
-
     def __init__(self, user=None, instance=None, *args, **kwargs):
         super().__init__(*args, instance=instance, **kwargs)
 
@@ -259,8 +257,8 @@ class EditPaymentRequestForm(forms.ModelForm):
         self.fields['requested_value'].label = 'Value'
 
     @transaction.atomic
-    def save(self, *args, **kwargs):
-        request = super().save(*args, **kwargs)
+    def save(self, commit=True):
+        request = super().save(commit=commit)
 
         removed_receipts = self.cleaned_data['receipt_list']
 
diff --git a/opentech/apply/projects/migrations/0023_ensure_contracts_uses_private_storage.py b/opentech/apply/projects/migrations/0023_ensure_contracts_uses_private_storage.py
new file mode 100644
index 000000000..b7cbcf235
--- /dev/null
+++ b/opentech/apply/projects/migrations/0023_ensure_contracts_uses_private_storage.py
@@ -0,0 +1,20 @@
+# Generated by Django 2.1.11 on 2019-09-05 08:48
+
+import django.core.files.storage
+from django.db import migrations, models
+import opentech.apply.projects.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('application_projects', '0022_update_field_definitions_for_forms'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='contract',
+            name='file',
+            field=models.FileField(storage=django.core.files.storage.FileSystemStorage(), upload_to=opentech.apply.projects.models.contract_path),
+        ),
+    ]
diff --git a/opentech/apply/projects/migrations/0024_allow_no_comments_on_pr.py b/opentech/apply/projects/migrations/0024_allow_no_comments_on_pr.py
new file mode 100644
index 000000000..a694b4d96
--- /dev/null
+++ b/opentech/apply/projects/migrations/0024_allow_no_comments_on_pr.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.1.11 on 2019-09-05 08:49
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('application_projects', '0023_ensure_contracts_uses_private_storage'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='paymentrequest',
+            name='comment',
+            field=models.TextField(blank=True),
+        ),
+    ]
diff --git a/opentech/apply/projects/models.py b/opentech/apply/projects/models.py
index 9aba62c2d..7e2739bd1 100644
--- a/opentech/apply/projects/models.py
+++ b/opentech/apply/projects/models.py
@@ -75,16 +75,22 @@ class Contract(models.Model):
     approver = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL, related_name='contracts')
     project = models.ForeignKey("Project", on_delete=models.CASCADE, related_name="contracts")
 
-    file = models.FileField(upload_to=contract_path)
+    file = models.FileField(upload_to=contract_path, storage=PrivateStorage())
 
     is_signed = models.BooleanField("Signed?", default=False)
     created_at = models.DateTimeField(auto_now_add=True)
 
     objects = ContractQuerySet.as_manager()
 
+    @property
+    def state(self):
+        return 'Signed' if self.is_signed else 'Unsigned'
+
     def __str__(self):
-        state = 'Signed' if self.is_signed else 'Unsigned'
-        return f'Contract for {self.project} ({state})'
+        return f'Contract for {self.project} ({self.state})'
+
+    def get_absolute_url(self):
+        return reverse('apply:projects:contract', args=[self.project.pk, self.pk])
 
 
 class PacketFile(models.Model):
@@ -190,7 +196,7 @@ class PaymentRequest(models.Model):
     requested_at = models.DateTimeField(auto_now_add=True)
     date_from = models.DateTimeField()
     date_to = models.DateTimeField()
-    comment = models.TextField()
+    comment = models.TextField(blank=True)
     status = models.TextField(choices=REQUEST_STATUS_CHOICES, default=SUBMITTED)
 
     objects = PaymentRequestQueryset.as_manager()
@@ -211,6 +217,10 @@ class PaymentRequest(models.Model):
             if self.status in (SUBMITTED, CHANGES_REQUESTED):
                 return True
 
+        if user.is_apply_staff:
+            if self.status in {SUBMITTED}:
+                return True
+
         return False
 
     def can_user_edit(self, user):
@@ -224,13 +234,6 @@ class PaymentRequest(models.Model):
 
         return False
 
-    def user_can_delete(self, user):
-        if user.is_apply_staff:
-            if self.status in {SUBMITTED}:
-                return True
-
-        return False
-
     def can_user_change_status(self, user):
         if not user.is_apply_staff:
             return False  # Users can't change status
@@ -341,6 +344,10 @@ class Project(BaseStreamForm, AccessFormData, models.Model):
     def __str__(self):
         return self.title
 
+    @property
+    def status_display(self):
+        return self.get_status_display()
+
     def get_address_display(self):
         address = json.loads(self.contact_address)
         return ', '.join(
@@ -415,6 +422,9 @@ class Project(BaseStreamForm, AccessFormData, models.Model):
 
     @property
     def editable(self):
+        if self.status not in (CONTRACTING, COMMITTED):
+            return True
+
         # Someone has approved the project - consider it locked while with contracting
         if self.approvals.exists():
             return False
diff --git a/opentech/apply/projects/templates/application_projects/includes/payment_requests.html b/opentech/apply/projects/templates/application_projects/includes/payment_requests.html
index 103e91908..940cd498a 100644
--- a/opentech/apply/projects/templates/application_projects/includes/payment_requests.html
+++ b/opentech/apply/projects/templates/application_projects/includes/payment_requests.html
@@ -3,10 +3,8 @@
 <div id="payment-requests" class="payment-block">
     <div class="payment-block__header">
         <p class="payment-block__title">Payment Requests</p>
-        <a data-fancybox
-           data-src="#request-payment"
-           class="payment-block__button button button--primary"
-           href="#">
+        <a class="payment-block__button button button--primary"
+           href="{% url "apply:projects:request" pk=object.pk %}">
             Add Request
         </a>
     </div>
@@ -32,19 +30,16 @@
                     <a href="{{ payment_request.get_absolute_url }}">View</a>
                     {% can_edit payment_request user as user_can_edit_request %}
                     {% if user_can_edit_request %}
-                    <a
-                        class="payment-block__edit-link"
-                        href="{% url "apply:projects:payments:edit" pk=payment_request.pk %}">
+                    <a href="{% url "apply:projects:payments:edit" pk=payment_request.pk %}">
                         Edit
                     </a>
                     {% endif %}
 
                     {% can_delete payment_request user as user_can_delete_request %}
                     {% if user_can_delete_request %}
-                    <form method="POST" action="{% url 'apply:projects:delete-payment-request' pk=object.pk payment_request_id=payment_request.pk %}">
-                        {% csrf_token %}
-                        <button class="button button--primary" type="submit">Delete</button>
-                    </form>
+                    <a href="{% url 'apply:projects:payments:delete' pk=payment_request.pk %}">
+                        Delete
+                    </a>
                     {% endif %}
                 </td>
             </tr>
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 80d56ac2e..597258a5b 100644
--- a/opentech/apply/projects/templates/application_projects/includes/supporting_documents.html
+++ b/opentech/apply/projects/templates/application_projects/includes/supporting_documents.html
@@ -14,7 +14,9 @@
             </div>
             <div class="docs-block__row-inner">
                 <a class="docs-block__link" href="{% url 'apply:submissions:simplified' pk=project.submission.pk %}">View</a>
-                <a class="docs-block__link" href="#">Download</a>
+                {% if not user.is_applicant %}
+                    <a class="docs-block__link" href="#">Download</a>
+                {% endif %}
             </div>
         </li>
 
@@ -35,7 +37,7 @@
                         {% endif %}
                     </a>
                 {% endif %}
-                {% if object.user_has_updated_details %}
+                {% if object.user_has_updated_details and not user.is_applicant %}
                     <a class="docs-block__link" href="{% url 'apply:projects:simplified' pk=project.pk %}">
                         View
                     </a>
@@ -110,7 +112,8 @@
         </li>
     </ul>
     <div class="docs-block__buttons">
-        {% if object.can_send_for_approval %}
+        {% can_send_for_approval object user as can_approve %}
+        {% if can_approve %}
             <a data-fancybox
             data-src="#send-for-approval"
             class="button button--primary"
@@ -118,7 +121,6 @@
                 Submit for Approval
             </a>
         {% endif %}
-        <!-- <button class="button button--primary" href="#">Ready for contracting</button> -->
     </div>
 </div>
 
@@ -130,5 +132,6 @@
 
 <div class="modal" id="upload-supporting-doc">
     <h4 class="modal__header-bar">Upload a new document</h4>
+    <p></p>
     {% include 'funds/includes/delegated_form_base.html' with form=document_form value='Upload'%}
 </div>
diff --git a/opentech/apply/projects/templates/application_projects/paymentrequest_confirm_delete.html b/opentech/apply/projects/templates/application_projects/paymentrequest_confirm_delete.html
new file mode 100644
index 000000000..d3907d95c
--- /dev/null
+++ b/opentech/apply/projects/templates/application_projects/paymentrequest_confirm_delete.html
@@ -0,0 +1,37 @@
+
+{% extends "base-apply.html" %}
+{% load humanize payment_request_tools %}
+
+{% block title %}Payment Request: {{ object.project.title }}{% endblock %}
+{% block content %}
+<div class="admin-bar">
+    <div class="admin-bar__inner">
+        <a class="simplified__projects-link" href="{{ object.project.get_absolute_url }}">
+            Project
+        </a>
+        <h2 class="heading heading--no-margin">Delete Payment Request</h2>
+        <h5 class="heading heading--no-margin">For: {{ object.project.title }}</h5>
+    </div>
+</div>
+
+<div class="wrapper wrapper--sidebar wrapper--outer-space-medium">
+    <div class="wrapper--sidebar--inner">
+
+        <div class="card card--solid">
+            <p class="card__text"><b>Status:</b> {{ object.get_status_display }}</p>
+            <p class="card__text"><b>Name of Vendor:</b> {{ object.project.contact_legal_name }}</p>
+            <p class="card__text"><b>Invoice Number:</b> {{ object.pk }}</p>
+            <p class="card__text"><b>Period of Performance:</b> {{ object.date_from.date }} | {{ object.date_to.date }}</p>
+            <p class="card__text"><b>Total:</b> ${{ object.value|intcomma }}</p>
+
+        </div>
+        <div class="card card--solid">
+            <form method="post">{% csrf_token %}
+                <p>Are you sure you want to delete this payment request for {{ object.project.title }}?</p>
+                <input class="button button--primary" type="submit" value="Confirm">
+            </form>
+
+        </div>
+    </div>
+</div>
+{% endblock %}
diff --git a/opentech/apply/projects/templates/application_projects/paymentrequest_detail.html b/opentech/apply/projects/templates/application_projects/paymentrequest_detail.html
index 63d8987fa..eb034ad84 100644
--- a/opentech/apply/projects/templates/application_projects/paymentrequest_detail.html
+++ b/opentech/apply/projects/templates/application_projects/paymentrequest_detail.html
@@ -53,6 +53,12 @@
                 >
                     Edit
                 </a>
+                {% can_delete object user as user_can_delete_request %}
+                {% if user_can_delete_request %}
+                <a
+                    class="button button--bottom-space button--primary button--full-width"
+                    href="{% url 'apply:projects:payments:delete' pk=object.pk %}">Delete</a>
+            {% endif %}
             {% endblock %}
         </div>
     </aside>
diff --git a/opentech/apply/projects/templates/application_projects/paymentrequest_form.html b/opentech/apply/projects/templates/application_projects/paymentrequest_form.html
index b282e7ca1..c0b49835b 100644
--- a/opentech/apply/projects/templates/application_projects/paymentrequest_form.html
+++ b/opentech/apply/projects/templates/application_projects/paymentrequest_form.html
@@ -1,12 +1,12 @@
 {% extends "base-apply.html" %}
 {% load static %}
 
-{% block title %}Edit Payment Request: {{ object.project.title }}{% endblock %}
+{% block title %}{% if object %}Edit{% else %}Create{% endif %} Payment Request: {% if object %}{{ object.project.title }}{% else %}{{ project.title }}{% endif %}{% endblock %}
 {% block content %}
 <div class="admin-bar">
     <div class="admin-bar__inner">
-        <h2 class="heading heading--no-margin">Editing Payment Request</h2>
-        <h5 class="heading heading--no-margin">{{ object.project.title }}</h5>
+        <h2 class="heading heading--no-margin">{% if object %}Editing{% else %}Create{% endif %} Payment Request</h2>
+        <h5 class="heading heading--no-margin">{% if object %}{{ object.project.title }}{% else %}{{ project.title }}{% endif %}</h5>
     </div>
 </div>
 
diff --git a/opentech/apply/projects/templates/application_projects/project_detail.html b/opentech/apply/projects/templates/application_projects/project_detail.html
index e671b2cde..de8cabd35 100644
--- a/opentech/apply/projects/templates/application_projects/project_detail.html
+++ b/opentech/apply/projects/templates/application_projects/project_detail.html
@@ -169,15 +169,8 @@
                     {% endif %}
 
                     {% if object.can_request_funding %}
-                    <div class="modal" id="request-payment">
-                        <h4 class="modal__header-bar">Request Payment</h4>
-                        {% include 'funds/includes/delegated_form_base.html' with form=request_payment_form value='Request'%}
-                    </div>
-
-                    <a data-fancybox
-                       data-src="#request-payment"
-                       class="button button--primary button--bottom-space button--full-width"
-                       href="#">
+                    <a class="button button--primary button--bottom-space button--full-width"
+                       href="{% url "apply:projects:request" pk=object.pk %}">
                         Add payment request
                     </a>
                     {% endif %}
diff --git a/opentech/apply/projects/templatetags/approval_tools.py b/opentech/apply/projects/templatetags/approval_tools.py
index 2c51d1a59..17de68f47 100644
--- a/opentech/apply/projects/templatetags/approval_tools.py
+++ b/opentech/apply/projects/templatetags/approval_tools.py
@@ -8,6 +8,11 @@ def user_has_approved(project, user):
     return project.approvals.filter(by=user).exists()
 
 
+@register.simple_tag
+def can_send_for_approval(project, user):
+    return user.is_staff and project.can_send_for_approval
+
+
 @register.simple_tag
 def user_can_approve_project(project, user):
     return user.is_approver and not user_has_approved(project, user)
diff --git a/opentech/apply/projects/tests/test_forms.py b/opentech/apply/projects/tests/test_forms.py
index 7685bbdb4..1e24303d6 100644
--- a/opentech/apply/projects/tests/test_forms.py
+++ b/opentech/apply/projects/tests/test_forms.py
@@ -11,8 +11,8 @@ from opentech.apply.users.tests.factories import (
 from ..files import get_files
 from ..forms import (
     ChangePaymentRequestStatusForm,
+    CreatePaymentRequestForm,
     ProjectApprovalForm,
-    RequestPaymentForm,
     SelectDocumentForm,
     StaffUploadContractForm,
     UploadContractForm,
@@ -109,7 +109,7 @@ class TestProjectApprovalForm(TestCase):
         self.assertTrue(project.user_has_updated_details)
 
 
-class TestRequestPaymentForm(TestCase):
+class TestCreatePaymentRequestForm(TestCase):
     def test_adding_payment_request(self):
         data = {
             'requested_value': '10',
@@ -125,7 +125,7 @@ class TestRequestPaymentForm(TestCase):
             'receipts': receipts,
         }
 
-        form = RequestPaymentForm(data=data, files=files)
+        form = CreatePaymentRequestForm(data=data, files=files)
         self.assertTrue(form.is_valid(), msg=form.errors)
 
         form.instance.by = UserFactory()
@@ -142,7 +142,7 @@ class TestRequestPaymentForm(TestCase):
             'receipts': receipts,
         }
 
-        form = RequestPaymentForm(
+        form = CreatePaymentRequestForm(
             files=files,
             data={
                 'requested_value': '10',
@@ -153,7 +153,7 @@ class TestRequestPaymentForm(TestCase):
         )
         self.assertTrue(form.is_valid(), msg=form.errors)
 
-        form = RequestPaymentForm(
+        form = CreatePaymentRequestForm(
             files=files,
             data={
                 'requested_value': '10',
diff --git a/opentech/apply/projects/tests/test_models.py b/opentech/apply/projects/tests/test_models.py
index 5225dba1f..a467f9f1d 100644
--- a/opentech/apply/projects/tests/test_models.py
+++ b/opentech/apply/projects/tests/test_models.py
@@ -77,11 +77,11 @@ class TestProjectModel(TestCase):
 
 
 class TestPaymentRequestModel(TestCase):
-    def test_staff_cant_delete_from_submitted(self):
+    def test_staff_can_delete_from_submitted(self):
         payment_request = PaymentRequestFactory(status=SUBMITTED)
         staff = StaffFactory()
 
-        self.assertFalse(payment_request.can_user_delete(staff))
+        self.assertTrue(payment_request.can_user_delete(staff))
 
     def test_staff_cant_delete_from_under_review(self):
         payment_request = PaymentRequestFactory(status=UNDER_REVIEW)
diff --git a/opentech/apply/projects/tests/test_templatetags.py b/opentech/apply/projects/tests/test_templatetags.py
index a6ce390f9..72bccc8e3 100644
--- a/opentech/apply/projects/tests/test_templatetags.py
+++ b/opentech/apply/projects/tests/test_templatetags.py
@@ -152,11 +152,11 @@ class TestPaymentRequestTools(TestCase):
 
         self.assertFalse(can_change_status(payment_request, user))
 
-    def test_staff_cant_delete_from_submitted(self):
+    def test_staff_can_delete_from_submitted(self):
         payment_request = PaymentRequestFactory(status=SUBMITTED)
         staff = StaffFactory()
 
-        self.assertFalse(can_delete(payment_request, staff))
+        self.assertTrue(can_delete(payment_request, staff))
 
     def test_staff_cant_delete_from_under_review(self):
         payment_request = PaymentRequestFactory(status=UNDER_REVIEW)
diff --git a/opentech/apply/projects/tests/test_views.py b/opentech/apply/projects/tests/test_views.py
index a2311e728..8d84549be 100644
--- a/opentech/apply/projects/tests/test_views.py
+++ b/opentech/apply/projects/tests/test_views.py
@@ -820,7 +820,7 @@ class TestAnonPacketView(BasePacketFileViewTestCase):
 
 
 class TestRequestPaymentViewAsApplicant(BaseViewTestCase):
-    base_view_name = 'detail'
+    base_view_name = 'request'
     url_name = 'funds:projects:{}'
     user_factory = ApplicantFactory
 
@@ -842,7 +842,6 @@ class TestRequestPaymentViewAsApplicant(BaseViewTestCase):
             'requested_value': '10',
             'date_from': '2018-08-15',
             'date_to': '2019-08-15',
-            'comment': 'test comment',
             'invoice': invoice,
             'receipts': receipts,
         })
@@ -854,7 +853,7 @@ class TestRequestPaymentViewAsApplicant(BaseViewTestCase):
 
 
 class TestRequestPaymentViewAsStaff(BaseViewTestCase):
-    base_view_name = 'detail'
+    base_view_name = 'request'
     url_name = 'funds:projects:{}'
     user_factory = StaffFactory
 
diff --git a/opentech/apply/projects/urls.py b/opentech/apply/projects/urls.py
index 1ee9eec77..81c86378d 100644
--- a/opentech/apply/projects/urls.py
+++ b/opentech/apply/projects/urls.py
@@ -2,6 +2,8 @@ from django.conf import settings
 from django.urls import include, path
 
 from .views import (
+    ContractPrivateMediaView,
+    CreatePaymentRequestView,
     DeletePaymentRequestView,
     EditPaymentRequestView,
     PaymentRequestListView,
@@ -27,7 +29,9 @@ if settings.PROJECTS_ENABLED:
             path('', ProjectDetailView.as_view(), name='detail'),
             path('edit/', ProjectEditView.as_view(), name="edit"),
             path('documents/<int:file_pk>/', ProjectPrivateMediaView.as_view(), name="document"),
+            path('contract/<int:file_pk>/', ContractPrivateMediaView.as_view(), name="contract"),
             path('simplified/', ProjectDetailSimplifiedView.as_view(), name='simplified'),
+            path('request/', CreatePaymentRequestView.as_view(), name='request'),
         ])),
         path('payment-requests/', include(([
             path('', PaymentRequestListView.as_view(), name='all'),
diff --git a/opentech/apply/projects/views/payment.py b/opentech/apply/projects/views/payment.py
index 768573140..b9306f7bc 100644
--- a/opentech/apply/projects/views/payment.py
+++ b/opentech/apply/projects/views/payment.py
@@ -24,8 +24,8 @@ from opentech.apply.utils.views import (
 
 from ..forms import (
     ChangePaymentRequestStatusForm,
+    CreatePaymentRequestForm,
     EditPaymentRequestForm,
-    RequestPaymentForm,
 )
 from ..filters import PaymentRequestListFilter
 from ..models import (
@@ -74,13 +74,10 @@ class ChangePaymentRequestStatusView(DelegatedViewMixin, PaymentRequestAccessMix
 
 class DeletePaymentRequestView(DeleteView):
     model = PaymentRequest
-    pk_url_kwarg = 'payment_request_id'
 
     def dispatch(self, request, *args, **kwargs):
-        self.project = get_object_or_404(Project, pk=self.kwargs['pk'])
-
         self.object = self.get_object()
-        if not self.object.user_can_delete(request.user):
+        if not self.object.can_user_delete(request.user):
             raise PermissionDenied
 
         return super().dispatch(request, *args, **kwargs)
@@ -119,6 +116,36 @@ class PaymentRequestView(ViewDispatcher):
     applicant_view = PaymentRequestApplicantView
 
 
+class CreatePaymentRequestView(CreateView):
+    model = PaymentRequest
+    form_class = CreatePaymentRequestForm
+
+    def dispatch(self, request, *args, **kwargs):
+        self.project = Project.objects.get(pk=kwargs['pk'])
+        if not request.user.is_apply_staff and not self.project.user == request.user:
+            return redirect(self.project)
+        return super().dispatch(request, *args, **kwargs)
+
+    def get_context_data(self, **kwargs):
+        return super().get_context_data(project=self.project, **kwargs)
+
+    def form_valid(self, form):
+        form.instance.project = self.project
+        form.instance.by = self.request.user
+
+        response = super().form_valid(form)
+
+        messenger(
+            MESSAGES.REQUEST_PAYMENT,
+            request=self.request,
+            user=self.request.user,
+            source=self.project,
+            related=self.object,
+        )
+
+        return response
+
+
 class EditPaymentRequestView(PaymentRequestAccessMixin, UpdateView):
     form_class = EditPaymentRequestForm
 
@@ -170,28 +197,6 @@ class PaymentRequestPrivateMedia(UserPassesTestMixin, PrivateMediaView):
         return False
 
 
-class RequestPaymentView(DelegatedViewMixin, CreateView):
-    context_name = 'request_payment_form'
-    form_class = RequestPaymentForm
-    model = PaymentRequest
-
-    def form_valid(self, form):
-        project = self.kwargs['object']
-
-        form.instance.by = self.request.user
-        form.instance.project = project
-        response = super().form_valid(form)
-
-        messenger(
-            MESSAGES.REQUEST_PAYMENT,
-            request=self.request,
-            user=self.request.user,
-            source=project,
-        )
-
-        return response
-
-
 @method_decorator(staff_required, name='dispatch')
 class PaymentRequestListView(SingleTableMixin, FilterView):
     filterset_class = PaymentRequestListFilter
diff --git a/opentech/apply/projects/views/project.py b/opentech/apply/projects/views/project.py
index 7bd9ee75e..88cf33d6b 100644
--- a/opentech/apply/projects/views/project.py
+++ b/opentech/apply/projects/views/project.py
@@ -67,8 +67,6 @@ from ..tables import (
     ProjectsListTable
 )
 
-from .payment import RequestPaymentView
-
 
 # APPROVAL VIEWS
 
@@ -107,6 +105,7 @@ class CreateApprovalView(DelegatedViewMixin, CreateView):
     @transaction.atomic()
     def form_valid(self, form):
         project = self.kwargs['object']
+        old_stage = project.get_status_display()
 
         form.instance.project = project
 
@@ -125,6 +124,14 @@ class CreateApprovalView(DelegatedViewMixin, CreateView):
         project.status = CONTRACTING
         project.save(update_fields=['is_locked', 'status'])
 
+        messenger(
+            MESSAGES.PROJECT_TRANSITION,
+            request=self.request,
+            user=self.request.user,
+            source=project,
+            related=old_stage,
+        )
+
         return response
 
 
@@ -315,6 +322,8 @@ class ApproveContractView(DelegatedViewMixin, UpdateView):
             form.instance.project = self.project
             response = super().form_valid(form)
 
+            old_stage = self.project.get_status_display()
+
             messenger(
                 MESSAGES.APPROVE_CONTRACT,
                 request=self.request,
@@ -326,6 +335,14 @@ class ApproveContractView(DelegatedViewMixin, UpdateView):
             self.project.status = IN_PROGRESS
             self.project.save(update_fields=['status'])
 
+            messenger(
+                MESSAGES.PROJECT_TRANSITION,
+                request=self.request,
+                user=self.request.user,
+                source=self.project,
+                related=old_stage,
+            )
+
         return response
 
     def get_success_url(self):
@@ -373,18 +390,26 @@ class UploadContractView(DelegatedViewMixin, CreateView):
             request=self.request,
             user=self.request.user,
             source=project,
+            related=form.instance,
         )
 
         return response
 
 
 # PROJECT VIEW
+class BaseProjectDetailView(DetailView):
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
+        context['statuses'] = PROJECT_STATUS_CHOICES
+        context['current_status_index'] = [status for status, _ in PROJECT_STATUS_CHOICES].index(self.object.status)
+        return context
+
 
 class AdminProjectDetailView(
     ActivityContextMixin,
     DelegateableView,
     ContractsMixin,
-    DetailView,
+    BaseProjectDetailView,
 ):
     form_views = [
         ApproveContractView,
@@ -392,7 +417,6 @@ class AdminProjectDetailView(
         CreateApprovalView,
         RejectionView,
         RemoveDocumentView,
-        RequestPaymentView,
         SelectDocumentView,
         SendForApprovalView,
         UpdateLeadView,
@@ -404,19 +428,22 @@ class AdminProjectDetailView(
 
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
-        context['statuses'] = PROJECT_STATUS_CHOICES
-        context['current_status_index'] = [status for status, _ in PROJECT_STATUS_CHOICES].index(self.object.status)
         context['approvals'] = self.object.approvals.distinct('by')
         context['remaining_document_categories'] = list(self.object.get_missing_document_categories())
         return context
 
 
-class ApplicantProjectDetailView(ActivityContextMixin, DelegateableView, ContractsMixin, DetailView):
+class ApplicantProjectDetailView(
+    ActivityContextMixin,
+    DelegateableView,
+    ContractsMixin,
+    BaseProjectDetailView,
+):
     form_views = [
         CommentFormView,
-        RequestPaymentView,
         SelectDocumentView,
         UploadContractView,
+        UploadDocumentView,
     ]
 
     model = Project
@@ -459,6 +486,31 @@ class ProjectPrivateMediaView(UserPassesTestMixin, PrivateMediaView):
         return False
 
 
+@method_decorator(login_required, name='dispatch')
+class ContractPrivateMediaView(UserPassesTestMixin, PrivateMediaView):
+    raise_exception = True
+
+    def dispatch(self, *args, **kwargs):
+        project_pk = self.kwargs['pk']
+        self.project = get_object_or_404(Project, pk=project_pk)
+        return super().dispatch(*args, **kwargs)
+
+    def get_media(self, *args, **kwargs):
+        document = Contract.objects.get(pk=kwargs['file_pk'])
+        if document.project != self.project:
+            raise Http404
+        return document.file
+
+    def test_func(self):
+        if self.request.user.is_apply_staff:
+            return True
+
+        if self.request.user == self.project.user:
+            return True
+
+        return False
+
+
 # PROJECT EDIT
 
 @method_decorator(staff_required, name='dispatch')
diff --git a/opentech/static_src/src/sass/apply/components/_payment-block.scss b/opentech/static_src/src/sass/apply/components/_payment-block.scss
index 5b1c248b9..3e2a5da4f 100644
--- a/opentech/static_src/src/sass/apply/components/_payment-block.scss
+++ b/opentech/static_src/src/sass/apply/components/_payment-block.scss
@@ -43,10 +43,6 @@
         }
     }
 
-    &__edit-link {
-        margin-left: 1rem;
-    }
-
     &__rejected {
         text-align: center;
     }
@@ -118,9 +114,11 @@
 
                 &:last-child {
                     padding: 0 0 1rem;
-
-                    @include media-query(tablet-landscape) {
-                        padding: 1rem;
+                    display: flex;
+                    flex-wrap: wrap;
+                    & > * {
+                        flex: 1 1 55px;
+                        max-width: 55px;
                     }
                 }
 
-- 
GitLab