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:
1
Pipfile
1
Pipfile
@@ -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
60
Pipfile.lock
generated
@@ -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": {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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> </th>
|
<th> </th>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ INSTALLED_APPS = [
|
|||||||
"django.contrib.messages",
|
"django.contrib.messages",
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
"guardian",
|
"guardian",
|
||||||
|
"colorfield",
|
||||||
"board",
|
"board",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user