diff --git a/totem/templates/header.html b/totem/templates/header.html
index 90f75718..fc0c8814 100644
--- a/totem/templates/header.html
+++ b/totem/templates/header.html
@@ -15,6 +15,7 @@
"logout": "{% url "account_logout" %}",
"login": "{% url "users:login" %}",
"profile": "{% url "users:profile" %}",
+ "feedback": "{% url "users:feedback" %}",
"marketing": [
{
"title": "How it works", "href": "{% url "pages:how-it-works" %}"
diff --git a/totem/users/admin.py b/totem/users/admin.py
index cb6338c4..d2fa410b 100644
--- a/totem/users/admin.py
+++ b/totem/users/admin.py
@@ -5,7 +5,7 @@
from totem.users.forms import UserAdminChangeForm, UserAdminCreationForm
-from .models import KeeperProfile, User
+from .models import Feedback, KeeperProfile, User
@admin.register(User)
@@ -47,3 +47,8 @@ class UserAdmin(UserAdminImpersonateMixin, auth_admin.UserAdmin):
@admin.register(KeeperProfile)
class KeeperProfileAdmin(admin.ModelAdmin):
autocomplete_fields = ("user",)
+
+
+@admin.register(Feedback)
+class FeedbackAdmin(admin.ModelAdmin):
+ pass
diff --git a/totem/users/migrations/0023_feedback.py b/totem/users/migrations/0023_feedback.py
new file mode 100644
index 00000000..d6ddfe0f
--- /dev/null
+++ b/totem/users/migrations/0023_feedback.py
@@ -0,0 +1,41 @@
+# Generated by Django 4.2.6 on 2023-11-02 21:52
+
+from django.conf import settings
+import django.core.validators
+from django.db import migrations, models
+import django.db.models.deletion
+import totem.utils.fields
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("users", "0022_keeperprofile"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="Feedback",
+ fields=[
+ ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
+ ("email", models.EmailField(blank=True, max_length=254, null=True, verbose_name="Email Address")),
+ (
+ "message",
+ totem.utils.fields.MaxLengthTextField(
+ max_length=10000,
+ validators=[django.core.validators.MaxLengthValidator(10000)],
+ verbose_name="Feedback",
+ ),
+ ),
+ ("date_created", models.DateTimeField(auto_now_add=True)),
+ (
+ "user",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="feedback",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ ],
+ ),
+ ]
diff --git a/totem/users/models.py b/totem/users/models.py
index 2a05ab82..d9c0f1a8 100644
--- a/totem/users/models.py
+++ b/totem/users/models.py
@@ -17,6 +17,7 @@
from totem.email.utils import validate_email_blocked
from totem.users.managers import UserManager
+from totem.utils.fields import MaxLengthTextField
from totem.utils.hash import basic_hash
from totem.utils.md import MarkdownField, MarkdownMixin
from totem.utils.models import AdminURLMixin, SluggedModel
@@ -144,3 +145,13 @@ def __str__(self):
def get_absolute_url(self):
return self.user.get_absolute_url()
+
+
+class Feedback(models.Model):
+ user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="feedback", null=True)
+ email = EmailField(_("Email Address"), null=True, blank=True)
+ message = MaxLengthTextField(null=False, max_length=10000, blank=False, verbose_name=_("Feedback"))
+ date_created = models.DateTimeField(auto_now_add=True)
+
+ def __str__(self):
+ return f"
"
diff --git a/totem/users/templates/users/feedback.html b/totem/users/templates/users/feedback.html
new file mode 100644
index 00000000..dfdbd063
--- /dev/null
+++ b/totem/users/templates/users/feedback.html
@@ -0,0 +1,26 @@
+{% extends "base.html" %}
+{% block content %}
+ Feedback
+
+
+ We love hearing about how we can improve Totem. If you have any feedback, please let us know!
+
+
+
+{% endblock content %}
diff --git a/totem/users/tests/test_forms.py b/totem/users/tests/test_forms.py
index 50319cd5..285ced6c 100644
--- a/totem/users/tests/test_forms.py
+++ b/totem/users/tests/test_forms.py
@@ -1,10 +1,15 @@
"""
Module for all Form Tests.
"""
+from django.contrib.messages import get_messages
+from django.test import TestCase
+from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from totem.users.forms import UserAdminCreationForm
-from totem.users.models import User
+from totem.users.models import Feedback, User
+from totem.users.tests.factories import UserFactory
+from totem.users.views import FeedbackForm
class TestUserAdminCreationForm:
@@ -34,3 +39,49 @@ def test_username_validation_error_msg(self, user: User):
assert len(form.errors) == 1
assert "email" in form.errors
assert form.errors["email"][0] == _("This email has already been taken.")
+
+
+class TestUserFeedbackView(TestCase):
+ def test_feedback_form_display(self):
+ response = self.client.get(reverse("users:feedback"))
+ self.assertEqual(response.status_code, 200)
+ self.assertIsInstance(response.context["form"], FeedbackForm)
+
+ def test_feedback_form_submission_authenticated(self):
+ user = UserFactory()
+ self.client.force_login(user)
+ response = self.client.post(
+ reverse("users:feedback"),
+ data={
+ "message": "value2",
+ },
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(Feedback.objects.count(), 1)
+ feedback = Feedback.objects.first()
+ assert feedback
+ self.assertEqual(feedback.user, user)
+ self.assertEqual(feedback.email, None)
+ self.assertEqual(feedback.message, "value2")
+ messages = list(get_messages(response.wsgi_request))
+ self.assertEqual(len(messages), 1)
+ self.assertEqual(str(messages[0]), "Feedback successfully submitted. Thank you!")
+
+ def test_feedback_form_submission_unauthenticated(self):
+ response = self.client.post(
+ reverse("users:feedback"),
+ data={
+ "email": "dfgsdg@sdfjsd.com",
+ "message": "value3",
+ },
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(Feedback.objects.count(), 1)
+ feedback = Feedback.objects.first()
+ assert feedback
+ self.assertIsNone(feedback.user)
+ self.assertEqual(feedback.email, "dfgsdg@sdfjsd.com")
+ self.assertEqual(feedback.message, "value3")
+ messages = list(get_messages(response.wsgi_request))
+ self.assertEqual(len(messages), 1)
+ self.assertEqual(str(messages[0]), "Feedback successfully submitted. Thank you!")
diff --git a/totem/users/urls.py b/totem/users/urls.py
index ab663836..b7ef1672 100644
--- a/totem/users/urls.py
+++ b/totem/users/urls.py
@@ -4,6 +4,7 @@
LogInView,
user_dashboard_view,
user_detail_view,
+ user_feedback_view,
user_index_view,
user_profile_delete_view,
user_profile_image_view,
@@ -19,6 +20,7 @@
path("profile/", user_profile_view, name="profile"),
path("profile/delete", user_profile_delete_view, name="profile-delete"),
path("profile/image", user_profile_image_view, name="profile-image"),
+ path("feedback/", user_feedback_view, name="feedback"),
path("", user_index_view, name="index"),
path("u//", view=user_detail_view, name="detail"),
]
diff --git a/totem/users/views.py b/totem/users/views.py
index 533ce2a5..a35edb8b 100644
--- a/totem/users/views.py
+++ b/totem/users/views.py
@@ -15,7 +15,7 @@
from . import analytics
from .forms import LoginForm
-from .models import User
+from .models import Feedback, User
def user_detail_view(request, slug):
@@ -202,3 +202,27 @@ def user_profile_delete_view(request):
messages.success(request, "Account successfully deleted.")
return redirect("pages:home")
return HttpResponseForbidden()
+
+
+class FeedbackForm(forms.ModelForm):
+ class Meta:
+ model = Feedback
+ fields = ("email", "message")
+ widgets = {
+ "message": forms.Textarea(attrs={"rows": 5, "cols": 15}),
+ }
+
+
+def user_feedback_view(request):
+ form = FeedbackForm()
+ if request.method == "POST":
+ data = request.POST.copy()
+ form = FeedbackForm(data)
+ if form.is_valid():
+ if request.user.is_authenticated:
+ Feedback.objects.create(**form.cleaned_data, user=request.user)
+ else:
+ Feedback.objects.create(**form.cleaned_data)
+ messages.success(request, "Feedback successfully submitted. Thank you!")
+ form = FeedbackForm()
+ return render(request, "users/feedback.html", context={"form": form})
diff --git a/totem/utils/fields.py b/totem/utils/fields.py
index e69de29b..96a7d8ae 100644
--- a/totem/utils/fields.py
+++ b/totem/utils/fields.py
@@ -0,0 +1,9 @@
+from django.core.validators import MaxLengthValidator
+from django.db.models import TextField
+
+
+class MaxLengthTextField(TextField):
+ def __init__(self, *args, **kwargs):
+ kwargs["max_length"] = kwargs.get("max_length", 10000)
+ kwargs["validators"] = kwargs.get("validators", [MaxLengthValidator(kwargs["max_length"])])
+ super().__init__(*args, **kwargs)