From d3e1f5a978de5f6447192eae32164a7e642942ae Mon Sep 17 00:00:00 2001 From: Alek Ratzloff Date: Tue, 12 Jul 2022 18:30:16 -0700 Subject: [PATCH] Add board locking, update tests, and update clean() methods * Boards can be locked from allowing posts - this is can be useful for things like archived boards or locking down in the event of an emergency * Some validation checks for new posts are from the reply/thread form to the Post model's clean() method * Add some new tests, I've really been falling behind on those Signed-off-by: Alek Ratzloff --- board/forms.py | 2 -- board/models.py | 17 +++++++++++--- tests/test_posts.py | 57 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/board/forms.py b/board/forms.py index df239e5..412bb8a 100644 --- a/board/forms.py +++ b/board/forms.py @@ -68,8 +68,6 @@ class ReplyForm(PostForm): def clean(self): super().clean() - if self.instance.op.lock: - raise ValidationError(_("This thread is locked, you cannot reply to it")) capcode = self.cleaned_data["capcode"] if capcode: if not self.user or not self.user.has_perm("board.use_capcode", capcode): diff --git a/board/models.py b/board/models.py index 634d936..70efc76 100644 --- a/board/models.py +++ b/board/models.py @@ -58,6 +58,8 @@ class Board(models.Model): # Auto-sink threshhold. This is the number of replies that a thread can have # before it stops being bumped. autosink = models.IntegerField(default=300) + # Whether this board is read-only or not. + readonly = models.BooleanField(default=False) @property def threads(self): @@ -115,10 +117,10 @@ class Post(models.Model): # Thumbnail thumbnail = models.ImageField(upload_to=thumbs_upload, editable=False, null=True) # Original image name - original_image_name = models.CharField(max_length=255, null=True) + original_image_name = models.CharField(max_length=255, null=True, blank=True) # Image width and height - image_width = models.IntegerField(null=True) - image_height = models.IntegerField(null=True) + image_width = models.IntegerField(null=True, blank=True) + image_height = models.IntegerField(null=True, blank=True) class Meta: permissions = [ @@ -183,6 +185,15 @@ class Post(models.Model): _("Image supplied is too large. Maximum image size is %(max)s"), params={"max": settings.MAX_UPLOAD_SIZE}, ) + # Check if board is readonly + if self.board.readonly: + raise ValidationError( + _("This board is in readonly mode, you cannot create new posts.") + ) + # Check if OP is locked + if self.op and self.op.lock: + raise ValidationError(_("This thread is locked, you cannot reply to it")) + # Rate limiting for posts # TODO BUG: if a user's last post is deleted, and it is their only post, # they will not hit rate limit. This could probably be abused diff --git a/tests/test_posts.py b/tests/test_posts.py index 7cca6e5..f307db7 100644 --- a/tests/test_posts.py +++ b/tests/test_posts.py @@ -1,10 +1,15 @@ +from datetime import timedelta +from django.core.exceptions import ValidationError from django.test import TestCase +from board.forms import PostForm, ReplyForm from board.models import Board, Post class BumpTestCase(TestCase): def setUp(self): - Board.objects.create(url="test", name="test") + Board.objects.create( + url="test", name="test", post_cooldown=timedelta(seconds=0) + ) def test_bumping(self): board = Board.objects.get(url="test") @@ -63,3 +68,53 @@ class BumpTestCase(TestCase): self.assertEquals( list(board.threads.order_by("-last_bump")), [post4, post3, post2, post1] ) + + def test_thread_sticky(self): + board = Board.objects.get(url="test") + self.assertEquals(board.threads.count(), 0) + + board = Board.objects.get(url="test") + self.assertEquals(board.threads.count(), 0) + post1 = Post.objects.create( + board=board, text="test 1", ip="127.0.0.1", sticky=True + ) + post2 = Post.objects.create(board=board, text="test 2", ip="127.0.0.1") + post3 = Post.objects.create(board=board, text="test 3", ip="127.0.0.1") + post4 = Post.objects.create(board=board, text="test 4", ip="127.0.0.1") + + self.assertEquals( + list(board.threads.order_by("-sticky", "-last_bump")), + [post1, post4, post3, post2], + ) + + Post.objects.create(board=board, text="bump", ip="127.0.0.1", op=post3) + self.assertEquals( + list(board.threads.order_by("-sticky", "-last_bump")), + [post1, post3, post4, post2], + ) + + def test_thread_lock(self): + board = Board.objects.get(url="test") + op = Post.objects.create(board=board, text="test 1", ip="127.0.0.1", lock=True) + reply = Post(board=board, text="reply 1", op=op, ip="127.0.0.1") + self.assertRaises(ValidationError, reply.full_clean) + + # Also, make sure that thread locks work after new posts are made + op = Post.objects.create(board=board, text="test 1", ip="127.0.0.1") + reply1 = Post(board=board, text="reply 1", op=op, ip="127.0.0.1") + reply1.full_clean() # should not raise + op.lock = True + reply2 = Post(board=board, text="reply 2", op=op, ip="127.0.0.2") + self.assertRaises(ValidationError, reply2.full_clean) + + +class BoardReadonlyTestCase(TestCase): + def setUp(self): + Board.objects.create( + url="test", name="test", post_cooldown=timedelta(seconds=0), readonly=True + ) + + def test_board_readonly(self): + board = Board.objects.get(url="test") + op = Post(board=board, text="test 1", ip="127.0.0.1") + self.assertRaises(ValidationError, op.full_clean)