From f2b00690108b4477c3644295d29e4cb36eeef1f5 Mon Sep 17 00:00:00 2001
From: Chris Zubak-Skees <chriszs@gmail.com>
Date: Tue, 22 Feb 2022 03:30:33 -0500
Subject: [PATCH 1/6] feat: add submission withdrawal

Closes #3296
---
 hypha/apply/funds/models/submissions.py       |  3 +-
 ...pplicationsubmission_confirm_withdraw.html | 23 ++++++++++
 .../funds/applicationsubmission_detail.html   |  7 +++
 hypha/apply/funds/urls.py                     |  2 +
 hypha/apply/funds/views.py                    | 33 +++++++++++++-
 hypha/apply/funds/workflow.py                 | 43 ++++++++++++++++++-
 .../src/sass/apply/custom/_custom.scss        |  7 ++-
 7 files changed, 114 insertions(+), 4 deletions(-)
 create mode 100644 hypha/apply/funds/templates/funds/applicationsubmission_confirm_withdraw.html

diff --git a/hypha/apply/funds/models/submissions.py b/hypha/apply/funds/models/submissions.py
index 50e61b63bc..1ccbee128e 100644
--- a/hypha/apply/funds/models/submissions.py
+++ b/hypha/apply/funds/models/submissions.py
@@ -829,7 +829,8 @@ def in_external_review_phase(self):
     def is_finished(self):
         accepted = self.status in PHASES_MAPPING['accepted']['statuses']
         dismissed = self.status in PHASES_MAPPING['dismissed']['statuses']
-        return accepted or dismissed
+        withdrawn = self.status in PHASES_MAPPING['withdrawn']['statuses']
+        return accepted or dismissed or withdrawn
 
     # Methods for accessing data on the submission
 
diff --git a/hypha/apply/funds/templates/funds/applicationsubmission_confirm_withdraw.html b/hypha/apply/funds/templates/funds/applicationsubmission_confirm_withdraw.html
new file mode 100644
index 0000000000..d0830b01b6
--- /dev/null
+++ b/hypha/apply/funds/templates/funds/applicationsubmission_confirm_withdraw.html
@@ -0,0 +1,23 @@
+{% extends "base-apply.html" %}
+{% load i18n static %}
+
+{% block title %}{% trans "Withdrawing" %}: {{object.title }}{% endblock %}
+
+{% block content %}
+<div class="admin-bar">
+    <div class="admin-bar__inner">
+        <h2 class="heading heading--no-margin">{% trans "Withdrawing" %}: {{ object.title }}</h2>
+    </div>
+</div>
+
+<div class="wrapper wrapper--light-grey-bg wrapper--form wrapper--sidebar">
+    <div class="wrapper--sidebar--inner">
+        <form class="form" action="" method="post">
+            {% csrf_token %}
+            <p><strong>{% blocktrans %}Are you sure you want to withdraw "{{ object }}" from consideration?{% endblocktrans %}</strong></p>
+            <button class="button button--warning button--submit button--top-space" type="submit">{% trans "Confirm" %}</button>
+        </form>
+    </div>
+</div>
+
+{% endblock %}
diff --git a/hypha/apply/funds/templates/funds/applicationsubmission_detail.html b/hypha/apply/funds/templates/funds/applicationsubmission_detail.html
index da587109ea..cabd6fe5b8 100644
--- a/hypha/apply/funds/templates/funds/applicationsubmission_detail.html
+++ b/hypha/apply/funds/templates/funds/applicationsubmission_detail.html
@@ -102,6 +102,13 @@ <h5>{% blocktrans with stage=object.previous.stage %}Your {{ stage }} applicatio
                                     <svg class="icon icon--delete"><use xlink:href="#delete"></use></svg>
                                 </a>
                             {% endif %}
+                            {% if request.user|has_edit_perm:object %}
+                                {% if request.user.is_applicant %}
+                                <a class="link link--withdraw-submission is-active" href="{% url 'funds:submissions:withdraw' object.id %}">
+                                    {% trans "Withdraw" %}
+                                </a>
+                                {% endif %}
+                            {% endif %}
                             {% if request.user|has_edit_perm:object %}
                                 <a class="link link--edit-submission is-active" href="{% url 'funds:submissions:edit' object.id %}">
                                     {% trans "Edit" %}
diff --git a/hypha/apply/funds/urls.py b/hypha/apply/funds/urls.py
index d241b31757..d8c50948dc 100644
--- a/hypha/apply/funds/urls.py
+++ b/hypha/apply/funds/urls.py
@@ -27,6 +27,7 @@
     SubmissionSealedView,
     SubmissionStaffFlaggedView,
     SubmissionUserFlaggedView,
+    SubmissionWithdrawView,
 )
 from .views_beta import (
     bulk_archive_submissions,
@@ -101,6 +102,7 @@
         path('simplified/', SubmissionDetailSimplifiedView.as_view(), name="simplified"),
         path('download/', SubmissionDetailPDFView.as_view(), name="download"),
         path('delete/', SubmissionDeleteView.as_view(), name="delete"),
+        path('withdraw/', SubmissionWithdrawView.as_view(), name="withdraw"),
         path(
             'documents/<uuid:field_id>/<str:file_name>',
             SubmissionPrivateMediaView.as_view(), name='serve_private_media'
diff --git a/hypha/apply/funds/views.py b/hypha/apply/funds/views.py
index 5dfcb0aa7c..c30e4e9afe 100644
--- a/hypha/apply/funds/views.py
+++ b/hypha/apply/funds/views.py
@@ -30,7 +30,11 @@
     UpdateView,
 )
 from django.views.generic.base import TemplateView
-from django.views.generic.detail import SingleObjectMixin
+from django.views.generic.detail import (
+    BaseDetailView,
+    SingleObjectMixin,
+    SingleObjectTemplateResponseMixin,
+)
 from django_file_form.models import PlaceholderUploadedFile
 from django_filters.views import FilterView
 from django_tables2.paginators import LazyPaginator
@@ -1414,6 +1418,33 @@ def delete(self, request, *args, **kwargs):
         response = super().delete(request, *args, **kwargs)
         return response
 
+class SubmissionWithdrawView(SingleObjectTemplateResponseMixin, BaseDetailView):
+    model = ApplicationSubmission
+    success_url = reverse_lazy('funds:submissions:list')
+    template_name_suffix = '_confirm_withdraw'
+
+    def post(self, request, *args, **kwargs):
+        return self.withdraw(request, *args, **kwargs)
+
+    def withdraw(self, request, *args, **kwargs):
+        obj = self.get_object()
+
+        if not obj.phase.permissions.can_edit(request.user):
+            raise PermissionDenied
+
+        withdraw_actions = [action for action in obj.workflow[obj.status].transitions.keys() if 'withdraw' in action]
+
+        if len(withdraw_actions) > 0:
+            action = withdraw_actions[0]
+            obj.perform_transition(
+                action,
+                self.request.user,
+                request=self.request,
+                notify=False
+            )
+
+        success_url = obj.get_absolute_url()
+        return HttpResponseRedirect(success_url)
 
 @method_decorator(login_required, name='dispatch')
 class SubmissionPrivateMediaView(UserPassesTestMixin, PrivateMediaView):
diff --git a/hypha/apply/funds/workflow.py b/hypha/apply/funds/workflow.py
index 9eaa707c6b..8aa372d749 100644
--- a/hypha/apply/funds/workflow.py
+++ b/hypha/apply/funds/workflow.py
@@ -358,6 +358,7 @@ def make_permissions(edit=None, review=None, view=None):
                 'ext_internal_review': _('Open Review'),
                 'ext_determination': _('Ready For Determination'),
                 'ext_rejected': _('Dismiss'),
+                'ext_screening_withdrawn': _('Withdraw'),
             },
             'display': _('Need screening'),
             'public': _('Application Received'),
@@ -371,16 +372,23 @@ def make_permissions(edit=None, review=None, view=None):
                     'permissions': {UserPermissions.APPLICANT, UserPermissions.STAFF, UserPermissions.LEAD, UserPermissions.ADMIN},
                     'method': 'create_revision',
                 },
+                'ext_screening_withdrawn': _('Withdraw'),
             },
             'display': _('More information required'),
             'stage': RequestExt,
             'permissions': applicant_edit_permissions,
         },
+        'ext_screening_withdrawn': {
+            'display': _('Withdrawn'),
+            'stage': RequestExt,
+            'permissions': staff_edit_permissions,
+        },
     },
     {
         'ext_internal_review': {
             'transitions': {
                 'ext_post_review_discussion': _('Close Review'),
+                'ext_review_withdrawn': _('Withdraw'),
                 INITIAL_STATE: _('Need screening (revert)'),
             },
             'display': _('Internal Review'),
@@ -409,6 +417,7 @@ def make_permissions(edit=None, review=None, view=None):
                     'permissions': {UserPermissions.APPLICANT, UserPermissions.STAFF, UserPermissions.LEAD, UserPermissions.ADMIN},
                     'method': 'create_revision',
                 },
+                'ext_review_withdrawn': _('Withdraw'),
             },
             'display': _('More information required'),
             'stage': RequestExt,
@@ -425,6 +434,11 @@ def make_permissions(edit=None, review=None, view=None):
             'stage': RequestExt,
             'permissions': reviewer_review_permissions,
         },
+        'ext_review_withdrawn': {
+            'display': _('Withdrawn'),
+            'stage': RequestExt,
+            'permissions': staff_edit_permissions,
+        },
     },
     {
         'ext_post_external_review_discussion': {
@@ -435,6 +449,7 @@ def make_permissions(edit=None, review=None, view=None):
                 'ext_almost': _('Accept but additional info required'),
                 'ext_accepted': _('Accept'),
                 'ext_rejected': _('Dismiss'),
+                'ext_external_review_withdrawn': _('Withdraw'),
             },
             'display': _('Ready For Discussion'),
             'stage': RequestExt,
@@ -447,11 +462,17 @@ def make_permissions(edit=None, review=None, view=None):
                     'permissions': {UserPermissions.APPLICANT, UserPermissions.STAFF, UserPermissions.LEAD, UserPermissions.ADMIN},
                     'method': 'create_revision',
                 },
+                'ext_external_review_withdrawn': _('Withdraw'),
             },
             'display': _('More information required'),
             'stage': RequestExt,
             'permissions': applicant_edit_permissions,
         },
+        'ext_external_review_withdrawn': {
+            'display': _('Withdrawn'),
+            'stage': RequestExt,
+            'permissions': staff_edit_permissions,
+        },
     },
     {
         'ext_determination': {
@@ -460,6 +481,7 @@ def make_permissions(edit=None, review=None, view=None):
                 'ext_almost': _('Accept but additional info required'),
                 'ext_accepted': _('Accept'),
                 'ext_rejected': _('Dismiss'),
+                'ext_withdrawn': _('Withdraw'),
             },
             'display': _('Ready for Determination'),
             'permissions': hidden_from_applicant_permissions,
@@ -485,7 +507,12 @@ def make_permissions(edit=None, review=None, view=None):
         'ext_rejected': {
             'display': _('Dismissed'),
             'stage': RequestExt,
-            'permissions': no_permissions,
+            'permissions': staff_edit_permissions,
+        },
+        'ext_withdrawn': {
+            'display': _('Withdrawn'),
+            'stage': RequestExt,
+            'permissions': staff_edit_permissions,
         },
     },
 ]
@@ -1076,11 +1103,21 @@ def get_dismissed_statuses():
     return dismissed_statuses
 
 
+def get_withdrawn_statuses():
+    withdrawn_statuses = set()
+    for phase_name, phase in PHASES:
+        if phase.display_name == 'Dismissed':
+            withdrawn_statuses.add(phase_name)
+    return withdrawn_statuses
+
+
+ext_or_higher_statuses = get_ext_or_higher_statuses()
 review_statuses = get_review_statuses()
 ext_review_statuses = get_ext_review_statuses()
 ext_or_higher_statuses = get_ext_or_higher_statuses()
 accepted_statuses = get_accepted_statuses()
 dismissed_statuses = get_dismissed_statuses()
+withdrawn_statuses = get_withdrawn_statuses()
 
 DETERMINATION_PHASES = [phase_name for phase_name, _ in PHASES if '_discussion' in phase_name]
 DETERMINATION_RESPONSE_PHASES = [
@@ -1174,6 +1211,10 @@ def phases_matching(phrase, exclude=None):
         'name': _('Dismissed'),
         'statuses': phases_matching('rejected'),
     },
+    'withdrawn': {
+        'name': _('Withdrawn'),
+        'statuses': phases_matching('withdrawn'),
+    },
 }
 
 OPEN_CALL_PHASES = [
diff --git a/hypha/static_src/src/sass/apply/custom/_custom.scss b/hypha/static_src/src/sass/apply/custom/_custom.scss
index 061b3d43d2..cc17f79c15 100644
--- a/hypha/static_src/src/sass/apply/custom/_custom.scss
+++ b/hypha/static_src/src/sass/apply/custom/_custom.scss
@@ -1 +1,6 @@
-// stylelint-disable no-empty-source
+.link--withdraw-submission {
+    margin-right: 1rem;
+    padding-right: 1rem;
+    border-right: 2px solid #cfcfcf;
+    font-weight: 700;
+}

From 6652c78fea67f0a281de31d8aa80d8f6df300954 Mon Sep 17 00:00:00 2001
From: Chris Zubak-Skees <chriszs@opentechstrategies.com>
Date: Tue, 22 Feb 2022 21:30:30 +0000
Subject: [PATCH 2/6] fix: remove refs to dismiss while adding withdrawn

---
 hypha/apply/funds/workflow.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/hypha/apply/funds/workflow.py b/hypha/apply/funds/workflow.py
index 8aa372d749..5bddccf840 100644
--- a/hypha/apply/funds/workflow.py
+++ b/hypha/apply/funds/workflow.py
@@ -1106,7 +1106,7 @@ def get_dismissed_statuses():
 def get_withdrawn_statuses():
     withdrawn_statuses = set()
     for phase_name, phase in PHASES:
-        if phase.display_name == 'Dismissed':
+        if phase.display_name == 'Withdrawn':
             withdrawn_statuses.add(phase_name)
     return withdrawn_statuses
 

From 997f653413566625873415c13cc98efab035d7e9 Mon Sep 17 00:00:00 2001
From: Frank Duncan <frank@kank.net>
Date: Thu, 23 Mar 2023 05:20:20 -0500
Subject: [PATCH 3/6] Add ENABLE_SUBMISSION_WITHDRAWAL to allow enabling
 withdrawals

This only affects about enabling them, not whether they are in the
system.  That means that if the configuration is changed over the
lifetime of a system, things that were withdrawn when it was enabled
retain that status.
---
 docs/setup/administrators/configuration.md | 4 ++++
 hypha/apply/funds/views.py                 | 3 +++
 hypha/apply/funds/workflow.py              | 2 +-
 hypha/core/context_processors.py           | 1 +
 hypha/settings/base.py                     | 3 +++
 5 files changed, 12 insertions(+), 1 deletion(-)

diff --git a/docs/setup/administrators/configuration.md b/docs/setup/administrators/configuration.md
index e52c0b2f00..b890261ace 100644
--- a/docs/setup/administrators/configuration.md
+++ b/docs/setup/administrators/configuration.md
@@ -76,6 +76,10 @@ The corrosponding locale dir is named: en, en_GB, en_US
 
     FORCE_LOGIN_FOR_APPLICATION = env.bool('FORCE_LOGIN_FOR_APPLICATION', False)
 
+### Allow Withdrawing of Submissions
+
+    ENABLE_SUBMISSION_WITHDRAWAL = env.bool('ENABLE_SUBMISSION_WITHDRAWAL', False)
+
 ### Set the allowed file extension for all uploads fields.
 
     FILE_ALLOWED_EXTENSIONS = ['doc', 'docx', 'odp', 'ods', 'odt', 'pdf', 'ppt', 'pptx', 'rtf', 'txt', 'xls', 'xlsx']
diff --git a/hypha/apply/funds/views.py b/hypha/apply/funds/views.py
index c30e4e9afe..c360ce3409 100644
--- a/hypha/apply/funds/views.py
+++ b/hypha/apply/funds/views.py
@@ -1427,6 +1427,9 @@ def post(self, request, *args, **kwargs):
         return self.withdraw(request, *args, **kwargs)
 
     def withdraw(self, request, *args, **kwargs):
+        if not settings.ENABLE_SUBMISSION_WITHDRAWAL:
+            raise PermissionDenied
+
         obj = self.get_object()
 
         if not obj.phase.permissions.can_edit(request.user):
diff --git a/hypha/apply/funds/workflow.py b/hypha/apply/funds/workflow.py
index 5bddccf840..a865ac1400 100644
--- a/hypha/apply/funds/workflow.py
+++ b/hypha/apply/funds/workflow.py
@@ -363,7 +363,7 @@ def make_permissions(edit=None, review=None, view=None):
             'display': _('Need screening'),
             'public': _('Application Received'),
             'stage': RequestExt,
-            'permissions': default_permissions,
+            'permissions': applicant_edit_permissions,
         },
         'ext_more_info': {
             'transitions': {
diff --git a/hypha/core/context_processors.py b/hypha/core/context_processors.py
index 5e41caf971..4282171e2a 100644
--- a/hypha/core/context_processors.py
+++ b/hypha/core/context_processors.py
@@ -18,4 +18,5 @@ def global_vars(request):
         'SENTRY_DENY_URLS': settings.SENTRY_DENY_URLS,
         'SENTRY_DEBUG': settings.SENTRY_DEBUG,
         'SENTRY_PUBLIC_KEY': settings.SENTRY_PUBLIC_KEY,
+        'ENABLE_SUBMISSION_WITHDRAWAL': settings.ENABLE_SUBMISSION_WITHDRAWAL,
     }
diff --git a/hypha/settings/base.py b/hypha/settings/base.py
index b502805ab2..68c40fa9ce 100644
--- a/hypha/settings/base.py
+++ b/hypha/settings/base.py
@@ -107,6 +107,9 @@
 # Enable users to create accounts without submitting an application.
 ENABLE_REGISTRATION_WITHOUT_APPLICATION = env.bool('ENABLE_REGISTRATION_WITHOUT_APPLICATION', False)
 
+# Allow Withdrawing of Submissions
+ENABLE_SUBMISSION_WITHDRAWAL = env.bool('ENABLE_SUBMISSION_WITHDRAWAL', False)
+
 # Project settings.
 
 # SECRET_KEY is required

From 14758f25714b742fff564c132c6a5968f5abaf6d Mon Sep 17 00:00:00 2001
From: Frank Duncan <frank@kank.net>
Date: Sat, 15 Apr 2023 09:02:09 -0500
Subject: [PATCH 4/6] Change permissions to withdraw to just being an applicant

Based on Review in #3298, the decision was made that an applicant can
withdraw at any time, without edit permissions, and edit permissions
should not be to applicants for their own submissions.
---
 .../funds/templates/funds/applicationsubmission_detail.html   | 4 +---
 hypha/apply/funds/views.py                                    | 2 +-
 hypha/apply/funds/workflow.py                                 | 2 +-
 hypha/static_src/src/sass/apply/custom/_custom.scss           | 1 -
 4 files changed, 3 insertions(+), 6 deletions(-)

diff --git a/hypha/apply/funds/templates/funds/applicationsubmission_detail.html b/hypha/apply/funds/templates/funds/applicationsubmission_detail.html
index cabd6fe5b8..eb41290caf 100644
--- a/hypha/apply/funds/templates/funds/applicationsubmission_detail.html
+++ b/hypha/apply/funds/templates/funds/applicationsubmission_detail.html
@@ -102,12 +102,10 @@ <h5>{% blocktrans with stage=object.previous.stage %}Your {{ stage }} applicatio
                                     <svg class="icon icon--delete"><use xlink:href="#delete"></use></svg>
                                 </a>
                             {% endif %}
-                            {% if request.user|has_edit_perm:object %}
-                                {% if request.user.is_applicant %}
+                            {% if ENABLE_SUBMISSION_WITHDRAWAL and request.user.is_applicant %}
                                 <a class="link link--withdraw-submission is-active" href="{% url 'funds:submissions:withdraw' object.id %}">
                                     {% trans "Withdraw" %}
                                 </a>
-                                {% endif %}
                             {% endif %}
                             {% if request.user|has_edit_perm:object %}
                                 <a class="link link--edit-submission is-active" href="{% url 'funds:submissions:edit' object.id %}">
diff --git a/hypha/apply/funds/views.py b/hypha/apply/funds/views.py
index c360ce3409..737eea50bb 100644
--- a/hypha/apply/funds/views.py
+++ b/hypha/apply/funds/views.py
@@ -1432,7 +1432,7 @@ def withdraw(self, request, *args, **kwargs):
 
         obj = self.get_object()
 
-        if not obj.phase.permissions.can_edit(request.user):
+        if not request.user.is_applicant:
             raise PermissionDenied
 
         withdraw_actions = [action for action in obj.workflow[obj.status].transitions.keys() if 'withdraw' in action]
diff --git a/hypha/apply/funds/workflow.py b/hypha/apply/funds/workflow.py
index a865ac1400..5bddccf840 100644
--- a/hypha/apply/funds/workflow.py
+++ b/hypha/apply/funds/workflow.py
@@ -363,7 +363,7 @@ def make_permissions(edit=None, review=None, view=None):
             'display': _('Need screening'),
             'public': _('Application Received'),
             'stage': RequestExt,
-            'permissions': applicant_edit_permissions,
+            'permissions': default_permissions,
         },
         'ext_more_info': {
             'transitions': {
diff --git a/hypha/static_src/src/sass/apply/custom/_custom.scss b/hypha/static_src/src/sass/apply/custom/_custom.scss
index cc17f79c15..55e678b65a 100644
--- a/hypha/static_src/src/sass/apply/custom/_custom.scss
+++ b/hypha/static_src/src/sass/apply/custom/_custom.scss
@@ -1,6 +1,5 @@
 .link--withdraw-submission {
     margin-right: 1rem;
     padding-right: 1rem;
-    border-right: 2px solid #cfcfcf;
     font-weight: 700;
 }

From 5d0405eef7426c5dc6f6715973232826fdb1181e Mon Sep 17 00:00:00 2001
From: Frank Duncan <frank@kank.net>
Date: Sat, 15 Apr 2023 09:04:03 -0500
Subject: [PATCH 5/6] Add withdraw to all workflows, move to end of workflow

This creates a single withdrawn status for each workflow, living at the
end (next to accepted/rejected), and adds transistions for all the
different workflow types.
---
 hypha/apply/funds/workflow.py | 81 ++++++++++++++++++++++++++---------
 1 file changed, 60 insertions(+), 21 deletions(-)

diff --git a/hypha/apply/funds/workflow.py b/hypha/apply/funds/workflow.py
index 5bddccf840..2fd827b02e 100644
--- a/hypha/apply/funds/workflow.py
+++ b/hypha/apply/funds/workflow.py
@@ -233,6 +233,7 @@ def make_permissions(edit=None, review=None, view=None):
                 'almost': _('Accept but additional info required'),
                 'accepted': _('Accept'),
                 'rejected': _('Dismiss'),
+                'withdrawn': _('Withdraw'),
             },
             'display': _('Need screening'),
             'public': _('Application Received'),
@@ -250,6 +251,7 @@ def make_permissions(edit=None, review=None, view=None):
                 'almost': _('Accept but additional info required'),
                 'accepted': _('Accept'),
                 'rejected': _('Dismiss'),
+                'withdrawn': _('Withdraw'),
             },
             'display': _('More information required'),
             'stage': Request,
@@ -261,6 +263,7 @@ def make_permissions(edit=None, review=None, view=None):
             'transitions': {
                 'post_review_discussion': _('Close Review'),
                 INITIAL_STATE: _('Need screening (revert)'),
+                'withdrawn': _('Withdraw'),
             },
             'display': _('Internal Review'),
             'public': _('{org_short_name} Review').format(org_short_name=settings.ORG_SHORT_NAME),
@@ -277,6 +280,7 @@ def make_permissions(edit=None, review=None, view=None):
                 'almost': _('Accept but additional info required'),
                 'accepted': _('Accept'),
                 'rejected': _('Dismiss'),
+                'withdrawn': _('Withdraw'),
             },
             'display': _('Ready For Discussion'),
             'stage': Request,
@@ -293,6 +297,7 @@ def make_permissions(edit=None, review=None, view=None):
                 'almost': _('Accept but additional info required'),
                 'accepted': _('Accept'),
                 'rejected': _('Dismiss'),
+                'withdrawn': _('Withdraw'),
             },
             'display': _('More information required'),
             'stage': Request,
@@ -306,6 +311,7 @@ def make_permissions(edit=None, review=None, view=None):
                 'almost': _('Accept but additional info required'),
                 'accepted': _('Accept'),
                 'rejected': _('Dismiss'),
+                'withdrawn': _('Withdraw'),
             },
             'display': _('Ready for Determination'),
             'permissions': hidden_from_applicant_permissions,
@@ -323,6 +329,7 @@ def make_permissions(edit=None, review=None, view=None):
             'transitions': {
                 'accepted': _('Accept'),
                 'post_review_discussion': _('Ready For Discussion (revert)'),
+                'withdrawn': _('Withdraw'),
             },
             'display': _('Accepted but additional info required'),
             'stage': Request,
@@ -333,6 +340,11 @@ def make_permissions(edit=None, review=None, view=None):
             'stage': Request,
             'permissions': no_permissions,
         },
+        'withdrawn': {
+            'display': _('Withdrawn'),
+            'stage': Request,
+            'permissions': staff_edit_permissions,
+        },
     },
 ]
 
@@ -358,7 +370,7 @@ def make_permissions(edit=None, review=None, view=None):
                 'ext_internal_review': _('Open Review'),
                 'ext_determination': _('Ready For Determination'),
                 'ext_rejected': _('Dismiss'),
-                'ext_screening_withdrawn': _('Withdraw'),
+                'ext_withdrawn': _('Withdraw'),
             },
             'display': _('Need screening'),
             'public': _('Application Received'),
@@ -372,23 +384,18 @@ def make_permissions(edit=None, review=None, view=None):
                     'permissions': {UserPermissions.APPLICANT, UserPermissions.STAFF, UserPermissions.LEAD, UserPermissions.ADMIN},
                     'method': 'create_revision',
                 },
-                'ext_screening_withdrawn': _('Withdraw'),
+                'ext_withdrawn': _('Withdraw'),
             },
             'display': _('More information required'),
             'stage': RequestExt,
             'permissions': applicant_edit_permissions,
         },
-        'ext_screening_withdrawn': {
-            'display': _('Withdrawn'),
-            'stage': RequestExt,
-            'permissions': staff_edit_permissions,
-        },
     },
     {
         'ext_internal_review': {
             'transitions': {
                 'ext_post_review_discussion': _('Close Review'),
-                'ext_review_withdrawn': _('Withdraw'),
+                'ext_withdrawn': _('Withdraw'),
                 INITIAL_STATE: _('Need screening (revert)'),
             },
             'display': _('Internal Review'),
@@ -417,7 +424,7 @@ def make_permissions(edit=None, review=None, view=None):
                     'permissions': {UserPermissions.APPLICANT, UserPermissions.STAFF, UserPermissions.LEAD, UserPermissions.ADMIN},
                     'method': 'create_revision',
                 },
-                'ext_review_withdrawn': _('Withdraw'),
+                'ext_withdrawn': _('Withdraw'),
             },
             'display': _('More information required'),
             'stage': RequestExt,
@@ -434,11 +441,6 @@ def make_permissions(edit=None, review=None, view=None):
             'stage': RequestExt,
             'permissions': reviewer_review_permissions,
         },
-        'ext_review_withdrawn': {
-            'display': _('Withdrawn'),
-            'stage': RequestExt,
-            'permissions': staff_edit_permissions,
-        },
     },
     {
         'ext_post_external_review_discussion': {
@@ -449,7 +451,7 @@ def make_permissions(edit=None, review=None, view=None):
                 'ext_almost': _('Accept but additional info required'),
                 'ext_accepted': _('Accept'),
                 'ext_rejected': _('Dismiss'),
-                'ext_external_review_withdrawn': _('Withdraw'),
+                'ext_withdrawn': _('Withdraw'),
             },
             'display': _('Ready For Discussion'),
             'stage': RequestExt,
@@ -462,17 +464,12 @@ def make_permissions(edit=None, review=None, view=None):
                     'permissions': {UserPermissions.APPLICANT, UserPermissions.STAFF, UserPermissions.LEAD, UserPermissions.ADMIN},
                     'method': 'create_revision',
                 },
-                'ext_external_review_withdrawn': _('Withdraw'),
+                'ext_withdrawn': _('Withdraw'),
             },
             'display': _('More information required'),
             'stage': RequestExt,
             'permissions': applicant_edit_permissions,
         },
-        'ext_external_review_withdrawn': {
-            'display': _('Withdrawn'),
-            'stage': RequestExt,
-            'permissions': staff_edit_permissions,
-        },
     },
     {
         'ext_determination': {
@@ -499,6 +496,7 @@ def make_permissions(edit=None, review=None, view=None):
             'transitions': {
                 'ext_accepted': _('Accept'),
                 'ext_post_external_review_discussion': _('Ready For Discussion (revert)'),
+                'ext_withdrawn': _('Withdraw'),
             },
             'display': _('Accepted but additional info required'),
             'stage': RequestExt,
@@ -542,6 +540,7 @@ def make_permissions(edit=None, review=None, view=None):
                 'com_community_review': _('Open Community Review'),
                 'com_determination': _('Ready For Determination'),
                 'com_rejected': _('Dismiss'),
+                'com_withdrawn': _('Withdraw'),
             },
             'display': _('Need screening'),
             'public': _('Application Received'),
@@ -564,6 +563,7 @@ def make_permissions(edit=None, review=None, view=None):
             'transitions': {
                 INITIAL_STATE: _('Need screening (revert)'),
                 'com_rejected': _('Dismiss'),
+                'com_withdrawn': _('Withdraw'),
             },
             'display': 'Open Call (public)',
             'stage': RequestCom,
@@ -577,6 +577,7 @@ def make_permissions(edit=None, review=None, view=None):
                 'com_post_review_discussion': _('Close Review'),
                 INITIAL_STATE: _('Need screening (revert)'),
                 'com_rejected': _('Dismiss'),
+                'com_withdrawn': _('Withdraw'),
             },
             'display': _('Internal Review'),
             'public': _('{org_short_name} Review').format(org_short_name=settings.ORG_SHORT_NAME),
@@ -588,6 +589,7 @@ def make_permissions(edit=None, review=None, view=None):
                 'com_post_review_discussion': _('Close Review'),
                 'com_internal_review': _('Open Internal Review (revert)'),
                 'com_rejected': _('Dismiss'),
+                'com_withdrawn': _('Withdraw'),
             },
             'display': _('Community Review'),
             'public': _('{org_short_name} Review').format(org_short_name=settings.ORG_SHORT_NAME),
@@ -603,6 +605,7 @@ def make_permissions(edit=None, review=None, view=None):
                 'com_determination': _('Ready For Determination'),
                 'com_internal_review': _('Open Internal Review (revert)'),
                 'com_rejected': _('Dismiss'),
+                'com_withdrawn': _('Withdraw'),
             },
             'display': _('Ready For Discussion'),
             'stage': RequestCom,
@@ -615,6 +618,7 @@ def make_permissions(edit=None, review=None, view=None):
                     'permissions': {UserPermissions.APPLICANT, UserPermissions.STAFF, UserPermissions.LEAD, UserPermissions.ADMIN},
                     'method': 'create_revision',
                 },
+                'com_withdrawn': _('Withdraw'),
             },
             'display': _('More information required'),
             'stage': RequestCom,
@@ -626,6 +630,7 @@ def make_permissions(edit=None, review=None, view=None):
             'transitions': {
                 'com_post_external_review_discussion': _('Close Review'),
                 'com_post_review_discussion': _('Ready For Discussion (revert)'),
+                'com_withdrawn': _('Withdraw'),
             },
             'display': _('External Review'),
             'stage': RequestCom,
@@ -641,6 +646,7 @@ def make_permissions(edit=None, review=None, view=None):
                 'com_almost': _('Accept but additional info required'),
                 'com_accepted': _('Accept'),
                 'com_rejected': _('Dismiss'),
+                'com_withdrawn': _('Withdraw'),
             },
             'display': _('Ready For Discussion'),
             'stage': RequestCom,
@@ -653,6 +659,7 @@ def make_permissions(edit=None, review=None, view=None):
                     'permissions': {UserPermissions.APPLICANT, UserPermissions.STAFF, UserPermissions.LEAD, UserPermissions.ADMIN},
                     'method': 'create_revision',
                 },
+                'com_withdrawn': _('Withdraw'),
             },
             'display': _('More information required'),
             'stage': RequestCom,
@@ -666,6 +673,7 @@ def make_permissions(edit=None, review=None, view=None):
                 'com_almost': _('Accept but additional info required'),
                 'com_accepted': _('Accept'),
                 'com_rejected': _('Dismiss'),
+                'com_withdrawn': _('Withdraw'),
             },
             'display': _('Ready for Determination'),
             'permissions': hidden_from_applicant_permissions,
@@ -683,6 +691,7 @@ def make_permissions(edit=None, review=None, view=None):
             'transitions': {
                 'com_accepted': _('Accept'),
                 'com_post_external_review_discussion': _('Ready For Discussion (revert)'),
+                'com_withdrawn': _('Withdraw'),
             },
             'display': _('Accepted but additional info required'),
             'stage': RequestCom,
@@ -693,6 +702,11 @@ def make_permissions(edit=None, review=None, view=None):
             'stage': RequestCom,
             'permissions': no_permissions,
         },
+        'com_withdrawn': {
+            'display': _('Withdrawn'),
+            'stage': RequestCom,
+            'permissions': staff_edit_permissions,
+        },
     },
 ]
 
@@ -720,6 +734,7 @@ def make_permissions(edit=None, review=None, view=None):
                 'concept_determination': _('Ready For Preliminary Determination'),
                 'invited_to_proposal': _('Invite to Proposal'),
                 'concept_rejected': _('Dismiss'),
+                'concept_withdrawn': _('Withdraw'),
             },
             'display': _('Need screening'),
             'public': _('Concept Note Received'),
@@ -736,6 +751,7 @@ def make_permissions(edit=None, review=None, view=None):
                 'concept_rejected': _('Dismiss'),
                 'invited_to_proposal': _('Invite to Proposal'),
                 'concept_determination': _('Ready For Preliminary Determination'),
+                'concept_withdrawn': _('Withdraw'),
             },
             'display': _('More information required'),
             'stage': Concept,
@@ -748,6 +764,7 @@ def make_permissions(edit=None, review=None, view=None):
                 'concept_review_discussion': _('Close Review'),
                 INITIAL_STATE: _('Need screening (revert)'),
                 'invited_to_proposal': _('Invite to Proposal'),
+                'concept_withdrawn': _('Withdraw'),
             },
             'display': _('Internal Review'),
             'public': _('{org_short_name} Review').format(org_short_name=settings.ORG_SHORT_NAME),
@@ -763,6 +780,7 @@ def make_permissions(edit=None, review=None, view=None):
                 'concept_internal_review': _('Open Review (revert)'),
                 'invited_to_proposal': _('Invite to Proposal'),
                 'concept_rejected': _('Dismiss'),
+                'concept_withdrawn': _('Withdraw'),
             },
             'display': _('Ready For Discussion'),
             'stage': Concept,
@@ -776,6 +794,7 @@ def make_permissions(edit=None, review=None, view=None):
                     'method': 'create_revision',
                 },
                 'invited_to_proposal': _('Invite to Proposal'),
+                'concept_withdrawn': _('Withdraw'),
             },
             'display': _('More information required'),
             'stage': Concept,
@@ -788,6 +807,7 @@ def make_permissions(edit=None, review=None, view=None):
                 'concept_review_discussion': _('Ready For Discussion (revert)'),
                 'invited_to_proposal': _('Invite to Proposal'),
                 'concept_rejected': _('Dismiss'),
+                'concept_withdrawn': _('Withdraw'),
             },
             'display': _('Ready for Preliminary Determination'),
             'permissions': hidden_from_applicant_permissions,
@@ -814,6 +834,11 @@ def make_permissions(edit=None, review=None, view=None):
             'stage': Concept,
             'permissions': no_permissions,
         },
+        'concept_withdrawn': {
+            'display': _('Withdrawn'),
+            'stage': Concept,
+            'permissions': staff_edit_permissions,
+        },
     },
     {
         'draft_proposal': {
@@ -836,6 +861,7 @@ def make_permissions(edit=None, review=None, view=None):
                 'external_review': _('Open External Review'),
                 'proposal_determination': _('Ready For Final Determination'),
                 'proposal_rejected': _('Dismiss'),
+                'proposal_withdrawn': _('Withdraw'),
             },
             'display': _('Proposal Received'),
             'stage': Proposal,
@@ -851,6 +877,7 @@ def make_permissions(edit=None, review=None, view=None):
                 'external_review': _('Open External Review'),
                 'proposal_determination': _('Ready For Final Determination'),
                 'proposal_rejected': _('Dismiss'),
+                'proposal_withdrawn': _('Withdraw'),
             },
             'display': _('More information required'),
             'stage': Proposal,
@@ -862,6 +889,7 @@ def make_permissions(edit=None, review=None, view=None):
             'transitions': {
                 'post_proposal_review_discussion': _('Close Review'),
                 'proposal_discussion': _('Proposal Received (revert)'),
+                'proposal_withdrawn': _('Withdraw'),
             },
             'display': _('Internal Review'),
             'public': _('{org_short_name} Review').format(org_short_name=settings.ORG_SHORT_NAME),
@@ -877,6 +905,7 @@ def make_permissions(edit=None, review=None, view=None):
                 'proposal_determination': _('Ready For Final Determination'),
                 'proposal_internal_review': _('Open Internal Review (revert)'),
                 'proposal_rejected': _('Dismiss'),
+                'proposal_withdrawn': _('Withdraw'),
             },
             'display': _('Ready For Discussion'),
             'stage': Proposal,
@@ -890,6 +919,7 @@ def make_permissions(edit=None, review=None, view=None):
                     'method': 'create_revision',
                 },
                 'external_review': _('Open External Review'),
+                'proposal_withdrawn': _('Withdraw'),
             },
             'display': _('More information required'),
             'stage': Proposal,
@@ -901,6 +931,7 @@ def make_permissions(edit=None, review=None, view=None):
             'transitions': {
                 'post_external_review_discussion': _('Close Review'),
                 'post_proposal_review_discussion': _('Ready For Discussion (revert)'),
+                'proposal_withdrawn': _('Withdraw'),
             },
             'display': _('External Review'),
             'stage': Proposal,
@@ -916,6 +947,7 @@ def make_permissions(edit=None, review=None, view=None):
                 'proposal_almost': _('Accept but additional info required'),
                 'proposal_accepted': _('Accept'),
                 'proposal_rejected': _('Dismiss'),
+                'proposal_withdrawn': _('Withdraw'),
             },
             'display': _('Ready For Discussion'),
             'stage': Proposal,
@@ -928,6 +960,7 @@ def make_permissions(edit=None, review=None, view=None):
                     'permissions': {UserPermissions.APPLICANT, UserPermissions.STAFF, UserPermissions.LEAD, UserPermissions.ADMIN},
                     'method': 'create_revision',
                 },
+                'proposal_withdrawn': _('Withdraw'),
             },
             'display': _('More information required'),
             'stage': Proposal,
@@ -941,6 +974,7 @@ def make_permissions(edit=None, review=None, view=None):
                 'proposal_almost': _('Accept but additional info required'),
                 'proposal_accepted': _('Accept'),
                 'proposal_rejected': _('Dismiss'),
+                'proposal_withdrawn': _('Withdraw'),
             },
             'display': _('Ready for Final Determination'),
             'permissions': hidden_from_applicant_permissions,
@@ -968,6 +1002,11 @@ def make_permissions(edit=None, review=None, view=None):
             'stage': Proposal,
             'permissions': no_permissions,
         },
+        'proposal_withdrawn': {
+            'display': _('Withdrawn'),
+            'stage': Proposal,
+            'permissions': staff_edit_permissions,
+        },
     },
 ]
 

From 1d7b8297a47301b46a430e5b9430d48e651c81ee Mon Sep 17 00:00:00 2001
From: Fredrik Jonsson <frjo@xdeb.org>
Date: Wed, 26 Jul 2023 10:16:57 +0200
Subject: [PATCH 6/6] Move css from custom file.

---
 hypha/apply/funds/views.py                            | 4 ++--
 hypha/static_src/src/sass/apply/components/_link.scss | 8 +++++---
 hypha/static_src/src/sass/apply/custom/_custom.scss   | 6 +-----
 3 files changed, 8 insertions(+), 10 deletions(-)

diff --git a/hypha/apply/funds/views.py b/hypha/apply/funds/views.py
index 737eea50bb..2054fb8351 100644
--- a/hypha/apply/funds/views.py
+++ b/hypha/apply/funds/views.py
@@ -1430,11 +1430,11 @@ def withdraw(self, request, *args, **kwargs):
         if not settings.ENABLE_SUBMISSION_WITHDRAWAL:
             raise PermissionDenied
 
-        obj = self.get_object()
-
         if not request.user.is_applicant:
             raise PermissionDenied
 
+        obj = self.get_object()
+
         withdraw_actions = [action for action in obj.workflow[obj.status].transitions.keys() if 'withdraw' in action]
 
         if len(withdraw_actions) > 0:
diff --git a/hypha/static_src/src/sass/apply/components/_link.scss b/hypha/static_src/src/sass/apply/components/_link.scss
index 0edca13edc..989ff2ca37 100644
--- a/hypha/static_src/src/sass/apply/components/_link.scss
+++ b/hypha/static_src/src/sass/apply/components/_link.scss
@@ -230,7 +230,8 @@
     }
 
     &--edit-submission,
-    &--delete-submission {
+    &--delete-submission,
+    &--withdraw-submission {
         display: flex;
         align-items: center;
         font-weight: $weight--bold;
@@ -249,12 +250,13 @@
         }
     }
 
-    &--delete-submission {
+    &--delete-submission,
+    &--withdraw-submission {
         margin-right: 1rem;
         padding-right: 1rem;
         border-right: 2px solid $color--mid-grey;
 
-        &:only-child {
+        &:last-child {
             border-right: 0;
             padding-right: 0;
             margin-right: 0;
diff --git a/hypha/static_src/src/sass/apply/custom/_custom.scss b/hypha/static_src/src/sass/apply/custom/_custom.scss
index 55e678b65a..061b3d43d2 100644
--- a/hypha/static_src/src/sass/apply/custom/_custom.scss
+++ b/hypha/static_src/src/sass/apply/custom/_custom.scss
@@ -1,5 +1 @@
-.link--withdraw-submission {
-    margin-right: 1rem;
-    padding-right: 1rem;
-    font-weight: 700;
-}
+// stylelint-disable no-empty-source