Dodając pliki graficzne jako załączniki, zazwyczaj chcemy je przed przesłaniem na serwer przeprocesować. Zmienić ich rozmiar, bądź format. Sam framework (Django) daje w prosty sposób możliwość nadpisania metody save() i tym samym daje nam pełną kontrolę, nad zapisywanym plikiem.
Od czegoś trzeba zacząć
W poniższym przykładzie, wykorzystamy bibliotekę Pillow, którą instalujemy po przez:
pip install Pillow
A więc zacznijmy programować
Nasza klasa będzie całościowo zajmowała się obslugą oraz zapisywaniem obrazka. Zastosujemy w niej podejście podobne do tego które pozwala nam udostępniać bezpiecznie materiały przez googledrive.
1. Zapisywanie obrazka
def save(self, *args, **kwargs):
"""Save file to disk."""
if not self.id:
super().save()
image_extension = ["jpg", "jpeg"]
if self.extension() in image_extension:
img = Image.open(self.file.path)
if img.height > 1024 or img.width > 1024:
new_img = (1024, 1024)
img.thumbnail(new_img)
img.save(self.file.path)
Poniższa metoda:
- Sprawdza czy obiekt jest już stworzony.
- Jeśli nie, wywołuje metodę nadrzędną save.
- Po czym upewnia się czy rozszerzenie jest na liście dozwolonych
- Dodatkowo sprawdza czy obrazek ma któryś bok większy niż 1024px.
Tylko w przypadku spełnienia powyższych warunków, tworzy jego mniejszą wersję.
Dla wtajemniczonych polecam również sprawdzić rodzaj pliku bazując na znajdujących się tam danych np poleceniem file.
2. Bezpieczny adres
Dodatkowo celem stworzenia bezpieczniejszego adresu, do danego zasobu który użytkownicy bedą mogli sobie wzajemnie udostępniać, zastosujemy technikę dostępu do pliku przez wysoce skomplikowany adres url (jak np w google drive).
def get_file_path(instance, filename) -> str:
"""Generate secure path for filename.
:param instance: The model instance
:param filename: The name of the file that was uploaded
:return: path to file
:rtype: str
"""
_now = datetime.now()
filename = "{year}/{month}/{day}/{password}/{filename}".format(
year=_now.strftime("%Y"),
month=_now.strftime("%m"),
day=_now.strftime("%d"),
password=get_random_string(20),
filename=filename,
)
return path.join("zalaczniki/", filename)
Sam adres będzie się składał z daty / skomplikowanego hasła dostępu / nazwy pliku. Pozwoli nam to wdrożyć prostą ale i skuteczną metodę dzielenia się plikami, nawet dla użytkowników spoza serwisu.
Załączam również pełny kod całej klasy:
from datetime import datetime
from os import path
from django.db import models
from django.utils.crypto import get_random_string
from django.utils.translation import ugettext_lazy as _
from PIL import Image
class AttachmentsFiles(models.Model):
"""This class is used to store the files that are uploaded to the system"""
title = models.CharField(_("Title"), max_length=80, blank=True)
file = models.FileField(
upload_to=get_file_path,
verbose_name=_("File"),
null=True,
blank=True,
)
visible = models.BooleanField(_("Visible"), default=False)
createdDate = models.DateTimeField(
_("Created date"), default=datetime.now, blank=True
)
def __str__(self):
"""Return file title."""
return self.title
def get_file_path(instance, filename) -> str:
"""Generate secure path for filename.
:param instance: The model instance
:param filename: The name of the file that was uploaded
:return: path to file
:rtype: str
"""
_now = datetime.now()
filename = "{year}/{month}/{day}/{password}/{filename}".format(
year=_now.strftime("%Y"),
month=_now.strftime("%m"),
day=_now.strftime("%d"),
password=get_random_string(20),
filename=filename,
)
return path.join("zalaczniki/", filename)
def extension(self) -> str:
"""Return file extension.
:raises Exception: Cant extract the extension
:return: file extension
:rtype: str
"""
try:
name, extension = path.splitext(self.file.name)
return extension[1:].lower()
except Exception:
return None
def save(self, *args, **kwargs):
"""Save file to disk."""
if not self.id:
super().save()
image_extension = ["jpg", "jpeg"]
if self.extension() in image_extension:
img = Image.open(self.file.path)
if img.height > 1024 or img.width > 1024:
new_img = (1024, 1024)
img.thumbnail(new_img)
img.save(self.file.path)
class Meta:
verbose_name = _("Attachment")
verbose_name_plural = _("Attachments")
3. Dla wytrwałych
w ramach małego bonusu, przestawiam sposób jak w panelu administracyjnym, wyświetlić miniaturkę zdjęcia, filmu czy też pliku pdf.
class AttachmentsFilesAdmin(SuperInlineModelAdmin, admin.StackedInline):
model = AttachmentsFiles
extra = 1
readonly_fields = ('file_image',)
def file_image(self, obj):
image_extension = ['jpg', 'png', 'jpeg', 'gif', 'ico']
movie_extension = ['mp4', 'flv', 'avi']
sound_extension = ['mp3', 'ogv', 'vma', 'aac', 'ogg', 'wav']
if obj.extension() is not None:
if obj.extension() in image_extension:
return mark_safe(
'<a href="{url}" data-lightbox="roadtrip"><img src="{url}" loading="lazy" class="img-attachment" width="100" height="100" /></a>'.format(
url=obj.file.url, ))
elif obj.extension() in movie_extension:
return mark_safe(
'<video controls><source src="{url}" type="video/{ext}">'.format(
url=obj.file.url, ext=obj.extension()))
elif obj.extension() in sound_extension:
return mark_safe(
'<audio controls><source src="{url}" type="audio/{ext}">'.format(
url=obj.file.url, ext=obj.extension()))
elif obj.extension() == 'pdf':
return mark_safe(
"<object data='{url}' type='application/pdf' width='100%' height='700px'></object>".format(
url=obj.file.url, ext=obj.extension()))
else:
return mark_safe(
'File format unrecognized<br<a href="{url}">Click to download</a>>'.format(
url=obj.file.url, ))
else:
return ("")
file_image.short_description = ''
file_image.admin_order_field = ''
W tym przykładzie, dodajemy pole o nazwie file_image, w trybie tylko do odczytu. W momencie renderowania go (w zależności jaki ma format załącznik), ładujemy zupełnie inny tag html, umożliwiający wyświetlenie nam odpowiedniego znacznika.
Komentarze