Skip to content

Commit

Permalink
Add server side previews to the application workflow (#3725)
Browse files Browse the repository at this point in the history
## Description

Closes #3545.

This creates the option to require a user to preview their form before
submitting it. The requirement is set on a per fund/lab basis, and is a
~required checkbox when the lab/fund is configured in Wagtail admin~
global Django configuration option. The preview first saves the
application as a draft, then presents the user with a preview of their
application. Upon viewing, the user can either edit or submit as seen in
the flow diagram below.
  • Loading branch information
wes-otf authored Jan 29, 2024
1 parent 079e015 commit ad48694
Show file tree
Hide file tree
Showing 8 changed files with 181 additions and 68 deletions.
4 changes: 4 additions & 0 deletions docs/setup/administrators/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ This determines the length of time for which the user will remain logged in. The

FORCE_LOGIN_FOR_APPLICATION = env.bool('FORCE_LOGIN_FOR_APPLICATION', True)

### If applicants should be forced to preview their application before submitting

SUBMISSION_PREVIEW_REQUIRED = env.bool('SUBMISSION_PREVIEW_REQUIRED', True)

### Set the allowed file extension for all uploads fields.

FILE_ALLOWED_EXTENSIONS = ['doc', 'docx', 'odp', 'ods', 'odt', 'pdf', 'ppt', 'pptx', 'rtf', 'txt', 'xls', 'xlsx']
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,14 @@ <h2 class="font-light flex-1">{% trans "My active submissions" %}</h2>
<div class="wrapper wrapper--status-bar-inner">
<div class="mt-5 ml-4 lg:max-w-[30%]">
<h4 class="heading mb-2 font-bold line-clamp-3 hover:line-clamp-none"><a class="link" href="{% url 'funds:submissions:detail' submission.id %}">{{ submission.title }}</a></h4>
<p class="m-0 text-gray-400">{% trans "Submitted on " %} {{ submission.submit_time.date }} {% trans "by" %} {{ submission.user.get_full_name }}</p>
<p class="m-0 text-gray-400">
{% if submission.is_draft %}
{% trans "Drafted on " %}
{% else %}
{% trans "Submitted on " %}
{% endif %}
{{ submission.submit_time.date }} {% trans "by" %} {{ submission.user.get_full_name }}
</p>
</div>
{% status_bar submission.workflow submission.phase request.user css_class="status-bar--small" %}
</div>
Expand Down
30 changes: 28 additions & 2 deletions hypha/apply/funds/models/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,8 @@ def serve(self, request, *args, **kwargs):
# Overriding serve method to pass submission id to get_form method
copy_open_submission = request.GET.get("open_call_submission")
if request.method == "POST":
draft = request.POST.get("draft", False)
preview = "preview" in request.POST
draft = request.POST.get("draft", preview)
form = self.get_form(
request.POST,
request.FILES,
Expand All @@ -472,6 +473,17 @@ def serve(self, request, *args, **kwargs):
# 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:
context = self.get_context(request)
context["object"] = form_submission
context["form"] = form
return render(
request, "funds/application_preview.html", context
)

return self.render_landing_page(
request, form_submission, *args, **kwargs
)
Expand All @@ -483,6 +495,8 @@ def serve(self, request, *args, **kwargs):
context = self.get_context(request)
context["form"] = form
context["show_all_group_fields"] = True if copy_open_submission else False
# Check if a preview is required before submitting the application
context["require_preview"] = settings.SUBMISSION_PREVIEW_REQUIRED
return render(request, self.get_template(request), context)

# We hide the round as only the open round is used which is displayed through the
Expand Down Expand Up @@ -592,14 +606,24 @@ def serve(self, request, *args, **kwargs):
)

if request.method == "POST":
draft = request.POST.get("draft", False)
preview = "preview" in request.POST
draft = request.POST.get("draft", preview)
form = self.get_form(
request.POST, request.FILES, page=self, user=request.user, draft=draft
)
if form.is_valid():
form_submission = SubmittableStreamForm.process_form_submission(
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:
context = self.get_context(request)
context["object"] = form_submission
context["form"] = form
return render(request, "funds/application_preview.html", context)

return self.render_landing_page(
request, form_submission, *args, **kwargs
)
Expand All @@ -608,6 +632,8 @@ def serve(self, request, *args, **kwargs):

context = self.get_context(request)
context["form"] = form
# Check if a preview is required before submitting the application
context["require_preview"] = settings.SUBMISSION_PREVIEW_REQUIRED
return TemplateResponse(request, self.get_template(request), context)


Expand Down
134 changes: 72 additions & 62 deletions hypha/apply/funds/templates/funds/application_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,75 +4,85 @@

{% block content %}

<div class="wrapper wrapper--medium wrapper--form">
<div class="wrapper wrapper--light-grey-bg wrapper--form wrapper--sidebar">
<div class="wrapper--sidebar--inner">

{% if page.end_date and page.get_parent.specific.show_deadline %}
<p>
{% heroicon_mini "calendar-days" aria_hidden="true" class="inline mr-1 fill-fg-muted" %}
<span class="font-bold text-fg-muted">{% trans "Next deadline" %}: {{ page.end_date }}</span>
</p>
{% endif %}
<h1 class="text-5xl font-bold">{{ page.title }}</h1>
{% if form.errors or form.non_field_errors %}
<div class="wrapper wrapper--error">
{% heroicon_solid "exclamation-triangle" aria_hidden="true" class="inline mr-2 fill-red-500" %}
{% if form.non_field_errors %}
<ul>
{% for error in form.non_field_errors %}
<li class="error">{{ error }}</li>
{% endfor %}
</ul>
{% else %}
<h5 class="heading heading--no-margin heading--regular">{% trans "There were some errors with your form. Please amend the fields highlighted below" %}</h5>
{% endif %}
</div>
{% endif %}

{% if not page.open_round and not page.start_date and not request.is_preview %}
{# the page has no open rounds and we arent on a round page #}
<h3>{% blocktrans %}Sorry this {{ page|verbose_name }} is not accepting applications at the moment{% endblocktrans %}</h3>
{% else%}
{% if page.get_parent.specific.guide_link %}
<a href="{{ page.get_parent.specific.guide_link }}" class="link link--fixed-apply" target="_blank" rel="noopener noreferrer">
{% trans "Application guide" %}
</a>
{% if page.end_date and page.get_parent.specific.show_deadline %}
<p>
{% heroicon_mini "calendar-days" aria_hidden="true" class="inline mr-1 fill-fg-muted" %}
<span class="font-bold text-fg-muted">{% trans "Next deadline" %}: {{ page.end_date }}</span>
</p>
{% endif %}
<form class="form application-form" action="/test500/" method="POST" enctype="multipart/form-data">
{{ form.media }}
{% csrf_token %}

{% for field in form %}
{% if field.field %}
{% if field.field.multi_input_field %}
{% include "forms/includes/multi_input_field.html" with is_application=True %}
{% else %}
{% include "forms/includes/field.html" with is_application=True %}
{% endif %}
<h1 class="text-5xl font-bold">{{ page.title }}</h1>
{% if form.errors or form.non_field_errors %}
<div class="wrapper wrapper--error">
{% heroicon_solid "exclamation-triangle" aria_hidden="true" class="inline mr-2 fill-red-500" %}
{% if form.non_field_errors %}
<ul>
{% for error in form.non_field_errors %}
<li class="error">{{ error }}</li>
{% endfor %}
</ul>
{% else %}
{% if field.group_number > 1 %}
<div class="field-group-{{ field.group_number }}">
{{ field.block }}
</div>
<h5 class="heading heading--no-margin heading--regular">{% trans "There were some errors with your form. Please amend the fields highlighted below" %}</h5>
{% endif %}
</div>
{% endif %}

{% if not page.open_round and not page.start_date and not request.is_preview %}
{# the page has no open rounds and we arent on a round page #}
<h3>{% blocktrans %}Sorry this {{ page|verbose_name }} is not accepting applications at the moment{% endblocktrans %}</h3>
{% else%}
{% if page.get_parent.specific.guide_link %}
<a href="{{ page.get_parent.specific.guide_link }}" class="link link--fixed-apply" target="_blank" rel="noopener noreferrer">
{% trans "Application guide" %}
</a>
{% endif %}
<form class="form application-form" action="/test500/" method="POST" enctype="multipart/form-data">
{{ form.media }}
{% csrf_token %}

{% for field in form %}
{% if field.field %}
{% if field.field.multi_input_field %}
{% include "forms/includes/multi_input_field.html" with is_application=True %}
{% else %}
{% include "forms/includes/field.html" with is_application=True %}
{% endif %}
{% else %}
<div class="field-block prose max-w-none">
{{ field.block }}
</div>
{% if field.group_number > 1 %}
<div class="field-group-{{ field.group_number }}">
{{ field.block }}
</div>
{% else %}
<div class="field-block prose max-w-none">
{{ field.block }}
</div>
{% endif %}
{% endif %}
{% endif %}
{% endfor %}
{% endfor %}

{# Hidden fields needed e.g. for django-file-form. See `StreamBaseForm.hidden_fields` #}
{% for hidden_field in form.hidden_fields %}
{{ hidden_field }}
{% 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 class="form__group">
<button class="button button--primary" type="submit" disabled>{% trans "Submit for review" %}</button>
<button class="button button--secondary" type="submit" name="draft" value="Save draft" formnovalidate>{% trans "Save draft" %}</button>
</div>
</form>
<p class="wrapper--error message-no-js js-hidden">{% trans "You must have Javascript enabled to use this form." %}</p>
{% endif %}
<div class="form__group">
{# If a preview is required for this application, don't allow submitting yet (via name="preview"). At the moment, this functionality only works if a user is logged in. #}
{% if require_preview and request.user.is_authenticated %}
<button class="button button--submit button--primary" type="submit" name="preview" disabled>{% trans "Preview and submit" %}</button>
{% else %}
<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>
{% 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>
</form>
<p class="wrapper--error message-no-js js-hidden">{% trans "You must have Javascript enabled to use this form." %}</p>
{% endif %}
</div>
</div>
{% endblock %}

Expand Down
44 changes: 44 additions & 0 deletions hypha/apply/funds/templates/funds/application_preview.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{% extends "base-apply.html" %}
{% load wagtailcore_tags static i18n util_tags heroicons %}
{% block title %}{% trans "Previewing" %}: {{object.title }}{% endblock %}
{% block body_class %}bg-white{% endblock %}

{% block content %}

{% adminbar %}
{% slot header %}{% trans "Previewing" %}: {{ object.title }}{% endslot %}
{% endadminbar %}

<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">
{% csrf_token %}

<div class="preview-hidden-form" hidden>
{% for field in form %}
{% if field.field %}
{% if field.field.multi_input_field %}
{% include "forms/includes/multi_input_field.html" with is_application=True %}
{% else %}
{% include "forms/includes/field.html" with is_application=True %}
{% endif %}
{% else %}
{{ field.block }}
{% endif %}
{% 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 %}">
{% csrf_token %}
</form>

<div class="form__group">
<button class="button button--primary" form="preview-form-submit" name="submit" type="submit">{% trans "Submit for review" %}</button>
<button class="button button--secondary" form="preview-form-edit">{% trans "Edit" %}</button>
</div>

</div>
{% endblock %}
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,14 @@ <h5>{% blocktrans with stage=object.previous.stage %}Your {{ stage }} applicatio
{% else %}
<article class="wrapper--sidebar--inner">
<header class="heading heading--submission-meta heading-text zeta">
<span>{% trans "Submitted" %} <strong><relative-time datetime={{ object.submit_time|date:"c" }}>{{ object.submit_time|date:"SHORT_DATETIME_FORMAT" }}</relative-time></strong> {% trans "by" %} <strong>{{ object.user.get_full_name }}</strong></span>
<span>
{% if object.is_draft %}
{% trans "Drafted " %}
{% else %}
{% trans "Submitted " %}
{% endif %}
<strong><relative-time datetime={{ object.submit_time|date:"c" }}>{{ object.submit_time|date:"SHORT_DATETIME_FORMAT" }}</relative-time></strong> {% trans "by" %} <strong>{{ object.user.get_full_name }}</strong>
</span>
<span>{% trans "Updated" %} <strong><relative-time datetime={{ object.live_revision.timestamp|date:"c" }}>{{ object.live_revision.timestamp|date:"SHORT_DATETIME_FORMAT" }}</relative-time></strong> {% trans "by" %} <strong>{{ object.user.get_full_name }}</strong></span>
<div class="wrapper wrapper--submission-actions">
{% if perms.funds.delete_applicationsubmission %}
Expand Down
16 changes: 14 additions & 2 deletions hypha/apply/funds/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1305,8 +1305,13 @@ def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

def buttons(self):
yield ("submit", "primary", _("Submit"))
yield ("save", "white", _("Save draft"))
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"))
yield ("preview", "white", _("Preview"))

def get_form_kwargs(self):
"""
Expand Down Expand Up @@ -1439,6 +1444,13 @@ def form_valid(self, form):
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)

Expand Down
3 changes: 3 additions & 0 deletions hypha/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@
# Default visibility for reviews.
REVIEW_VISIBILITY_DEFAULT = env.str("REVIEW_VISIBILITY_DEFAULT", "private")

# Require an applicant to view their rendered application before submitting
SUBMISSION_PREVIEW_REQUIRED = env.bool("SUBMISSION_PREVIEW_REQUIRED", True)

# Project settings.

Expand Down Expand Up @@ -562,6 +564,7 @@
# Finance extension to finance2 for Project Invoicing
INVOICE_EXTENDED_WORKFLOW = env.bool("INVOICE_EXTENDED_WORKFLOW", True)


# Misc settings

# Use Pillow to create QR codes so they are PNG and not SVG.
Expand Down

0 comments on commit ad48694

Please sign in to comment.