Initial commit

This commit is contained in:
2024-08-27 20:33:44 +02:00
commit 1f1832267d
14794 changed files with 1599592 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
from .add_tags import AddTagsBulkAction
from .add_to_collection import AddToCollectionBulkAction
from .delete import DeleteBulkAction
__all__ = ["AddToCollectionBulkAction", "AddTagsBulkAction", "DeleteBulkAction"]

View File

@@ -0,0 +1,44 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext
from wagtail.admin import widgets
from wagtail.documents.views.bulk_actions.document_bulk_action import DocumentBulkAction
class TagForm(forms.Form):
tags = forms.Field(label=_("Tags"), widget=widgets.AdminTagWidget)
class AddTagsBulkAction(DocumentBulkAction):
display_name = _("Tag")
action_type = "add_tags"
aria_label = _("Add tags to the selected documents")
template_name = "wagtaildocs/bulk_actions/confirm_bulk_add_tags.html"
action_priority = 20
form_class = TagForm
def check_perm(self, document):
return self.permission_policy.user_has_permission_for_instance(
self.request.user, "change", document
)
def get_execution_context(self):
return {"tags": self.cleaned_form.cleaned_data["tags"].split(",")}
@classmethod
def execute_action(cls, objects, tags=[], **kwargs):
num_parent_objects = 0
if not tags:
return
for document in objects:
num_parent_objects += 1
document.tags.add(*tags)
return num_parent_objects, 0
def get_success_message(self, num_parent_objects, num_child_objects):
return ngettext(
"New tags have been added to %(num_parent_objects)d document",
"New tags have been added to %(num_parent_objects)d documents",
num_parent_objects,
) % {"num_parent_objects": num_parent_objects}

View File

@@ -0,0 +1,57 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext
from wagtail.documents.views.bulk_actions.document_bulk_action import DocumentBulkAction
class CollectionForm(forms.Form):
def __init__(self, *args, **kwargs):
user = kwargs.pop("user", None)
super().__init__(*args, **kwargs)
self.fields["collection"] = forms.ModelChoiceField(
label=_("Collection"),
queryset=DocumentBulkAction.permission_policy.collections_user_has_permission_for(
user, "add"
),
)
class AddToCollectionBulkAction(DocumentBulkAction):
display_name = _("Add to collection")
action_type = "add_to_collection"
aria_label = _("Add selected documents to collection")
template_name = "wagtaildocs/bulk_actions/confirm_bulk_add_to_collection.html"
action_priority = 30
form_class = CollectionForm
collection = None
def check_perm(self, document):
return self.permission_policy.user_has_permission_for_instance(
self.request.user, "change", document
)
def get_form_kwargs(self):
return {**super().get_form_kwargs(), "user": self.request.user}
def get_execution_context(self):
return {"collection": self.cleaned_form.cleaned_data["collection"]}
@classmethod
def execute_action(cls, objects, collection=None, **kwargs):
if collection is None:
return
num_parent_objects = (
cls.get_default_model()
.objects.filter(pk__in=[obj.pk for obj in objects])
.update(collection=collection)
)
return num_parent_objects, 0
def get_success_message(self, num_parent_objects, num_child_objects):
collection = self.cleaned_form.cleaned_data["collection"]
return ngettext(
"%(num_parent_objects)d document has been added to %(collection)s",
"%(num_parent_objects)d documents have been added to %(collection)s",
num_parent_objects,
) % {"num_parent_objects": num_parent_objects, "collection": collection}

View File

@@ -0,0 +1,33 @@
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext
from wagtail.documents.views.bulk_actions.document_bulk_action import DocumentBulkAction
class DeleteBulkAction(DocumentBulkAction):
display_name = _("Delete")
action_type = "delete"
aria_label = _("Delete selected documents")
template_name = "wagtaildocs/bulk_actions/confirm_bulk_delete.html"
action_priority = 100
classes = {"serious"}
def check_perm(self, document):
return self.permission_policy.user_has_permission_for_instance(
self.request.user, "delete", document
)
@classmethod
def execute_action(cls, objects, **kwargs):
num_parent_objects = len(objects)
cls.get_default_model().objects.filter(
pk__in=[obj.pk for obj in objects]
).delete()
return num_parent_objects, 0
def get_success_message(self, num_parent_objects, num_child_objects):
return ngettext(
"%(num_parent_objects)d document has been deleted",
"%(num_parent_objects)d documents have been deleted",
num_parent_objects,
) % {"num_parent_objects": num_parent_objects}

View File

@@ -0,0 +1,36 @@
from wagtail.admin.views.bulk_action import BulkAction
from wagtail.documents import get_document_model
from wagtail.documents.permissions import (
permission_policy as documents_permission_policy,
)
class DocumentBulkAction(BulkAction):
permission_policy = documents_permission_policy
models = [get_document_model()]
def get_all_objects_in_listing_query(self, parent_id):
listing_objects = self.model.objects.all()
if parent_id is not None:
listing_objects = listing_objects.filter(collection_id=parent_id)
listing_objects = listing_objects.values_list("pk", flat=True)
if "q" in self.request.GET:
query_string = self.request.GET.get("q", "")
listing_objects = listing_objects.search(query_string).results()
return listing_objects
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["items_with_no_access"] = [
{
"item": document,
"can_edit": self.permission_policy.user_has_permission_for_instance(
self.request.user, "change", document
),
}
for document in context["items_with_no_access"]
]
return context

View File

@@ -0,0 +1,195 @@
from django import forms
from django.utils.functional import cached_property
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from django.views.generic.base import View
from wagtail.admin.staticfiles import versioned_static
from wagtail.admin.ui.tables import Column, DateColumn, DownloadColumn
from wagtail.admin.views.generic.chooser import (
BaseChooseView,
ChooseResultsViewMixin,
ChooseViewMixin,
ChosenResponseMixin,
ChosenViewMixin,
CreateViewMixin,
CreationFormMixin,
)
from wagtail.admin.viewsets.chooser import ChooserViewSet
from wagtail.admin.widgets import BaseChooser, BaseChooserAdapter
from wagtail.blocks import ChooserBlock
from wagtail.documents import get_document_model, get_document_model_string
from wagtail.documents.permissions import permission_policy
class DocumentChosenResponseMixin(ChosenResponseMixin):
def get_chosen_response_data(self, document):
response_data = super().get_chosen_response_data(document)
response_data.update(
{
"url": document.url,
"filename": document.filename,
}
)
return response_data
class DocumentCreationFormMixin(CreationFormMixin):
creation_tab_id = "upload"
def get_creation_form_class(self):
from wagtail.documents.forms import get_document_form
return get_document_form(self.model)
def get_creation_form_kwargs(self):
kwargs = super().get_creation_form_kwargs()
kwargs.update(
{
"user": self.request.user,
"prefix": "document-chooser-upload",
}
)
if self.request.method in ("POST", "PUT"):
kwargs["instance"] = self.model(uploaded_by_user=self.request.user)
return kwargs
class BaseDocumentChooseView(BaseChooseView):
results_template_name = "wagtaildocs/chooser/results.html"
per_page = 10
ordering = "-created_at"
construct_queryset_hook_name = "construct_document_chooser_queryset"
def get_object_list(self):
return self.permission_policy.instances_user_has_any_permission_for(
self.request.user, ["choose"]
)
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
@property
def columns(self):
columns = super().columns + [
DownloadColumn("filename", label=_("File")),
DateColumn("created_at", label=_("Created"), width="16%"),
]
if self.collections:
columns.insert(2, Column("collection", label=_("Collection")))
return columns
def get(self, request):
self.model = get_document_model()
return super().get(request)
class DocumentChooseViewMixin(ChooseViewMixin):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["collections"] = self.collections
return context
class DocumentChooseView(
DocumentChooseViewMixin, DocumentCreationFormMixin, BaseDocumentChooseView
):
pass
class DocumentChooseResultsView(
ChooseResultsViewMixin, DocumentCreationFormMixin, BaseDocumentChooseView
):
pass
class DocumentChosenView(ChosenViewMixin, DocumentChosenResponseMixin, View):
def get(self, request, *args, pk, **kwargs):
self.model = get_document_model()
return super().get(request, *args, pk, **kwargs)
class DocumentChooserUploadView(
CreateViewMixin, DocumentCreationFormMixin, DocumentChosenResponseMixin, View
):
def dispatch(self, request, *args, **kwargs):
self.model = get_document_model()
return super().dispatch(request, *args, **kwargs)
class BaseAdminDocumentChooser(BaseChooser):
classname = "document-chooser"
js_constructor = "DocumentChooser"
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.model = get_document_model_string()
@property
def media(self):
return forms.Media(
js=[
versioned_static("wagtaildocs/js/document-chooser-modal.js"),
versioned_static("wagtaildocs/js/document-chooser.js"),
]
)
class DocumentChooserAdapter(BaseChooserAdapter):
js_constructor = "wagtail.documents.widgets.DocumentChooser"
@cached_property
def media(self):
return forms.Media(
js=[
versioned_static("wagtaildocs/js/document-chooser-modal.js"),
versioned_static("wagtaildocs/js/document-chooser-telepath.js"),
]
)
class BaseDocumentChooserBlock(ChooserBlock):
def render_basic(self, value, context=None):
if value:
return format_html('<a href="{0}">{1}</a>', value.url, value.title)
else:
return ""
class DocumentChooserViewSet(ChooserViewSet):
choose_view_class = DocumentChooseView
choose_results_view_class = DocumentChooseResultsView
chosen_view_class = DocumentChosenView
create_view_class = DocumentChooserUploadView
base_widget_class = BaseAdminDocumentChooser
widget_telepath_adapter_class = DocumentChooserAdapter
base_block_class = BaseDocumentChooserBlock
permission_policy = permission_policy
icon = "doc-full-inverse"
choose_one_text = _("Choose a document")
create_action_label = _("Upload")
create_action_clicked_label = _("Uploading…")
choose_another_text = _("Choose another document")
edit_item_text = _("Edit this document")
viewset = DocumentChooserViewSet(
"wagtaildocs_chooser",
model=get_document_model_string(),
url_prefix="documents/chooser",
)

View File

@@ -0,0 +1,318 @@
import os
from django.contrib.admin.utils import quote
from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.http import urlencode
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy, ngettext
from wagtail.admin import messages
from wagtail.admin.auth import PermissionPolicyChecker
from wagtail.admin.filters import BaseMediaFilterSet
from wagtail.admin.ui.tables import (
BulkActionsCheckboxColumn,
Column,
DateColumn,
DownloadColumn,
Table,
TitleColumn,
)
from wagtail.admin.utils import get_valid_next_url_from_request, set_query_params
from wagtail.admin.views import generic
from wagtail.documents import get_document_model
from wagtail.documents.forms import get_document_form
from wagtail.documents.permissions import permission_policy
permission_checker = PermissionPolicyChecker(permission_policy)
Document = get_document_model()
class BulkActionsColumn(BulkActionsCheckboxColumn):
def __init__(self, *args, **kwargs):
super().__init__(*args, obj_type="document", **kwargs)
def get_header_context_data(self, parent_context):
context = super().get_header_context_data(parent_context)
parent = parent_context.get("current_collection")
if parent:
context["parent"] = parent.id
return context
class DocumentTable(Table):
def get_context_data(self, parent_context):
context = super().get_context_data(parent_context)
context["current_collection"] = parent_context.get("current_collection")
return context
class DocumentsFilterSet(BaseMediaFilterSet):
permission_policy = permission_policy
class Meta:
model = Document
fields = []
class IndexView(generic.IndexView):
permission_policy = permission_policy
any_permission_required = ["add", "change", "delete"]
context_object_name = "documents"
page_title = gettext_lazy("Documents")
header_icon = "doc-full-inverse"
page_kwarg = "p"
paginate_by = 20
index_url_name = "wagtaildocs:index"
index_results_url_name = "wagtaildocs:index_results"
add_url_name = "wagtaildocs:add_multiple"
edit_url_name = "wagtaildocs:edit"
template_name = "wagtaildocs/documents/index.html"
results_template_name = "wagtaildocs/documents/index_results.html"
default_ordering = "title"
table_class = DocumentTable
filterset_class = DocumentsFilterSet
model = get_document_model()
add_item_label = gettext_lazy("Add a document")
show_other_searches = True
_show_breadcrumbs = True
def get_base_queryset(self):
# Get documents (filtered by user permission)
return self.permission_policy.instances_user_has_any_permission_for(
self.request.user, ["change", "delete"]
).select_related("collection")
@cached_property
def current_collection(self):
# Upon validation, the cleaned data is a Collection instance
return self.filters and self.filters.form.cleaned_data.get("collection_id")
@cached_property
def columns(self):
columns = [
BulkActionsColumn("bulk_actions"),
TitleColumn(
"title",
label=_("Title"),
sort_key="title",
get_url=self.get_edit_url,
get_title_id=lambda doc: f"document_{quote(doc.pk)}_title",
),
DownloadColumn("filename", label=_("File")),
DateColumn(
"created_at",
label=_("Created"),
sort_key="created_at",
width="16%",
),
]
if self.filters and "collection_id" in self.filters.filters:
columns.insert(
3,
Column("collection", label=_("Collection"), accessor="collection.name"),
)
return columns
@cached_property
def collections(self):
collections = permission_policy.collections_user_has_any_permission_for(
self.request.user, ["add", "change"]
)
if len(collections) < 2:
collections = None
return collections
def get_next_url(self):
next_url = self.index_url
request_query_string = self.request.META.get("QUERY_STRING")
if request_query_string:
next_url += "?" + request_query_string
return next_url
def get_add_url(self):
# Pass the collection filter to prefill the add form's collection field
return set_query_params(
super().get_add_url(),
{"collection_id": self.current_collection and self.current_collection.pk},
)
def get_edit_url(self, instance):
return set_query_params(
super().get_edit_url(instance),
{"next": self.get_next_url()},
)
def get_filterset_kwargs(self):
kwargs = super().get_filterset_kwargs()
kwargs["is_searching"] = self.is_searching
return kwargs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["current_collection"] = self.current_collection
return context
@permission_checker.require("add")
def add(request):
Document = get_document_model()
DocumentForm = get_document_form(Document)
if request.method == "POST":
doc = Document(uploaded_by_user=request.user)
form = DocumentForm(
request.POST, request.FILES, instance=doc, user=request.user
)
if form.is_valid():
form.save()
messages.success(
request,
_("Document '%(document_title)s' added.")
% {"document_title": doc.title},
buttons=[
messages.button(
reverse("wagtaildocs:edit", args=(doc.id,)), _("Edit")
)
],
)
return redirect("wagtaildocs:index")
else:
messages.error(request, _("The document could not be saved due to errors."))
else:
form = DocumentForm(user=request.user)
return TemplateResponse(
request,
"wagtaildocs/documents/add.html",
{
"form": form,
},
)
@permission_checker.require("change")
def edit(request, document_id):
Document = get_document_model()
DocumentForm = get_document_form(Document)
doc = get_object_or_404(Document, id=document_id)
if not permission_policy.user_has_permission_for_instance(
request.user, "change", doc
):
raise PermissionDenied
next_url = get_valid_next_url_from_request(request)
if request.method == "POST":
form = DocumentForm(
request.POST, request.FILES, instance=doc, user=request.user
)
if form.is_valid():
doc = form.save()
edit_url = reverse("wagtaildocs:edit", args=(doc.id,))
redirect_url = "wagtaildocs:index"
if next_url:
edit_url = f"{edit_url}?{urlencode({'next': next_url})}"
redirect_url = next_url
messages.success(
request,
_("Document '%(document_title)s' updated")
% {"document_title": doc.title},
buttons=[messages.button(edit_url, _("Edit"))],
)
return redirect(redirect_url)
else:
messages.error(request, _("The document could not be saved due to errors."))
else:
form = DocumentForm(instance=doc, user=request.user)
try:
local_path = doc.file.path
except NotImplementedError:
# Document is hosted externally (eg, S3)
local_path = None
if local_path:
# Give error if document file doesn't exist
if not os.path.isfile(local_path):
messages.error(
request,
_(
"The file could not be found. Please change the source or delete the document"
),
buttons=[
messages.button(
reverse("wagtaildocs:delete", args=(doc.id,)), _("Delete")
)
],
)
return TemplateResponse(
request,
"wagtaildocs/documents/edit.html",
{
"document": doc,
"filesize": doc.get_file_size(),
"form": form,
"user_can_delete": permission_policy.user_has_permission_for_instance(
request.user, "delete", doc
),
"next": next_url,
},
)
class DeleteView(generic.DeleteView):
model = get_document_model()
pk_url_kwarg = "document_id"
permission_policy = permission_policy
permission_required = "delete"
header_icon = "doc-full-inverse"
usage_url_name = "wagtaildocs:document_usage"
delete_url_name = "wagtaildocs:delete"
index_url_name = "wagtaildocs:index"
page_title = gettext_lazy("Delete document")
def user_has_permission(self, permission):
return self.permission_policy.user_has_permission_for_instance(
self.request.user, permission, self.object
)
@property
def confirmation_message(self):
# This message will only appear in the singular, but we specify a plural
# so it can share the translation string with confirm_bulk_delete.html
return ngettext(
"Are you sure you want to delete this document?",
"Are you sure you want to delete these documents?",
1,
)
def get_success_message(self):
return _("Document '%(document_title)s' deleted.") % {
"document_title": self.object.title
}
class UsageView(generic.UsageView):
model = get_document_model()
pk_url_kwarg = "document_id"
permission_policy = permission_policy
permission_required = "change"
header_icon = "doc-full-inverse"
def user_has_permission(self, permission):
return self.permission_policy.user_has_permission_for_instance(
self.request.user, permission, self.object
)
def get_page_subtitle(self):
return self.object.title

View File

@@ -0,0 +1,121 @@
import os.path
from wagtail.admin.views.generic.multiple_upload import AddView as BaseAddView
from wagtail.admin.views.generic.multiple_upload import (
CreateFromUploadView as BaseCreateFromUploadView,
)
from wagtail.admin.views.generic.multiple_upload import (
DeleteUploadView as BaseDeleteUploadView,
)
from wagtail.admin.views.generic.multiple_upload import DeleteView as BaseDeleteView
from wagtail.admin.views.generic.multiple_upload import EditView as BaseEditView
from .. import get_document_model
from ..forms import get_document_form, get_document_multi_form
from ..permissions import permission_policy
class AddView(BaseAddView):
permission_policy = permission_policy
template_name = "wagtaildocs/multiple/add.html"
edit_object_url_name = "wagtaildocs:edit_multiple"
delete_object_url_name = "wagtaildocs:delete_multiple"
edit_object_form_prefix = "doc"
context_object_name = "doc"
context_object_id_name = "doc_id"
edit_upload_url_name = "wagtaildocs:create_multiple_from_uploaded_document"
delete_upload_url_name = "wagtaildocs:delete_upload_multiple"
edit_upload_form_prefix = "uploaded-document"
context_upload_name = "uploaded_document"
context_upload_id_name = "uploaded_file_id"
def get_model(self):
return get_document_model()
def get_upload_form_class(self):
return get_document_form(self.model)
def get_edit_form_class(self):
return get_document_multi_form(self.model)
def save_object(self, form):
doc = form.save(commit=False)
doc.uploaded_by_user = self.request.user
doc._set_document_file_metadata()
doc.save()
return doc
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update(
{
"max_title_length": self.form.fields["title"].max_length,
}
)
return context
class EditView(BaseEditView):
permission_policy = permission_policy
pk_url_kwarg = "doc_id"
edit_object_form_prefix = "doc"
context_object_name = "doc"
context_object_id_name = "doc_id"
edit_object_url_name = "wagtaildocs:edit_multiple"
delete_object_url_name = "wagtaildocs:delete_multiple"
def get_model(self):
return get_document_model()
def get_edit_form_class(self):
return get_document_multi_form(self.model)
class DeleteView(BaseDeleteView):
permission_policy = permission_policy
pk_url_kwarg = "doc_id"
context_object_id_name = "doc_id"
def get_model(self):
return get_document_model()
class CreateFromUploadedDocumentView(BaseCreateFromUploadView):
edit_upload_url_name = "wagtaildocs:create_multiple_from_uploaded_document"
delete_upload_url_name = "wagtaildocs:delete_upload_multiple"
upload_pk_url_kwarg = "uploaded_file_id"
edit_upload_form_prefix = "uploaded-document"
context_object_id_name = "doc_id"
context_upload_name = "uploaded_document"
def get_model(self):
return get_document_model()
def get_edit_form_class(self):
return get_document_multi_form(self.model)
def save_object(self, form):
# assign the file content from uploaded_doc to the image object, to ensure it gets saved to
# Document's storage
self.object.file.save(
os.path.basename(self.upload.file.name), self.upload.file.file, save=False
)
self.object.uploaded_by_user = self.request.user
# form.save() would normally handle writing the image file metadata, but in this case the
# file handling happens outside the form, so we need to do that manually
self.object._set_document_file_metadata()
form.save()
class DeleteUploadView(BaseDeleteUploadView):
upload_pk_url_kwarg = "uploaded_file_id"
def get_model(self):
return get_document_model()

View File

@@ -0,0 +1,158 @@
from warnings import warn
from django.conf import settings
from django.http import FileResponse, Http404, HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.http import url_has_allowed_host_and_scheme
from django.views.decorators.http import etag
from wagtail import hooks
from wagtail.documents import get_document_model
from wagtail.documents.models import document_served
from wagtail.forms import PasswordViewRestrictionForm
from wagtail.models import CollectionViewRestriction
from wagtail.utils import sendfile_streaming_backend
from wagtail.utils.deprecation import RemovedInWagtail70Warning
from wagtail.utils.sendfile import sendfile
def document_etag(request, document_id, document_filename):
Document = get_document_model()
if hasattr(Document, "file_hash"):
return (
Document.objects.filter(id=document_id)
.values_list("file_hash", flat=True)
.first()
)
@etag(document_etag)
def serve(request, document_id, document_filename):
Document = get_document_model()
doc = get_object_or_404(Document, id=document_id)
# We want to ensure that the document filename provided in the URL matches the one associated with the considered
# document_id. If not we can't be sure that the document the user wants to access is the one corresponding to the
# <document_id, document_filename> pair.
if doc.filename != document_filename:
raise Http404("This document does not match the given filename.")
for fn in hooks.get_hooks("before_serve_document"):
result = fn(doc, request)
if isinstance(result, HttpResponse):
return result
# Send document_served signal
document_served.send(sender=Document, instance=doc, request=request)
try:
local_path = doc.file.path
except NotImplementedError:
local_path = None
try:
direct_url = doc.file.url
except NotImplementedError:
direct_url = None
serve_method = getattr(settings, "WAGTAILDOCS_SERVE_METHOD", None)
# If no serve method has been specified, select an appropriate default for the storage backend:
# redirect for remote storages (i.e. ones that provide a url but not a local path) and
# serve_view for all other cases
if serve_method is None:
if direct_url and not local_path:
serve_method = "redirect"
else:
serve_method = "serve_view"
if serve_method in ("redirect", "direct") and direct_url:
# Serve the file by redirecting to the URL provided by the underlying storage;
# this saves the cost of delivering the file via Python.
# For serve_method == 'direct', this view should not normally be reached
# (the document URL as used in links should point directly to the storage URL instead)
# but we handle it as a redirect to provide sensible fallback /
# backwards compatibility behaviour.
return redirect(direct_url)
if local_path:
# Use wagtail.utils.sendfile to serve the file;
# this provides support for mimetypes, if-modified-since and django-sendfile backends
sendfile_opts = {
"attachment": (doc.content_disposition != "inline"),
"attachment_filename": doc.filename,
"mimetype": doc.content_type,
}
if not hasattr(settings, "SENDFILE_BACKEND"):
# Fallback to streaming backend if user hasn't specified SENDFILE_BACKEND
sendfile_opts["backend"] = sendfile_streaming_backend.sendfile
return sendfile(request, local_path, **sendfile_opts)
else:
# We are using a storage backend which does not expose filesystem paths
# (e.g. storages.backends.s3boto.S3BotoStorage) AND the developer has not allowed
# redirecting to the file url directly.
# Fall back on pre-sendfile behaviour of reading the file content and serving it
# as a FileResponse
response = FileResponse(doc.file, doc.content_type)
# set filename and filename* to handle non-ascii characters in filename
# see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
response["Content-Disposition"] = doc.content_disposition
# FIXME: storage backends are not guaranteed to implement 'size'
response["Content-Length"] = doc.file.size
return response
def authenticate_with_password(request, restriction_id):
"""
Handle a submission of PasswordViewRestrictionForm to grant view access over a
subtree that is protected by a PageViewRestriction
"""
restriction = get_object_or_404(CollectionViewRestriction, id=restriction_id)
if request.method == "POST":
form = PasswordViewRestrictionForm(request.POST, instance=restriction)
if form.is_valid():
return_url = form.cleaned_data["return_url"]
if not url_has_allowed_host_and_scheme(
return_url, request.get_host(), request.is_secure()
):
return_url = settings.LOGIN_REDIRECT_URL
restriction.mark_as_passed(request)
return redirect(return_url)
else:
form = PasswordViewRestrictionForm(instance=restriction)
action_url = reverse(
"wagtaildocs_authenticate_with_password", args=[restriction.id]
)
password_required_template = getattr(
settings,
"WAGTAILDOCS_PASSWORD_REQUIRED_TEMPLATE",
"wagtaildocs/password_required.html",
)
if hasattr(settings, "DOCUMENT_PASSWORD_REQUIRED_TEMPLATE"):
warn(
"The `DOCUMENT_PASSWORD_REQUIRED_TEMPLATE` setting is deprecated - use `WAGTAILDOCS_PASSWORD_REQUIRED_TEMPLATE` instead.",
category=RemovedInWagtail70Warning,
)
password_required_template = getattr(
settings,
"DOCUMENT_PASSWORD_REQUIRED_TEMPLATE",
password_required_template,
)
context = {"form": form, "action_url": action_url}
return TemplateResponse(request, password_required_template, context)