wokring final build version
This commit is contained in:
40
backend/notifications/migrations/0001_initial.py
Normal file
40
backend/notifications/migrations/0001_initial.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# Generated by Django 5.2.7 on 2026-06-10 17:13
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Notification',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('is_deleted', models.BooleanField(default=False)),
|
||||
('deleted_at', models.DateTimeField(blank=True, null=True)),
|
||||
('title', models.CharField(help_text='Předmět oznámení.', max_length=200)),
|
||||
('text', models.TextField(help_text='Obsah oznámení.')),
|
||||
('notification_type', models.CharField(choices=[('system', 'Systém'), ('order', 'Objednávka'), ('payment', 'Platba'), ('social', 'Sociální'), ('chat', 'Chat'), ('advertisement', 'Inzerát')], default='system', help_text='Kategorie oznámení — používá se pro ikonky a filtrování na frontendu.', max_length=20)),
|
||||
('action_url', models.CharField(blank=True, help_text="Volitelný odkaz na detail (např. '/objednavky/123/').", max_length=500, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('bulk', models.BooleanField(default=False, help_text='True, pokud bylo oznámení vytvořeno hromadně pro více uživatelů.')),
|
||||
('is_read', models.BooleanField(default=False)),
|
||||
('read_at', models.DateTimeField(blank=True, null=True)),
|
||||
('send_email', models.BooleanField(default=False, help_text='True, pokud byl zároveň odeslán e-mail.')),
|
||||
('email_subject', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('email_template_path', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('user', models.ForeignKey(help_text='Příjemce oznámení.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -24,6 +24,7 @@ django-constance #allows you to store and manage settings of page in the Django
|
||||
|
||||
# -- OBJECT STORAGE --
|
||||
Pillow #adds image processing capabilities to your Python interpreter
|
||||
pillow-heif #HEIC/HEIF support for Pillow (iPhone photos)
|
||||
|
||||
whitenoise #pomáha se spuštěním serveru a načítaní static files
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.7 on 2026-06-10 17:13
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('chat', '0002_chatreadstatus'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='messagereaction',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='messagereaction',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('is_deleted', False)), fields=('message', 'user'), name='unique_active_reaction_per_user_message'),
|
||||
),
|
||||
]
|
||||
@@ -634,7 +634,7 @@ STATIC_ROOT = BASE_DIR / 'collectedstaticfiles'
|
||||
if not USE_S3:
|
||||
# Local filesystem — development only
|
||||
STORAGES = {
|
||||
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
|
||||
"default": {"BACKEND": "vontor_cz.storage.LocalMediaStorage"},
|
||||
"staticfiles": {"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"},
|
||||
}
|
||||
MEDIA_URL = os.getenv("MEDIA_URL", "/media/")
|
||||
@@ -650,8 +650,8 @@ else:
|
||||
S3_PROTO = 'https' if S3_SSL else 'http'
|
||||
|
||||
STORAGES = {
|
||||
"default": {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"},
|
||||
"staticfiles": {"BACKEND": "storages.backends.s3boto3.S3StaticStorage"},
|
||||
"default": {"BACKEND": "vontor_cz.storage.MediaStorage"},
|
||||
"staticfiles": {"BACKEND": "vontor_cz.storage.StaticStorage"},
|
||||
}
|
||||
|
||||
AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID')
|
||||
|
||||
82
backend/vontor_cz/storage.py
Normal file
82
backend/vontor_cz/storage.py
Normal file
@@ -0,0 +1,82 @@
|
||||
import io
|
||||
import os
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import FileSystemStorage
|
||||
import pillow_heif
|
||||
from PIL import Image, ImageSequence
|
||||
|
||||
pillow_heif.register_heif_opener()
|
||||
from storages.backends.s3boto3 import S3Boto3Storage, S3StaticStorage
|
||||
|
||||
# All formats Pillow handles natively (no extra plugins needed).
|
||||
# Add '.heic' / '.heif' if you install pillow-heif.
|
||||
_IMAGE_EXTS = {
|
||||
'.jpg', '.jpeg', '.jpe', '.jfif', # JPEG variants
|
||||
'.png', # PNG
|
||||
'.gif', # GIF (animated preserved)
|
||||
'.bmp', '.dib', # BMP
|
||||
'.tiff', '.tif', # TIFF
|
||||
'.tga', # Truevision TGA
|
||||
'.ico', # ICO (largest frame used)
|
||||
'.ppm', '.pgm', '.pbm', '.pnm', # Portable pixmap family
|
||||
'.pcx', # PCX
|
||||
'.heic', '.heif', # Apple HEIC/HEIF (pillow-heif)
|
||||
}
|
||||
|
||||
def _to_webp(content, quality: int = 85) -> io.BytesIO:
|
||||
img = Image.open(content)
|
||||
frames = list(ImageSequence.Iterator(img))
|
||||
out = io.BytesIO()
|
||||
if len(frames) > 1:
|
||||
converted = [f.copy().convert('RGBA') for f in frames]
|
||||
converted[0].save(
|
||||
out,
|
||||
format='WEBP',
|
||||
save_all=True,
|
||||
append_images=converted[1:],
|
||||
loop=img.info.get('loop', 0),
|
||||
quality=quality,
|
||||
)
|
||||
else:
|
||||
img.convert('RGBA').save(out, format='WEBP', quality=quality)
|
||||
out.seek(0)
|
||||
return out
|
||||
|
||||
|
||||
class WebPConversionMixin:
|
||||
"""
|
||||
Intercepts image/GIF uploads and converts them to WebP before storage.
|
||||
Videos are saved as-is; use `vontor_cz.tasks.convert_video_to_webm` async
|
||||
to transcode them to WebM after save.
|
||||
"""
|
||||
WEBP_QUALITY: int = 85
|
||||
|
||||
def _save(self, name: str, content) -> str:
|
||||
root, ext = os.path.splitext(name)
|
||||
if ext.lower() in _IMAGE_EXTS:
|
||||
try:
|
||||
webp_data = _to_webp(content, self.WEBP_QUALITY)
|
||||
content = ContentFile(webp_data.read())
|
||||
name = root + '.webp'
|
||||
except Exception:
|
||||
content.seek(0)
|
||||
return super()._save(name, content)
|
||||
|
||||
|
||||
class MediaStorage(WebPConversionMixin, S3Boto3Storage):
|
||||
def exists(self, name):
|
||||
if not name:
|
||||
return False
|
||||
return super().exists(name)
|
||||
|
||||
|
||||
class LocalMediaStorage(WebPConversionMixin, FileSystemStorage):
|
||||
pass
|
||||
|
||||
|
||||
class StaticStorage(S3StaticStorage):
|
||||
def exists(self, name):
|
||||
if not name:
|
||||
return False
|
||||
return super().exists(name)
|
||||
Reference in New Issue
Block a user