diff --git a/docs/setup/administrators/configuration.md b/docs/setup/administrators/configuration.md index 59aa689c68..7a3e7cf978 100644 --- a/docs/setup/administrators/configuration.md +++ b/docs/setup/administrators/configuration.md @@ -112,7 +112,11 @@ If applicants should be forced to preview their application before submitting ---- -Set the allowed file extension for all uploads fields. +### 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'] FILE_ACCEPT_ATTR_VALUE = ', '.join(['.' + ext for ext in FILE_ALLOWED_EXTENSIONS]) diff --git a/hypha/apply/funds/models/submissions.py b/hypha/apply/funds/models/submissions.py index 6bf73d5909..b9bb1727be 100644 --- a/hypha/apply/funds/models/submissions.py +++ b/hypha/apply/funds/models/submissions.py @@ -944,7 +944,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..396081fdbe --- /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 %} +
+
+

{% trans "Withdrawing" %}: {{ object.title }}

+
+
+ +
+
+
+ {% csrf_token %} +

{% blocktrans %}Are you sure you want to withdraw "{{ object }}" from consideration?{% endblocktrans %}

+ +
+
+
+ +{% endblock %} diff --git a/hypha/apply/funds/templates/funds/applicationsubmission_detail.html b/hypha/apply/funds/templates/funds/applicationsubmission_detail.html index 06945383a2..f3d26c4fa5 100644 --- a/hypha/apply/funds/templates/funds/applicationsubmission_detail.html +++ b/hypha/apply/funds/templates/funds/applicationsubmission_detail.html @@ -137,6 +137,14 @@
{% blocktrans with stage=object.previous.stage %}Your {{ stage }} applicatio {% trans "Delete" %} {% endif %} + {% if request.user|has_withdraw_perm:object %} + + {% heroicon_micro "arrow-uturn-down" class="inline me-1 fill-red-600" aria_hidden=true %} + {% trans "Withdraw" %} + + {% endif %} {% if request.user|has_edit_perm:object %} s return settings.ORG_LONG_NAME # Likely an edge case but covering bases return str(author) + + +@register.filter +def has_withdraw_perm(user, submission): + if settings.ENABLE_SUBMISSION_WITHDRAWAL: + return check_permission(user, "withdraw", submission) + return False diff --git a/hypha/apply/funds/urls.py b/hypha/apply/funds/urls.py index 00c343a38d..5e16bdcf59 100644 --- a/hypha/apply/funds/urls.py +++ b/hypha/apply/funds/urls.py @@ -26,6 +26,7 @@ SubmissionResultView, SubmissionsByStatus, SubmissionSealedView, + SubmissionWithdrawView, TranslateSubmissionView, UpdateLeadView, UpdateMetaTermsView, @@ -258,6 +259,9 @@ "download/", SubmissionDetailPDFView.as_view(), name="download" ), path("delete/", SubmissionDeleteView.as_view(), name="delete"), + path( + "withdraw/", SubmissionWithdrawView.as_view(), name="withdraw" + ), path( "documents//", SubmissionPrivateMediaView.as_view(), diff --git a/hypha/apply/funds/utils.py b/hypha/apply/funds/utils.py index 089e0cbe50..2fff9eebd0 100644 --- a/hypha/apply/funds/utils.py +++ b/hypha/apply/funds/utils.py @@ -94,6 +94,7 @@ def model_form_initial(instance, fields=None, exclude=None): ], "accepted": ["accepted"], "dismissed": ["dismissed"], + "withdrawn": ["withdrawn"], } diff --git a/hypha/apply/funds/views.py b/hypha/apply/funds/views.py index 67d19600c9..d253cbab28 100644 --- a/hypha/apply/funds/views.py +++ b/hypha/apply/funds/views.py @@ -12,7 +12,7 @@ user_passes_test, ) from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin -from django.core.exceptions import PermissionDenied +from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.db.models import Count, Q from django.forms import BaseModelForm from django.http import ( @@ -39,7 +39,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_htmx.http import ( @@ -136,6 +140,7 @@ PHASES_MAPPING, STAGE_CHANGE_ACTIONS, active_statuses, + get_withdraw_action_for_stage, review_statuses, ) @@ -1785,6 +1790,41 @@ def form_valid(self, form): return super().form_valid(form) +@method_decorator(login_required, name="dispatch") +class SubmissionWithdrawView( + SingleObjectTemplateResponseMixin, UserPassesTestMixin, BaseDetailView +): + model = ApplicationSubmission + success_url = reverse_lazy("funds:submissions:list") + template_name_suffix = "_confirm_withdraw" + + def dispatch(self, *args, **kwargs): + self.submission = self.get_object() + return super().dispatch(*args, **kwargs) + + def post(self, request, *args, **kwargs): + return self.withdraw(request, *args, **kwargs) + + def withdraw(self, request, *args, **kwargs): + withdraw_action = get_withdraw_action_for_stage(self.submission.stage) + + if withdraw_action: + self.submission.perform_transition( + withdraw_action, self.request.user, request=self.request, notify=False + ) + else: + raise ImproperlyConfigured( + f'No withdraw actions found in workflow "{self.submission.workflow}"' + ) + + return HttpResponseRedirect(self.submission.get_absolute_url()) + + def test_func(self): + can_withdraw = self.submission.phase.permissions.can_withdraw(self.request.user) + + return settings.ENABLE_SUBMISSION_WITHDRAWAL and can_withdraw + + @method_decorator(login_required, name="dispatch") class SubmissionPrivateMediaView(UserPassesTestMixin, PrivateMediaView): raise_exception = True diff --git a/hypha/apply/funds/workflow.py b/hypha/apply/funds/workflow.py index 89c9b5b931..6bb87e1095 100644 --- a/hypha/apply/funds/workflow.py +++ b/hypha/apply/funds/workflow.py @@ -181,6 +181,9 @@ def can_review(self, user): def can_view(self, user): return self.can_do(user, "view") + def can_withdraw(self, user): + return self.can_do(user, "withdraw") + staff_can = lambda user: user.is_apply_staff # NOQA @@ -193,7 +196,7 @@ def can_view(self, user): community_can = lambda user: user.is_community_reviewer # NOQA -def make_permissions(edit=None, review=None, view=None): +def make_permissions(edit=None, review=None, view=None, withdraw=None): return { "edit": edit or [], "review": review or [], @@ -204,12 +207,15 @@ def make_permissions(edit=None, review=None, view=None): reviewer_can, partner_can, ], + "withdraw": withdraw or [], } no_permissions = make_permissions() -default_permissions = make_permissions(edit=[staff_can], review=[staff_can]) +default_permissions = make_permissions( + edit=[staff_can], review=[staff_can], withdraw=[applicant_can] +) hidden_from_applicant_permissions = make_permissions( edit=[staff_can], review=[staff_can], view=[staff_can, reviewer_can] @@ -224,7 +230,7 @@ def make_permissions(edit=None, review=None, view=None): ) applicant_edit_permissions = make_permissions( - edit=[applicant_can, partner_can], review=[staff_can] + edit=[applicant_can, partner_can], review=[staff_can], withdraw=[applicant_can] ) staff_edit_permissions = make_permissions(edit=[staff_can]) @@ -271,6 +277,11 @@ def make_permissions(edit=None, review=None, view=None): "almost": _("Accept but additional info required"), "accepted": _("Accept"), "rejected": _("Dismiss"), + "withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Need screening"), "public": _("Application Received"), @@ -294,6 +305,11 @@ def make_permissions(edit=None, review=None, view=None): "almost": _("Accept but additional info required"), "accepted": _("Accept"), "rejected": _("Dismiss"), + "withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("More information required"), "stage": Request, @@ -305,6 +321,11 @@ def make_permissions(edit=None, review=None, view=None): "transitions": { "post_review_discussion": _("Close Review"), INITIAL_STATE: _("Need screening (revert)"), + "withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Internal Review"), "public": _("{org_short_name} Review").format( @@ -323,6 +344,11 @@ def make_permissions(edit=None, review=None, view=None): "almost": _("Accept but additional info required"), "accepted": _("Accept"), "rejected": _("Dismiss"), + "withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Ready For Discussion"), "stage": Request, @@ -345,6 +371,11 @@ def make_permissions(edit=None, review=None, view=None): "almost": _("Accept but additional info required"), "accepted": _("Accept"), "rejected": _("Dismiss"), + "withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("More information required"), "stage": Request, @@ -358,6 +389,11 @@ def make_permissions(edit=None, review=None, view=None): "almost": _("Accept but additional info required"), "accepted": _("Accept"), "rejected": _("Dismiss"), + "withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Ready for Determination"), "permissions": hidden_from_applicant_permissions, @@ -375,6 +411,11 @@ def make_permissions(edit=None, review=None, view=None): "transitions": { "accepted": _("Accept"), "post_review_discussion": _("Ready For Discussion (revert)"), + "withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Accepted but additional info required"), "stage": Request, @@ -385,6 +426,11 @@ def make_permissions(edit=None, review=None, view=None): "stage": Request, "permissions": no_permissions, }, + "withdrawn": { + "display": _("Withdrawn"), + "stage": Request, + "permissions": no_permissions, + }, }, ] @@ -411,6 +457,11 @@ def make_permissions(edit=None, review=None, view=None): "same_internal_review": _("Open Review"), "same_determination": _("Ready For Determination"), "same_rejected": _("Dismiss"), + "same_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Need screening"), "public": _("Application Received"), @@ -430,6 +481,11 @@ def make_permissions(edit=None, review=None, view=None): "method": "create_revision", "custom": {"trigger_on_submit": True}, }, + "same_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("More information required"), "stage": RequestSame, @@ -440,6 +496,11 @@ def make_permissions(edit=None, review=None, view=None): "same_internal_review": { "transitions": { "same_post_review_discussion": _("Close Review"), + "same_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, INITIAL_STATE: _("Need screening (revert)"), }, "display": _("Review"), @@ -457,6 +518,11 @@ def make_permissions(edit=None, review=None, view=None): "same_determination": _("Ready For Determination"), "same_internal_review": _("Open Review (revert)"), "same_rejected": _("Dismiss"), + "same_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Ready For Discussion"), "stage": RequestSame, @@ -475,6 +541,11 @@ def make_permissions(edit=None, review=None, view=None): "method": "create_revision", "custom": {"trigger_on_submit": True}, }, + "same_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("More information required"), "stage": RequestSame, @@ -488,6 +559,11 @@ def make_permissions(edit=None, review=None, view=None): "same_almost": _("Accept but additional info required"), "same_accepted": _("Accept"), "same_rejected": _("Dismiss"), + "same_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Ready for Determination"), "permissions": hidden_from_applicant_permissions, @@ -505,6 +581,11 @@ def make_permissions(edit=None, review=None, view=None): "transitions": { "same_accepted": _("Accept"), "same_post_review_discussion": _("Ready For Discussion (revert)"), + "same_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Accepted but additional info required"), "stage": RequestSame, @@ -515,6 +596,11 @@ def make_permissions(edit=None, review=None, view=None): "stage": RequestSame, "permissions": no_permissions, }, + "same_withdrawn": { + "display": _("Withdraw"), + "stage": RequestSame, + "permissions": no_permissions, + }, }, ] @@ -542,6 +628,11 @@ def make_permissions(edit=None, review=None, view=None): "ext_internal_review": _("Open Review"), "ext_determination": _("Ready For Determination"), "ext_rejected": _("Dismiss"), + "ext_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Need screening"), "public": _("Application Received"), @@ -561,6 +652,11 @@ def make_permissions(edit=None, review=None, view=None): "method": "create_revision", "custom": {"trigger_on_submit": True}, }, + "ext_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("More information required"), "stage": RequestExt, @@ -571,6 +667,11 @@ def make_permissions(edit=None, review=None, view=None): "ext_internal_review": { "transitions": { "ext_post_review_discussion": _("Close Review"), + "ext_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, INITIAL_STATE: _("Need screening (revert)"), }, "display": _("Internal Review"), @@ -589,6 +690,11 @@ def make_permissions(edit=None, review=None, view=None): "ext_determination": _("Ready For Determination"), "ext_internal_review": _("Open Internal Review (revert)"), "ext_rejected": _("Dismiss"), + "ext_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Ready For Discussion"), "stage": RequestExt, @@ -607,6 +713,11 @@ def make_permissions(edit=None, review=None, view=None): "method": "create_revision", "custom": {"trigger_on_submit": True}, }, + "ext_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("More information required"), "stage": RequestExt, @@ -633,6 +744,11 @@ def make_permissions(edit=None, review=None, view=None): "ext_almost": _("Accept but additional info required"), "ext_accepted": _("Accept"), "ext_rejected": _("Dismiss"), + "ext_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Ready For Discussion"), "stage": RequestExt, @@ -651,6 +767,11 @@ def make_permissions(edit=None, review=None, view=None): "method": "create_revision", "custom": {"trigger_on_submit": True}, }, + "ext_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("More information required"), "stage": RequestExt, @@ -666,6 +787,11 @@ def make_permissions(edit=None, review=None, view=None): "ext_almost": _("Accept but additional info required"), "ext_accepted": _("Accept"), "ext_rejected": _("Dismiss"), + "ext_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Ready for Determination"), "permissions": hidden_from_applicant_permissions, @@ -685,6 +811,11 @@ def make_permissions(edit=None, review=None, view=None): "ext_post_external_review_discussion": _( "Ready For Discussion (revert)" ), + "ext_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Accepted but additional info required"), "stage": RequestExt, @@ -695,6 +826,11 @@ def make_permissions(edit=None, review=None, view=None): "stage": RequestExt, "permissions": no_permissions, }, + "ext_withdrawn": { + "display": _("Withdrawn"), + "stage": RequestExt, + "permissions": no_permissions, + }, }, ] @@ -724,6 +860,11 @@ 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": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Need screening"), "public": _("Application Received"), @@ -752,6 +893,11 @@ def make_permissions(edit=None, review=None, view=None): "transitions": { INITIAL_STATE: _("Need screening (revert)"), "com_rejected": _("Dismiss"), + "com_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": "Open Call (public)", "stage": RequestCom, @@ -765,6 +911,11 @@ 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": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Internal Review"), "public": _("{org_short_name} Review").format( @@ -778,6 +929,11 @@ 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": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Community Review"), "public": _("{org_short_name} Review").format( @@ -795,6 +951,11 @@ 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": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Ready For Discussion"), "stage": RequestCom, @@ -813,6 +974,11 @@ def make_permissions(edit=None, review=None, view=None): "method": "create_revision", "custom": {"trigger_on_submit": True}, }, + "com_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("More information required"), "stage": RequestCom, @@ -824,6 +990,11 @@ 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": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("External Review"), "stage": RequestCom, @@ -839,6 +1010,11 @@ def make_permissions(edit=None, review=None, view=None): "com_almost": _("Accept but additional info required"), "com_accepted": _("Accept"), "com_rejected": _("Dismiss"), + "com_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Ready For Discussion"), "stage": RequestCom, @@ -857,6 +1033,11 @@ def make_permissions(edit=None, review=None, view=None): "method": "create_revision", "custom": {"trigger_on_submit": True}, }, + "com_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("More information required"), "stage": RequestCom, @@ -872,6 +1053,11 @@ def make_permissions(edit=None, review=None, view=None): "com_almost": _("Accept but additional info required"), "com_accepted": _("Accept"), "com_rejected": _("Dismiss"), + "com_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Ready for Determination"), "permissions": hidden_from_applicant_permissions, @@ -891,6 +1077,11 @@ def make_permissions(edit=None, review=None, view=None): "com_post_external_review_discussion": _( "Ready For Discussion (revert)" ), + "com_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Accepted but additional info required"), "stage": RequestCom, @@ -901,6 +1092,11 @@ def make_permissions(edit=None, review=None, view=None): "stage": RequestCom, "permissions": no_permissions, }, + "com_withdrawn": { + "display": _("Withdrawn"), + "stage": RequestCom, + "permissions": no_permissions, + }, }, ] @@ -929,6 +1125,11 @@ 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": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Need screening"), "public": _("Concept Note Received"), @@ -951,6 +1152,11 @@ 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": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("More information required"), "stage": Concept, @@ -963,6 +1169,11 @@ 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": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Internal Review"), "public": _("{org_short_name} Review").format( @@ -980,6 +1191,11 @@ 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": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Ready For Discussion"), "stage": Concept, @@ -999,6 +1215,11 @@ def make_permissions(edit=None, review=None, view=None): "custom": {"trigger_on_submit": True}, }, "invited_to_proposal": _("Invite to Proposal"), + "concept_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("More information required"), "stage": Concept, @@ -1011,6 +1232,11 @@ 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": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Ready for Preliminary Determination"), "permissions": hidden_from_applicant_permissions, @@ -1041,6 +1267,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": { @@ -1068,6 +1299,11 @@ 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": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Proposal Received"), "stage": Proposal, @@ -1089,6 +1325,11 @@ 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": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("More information required"), "stage": Proposal, @@ -1100,6 +1341,11 @@ def make_permissions(edit=None, review=None, view=None): "transitions": { "post_proposal_review_discussion": _("Close Review"), "proposal_discussion": _("Proposal Received (revert)"), + "proposal_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Internal Review"), "public": _("{org_short_name} Review").format( @@ -1117,6 +1363,11 @@ 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": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Ready For Discussion"), "stage": Proposal, @@ -1136,6 +1387,11 @@ def make_permissions(edit=None, review=None, view=None): "custom": {"trigger_on_submit": True}, }, "external_review": _("Open External Review"), + "proposal_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("More information required"), "stage": Proposal, @@ -1147,6 +1403,11 @@ 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": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("External Review"), "stage": Proposal, @@ -1162,6 +1423,11 @@ def make_permissions(edit=None, review=None, view=None): "proposal_almost": _("Accept but additional info required"), "proposal_accepted": _("Accept"), "proposal_rejected": _("Dismiss"), + "proposal_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Ready For Discussion"), "stage": Proposal, @@ -1180,6 +1446,11 @@ def make_permissions(edit=None, review=None, view=None): "method": "create_revision", "custom": {"trigger_on_submit": True}, }, + "proposal_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("More information required"), "stage": Proposal, @@ -1193,6 +1464,11 @@ def make_permissions(edit=None, review=None, view=None): "proposal_almost": _("Accept but additional info required"), "proposal_accepted": _("Accept"), "proposal_rejected": _("Dismiss"), + "proposal_withdrawn": { + "display": _("Withdraw"), + "permissions": {UserPermissions.APPLICANT}, + "method": "withdraw", + }, }, "display": _("Ready for Final Determination"), "permissions": hidden_from_applicant_permissions, @@ -1220,6 +1496,11 @@ def make_permissions(edit=None, review=None, view=None): "stage": Proposal, "permissions": no_permissions, }, + "proposal_withdrawn": { + "display": _("Withdrawn"), + "stage": Proposal, + "permissions": no_permissions, + }, }, ] @@ -1309,7 +1590,10 @@ def get_stage_change_actions(): active_statuses = [ status for status, _ in PHASES - if "accepted" not in status and "rejected" not in status and "invited" not in status + if "accepted" not in status + and "rejected" not in status + and "invited" not in status + and "withdrawn" not in status ] @@ -1367,6 +1651,18 @@ def get_ext_or_higher_statuses(): return reviews +def get_withdraw_action_for_stage(stage=None): + """ + Returns the withdraw action for stage. + """ + for workflow in WORKFLOWS.values(): + for phase in workflow.values(): + if phase.stage == stage and phase.name in withdrawn_statuses: + return phase.name + + return False + + def get_accepted_statuses(): accepted_statuses = set() for phase_name, phase in PHASES: @@ -1383,11 +1679,20 @@ def get_dismissed_statuses(): return dismissed_statuses +def get_withdrawn_statuses(): + withdrawn_statuses = set() + for phase_name, phase in PHASES: + if phase.display_name == "Withdrawn": + withdrawn_statuses.add(phase_name) + return withdrawn_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 @@ -1487,6 +1792,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/core/context_processors.py b/hypha/core/context_processors.py index d1efdf6b6b..e79c2b6718 100644 --- a/hypha/core/context_processors.py +++ b/hypha/core/context_processors.py @@ -22,4 +22,5 @@ def global_vars(request): "SENTRY_DEBUG": settings.SENTRY_DEBUG, "SENTRY_PUBLIC_KEY": settings.SENTRY_PUBLIC_KEY, "SUBMISSIONS_TABLE_EXCLUDED_FIELDS": settings.SUBMISSIONS_TABLE_EXCLUDED_FIELDS, + "ENABLE_SUBMISSION_WITHDRAWAL": settings.ENABLE_SUBMISSION_WITHDRAWAL, } diff --git a/hypha/settings/base.py b/hypha/settings/base.py index 37efac3224..b2a2419b82 100644 --- a/hypha/settings/base.py +++ b/hypha/settings/base.py @@ -162,6 +162,9 @@ # Require an applicant to view their rendered application before submitting SUBMISSION_PREVIEW_REQUIRED = env.bool("SUBMISSION_PREVIEW_REQUIRED", True) +# Allow Withdrawing of Submissions +ENABLE_SUBMISSION_WITHDRAWAL = env.bool("ENABLE_SUBMISSION_WITHDRAWAL", False) + # Project settings. # SECRET_KEY is required