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 <alekratz@gmail.com>
This commit is contained in:
2022-06-28 21:37:45 -07:00
parent 29dff20db9
commit ab749359fa
9 changed files with 110 additions and 46 deletions

View File

@@ -9,6 +9,7 @@ pillow = "*"
django-hcaptcha = "*" django-hcaptcha = "*"
django-environ = "*" django-environ = "*"
django-guardian = "*" django-guardian = "*"
django-colorfield = "*"
[dev-packages] [dev-packages]
mypy = "*" mypy = "*"

60
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "a71e0de66e9b1b37118b78315b96e0469e01b88a3fb67e86b78686500e178c54" "sha256": "d6de19da5ba20fe12b2634f6cec0326fca71557518a5e9fb591824ff63021ee6"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@@ -32,6 +32,14 @@
"index": "pypi", "index": "pypi",
"version": "==4.1b1" "version": "==4.1b1"
}, },
"django-colorfield": {
"hashes": [
"sha256:08d093d5b31b599b681665ab97b957045c4a4c0b93cfc434bd257378a7193c96",
"sha256:a58d1c5a56f0380439dc1305a131321b839df35ddfac2a6dbcc887de02232120"
],
"index": "pypi",
"version": "==0.7.1"
},
"django-environ": { "django-environ": {
"hashes": [ "hashes": [
"sha256:bff5381533056328c9ac02f71790bd5bf1cea81b1beeb648f28b81c9e83e0a21", "sha256:bff5381533056328c9ac02f71790bd5bf1cea81b1beeb648f28b81c9e83e0a21",
@@ -112,32 +120,32 @@
"develop": { "develop": {
"black": { "black": {
"hashes": [ "hashes": [
"sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b", "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90",
"sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176", "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c",
"sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09", "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78",
"sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a", "sha256:4af5bc0e1f96be5ae9bd7aaec219c901a94d6caa2484c21983d043371c733fc4",
"sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015", "sha256:560558527e52ce8afba936fcce93a7411ab40c7d5fe8c2463e279e843c0328ee",
"sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79", "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e",
"sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb", "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e",
"sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20", "sha256:6c1734ab264b8f7929cef8ae5f900b85d579e6cbfde09d7387da8f04771b51c6",
"sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464", "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9",
"sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968", "sha256:7ba9be198ecca5031cd78745780d65a3f75a34b2ff9be5837045dce55db83d1c",
"sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82", "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256",
"sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21", "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f",
"sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0", "sha256:a3db5b6409b96d9bd543323b23ef32a1a2b06416d525d27e0f67e74f1446c8f2",
"sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265", "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c",
"sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b", "sha256:b154e6bbde1e79ea3260c4b40c0b7b3109ffcdf7bc4ebf8859169a6af72cd70b",
"sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a", "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807",
"sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72", "sha256:b9fd45787ba8aa3f5e0a0a98920c1012c884622c6c920dbe98dbd05bc7c70fbf",
"sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce", "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def",
"sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0", "sha256:c9a3ac16efe9ec7d7381ddebcc022119794872abce99475345c5a61aa18c45ad",
"sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a", "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d",
"sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163", "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849",
"sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad", "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69",
"sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d" "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666"
], ],
"index": "pypi", "index": "pypi",
"version": "==22.3.0" "version": "==22.6.0"
}, },
"click": { "click": {
"hashes": [ "hashes": [
@@ -203,7 +211,7 @@
"sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
"sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
], ],
"markers": "python_version < '3.11'", "markers": "python_full_version < '3.11.0a7'",
"version": "==2.0.1" "version": "==2.0.1"
}, },
"typing-extensions": { "typing-extensions": {

View File

@@ -1,15 +1,10 @@
from django.contrib import admin from django.contrib import admin
from django.urls import reverse from django.urls import reverse
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from board.models import (
Ban, from guardian.admin import GuardedModelAdmin
BanTemplate,
Board, from board.models import *
Post,
RangeBan,
ReportReason,
ReportRecord,
)
# #
# Admin sites # Admin sites
@@ -82,3 +77,8 @@ class BanAdmin(admin.ModelAdmin):
@admin.register(BanTemplate) @admin.register(BanTemplate)
class BanTemplateAdmin(admin.ModelAdmin): class BanTemplateAdmin(admin.ModelAdmin):
ordering = ("board__url", "name") ordering = ("board__url", "name")
@admin.register(Capcode)
class CapcodeAdmin(GuardedModelAdmin):
pass

View File

@@ -1,3 +1,4 @@
from django.core.exceptions import ValidationError
from django.conf import settings from django.conf import settings
from django.db import transaction from django.db import transaction
from django.db.models import Q from django.db.models import Q
@@ -39,12 +40,21 @@ class ReplyForm(PostForm):
class Meta: class Meta:
model = Post 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) super(ReplyForm, self).__init__(*args, **kwargs)
self.instance.op = op 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): class ReportForm(ModelForm):

View File

@@ -2,17 +2,21 @@ from datetime import timedelta
import os import os
from pathlib import Path from pathlib import Path
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.db import models from django.db import models
from django.db.models import signals from django.db.models import signals
from django.db.models.deletion import SET_NULL
from django.dispatch import receiver from django.dispatch import receiver
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from PIL import Image
from io import BytesIO from io import BytesIO
from colorfield.fields import ColorField
from PIL import Image
def image_upload(instance, filename): def image_upload(instance, filename):
op_id = instance.op.id if instance.op else instance.id 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}" return f"{instance.board.url}/{now_sec}t{ext}"
User = get_user_model()
class Board(models.Model): class Board(models.Model):
# The short URL name for the board # The short URL name for the board
url = models.CharField(max_length=255, null=False, blank=False, unique=True) 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) text = models.TextField(max_length=10000, null=False, blank=True)
# The IP address of the user that made this post # The IP address of the user that made this post
ip = models.GenericIPAddressField() ip = models.GenericIPAddressField()
# Capcode
capcode = models.ForeignKey("Capcode", null=True, blank=True, on_delete=SET_NULL)
# Creation time # Creation time
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
# Last bump time # Last bump time
@@ -334,7 +343,12 @@ class BanTemplate(models.Model):
class Capcode(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. # Content to display *after* the capcoded user's name.
suffix = models.CharField(max_length=100) suffix = models.CharField(max_length=100)
color = ColorField()
class Meta:
permissions = (("use_capcode", "Can use capcode"),)
def __str__(self) -> str:
return self.suffix

View File

@@ -30,9 +30,14 @@
{% if post.subject %} {% if post.subject %}
<span class="post_subject">{{post.subject}}</span> <span class="post_subject">{{post.subject}}</span>
{% endif %} {% endif %}
{% blocktranslate with post_name=post.name|default:"Anonymous" post_created=post.created %} {% blocktranslate with post_name=post.name|default:"Anonymous" %}
by by
<span class="post_name">{{post_name}}</span> <span class="post_name">{{post_name}}</span>
{% endblocktranslate %}
{% if post.capcode %}
<span class="post_capcode" style="color: {{post.capcode.color}};">{{post.capcode}}</span>
{% endif %}
{% blocktranslate with post_created=post.created %}
at {{post_created}} at {{post_created}}
{% endblocktranslate %} {% endblocktranslate %}
{% if reply_link %} {% if reply_link %}

View File

@@ -5,7 +5,23 @@
<form method="post" action="{% url 'board:reply_create' url=board.url id=post.id %}" enctype="multipart/form-data"> <form method="post" action="{% url 'board:reply_create' url=board.url id=post.id %}" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<table> <table>
{{ form.as_table }} {# {{ form.as_table }} #}
{% for field in form %}
{% if field.name != "capcode" %}
<tr>
<th>{{field.label_tag}}</th>
<td>{{field}}</td>
</tr>
{% endif %}
{% endfor %}
{% if capcodes %}
<tr>
<th>{{form.capcode.label_tag}}</th>
<td>
{{form.capcode}}
</td>
</tr>
{% endif %}
<tr> <tr>
<th>&nbsp;</th> <th>&nbsp;</th>
<td> <td>

View File

@@ -1,5 +1,6 @@
from typing import Any, Dict from typing import Any, Dict
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model, get_user
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.db.models import Q 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.views.generic import edit
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils import timezone from django.utils import timezone
from guardian.shortcuts import get_objects_for_user
from board.forms import BanForm, PostForm, ReplyForm, ReportForm from board.forms import BanForm, PostForm, ReplyForm, ReportForm
from board.models import Ban, BanTemplate, Board, Post, Report from board.models import Ban, BanTemplate, Board, Post, Report
from board.utils import * from board.utils import *
@@ -26,6 +30,9 @@ __all__ = (
) )
User = get_user_model()
class BannedView(TemplateView): class BannedView(TemplateView):
template_name = "board/banned.html" template_name = "board/banned.html"
@@ -121,7 +128,9 @@ class ReplyCreateView(CreateView):
post_id = self.kwargs["id"] post_id = self.kwargs["id"]
context["post"] = get_object_or_404(Post, id=post_id) context["post"] = get_object_or_404(Post, id=post_id)
context["max_upload_size"] = settings.MAX_UPLOAD_SIZE 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 return context
def get_form_kwargs(self): def get_form_kwargs(self):
@@ -129,7 +138,7 @@ class ReplyCreateView(CreateView):
post_id = self.kwargs["id"] post_id = self.kwargs["id"]
post = get_object_or_404(Post, id=post_id) post = get_object_or_404(Post, id=post_id)
kwargs["op"] = post kwargs["op"] = post
kwargs["reply"] = post kwargs["user"] = self.request.user
return kwargs return kwargs
@@ -154,7 +163,7 @@ class PostView(CreateView):
post_id = self.kwargs["id"] post_id = self.kwargs["id"]
post = get_object_or_404(Post, id=post_id) post = get_object_or_404(Post, id=post_id)
kwargs["op"] = post kwargs["op"] = post
kwargs["reply"] = post kwargs["user"] = self.request.user
return kwargs return kwargs

View File

@@ -43,6 +43,7 @@ INSTALLED_APPS = [
"django.contrib.messages", "django.contrib.messages",
"django.contrib.staticfiles", "django.contrib.staticfiles",
"guardian", "guardian",
"colorfield",
"board", "board",
] ]