diff --git a/activities/admin.py b/activities/admin.py index 5417505..c319de5 100644 --- a/activities/admin.py +++ b/activities/admin.py @@ -73,7 +73,15 @@ class EmojiAdmin(admin.ModelAdmin): readonly_fields = ["preview", "created", "updated", "to_ap_tag"] - actions = ["force_execution", "approve_emoji", "reject_emoji"] + actions = ["force_execution", "approve_emoji", "reject_emoji", "copy_to_local"] + + def delete_queryset(self, request, queryset): + for instance in queryset: + # individual deletes to ensure file is deleted + instance.delete() + + def delete_model(self, request, obj): + super().delete_model(request, obj) @admin.action(description="Force Execution") def force_execution(self, request, queryset): @@ -96,6 +104,17 @@ class EmojiAdmin(admin.ModelAdmin): f'' ) + @admin.action(description="Copy Emoji to Local") + def copy_to_local(self, request, queryset): + emojis = {} + for instance in queryset: + emoji = instance.copy_to_local(save=False) + if emoji: + emojis[emoji.shortcode] = emoji + + Emoji.objects.bulk_create(emojis.values(), batch_size=50, ignore_conflicts=True) + Emoji.locals = Emoji.load_locals() + @admin.register(PostAttachment) class PostAttachmentAdmin(admin.ModelAdmin): diff --git a/activities/models/emoji.py b/activities/models/emoji.py index 61a0cd8..0e29a47 100644 --- a/activities/models/emoji.py +++ b/activities/models/emoji.py @@ -9,6 +9,7 @@ from asgiref.sync import sync_to_async from cachetools import TTLCache, cached from django.conf import settings from django.core.exceptions import ValidationError +from django.core.files.base import ContentFile from django.db import models from django.utils.safestring import mark_safe @@ -131,9 +132,15 @@ class Emoji(StatorModel): admin_delete = "{admin}{self.pk}/delete/" admin_enable = "{admin}{self.pk}/enable/" admin_disable = "{admin}{self.pk}/disable/" + admin_copy = "{admin}{self.pk}/copy/" emoji_regex = re.compile(r"\B:([a-zA-Z0-9(_)-]+):\B") + def delete(self, using=None, keep_parents=False): + if self.file: + self.file.delete() + return super().delete(using=using, keep_parents=keep_parents) + def clean(self): super().clean() if self.local ^ (self.domain is None): @@ -195,6 +202,40 @@ class Emoji(StatorModel): ) return self.fullcode + @property + def can_copy_local(self): + if not hasattr(Emoji, "locals"): + Emoji.locals = Emoji.load_locals() + return not self.local and self.is_usable and self.shortcode not in Emoji.locals + + def copy_to_local(self, *, save: bool = True): + """ + Copy this (non-local) Emoji to local for use by Users of this instance. Returns + the Emoji instance, or None if the copy failed to happen. Specify save=False to + return the object without saving to database (for bulk saving). + """ + if not self.can_copy_local: + return None + + emoji = None + if self.file: + # new emoji gets its own copy of the file + file = ContentFile(self.file.read()) + file.name = self.file.name + emoji = Emoji( + shortcode=self.shortcode, + domain=None, + local=True, + mimetype=self.mimetype, + file=file, + category=self.category, + ) + if save: + emoji.save() + # add this new one to the locals cache + Emoji.locals[self.shortcode] = emoji + return emoji + @classmethod def emojis_from_content(cls, content: str, domain: Domain | None) -> list["Emoji"]: """ diff --git a/takahe/urls.py b/takahe/urls.py index d6046b1..97bb8d4 100644 --- a/takahe/urls.py +++ b/takahe/urls.py @@ -173,6 +173,7 @@ urlpatterns = [ path("admin/emoji//enable/", admin.EmojiEnable.as_view()), path("admin/emoji//disable/", admin.EmojiEnable.as_view(enable=False)), path("admin/emoji//delete/", admin.EmojiDelete.as_view()), + path("admin/emoji//copy/", admin.EmojiCopyLocal.as_view()), path( "admin/announcements/", admin.AnnouncementsRoot.as_view(), diff --git a/templates/admin/emoji.html b/templates/admin/emoji.html index dda415b..670db55 100644 --- a/templates/admin/emoji.html +++ b/templates/admin/emoji.html @@ -32,6 +32,9 @@ + {% if emoji.can_copy_local %} + + {% endif %} {% if not emoji.is_usable %} Disabled diff --git a/users/views/admin/__init__.py b/users/views/admin/__init__.py index 7a8f5ca..02f6c13 100644 --- a/users/views/admin/__init__.py +++ b/users/views/admin/__init__.py @@ -17,6 +17,7 @@ from users.views.admin.domains import ( # noqa Domains, ) from users.views.admin.emoji import ( # noqa + EmojiCopyLocal, EmojiCreate, EmojiDelete, EmojiEnable, diff --git a/users/views/admin/emoji.py b/users/views/admin/emoji.py index fc521a9..a108d13 100644 --- a/users/views/admin/emoji.py +++ b/users/views/admin/emoji.py @@ -99,3 +99,18 @@ class EmojiEnable(HTMXActionView): def action(self, emoji: Emoji): emoji.public = self.enable emoji.save() + + +@method_decorator(moderator_required, name="dispatch") +class EmojiCopyLocal(HTMXActionView): + """ + Duplicates a domain emoji to be local, as long as it is usable and the + shortcode is available. + """ + + model = Emoji + + def action(self, emoji: Emoji): + # Force reload locals cache to avoid potential for shortcode dupes + Emoji.locals = Emoji.load_locals() + emoji.copy_to_local()