Create dedicated image upload model

This commit is contained in:
Antoine Bartuccio 2025-02-28 15:06:47 +01:00
parent d65cabe4f3
commit 8088536ad6
7 changed files with 142 additions and 55 deletions

View File

@ -1,12 +1,7 @@
from io import BytesIO
from pathlib import Path
from typing import Annotated
from uuid import uuid4
import annotated_types
from django.conf import settings
from django.core.files.base import ContentFile
from django.db import transaction
from django.db.models import F
from django.http import HttpResponse
from ninja import Query, UploadedFile
@ -14,11 +9,11 @@ from ninja_extra import ControllerBase, api_controller, paginate, route
from ninja_extra.exceptions import PermissionDenied
from ninja_extra.pagination import PageNumberPaginationExtra
from ninja_extra.schemas import PaginatedResponseSchema
from PIL import Image, UnidentifiedImageError
from PIL import UnidentifiedImageError
from club.models import Mailing
from core.auth.api_permissions import CanAccessLookup, CanView, IsOldSubscriber
from core.models import Group, SithFile, User
from core.models import Group, QuickUploadImage, SithFile, User
from core.schemas import (
FamilyGodfatherSchema,
GroupSchema,
@ -49,47 +44,13 @@ class UploadController(ControllerBase):
message=f"{file.name} isn't a file image", status_code=415
)
def convert_image(file: UploadedFile) -> ContentFile:
content = BytesIO()
Image.open(BytesIO(file.read())).save(
fp=content, format="webp", optimize=True
)
return ContentFile(content.getvalue())
try:
converted = convert_image(file)
image = QuickUploadImage.create_from_uploaded(file)
except UnidentifiedImageError:
return self.create_response(
message=f"{file.name} can't be processed", status_code=415
)
with transaction.atomic():
parent = SithFile.objects.filter(parent=None, name="upload").first()
if parent is None:
root = User.objects.get(id=settings.SITH_ROOT_USER_ID)
parent = SithFile.objects.create(
parent=None,
name="upload",
owner=root,
)
image = SithFile(
parent=parent,
name=f"{Path(file.name).stem}_{uuid4()}.webp",
file=converted,
owner=self.context.request.user,
is_folder=False,
mime_type="img/webp",
size=converted.size,
moderator=self.context.request.user,
is_moderated=True,
)
image.file.name = image.name
image.clean()
image.save()
image.view_groups.add(
Group.objects.filter(id=settings.SITH_GROUP_PUBLIC_ID).first()
)
image.save()
return image

View File

@ -856,10 +856,6 @@ Welcome to the wiki page!
]
)
# Upload folder
SithFile.objects.create(name="upload", owner=root)
(settings.MEDIA_ROOT / "upload").mkdir(parents=True, exist_ok=True)
def _create_profile_pict(self, user: User):
path = self.SAS_FIXTURE_PATH / "Family" / f"{user.username}.jpg"
file = resize_image(Image.open(path), 400, "WEBP")

View File

@ -0,0 +1,52 @@
# Generated by Django 4.2.17 on 2025-02-28 10:51
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("core", "0044_alter_userban_options"),
]
operations = [
migrations.CreateModel(
name="QuickUploadImage",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100)),
("image", models.ImageField(upload_to="upload")),
("content_type", models.CharField(max_length=50)),
(
"related_model_id",
models.PositiveIntegerField(blank=True, null=True),
),
(
"related_model_type",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.contenttype",
),
),
],
options={
"indexes": [
models.Index(
fields=["related_model_type", "related_model_id"],
name="core_quicku_related_23899b_idx",
)
],
},
),
]

View File

@ -17,7 +17,7 @@
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# this program; if not, write to the Free Software Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
@ -32,15 +32,19 @@ from datetime import timedelta
from io import BytesIO
from pathlib import Path
from typing import TYPE_CHECKING, Optional, Self
from uuid import uuid4
from django.conf import settings
from django.contrib.auth.models import AbstractUser, UserManager
from django.contrib.auth.models import AbstractUser, ContentType, UserManager
from django.contrib.auth.models import AnonymousUser as AuthAnonymousUser
from django.contrib.auth.models import Group as AuthGroup
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.staticfiles.storage import staticfiles_storage
from django.core import validators
from django.core.cache import cache
from django.core.exceptions import PermissionDenied, ValidationError
from django.core.files import File
from django.core.files.base import ContentFile
from django.core.mail import send_mail
from django.db import models, transaction
from django.db.models import Exists, F, OuterRef, Q
@ -54,6 +58,7 @@ from phonenumber_field.modelfields import PhoneNumberField
from PIL import Image
if TYPE_CHECKING:
from django.core.files.uploadedfile import UploadedFile
from pydantic import NonNegativeInt
from club.models import Club
@ -1108,6 +1113,54 @@ class SithFile(models.Model):
return reverse("core:download", kwargs={"file_id": self.id})
class QuickUploadImage(models.Model):
"""Images uploaded by user outside of the SithFile mechanism"""
IMAGE_NAME_SIZE = 100
name = models.CharField(max_length=IMAGE_NAME_SIZE, blank=False)
image = models.ImageField(upload_to="upload")
content_type = models.CharField(max_length=50, blank=False)
related_model = GenericForeignKey("related_model_type", "related_model_id")
related_model_id = models.PositiveIntegerField(null=True, blank=True)
related_model_type = models.ForeignKey(
ContentType, on_delete=models.CASCADE, null=True, blank=True
)
class Meta:
indexes = [models.Index(fields=["related_model_type", "related_model_id"])]
def __str__(self) -> str:
return f"{self.name}{Path(self.image.path).suffix}"
@classmethod
def create_from_uploaded(
cls, image: UploadedFile, related_model: models.Model | None = None
) -> Self:
def convert_image(file: UploadedFile) -> ContentFile:
content = BytesIO()
Image.open(BytesIO(file.read())).save(
fp=content, format="webp", optimize=True
)
return ContentFile(content.getvalue())
name = Path(image.name).stem[: cls.IMAGE_NAME_SIZE - 1]
file = File(convert_image(image), name=f"{name}_{uuid4()}.webp")
return cls.objects.create(
name=name,
image=file,
content_type="image/webp",
related_model=related_model,
)
def can_be_viewed_by(self, user: User) -> bool:
if not self.related_model:
return True
return user.can_view(self.related_model)
class LockError(Exception):
"""There was a lock error on the object."""

View File

@ -9,9 +9,8 @@ from django.utils.text import slugify
from haystack.query import SearchQuerySet
from ninja import FilterSchema, ModelSchema, Schema
from pydantic import AliasChoices, Field
from pydantic_core import Url
from core.models import Group, SithFile, User
from core.models import Group, QuickUploadImage, SithFile, User
class SimpleUserSchema(ModelSchema):
@ -50,14 +49,29 @@ class UserProfileSchema(ModelSchema):
class UploadedFileSchema(ModelSchema):
class Meta:
model = SithFile
fields = ["id", "name", "mime_type", "size"]
model = QuickUploadImage
fields = ["id", "name", "content_type"]
width: int
height: int
size: int
href: str
@staticmethod
def resolve_href(obj: SithFile) -> Url:
return reverse("core:download", kwargs={"file_id": obj.id})
def resolve_width(obj: QuickUploadImage):
return obj.image.width
@staticmethod
def resolve_height(obj: QuickUploadImage):
return obj.image.height
@staticmethod
def resolve_size(obj: QuickUploadImage):
return obj.image.size
@staticmethod
def resolve_href(obj: QuickUploadImage) -> str:
return reverse("core:uploaded_image", kwargs={"image_id": obj.id})
class SithFileSchema(ModelSchema):

View File

@ -33,7 +33,7 @@ const loadEasyMde = (textarea: HTMLTextAreaElement) => {
easymde.codemirror.replaceSelection("!");
easymde.codemirror.setSelection({ line: cursor.line, ch: cursor.ch + 1 });
easymde.codemirror.replaceSelection(file.name.split(".").slice(0, -1).join("."));
easymde.codemirror.replaceSelection(response.data.name);
// Move cursor at the end of the url and add a new line
cursor = easymde.codemirror.getCursor();

View File

@ -30,6 +30,7 @@ from core.converters import (
FourDigitYearConverter,
TwoDigitMonthConverter,
)
from core.models import QuickUploadImage
from core.views import (
FileDeleteView,
FileEditPropView,
@ -213,6 +214,16 @@ urlpatterns = [
"file/<int:file_id>/moderate/", FileModerateView.as_view(), name="file_moderate"
),
path("file/<int:file_id>/download/", send_file, name="download"),
path(
"file/<int:image_id>/uploads/",
lambda request, image_id: send_file(
request=request,
file_id=image_id,
file_class=QuickUploadImage,
file_attr="image",
),
name="uploaded_image",
),
# Page views
path("page/", PageListView.as_view(), name="page_list"),
path("page/create/", PageCreateView.as_view(), name="page_new"),