Skip to content

Commit

Permalink
Add new fields to attachments, WIP move attachments admin
Browse files Browse the repository at this point in the history
  • Loading branch information
rafalp committed Jan 5, 2025
1 parent ead4b88 commit ef1b351
Show file tree
Hide file tree
Showing 21 changed files with 489 additions and 17 deletions.
48 changes: 48 additions & 0 deletions misago/admin/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from django.urls import path
from django.utils.translation import pgettext_lazy

from .attachments import views as attachments
from .attachmenttypes import views as attachmenttypes
from .categories import views as categories
from .groups import views as groups
from .moderators import views as moderators
Expand All @@ -27,6 +29,22 @@ def register_navigation_nodes(self, site):
after="categories:index",
namespace="moderators",
)
site.add_node(
name=pgettext_lazy("admin node", "Attachments"),
icon="fas fa-paperclip",
after="permissions:index",
namespace="attachments",
)
site.add_node(
name=pgettext_lazy("admin node", "Attachment types"),
description=pgettext_lazy(
"admin node",
"Specify what files may be uploaded as part of user posts.",
),
parent="settings",
after="agreements:index",
namespace="attachment-types",
)

def register_urlpatterns(self, urlpatterns):
urlpatterns.namespace("groups/", "groups")
Expand Down Expand Up @@ -84,3 +102,33 @@ def register_urlpatterns(self, urlpatterns):
path("edit/<int:pk>/", moderators.EditView.as_view(), name="edit"),
path("delete/<int:pk>/", moderators.DeleteView.as_view(), name="delete"),
)

urlpatterns.namespace("attachments/", "attachments")
urlpatterns.patterns(
"attachments",
path("", attachments.AttachmentsList.as_view(), name="index"),
path("<int:page>/", attachments.AttachmentsList.as_view(), name="index"),
path(
"delete/<int:pk>/",
attachments.DeleteAttachment.as_view(),
name="delete",
),
)

# AttachmentType
urlpatterns.namespace("attachment-types/", "attachment-types", "settings")
urlpatterns.patterns(
"settings:attachment-types",
path("", attachmenttypes.AttachmentTypesList.as_view(), name="index"),
path("new/", attachmenttypes.NewAttachmentType.as_view(), name="new"),
path(
"edit/<int:pk>/",
attachmenttypes.EditAttachmentType.as_view(),
name="edit",
),
path(
"delete/<int:pk>/",
attachmenttypes.DeleteAttachmentType.as_view(),
name="delete",
),
)
File renamed without changes.
70 changes: 70 additions & 0 deletions misago/admin/attachments/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from django import forms
from django.utils.translation import pgettext_lazy

from ...attachments.models import AttachmentType


def get_searchable_filetypes():
choices = [(0, pgettext_lazy("admin attachments type filter choice", "All types"))]
choices += [(a.id, a.name) for a in AttachmentType.objects.order_by("name")]
return choices


class FilterAttachmentsForm(forms.Form):
uploader = forms.CharField(
label=pgettext_lazy("admin attachments filter form", "Uploader name contains"),
required=False,
)
filename = forms.CharField(
label=pgettext_lazy("admin attachments filter form", "Filename contains"),
required=False,
)
filetype = forms.TypedChoiceField(
label=pgettext_lazy("admin attachments filter form", "File type"),
coerce=int,
choices=get_searchable_filetypes,
empty_value=0,
required=False,
)
is_orphan = forms.ChoiceField(
label=pgettext_lazy("admin attachments filter form", "State"),
required=False,
choices=[
(
"",
pgettext_lazy(
"admin attachments orphan filter choice",
"All",
),
),
(
"yes",
pgettext_lazy(
"admin attachments orphan filter choice",
"Only orphaned",
),
),
(
"no",
pgettext_lazy(
"admin attachments orphan filter choice",
"Not orphaned",
),
),
],
)

def filter_queryset(self, criteria, queryset):
if criteria.get("uploader"):
queryset = queryset.filter(
uploader_slug__contains=criteria["uploader"].lower()
)
if criteria.get("filename"):
queryset = queryset.filter(filename__icontains=criteria["filename"])
if criteria.get("filetype"):
queryset = queryset.filter(filetype_id=criteria["filetype"])
if criteria.get("is_orphan") == "yes":
queryset = queryset.filter(post__isnull=True)
elif criteria.get("is_orphan") == "no":
queryset = queryset.filter(post__isnull=False)
return queryset
106 changes: 106 additions & 0 deletions misago/admin/attachments/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from django.contrib import messages
from django.db import transaction
from django.utils.translation import pgettext, pgettext_lazy

from ...attachments.models import Attachment
from ...threads.models import Post
from ..views import generic
from .forms import FilterAttachmentsForm


class AttachmentAdmin(generic.AdminBaseMixin):
root_link = "misago:admin:attachments:index"
model = Attachment
templates_dir = "misago/admin/attachments"
message_404 = pgettext_lazy(
"admin attachments", "Requested attachment does not exist."
)

def get_queryset(self):
qs = super().get_queryset()
return qs.select_related(
"filetype", "uploader", "post", "post__thread", "post__category"
)


class AttachmentsList(AttachmentAdmin, generic.ListView):
items_per_page = 20
ordering = [
("-id", pgettext_lazy("admin attachments ordering choice", "From newest")),
("id", pgettext_lazy("admin attachments ordering choice", "From oldest")),
("filename", pgettext_lazy("admin attachments ordering choice", "A to z")),
("-filename", pgettext_lazy("admin attachments ordering choice", "Z to a")),
("size", pgettext_lazy("admin attachments ordering choice", "Smallest files")),
("-size", pgettext_lazy("admin attachments ordering choice", "Largest files")),
]
selection_label = pgettext_lazy("admin attachments", "With attachments: 0")
empty_selection_label = pgettext_lazy("admin attachments", "Select attachments")
mass_actions = [
{
"action": "delete",
"name": pgettext_lazy("admin attachments", "Delete attachments"),
"confirmation": pgettext_lazy(
"admin attachments",
"Are you sure you want to delete selected attachments?",
),
"is_atomic": False,
}
]
filter_form = FilterAttachmentsForm

def action_delete(self, request, attachments):
deleted_attachments = []
desynced_posts = []

for attachment in attachments:
if attachment.post:
deleted_attachments.append(attachment.pk)
desynced_posts.append(attachment.post_id)

if desynced_posts:
with transaction.atomic():
for post in Post.objects.filter(id__in=desynced_posts):
self.delete_from_cache(post, deleted_attachments)

for attachment in attachments:
attachment.delete()

message = pgettext(
"admin attachments", "Selected attachments have been deleted."
)
messages.success(request, message)

def delete_from_cache(self, post, attachments):
if not post.attachments_cache:
return # admin action may be taken due to desynced state

clean_cache = []
for a in post.attachments_cache:
if a["id"] not in attachments:
clean_cache.append(a)

post.attachments_cache = clean_cache or None
post.save(update_fields=["attachments_cache"])


class DeleteAttachment(AttachmentAdmin, generic.ButtonView):
def button_action(self, request, target):
if target.post:
self.delete_from_cache(target)
target.delete()
message = pgettext(
"admin attachments", 'Attachment "%(filename)s" has been deleted.'
)
messages.success(request, message % {"filename": target.filename})

def delete_from_cache(self, attachment):
if not attachment.post.attachments_cache:
return # admin action may be taken due to desynced state

clean_cache = []
for a in attachment.post.attachments_cache:
if a["id"] != attachment.id:
clean_cache.append(a)

attachment.post.attachments_cache = clean_cache or None
attachment.post.save(update_fields=["attachments_cache"])
File renamed without changes.
82 changes: 82 additions & 0 deletions misago/admin/attachmenttypes/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from django import forms
from django.utils.translation import pgettext, pgettext_lazy

from ...attachments.models import AttachmentType


class AttachmentTypeForm(forms.ModelForm):
class Meta:
model = AttachmentType
fields = [
"name",
"extensions",
"mimetypes",
"size_limit",
"status",
"limit_uploads_to",
"limit_downloads_to",
]
labels = {
"name": pgettext_lazy("admin attachment type form", "Type name"),
"extensions": pgettext_lazy(
"admin attachment type form", "File extensions"
),
"mimetypes": pgettext_lazy("admin attachment type form", "Mimetypes"),
"size_limit": pgettext_lazy(
"admin attachment type form", "Maximum allowed uploaded file size"
),
"status": pgettext_lazy("admin attachment type form", "Status"),
"limit_uploads_to": pgettext_lazy(
"admin attachment type form", "Limit uploads to"
),
"limit_downloads_to": pgettext_lazy(
"admin attachment type form", "Limit downloads to"
),
}
help_texts = {
"extensions": pgettext_lazy(
"admin attachment type form",
"List of comma separated file extensions associated with this attachment type.",
),
"mimetypes": pgettext_lazy(
"admin attachment type form",
"Optional list of comma separated mime types associated with this attachment type.",
),
"size_limit": pgettext_lazy(
"admin attachment type form",
"Maximum allowed uploaded file size for this type, in kb. This setting is deprecated and has no effect. It will be deleted in Misago 1.0.",
),
"status": pgettext_lazy(
"admin attachment type form",
"Controls this attachment type availability on your site.",
),
"limit_uploads_to": pgettext_lazy(
"admin attachment type form",
"If you wish to limit option to upload files of this type to users with specific roles, select them on this list. Otherwise don't select any roles to allow all users with permission to upload attachments to be able to upload attachments of this type.",
),
"limit_downloads_to": pgettext_lazy(
"admin attachment type form",
"If you wish to limit option to download files of this type to users with specific roles, select them on this list. Otherwise don't select any roles to allow all users with permission to download attachments to be able to download attachments of this type.",
),
}
widgets = {
"limit_uploads_to": forms.CheckboxSelectMultiple,
"limit_downloads_to": forms.CheckboxSelectMultiple,
}

def clean_extensions(self):
data = self.clean_list(self.cleaned_data["extensions"])
if not data:
raise forms.ValidationError(
pgettext("admin attachment type form", "This field is required.")
)
return data

def clean_mimetypes(self):
data = self.cleaned_data["mimetypes"]
if data:
return self.clean_list(data)

def clean_list(self, value):
items = [v.lstrip(".") for v in value.lower().replace(" ", "").split(",")]
return ",".join(set(filter(bool, items)))
63 changes: 63 additions & 0 deletions misago/admin/attachmenttypes/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from django.contrib import messages
from django.db.models import Count
from django.utils.translation import pgettext, pgettext_lazy

from ...attachments.models import AttachmentType
from ..views import generic
from .forms import AttachmentTypeForm


class AttachmentTypeAdmin(generic.AdminBaseMixin):
root_link = "misago:admin:settings:attachment-types:index"
model = AttachmentType
form_class = AttachmentTypeForm
templates_dir = "misago/admin/attachmenttypes"
message_404 = pgettext_lazy(
"admin attachments types", "Requested attachment type does not exist."
)

def update_roles(self, target, roles):
target.roles.clear()
if roles:
target.roles.add(*roles)

def handle_form(self, form, request, target):
super().handle_form(form, request, target)
form.save()


class AttachmentTypesList(AttachmentTypeAdmin, generic.ListView):
ordering = (("name", None),)

def get_queryset(self):
queryset = super().get_queryset()
return queryset.annotate(num_files=Count("attachment"))


class NewAttachmentType(AttachmentTypeAdmin, generic.ModelFormView):
message_submit = pgettext_lazy(
"admin attachments types", 'New type "%(name)s" has been saved.'
)


class EditAttachmentType(AttachmentTypeAdmin, generic.ModelFormView):
message_submit = pgettext_lazy(
"admin attachments types", 'Attachment type "%(name)s" has been edited.'
)


class DeleteAttachmentType(AttachmentTypeAdmin, generic.ButtonView):
def check_permissions(self, request, target):
if target.attachment_set.exists():
message = pgettext(
"admin attachments types",
'Attachment type "%(name)s" has associated attachments and can\'t be deleted.',
)
return message % {"name": target.name}

def button_action(self, request, target):
target.delete()
message = pgettext(
"admin attachments types", 'Attachment type "%(name)s" has been deleted.'
)
messages.success(request, message % {"name": target.name})
Loading

0 comments on commit ef1b351

Please sign in to comment.