diff --git a/apps/feedback/__init__.py b/apps/feedback/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/feedback/admin.py b/apps/feedback/admin.py new file mode 100644 index 0000000..25646d3 --- /dev/null +++ b/apps/feedback/admin.py @@ -0,0 +1,18 @@ +from django.utils.translation import gettext_lazy as _ +from wagtail_modeladmin.options import ModelAdmin, modeladmin_register + +from apps.feedback.models import Feedback + + +class FeedbackAdmin(ModelAdmin): + model = Feedback + menu_label = _("Feedback") + menu_icon = "comment" + menu_order = 291 + add_to_settings_menu = False + exclude_from_explorer = False + list_display = ("answer", "feedback", "user_email", "date_asked", "status") + search_fields = ("user_email", "answer", "feedback", "status", "date_asked") + + +modeladmin_register(FeedbackAdmin) diff --git a/apps/feedback/apps.py b/apps/feedback/apps.py new file mode 100644 index 0000000..bd32c7e --- /dev/null +++ b/apps/feedback/apps.py @@ -0,0 +1,10 @@ +from django.apps import AppConfig + +from apps.core.utils import check_for_debug_settings_in_production + + +class FeedbackConfig(AppConfig): + name = "apps.feedback" + + def ready(self): + check_for_debug_settings_in_production() diff --git a/apps/feedback/forms.py b/apps/feedback/forms.py new file mode 100644 index 0000000..38b7fa6 --- /dev/null +++ b/apps/feedback/forms.py @@ -0,0 +1,45 @@ +from django import forms +from django.utils.translation import gettext_lazy as _ + +from apps.feedback.models.models import Feedback + + +class FeedbackForm(forms.ModelForm): + feedback = forms.CharField( + label=_("Have a question or comment? Leave it here!"), + help_text=_("We will forward this response to the expert."), + required=False, + ) + + grade = forms.IntegerField( + label=_("Do you find this article clearly explained and easy to follow?*"), + help_text=_("(0 = not clear, 5 = very clear)"), + required=True, + ) + reliability_grade = forms.IntegerField( + label=_("Do you find this AI-Helpdesk article reliable?*"), + help_text=_("(0 = not reliable, 5 = very reliable)"), + required=True, + ) + + user_name = forms.CharField( + label=_("What is your name?"), + required=False, + ) + user_email = forms.CharField( + label=_("Email"), + help_text=_("We will forward the expert's response when we receive it."), + required=False, + ) + user_age = forms.IntegerField(label=_("What is your age?"), required=False) + + class Meta: + model = Feedback + fields = [ + "feedback", + "grade", + "reliability_grade", + "user_name", + "user_email", + "user_age", + ] diff --git a/apps/feedback/migrations/0001_initial.py b/apps/feedback/migrations/0001_initial.py new file mode 100644 index 0000000..a0cc514 --- /dev/null +++ b/apps/feedback/migrations/0001_initial.py @@ -0,0 +1,132 @@ +# Generated by Django 4.2.11 on 2024-04-30 09:22 + +from django.db import migrations, models +import django.db.models.deletion +import wagtail.contrib.routable_page.models +import wagtail.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("cms", "0003_alter_answertag_tag"), + ("wagtailcore", "0089_log_entry_data_json_null_to_object"), + ] + + operations = [ + migrations.CreateModel( + name="FeedbackFormPage", + fields=[ + ( + "page_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="wagtailcore.page", + ), + ), + ("intro", wagtail.fields.RichTextField(verbose_name="Intro")), + ( + "thank_you_text", + wagtail.fields.RichTextField( + default="

Bedankt voor het stellen van je vraag. We nemen je vraag in behandeling en proberen zo snel mogelijk een expert te vinden die je vraag kan beantwoorden.

", + verbose_name="Tekst", + ), + ), + ], + options={ + "verbose_name": "Feedback formulier pagina", + "verbose_name_plural": "Feedback formulier pagina's", + }, + bases=( + wagtail.contrib.routable_page.models.RoutablePageMixin, + "wagtailcore.page", + ), + ), + migrations.CreateModel( + name="Feedback", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "feedback", + models.TextField( + help_text="We will forward this response to the expert.", + verbose_name="Have a question or comment? Leave it here!", + ), + ), + ( + "grade", + models.PositiveSmallIntegerField( + blank=True, + help_text="(0 = not clear, 5 = very clear)", + null=True, + verbose_name="Do you find this article clearly explained and easy to follow?", + ), + ), + ( + "reliability_grade", + models.PositiveSmallIntegerField( + help_text="(0 = not reliable, 5 = very reliable)", + verbose_name="Do you find this KlimaatHelpdesk article reliable?", + ), + ), + ( + "user_name", + models.TextField( + blank=True, null=True, verbose_name="What is your name?" + ), + ), + ( + "user_email", + models.EmailField( + blank=True, + help_text="We will forward the expert's response when we receive it.", + max_length=254, + null=True, + verbose_name="Email", + ), + ), + ( + "user_age", + models.PositiveSmallIntegerField( + blank=True, null=True, verbose_name="What is your age?" + ), + ), + ("feedback_by_ip", models.GenericIPAddressField(blank=True, null=True)), + ("date_asked", models.DateTimeField(auto_now_add=True)), + ( + "status", + models.IntegerField( + choices=[ + (0, "Undecided"), + (1, "Approved"), + (2, "Answered"), + (3, "Rejected"), + ], + default=0, + ), + ), + ( + "answer", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="cms.answer", + ), + ), + ], + ), + ] diff --git a/apps/feedback/migrations/0002_alter_feedback_grade_and_more.py b/apps/feedback/migrations/0002_alter_feedback_grade_and_more.py new file mode 100644 index 0000000..128bb79 --- /dev/null +++ b/apps/feedback/migrations/0002_alter_feedback_grade_and_more.py @@ -0,0 +1,52 @@ +# Generated by Django 4.2.11 on 2024-05-06 08:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("feedback", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="feedback", + name="grade", + field=models.PositiveSmallIntegerField( + blank=True, + help_text="(0 = not clear, 5 = very clear)", + verbose_name="Do you find this article clearly explained and easy to follow?", + ), + ), + migrations.AlterField( + model_name="feedback", + name="reliability_grade", + field=models.PositiveSmallIntegerField( + help_text="(0 = not reliable, 5 = very reliable)", + verbose_name="Do you find this AIHelpdesk article reliable?", + ), + ), + migrations.AlterField( + model_name="feedback", + name="user_age", + field=models.PositiveSmallIntegerField( + blank=True, verbose_name="What is your age?" + ), + ), + migrations.AlterField( + model_name="feedback", + name="user_email", + field=models.EmailField( + blank=True, + help_text="We will forward the expert's response when we receive it.", + max_length=254, + verbose_name="Email", + ), + ), + migrations.AlterField( + model_name="feedback", + name="user_name", + field=models.TextField(blank=True, verbose_name="What is your name?"), + ), + ] diff --git a/apps/feedback/migrations/0003_alter_feedback_user_age.py b/apps/feedback/migrations/0003_alter_feedback_user_age.py new file mode 100644 index 0000000..bacc93a --- /dev/null +++ b/apps/feedback/migrations/0003_alter_feedback_user_age.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.11 on 2024-05-06 08:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("feedback", "0002_alter_feedback_grade_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="feedback", + name="user_age", + field=models.PositiveSmallIntegerField( + blank=True, null=True, verbose_name="What is your age?" + ), + ), + ] diff --git a/apps/feedback/migrations/__init__.py b/apps/feedback/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/feedback/models/__init__.py b/apps/feedback/models/__init__.py new file mode 100644 index 0000000..327332a --- /dev/null +++ b/apps/feedback/models/__init__.py @@ -0,0 +1,2 @@ +from .models import * # noqa +from .pages import * # noqa diff --git a/apps/feedback/models/models.py b/apps/feedback/models/models.py new file mode 100644 index 0000000..df62bfb --- /dev/null +++ b/apps/feedback/models/models.py @@ -0,0 +1,83 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ +from wagtail.admin.panels import FieldPanel, MultiFieldPanel, HelpPanel, \ + TitleFieldPanel, InlinePanel + + +class Feedback(models.Model): + UNDECIDED = 0 + APPROVED = 1 + ANSWERED = 2 + REJECTED = 3 + + STATUS_CHOICES = ( + (UNDECIDED, _("Undecided")), + (APPROVED, _("Approved")), + (ANSWERED, _("Answered")), + (REJECTED, _("Rejected")), + ) + answer = models.ForeignKey( + to="cms.Answer", + on_delete=models.SET_NULL, + null=True, + ) + + feedback = models.TextField( + verbose_name=_("Have a question or comment? Leave it here!"), + help_text=_("We will forward this response to the expert."), + + ) + + grade = models.PositiveSmallIntegerField( + verbose_name=_( + "Do you find this article clearly explained and easy to follow?" + ), + help_text=_("(0 = not clear, 5 = very clear)"), blank=True + ) + reliability_grade = models.PositiveSmallIntegerField( + verbose_name=_("Do you find this AI-Helpdesk article reliable?"), + help_text=_("(0 = not reliable, 5 = very reliable)"), + ) + + user_name = models.TextField(verbose_name=_("What is your name?"), + blank=True) + user_email = models.EmailField( + verbose_name=_("Email"), + help_text=_("We will forward the expert's response when we receive it."), + blank=True, + ) + user_age = models.PositiveSmallIntegerField(verbose_name=_("What is your age?"), + null=True, blank=True) + + feedback_by_ip = models.GenericIPAddressField(null=True, blank=True) + date_asked = models.DateTimeField(auto_now_add=True) + status = models.IntegerField(choices=STATUS_CHOICES, default=UNDECIDED) + + panels = [ + MultiFieldPanel( + [ + FieldPanel("answer", read_only=True), + ], + heading=_("Answer"), + ), + MultiFieldPanel( + [ + FieldPanel("status"), + FieldPanel("feedback", read_only=True), + FieldPanel("grade", read_only=True), + FieldPanel("reliability_grade", read_only=True), + ], + heading=_("Feedback"), + ), + MultiFieldPanel( + [ + + FieldPanel("user_name", read_only=True), + FieldPanel("user_email", read_only=True), + FieldPanel("user_age", read_only=True), + FieldPanel("feedback_by_ip", read_only=True), + + ], + heading=_("User information"), + ), + ] diff --git a/apps/feedback/models/pages.py b/apps/feedback/models/pages.py new file mode 100644 index 0000000..0db49e6 --- /dev/null +++ b/apps/feedback/models/pages.py @@ -0,0 +1,66 @@ +from django.apps import apps +from django.http import HttpResponseRedirect +from django.shortcuts import render +from wagtail.admin.panels import FieldPanel, MultiFieldPanel +from wagtail.contrib.routable_page.models import RoutablePageMixin, path, re_path, route +from wagtail.fields import RichTextField +from wagtail.models import Page +from wagtail_helpdesk.cms.models import Answer + +from apps.feedback.forms import FeedbackForm + + +class FeedbackFormPage(RoutablePageMixin, Page): + intro = RichTextField( + verbose_name="Intro", + ) + thank_you_text = RichTextField( + verbose_name="Tekst", + default="

Bedankt voor het stellen van je vraag. " + "We nemen je vraag in behandeling en proberen zo snel mogelijk een " + "expert te vinden die je vraag kan beantwoorden.

", + ) + + content_panels = Page.content_panels + [ + MultiFieldPanel( + [ + FieldPanel("intro"), + FieldPanel("thank_you_text"), + ], + heading="Formulier tekst", + ), + ] + + @path("/") + def form(self, request, answer_id): + """ + Index page, the form is spread over two steps using JavaScript. + """ + if request.method == "POST": + form = FeedbackForm(request.POST) + if form.is_valid(): + obj = form.save(commit=False) + obj.answer_id = answer_id + obj.asked_by_ip = request.META.get("REMOTE_ADDR", "") + obj.save() + return HttpResponseRedirect( + self.url + self.reverse_subpage("thank-you") + ) + else: + form = FeedbackForm() + + template = "feedback/feedback_form_page.html" + context = self.get_context(request) + context.update({"answer": Answer.objects.get(id=answer_id)}) + context.update({"form": form}) + return render(request, template, context) + + @re_path(r"^dank/", name="thank-you") + def thank_you(self, request): + template = "wagtail_helpdesk/cms/ask_question_page_thank_you.html" + context = self.get_context(request) + return render(request, template, context) + + class Meta: + verbose_name = "Feedback formulier pagina" + verbose_name_plural = "Feedback formulier pagina's" diff --git a/apps/feedback/templatetags/feedback_tag.py b/apps/feedback/templatetags/feedback_tag.py new file mode 100644 index 0000000..dcfaf71 --- /dev/null +++ b/apps/feedback/templatetags/feedback_tag.py @@ -0,0 +1,10 @@ +from django import template + +from apps.feedback.models import FeedbackFormPage + +register = template.Library() + + +@register.simple_tag +def get_feedback_page(): + return FeedbackFormPage.objects.live().first() diff --git a/apps/feedback/tests/__init__.py b/apps/feedback/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/feedback/tests/conftest.py b/apps/feedback/tests/conftest.py new file mode 100644 index 0000000..cbdccd9 --- /dev/null +++ b/apps/feedback/tests/conftest.py @@ -0,0 +1,13 @@ +import pytest +from wagtail.models import Site + +from .factories import HomePageFactory + + +@pytest.fixture() +def home_page(): + site = Site.objects.get() + home_page = HomePageFactory() + site.root_page = home_page + site.save() + return home_page diff --git a/apps/feedback/tests/factories.py b/apps/feedback/tests/factories.py new file mode 100644 index 0000000..3698651 --- /dev/null +++ b/apps/feedback/tests/factories.py @@ -0,0 +1,7 @@ +from wagtail_factories import PageFactory +from wagtail_helpdesk.cms.models import HomePage + + +class HomePageFactory(PageFactory): + class Meta: + model = HomePage diff --git a/apps/feedback/tests/test_admin.py b/apps/feedback/tests/test_admin.py new file mode 100644 index 0000000..666e4d8 --- /dev/null +++ b/apps/feedback/tests/test_admin.py @@ -0,0 +1,12 @@ +from http import HTTPStatus + +import pytest +from django.urls import reverse + + +@pytest.mark.django_db +@pytest.mark.usefixtures("home_page") +def test_django_admin_login_shows_release_version_from_setting(django_app, settings): + response = django_app.get(reverse("admin:login")) + assert response.status_code == HTTPStatus.OK + assert f"ai-helpdesk versie {settings.RELEASE_VERSION} in response" diff --git a/apps/feedback/tests/test_utils.py b/apps/feedback/tests/test_utils.py new file mode 100644 index 0000000..8c20197 --- /dev/null +++ b/apps/feedback/tests/test_utils.py @@ -0,0 +1,34 @@ +import os +from unittest.mock import patch + +import pytest +from django.core.exceptions import ImproperlyConfigured + +from apps.core.utils import check_for_debug_settings_in_production + + +def test_check_for_debug_settings_in_production(settings): + settings.DEBUG = False + with patch.dict(os.environ, {"DJANGO_SETTINGS_MODULE": "settings.production"}): + check_for_debug_settings_in_production(), ( + "No error should be raised when DEBUG is set to False while " + "production settings are active" + ) + settings.DEBUG = True + with pytest.raises( + ImproperlyConfigured, match="Running production settings with DEBUG = True" + ): + check_for_debug_settings_in_production(), ( + "An error should be raised when DEBUG is set to True while " + "production settings are active" + ) + with patch.dict(os.environ, {"DJANGO_SETTINGS_MODULE": "settings.development"}): + check_for_debug_settings_in_production(), ( + "No error should be raised when DEBUG is set to False while " + "development settings are active" + ) + settings.DEBUG = True + check_for_debug_settings_in_production(), ( + "No error should be raised when DEBUG is set to True while " + "development settings are active" + ) diff --git a/apps/feedback/utils.py b/apps/feedback/utils.py new file mode 100644 index 0000000..ae330b0 --- /dev/null +++ b/apps/feedback/utils.py @@ -0,0 +1,10 @@ +import os + +from django.conf import ENVIRONMENT_VARIABLE, settings +from django.core.exceptions import ImproperlyConfigured + + +def check_for_debug_settings_in_production(): + settings_module = os.environ.get(ENVIRONMENT_VARIABLE) + if settings_module.endswith("production") and settings.DEBUG: + raise ImproperlyConfigured("Running production settings with DEBUG = True") diff --git a/apps/frontend/static_src/components/blocks/_content.scss b/apps/frontend/static_src/components/blocks/_content.scss new file mode 100644 index 0000000..21d4cb5 --- /dev/null +++ b/apps/frontend/static_src/components/blocks/_content.scss @@ -0,0 +1,7 @@ +.content { + + &::before { + height: calc(100% + 200px); + } + +} diff --git a/apps/frontend/static_src/components/blocks/_feedback-request.scss b/apps/frontend/static_src/components/blocks/_feedback-request.scss new file mode 100644 index 0000000..f8584d1 --- /dev/null +++ b/apps/frontend/static_src/components/blocks/_feedback-request.scss @@ -0,0 +1,16 @@ +.feedback-request { + letter-spacing: 0; + letter-spacing: var(--button-letter-spacing); + text-transform: var(--button-text-transform); + word-spacing: var(--button-word-spacing); + max-width: calc(100% + 5px * 2 + 0 * 2); + max-width: calc(var(--grid-max-width) + var(--grid-gutter) * 2 + var(--grid-margin) * 2); + margin-left: auto; + margin-right: auto; + padding-left: 0; + padding-left: var(--grid-margin); + padding-right: 0; + padding-right: var(--grid-margin); + position: relative; + margin-bottom: 5px; +} diff --git a/apps/frontend/static_src/components/blocks/_form.scss b/apps/frontend/static_src/components/blocks/_form.scss new file mode 100644 index 0000000..71ee065 --- /dev/null +++ b/apps/frontend/static_src/components/blocks/_form.scss @@ -0,0 +1,13 @@ +.form { + + .form__answer-details { + border-radius: 30px; + background-color: #FAE7F3; + padding: 5px; + padding-left: 50px; + margin-bottom: 50px; + margin-top: 20px ; + } +} + + diff --git a/apps/frontend/static_src/scss/main.scss b/apps/frontend/static_src/scss/main.scss new file mode 100644 index 0000000..15bb03d --- /dev/null +++ b/apps/frontend/static_src/scss/main.scss @@ -0,0 +1,6 @@ +// This is the main stylesheet target +@charset "UTF-8"; + +@import "../components/blocks/feedback-request"; +@import "../components/blocks/content"; +@import "../components/blocks/form"; diff --git a/apps/frontend/templates/feedback/feedback_form_page.html b/apps/frontend/templates/feedback/feedback_form_page.html new file mode 100644 index 0000000..af60b3c --- /dev/null +++ b/apps/frontend/templates/feedback/feedback_form_page.html @@ -0,0 +1,114 @@ +{% extends "wagtail_helpdesk/base.html" %} +{% load static i18n wagtailcore_tags %} + +{% block smokedglass %} +{% endblock %} + +{% block content %} + + +
+ +
+ + {% if answer %} +
+

{% trans 'Please fill out the form to leave feedback' %}

+
+
+ {% csrf_token %} + + {{ form.non_field_errors }} + + {{ form.source.errors }} + {{ form.source }} + +
+
+ {% trans 'I have a question about' %}: +
+
+ {% trans "Please check if this is the correct article" %} +
+
+ {{ answer.get_as_overview_row_card }} +
+
+
+ {{ form.feedback.label_tag }} +
+ {{ form.feedback.help_text }} +
+ {{ form.feedback.errors }} + {{ form.feedback }} +
+ +
+ {{ form.grade.label_tag }} +
+ {{ form.grade.help_text }} +
+ {{ form.grade.errors }} + {{ form.grade }} +
+ +
+ {{ form.reliability_grade.label_tag }} +
+ {{ form.reliability_grade.help_text }} +
+ {{ form.reliability_grade.errors }} + {{ form.reliability_grade }} +
+ +
+ {{ form.user_name.label_tag }} +
+ {{ form.user_name.help_text }} +
+ {{ form.user_name.errors }} + {{ form.user_name }} +
+ +
+ {{ form.user_email.label_tag }} +
+ {{ form.user_email.help_text }} +
+ {{ form.user_email.errors }} + {{ form.user_email }} +
+ +
+ {{ form.user_age.label_tag }} +
+ {{ form.user_age.help_text }} +
+ {{ form.user_age.errors }} + {{ form.user_age }} +
+
+ +
+ +
+
+
+ {% else %} +
+

{% trans 'No specific answer has been selected, please go to the answer and use the feedback link.' %}

+
+ {% endif %} + +
+
+ + {% include "wagtail_helpdesk/includes/social_share_buttons.html" %} + +{% endblock %} + diff --git a/apps/frontend/templates/feedback/partials/feedback_request.html b/apps/frontend/templates/feedback/partials/feedback_request.html new file mode 100644 index 0000000..de1e2fe --- /dev/null +++ b/apps/frontend/templates/feedback/partials/feedback_request.html @@ -0,0 +1,12 @@ +{% load i18n wagtailroutablepage_tags feedback_tag %} + +{% get_feedback_page as feedback_page %} + + +
+

+ {% trans "What did you think of this answer?" %} {% trans "Give us your opinion" %} +

+
diff --git a/apps/frontend/templates/wagtail_helpdesk/cms/answer_detail.html b/apps/frontend/templates/wagtail_helpdesk/cms/answer_detail.html new file mode 100644 index 0000000..4c21ed7 --- /dev/null +++ b/apps/frontend/templates/wagtail_helpdesk/cms/answer_detail.html @@ -0,0 +1,23 @@ +{% extends "wagtail_helpdesk/cms/answer_detail.html" %} +{% load i18n static wagtailcore_tags wagtailimages_tags %} +{% wagtail_site as current_site %} + +{% block content %} + {% for block in self.page_content %} + {% include_block block %} + {% endfor %} + {% include "feedback/partials/feedback_request.html" with answer_id=page.id %} + {% include "wagtail_helpdesk/includes/social_share_buttons.html" %} + + {% for block in self.answer_origin %} + {% include_block block %} + {% endfor %} + + {% for block in self.related_items %} + {% include_block block %} + {% endfor %} + +{% endblock %} + + +