From ab749359fab1482675da1a32f91863040128de24 Mon Sep 17 00:00:00 2001 From: Alek Ratzloff Date: Tue, 28 Jun 2022 21:37:45 -0700 Subject: [PATCH] Add admin capcodes Sometimes, it is necessary for an admin or other moderator to reveal themselves. Capcodes allow them to do so by appending a special colored portion at the end of their name on a post. This adds object-level permissions, so if you have a moderator who shouldn't be able to use the "admin" capcode, you can give them permission only to the "moderator" capcode. Signed-off-by: Alek Ratzloff --- Pipfile | 1 + Pipfile.lock | 60 +++++++++++--------- board/admin.py | 18 +++--- board/forms.py | 16 +++++- board/models.py | 20 ++++++- board/templates/board/post_snippet.html | 7 ++- board/templates/board/reply_create_view.html | 18 +++++- board/views.py | 15 ++++- threadchat/settings.py | 1 + 9 files changed, 110 insertions(+), 46 deletions(-) diff --git a/Pipfile b/Pipfile index d6e7cfe..3d33cd7 100644 --- a/Pipfile +++ b/Pipfile @@ -9,6 +9,7 @@ pillow = "*" django-hcaptcha = "*" django-environ = "*" django-guardian = "*" +django-colorfield = "*" [dev-packages] mypy = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 736e5c5..963ebec 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a71e0de66e9b1b37118b78315b96e0469e01b88a3fb67e86b78686500e178c54" + "sha256": "d6de19da5ba20fe12b2634f6cec0326fca71557518a5e9fb591824ff63021ee6" }, "pipfile-spec": 6, "requires": { @@ -32,6 +32,14 @@ "index": "pypi", "version": "==4.1b1" }, + "django-colorfield": { + "hashes": [ + "sha256:08d093d5b31b599b681665ab97b957045c4a4c0b93cfc434bd257378a7193c96", + "sha256:a58d1c5a56f0380439dc1305a131321b839df35ddfac2a6dbcc887de02232120" + ], + "index": "pypi", + "version": "==0.7.1" + }, "django-environ": { "hashes": [ "sha256:bff5381533056328c9ac02f71790bd5bf1cea81b1beeb648f28b81c9e83e0a21", @@ -112,32 +120,32 @@ "develop": { "black": { "hashes": [ - "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b", - "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176", - "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09", - "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a", - "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015", - "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79", - "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb", - "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20", - "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464", - "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968", - "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82", - "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21", - "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0", - "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265", - "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b", - "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a", - "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72", - "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce", - "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0", - "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a", - "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163", - "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad", - "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d" + "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90", + "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c", + "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78", + "sha256:4af5bc0e1f96be5ae9bd7aaec219c901a94d6caa2484c21983d043371c733fc4", + "sha256:560558527e52ce8afba936fcce93a7411ab40c7d5fe8c2463e279e843c0328ee", + "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e", + "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e", + "sha256:6c1734ab264b8f7929cef8ae5f900b85d579e6cbfde09d7387da8f04771b51c6", + "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9", + "sha256:7ba9be198ecca5031cd78745780d65a3f75a34b2ff9be5837045dce55db83d1c", + "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256", + "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f", + "sha256:a3db5b6409b96d9bd543323b23ef32a1a2b06416d525d27e0f67e74f1446c8f2", + "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c", + "sha256:b154e6bbde1e79ea3260c4b40c0b7b3109ffcdf7bc4ebf8859169a6af72cd70b", + "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807", + "sha256:b9fd45787ba8aa3f5e0a0a98920c1012c884622c6c920dbe98dbd05bc7c70fbf", + "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def", + "sha256:c9a3ac16efe9ec7d7381ddebcc022119794872abce99475345c5a61aa18c45ad", + "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d", + "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849", + "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69", + "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666" ], "index": "pypi", - "version": "==22.3.0" + "version": "==22.6.0" }, "click": { "hashes": [ @@ -203,7 +211,7 @@ "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" ], - "markers": "python_version < '3.11'", + "markers": "python_full_version < '3.11.0a7'", "version": "==2.0.1" }, "typing-extensions": { diff --git a/board/admin.py b/board/admin.py index 6ecc685..c30ca47 100644 --- a/board/admin.py +++ b/board/admin.py @@ -1,15 +1,10 @@ from django.contrib import admin from django.urls import reverse from django.utils.safestring import mark_safe -from board.models import ( - Ban, - BanTemplate, - Board, - Post, - RangeBan, - ReportReason, - ReportRecord, -) + +from guardian.admin import GuardedModelAdmin + +from board.models import * # # Admin sites @@ -82,3 +77,8 @@ class BanAdmin(admin.ModelAdmin): @admin.register(BanTemplate) class BanTemplateAdmin(admin.ModelAdmin): ordering = ("board__url", "name") + + +@admin.register(Capcode) +class CapcodeAdmin(GuardedModelAdmin): + pass diff --git a/board/forms.py b/board/forms.py index a7e86f5..0cf3b56 100644 --- a/board/forms.py +++ b/board/forms.py @@ -1,3 +1,4 @@ +from django.core.exceptions import ValidationError from django.conf import settings from django.db import transaction from django.db.models import Q @@ -39,12 +40,21 @@ class ReplyForm(PostForm): class Meta: model = Post - fields = ["name", "text", "bump", "image"] + fields = ["name", "text", "bump", "capcode", "image"] - def __init__(self, *args, op, reply, **kwargs): + def __init__(self, *args, user, op, **kwargs): super(ReplyForm, self).__init__(*args, **kwargs) self.instance.op = op - self.instance.reply = reply + self.user = user + + def clean(self): + super().clean() + # TODO + # Check if the user has the right permissions to use the selected capcode + capcode = self.cleaned_data["capcode"] + if capcode: + if not self.user or not self.user.has_perm("board.use_capcode", capcode): + raise ValidationError("Could not create post") class ReportForm(ModelForm): diff --git a/board/models.py b/board/models.py index 7c4a74c..15a3047 100644 --- a/board/models.py +++ b/board/models.py @@ -2,17 +2,21 @@ from datetime import timedelta import os from pathlib import Path from django.conf import settings +from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError from django.core.files.base import ContentFile from django.db import models from django.db.models import signals +from django.db.models.deletion import SET_NULL from django.dispatch import receiver from django.urls import reverse from django.utils import timezone from django.utils.translation import gettext as _ -from PIL import Image from io import BytesIO +from colorfield.fields import ColorField +from PIL import Image + def image_upload(instance, filename): op_id = instance.op.id if instance.op else instance.id @@ -36,6 +40,9 @@ def thumbs_upload(instance, filename): return f"{instance.board.url}/{now_sec}t{ext}" +User = get_user_model() + + class Board(models.Model): # The short URL name for the board url = models.CharField(max_length=255, null=False, blank=False, unique=True) @@ -84,6 +91,8 @@ class Post(models.Model): text = models.TextField(max_length=10000, null=False, blank=True) # The IP address of the user that made this post ip = models.GenericIPAddressField() + # Capcode + capcode = models.ForeignKey("Capcode", null=True, blank=True, on_delete=SET_NULL) # Creation time created = models.DateTimeField(auto_now_add=True) # Last bump time @@ -334,7 +343,12 @@ class BanTemplate(models.Model): class Capcode(models.Model): - # Content to display *before* the capcoded user's name. - prefix = models.CharField(max_length=100) # Content to display *after* the capcoded user's name. suffix = models.CharField(max_length=100) + color = ColorField() + + class Meta: + permissions = (("use_capcode", "Can use capcode"),) + + def __str__(self) -> str: + return self.suffix diff --git a/board/templates/board/post_snippet.html b/board/templates/board/post_snippet.html index ebced2b..8297397 100644 --- a/board/templates/board/post_snippet.html +++ b/board/templates/board/post_snippet.html @@ -30,9 +30,14 @@ {% if post.subject %} {{post.subject}} {% endif %} -{% blocktranslate with post_name=post.name|default:"Anonymous" post_created=post.created %} +{% blocktranslate with post_name=post.name|default:"Anonymous" %} by {{post_name}} +{% endblocktranslate %} +{% if post.capcode %} +{{post.capcode}} +{% endif %} +{% blocktranslate with post_created=post.created %} at {{post_created}} {% endblocktranslate %} {% if reply_link %} diff --git a/board/templates/board/reply_create_view.html b/board/templates/board/reply_create_view.html index 94ff748..5b1fc17 100644 --- a/board/templates/board/reply_create_view.html +++ b/board/templates/board/reply_create_view.html @@ -5,7 +5,23 @@
{% csrf_token %} - {{ form.as_table }} + {# {{ form.as_table }} #} + {% for field in form %} + {% if field.name != "capcode" %} + + + + + {% endif %} + {% endfor %} + {% if capcodes %} + + + + + {% endif %}
{{field.label_tag}}{{field}}
{{form.capcode.label_tag}} + {{form.capcode}} +
  diff --git a/board/views.py b/board/views.py index e1a1498..74f09e5 100644 --- a/board/views.py +++ b/board/views.py @@ -1,5 +1,6 @@ from typing import Any, Dict from django.conf import settings +from django.contrib.auth import get_user_model, get_user from django.contrib.auth.mixins import PermissionRequiredMixin from django.core.exceptions import ImproperlyConfigured from django.db.models import Q @@ -9,6 +10,9 @@ from django.views.generic.base import TemplateView from django.views.generic import edit from django.urls import reverse, reverse_lazy from django.utils import timezone + +from guardian.shortcuts import get_objects_for_user + from board.forms import BanForm, PostForm, ReplyForm, ReportForm from board.models import Ban, BanTemplate, Board, Post, Report from board.utils import * @@ -26,6 +30,9 @@ __all__ = ( ) +User = get_user_model() + + class BannedView(TemplateView): template_name = "board/banned.html" @@ -121,7 +128,9 @@ class ReplyCreateView(CreateView): post_id = self.kwargs["id"] context["post"] = get_object_or_404(Post, id=post_id) context["max_upload_size"] = settings.MAX_UPLOAD_SIZE - print(context["board"]) + context["capcodes"] = get_objects_for_user( + get_user(self.request), "board.use_capcode" + ) return context def get_form_kwargs(self): @@ -129,7 +138,7 @@ class ReplyCreateView(CreateView): post_id = self.kwargs["id"] post = get_object_or_404(Post, id=post_id) kwargs["op"] = post - kwargs["reply"] = post + kwargs["user"] = self.request.user return kwargs @@ -154,7 +163,7 @@ class PostView(CreateView): post_id = self.kwargs["id"] post = get_object_or_404(Post, id=post_id) kwargs["op"] = post - kwargs["reply"] = post + kwargs["user"] = self.request.user return kwargs diff --git a/threadchat/settings.py b/threadchat/settings.py index f424dcd..151a529 100644 --- a/threadchat/settings.py +++ b/threadchat/settings.py @@ -43,6 +43,7 @@ INSTALLED_APPS = [ "django.contrib.messages", "django.contrib.staticfiles", "guardian", + "colorfield", "board", ]