import os from pathlib import Path from django.db import models from django.db.models import signals from django.conf import settings from django.core.exceptions import ValidationError from django.core.files.base import ContentFile from django.dispatch import receiver from django.urls import reverse from django.utils import timezone from PIL import Image from io import BytesIO def image_upload(instance, filename): op_id = instance.op.id if instance.op else instance.id now = timezone.now() now_sec = now.strftime("%s.%f") ext = Path(filename).suffix.lower() if ext in (".jpeg", ".jpg"): ext = ".jpg" if ext not in (".jpg", ".png", ".gif"): raise Exception("File type invalid") return f"{instance.board.url}/{now_sec}{ext}" def thumbs_upload(instance, filename): op_id = instance.op.id if instance.op else instance.id now = timezone.now() now_sec = now.strftime("%s.%f") ext = Path(filename).suffix.lower() return f"{instance.board.url}/{now_sec}t{ext}" class Board(models.Model): # The short URL name for the board url = models.CharField(max_length=255, null=False, blank=False, unique=True) # Human-readable name for the board name = models.CharField(max_length=255, null=False, blank=False) @property def threads(self): return Post.objects.filter(board=self, op=None) class Post(models.Model): # Board that this post was made on board = models.ForeignKey("Board", on_delete=models.CASCADE) # Thread that this is a part of op = models.ForeignKey( "self", null=True, on_delete=models.CASCADE, related_name="all_replies" ) # Post that this is replying to reply = models.ForeignKey( "self", null=True, on_delete=models.CASCADE, related_name="replies" ) # User's supplied name for this post name = models.CharField(max_length=255, null=True, blank=True) # User's supplied subject for this post subject = models.CharField(max_length=255, null=True, blank=True) # Text of this post text = models.TextField(max_length=10000, null=False, blank=True) # The IP address of the user that made this post ip = models.GenericIPAddressField() # Creation time created = models.DateTimeField(auto_now_add=True) # Last bump time last_bump = models.DateTimeField(auto_now_add=True) # Image image = models.ImageField( upload_to=image_upload, null=True, blank=True, width_field="image_width", height_field="image_height", ) # 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) # Image width and height image_width = models.IntegerField(null=True) image_height = models.IntegerField(null=True) def save(self, *args, **kwargs): if self.image: self.original_image_name = Path(self.image.name).parts[-1] self.__make_thumbnail() super(Post, self).save(*args, **kwargs) def __make_thumbnail(self): image = Image.open(self.image) image.thumbnail(settings.THUMB_SIZE, Image.ANTIALIAS) image_path = Path(self.image.name) thumb_path = Path(image_path.stem + "t" + image_path.suffix) ext = thumb_path.suffix.lower() if ext in (".jpg", ".jpeg"): FTYPE = "JPEG" elif ext == ".gif": FTYPE = "GIF" elif ext == ".png": FTYPE = "PNG" else: raise Exception("File type invalid") thumb_temp = BytesIO() image.save(thumb_temp, FTYPE) thumb_temp.seek(0) # save=False stops recursion self.thumbnail.save(thumb_path, ContentFile(thumb_temp.read()), save=False) thumb_temp.close() def get_absolute_url(self): if self.op is None: return reverse( "board:post_detail", kwargs={"url": self.board.url, "id": self.id} ) else: return ( reverse( "board:post_detail", kwargs={"url": self.board.url, "id": self.op.id}, ) + f"#p{self.id}" ) def clean(self): if self.image and self.image.size > settings.MAX_UPLOAD_SIZE: raise ValidationError( "Image supplied is too large. Maximum image size is %(max)s", params={"max": settings.MAX_UPLOAD_SIZE}, ) @receiver(signals.post_save, sender=Post) def post_created(sender, instance, created, **kwargs): if created and instance.op: instance.op.last_bump = timezone.now() instance.op.save() @receiver(signals.post_delete, sender=Post) def post_deleted(sender, instance, **kwargs): if instance.image and Path(instance.image.path).is_file(): os.remove(instance.image.path) if instance.thumbnail and Path(instance.thumbnail.path).is_file(): os.remove(instance.thumbnail.path)