From 3ed3b0ad23bed159e2124f07e8d45cd1b57b6d5e Mon Sep 17 00:00:00 2001
From: Wes Appler <145372368+wes-otf@users.noreply.github.com>
Date: Wed, 17 Apr 2024 09:57:17 -0400
Subject: [PATCH] Fixes to application previews and draft refactoring (#3795)

Fixes #3788 and a few other issues. The main problems being seen around
the preview were when it came to the edit view. This was because the use
of the `Draft` status was being relied on for the saving of content
before previews, when realistically a new application revision had the
same effect.

Previously, Admins/Staff could submit new applications, but could not
submit existing applications. This is why when an Admin had attempted to
submit a preview or submit an edited application, they could not do so.
This functionality has been moved from `ApplicantSubmissionEditView` to
`BaseSubmissionEditView` as it made sense to me that Admins would be
able to access the same edit workflow & transitions as Applicants. I
believe most OTF staff avoid editing incoming applications though.
---
 .../0117_applicationrevision_is_draft.py      |  17 ++
 .../funds/models/application_revisions.py     |   3 +
 hypha/apply/funds/models/applications.py      |  27 +-
 hypha/apply/funds/models/submissions.py       |  72 ++++-
 .../templates/funds/application_base.html     |   3 +-
 .../templates/funds/application_preview.html  |  11 +-
 .../funds/applicationrevision_list.html       |   3 +
 hypha/apply/funds/tests/test_models.py        |   4 -
 hypha/apply/funds/tests/test_views.py         |  16 +-
 hypha/apply/funds/views.py                    | 263 +++++++++++-------
 hypha/settings/base.py                        |   3 +-
 .../static_src/sass/components/_revision.scss |   3 +-
 12 files changed, 286 insertions(+), 139 deletions(-)
 create mode 100644 hypha/apply/funds/migrations/0117_applicationrevision_is_draft.py

diff --git a/hypha/apply/funds/migrations/0117_applicationrevision_is_draft.py b/hypha/apply/funds/migrations/0117_applicationrevision_is_draft.py
new file mode 100644
index 000000000..6691630a0
--- /dev/null
+++ b/hypha/apply/funds/migrations/0117_applicationrevision_is_draft.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.2.10 on 2024-03-12 16:23
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("funds", "0115_list_on_front_page"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="applicationrevision",
+            name="is_draft",
+            field=models.BooleanField(default=False),
+        ),
+    ]
diff --git a/hypha/apply/funds/models/application_revisions.py b/hypha/apply/funds/models/application_revisions.py
index 14508d6f6..3ee8d3800 100644
--- a/hypha/apply/funds/models/application_revisions.py
+++ b/hypha/apply/funds/models/application_revisions.py
@@ -22,6 +22,9 @@ class ApplicationRevision(BaseStreamForm, AccessFormData, models.Model):
         settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True
     )
 
+    # Is the revision a draft - also used by previews to save before rendering
+    is_draft = models.BooleanField(default=False)
+
     class Meta:
         ordering = ["-timestamp"]
 
diff --git a/hypha/apply/funds/models/applications.py b/hypha/apply/funds/models/applications.py
index 951efbabf..2fa80e184 100644
--- a/hypha/apply/funds/models/applications.py
+++ b/hypha/apply/funds/models/applications.py
@@ -484,6 +484,9 @@ class RoundBase(WorkflowStreamForm, SubmittableStreamForm):  # type: ignore
         return form_class(*args, **form_params)
 
     def serve(self, request, *args, **kwargs):
+        # NOTE: `is_preview` is referring to the Wagtail admin preview
+        # functionality, while `preview` refers to the applicant rendering
+        # a preview of their application.
         if hasattr(request, "is_preview") or hasattr(request, "show_round"):
             # Overriding serve method to pass submission id to get_form method
             copy_open_submission = request.GET.get("open_call_submission")
@@ -500,13 +503,11 @@ class RoundBase(WorkflowStreamForm, SubmittableStreamForm):  # type: ignore
 
                 if form.is_valid():
                     form_submission = self.process_form_submission(form, draft=draft)
-                    # Required for django-file-form: delete temporary files for the new files
-                    # that are uploaded.
-                    form.delete_temporary_files()
 
-                    # If a preview is specified in form submission, render the applicant's answers rather than the landing page.
-                    # At the moment ALL previews are drafted first and then shown
-                    if preview and draft:
+                    # If a preview is specified in form submission, render the
+                    # applicant's answers rather than the landing page.
+                    # Previews are drafted first and then shown
+                    if preview:
                         context = self.get_context(request)
                         context["object"] = form_submission
                         context["form"] = form
@@ -514,6 +515,10 @@ class RoundBase(WorkflowStreamForm, SubmittableStreamForm):  # type: ignore
                             request, "funds/application_preview.html", context
                         )
 
+                    # Required for django-file-form: delete temporary files for the new files
+                    # that are uploaded.
+                    form.delete_temporary_files()
+
                     return self.render_landing_page(
                         request, form_submission, *args, **kwargs
                     )
@@ -653,14 +658,18 @@ class LabBase(EmailForm, WorkflowStreamForm, SubmittableStreamForm):  # type: ig
                     self, form, draft=draft
                 )
 
-                # If a preview is specified in form submission, render the applicant's answers rather than the landing page.
-                # At the moment ALL previews are drafted first and then shown
-                if preview and draft:
+                # If a preview is specified in form submission, render the
+                # applicant's answers rather than the landing page.
+                if preview:
                     context = self.get_context(request)
                     context["object"] = form_submission
                     context["form"] = form
                     return render(request, "funds/application_preview.html", context)
 
+                # Required for django-file-form: delete temporary files for the new files
+                # that are uploaded.
+                form.delete_temporary_files()
+
                 return self.render_landing_page(
                     request, form_submission, *args, **kwargs
                 )
diff --git a/hypha/apply/funds/models/submissions.py b/hypha/apply/funds/models/submissions.py
index 06cda0d7d..bb31213dd 100644
--- a/hypha/apply/funds/models/submissions.py
+++ b/hypha/apply/funds/models/submissions.py
@@ -1,11 +1,12 @@
 import json
 import operator
 from functools import partialmethod, reduce
+from typing import Optional, Self
 
 from django.apps import apps
 from django.conf import settings
 from django.contrib.auth import get_user_model
-from django.contrib.auth.models import Group
+from django.contrib.auth.models import AbstractBaseUser, AnonymousUser, Group
 from django.contrib.contenttypes.fields import GenericRelation
 from django.contrib.postgres.indexes import GinIndex
 from django.contrib.postgres.search import SearchVector, SearchVectorField
@@ -472,8 +473,6 @@ class ApplicationSubmission(
         verbose_name=_("submit time"), auto_now_add=False
     )
 
-    _is_draft = False
-
     live_revision = models.OneToOneField(
         "ApplicationRevision",
         on_delete=models.CASCADE,
@@ -626,20 +625,40 @@ class ApplicationSubmission(
         submission_in_db.next = self
         submission_in_db.save()
 
-    def new_data(self, data):
-        self._is_draft = False
-        self.form_data = data
-        return self
+    def from_draft(self) -> Self:
+        """Sets current `form_data` to the `form_data` from the draft revision.
 
-    def from_draft(self):
-        self._is_draft = True
+        Returns:
+            Self with the `form_data` attribute updated.
+        """
         self.form_data = self.deserialised_data(
             self, self.draft_revision.form_data, self.form_fields
         )
+
         return self
 
-    def create_revision(self, draft=False, force=False, by=None, **kwargs):
-        # Will return True/False if the revision was created or not
+    # TODO: It would be nice to extract this to a services.py and potentially break this into smaller, more logical functions.
+    def create_revision(
+        self,
+        draft=False,
+        force=False,
+        by: Optional[AnonymousUser | AbstractBaseUser] = None,
+        **kwargs,
+    ) -> Optional[models.Model]:
+        """Create a new revision on the submission
+
+        This is used to save drafts, track changes when an RFI is made and
+        save changes before rendering a preview
+
+        Args:
+            draft: if the revision is a draft
+            force: force a revision even if form data is the same
+            by: the author of the revision
+            preview: if the revision is being used to save befor a preview
+
+        Returns:
+            Returns the [`ApplicationRevision`][hypha.apply.funds.models.ApplicationRevision] if it was created, otherwise returns `None`
+        """
         ApplicationRevision = apps.get_model("funds", "ApplicationRevision")
         self.clean_submission()
         current_submission = ApplicationSubmission.objects.get(id=self.id)
@@ -647,7 +666,10 @@ class ApplicationSubmission(
         if current_data != self.form_data or force:
             if self.live_revision == self.draft_revision:
                 revision = ApplicationRevision.objects.create(
-                    submission=self, form_data=self.form_data, author=by
+                    submission=self,
+                    form_data=self.form_data,
+                    author=by,
+                    is_draft=draft,
                 )
             else:
                 revision = self.draft_revision
@@ -658,6 +680,10 @@ class ApplicationSubmission(
             if draft:
                 self.form_data = current_submission.form_data
             else:
+                # Move the revision state out of draft as it is being submitted
+                if revision.is_draft:
+                    revision.is_draft = False
+                    revision.save()
                 self.live_revision = revision
                 self.search_data = " ".join(list(self.prepare_search_values()))
                 self.search_document = self.prepare_search_vector()
@@ -665,6 +691,22 @@ class ApplicationSubmission(
             self.draft_revision = revision
             self.save(skip_custom=True)
             return revision
+        else:
+            revision = self.draft_revision
+
+            # Utilized when the user has previously saved a draft,
+            # then doesn't edit the draft but submits it straight
+            # from the edit view
+            if not draft and revision.is_draft:
+                revision.is_draft = False
+                revision.save()
+                self.live_revision = revision
+                self.search_data = " ".join(list(self.prepare_search_values()))
+                self.search_document = self.prepare_search_vector()
+                self.save(skip_custom=True)
+
+                return revision
+
         return None
 
     def clean_submission(self):
@@ -691,9 +733,6 @@ class ApplicationSubmission(
         elif skip_custom:
             return super().save(*args, **kwargs)
 
-        if self._is_draft:
-            raise ValueError("Cannot save with draft data")
-
         creating = not self.id
 
         if creating:
@@ -715,6 +754,7 @@ class ApplicationSubmission(
 
         super().save(*args, **kwargs)
 
+        # TODO: This functionality should be extracted and moved to a seperate function, too hidden here
         if creating:
             AssignedReviewers = apps.get_model("funds", "AssignedReviewers")
             ApplicationRevision = apps.get_model("funds", "ApplicationRevision")
@@ -724,10 +764,12 @@ class ApplicationSubmission(
                 list(self.get_from_parent("reviewers").all()),
                 self,
             )
+            # TODO: This functionality should be implemented into `ApplicationSubmission.create_revision`
             first_revision = ApplicationRevision.objects.create(
                 submission=self,
                 form_data=self.form_data,
                 author=self.user,
+                is_draft=self.is_draft,
             )
             self.live_revision = first_revision
             self.draft_revision = first_revision
diff --git a/hypha/apply/funds/templates/funds/application_base.html b/hypha/apply/funds/templates/funds/application_base.html
index d158f3698..073666dec 100644
--- a/hypha/apply/funds/templates/funds/application_base.html
+++ b/hypha/apply/funds/templates/funds/application_base.html
@@ -75,8 +75,7 @@
                             <button class="button button--submit button--primary" type="submit" disabled>{% trans "Submit for review" %}</button>
                         {% endif %}
                         <button class="button button--submit button--white" type="submit" name="draft" value="Save draft" formnovalidate>{% trans "Save draft" %}</button>
-                        {# TODO Fix preview bugs before reactivating. #}
-                        {% if False and not require_preview and request.user.is_authenticated %}
+                        {% if not require_preview and request.user.is_authenticated %}
                             <button class="button button--submit button--white" type="submit" name="preview">{% trans "Preview" %}</button>
                         {% endif %}
                     </div>
diff --git a/hypha/apply/funds/templates/funds/application_preview.html b/hypha/apply/funds/templates/funds/application_preview.html
index 932cdac75..00c300630 100644
--- a/hypha/apply/funds/templates/funds/application_preview.html
+++ b/hypha/apply/funds/templates/funds/application_preview.html
@@ -12,10 +12,11 @@
     <div class="wrapper wrapper--medium wrapper--form">
         {% include "funds/includes/rendered_answers.html" %}
 
-        <form id="preview-form-submit" class="form application-form" action="{% url 'funds:submissions:edit' object.id %}" method="POST" enctype="multipart/form-data">
+        <form id="preview-form-submit" class="form application-form" action="{% url 'funds:submissions:edit' object.id %}" method="POST" enctype="multipart/form-data" novalidate>
             {% csrf_token %}
 
-            <div class="preview-hidden-form" hidden>
+            {# Hidden form fields to allow for POSTing to funds:submissions:edit on submit/edit #}
+            <div hidden>
                 {% for field in form %}
                     {% if field.field %}
                         {% if field.field.multi_input_field %}
@@ -27,8 +28,12 @@
                         {{ field.block }}
                     {% endif %}
                 {% endfor %}
+
+                {# Hidden fields needed e.g. for django-file-form. See `StreamBaseForm.hidden_fields` #}
+                {% for hidden_field in form.hidden_fields %}
+                    {{ hidden_field }}
+                {% endfor %}
             </div>
-            <!-- <button class="button button--primary" name="submit" type="submit">{% trans "Submit for review" %}</button> -->
         </form>
 
         <form id="preview-form-edit" class="form application-form" action="{% url 'funds:submissions:edit' object.id %}">
diff --git a/hypha/apply/funds/templates/funds/applicationrevision_list.html b/hypha/apply/funds/templates/funds/applicationrevision_list.html
index ca769513d..437b118a4 100644
--- a/hypha/apply/funds/templates/funds/applicationrevision_list.html
+++ b/hypha/apply/funds/templates/funds/applicationrevision_list.html
@@ -18,6 +18,9 @@
                         {% if forloop.first %}
                             <span class="revision__current">- {% trans "current" %}</span>
                         {% endif %}
+                        {% if revision.is_draft %}
+                            <span class="revision__draft">(<span class="text-red-600">{% trans "draft" %}</span>)</span
+                        {% endif %}
                     </p>
                     {% if not forloop.first %}
                         <a class="button button--compare" href="{{ revision.get_compare_url_to_latest }}">{% trans "Compare" %}</a>
diff --git a/hypha/apply/funds/tests/test_models.py b/hypha/apply/funds/tests/test_models.py
index f1a518902..e859bcb28 100644
--- a/hypha/apply/funds/tests/test_models.py
+++ b/hypha/apply/funds/tests/test_models.py
@@ -505,10 +505,6 @@ class TestApplicationSubmission(TestCase):
         draft_submission = submission.from_draft()
         self.assertDictEqual(draft_submission.form_data, submission.form_data)
         self.assertEqual(draft_submission.title, title)
-        self.assertTrue(draft_submission._is_draft, True)
-
-        with self.assertRaises(ValueError):
-            draft_submission.save()
 
         submission = self.refresh(submission)
         self.assertNotEqual(submission.title, title)
diff --git a/hypha/apply/funds/tests/test_views.py b/hypha/apply/funds/tests/test_views.py
index 75a397815..d5ac5d778 100644
--- a/hypha/apply/funds/tests/test_views.py
+++ b/hypha/apply/funds/tests/test_views.py
@@ -6,7 +6,7 @@ from django.conf import settings
 from django.contrib.auth.models import AnonymousUser
 from django.core.exceptions import PermissionDenied
 from django.http import Http404
-from django.test import RequestFactory, TestCase
+from django.test import RequestFactory, TestCase, override_settings
 from django.urls import reverse
 from django.utils import timezone
 
@@ -581,7 +581,7 @@ class TestStaffSubmissionView(BaseSubmissionViewTestCase):
         )
         assert_view_determination_not_displayed(submission)
 
-    def test_cant_see_application_draft_status(self):
+    def test_staff_cant_see_application_draft_status(self):
         factory = RequestFactory()
         submission = ApplicationSubmissionFactory(status="draft")
         ProjectFactory(submission=submission)
@@ -592,6 +592,18 @@ class TestStaffSubmissionView(BaseSubmissionViewTestCase):
         with self.assertRaises(Http404):
             SubmissionDetailView.as_view()(request, pk=submission.pk)
 
+    @override_settings(SUBMISSIONS_DRAFT_ACCESS_STAFF=True)
+    def test_staff_can_see_application_draft_status(self):
+        factory = RequestFactory()
+        submission = ApplicationSubmissionFactory(status="draft")
+        ProjectFactory(submission=submission)
+
+        request = factory.get(f"/submission/{submission.pk}")
+        request.user = StaffFactory()
+
+        response = SubmissionDetailView.as_view()(request, pk=submission.pk)
+        self.assertEqual(response.status_code, 200)
+
     def test_applicant_can_see_application_draft_status(self):
         factory = RequestFactory()
         user = ApplicantFactory()
diff --git a/hypha/apply/funds/views.py b/hypha/apply/funds/views.py
index 81a2cef5c..c98626664 100644
--- a/hypha/apply/funds/views.py
+++ b/hypha/apply/funds/views.py
@@ -2,6 +2,7 @@ import csv
 from copy import copy
 from datetime import timedelta
 from io import StringIO
+from typing import Generator, Tuple
 
 import django_tables2 as tables
 from django.conf import settings
@@ -13,7 +14,14 @@ from django.contrib.auth.models import Group
 from django.contrib.humanize.templatetags.humanize import intcomma
 from django.core.exceptions import PermissionDenied
 from django.db.models import Count, F, Q
-from django.http import FileResponse, Http404, HttpResponse, HttpResponseRedirect
+from django.forms import BaseModelForm
+from django.http import (
+    FileResponse,
+    Http404,
+    HttpRequest,
+    HttpResponse,
+    HttpResponseRedirect,
+)
 from django.shortcuts import get_object_or_404, render
 from django.urls import reverse_lazy
 from django.utils import timezone
@@ -1297,6 +1305,39 @@ class BaseSubmissionEditView(UpdateView):
 
     model = ApplicationSubmission
 
+    @property
+    def transitions(self):
+        transitions = self.object.get_available_user_status_transitions(
+            self.request.user
+        )
+        return {transition.name: transition for transition in transitions}
+
+    def render_preview(self, request: HttpRequest, form: BaseModelForm) -> HttpResponse:
+        """Gets a rendered preview of a form
+
+        Creates a new revision on the `ApplicationSubmission`, removes the
+        forms temporary files
+
+        Args:
+            request:
+                Request used to trigger the preview to be used in the render
+            form:
+                Form to be rendered
+
+        Returns:
+            An `HttpResponse` containing a preview of the given form
+        """
+
+        self.object.create_revision(draft=True, by=request.user)
+        messages.success(self.request, _("Draft saved"))
+
+        # Required for django-file-form: delete temporary files for the new files
+        # uploaded while edit.
+        form.delete_temporary_files()
+
+        context = self.get_context_data()
+        return render(request, "funds/application_preview.html", context)
+
     def dispatch(self, request, *args, **kwargs):
         permission, _ = has_permission(
             "submission_edit",
@@ -1308,15 +1349,99 @@ class BaseSubmissionEditView(UpdateView):
             raise PermissionDenied
         return super().dispatch(request, *args, **kwargs)
 
-    def buttons(self):
+    def buttons(
+        self,
+    ) -> Generator[Tuple[str, str, str], Tuple[str, str, str], Tuple[str, str, str]]:
+        """The buttons to be presented to the in the EditView
+
+        Returns:
+            A generator returning a tuple strings in the format of:
+            (<button type>, <button styling>, <button label>)
+        """
         if settings.SUBMISSION_PREVIEW_REQUIRED:
             yield ("preview", "primary", _("Preview and submit"))
             yield ("save", "white", _("Save draft"))
         else:
             yield ("submit", "primary", _("Submit"))
             yield ("save", "white", _("Save draft"))
-            # TODO Fix preview bugs before reactivating.
-            # yield ("preview", "white", _("Preview"))
+            yield ("preview", "white", _("Preview"))
+
+    def get_object_fund_current_round(self):
+        assigned_fund = self.object.round.get_parent().specific
+        if assigned_fund.open_round:
+            return assigned_fund.open_round
+        return False
+
+    def form_valid(self, form: BaseModelForm) -> HttpResponse:
+        """Handle the form returned from a `SubmissionEditView`.
+
+        Determine whether to return a form preview, draft the new edits,
+        or submit and transition the `ApplicationSubmission` object
+
+        Args:
+            form: The valid form
+
+        Returns:
+            An `HttpResponse` depending on the actions taken in the edit view
+        """
+
+        self.object.form_data = form.cleaned_data
+
+        is_draft = self.object.status == DRAFT_STATE
+
+        # Handle a preview or a save (aka a draft)
+        if "preview" in self.request.POST:
+            return self.render_preview(self.request, form)
+
+        if "save" in self.request.POST:
+            return self.save_draft_and_refresh_page(form=form)
+
+        # Handle an application being submitted from a DRAFT_STATE. This includes updating submit_time
+        if is_draft and "submit" in self.request.POST:
+            self.object.submit_time = timezone.now()
+            if self.object.round:
+                current_round = self.get_object_fund_current_round()
+                if current_round:
+                    self.object.round = current_round
+            self.object.save(update_fields=["submit_time", "round"])
+
+        revision = self.object.create_revision(by=self.request.user)
+        submitting_proposal = self.object.phase.name in STAGE_CHANGE_ACTIONS
+
+        if submitting_proposal:
+            messenger(
+                MESSAGES.PROPOSAL_SUBMITTED,
+                request=self.request,
+                user=self.request.user,
+                source=self.object,
+            )
+        elif revision and not self.object.status == DRAFT_STATE:
+            messenger(
+                MESSAGES.APPLICANT_EDIT,
+                request=self.request,
+                user=self.request.user,
+                source=self.object,
+                related=revision,
+            )
+
+        action = set(self.request.POST.keys()) & set(self.transitions.keys())
+        try:
+            transition = self.transitions[action.pop()]
+        except KeyError:
+            pass
+        else:
+            self.object.perform_transition(
+                transition.target,
+                self.request.user,
+                request=self.request,
+                notify=not (revision or submitting_proposal)
+                or self.object.status == DRAFT_STATE,  # Use the other notification
+            )
+
+        # Required for django-file-form: delete temporary files for the new files
+        # uploaded while edit.
+        form.delete_temporary_files()
+        return HttpResponseRedirect(self.get_success_url())
 
     def get_form_kwargs(self):
         """
@@ -1393,27 +1518,20 @@ class BaseSubmissionEditView(UpdateView):
 
 @method_decorator(staff_required, name="dispatch")
 class AdminSubmissionEditView(BaseSubmissionEditView):
-    def form_valid(self, form):
-        self.object.new_data(form.cleaned_data)
+    def buttons(
+        self,
+    ) -> Generator[Tuple[str, str, str], Tuple[str, str, str], Tuple[str, str, str]]:
+        """The buttons to be presented in the `AdminSubmissionEditView`
 
-        if "save" in self.request.POST:
-            return self.save_draft_and_refresh_page(form=form)
+        Admins shouldn't be required to preview, but should have the option.
 
-        if "submit" in self.request.POST:
-            revision = self.object.create_revision(by=self.request.user)
-            if revision:
-                messenger(
-                    MESSAGES.EDIT_SUBMISSION,
-                    request=self.request,
-                    user=self.request.user,
-                    source=self.object,
-                    related=revision,
-                )
-
-        # Required for django-file-form: delete temporary files for the new files
-        # uploaded while edit.
-        form.delete_temporary_files()
-        return HttpResponseRedirect(self.get_success_url())
+        Returns:
+            A generator returning a tuple strings in the format of:
+            (<button type>, <button styling>, <button label>)
+        """
+        yield ("submit", "primary", _("Submit"))
+        yield ("save", "white", _("Save draft"))
+        yield ("preview", "white", _("Preview"))
 
 
 @method_decorator(login_required, name="dispatch")
@@ -1424,79 +1542,6 @@ class ApplicantSubmissionEditView(BaseSubmissionEditView):
             raise PermissionDenied
         return super().dispatch(request, *args, **kwargs)
 
-    @property
-    def transitions(self):
-        transitions = self.object.get_available_user_status_transitions(
-            self.request.user
-        )
-        return {transition.name: transition for transition in transitions}
-
-    def get_object_fund_current_round(self):
-        assigned_fund = self.object.round.get_parent().specific
-        if assigned_fund.open_round:
-            return assigned_fund.open_round
-        return False
-
-    def form_valid(self, form):
-        self.object.new_data(form.cleaned_data)
-
-        # Update submit_time only when application is getting submitted from the Draft State for the first time.
-        if self.object.status == DRAFT_STATE and "submit" in self.request.POST:
-            self.object.submit_time = timezone.now()
-            if self.object.round:
-                current_round = self.get_object_fund_current_round()
-                if current_round:
-                    self.object.round = current_round
-            self.object.save(update_fields=["submit_time", "round"])
-
-        if self.object.status == DRAFT_STATE and "preview" in self.request.POST:
-            self.object.create_revision(draft=True, by=self.request.user)
-            form.delete_temporary_files()
-            # messages.success(self.request, _("Draft saved"))
-            context = self.get_context_data()
-            return render(self.request, "funds/application_preview.html", context)
-
-        if "save" in self.request.POST:
-            return self.save_draft_and_refresh_page(form=form)
-
-        revision = self.object.create_revision(by=self.request.user)
-        submitting_proposal = self.object.phase.name in STAGE_CHANGE_ACTIONS
-
-        if submitting_proposal:
-            messenger(
-                MESSAGES.PROPOSAL_SUBMITTED,
-                request=self.request,
-                user=self.request.user,
-                source=self.object,
-            )
-        elif revision and not self.object.status == DRAFT_STATE:
-            messenger(
-                MESSAGES.APPLICANT_EDIT,
-                request=self.request,
-                user=self.request.user,
-                source=self.object,
-                related=revision,
-            )
-
-        action = set(self.request.POST.keys()) & set(self.transitions.keys())
-        try:
-            transition = self.transitions[action.pop()]
-        except KeyError:
-            pass
-        else:
-            self.object.perform_transition(
-                transition.target,
-                self.request.user,
-                request=self.request,
-                notify=not (revision or submitting_proposal)
-                or self.object.status == DRAFT_STATE,  # Use the other notification
-            )
-
-        # Required for django-file-form: delete temporary files for the new files
-        # uploaded while edit.
-        form.delete_temporary_files()
-        return HttpResponseRedirect(self.get_success_url())
-
 
 @method_decorator(login_required, name="dispatch")
 class PartnerSubmissionEditView(ApplicantSubmissionEditView):
@@ -1524,15 +1569,31 @@ class RevisionListView(ListView):
     model = ApplicationRevision
 
     def get_queryset(self):
+        """Get a queryset of all valid `ApplicationRevision`s that can be
+        compared for the current submission
+
+        This excludes draft & preview revisions unless draft(s) are the only
+        existing revisions, in which the last draft will be returned in a QuerySet
+
+        Returns:
+            An [`ApplicationRevision`][hypha.apply.funds.models.ApplicationRevision] QuerySet
+        """
         self.submission = get_object_or_404(
             ApplicationSubmission, id=self.kwargs["submission_pk"]
         )
-        self.queryset = self.model.objects.filter(
-            submission=self.submission,
-        ).exclude(
-            draft__isnull=False,
-            live__isnull=True,
+        revisions = self.model.objects.filter(submission=self.submission).exclude(
+            draft__isnull=False, live__isnull=True
         )
+
+        filtered_revisions = revisions.filter(is_draft=False)
+
+        # An edge case for when an instance has `SUBMISSIONS_DRAFT_ACCESS_STAFF=True`
+        # and a staff member tries to view the revisions of the draft.
+        if len(filtered_revisions) < 1:
+            self.queryset = self.model.objects.filter(id=revisions.last().id)
+        else:
+            self.queryset = filtered_revisions
+
         return super().get_queryset()
 
     def get_context_data(self, **kwargs):
diff --git a/hypha/settings/base.py b/hypha/settings/base.py
index 908b37b78..ff0c798c3 100644
--- a/hypha/settings/base.py
+++ b/hypha/settings/base.py
@@ -148,8 +148,7 @@ TRANSITION_AFTER_REVIEWS = env.bool("TRANSITION_AFTER_REVIEWS", False)
 REVIEW_VISIBILITY_DEFAULT = env.str("REVIEW_VISIBILITY_DEFAULT", "private")
 
 # Require an applicant to view their rendered application before submitting
-# TODO Fix preview bugs before setting this to True as default.
-SUBMISSION_PREVIEW_REQUIRED = env.bool("SUBMISSION_PREVIEW_REQUIRED", False)
+SUBMISSION_PREVIEW_REQUIRED = env.bool("SUBMISSION_PREVIEW_REQUIRED", True)
 
 # Project settings.
 
diff --git a/hypha/static_src/sass/components/_revision.scss b/hypha/static_src/sass/components/_revision.scss
index 9a0682b09..d675828ee 100644
--- a/hypha/static_src/sass/components/_revision.scss
+++ b/hypha/static_src/sass/components/_revision.scss
@@ -32,7 +32,8 @@
     }
 
     &__date,
-    &__current {
+    &__current,
+    &__draft {
         font-weight: $weight--semibold;
     }
 }
-- 
GitLab