Package: paper-uploads
paper-uploadsАсинхронная загрузка файлов для административного интерфейса Django.RequirementsPython >= 3.9Django >= 3.2paper-admin>= 7.0variationsFeaturesКаждый файл представлен своей моделью. Это позволяет хранить
вместе с файлом дополнительные данные. Например,altдля изображений.Загрузка файлов происходит асинхронно и начинается сразу
при выборе файла в интерфейсе администратора.Поля модели, предоставляемые библиотекойpaper-uploads,
являются производными отOneToOneFieldи не используют<input type="file">. Благодаря этому, при ошибках валидации формы
прикрепленные файлы не сбрасываются.Загруженные картинки можно нарезать на множество вариаций.
Каждая вариация гибко настраивается. Можно указать размеры,
качество сжатия, формат, добавить дополнительныеpilkit-процессоры, распознавание лиц и прочее.См.variations.Совместим сdjango-storages.Опциональная интеграция сdjango-rqдля отложенной нарезки картинок на вариации.Возможность создавать коллекции файлов. В частности, галерей
изображений с возможностью сортировки элементов.Table of ContentsInstallationОписаниеFileField и ImageFieldПоля моделей загруженных файловStorageКаталог загрузки файлаВалидаторыПрограммная загрузка файловВариацииВерсии вариацийRedis QueueSVGFileFieldКоллекцииЭлементы коллекцииStorage и каталог загрузки файловВалидаторыПрограммное создание элемента коллекцииВариацииHTML Template ExampleManagement командыSettingsInstallationInstallpaper-uploads:pipinstallpaper-uploads[full]Addpaper_uploadstoINSTALLED_APPSinsettings.py:INSTALLED_APPS=[# ..."paper_uploads",# ...]Configurepaper-uploadsin django'ssettings.py:PAPER_UPLOADS={"VARIATION_DEFAULTS":{"jpeg":dict(quality=80,progressive=True,),"webp":dict(quality=75,)}}# Add JS translationsPAPER_LOCALE_PACKAGES=["paper_admin","paper_uploads","django.contrib.admin",]ОписаниеВ состав библиотеки входит два поля —FileFieldиImageField—
и модельCollection, предназначенная для группировки загруженных файлов
с целью создания, к примеру, фотогалерей.С примерами использования библиотеки вы можете ознакомитьсяздесь.FileField и ImageFieldfromdjango.dbimportmodelsfromdjango.utils.translationimportgettext_lazyas_frompaper_uploads.modelsimportFileField,ImageFieldclassPage(models.Model):file=FileField(_("file"),blank=True)image=ImageField(_("image"),blank=True)Эти поля используются для тех же целей, что и одноимённые стандартные поля Django
— для загрузки файлов и изображений — но имеют ряд существенных отличий.Главное отличие заключается в том, что поляFileFieldиImageFieldявляются
производными от стандартногоOneToOneField. Соответственно, загруженные файлы
представлены экземплярами полноценных моделей.Поля моделей загруженных файловФайлы, загруженные с помощью полейFileFieldиImageField, хранятся в
экземплярах моделейUploadedFileиUploadedImageсоответственно.В следующих таблицах перечислены общие поля и свойства обеих моделей:ПолеОписаниеresource_nameИмя файла без пути, суффикса и расширения.Пример:report2020.extensionРасширение файла в нижнем регистре без точки.Пример:pdf.sizeРазмер файла в байтах.checksumКонтрольная сумма содержимого файла.Используется для отслеживания изменений файла.uploaded_atДата и время загрузки файла.created_atДата и время создания экземпляра модели.modified_atДата и время изменения экземпляра модели.СвойствоОписаниеnameПолное имя файла.Пример:files/report2020_19sc2Kj.pdf.urlURL-адрес файла.Пример:/media/files/report2020_19sc2Kj.pdf.pathАбсолютный путь к файлу.Пример:/home/www/django/media/files/report2020_19sc2Kj.pdf.Ниже перечислены поля и свойства, специфичные для каждой модели.Специфичные поляUploadedFile:ПолеОписаниеdisplay_nameУдобочитаемое название файла для вывода на сайте.Заполняется в диалоговом окне редактирования файла.Пример:Annual report 2020.Специфичные поляUploadedImage:ПолеОписаниеtitleНазвание изображения, которое можно вставить в атрибутtitleтэга<img>.descriptionОписание изображения, которое можно вставить в атрибутaltтэга<img>.widthШирина загруженного изображения.heightВысота загруженного изображения.ratioОтношение ширины изображения к высоте в форматеDecimal.hw_ratioОтношение высоте изображения к ширине в форматеDecimal.srcsetСтрока формата[URL] [WIDTH]w.Большинство полей заполняются автоматически при загрузке файла и предназначены
только для чтения. Но такие поля, какdisplay_nameилиtitle, заполняются
пользователем в диалоговом окне редактирования файла:StorageПо умолчанию все поляpaper-uploadsиспользуют единый экземпляр хранилища,
определяемый настройкамиSTORAGEиSTORAGE_OPTIONS:# settings.pyPAPER_UPLOADS={"STORAGE":"django.core.files.storage.FileSystemStorage","STORAGE_OPTIONS":{},# ...}Вы можете указать экземпляр хранилища для конкретного поля:fromdjango.dbimportmodelsfromdjango.core.files.storageimportFileSystemStoragefromdjango.utils.translationimportgettext_lazyas_frompaper_uploads.modelsimportFileFieldclassPage(models.Model):report=FileField(_("report"),blank=True,storage=FileSystemStorage(location="uploads/"),upload_to="reports/%Y/%m")Каталог загрузки файлаВсе поля используют единые значения, указанные в настройкахFILES_UPLOAD_TOиIMAGES_UPLOAD_TO:# settings.pyPAPER_UPLOADS={# ..."FILES_UPLOAD_TO":"files/%Y/%m/%d","IMAGES_UPLOAD_TO":"images/%Y/%m/%d",# ...}Для конкретного поля каталог сохранения можно указать в параметре поляupload_to.
Параметр поддерживает форматированиеstrftime(), которое будет заменено на
дату/время загруженного файла (и загружаемые файлы не заполнят один каталог).fromdjango.dbimportmodelsfromdjango.utils.translationimportgettext_lazyas_frompaper_uploads.modelsimportFileFieldclassPage(models.Model):report=FileField(_("report"),blank=True,upload_to="pdf/reports/%Y")Обратите внимание, что в параметрupload_toнельзя передать вызываемый объект.Если вам требуется динамическое определение каталога или имени загруженного файла,
создайте proxy-модель и переопрелите методgenerate_filename():importosimportdatetimefromdjango.dbimportmodelsfromdjango.utils.translationimportgettext_lazyas_frompaper_uploads.modelsimportFileField,UploadedFileclassUploadedFileProxy(UploadedFile):classMeta:proxy=Truedefgenerate_filename(self,filename:str)->str:_,ext=os.path.splitext(filename)filename="proxy-files/file-%Y-%m-%d_%H%M%S{}".format(ext)filename=datetime.datetime.now().strftime(filename)storage=self.get_file_storage()returnstorage.generate_filename(filename)classPage(models.Model):file=FileField(_("file"),to=UploadedFileProxy,blank=True,)ВалидаторыНа загружаемые файлы можно наложить ограничения с помощью валидаторов.Модульpaper-uploads.validatorsпредоставляет следующие классы для валидации файлов:MaxSizeValidator- задает максимально допустимый размер файла.Максимальный размер можно указать как в виде числа (в байтах),
так и в виде строки.Например:4 * 10 ** 6,4mb,4MB,4M.ExtensionValidator- задает допустимые расширения файлов.MimeTypeValidator- задает допустимые MIME-типы файлов.ImageMinSizeValidator- устанавливает минимальный размер загружаемых изображений.ImageMaxSizeValidator- устанавливает максимальный размер загружаемых изображений.Пример:fromdjango.dbimportmodelsfromdjango.utils.translationimportgettext_lazyas_frompaper_uploads.modelsimportFileFieldfrompaper_uploads.validatorsimportExtensionValidator,MaxSizeValidatorclassPage(models.Model):report=FileField(_("file"),blank=True,validators=[ExtensionValidator([".pdf",".doc",".docx"]),MaxSizeValidator("10MB")])Ограничения, наложенные этими валидаторами, отображаются в виджете:Программная загрузка файловfrompaper_uploads.modelsimport*report=UploadedFile()report.set_owner_field(Page,"report")report.attach("/tmp/file.doc")report.save()page=Page.objects.create(report=report)В методset_owner_field()передаётся модель и имя поля модели, в которое будет
сохранен экземпляр модели файла. Эти данные необходимы для выявления файлов, которые
нигде не используются.Методattach()производит непосредственное сохранение файла и заполняет экземпляр
дополнительными данными. В метод можно передать как путь к локальному файлу, так
и файловый объект.ВариацииImageFieldпозволяет создавать вариации для загруженного изображения.
Вариация - это изображение, полученное из исходного позаранееобъявленным правилам.Для создания вариаций используется библиотекаvariations.fromdjango.dbimportmodelsfromdjango.utils.translationimportgettext_lazyas_frompaper_uploads.modelsimport*classPage(models.Model):image=ImageField(_("image"),blank=True,variations=dict(desktop=dict(size=(800,0),clip=False,),mobile=dict(size=(600,0),clip=False,),))К файлам вариаций можно обратиться через модельUploadedImageиспользуя их имена:print(page.image.desktop.url)# /media/images/2022/02/21/sample.desktop.jpgСоздание файлов вариаций происходит в момент загрузки изображения на сервер.
Поэтому изменение настроек вариаций не окажет никакого эффекта на уже загруженные
изображения.Для того, чтобы создать файлы для новых вариаций (либо перезаписать существующие файлы
вариаций) можно поступить одним из ниже описанных способов.Вызвать методrecut():page.image.recut()При вызове этого метода все файлы вариаций для текущего экземпляра
создаются заново.Можно явно указать имена вариаций, которые необходимо перезаписать:page.image_group.recut(["desktop","mobile"])Выполнить management-командуrecreate_variations:python3manage.pyrecreate_variationsapp.page\--fieldimage--variationsdesktopmobileЭта команда сгенерирует вариации для всех экземпляров указанной модели.Версии вариацийДопустим, у нас есть изображение, которое нужно отобразить в трех
вариантах:desktop,tabletиmobile. Если мы хотим поддерживать
дисплеи Retina, нам нужно добавить ещё три вариации для размера2x.
Если мы также хотим использовать форматWebP(сохранив исходный формат
для обратной совместимости), то общее количество вариаций достигает12.Поскольку Retina-вариации отличаются от обычных только увеличенным
на постоянный коэффициент размером, аWebP-вариации — добавлением
параметраformat = "webp", мы можем создавать эти вариации
автоматически. Это и есть версии вариации.Перечень версий, которые нужно сгенерировать, указываются в параметре
вариацииversions. Поддерживаются следующие значения:webp,2x,3x,4x.fromdjango.dbimportmodelsfromdjango.utils.translationimportgettext_lazyas_frompaper_uploads.modelsimport*classPage(models.Model):image=ImageField(_("image"),blank=True,variations=dict(desktop=dict(# ...versions={"webp","2x","3x"})))Приведенный выше код создаст следующие вариации:desktop- оригинальная вариацияdesktop_webp-WebP-версия оригинальной вариацииdesktop_2x- Retina 2xdesktop_webp_2x-WebP-версия Retina 2xdesktop_3x- Retina 3xdesktop_webp_3x-WebP-версия Retina 3xNOTE: Retina-суффикс всегда следует после суффиксаwebp, если
он есть.Если необходимо переопределить какие-то параметры дополнительной
вариации, то придётся объявлять вариацию явно:fromdjango.dbimportmodelsfromdjango.utils.translationimportgettext_lazyas_frompaper_uploads.modelsimport*classPage(models.Model):image=ImageField(_('image'),blank=True,variations=dict(desktop=dict(size=(800,600),versions={'webp','2x','3x'}),desktop_2x=dict(size=(1600,1200),jpeg=dict(quality=72))))Redis QueueПри загрузке большого количества изображений процесс создания вариаций может занимать
значительное время. Эту работу можно вынести в отдельный процесс с помощьюdjango-rq:pipinstalldjango-rq# settings.pyPAPER_UPLOADS={"RQ_ENABLED":True,"RQ_QUEUE_NAME":"default",# ...}Теперь при загрузке изображений, в очередь под именемdefaultбудет добавляться
задача, которая создаст все необходимые вариации.SVGFileFieldПолеSVGFileFieldпредназначено для загрузки SVG-файлов. Оно идентичноFileField,
но связанная с ним модельUploadedSVGFileвключает несколько дополнительных полей:ПолеОписаниеwidthШирина изображения в форматеDecimal. Может быть вещественным числом.heightВысота изображения в форматеDecimal. Может быть вещественным числом.titleНазвание изображения, которое можно вставить в атрибутtitleтэга<img>.descriptionОписание изображения, которое можно вставить в атрибутaltтэга<img>.fromdjango.dbimportmodelsfromdjango.utils.translationimportgettext_lazyas_frompaper_uploads.modelsimportSVGFileFieldclassPage(models.Model):svg=SVGFileField(_("svg"),blank=True)КоллекцииКоллекция — это модель, группирующая экземпляры других моделей (элементов коллекции).
В частности, с помощью коллекции можно создать фотогалерею или список файлов.Для создания коллекции необходимо объявить класс, унаследованный отCollectionи указать модели элементов, которые могут входить в коллекцию с помощью псевдо-поляCollectionItem:fromdjango.dbimportmodelsfrompaper_uploads.modelsimport*# Collection modelclassPageFiles(Collection):svg=CollectionItem(SVGItem)image=CollectionItem(ImageItem)file=CollectionItem(FileItem)classPage(models.Model):files=CollectionField(PageFiles)КлассCollectionобладает особенным свойством:любой дочерний класс, унаследованный
отCollection, является proxy-моделью дляCollection.В большинстве случаев коллекции отличаются друг от друга только набором элементов,
которые могут входить в коллекцию. Использование proxy-моделей предотвращает создание
множества одинаковых таблиц в БД.Если же для коллекции необходима отдельная таблица (например, если вы решили добавить
в неё новое поле), то необходимо явно установить свойствоMeta.proxyв значениеFalse:fromdjango.dbimportmodelsfrompaper_uploads.modelsimport*classCustomCollection(Collection):name=models.CharField("name",max_length=128,blank=True)file=CollectionItem(FileItem)classMeta:proxy=FalseЭлементы коллекцииПсевдо-полеCollectionItemрегистрирует модель элемента коллекции под заданным именем.frompaper_uploads.modelsimport*classPageFiles(Collection):svg=CollectionItem(SVGItem)image=CollectionItem(ImageItem)file=CollectionItem(FileItem)В приведённом примере, коллекцияPageFilesможет включать элементы трех моделей:SVGItem,ImageItemиFileItem.Порядок объявления элементов коллекции важен: первый класс модели, чей методaccept()вернетTrue, определит модель загруженного файла. По этой причинеFileItemдолжен
указываться последним, т.к. он принимает любые файлы.Получить элементы определённого типа можно с помощью методаget_items():foriteminpage.files.get_items("image"):# ...В состав библиотеки входят следующие модели элементов:ImageItemДля хранения изображения с возможностью нарезки навариации.
Допускются только те файлы, которые можно открыть с помощьюPillow.SVGItemДля хранения SVG иконок.FileItemМожет хранить любой файл.Storage и каталог загрузки файловПо умолчанию элементы коллекции используют тот же экземпляр хранилища,
что используется полямиFileFieldиImageField.Каталоги загрузки файлов для элементов коллекций указываются в настройкахCOLLECTION_FILES_UPLOAD_TOиCOLLECTION_IMAGES_UPLOAD_TO:# settings.pyPAPER_UPLOADS={# ..."COLLECTION_FILES_UPLOAD_TO":"collections/files/%Y/%m/%d","COLLECTION_IMAGES_UPLOAD_TO":"collections/images/%Y/%m/%d",# ...}Для отдельно взятого элемента коллекции экземпляр хранилища и каталог загрузкиможно указать в параметреoptionsпсевдо-поляCollectionItem:fromdjango.core.files.storageimportFileSystemStoragefrompaper_uploads.modelsimport*classPageFiles(Collection):image=CollectionItem(ImageItem,options={"storage":FileSystemStorage(location="uploads/"),"upload_to":"gallery",})Как в случае сFileFieldиImageField, значениемupload_toне может выступать
вызываемый объект.Если вам требуется динамическое определение каталога или имени загруженного файла,
создайте proxy-модель и переопрелите методgenerate_filename():importosimportdatetimefromdjango.dbimportmodelsfromdjango.utils.translationimportgettext_lazyas_frompaper_uploads.modelsimport*classProxyImageItem(ImageItem):classMeta:proxy=Truedefgenerate_filename(self,filename:str)->str:_,ext=os.path.splitext(filename)filename="gallery/image-%Y-%m-%d_%H%M%S{}".format(ext)filename=datetime.datetime.now().strftime(filename)storage=self.get_file_storage()returnstorage.generate_filename(filename)classPageGallery(Collection):image=CollectionItem(ProxyImageItem)classPage(models.Model):gallery=CollectionField(PageGallery,verbose_name=_("gallery"))ВалидаторыНа загружаемые в коллекции файлы можно наложить ограничения с помощью валидаторов:fromdjango.dbimportmodelsfromdjango.utils.translationimportgettext_lazyas_frompaper_uploads.modelsimport*frompaper_uploads.validatorsimportImageMaxSizeValidator,ImageMinSizeValidatorclassPageGallery(Collection):image=CollectionItem(ImageItem,validators=[ImageMinSizeValidator(640,480),ImageMaxSizeValidator(4000,3000)])classPage(models.Model):gallery=CollectionField(PageGallery,verbose_name=_("gallery"))Программное создание элемента коллекцииЭлементы коллекций создаются почти также, какUploadedFileиUploadedImage.
Разница лишь в том, что вместо вызова методаset_owner_field()необходимо
вызвать методattach_to()для присоединения элемента к коллекции:frompaper_uploads.modelsimport*collection=PageGallery.objects.create()item=ImageItem()item.attach_to(collection)item.attach("/tmp/image.jpg")item.save()page=Page.objects.create(gallery=collection)ВариацииВариации для изображений коллекции можно указать одним из двух способов:Параметрoptionsпсевдо-поляCollectionItem:frompaper_uploads.modelsimport*classPageGallery(Collection):image=CollectionItem(ImageItem,options={"variations":dict(mobile=dict(size=(640,0),clip=False))})Атрибут класса коллекцииVARIATIONS:frompaper_uploads.modelsimport*classPageGallery(Collection):VARIATIONS=dict(mobile=dict(size=(640,0),clip=False))image=CollectionItem(ImageItem)HTML Template Example{% if page.gallery %}<divclass="gallery">{% for item in page.gallery %} {% if item.type == "image" %}<divclass="item item--{{ item.type }}"><imgsrc="{{ item.url }}"width="{{ item.width }}"height="{{ item.height }}"title="{{ item.title }}"alt="{{ item.description }}"/></div>{% elif item.type == "file" %}}<divclass="item item--{{ item.type }}"><ahref="{{ item.url }}"download>Download file "{{ item.display_name }}" ({{ item.size|filesizeformat }})</a></div>{% endif %}} {% endfor %}</div>{% endif %}ImageCollectionДля коллекций изображений существует специальный базовый классImageCollection,
который не требует для каждого отдельного случая создавать отдельный класс элемента
коллекции. Все необходимые параметры можно указать сразу, через атрибуты класса:fromdjango.dbimportmodelsfromdjango.utils.translationimportgettext_lazyas_frompaper_uploads.modelsimport*frompaper_uploads.validatorsimportImageMaxSizeValidator,ImageMinSizeValidatorclassPageGallery(ImageCollection):UPLOAD_TO="page/gallery",VARIATIONS=dict(gallery=dict(size=(1600,900),))VALIDATORS=[ImageMinSizeValidator(640,480),ImageMaxSizeValidator(4000,3000)]classPage(models.Model):gallery=CollectionField(PageGallery,verbose_name=_("gallery"))Management командыcheck_uploadsЗапускает комплексную проверку загруженных файлов
и выводит результат.Список производимых тестов:загруженный файл существуеткласс модели владельца (указанный вowner_app_labelиowner_model_name) существуетв классе модели владельца существует поле, указанное вowner_fieldnameу элементов коллекций указано корректное значение в полеtypeмодель элемента коллекции соответствует модели, указанной в классе коллекцииpython3manage.pycheck_uploadsclean_uploadsНаходит мусорные записи в БД (например те, у которых нет владельца)
и предлагает их удалить.Владелец загруженного файла устанавливается в момент сохранения страницы
в интерфейсе администратора. Это происходит позже фактической загрузки файла
на сервер. В промежутке времени между этими событиями файл будет являться "сиротой".
Для того, чтобы такие файлы не удалялись, командаclean_uploadsигнорирует
файлы, загруженные в течение последнего часа.python3manage.pyclean_uploadsremove_empty_collectionsУдаление экземпляров коллекций, в которых нет ни одного элемента.python3manage.pyclean_uploadscreate_missing_variationsСоздаёт отсутствующие файлы вариаций.python3manage.pycreate_missing_variationsrecreate_variationsСоздание/перезапись вариаций для всех экземпляров указанной модели.# for collectionspython3manage.pyrecreate_variationsapp.Photosimage# for regular modelspython3manage.pyrecreate_variationsapp.PageimageВ результате вызова этих команд, пользователю будет предложено выбрать
вариации, которые следует обновить.Можно сразу указать нужные вариации:python3manage.pyrecreate_variationsapp.Pageimage--desktopmobileremove_variationsУдаление файлов вариаций.
Удаляются только файлы объявленных вариаций.
Параметры аналогичны параметрамrecreate_variations.# for collectionspython3manage.pyremove_variationsapp.Photosimage# for regular modelspython3manage.pyremove_variationsapp.PageimageSettingsВсе настройки указываются в словареPAPER_UPLOADS.PAPER_UPLOADS={"STORAGE":"django.core.files.storage.FileSystemStorage","STORAGE_OPTIONS":{},"FILES_UPLOAD_TO":"files/%Y/%m/%d","IMAGES_UPLOAD_TO":"images/%Y/%m/%d","COLLECTION_FILES_UPLOAD_TO":"collections/files/%Y/%m/%d","COLLECTION_IMAGES_UPLOAD_TO":"collections/images/%Y/%m/%d","RQ_ENABLED":True,"RQ_QUEUE_NAME":"default","VARIATION_DEFAULTS":{"jpeg":dict(quality=80,progressive=True,),"webp":dict(quality=75,)}}STORAGEПуть к классухранилища Django.Значение по умолчанию:django.core.files.storage.FileSystemStorageSTORAGE_OPTIONSПараметры инициализации хранилища.Значение по умолчанию:{}FILES_UPLOAD_TOПуть к папке, в которую загружаются файлы из FileField.
Может содержать параметры для даты и времени (см.upload_to).Значение по умолчанию:files/%Y/%m/%dIMAGES_UPLOAD_TOПуть к папке, в которую загружаются файлы из ImageField.Значение по умолчанию:images/%Y/%m/%dCOLLECTION_FILES_UPLOAD_TOПуть к папке, в которую загружаются файлы коллекций.Значение по умолчанию:collections/files/%Y/%m/%dCOLLECTION_IMAGES_UPLOAD_TOПуть к папке, в которую загружаются изображения коллекций.Значение по умолчанию:collections/images/%Y/%m/%dCOLLECTION_ITEM_PREVIEW_WIDTH,COLLECTION_ITEM_PREVIEW_HEIGHTРазмеры превью элементов коллекций в админке.Значение по умолчанию:180x135COLLECTION_IMAGE_ITEM_PREVIEW_VARIATIONSВариации, добавляемые к каждому классу изображений коллекций
для отображения превью в админке. Размеры файлов должны
совпадать сCOLLECTION_ITEM_PREVIEW_WIDTHиCOLLECTION_ITEM_PREVIEW_HEIGHT.RQ_ENABLEDВключает нарезку картинок на вариации через отложенные задачи.
Требует наличие установленного пакетаdjango-rq.Значение по умолчанию:FalseRQ_QUEUE_NAMEНазвание очереди, в которую помещаются задачи по нарезке картинок.Значение по умолчанию:defaultVARIATION_DEFAULTSПараметры вариаций по умолчанию.Параметры, указанные в этом словаре, будут применены к каждой
вариации — если только вариация их явно не переопределяет.Значение по умолчанию:NoneDevelopment and TestingAfter cloning the Git repository, you should install this
in a virtualenv and set up for development:virtualenv.venvsource.venv/bin/activate
pipinstall-r./requirements.txt
pre-commitinstallInstallnpmdependencies and build static files:npmci
npxwebpack