from django.conf import settings from django.shortcuts import get_object_or_404 from django.template.loader import render_to_string from django.urls import path, reverse from django.utils.functional import cached_property from django.utils.http import urlencode from django.utils.translation import gettext_lazy as _ from django.views.generic.base import View from wagtail.admin.auth import PermissionPolicyChecker from wagtail.admin.modal_workflow import render_modal_workflow from wagtail.admin.models import popular_tags_for_model from wagtail.admin.views.generic.chooser import ( BaseChooseView, ChooseResultsViewMixin, ChooseViewMixin, ChosenMultipleViewMixin, ChosenResponseMixin, ChosenViewMixin, CreateViewMixin, CreationFormMixin, PreserveURLParametersMixin, ) from wagtail.admin.viewsets.chooser import ChooserViewSet from wagtail.images import get_image_model from wagtail.images.formats import get_image_format from wagtail.images.forms import ImageInsertionForm, get_image_form from wagtail.images.permissions import permission_policy from wagtail.images.utils import find_image_duplicates permission_checker = PermissionPolicyChecker(permission_policy) class ImageChosenResponseMixin(ChosenResponseMixin): def get_chosen_response_data(self, image, preview_image_filter="max-165x165"): """ Given an image, return the json data to pass back to the image chooser panel """ response_data = super().get_chosen_response_data(image) preview_image = image.get_rendition(preview_image_filter) response_data["preview"] = { "url": preview_image.url, "width": preview_image.width, "height": preview_image.height, } return response_data class ImageCreationFormMixin(CreationFormMixin): creation_tab_id = "upload" create_action_label = _("Upload") create_action_clicked_label = _("Uploading…") permission_policy = permission_policy def get_creation_form_class(self): return get_image_form(self.model) def get_creation_form_kwargs(self): kwargs = super().get_creation_form_kwargs() kwargs.update( { "user": self.request.user, "prefix": "image-chooser-upload", } ) if self.request.method in ("POST", "PUT"): kwargs["instance"] = self.model(uploaded_by_user=self.request.user) return kwargs class BaseImageChooseView(BaseChooseView): template_name = "wagtailimages/chooser/chooser.html" results_template_name = "wagtailimages/chooser/results.html" per_page = 12 ordering = "-created_at" construct_queryset_hook_name = "construct_image_chooser_queryset" def get_object_list(self): return ( permission_policy.instances_user_has_any_permission_for( self.request.user, ["choose"] ) .select_related("collection") .prefetch_renditions("max-165x165") ) def filter_object_list(self, objects): tag_name = self.request.GET.get("tag") if tag_name: objects = objects.filter(tags__name=tag_name) return super().filter_object_list(objects) def get_filter_form(self): FilterForm = self.get_filter_form_class() return FilterForm(self.request.GET, collections=self.collections) @cached_property def collections(self): collections = self.permission_policy.collections_user_has_permission_for( self.request.user, "choose" ) if len(collections) < 2: return None return collections def get(self, request): self.model = get_image_model() return super().get(request) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) chosen_url_name = ( "wagtailimages_chooser:select_format" if self.request.GET.get("select_format") else "wagtailimages_chooser:chosen" ) for image in context["results"]: image.chosen_url = self.append_preserved_url_parameters( reverse(chosen_url_name, args=(image.id,)) ) context["collections"] = self.collections return context class ImageChooseViewMixin(ChooseViewMixin): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["popular_tags"] = popular_tags_for_model(self.model) return context class ImageChooseView( ImageChooseViewMixin, ImageCreationFormMixin, BaseImageChooseView ): pass class ImageChooseResultsView( ChooseResultsViewMixin, ImageCreationFormMixin, BaseImageChooseView ): pass class ImageChosenView(ChosenViewMixin, ImageChosenResponseMixin, View): def get(self, request, *args, pk, **kwargs): self.model = get_image_model() return super().get(request, *args, pk, **kwargs) class ImageChosenMultipleView(ChosenMultipleViewMixin, ImageChosenResponseMixin, View): def get(self, request, *args, **kwargs): self.model = get_image_model() return super().get(request, *args, **kwargs) class SelectFormatResponseMixin(PreserveURLParametersMixin): def render_select_format_response(self, image, form): action_url = self.append_preserved_url_parameters( reverse("wagtailimages_chooser:select_format", args=(image.id,)) ) return render_modal_workflow( self.request, "wagtailimages/chooser/select_format.html", None, {"image": image, "form": form, "select_format_action_url": action_url}, json_data={"step": "select_format"}, ) class ImageUploadViewMixin(SelectFormatResponseMixin, CreateViewMixin): def get(self, request): self.model = get_image_model() return super().get(request) def post(self, request): self.model = get_image_model() self.form = self.get_creation_form() if self.form.is_valid(): image = self.save_form(self.form) duplicates = find_image_duplicates( image=image, user=request.user, permission_policy=permission_policy, ) existing_image = duplicates.first() if existing_image: return self.render_duplicate_found_response( request, image, existing_image ) if request.GET.get("select_format"): insertion_form = ImageInsertionForm( initial={"alt_text": image.default_alt_text}, prefix="image-chooser-insertion", ) return self.render_select_format_response(image, insertion_form) else: # not specifying a format; return the image details now return self.get_chosen_response(image) else: # form is invalid return self.get_reshow_creation_form_response() def render_duplicate_found_response(self, request, new_image, existing_image): next_step_url = ( "wagtailimages_chooser:select_format" if request.GET.get("select_format") else "wagtailimages_chooser:chosen" ) choose_new_image_url = self.append_preserved_url_parameters( reverse(next_step_url, args=(new_image.id,)) ) choose_existing_image_url = self.append_preserved_url_parameters( reverse(next_step_url, args=(existing_image.id,)) ) cancel_duplicate_upload_action = ( f"{reverse('wagtailimages:delete', args=(new_image.id,))}?" f"{urlencode({'next': choose_existing_image_url})}" ) duplicate_upload_html = render_to_string( "wagtailimages/chooser/confirm_duplicate_upload.html", { "new_image": new_image, "existing_image": existing_image, "confirm_duplicate_upload_action": choose_new_image_url, "cancel_duplicate_upload_action": cancel_duplicate_upload_action, }, request, ) return render_modal_workflow( request, None, None, None, json_data={ "step": "duplicate_found", "htmlFragment": duplicate_upload_html, }, ) class ImageUploadView( ImageUploadViewMixin, ImageCreationFormMixin, ImageChosenResponseMixin, View ): pass class ImageSelectFormatView(SelectFormatResponseMixin, ImageChosenResponseMixin, View): model = None def get(self, request, image_id): image = get_object_or_404(self.model, id=image_id) initial = {"alt_text": image.default_alt_text} initial.update(request.GET.dict()) # If you edit an existing image, and there is no alt text, ensure that # "image is decorative" is ticked when you open the form initial["image_is_decorative"] = initial["alt_text"] == "" form = ImageInsertionForm(initial=initial, prefix="image-chooser-insertion") return self.render_select_format_response(image, form) def get_chosen_response_data(self, image): format = get_image_format(self.form.cleaned_data["format"]) alt_text = self.form.cleaned_data["alt_text"] response_data = super().get_chosen_response_data( image, preview_image_filter=format.filter_spec ) response_data.update( { "format": format.name, "alt": alt_text, "class": format.classname, "html": format.image_to_editor_html(image, alt_text), } ) return response_data def post(self, request, image_id): image = get_object_or_404(get_image_model(), id=image_id) self.form = ImageInsertionForm( request.POST, initial={"alt_text": image.default_alt_text}, prefix="image-chooser-insertion", ) if self.form.is_valid(): return self.get_chosen_response(image) else: return self.render_select_format_response(image, self.form) class ImageChooserViewSet(ChooserViewSet): choose_view_class = ImageChooseView choose_results_view_class = ImageChooseResultsView chosen_view_class = ImageChosenView chosen_multiple_view_class = ImageChosenMultipleView create_view_class = ImageUploadView select_format_view_class = ImageSelectFormatView permission_policy = permission_policy register_widget = False preserve_url_parameters = ChooserViewSet.preserve_url_parameters + ["select_format"] icon = "image" per_page = getattr(settings, "WAGTAILIMAGES_CHOOSER_PAGE_SIZE", 10) choose_one_text = _("Choose an image") create_action_label = _("Upload") create_action_clicked_label = _("Uploading…") choose_another_text = _("Choose another image") edit_item_text = _("Edit this image") @property def select_format_view(self): return self.select_format_view_class.as_view( model=self.model, preserve_url_parameters=self.preserve_url_parameters, ) def get_urlpatterns(self): return super().get_urlpatterns() + [ path( "/select_format/", self.select_format_view, name="select_format", ), ] viewset = ImageChooserViewSet( "wagtailimages_chooser", model=get_image_model(), url_prefix="images/chooser", )