From 6f2f28a3a752cc47d9dc96bda862ed67cd75c9af Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Thu, 1 Dec 2022 18:46:49 -0700 Subject: Image attachment uploads --- activities/views/compose.py | 182 ++++++++++++++++++++++++++++++++++++++++++++ activities/views/posts.py | 140 +--------------------------------- 2 files changed, 184 insertions(+), 138 deletions(-) create mode 100644 activities/views/compose.py (limited to 'activities/views') diff --git a/activities/views/compose.py b/activities/views/compose.py new file mode 100644 index 0000000..58dc7fc --- /dev/null +++ b/activities/views/compose.py @@ -0,0 +1,182 @@ +from django import forms +from django.core.exceptions import PermissionDenied +from django.shortcuts import get_object_or_404, redirect, render +from django.utils.decorators import method_decorator +from django.views.generic import FormView + +from activities.models import ( + Post, + PostAttachment, + PostAttachmentStates, + PostStates, + TimelineEvent, +) +from core.files import blurhash_image, resize_image +from core.html import html_to_plaintext +from core.models import Config +from users.decorators import identity_required + + +@method_decorator(identity_required, name="dispatch") +class Compose(FormView): + + template_name = "activities/compose.html" + + class form_class(forms.Form): + text = forms.CharField( + widget=forms.Textarea( + attrs={ + "placeholder": "What's on your mind?", + }, + ) + ) + visibility = forms.ChoiceField( + choices=[ + (Post.Visibilities.public, "Public"), + (Post.Visibilities.local_only, "Local Only"), + (Post.Visibilities.unlisted, "Unlisted"), + (Post.Visibilities.followers, "Followers & Mentioned Only"), + (Post.Visibilities.mentioned, "Mentioned Only"), + ], + ) + content_warning = forms.CharField( + required=False, + label=Config.lazy_system_value("content_warning_text"), + widget=forms.TextInput( + attrs={ + "placeholder": Config.lazy_system_value("content_warning_text"), + }, + ), + help_text="Optional - Post will be hidden behind this text until clicked", + ) + reply_to = forms.CharField(widget=forms.HiddenInput(), required=False) + + def clean_text(self): + text = self.cleaned_data.get("text") + if not text: + return text + length = len(text) + if length > Config.system.post_length: + raise forms.ValidationError( + f"Maximum post length is {Config.system.post_length} characters (you have {length})" + ) + return text + + def get_initial(self): + initial = super().get_initial() + if self.post_obj: + initial.update( + { + "reply_to": self.reply_to.pk if self.reply_to else "", + "visibility": self.post_obj.visibility, + "text": html_to_plaintext(self.post_obj.content), + "content_warning": self.post_obj.summary, + } + ) + else: + initial[ + "visibility" + ] = self.request.identity.config_identity.default_post_visibility + if self.reply_to: + initial["reply_to"] = self.reply_to.pk + if self.reply_to.visibility == Post.Visibilities.public: + initial["visibility"] = Post.Visibilities.unlisted + else: + initial["visibility"] = self.reply_to.visibility + initial["text"] = f"@{self.reply_to.author.handle} " + return initial + + def form_valid(self, form): + # Gather any attachment objects now, they're not in the form proper + attachments = [] + if "attachment" in self.request.POST: + attachments = PostAttachment.objects.filter( + pk__in=self.request.POST.getlist("attachment", []) + ) + # Dispatch based on edit or not + if self.post_obj: + self.post_obj.edit_local( + content=form.cleaned_data["text"], + summary=form.cleaned_data.get("content_warning"), + visibility=form.cleaned_data["visibility"], + attachments=attachments, + ) + self.post_obj.transition_perform(PostStates.edited) + else: + post = Post.create_local( + author=self.request.identity, + content=form.cleaned_data["text"], + summary=form.cleaned_data.get("content_warning"), + visibility=form.cleaned_data["visibility"], + reply_to=self.reply_to, + attachments=attachments, + ) + # Add their own timeline event for immediate visibility + TimelineEvent.add_post(self.request.identity, post) + return redirect("/") + + def dispatch(self, request, handle=None, post_id=None, *args, **kwargs): + self.post_obj = None + if handle and post_id: + # Make sure the request identity owns the post! + if handle != request.identity.handle: + raise PermissionDenied("Post author is not requestor") + + self.post_obj = get_object_or_404(request.identity.posts, pk=post_id) + + # Grab the reply-to post info now + self.reply_to = None + reply_to_id = request.POST.get("reply_to") or request.GET.get("reply_to") + if reply_to_id: + try: + self.reply_to = Post.objects.get(pk=reply_to_id) + except Post.DoesNotExist: + pass + # Keep going with normal rendering + return super().dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["reply_to"] = self.reply_to + if self.post_obj: + context["post"] = self.post_obj + return context + + +@method_decorator(identity_required, name="dispatch") +class ImageUpload(FormView): + """ + Handles image upload - returns a new input type hidden to embed in + the main form that references an orphaned PostAttachment + """ + + template_name = "activities/_image_upload.html" + + class form_class(forms.Form): + image = forms.ImageField() + description = forms.CharField(required=False) + + def form_valid(self, form): + # Make a PostAttachment + thumbnail_file = resize_image(form.cleaned_data["image"], size=(400, 225)) + attachment = PostAttachment.objects.create( + blurhash=blurhash_image(thumbnail_file), + mimetype=form.cleaned_data["image"].image.get_format_mimetype(), + width=form.cleaned_data["image"].image.width, + height=form.cleaned_data["image"].image.height, + name=form.cleaned_data.get("description"), + state=PostAttachmentStates.fetched, + ) + attachment.file.save( + form.cleaned_data["image"].name, + form.cleaned_data["image"], + ) + attachment.thumbnail.save( + form.cleaned_data["image"].name, + thumbnail_file, + ) + attachment.save() + # Return the response, with a hidden input plus a note + return render( + self.request, "activities/_image_uploaded.html", {"attachment": attachment} + ) diff --git a/activities/views/posts.py b/activities/views/posts.py index 6dbfc8e..8b0ff33 100644 --- a/activities/views/posts.py +++ b/activities/views/posts.py @@ -1,21 +1,12 @@ -from django import forms from django.core.exceptions import PermissionDenied from django.db import models from django.http import JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.utils.decorators import method_decorator -from django.views.generic import FormView, TemplateView, View +from django.views.generic import TemplateView, View -from activities.models import ( - Post, - PostInteraction, - PostInteractionStates, - PostStates, - TimelineEvent, -) -from core.html import html_to_plaintext +from activities.models import Post, PostInteraction, PostInteractionStates, PostStates from core.ld import canonicalise -from core.models import Config from users.decorators import identity_required from users.shortcuts import by_handle_or_404 @@ -188,130 +179,3 @@ class Delete(TemplateView): def post(self, request): self.post_obj.transition_perform(PostStates.deleted) return redirect("/") - - -@method_decorator(identity_required, name="dispatch") -class Compose(FormView): - - template_name = "activities/compose.html" - - class form_class(forms.Form): - id = forms.IntegerField( - required=False, - widget=forms.HiddenInput(), - ) - - text = forms.CharField( - widget=forms.Textarea( - attrs={ - "placeholder": "What's on your mind?", - }, - ) - ) - visibility = forms.ChoiceField( - choices=[ - (Post.Visibilities.public, "Public"), - (Post.Visibilities.local_only, "Local Only"), - (Post.Visibilities.unlisted, "Unlisted"), - (Post.Visibilities.followers, "Followers & Mentioned Only"), - (Post.Visibilities.mentioned, "Mentioned Only"), - ], - ) - content_warning = forms.CharField( - required=False, - label=Config.lazy_system_value("content_warning_text"), - widget=forms.TextInput( - attrs={ - "placeholder": Config.lazy_system_value("content_warning_text"), - }, - ), - help_text="Optional - Post will be hidden behind this text until clicked", - ) - reply_to = forms.CharField(widget=forms.HiddenInput(), required=False) - - def clean_text(self): - text = self.cleaned_data.get("text") - if not text: - return text - length = len(text) - if length > Config.system.post_length: - raise forms.ValidationError( - f"Maximum post length is {Config.system.post_length} characters (you have {length})" - ) - return text - - def get_initial(self): - initial = super().get_initial() - if self.post_obj: - initial.update( - { - "id": self.post_obj.id, - "reply_to": self.reply_to.pk if self.reply_to else "", - "visibility": self.post_obj.visibility, - "text": html_to_plaintext(self.post_obj.content), - "content_warning": self.post_obj.summary, - } - ) - else: - initial[ - "visibility" - ] = self.request.identity.config_identity.default_post_visibility - if self.reply_to: - initial["reply_to"] = self.reply_to.pk - if self.reply_to.visibility == Post.Visibilities.public: - initial["visibility"] = Post.Visibilities.unlisted - else: - initial["visibility"] = self.reply_to.visibility - initial["text"] = f"@{self.reply_to.author.handle} " - return initial - - def form_valid(self, form): - post_id = form.cleaned_data.get("id") - if post_id: - post = get_object_or_404(self.request.identity.posts, pk=post_id) - post.edit_local( - content=form.cleaned_data["text"], - summary=form.cleaned_data.get("content_warning"), - visibility=form.cleaned_data["visibility"], - ) - - # Should there be a timeline event for edits? - # E.g. "@user edited #123" - - post.transition_perform(PostStates.edited) - else: - post = Post.create_local( - author=self.request.identity, - content=form.cleaned_data["text"], - summary=form.cleaned_data.get("content_warning"), - visibility=form.cleaned_data["visibility"], - reply_to=self.reply_to, - ) - # Add their own timeline event for immediate visibility - TimelineEvent.add_post(self.request.identity, post) - return redirect("/") - - def dispatch(self, request, handle=None, post_id=None, *args, **kwargs): - self.post_obj = None - if handle and post_id: - # Make sure the request identity owns the post! - if handle != request.identity.handle: - raise PermissionDenied("Post author is not requestor") - - self.post_obj = get_object_or_404(request.identity.posts, pk=post_id) - - # Grab the reply-to post info now - self.reply_to = None - reply_to_id = request.POST.get("reply_to") or request.GET.get("reply_to") - if reply_to_id: - try: - self.reply_to = Post.objects.get(pk=reply_to_id) - except Post.DoesNotExist: - pass - # Keep going with normal rendering - return super().dispatch(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["reply_to"] = self.reply_to - return context -- cgit v1.2.3