Skip to content

Commit

Permalink
Feature/feedback form (#1401)
Browse files Browse the repository at this point in the history
* Added initial feedback form

* Added management command to write csv to bucket. Added tests

* Fixed black error

* changed parameter to weeks

* Fixed references to num_weeks in command

* Bumped dependencies

* Removed unneeded code

* Feedback on feedback code

* Ran migration
  • Loading branch information
jamesstottmoj authored Dec 10, 2024
1 parent bb318f1 commit 9715598
Show file tree
Hide file tree
Showing 21 changed files with 395 additions and 23 deletions.
7 changes: 6 additions & 1 deletion controlpanel/api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from simple_history.admin import SimpleHistoryAdmin

# First-party/Local
from controlpanel.api.models import App, IPAllowlist, S3Bucket, User
from controlpanel.api.models import App, Feedback, IPAllowlist, S3Bucket, User


def make_migration_pending(modeladmin, request, queryset):
Expand Down Expand Up @@ -58,7 +58,12 @@ class IPAllowlistAdmin(SimpleHistoryAdmin):
history_list_display = ("description", "contact", "allowed_ip_ranges")


class FeedbackAdmin(admin.ModelAdmin):
list_display = ("satisfaction_rating", "suggestions", "date_added")


admin.site.register(App, AppAdmin)
admin.site.register(S3Bucket, S3Admin)
admin.site.register(User, UserAdmin)
admin.site.register(IPAllowlist, IPAllowlistAdmin)
admin.site.register(Feedback, FeedbackAdmin)
7 changes: 5 additions & 2 deletions controlpanel/api/aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -656,8 +656,7 @@ def create(self, bucket_name, is_data_warehouse=False):
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html?highlight=s3#S3.BucketVersioning # noqa: E501
versioning = bucket.Versioning()
versioning.enable()
# Set bucket lifecycle. Send non-current versions of files to glacier
# storage after 30 days.
# Set bucket lifecycle. Set to intelligent tiering
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html#S3.Client.put_bucket_lifecycle_configuration # noqa: E501
self.apply_lifecycle_config(bucket_name, s3_client)
if is_data_warehouse:
Expand Down Expand Up @@ -800,6 +799,10 @@ def exists(self, bucket_name):

return True

def write_to_bucket(self, bucket_name, key, data):
s3_client = self.boto3_session.client("s3")
s3_client.put_object(Bucket=bucket_name, Key=key, Body=data)


class AWSPolicy(AWSService):
def create_policy(self, name, path, policy_document=None):
Expand Down
43 changes: 43 additions & 0 deletions controlpanel/api/migrations/0048_feedback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Generated by Django 5.1.2 on 2024-12-03 10:35

# Third-party
import django.utils.timezone
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("api", "0047_app_cloud_platform_role_arn"),
]

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"),
]
),
),
("suggestions", models.TextField()),
("date_added", models.DateTimeField(default=django.utils.timezone.now)),
],
options={
"db_table": "control_panel_api_feedback",
},
),
]
19 changes: 19 additions & 0 deletions controlpanel/api/migrations/0049_alter_feedback_suggestions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 5.1.2 on 2024-12-10 15:29

# Third-party
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("api", "0048_feedback"),
]

operations = [
migrations.AlterField(
model_name="feedback",
name="suggestions",
field=models.TextField(blank=True),
),
]
1 change: 1 addition & 0 deletions controlpanel/api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from controlpanel.api.models.app import App
from controlpanel.api.models.app_ip_allowlist import AppIPAllowList
from controlpanel.api.models.apps3bucket import AppS3Bucket
from controlpanel.api.models.feedback import Feedback
from controlpanel.api.models.iam_managed_policy import IAMManagedPolicy
from controlpanel.api.models.parameter import Parameter
from controlpanel.api.models.policys3bucket import PolicyS3Bucket
Expand Down
25 changes: 25 additions & 0 deletions controlpanel/api/models/feedback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Third-party
from django.db import models
from django.utils import timezone


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,
null=False,
blank=False,
)

suggestions = models.TextField(blank=True)
date_added = models.DateTimeField(default=timezone.now)

class Meta:
db_table = "control_panel_api_feedback"
65 changes: 65 additions & 0 deletions controlpanel/cli/management/commands/feedback_csv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Standard library
import csv
from datetime import datetime, timedelta
from io import StringIO

# Third-party
from django.conf import settings
from django.core.management.base import BaseCommand

# First-party/Local
from controlpanel.api.aws import AWSBucket
from controlpanel.api.models import Feedback


class Command(BaseCommand):
help = "Writes a csv file with the feedback data to an S3 Bucket"
csv_headings = ["Satisfaction Rating", "Suggestions", "Date Added"]

def add_arguments(self, parser):
parser.add_argument(
"--weeks",
"-w",
type=int,
default=2,
help="Get feedback over an x week period from today's date",
)
parser.add_argument("--all", "-a", action="store_true", help="Get all feedback received")

def handle(self, *args, **options):
today = datetime.today()

if options["all"]:
feedback_items = Feedback.objects.all()
else:
self.stdout.write(f"weeks: {options['weeks']}")
timeframe = today - timedelta(weeks=options["weeks"])
feedback_items = Feedback.objects.filter(date_added__gte=timeframe)

if not feedback_items:
self.stdout.write(f"No feedback found for the past {options['weeks']} weeks")
return

filename = f"feedback_{today}.csv"
csv_buffer = StringIO()
writer = csv.writer(csv_buffer, delimiter=",", quotechar="|", quoting=csv.QUOTE_MINIMAL)
writer.writerow(self.csv_headings)
for feedback in feedback_items:
row = [
feedback.get_satisfaction_rating_display(),
feedback.suggestions,
feedback.date_added.date(),
]
writer.writerow(row)

try:
csv_value = csv_buffer.getvalue()
bucket = AWSBucket()

if not bucket.exists(settings.FEEDBACK_BUCKET_NAME):
bucket.create(settings.FEEDBACK_BUCKET_NAME)

bucket.write_to_bucket(settings.FEEDBACK_BUCKET_NAME, filename, csv_value)
self.stdout.write(f"Feedback data written to {settings.FEEDBACK_BUCKET_NAME}")
except Exception as e:
self.stdout.write(f"Failed to write to S3 bucket: {e}")
10 changes: 10 additions & 0 deletions controlpanel/frontend/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from controlpanel.api.models import (
QUICKSIGHT_EMBED_PERMISSION,
App,
Feedback,
S3Bucket,
Tool,
User,
Expand Down Expand Up @@ -655,3 +656,12 @@ def grant_access(self):
self.user.user_permissions.add(permission)
else:
self.user.user_permissions.remove(permission)


class FeedbackForm(forms.ModelForm):
class Meta:
model = Feedback
fields = [
"satisfaction_rating",
"suggestions",
]
8 changes: 8 additions & 0 deletions controlpanel/frontend/jinja2/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@
{% endblock %}

{% block beforeContent %}
<div class="govuk-phase-banner govuk-width-container">
<p class="govuk-phase-banner__content">
<span class="govuk-phase-banner__text">
Provide your <a class="govuk-link" href="{{url("feedback-create")}}">Feedback</a> to help us improve this service.
</span>
</p>
</div>

{% if not hide_nav and request.user.is_authenticated %}
{{ mojPrimaryNavigation({
"label": "Primary navigation",
Expand Down
80 changes: 80 additions & 0 deletions controlpanel/frontend/jinja2/feedback-create.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{% from "error-message/macro.html" import govukErrorMessage %}
{% from "label/macro.html" import govukLabel %}
{% from "radios/macro.html" import govukRadios %}
{% from "includes/auth0-connections-form.html" import auth0_connections_form with context %}


{% extends "base.html" %}

{% set page_title = "Feedback" %}

{% block content %}
<h1 class="govuk-heading-xl">Give feedback on the Analytical Platform</h1>


<form method="post" id="feedback" action="{{ url("feedback-create") }}">
{{ csrf_input }}

{{ govukRadios({
"name": "satisfaction_rating",
"fieldset": {
"legend": {
"text": "Satisfaction survey",
"classes": "govuk-fieldset__legend--l",
},
},
"items": [
{
"value": 5,
"text": "Very satisfied",
"checked": form.satisfaction_rating.value() == "5"
},
{
"value": 4,
"text": "Satisfied",
"checked": form.satisfaction_rating.value() == "4"
},
{
"value": 3,
"text": "Neither satisfied or dissatisfied",
"checked": form.satisfaction_rating.value() == "3"
},
{
"value": 2,
"text": "Dissatisfied",
"checked": form.satisfaction_rating.value() == "2"
},
{
"value": 1,
"text": "Very dissatisfied",
"checked": form.satisfaction_rating.value() == "1"
},
],
"errorMessage": { "text": form.errors.get("satisfaction_rating") } if form.errors.get("satisfaction_rating") else {}
}) }}



<div class="govuk-form-group {%- if form.errors.get("suggestions") %} govuk-form-group--error{% endif %}">
<h2 class="govuk-label-wrapper">
<label class="govuk-label govuk-label--l" for="{{form.suggestions.id_for_label}}">
How can we improve this service?
</label>
</h2>
<div id="more-detail-hint" class="govuk-hint">
Do not include personal or financial information, like your National Insurance number or credit card details.
</div>

{% if form.errors.get("suggestions") %}
{{ govukErrorMessage({"text": form.errors.get("suggestions")}) }}
{% endif %}

<textarea class="govuk-textarea" id="{{form.suggestions.id_for_label}}" name="{{form.suggestions.html_name}}" rows="5" aria-describedby="more-detail-hint"></textarea>
</div>


<div class="govuk-form-group">
<button class="govuk-button">Send feedback</button>
</div>
</form>
{% endblock %}
12 changes: 12 additions & 0 deletions controlpanel/frontend/jinja2/feedback-thanks.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{% from "error-message/macro.html" import govukErrorMessage %}
{% from "includes/auth0-connections-form.html" import auth0_connections_form with context %}


{% extends "base.html" %}

{% set page_title = "Thank you" %}

{% block content %}
<h1 class="govuk-heading-xl">Thank you for your feedback</h1>
<p class="govuk-body">Your feedback will help us improve the service.</p>
{% endblock %}
35 changes: 35 additions & 0 deletions controlpanel/frontend/mixins.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Standard library
import csv
from datetime import datetime

# Third-party
from django.contrib import messages
from django.http import HttpResponse
from rules.contrib.views import PermissionRequiredMixin

# First-party/Local
Expand Down Expand Up @@ -28,3 +33,33 @@ def form_valid(self, form):
def get_success_url(self):
messages.success(self.request, self.success_message)
return super().get_success_url()


class CsvWriterMixin(OIDCLoginRequiredMixin, PermissionRequiredMixin):
"""
Allows exporting a list of models to a CSV file.
"""

http_method_names = ["post"]
permission_required = "api.is_superuser"
filename = ""
csv_headings = []
model_attributes = []

def write_csv(self, models):
timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")

response = HttpResponse(
content_type="text/csv",
headers={
"Content-Disposition": f'attachment; filename="{self.filename}_{timestamp}.csv"'
},
)

writer = csv.writer(response)
writer.writerow(self.csv_headings)
for model in models:
row = [model[attribute] for attribute in self.model_attributes]
writer.writerow(row)

return response
2 changes: 2 additions & 0 deletions controlpanel/frontend/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,4 +283,6 @@
),
path("parameters/<int:pk>/delete/", views.ParameterDelete.as_view(), name="delete-parameter"),
path("quicksight/", views.QuicksightView.as_view(), name="quicksight"),
path("feedback/", views.CreateFeedback.as_view(), name="feedback-create"),
path("feedback/thanks", views.FeedbackThanks.as_view(), name="feedback-thanks"),
]
1 change: 1 addition & 0 deletions controlpanel/frontend/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
UpdateIAMManagedPolicyAccessLevel,
WebappBucketList,
)
from controlpanel.frontend.views.feedback import CreateFeedback, FeedbackThanks
from controlpanel.frontend.views.help import Help
from controlpanel.frontend.views.ip_allowlist import (
IPAllowlistCreate,
Expand Down
Loading

0 comments on commit 9715598

Please sign in to comment.