Skip to content

Commit

Permalink
Feedback survey (#669)
Browse files Browse the repository at this point in the history
* feat: add feedback survey

This follows the pattern set by GOV.UK but stores
responses in the database. We are building our own because we're in
private beta this is an internal service (not on GOV.UK)

The form is linked from the MOJ internal header, rather than the
phase banner where we would put it on an external service.

* fix: make sure RadioSelect doesn't have a blank choice

* fix: handle form errors in the template

* feat: add command to export survey data

* fix: add env var for CSRF_TRUSTED_ORIGINS
  • Loading branch information
MatMoore authored Aug 14, 2024
1 parent cb19bbf commit d98514f
Show file tree
Hide file tree
Showing 24 changed files with 361 additions and 1,578 deletions.
1 change: 1 addition & 0 deletions .github/workflows/reusable-build-and-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ jobs:
CATALOGUE_URL: ${{ vars.CATALOGUE_URL }}
DEBUG: ${{ vars.DEBUG }}
DJANGO_ALLOWED_HOSTS: ${{ vars.DJANGO_ALLOWED_HOSTS }}
CSRF_TRUSTED_ORIGINS: ${{ vars.CSRF_TRUSTED_ORIGINS }}
DJANGO_LOG_LEVEL: ${{ vars.DJANGO_LOG_LEVEL }}
SENTRY_DSN_WORKAROUND: ${{ vars.SENTRY_DSN_WORKAROUND }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
Expand Down
3 changes: 3 additions & 0 deletions core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"django.contrib.staticfiles",
"django.contrib.humanize",
"home.apps.HomeConfig",
"feedback.apps.FeedbackConfig",
"django_prometheus",
"users",
"waffle",
Expand Down Expand Up @@ -254,3 +255,5 @@
USE_I18N = True
LANGUAGE_CODE = "en"
LOCALE_PATHS = [BASE_DIR / "locale"]

CSRF_TRUSTED_ORIGINS = os.environ.get("CSRF_TRUSTED_ORIGINS", "").split(" ")
2 changes: 2 additions & 0 deletions core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@
from django.contrib import admin
from django.urls import include, path

app_name = "core"

urlpatterns = [
path("admin/", view=admin.site.urls),
path("azure_auth/", include("azure_auth.urls", namespace="azure_auth")),
path("feedback/", include("feedback.urls", namespace="feedback")),
path("", include("home.urls", namespace="home")),
path("", include("django_prometheus.urls")),
]
2 changes: 2 additions & 0 deletions deployments/templates/deployment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ spec:
value: "$AZURE_REDIRECT_URI"
- name: AZURE_AUTHORITY
value: "$AZURE_AUTHORITY"
- name: CSRF_TRUSTED_ORIGINS
value: "${CSRF_TRUSTED_ORIGINS}"
- name: SECRET_KEY
valueFrom:
secretKeyRef:
Expand Down
Empty file added feedback/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions feedback/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.contrib import admin

# Register your models here.
6 changes: 6 additions & 0 deletions feedback/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class FeedbackConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "feedback"
25 changes: 25 additions & 0 deletions feedback/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from django.forms import ModelForm
from django.forms.widgets import RadioSelect

from .models import Feedback


def formfield(field, **kwargs):
"""
Wrapper around the db model's formfield method that maps db fields to form fields.
This is a workaround to prevent a blank choice being included in the ChoiceField.
By default, if the model field has no default value (or blank is set to True),
then it will include this empty value, even if we have customised the widget is customised.
"""
return field.formfield(initial=None, **kwargs)


class FeedbackForm(ModelForm):
class Meta:
model = Feedback
fields = ["satisfaction_rating", "how_can_we_improve"]
widgets = {
"satisfaction_rating": RadioSelect(attrs={"class": "govuk-radios__input"})
}
formfield_callback = formfield
Empty file added feedback/management/__init__.py
Empty file.
Empty file.
17 changes: 17 additions & 0 deletions feedback/management/commands/export_feedback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import csv
from sys import stdout

from django.core.management.base import BaseCommand

from feedback.models import Feedback


class Command(BaseCommand):
help = "Export feedback survey data"

def handle(self, *args, **options):
writer = csv.writer(stdout)
writer.writerow(["satisfaction_rating", "how_can_we_improve"])

for feedback in Feedback.objects.all():
writer.writerow([feedback.satisfaction_rating, feedback.how_can_we_improve])
44 changes: 44 additions & 0 deletions feedback/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Generated by Django 5.0.7 on 2024-08-13 11:31

from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = []

operations = [
migrations.CreateModel(
name="Feedback",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"satisfaction_rating",
models.IntegerField(
choices=[
(5, "Very satisfied"),
(4, "Satisfied"),
(3, "Neither satisfied or dissatisfied"),
(2, "Dissatisfied"),
(1, "Very dissatisfied"),
],
verbose_name="Satisfaction survey",
),
),
(
"how_can_we_improve",
models.TextField(verbose_name="How can we improve this service?"),
),
],
),
]
Empty file added feedback/migrations/__init__.py
Empty file.
23 changes: 23 additions & 0 deletions feedback/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from django.db import models
from django.utils.translation import gettext as _


class Feedback(models.Model):
SATISFACTION_RATINGS = [
(5, "Very satisfied"),
(4, "Satisfied"),
(3, "Neither satisfied or dissatisfied"),
(2, "Dissatisfied"),
(1, "Very dissatisfied"),
]

satisfaction_rating = models.IntegerField(
choices=SATISFACTION_RATINGS,
verbose_name=_("Satisfaction survey"),
null=False,
blank=False,
)

how_can_we_improve = models.TextField(
verbose_name=_("How can we improve this service?"), null=False, blank=True
)
69 changes: 69 additions & 0 deletions feedback/templates/feedback.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{% extends "base/base.html" %}
{% load static %}
{% load i18n %}

{% block content %}
<div class="govuk-grid-row">
<div class="govuk-grid-column-full">
<h1 class="govuk-heading-xl">{{h1_value}}</h1>
</div>

<div class="govuk-grid-column-two-thirds">
<form action="{% url 'feedback:feedback' %}" method="post" novalidate>

{% if form.errors %}
<div class="govuk-error-summary" data-module="govuk-error-summary">
<div role="alert">
<h2 class="govuk-error-summary__title">
{% translate "There is a problem" %}
</h2>
<div class="govuk-error-summary__body">
<p class="govuk-body">{% translate 'Make sure you have filled in all the fields.' %}</p>
</div>
</div>
</div>
{% endif %}

<div class="govuk-form-group {% if form.satisfaction_rating.errors %}govuk-form-group-errors{% endif %}">
<fieldset class="govuk-fieldset">
<legend class="govuk-fieldset__legend govuk-fieldset__legend--l">
<h2 class="govuk-fieldset__heading">
{{form.satisfaction_rating.label}}
</h2>
</legend>
{% for error in form.satisfaction_rating.errors %}
<p id="passport-issued-error" class="govuk-error-message">
<span class="govuk-visually-hidden">Error:</span> {{error}}
</p>
{% endfor %}
<div class="govuk-radios" data-module="govuk-radios">
{% for radio in form.satisfaction_rating %}
<div class="govuk-radios__item">
{{radio.tag}}
<label class="govuk-label govuk-radios__label" for="{{radio.id_for_label}}">
{{radio.choice_label}}
</label>
</div>
{% endfor %}
</div>
</fieldset>
</div>
<div class="govuk-form-group">
<h2 class="govuk-label-wrapper">
<label class="govuk-label govuk-label--l" for="{{form.how_can_we_improve.id_for_label}}">
{{form.how_can_we_improve.label}}
</label>
</h2>
<div id="more-detail-hint" class="govuk-hint">
{% translate 'Do not include personal or financial information, like your National Insurance number or credit card details.' %}
</div>
<textarea class="govuk-textarea" id="{{form.how_can_we_improve.id_for_label}}" name="{{form.how_can_we_improve.html_name}}" rows="5" aria-describedby="more-detail-hint"></textarea>
</div>
<button type="submit" class="govuk-button" data-module="govuk-button">
Send feedback
</button>
{% csrf_token %}
</form>
</div>
</div>
{% endblock content %}
16 changes: 16 additions & 0 deletions feedback/templates/thanks.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{% extends "base/base.html" %}
{% load i18n %}
{% load static %}

{% block content %}
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<h1 class="govuk-heading-xl">
{% translate 'Thank you for your feedback' %}
</h1>

<p class="govuk-body">{% translate 'Your feedback will help us improve the service.' %}</p>
</div>

</div>
{% endblock content %}
3 changes: 3 additions & 0 deletions feedback/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.test import TestCase

# Create your tests here.
10 changes: 10 additions & 0 deletions feedback/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django.urls import path

from . import views

app_name = "feedback"

urlpatterns = [
path("", views.feedback_form_view, name="feedback"),
path("thanks", views.thank_you_view, name="thanks"),
]
38 changes: 38 additions & 0 deletions feedback/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import logging

from django.http import HttpResponse
from django.shortcuts import redirect, render
from django.utils.translation import gettext as _

from .forms import FeedbackForm

log = logging.getLogger(__name__)


def feedback_form_view(request) -> HttpResponse:
if request.method == "POST":
form = FeedbackForm(request.POST)
if form.is_valid():
form.save()
return redirect("feedback:thanks")
else:
log.error(f"Unexpected invalid feedback form submission: {form.errors}")
else:
form = FeedbackForm()

return render(
request,
"feedback.html",
{
"h1_value": _("Give feedback on Find MOJ data"),
"form": form,
},
)


def thank_you_view(request) -> HttpResponse:
return render(
request,
"thanks.html",
{"h1_value": _("Thank you for your feedback")},
)
Loading

0 comments on commit d98514f

Please sign in to comment.