import functools from typing import Any, Dict from django.conf import settings from django.contrib.auth import get_user from django.contrib.auth.mixins import PermissionRequiredMixin from django.db.models import Q 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 detail, edit, list from django.urls import reverse, reverse_lazy from django.utils import timezone from guardian.shortcuts import get_objects_for_user from board.forms import * from board.models import Ban, BanTemplate, Board, NewsPost, Post, Report from board.utils import * __all__ = ( "ActionSuccessView", "BanCreateView", "BanSuccessView", "BannedView", "BoardView", "NewsListView", "PostCreateView", "PostModifyView", "PostModifySuccessView", "PostView", "PostSuccessView", "PostDeleteView", "PostWipeView", "ReplyCreateView", "ReportView", ) def can_modify(user): if not user: return False # TODO add more permissions as required return ( user.has_perm("board.set_sticky") or user.has_perm("board.set_bump") or user.has_perm("board.set_lock") ) class ActionSuccessView(TemplateView): template_name = "board/action_success.html" message = "Action completed." window_timeout = 0 def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: context = super().get_context_data(**kwargs) context["message"] = self.message context["window_timeout"] = self.window_timeout return context class BannedView(TemplateView): template_name = "board/banned.html" def get_context_data(self, **kwargs): context = super(TemplateView, self).get_context_data(**kwargs) ip = get_client_ip(self.request) bans = get_ip_bans(ip) now = timezone.now() active_bans = [ban for ban in bans if not ban.expires or ban.expires > now] context["bans"] = bans context["active_bans"] = active_bans context["ip"] = ip return context class BoardMixin: @functools.cached_property def board(self) -> Board: return get_object_or_404(Board, url=self.kwargs["url"]) # type: ignore class NewsSnippetMixin: def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["news"] = NewsPost.objects.order_by("-id")[:3] return context class CreateView(BoardMixin, edit.CreateView): """ Helper class that sets a few variables for posts and check against bans. This should not be used by itself. This class sets the following variables on GET and POST: * self.board """ def get_form_kwargs(self): kwargs = super(CreateView, self).get_form_kwargs() kwargs["board"] = self.board kwargs["ip"] = get_client_ip(self.request) return kwargs def dispatch(self, request, *args, **kwargs): # Check for bans ip = get_client_ip(request) if request.method == "POST" and is_banned(ip, self.board): return HttpResponseRedirect(reverse("board:banned")) else: return super(CreateView, self).dispatch(request, *args, **kwargs) class BoardView(NewsSnippetMixin, BoardMixin, TemplateView): template_name = "board/board_detail.html" def get(self, request, *args, **kwargs): # If the page isn't set, then redirect to the /page/1 url if "page" not in kwargs: return HttpResponseRedirect( reverse("board:board_detail", kwargs={"url": kwargs["url"], "page": 1}) ) return super(BoardView, self).get(request, *args, **kwargs) def get_context_data(self, **kwargs): page = self.kwargs["page"] if page not in range(1, self.board.max_pages + 1): raise Http404() threads_per_page = self.board.threads_per_page start = (page - 1) * threads_per_page end = start + threads_per_page thread_count = self.board.threads.count() last_page = (thread_count // threads_per_page) + 1 kwargs["board"] = self.board kwargs["threads"] = self.board.threads.order_by("-sticky", "-last_bump")[ start:end ] kwargs["current_page"] = page kwargs["pages"] = range(1, last_page + 1) kwargs["last_page"] = last_page kwargs["max_upload_size"] = settings.MAX_UPLOAD_SIZE return super(BoardView, self).get_context_data(**kwargs) class PostCreateView(CreateView): model = Post form_class = PostForm template_name = "board/post_create.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["board"] = self.board context["max_upload_size"] = settings.MAX_UPLOAD_SIZE context["capcodes"] = get_objects_for_user( get_user(self.request), "board.use_capcode" ) return context def get_form_kwargs(self): kwargs = super(PostCreateView, self).get_form_kwargs() kwargs["user"] = self.request.user if "user_token" not in self.request.session: # Generate a user token self.request.session["user_token"] = generate_user_token() kwargs["user_token"] = self.request.session["user_token"] return kwargs def get_success_url(self) -> str: query = QueryDict(mutable=True) query["next"] = self.get_form().instance.get_absolute_url().split("#")[0] return reverse("board:post_success") + "?" + query.urlencode() class PostModifyView(PermissionRequiredMixin, edit.UpdateView): model = Post 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) def dispatch(self, request, *args, **kwargs): self.post_obj = get_object_or_404(Post, id=kwargs["pk"]) self.board = self.post_obj.board return super(PostModifyView, self).dispatch(request, *args, **kwargs) def get_form_kwargs(self) -> Dict[str, Any]: kwargs = super(PostModifyView, self).get_form_kwargs() kwargs["user"] = self.request.user return kwargs class PostModifySuccessView(PermissionRequiredMixin, ActionSuccessView): raise_exception = True def has_permission(self) -> bool: return can_modify(self.request.user) class ReplyCreateView(CreateView): model = Post form_class = ReplyForm template_name = "board/reply_create.html" success_url = reverse_lazy("board:post_success") def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["board"] = self.board post_id = self.kwargs["id"] context["post"] = get_object_or_404(Post, id=post_id) context["max_upload_size"] = settings.MAX_UPLOAD_SIZE context["capcodes"] = get_objects_for_user( get_user(self.request), "board.use_capcode" ) return context def get_form_kwargs(self): kwargs = super(ReplyCreateView, self).get_form_kwargs() post_id = self.kwargs["id"] post = get_object_or_404(Post, id=post_id) kwargs["op"] = post kwargs["user"] = self.request.user if "user_token" not in self.request.session: # Generate a user token self.request.session["user_token"] = generate_user_token() kwargs["user_token"] = self.request.session["user_token"] return kwargs def get_success_url(self) -> str: query = QueryDict(mutable=True) query["next"] = self.get_form().instance.get_absolute_url() return reverse("board:post_success") + "?" + query.urlencode() class PostView(NewsSnippetMixin, TemplateView): template_name = "board/post_detail.html" def get_context_data(self, **kwargs): kwargs["board"] = self.board post_id = self.kwargs["id"] post = get_object_or_404(Post, id=post_id) kwargs["post"] = post kwargs["replies"] = post.replies.all().order_by("id") kwargs["max_upload_size"] = settings.MAX_UPLOAD_SIZE return super(PostView, self).get_context_data(**kwargs) def dispatch(self, request, *args, **kwargs): # Set the board on this object self.board = get_object_or_404(Board, url=kwargs["url"]) return super(PostView, self).dispatch(request, *args, **kwargs) class PostSuccessView(ActionSuccessView): template_name = "board/post_success.html" window_timeout = settings.POST_WINDOW_CLOSE_TIMEOUT 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 has_permission(self) -> bool: object = self.get_object() user_token = self.request.session.get("user_token", None) return self.request.user.has_perm("board.delete_post") or ( user_token and object.user_token == user_token ) 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 PostWipeView(PermissionRequiredMixin, detail.SingleObjectMixin, TemplateView): model = Post permission_required = ("wipe_user",) template_name = "board/post_wipe.html" success_url = reverse_lazy("board:post_wipe_success") @property def all_posts(self): return Post.objects.filter( Q(ip=self.object.ip) | Q(user_token=self.object.user_token) ) def dispatch(self, request, *args, **kwargs): self.object = self.get_object() return super(PostWipeView, self).dispatch(request, *args, **kwargs) def post(self, request, *args, **kwargs): # Wipe user's posts self.all_posts.delete() # Success redirect success_url = self.get_success_url() return HttpResponseRedirect(success_url) def get_context_data(self, **kwargs): context = super(PostWipeView, self).get_context_data(**kwargs) # context["all_posts"] = self.all_posts # for some reason, {{all_posts.count}} does not seem to work in the template. context["all_posts_count"] = self.all_posts.count() return context def get_success_url(self): return self.success_url class ReportView(CreateView): model = Report form_class = ReportForm success_url = reverse_lazy("board:report_success") @property def board_url(self) -> str: return self.kwargs["url"] @property def post_id(self) -> int: return self.kwargs["id"] def get_form_kwargs(self): kwargs = super(ReportView, self).get_form_kwargs() post_id = self.kwargs["id"] post = get_object_or_404(Post, id=post_id) kwargs["op"] = post return kwargs class BanCreateView(PermissionRequiredMixin, edit.CreateView): model = Ban 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) context["board"] = self.board context["post"] = self.post_obj ip = self.post_obj.ip now = timezone.now() bans = [ ban for ban in get_ip_bans(ip) if ban.board == self.board or ban.board is None ] context["previous_bans"] = [ ban for ban in bans if ban.expires and ban.expires < now ] context["current_bans"] = [ ban for ban in bans if not ban.expires or ban.expires > now ] context["templates"] = BanTemplate.objects.filter( Q(board=self.board) | Q(board=None) ) return context def get_form_kwargs(self) -> Dict[str, Any]: kwargs = super(edit.CreateView, self).get_form_kwargs() post_id = self.kwargs["id"] post = get_object_or_404(Post, id=post_id) kwargs["op"] = post return kwargs def dispatch(self, request, *args, **kwargs): self.board = get_object_or_404(Board, url=kwargs["url"]) self.post_obj = get_object_or_404(Post, pk=kwargs["id"]) return super().dispatch(request, *args, **kwargs) class BanSuccessView(PermissionRequiredMixin, ActionSuccessView): permission_required = "ban.create" raise_exception = True class NewsListView(list.ListView): model = NewsPost template_name = "board/news_list.html" ordering = ("-created",)