Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/feedback form #1401

Merged
merged 14 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",
},
),
]
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()
jamesstottmoj marked this conversation as resolved.
Show resolved Hide resolved
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
22 changes: 6 additions & 16 deletions controlpanel/frontend/views/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
RemoveCustomerByEmailForm,
UpdateAppAuth0ConnectionsForm,
)
from controlpanel.frontend.mixins import PolicyAccessMixin
from controlpanel.frontend.mixins import CsvWriterMixin, PolicyAccessMixin
from controlpanel.frontend.views.apps_mng import AppManager
from controlpanel.oidc import OIDCLoginRequiredMixin

Expand Down Expand Up @@ -78,8 +78,10 @@ def get_queryset(self):
)


class AppAdminCSV(OIDCLoginRequiredMixin, PermissionRequiredMixin, View):
permission_required = "api.is_superuser"
class AppAdminCSV(CsvWriterMixin, View):
filename = "app_admins"
csv_headings = ["App Name", "Admins", "Emails"]
model_attributes = ["name", "users", "emails"]

def post(self, request, *args, **kwargs):
apps = (
Expand All @@ -95,19 +97,7 @@ def post(self, request, *args, **kwargs):
.order_by("name")
)

timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")

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

writer = csv.writer(response)
writer.writerow(["App Name", "Admins", "Emails"])
for app in apps:
writer.writerow([app["name"], app["users"], app["emails"]])

return response
return self.write_csv(apps)


class AppDetail(OIDCLoginRequiredMixin, PermissionRequiredMixin, DetailView):
Expand Down
Loading
Loading