Skip to content

Instantly share code, notes, and snippets.

@kristinriebe
Last active December 2, 2023 17:01
Show Gist options
  • Save kristinriebe/33f5f0419ef210fad2c47ab94c63ac5a to your computer and use it in GitHub Desktop.
Save kristinriebe/33f5f0419ef210fad2c47ab94c63ac5a to your computer and use it in GitHub Desktop.
Wagtail - use a custom url (document slug) to serve documents via a custom serve_view
# I prefer to serve documents using a document slug (derived from the title of the document)
# instead of using the filename, especially since filenames can become quite ugly (appended random strings).
# E.g. a file named 'campus_map.pdf' with titel 'Campus Map' may be uploaded as 'campus_map_asRfsgt.pdf'
# by Wagtail and is then served as e.g. '/documents/24/campus_map_asRfsgt.pdf' (if WAGTAILDOCS_SERVE_METHOD = 'serve_view').
# However, it would look much nicer, if it was served as '/documents/24/campus_map/', which could
# also be a more persistent document url, i.e. it would not change if the document object was changed by
# replacing the file with a new version (e.g. 'campus_map_v2.pdf').
# I could also shorten the URL to '/documents/campus_map/', omitting the id. However, lookups by id are
# much faster and we would need to ensure that these urls are unique.
# In this example, I am using 'mydocuments' as the app name, adjust it to the app where you store your new
# views.py and urls.py for the documents.
# In your settings file (could also be named differently), define the custom document
# model and the serve method
# ...
WAGTAILDOCS_DOCUMENT_MODEL = 'mydocuments.CustomDocument'
WAGTAILDOCS_SERVE_METHOD = 'serve_view'
# ...
# In your global urls.py include the wagtail_serve urls
from django.conf.urls import url
from django.urls import include
# from wagtail.documents import urls as wagtaildocs_urls
from mydocuments import urls as docs_urls
# ...
urlpatterns = [
# ...
# url(r'^documents/', include(wagtaildocs_urls)),
url(r'^documents/', include(docs_urls)),
# ...
]
# ...
# We need a custom document model so that we can redefine its url method
class CustomDocument(AbstractDocument):
# put your custom properties here;
# the slug could also be added as a property, put into admin fields and indexed,
# so it does not have to be calculated again each time.
# ...
# redefine the url method of AbstractDocument, so it uses a slugified title
# instead of the file name; if you change this, make sure to change the serve-view
# and the urls.py above as well
@property
def url(self):
if getattr(settings, 'WAGTAILDOCS_SERVE_METHOD', None) == 'direct':
try:
return self.file.url
except NotImplementedError:
# backend does not provide a url, so fall back on the serve view
pass
slug = slugify(self.title)
return reverse('wagtaildocs_serve', args=[self.id, slug])
# Change the wagtaildocs_serve urls to suit our needs
from django.urls import path
from mydocuments import views
urlpatterns = [
path('<int:document_id>/<slug:document_slug>/', views.serve, name='wagtaildocs_serve'),
# keep the entry below as in the original version in wagtail/documents/urls.py
path(
'authenticate_with_password/<int:restriction_id>/',
serve.authenticate_with_password,
name='wagtaildocs_authenticate_with_password'),
]
# The following lines are copied from wagtail/documents/views/serve.py, v. 2.11.2
# Use in document_etag and in the serve-view the new document_slug instead of the filename.
from wsgiref.util import FileWrapper
from django.conf import settings
from django.http import Http404, HttpResponse, StreamingHttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.utils.text import slugify
from django.views.decorators.cache import cache_control
from django.views.decorators.http import etag
from wagtail.core import hooks
from wagtail.documents import get_document_model
from wagtail.documents.models import document_served
from wagtail.utils import sendfile_streaming_backend
from wagtail.utils.sendfile import sendfile
def document_etag(request, document_id, document_slug):
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)
@cache_control(max_age=3600, public=True)
def serve(request, document_id, document_slug):
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_slug> pair.
if slugify(doc.title) != document_slug:
raise Http404('This document does not match the given file slug.')
for fn in hooks.get_hooks('before_serve_document'):
# ...
# continue with the remaining code in this function; use the up-to-date code from your Wagtail version
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment