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)