diff --git a/board/forms.py b/board/forms.py index 519369a..20dd71f 100644 --- a/board/forms.py +++ b/board/forms.py @@ -155,6 +155,10 @@ class BanForm(ModelForm): self.instance.expires = expires +class PostDeleteForm(forms.Form): + image_only = forms.CharField(widget=forms.HiddenInput(), initial="0") + + class PostModifyForm(ModelForm): """ A form used to modify the attributes of a post (sticky, locked, sink, etc) diff --git a/board/static/board/post.js b/board/static/board/post.js index 7da902c..86d0702 100644 --- a/board/static/board/post.js +++ b/board/static/board/post.js @@ -3,7 +3,8 @@ const OPEN = "open"; const CLOSED = "closed"; const replyWindowName = "reply-window"; const postWindowName = "post-window"; -const reportWindowName = "report-window" +const reportWindowName = "report-window"; +const deleteWindowName = "delete-window"; const WINDOW_INNER_PADDING = 25; @@ -204,6 +205,31 @@ function openReportWindow(reportUrl) { reportWindow.show(); } +//////////////////////////////////////////////////////////////////////////////// +// Post delete window +//////////////////////////////////////////////////////////////////////////////// +function getDeleteWindow() { + return window.top.jsFrame.getWindowByName(deleteWindowName); +} + +function openDeleteWindow(deleteUrl) { + if (window.top.jsFrame.containsWindowName(deleteWindowName)) { + getDeleteWindow().closeFrame(); + } + + let deleteWindow = window.top.jsFrame.create({ + title: "Deleting post", + name: deleteWindowName, + width: 475, + url: deleteUrl, + }); + $(deleteWindow.iframe, "iframe").on("load", () => { + fitWindowToContent(deleteWindow); + deleteWindow.setResizable(false); + }); + deleteWindow.show(); +} + //////////////////////////////////////////////////////////////////////////////// // Post hiding //////////////////////////////////////////////////////////////////////////////// @@ -365,4 +391,13 @@ window.menuItemFactories.push( addHiddenPost(id); } }) +); +window.menuItemFactories.push( + (postElement) => $("") + .text("Delete post") + .attr("href", "#") + .on("click", (e) => { + e.preventDefault(); + openDeleteWindow($(postElement).attr("data-delete-url")); + }) ); \ No newline at end of file diff --git a/board/templates/403.html b/board/templates/403.html new file mode 100644 index 0000000..d92c37c --- /dev/null +++ b/board/templates/403.html @@ -0,0 +1,8 @@ +{% extends "board/base.html" %} +{% block title %} +Permission denied +{% endblock title %} + +{% block content %} +

You do not have permission to do this.

+{% endblock content %} \ No newline at end of file diff --git a/board/templates/board/post_confirm_delete.html b/board/templates/board/post_confirm_delete.html new file mode 100644 index 0000000..d43658e --- /dev/null +++ b/board/templates/board/post_confirm_delete.html @@ -0,0 +1,23 @@ +{% extends "board/base.html" %} +{% load i18n %} + +{% block content %} +
+ {% csrf_token %} + + + + + +
+ {{form.image_only}} +
+ + +{% endblock content %} \ No newline at end of file diff --git a/board/templates/board/post_delete_success.html b/board/templates/board/post_delete_success.html new file mode 100644 index 0000000..a6fecac --- /dev/null +++ b/board/templates/board/post_delete_success.html @@ -0,0 +1,33 @@ +{% extends "board/base.html" %} +{% load i18n static %} +{# Title #} +{% block title %}{% translate "Post delete success" %}{% endblock %} +{# Body #} +{% block content %} +
+ {# We do not use pluralize filter for "seconds" because it's a pain to get it to translate. #} + {% blocktranslate %}Delete successful. This window will close in {{window_timeout}} second(s).{% endblocktranslate %} +
+ + +{% endblock %} \ No newline at end of file diff --git a/board/templates/board/post_snippet.html b/board/templates/board/post_snippet.html index 7628263..25dfe68 100644 --- a/board/templates/board/post_snippet.html +++ b/board/templates/board/post_snippet.html @@ -4,6 +4,7 @@ id="p{{post.id}}" class="post" data-report-url="{% url 'board:report_form' board.url post.id %}" + data-delete-url="{% url 'board:post_delete' post.id %}" {% if perms.board.add_ban %} data-ban-url="{% url 'board:ban_create' board.url post.id %}" {% endif %} diff --git a/board/urls.py b/board/urls.py index a1e14d1..1b35d27 100644 --- a/board/urls.py +++ b/board/urls.py @@ -19,12 +19,18 @@ urlpatterns = [ # Bans path("ban///", BanCreateView.as_view(), name="ban_create"), path("ban/success/", BanSuccessView.as_view(), name="ban_success"), - path("banned", BannedView.as_view(), name="banned"), + path("banned/", BannedView.as_view(), name="banned"), # Other moderation pages path("modify//", PostModifyView.as_view(), name="post_modify"), path( "modify/success/", PostModifySuccessView.as_view(), name="post_modify_success" ), + path("post/delete//", PostDeleteView.as_view(), name="post_delete"), + path( + "post/delete/success/", + PostDeleteSuccessView.as_view(), + name="post_delete_success", + ), ] # TODO - make this conditional so we can serve images up with whatever server we want urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/board/views.py b/board/views.py index 820d3b2..47cc214 100644 --- a/board/views.py +++ b/board/views.py @@ -8,7 +8,7 @@ from django.http import Http404, HttpResponseRedirect from django.http.request import QueryDict from django.shortcuts import get_object_or_404 from django.views.generic.base import TemplateView -from django.views.generic import edit +from django.views.generic import detail, edit from django.urls import reverse, reverse_lazy from django.utils import timezone @@ -28,6 +28,8 @@ __all__ = ( "PostModifySuccessView", "PostView", "PostSuccessView", + "PostDeleteView", + "PostDeleteSuccessView", "ReplyCreateView", "ReportView", "ReportSuccessView", @@ -163,6 +165,7 @@ class PostModifyView(PermissionRequiredMixin, edit.UpdateView): form_class = PostModifyForm template_name = "board/post_modify.html" success_url = reverse_lazy("board:post_modify_success") + raise_exception = True def has_permission(self) -> bool: return can_modify(self.request.user) @@ -180,6 +183,7 @@ class PostModifyView(PermissionRequiredMixin, edit.UpdateView): class PostModifySuccessView(PermissionRequiredMixin, TemplateView): template_name = "board/post_modify_success.html" + raise_exception = True def has_permission(self) -> bool: return can_modify(self.request.user) @@ -255,6 +259,33 @@ class PostSuccessView(TemplateView): return context +class PostDeleteView(PermissionRequiredMixin, edit.DeleteView): + model = Post + form_class = PostDeleteForm + permission_required = ("board.delete_post",) + template_name = "board/post_confirm_delete.html" + success_url = reverse_lazy("board:post_delete_success") + raise_exception = True + + def form_valid(self, form): + success_url = self.get_success_url() + if form["image_only"].value() != "0": + self.object.image.delete() + self.object.thumbnail.delete() + else: + self.object.delete() + return HttpResponseRedirect(success_url) + + +class PostDeleteSuccessView(TemplateView): + template_name = "board/post_delete_success.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["window_timeout"] = settings.BAN_WINDOW_CLOSE_TIMEOUT + return context + + class ReportView(CreateView): model = Report form_class = ReportForm @@ -290,6 +321,7 @@ class BanCreateView(PermissionRequiredMixin, edit.CreateView): form_class = BanForm permission_required = "board.add_ban" success_url = reverse_lazy("board:ban_success") + raise_exception = True def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: context = super().get_context_data(**kwargs) @@ -330,6 +362,7 @@ class BanCreateView(PermissionRequiredMixin, edit.CreateView): class BanSuccessView(PermissionRequiredMixin, TemplateView): permission_required = "ban.create" template_name = "board/ban_success.html" + raise_exception = True def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) diff --git a/threadchat/settings.py b/threadchat/settings.py index 151a529..c02ad90 100644 --- a/threadchat/settings.py +++ b/threadchat/settings.py @@ -124,6 +124,7 @@ USE_I18N = True USE_TZ = True +GUARDIAN_RENDER_403 = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.0/howto/static-files/