diff --git a/src/core/admin.py b/src/core/admin.py index ceedbab5..eb7ed592 100644 --- a/src/core/admin.py +++ b/src/core/admin.py @@ -11,7 +11,7 @@ from core.marker_utils import generate_marker_variants from core.media_dimensions import extract_dimensions -from core.models import Artwork, Exhibit, Marker, Object, Sound +from core.models import Artwork, Exhibit, Feedback, Marker, Object, Sound from core.spritesheet_converter import gif_to_spritesheet from core.utils import filesizeformat, get_admin_url @@ -418,3 +418,12 @@ def _owner(self, obj): def preview(self, obj): return format_html(obj.as_html(), "") + + +@admin.register(Feedback) +class FeedbackAdmin(admin.ModelAdmin): + list_display = ["__str__", "kind", "email", "handled", "created"] + list_filter = ["kind", "handled"] + list_editable = ["handled"] + search_fields = ["description", "email"] + readonly_fields = ["kind", "description", "email", "attachment", "created"] diff --git a/src/core/forms.py b/src/core/forms.py index b9aa69f8..44b5dd9c 100644 --- a/src/core/forms.py +++ b/src/core/forms.py @@ -20,7 +20,7 @@ strip_existing_border, ) from core.media_dimensions import extract_dimensions -from core.models import Artwork, Marker, ObjectExtensions +from core.models import Artwork, Marker, ObjectExtensions, Feedback from core.upload_limits import ( AUDIO, OBJECT_KIND_BY_EXTENSION, @@ -93,6 +93,8 @@ def render(self, name, value, attrs=None, renderer=None): return mark_safe(template) +# Feedback attachments come from anonymous visitors, so keep them small. +MAX_FEEDBACK_ATTACHMENT_BYTES = 5 * 1024 * 1024 # Markers are rasterised and uploaded to the AR viewer as textures, so an # upload far larger than a 4K texture costs storage and marker-generation time # without improving detection. Both sides are capped at this value. @@ -606,3 +608,40 @@ def save(self, *args, **kwargs): self.instance.file_extension = self.instance.file.name.split(".")[-1].lower() return super(SoundForm, self).save(*args, **kwargs) + + +class FeedbackForm(forms.ModelForm): + """Public bug report / feature request form linked from the footer.""" + + class Meta: + model = Feedback + fields = ("kind", "description", "email", "attachment") + widgets = { + "description": forms.Textarea( + attrs={ + "rows": 6, + "placeholder": _("What happened, and what did you expect?"), + } + ), + "email": forms.EmailInput( + attrs={"placeholder": _("Your e-mail (optional, so we can reply)")} + ), + } + + def clean_attachment(self): + attachment = self.cleaned_data.get("attachment") + if not attachment: + return None + + allowed_extensions = ["png", "jpg", "jpeg", "gif", "webp", "pdf", "txt", "log"] + extension = getattr(attachment, "name", "").split(".")[-1].lower() + if extension not in allowed_extensions: + raise forms.ValidationError( + _("Attach a screenshot (PNG, JPG, GIF, WebP), a PDF or a text log.") + ) + if attachment.size > MAX_FEEDBACK_ATTACHMENT_BYTES: + raise forms.ValidationError( + _("Attachments are limited to %(limit)s MB."), + params={"limit": MAX_FEEDBACK_ATTACHMENT_BYTES // (1024 * 1024)}, + ) + return attachment diff --git a/src/core/jinja2/core/feedback.jinja2 b/src/core/jinja2/core/feedback.jinja2 new file mode 100644 index 00000000..f8a8c13f --- /dev/null +++ b/src/core/jinja2/core/feedback.jinja2 @@ -0,0 +1,28 @@ +{% extends "core/focused_layout.jinja2" %} +{% block content %} +
+ {% if submitted %} +

{{ _("Thank you!") }}

+

{{ _("Your report was received. If you left an e-mail, we may get back to you.") }}

+ {{ _("Back to Jandig") }} + {% else %} +

{{ _("Report a bug or request a feature") }}

+

+ {{ _("Found something broken, or missing? Tell us here — no GitHub account needed.") }} +

+
+ {{ csrf_input }} + {% for field in form %} +
+ + {{ field }} + {% if field.help_text %}{{ field.help_text }}{% endif %} + {{ field.errors }} +
+ {% endfor %} + {{ form.non_field_errors() }} + +
+ {% endif %} +
+{% endblock content %} diff --git a/src/core/jinja2/core/footer.jinja2 b/src/core/jinja2/core/footer.jinja2 index af13af06..6765d2cd 100644 --- a/src/core/jinja2/core/footer.jinja2 +++ b/src/core/jinja2/core/footer.jinja2 @@ -1,6 +1,7 @@