From 2e605ca9a9049fdf092994b66bc1c20b51407a94 Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Wed, 3 May 2023 11:41:52 -0600 Subject: [PATCH] Fix linting, restore one image to compose --- activities/views/compose.py | 176 +++++++++++++------------ activities/views/posts.py | 8 +- activities/views/timelines.py | 9 +- core/migrations/0002_domain_config.py | 2 +- core/views.py | 3 - templates/activities/_post.html | 2 +- templates/activities/compose.html | 16 +-- templates/activities/home.html | 2 +- tests/activities/views/test_compose.py | 2 - tests/conftest.py | 1 - users/decorators.py | 4 - users/middleware.py | 4 +- users/models/domain.py | 4 +- users/models/user.py | 6 +- users/views/admin/domains.py | 2 +- users/views/announcements.py | 2 +- users/views/base.py | 5 +- users/views/identity.py | 8 +- users/views/settings/follows.py | 2 - users/views/settings/import_export.py | 5 +- users/views/settings/profile.py | 4 +- users/views/settings/security.py | 3 +- users/views/settings/settings_page.py | 2 +- 23 files changed, 131 insertions(+), 141 deletions(-) diff --git a/activities/views/compose.py b/activities/views/compose.py index d8cfb41..4da4154 100644 --- a/activities/views/compose.py +++ b/activities/views/compose.py @@ -1,27 +1,17 @@ from django import forms from django.conf import settings -from django.core.exceptions import PermissionDenied -from django.shortcuts import get_object_or_404, redirect, render +from django.contrib import messages +from django.shortcuts import redirect from django.utils import timezone -from django.utils.decorators import method_decorator from django.views.generic import FormView -from activities.models import ( - Post, - PostAttachment, - PostAttachmentStates, - PostStates, - TimelineEvent, -) +from activities.models import Post, PostAttachment, PostAttachmentStates, TimelineEvent from core.files import blurhash_image, resize_image -from core.html import FediverseHtmlParser from core.models import Config -from users.shortcuts import by_handle_for_user_or_404 -from django.contrib.auth.decorators import login_required +from users.views.base import IdentityViewMixin -@method_decorator(login_required, name="dispatch") -class Compose(FormView): +class Compose(IdentityViewMixin, FormView): template_name = "activities/compose.html" class form_class(forms.Form): @@ -33,6 +23,7 @@ class Compose(FormView): }, ) ) + visibility = forms.ChoiceField( choices=[ (Post.Visibilities.public, "Public"), @@ -42,6 +33,7 @@ class Compose(FormView): (Post.Visibilities.mentioned, "Mentioned Only"), ], ) + content_warning = forms.CharField( required=False, label=Config.lazy_system_value("content_warning_text"), @@ -52,7 +44,38 @@ class Compose(FormView): ), help_text="Optional - Post will be hidden behind this text until clicked", ) - reply_to = forms.CharField(widget=forms.HiddenInput(), required=False) + + image = forms.ImageField( + required=False, + help_text="Optional - For multiple image uploads and cropping, please use an app", + widget=forms.FileInput( + attrs={ + "_": f""" + on change + if me.files[0].size > {settings.SETUP.MEDIA_MAX_IMAGE_FILESIZE_MB * 1024 ** 2} + add [@disabled=] to #upload + + remove + make called errorlist + make
  • called error + set size_in_mb to (me.files[0].size / 1024 / 1024).toFixed(2) + put 'File must be {settings.SETUP.MEDIA_MAX_IMAGE_FILESIZE_MB}MB or less (actual: ' + size_in_mb + 'MB)' into error + put error into errorlist + put errorlist before me + else + remove @disabled from #upload + remove + end + end + """ + } + ), + ) + + image_caption = forms.CharField( + required=False, + help_text="Provide an image caption for the visually impaired", + ) def __init__(self, identity, *args, **kwargs): super().__init__(*args, **kwargs) @@ -102,90 +125,75 @@ class Compose(FormView): ) return text + def clean_image(self): + value = self.cleaned_data.get("image") + if value: + max_mb = settings.SETUP.MEDIA_MAX_IMAGE_FILESIZE_MB + max_bytes = max_mb * 1024 * 1024 + if value.size > max_bytes: + # Erase the file from our data to stop trying to show it again + self.files = {} + raise forms.ValidationError( + f"File must be {max_mb}MB or less (actual: {value.size / 1024 ** 2:.2f})" + ) + return value + def get_form(self, form_class=None): return self.form_class(identity=self.identity, **self.get_form_kwargs()) 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": FediverseHtmlParser(self.post_obj.content).plain_text, - "content_warning": self.post_obj.summary, - } - ) - else: - initial[ - "visibility" - ] = self.identity.config_identity.default_post_visibility - if self.reply_to: - initial["reply_to"] = self.reply_to.pk - initial["visibility"] = self.reply_to.visibility - initial["content_warning"] = self.reply_to.summary - # Build a set of mentions for the content to start as - mentioned = {self.reply_to.author} - mentioned.update(self.reply_to.mentions.all()) - mentioned.discard(self.identity) - initial["text"] = "".join( - f"@{identity.handle} " - for identity in mentioned - if identity.username - ) + initial["visibility"] = self.identity.config_identity.default_post_visibility return initial def form_valid(self, form): - # Gather any attachment objects now, they're not in the form proper + # See if we need to make an image attachment attachments = [] - if "attachment" in self.request.POST: - attachments = PostAttachment.objects.filter( - pk__in=self.request.POST.getlist("attachment", []) + if form.cleaned_data.get("image"): + main_file = resize_image( + form.cleaned_data["image"], + size=(2000, 2000), + cover=False, ) - # 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, + thumbnail_file = resize_image( + form.cleaned_data["image"], + size=(400, 225), + cover=True, ) - self.post_obj.transition_perform(PostStates.edited) - else: - post = Post.create_local( + attachment = PostAttachment.objects.create( + blurhash=blurhash_image(thumbnail_file), + mimetype="image/webp", + width=main_file.image.width, + height=main_file.image.height, + name=form.cleaned_data.get("image_caption"), + state=PostAttachmentStates.fetched, author=self.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.identity, post) - return redirect(self.identity.urls.view) - - def dispatch(self, request, handle=None, post_id=None, *args, **kwargs): - self.identity = by_handle_for_user_or_404(self.request, handle) - self.post_obj = None - if handle and post_id: - self.post_obj = get_object_or_404(self.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) + attachment.file.save( + main_file.name, + main_file, + ) + attachment.thumbnail.save( + thumbnail_file.name, + thumbnail_file, + ) + attachment.save() + attachments.append(attachment) + # Create the post + post = Post.create_local( + author=self.identity, + content=form.cleaned_data["text"], + summary=form.cleaned_data.get("content_warning"), + visibility=form.cleaned_data["visibility"], + attachments=attachments, + ) + # Add their own timeline event for immediate visibility + TimelineEvent.add_post(self.identity, post) + messages.success(self.request, "Your post was created.") + return redirect(".") def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["reply_to"] = self.reply_to context["identity"] = self.identity context["section"] = "compose" - if self.post_obj: - context["post"] = self.post_obj return context diff --git a/activities/views/posts.py b/activities/views/posts.py index 99539fa..797a6df 100644 --- a/activities/views/posts.py +++ b/activities/views/posts.py @@ -1,15 +1,13 @@ -from django.core.exceptions import PermissionDenied from django.http import Http404, JsonResponse -from django.shortcuts import get_object_or_404, redirect, render +from django.shortcuts import get_object_or_404, redirect from django.utils.decorators import method_decorator from django.views.decorators.vary import vary_on_headers -from django.views.generic import TemplateView, View +from django.views.generic import TemplateView -from activities.models import Post, PostInteraction, PostStates +from activities.models import Post, PostStates from activities.services import PostService from core.decorators import cache_page_by_ap_json from core.ld import canonicalise -from django.contrib.auth.decorators import login_required from users.models import Identity from users.shortcuts import by_handle_or_404 diff --git a/activities/views/timelines.py b/activities/views/timelines.py index 4f8dad4..20ba613 100644 --- a/activities/views/timelines.py +++ b/activities/views/timelines.py @@ -1,15 +1,12 @@ -from typing import Any -from django.http import Http404 -from django.core.paginator import Paginator +from django.contrib.auth.decorators import login_required from django.shortcuts import get_object_or_404, redirect from django.utils.decorators import method_decorator from django.views.generic import ListView, TemplateView -from activities.models import Hashtag, PostInteraction, TimelineEvent +from activities.models import Hashtag, TimelineEvent from activities.services import TimelineService from core.decorators import cache_page -from django.contrib.auth.decorators import login_required -from users.models import Bookmark, HashtagFollow, Identity +from users.models import Identity from users.views.base import IdentityViewMixin diff --git a/core/migrations/0002_domain_config.py b/core/migrations/0002_domain_config.py index 3d220b4..e5305a2 100644 --- a/core/migrations/0002_domain_config.py +++ b/core/migrations/0002_domain_config.py @@ -1,8 +1,8 @@ # Generated by Django 4.2 on 2023-04-29 18:49 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/core/views.py b/core/views.py index 7fae532..4171736 100644 --- a/core/views.py +++ b/core/views.py @@ -1,14 +1,11 @@ -import json from typing import ClassVar import markdown_it from django.conf import settings from django.http import HttpResponse from django.shortcuts import redirect -from django.templatetags.static import static from django.utils.decorators import method_decorator from django.utils.safestring import mark_safe -from django.views.decorators.cache import cache_control from django.views.generic import TemplateView, View from django.views.static import serve diff --git a/templates/activities/_post.html b/templates/activities/_post.html index f3219ac..6834ffb 100644 --- a/templates/activities/_post.html +++ b/templates/activities/_post.html @@ -28,7 +28,7 @@ {% if post.summary %}
    - {{ post.summary }} + {{ post.summary }}
    {% endif %} diff --git a/templates/activities/compose.html b/templates/activities/compose.html index b7b4cdf..2f2eaed 100644 --- a/templates/activities/compose.html +++ b/templates/activities/compose.html @@ -3,20 +3,20 @@ {% block title %}Compose{% endblock %} {% block settings_content %} -
    + {% csrf_token %}
    Compose - {% if reply_to %} - - {% include "activities/_mini_post.html" with post=reply_to %} - {% endif %} -

    For more advanced posting options, like editing and image uploads, please use an app.

    - {{ form.reply_to }} +

    For more advanced posting options, like editing and multiple image uploads, please use an app.

    {{ form.id }} {% include "forms/_field.html" with field=form.text %} - {% include "forms/_field.html" with field=form.content_warning %} {% include "forms/_field.html" with field=form.visibility %} + {% include "forms/_field.html" with field=form.content_warning %} +
    +
    + Image + {% include "forms/_field.html" with field=form.image %} + {% include "forms/_field.html" with field=form.image_caption %}
    {{ config.post_length }} diff --git a/templates/activities/home.html b/templates/activities/home.html index 016fd6b..3194798 100644 --- a/templates/activities/home.html +++ b/templates/activities/home.html @@ -29,7 +29,7 @@

    To see your timelines, compose and edit messages, and follow people, you will need to use a Mastodon-compatible app. Our favourites - are listed below. + are listed below, but there's lots more options out there!

    diff --git a/tests/activities/views/test_compose.py b/tests/activities/views/test_compose.py index ded9c69..f080b63 100644 --- a/tests/activities/views/test_compose.py +++ b/tests/activities/views/test_compose.py @@ -2,8 +2,6 @@ import pytest from django.test.client import Client from pytest_django.asserts import assertContains -from activities.models import Post -from core.models import Config from users.models import Identity diff --git a/tests/conftest.py b/tests/conftest.py index 9703d6d..e1e25a2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,6 @@ import time import pytest -from django.conf import settings from django.test import Client from api.models import Application, Token diff --git a/users/decorators.py b/users/decorators.py index 9136a78..b61b01e 100644 --- a/users/decorators.py +++ b/users/decorators.py @@ -1,8 +1,4 @@ -from functools import wraps - from django.contrib.auth.decorators import user_passes_test -from django.contrib.auth.views import redirect_to_login -from django.http import HttpResponseRedirect def moderator_required(function): diff --git a/users/middleware.py b/users/middleware.py index 5a7f30b..8abbbe4 100644 --- a/users/middleware.py +++ b/users/middleware.py @@ -11,7 +11,7 @@ class DomainMiddleware: def __call__(self, request): request.domain = None - if "HTTP_HOST" in request.META: - request.domain = Domain.get_domain(request.META["HTTP_HOST"]) + if "host" in request.headers: + request.domain = Domain.get_domain(request.headers["host"]) response = self.get_response(request) return response diff --git a/users/models/domain.py b/users/models/domain.py index c8d3e61..318407f 100644 --- a/users/models/domain.py +++ b/users/models/domain.py @@ -1,7 +1,7 @@ import json import ssl -from typing import Optional from functools import cached_property +from typing import Optional import httpx import pydantic @@ -11,9 +11,9 @@ from django.conf import settings from django.db import models from core.exceptions import capture_message +from core.models import Config from stator.models import State, StateField, StateGraph, StatorModel from users.schemas import NodeInfo -from core.models import Config class DomainStates(StateGraph): diff --git a/users/models/user.py b/users/models/user.py index 82d77c9..902e98f 100644 --- a/users/models/user.py +++ b/users/models/user.py @@ -1,9 +1,11 @@ -import urlman from functools import cached_property -from core.models import Config + +import urlman from django.contrib.auth.models import AbstractBaseUser, BaseUserManager from django.db import models +from core.models import Config + class UserManager(BaseUserManager): """ diff --git a/users/views/admin/domains.py b/users/views/admin/domains.py index 6980b66..97b2b03 100644 --- a/users/views/admin/domains.py +++ b/users/views/admin/domains.py @@ -1,10 +1,10 @@ from django import forms +from django.core.files import File from django.core.validators import RegexValidator from django.db import models from django.shortcuts import get_object_or_404, redirect from django.utils.decorators import method_decorator from django.views.generic import FormView, TemplateView -from django.core.files import File from core.models import Config from users.decorators import admin_required diff --git a/users/views/announcements.py b/users/views/announcements.py index 0bbc78f..9b094b1 100644 --- a/users/views/announcements.py +++ b/users/views/announcements.py @@ -1,9 +1,9 @@ +from django.contrib.auth.decorators import login_required from django.http import HttpResponse from django.shortcuts import get_object_or_404 from django.utils.decorators import method_decorator from django.views.generic import View -from django.contrib.auth.decorators import login_required from users.models import Announcement from users.services import AnnouncementService diff --git a/users/views/base.py b/users/views/base.py index eb489fe..cdef9bb 100644 --- a/users/views/base.py +++ b/users/views/base.py @@ -1,6 +1,7 @@ -from users.shortcuts import by_handle_for_user_or_404 -from django.utils.decorators import method_decorator from django.contrib.auth.decorators import login_required +from django.utils.decorators import method_decorator + +from users.shortcuts import by_handle_for_user_or_404 @method_decorator(login_required, name="dispatch") diff --git a/users/views/identity.py b/users/views/identity.py index 3b2fc14..cd6c8bc 100644 --- a/users/views/identity.py +++ b/users/views/identity.py @@ -10,17 +10,15 @@ from django.utils.decorators import method_decorator from django.utils.feedgenerator import Rss201rev2Feed from django.utils.xmlutils import SimplerXMLGenerator from django.views.decorators.vary import vary_on_headers -from django.views.generic import FormView, ListView, TemplateView, View +from django.views.generic import FormView, ListView -from activities.models import Post, PostInteraction -from activities.services import TimelineService +from activities.models import Post +from activities.services import SearchService, TimelineService from core.decorators import cache_page, cache_page_by_ap_json from core.ld import canonicalise from core.models import Config -from django.contrib.auth.decorators import login_required from users.models import Domain, FollowStates, Identity, IdentityStates from users.services import IdentityService -from activities.services import SearchService from users.shortcuts import by_handle_or_404 diff --git a/users/views/settings/follows.py b/users/views/settings/follows.py index 7e24afc..ac59111 100644 --- a/users/views/settings/follows.py +++ b/users/views/settings/follows.py @@ -1,8 +1,6 @@ from django.db import models -from django.utils.decorators import method_decorator from django.views.generic import ListView -from django.contrib.auth.decorators import login_required from users.models import Follow, FollowStates, IdentityStates from users.views.base import IdentityViewMixin diff --git a/users/views/settings/import_export.py b/users/views/settings/import_export.py index f675363..27f27d1 100644 --- a/users/views/settings/import_export.py +++ b/users/views/settings/import_export.py @@ -1,13 +1,12 @@ import csv -from typing import Any -from django import forms, http +from django import forms +from django.contrib.auth.decorators import login_required from django.http import HttpResponse from django.shortcuts import redirect from django.utils.decorators import method_decorator from django.views.generic import FormView, View -from django.contrib.auth.decorators import login_required from users.models import Follow, InboxMessage from users.views.base import IdentityViewMixin diff --git a/users/views/settings/profile.py b/users/views/settings/profile.py index 287e6e8..30d3719 100644 --- a/users/views/settings/profile.py +++ b/users/views/settings/profile.py @@ -1,4 +1,5 @@ from django import forms +from django.contrib.auth.decorators import login_required from django.core.files import File from django.shortcuts import redirect from django.utils.decorators import method_decorator @@ -6,10 +7,9 @@ from django.views.generic import FormView from core.html import FediverseHtmlParser from core.models.config import Config -from django.contrib.auth.decorators import login_required from users.models import IdentityStates -from users.shortcuts import by_handle_or_404 from users.services import IdentityService +from users.shortcuts import by_handle_or_404 @method_decorator(login_required, name="dispatch") diff --git a/users/views/settings/security.py b/users/views/settings/security.py index 695d88f..0eda35b 100644 --- a/users/views/settings/security.py +++ b/users/views/settings/security.py @@ -1,9 +1,8 @@ from django import forms +from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from django.views.generic import FormView -from django.contrib.auth.decorators import login_required - @method_decorator(login_required, name="dispatch") class SecurityPage(FormView): diff --git a/users/views/settings/settings_page.py b/users/views/settings/settings_page.py index e0eeaf4..d4fc7f2 100644 --- a/users/views/settings/settings_page.py +++ b/users/views/settings/settings_page.py @@ -2,13 +2,13 @@ from functools import partial from typing import ClassVar from django import forms +from django.contrib.auth.decorators import login_required from django.core.files import File from django.shortcuts import redirect from django.utils.decorators import method_decorator from django.views.generic import FormView from core.models.config import Config, UploadedImage -from django.contrib.auth.decorators import login_required from users.shortcuts import by_handle_or_404