Source code for galleryfield.widgets
import json
from django import forms
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from galleryfield import conf, defaults
from galleryfield.utils import (convert_dict_to_plain_text,
get_formatted_thumbnail_size, get_url_from_str,
logger)
[docs]class GalleryWidget(forms.HiddenInput):
"""This is the default widget used by :class:`galleryfield.fields.GalleryFormField`.
:param upload_url: An URL name or an URL of the upload handler
view used by the widget instance, defaults to `None`.
When not set, this param will be auto-configured if the parent
:class:`galleryfield.fields.GalleryFormField` is used by
a :class:`galleryfield.fields.GalleryField`, and the value will follow
the :ref:`naming rule <image_handling_url_naming_rule>`.
The value can also be `None` if set explicitly by::
self.fields["images"].widget.upload_url = None
In this case, upload ui won't show upload buttons.
:type upload_url: str, optional
:param fetch_url: An URL name or an URL for fetching the existing
images in the gallery instance, defaults to `None`.
When not set, this param will be auto-configured if the parent
:class:`galleryfield.fields.GalleryFormField` is used by
a :class:`galleryfield.fields.GalleryField`, the value will follow
the :ref:`naming rule <image_handling_url_naming_rule>`.
:type fetch_url: str, optional
:param multiple: Whether allow to select multiple image files in the
file picker. Defaults to ``True``.
:type multiple: bool, optional
:param thumbnail_size: The thumbnail size (both width and height), defaults
to ``defaults.DEFAULT_THUMBNAIL_SIZE``, which can be overridden by
``settings.DJANGO_GALLERY_FIELD_CONFIG["thumbnails"]["size"]``.
The value can be set after the widget is initialized.
:type thumbnail_size: int, optional
:param template: The path of template which is used to render the widget.
defaults to ``galleryfield/widget.html``, which support template
inheritance.
:type template: str, optional
:param upload_template: The path of upload template used in the widget,
defaults to ``galleryfield/upload_template.html``.
:type upload_template: str, optional
:param download_template: The path of download template used in the widget,
defaults to ``galleryfield/download_template.html``.
:type download_template: str, optional
:param attrs: Html attribute when rendering the field (Which is a
:class:`django.forms.HiddenInput`), defaults to `None`. See
`Django docs <https://docs.djangoproject.com/en/dev/ref/forms/widgets/#django.forms.Widget.attrs>`_.
:type attrs: dict, optional
:param options: Other options when rendering the widget. Implemented options:
* **accepted_mime_types** (`list`, `optional`) - A list of MIME types
used to filter files when picking files with file picker, defaults to ``['image/*']``
:type options: dict, optional
:param jquery_file_upload_ui_options: The default template is using
blueimp/jQuery-File-Upload package to render the ui and dealing with
AJAX upload. See :setting:`jquery_file_upload_ui_options` for more information.
:type jquery_file_upload_ui_options: dict, optional
:param jquery_file_upload_ui_sortable_options: The default template is using
SortableJS/Sortable package to handle sorting of uploaded images in
the UI. See :setting:`jquery_file_upload_ui_sortable_options` for more information.
:type jquery_file_upload_ui_sortable_options: dict, optional
:param disable_fetch: Whether disable fetching existing images of the
form instance (if any), defaults to `False`. If True, the validity of
``fetch_url`` will not be checked.
:type disable_fetch: bool, optional
:param disable_server_side_crop: Whether disable server side cropping of
uploaded images, defaults to `False`.
:type disable_server_side_crop: bool, optional
""" # noqa
def __init__(
self,
upload_url=None,
fetch_url=None,
multiple=True,
thumbnail_size=conf.DEFAULT_THUMBNAIL_SIZE,
template="galleryfield/widget.html",
upload_template="galleryfield/upload_template.html",
download_template="galleryfield/download_template.html",
attrs=None, options=None,
jquery_file_upload_ui_options=None,
jquery_file_upload_ui_sortable_options=None,
disable_fetch=False,
disable_server_side_crop=False,
**kwargs):
super(GalleryWidget, self).__init__(attrs)
self.multiple = multiple
self._thumbnail_size = get_formatted_thumbnail_size(thumbnail_size)
self.template = template
self._upload_template = upload_template
self._download_template = download_template
self.disable_fetch = disable_fetch
self.disable_server_side_crop = disable_server_side_crop
self.upload_url = upload_url
self.fetch_url = (
None if disable_fetch else fetch_url)
self._jquery_file_upload_ui_options = jquery_file_upload_ui_options or {}
self._jquery_file_upload_ui_sortable_options = (
jquery_file_upload_ui_sortable_options or {})
self.options = options and options.copy() or {}
self.options.setdefault("accepted_mime_types", ['image/*'])
self._disabled = False
@property
def upload_template(self):
return self._upload_template
@upload_template.setter
def upload_template(self, value):
self._upload_template = value
@property
def download_template(self):
return self._download_template
@download_template.setter
def download_template(self, value):
self._download_template = value
@property
def thumbnail_size(self):
return self._thumbnail_size
@thumbnail_size.setter
def thumbnail_size(self, value):
self._thumbnail_size = get_formatted_thumbnail_size(value)
@property
def jquery_file_upload_ui_sortable_options(self):
return (self._jquery_file_upload_ui_sortable_options
or conf.JQUERY_FILE_UPLOAD_UI_DEFAULT_SORTABLE_OPTIONS)
@jquery_file_upload_ui_sortable_options.setter
def jquery_file_upload_ui_sortable_options(self, options):
if options is None:
return
if not isinstance(options, dict):
raise ImproperlyConfigured(
f"{self.__class__.__name__}: "
"'jquery_file_upload_ui_sortable_options' must be a dict"
)
sortable_settings = (
conf.JQUERY_FILE_UPLOAD_UI_DEFAULT_SORTABLE_OPTIONS.copy())
sortable_settings.update(options)
self._jquery_file_upload_ui_sortable_options = sortable_settings
@property
def jquery_file_upload_ui_options(self):
return (self._jquery_file_upload_ui_options
or conf.JQUERY_FILE_UPLOAD_UI_DEFAULT_OPTIONS)
@jquery_file_upload_ui_options.setter
def jquery_file_upload_ui_options(self, options):
if options is None:
return
if not isinstance(options, dict):
raise ImproperlyConfigured(
f"{self.__class__.__name__}: "
"'jquery_file_upload_ui_options' must be a dict"
)
ju_settings = (
conf.JQUERY_FILE_UPLOAD_UI_DEFAULT_OPTIONS.copy())
ju_settings.update(options)
if "maxNumberOfFiles" in ju_settings:
logger.warning(
"%(obj)s: 'maxNumberOfFiles' in 'jquery_file_upload_ui_options' "
"will be overridden later by the formfield. You should set that "
"value in the formfield it belongs to, e.g. \n"
"self.fields['my_gallery_field'].max_number_of_images = %(value)s"
% {"obj": self.__class__.__name__,
"value": str(ju_settings["maxNumberOfFiles"])}
)
if ("singleFileUploads" in ju_settings
and str(ju_settings["singleFileUploads"]).lower() == "false"):
logger.warning(
"%(obj)s: 'singleFileUploads=False' in "
"'jquery_file_upload_ui_options' is not allowed and will be "
"ignored."
% {"obj": self.__class__.__name__}
)
if "previewMaxWidth" in ju_settings or "previewMaxHeight" in ju_settings:
logger.warning(
"%(obj)s: 'previewMaxWidth' and 'previewMaxHeight' in "
"'jquery_file_upload_ui_options' are ignored. You should set "
"the value by the 'thumbnail_size' option, e.g., "
"thumbnail_size='120x60'"
% {"obj": self.__class__.__name__}
)
self._jquery_file_upload_ui_options = ju_settings
def set_and_check_urls(self):
# We now then the URL names into an actual URL, that
# can't be down in __init__ because url_conf is not
# loaded when doing the system check, and will result
# in failure to start.
try:
self.upload_url = get_url_from_str(
self.upload_url, require_urlconf_ready=True)
except Exception as e:
raise ImproperlyConfigured(
f"'upload_url' is invalid: '{type(e).__name__}': '{str(e)}'")
if self.disable_fetch:
self.fetch_url = None
else:
try:
self.fetch_url = get_url_from_str(
self.fetch_url, require_urlconf_ready=True)
except Exception as e:
raise ImproperlyConfigured(
f"'fetch_url' is invalid: '{type(e).__name__}': '{str(e)}'")
# In the following we validate update the urls from url names (if it is
# not an url) and check the potential conflicts of init params
# of the widget with the target_image_model set to the widget.
# For validation of urls, because those urls are assigned by reverse_lazy,
# the result of which will only be validated until evaluation, we
# can only do that until the request context is available.
# For checking conflicts, the logic is: if the target_image_model is not
# using the built-in model, i.e., defaults.DEFAULT_TARGET_IMAGE_MODEL,
# then the upload, crop and fetch views should not use the built-in ones.
# Because those views are using defaults.DEFAULT_TARGET_IMAGE_MODEL
# as the target image model.
# BTW, this check can't be done at the model/field check stage or formfield
# init stage, because we should allow the widget be changed after the form
# is initialized. So, we have to do this before rendering the widget,
# and throw the error if any.
target_image_model = getattr(self, "image_model", None)
image_model_is_default = (
target_image_model == defaults.DEFAULT_TARGET_IMAGE_MODEL)
if image_model_is_default:
return
conflict_config = []
upload_url_is_default = (
self.upload_url
== get_url_from_str(defaults.DEFAULT_UPLOAD_URL_NAME,
require_urlconf_ready=True))
if upload_url_is_default:
conflict_config.append(
{"param": "upload_url",
"value": self.upload_url})
if self.fetch_url:
fetch_url_is_default = (
self.fetch_url
== get_url_from_str(defaults.DEFAULT_FETCH_URL_NAME,
require_urlconf_ready=True))
if fetch_url_is_default:
conflict_config.append(
{"param": "fetch_url",
"value": self.fetch_url})
if not conflict_config:
return
widget_is_servicing = getattr(self, "widget_is_servicing")
msgs = ["'%(obj)s' is using '%(used_model)s' "
"instead of built-in '%(default_model)s', while "
% {"obj": widget_is_servicing,
"used_model": target_image_model,
"default_model": defaults.DEFAULT_TARGET_IMAGE_MODEL}
]
for cc in conflict_config:
msgs.append(
"'%(param)s' is using built-in value '%(value)s', " % cc)
msgs.append(
"which use built-in '%(default_model)s'. You need to write "
"your own views for your image model." % {
"default_model": defaults.DEFAULT_TARGET_IMAGE_MODEL
})
msg = "".join(msgs)
raise ImproperlyConfigured(msg)
class Media:
js = tuple(conf.JS)
css = {'all': tuple(conf.CSS)}
@property
def is_hidden(self):
return False
def get_stringfied_jquery_file_upload_ui_options(self):
# See blueimp/jQuery-File-Upload
# https://github.com/blueimp/jQuery-File-Upload/wiki/Options
# We copy the options as the actual context used in rendering
# so as to avoid logger warnings
ui_options = self.jquery_file_upload_ui_options.copy()
# Remove the option (i.e., use default False)
ui_options.pop("singleFileUploads", None)
# Remove other options which we are using default but
# don't allow user to change
for option in ["fileInput", "formData"]:
ui_options.pop(option, None)
# Fixme: this is hardcoded
ui_options["paramName"] = "files[]"
# override maxNumberOfFiles
ui_options.pop("maxNumberOfFiles", None)
max_number_of_images = (
getattr(self, "max_number_of_images", None))
if max_number_of_images:
ui_options["maxNumberOfFiles"] = max_number_of_images
# override previewMaxWidth and previewMaxHeight
_width, _height = self.thumbnail_size.split("x")
ui_options.update(
{"previewMaxWidth": int(_width),
"previewMaxHeight": int(_height),
# This is used as a CSS selector to fine the input field
"hiddenFileInput": f".{conf.FILES_FIELD_CLASS_NAME}",
})
# Compatibility with Bootstrap 4 and 5
# https://github.com/blueimp/jQuery-File-Upload/wiki/Style-Guide#bootstrap-ui
if conf.BOOTSTRAP_VERSION > 3:
ui_options["showElementClass"] = "show"
return convert_dict_to_plain_text(
ui_options, indent=16,
no_wrap_keys=["loadImageFileTypes", "acceptFileTypes",
"disableImageResize"])
def render(self, name, value, attrs=None, renderer=None):
self.set_and_check_urls()
context = {
'input_string': super().render(name, value, attrs, renderer),
'name': name,
'multiple': self.multiple and 1 or 0,
'thumbnail_size': str(self.thumbnail_size),
'prompt_alert_on_window_reload_if_changed':
conf.PROMPT_ALERT_ON_WINDOW_RELOAD_IF_CHANGED,
'upload_template': self.upload_template,
'download_template': self.download_template,
'bootstrap_version': conf.BOOTSTRAP_VERSION,
}
# Do not fill in empty value to hidden inputs
if value:
context["pks"] = json.loads(value)
context["fetch_url"] = self.fetch_url
_context = self.get_context(name, value, attrs)
if (_context["widget"]["attrs"].get("disabled", False)
or _context["widget"]["attrs"].get("readonly")):
context["uploader_disabled"] = True
self._disabled = True
else:
context.update({
"upload_url": self.upload_url,
"accepted_mime_types": self.options["accepted_mime_types"],
"disable_server_side_crop": self.disable_server_side_crop
})
context["widget"] = _context["widget"]
context["jquery_fileupload_ui_options"] = (
self.get_stringfied_jquery_file_upload_ui_options())
sortable_options = self.jquery_file_upload_ui_sortable_options.copy()
if self._disabled:
sortable_options["disabled"] = True
context["jquery_fileupload_ui_sortable_options"] = (
convert_dict_to_plain_text(sortable_options, indent=16)
)
context["csrfCookieName"] = getattr(settings, "CSRF_COOKIE_NAME")
return renderer.render(template_name=self.template, context=context)