Source code for galleryfield.fields
from django import forms
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.core.validators import BaseValidator
from django.db import models
from django.db.models import Case, IntegerField, Value, When
from django.db.models.query_utils import DeferredAttribute
from django.utils.deconstruct import deconstructible
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext_lazy
from galleryfield import conf
from galleryfield import defaults as _defaults
from galleryfield.utils import apps, get_or_check_image_field, logger
from galleryfield.widgets import GalleryWidget
@deconstructible
class MaxNumberOfImageValidator(BaseValidator):
message = ngettext_lazy(
'Number of images exceeded, only %(limit_value)d allowed',
'Number of images exceeded, only %(limit_value)d allowed',
'limit_value')
code = 'max_number_of_images'
def compare(self, a, b):
return a > b
def clean(self, x):
return len(x)
class GalleryDescriptor(DeferredAttribute):
"""
A collections of pks of image model instances
"""
# Used django.db.models.fields.files.FileDescriptor as an example.
def __set__(self, instance, value):
instance.__dict__[self.field.attname] = value
def __get__(self, instance, cls=None):
image_list = super().__get__(instance, cls)
if not isinstance(image_list, GalleryImages):
attr = self.field.attr_class(instance, self.field, image_list)
instance.__dict__[self.field.name] = attr
return instance.__dict__[self.field.name]
[docs]class GalleryImages(list):
def __init__(self, instance, field, field_value):
# When field_value is None,
# (This happens when the GalleryField was saved as null)
field_value = field_value or []
super().__init__(field_value)
self._field = field
self.instance = instance
self._value = field_value or []
@property
def objects(self):
model = apps.get_model(self._field.target_model)
# Preserving the order of image using id__in=pks
# https://stackoverflow.com/a/37146498/3437454
cases = [When(id=x, then=Value(i)) for i, x in enumerate(self._value)]
case = Case(*cases, output_field=IntegerField())
filter_kwargs = {"id__in": self._value}
queryset = model.objects.filter(**filter_kwargs)
queryset = queryset.annotate(_order=case).order_by('_order')
return queryset
[docs]class GalleryField(models.JSONField):
"""This is a model field which saves the id of images.
:param target_model: A string in the form of ``"app_label.model_name"``,
which can be loaded by :meth:`django.apps.get_model` (see
`Django docs <https://docs.djangoproject.com/en/dev/ref/applications/#django.apps.apps.get_model>`_),
defaults to `None`. If `None`, :class:`galleryfield.BuiltInGalleryImage` will be used.
If set, it should be :ref:`a valid image model <customize-valid-image-model>`.
:type target_model: str, optional.
""" # noqa
attr_class = GalleryImages
descriptor_class = GalleryDescriptor
description = "A collections pks of Image"
def contribute_to_class(self, cls, name, private_only=False):
super().contribute_to_class(cls, name, private_only)
setattr(cls, self.attname, self.descriptor_class(self))
def __init__(self, target_model=None, *args, **kwargs):
self._init_target_model = self.target_model = target_model
if target_model is None:
self.target_model = _defaults.DEFAULT_TARGET_IMAGE_MODEL
self.target_model_image_field = (
self._get_image_field_or_test(is_checking=False))
super().__init__(*args, **kwargs)
def _get_image_field_or_test(self, is_checking=False):
return get_or_check_image_field(
obj=self,
target_model=self._init_target_model,
check_id_prefix="gallery_field",
is_checking=is_checking)
def check(self, **kwargs):
errors = super().check(**kwargs)
errors.extend(self._check_target_model())
return errors
def _check_target_model(self):
return self._get_image_field_or_test(is_checking=True)
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
kwargs['target_model'] = self.target_model
return name, path, args, kwargs
def formfield(self, **kwargs):
defaults = ({
"required": True,
# The following 2 params will be passed to GalleryWidget
# to check if there're potential conflicts between
# target_model and upload_url and fetch_url.
# see GalleryWidget.set_and_check_urls()
"target_model": self.target_model,
"model_field": self.__class__.__name__
})
defaults.update(kwargs)
formfield = super().formfield(**{
'form_class': GalleryFormField,
**defaults,
})
return formfield
[docs]class GalleryFormField(forms.JSONField):
"""The default formfield for :class:`galleryfield.fields.GalleryField`.
:param max_number_of_images: Max allowed number of images, defaults
to `None`, which means unlimited.
:type max_number_of_images: int, optional.
:param kwargs: Besides the options from parent class, the following were added:
* target_model: str, a valid target image model which can be loaded by
``apps.get_model``. When this field is used in the model form,
it is auto configured by the model instance.
However, if this field is used as a non-model form field, when
not specified, it will use the built-in default target image
model ``galleryfield.BuiltInGalleryImage``.
* widget: if not specified, defaults to ``GalleryWidget`` with default
values.
.. note:: If `target_model` not specified when initializing, an info will
be logged to the stdout, which can be turned off by adding
``gallery_form_field.I001`` in ``settings.SILENCED_SYSTEM_CHECKS``.
"""
default_error_messages = {
'required': _("The submitted file is empty."),
'invalid': _("The submitted images are invalid."),
}
def __init__(self, max_number_of_images=None, **kwargs):
# The following 2 are used to validate GalleryWidget params
# see GalleryWidget.defaults_checks()
self._image_model = kwargs.pop("target_model", None)
image_model_not_configured = False
if self._image_model is None:
# This happens when the formfield is used in a Non-model form
image_model_not_configured = True
self._image_model = _defaults.DEFAULT_TARGET_IMAGE_MODEL
# Make sure the model is valid target image model
if (self._image_model != _defaults.DEFAULT_TARGET_IMAGE_MODEL
or image_model_not_configured):
errors = get_or_check_image_field(
obj=self,
target_model=(
None if image_model_not_configured else self._image_model),
check_id_prefix="gallery_form_field",
is_checking=True)
for error in errors:
if error.is_serious():
raise ImproperlyConfigured(str(error))
else:
if error.is_silenced():
continue
logger.info(str(error))
# This is used for widget to identify which object the widget is servicing.
# That information will be used when raising errors.
self._widget_is_servicing = (
kwargs.pop("model_field", None) or self.__class__.__name__)
self._max_number_of_images = max_number_of_images
super().__init__(**kwargs)
_widget = GalleryWidget
@property
def widget(self):
return self._widget
@widget.setter
def widget(self, value):
# Property and setter are used to make sure the attributes will
# be passed to new widget instance when the widget instance
# is changed.
setattr(value, "max_number_of_images", self.max_number_of_images)
setattr(value, "image_model", self._image_model)
setattr(value, "widget_is_servicing", self._widget_is_servicing)
# Re-initialize the widget
value.is_localized = bool(self.localize)
value.is_required = self.required
extra_attrs = self.widget_attrs(value) or {}
value.attrs.update(extra_attrs)
self._widget = value
if not isinstance(self.widget, GalleryWidget):
return
# We set the upload_url and fetch_url
# when the widget didn't specify them.
self._set_widget_upload_url()
self._set_widget_fetch_url()
@property
def _target_app_model_name(self):
return "-".join(self._image_model.split(".")).lower()
def _set_widget_upload_url(self):
if self.widget.upload_url:
return
# Here we required a target_model should have a upload_url
# name in url_conf in the form of app_label_model_name-upload
# in lower case
self.widget.upload_url = (
"{}-upload".format(self._target_app_model_name.lower()))
def _set_widget_fetch_url(self):
if self.widget.disable_fetch or self.widget.fetch_url:
return
# Here we required a target_model should have a fetch_url
# name in url_conf in the form of app_label-model_name-fetch
# in lower case
self.widget.fetch_url = (
"{}-fetch".format(self._target_app_model_name.lower()))
@property
def max_number_of_images(self):
return self._max_number_of_images
@max_number_of_images.setter
def max_number_of_images(self, value):
if value is not None:
if not str(value).isdigit():
raise TypeError(
"'max_number_of_images' expects a positive integer, "
f"got {value}.")
value = int(value)
self._max_number_of_images = value
self._widget.max_number_of_images = value
if value:
self.validators.append(
MaxNumberOfImageValidator(int(value)))
def widget_attrs(self, widget):
# If BootStrap is loaded, "hiddeninput" is added by BootStrap.
# However, we need that css class to check changes of the form,
# so we added it manually.
return {
"class": " ".join(
[conf.FILES_FIELD_CLASS_NAME, "hiddeninput"])
}
def to_python(self, value):
converted = super().to_python(value)
if converted in self.empty_values:
return converted
# Make sure the json is a list of pks
if not isinstance(converted, list):
raise ValidationError(
self.error_messages['invalid'],
code='invalid',
params={'value': converted},
)
for _pk in converted:
if not str(_pk).isdigit():
raise ValidationError(
self.error_messages['invalid'],
code='invalid',
params={'value': converted},
)
# Make sure all pks exists
image_model = apps.get_model(self._image_model)
if (image_model.objects.filter(
pk__in=list(map(int, converted))).count()
!= len(converted)):
converted_copy = converted[:]
converted = []
for pk in converted_copy:
if image_model.objects.filter(pk=pk).count():
converted.append(pk)
return converted