Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

URLs that stay the same when a file is replaced. Fixes #478 #591

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/installation.rst
Expand Up @@ -110,6 +110,34 @@ secure downloads

See :ref:`secure_downloads` section.

Canonical URLs
..............

You can configure your project to generate canonical URLs for your public files. Just include django-filer's
URLConf in your project's ``urls.py``::

urlpatterns = [
...
url(r'^filer/', include('filer.urls')),
...
]

Contrary to the file's actual URL, the canonical URL does not change if you upload a new version of the file.
Thus, you can safely share the canonical URL. As long as the file exists, people will be redirected to its
latest version.

The canonical URL is displayed in the "advanced" panel on the file's admin page. It has the form::

/filer/canonical/1442488644/12/

The "filer" part of the URL is configured in the project's URLconf as described above. The "canonical" part can be
changed with the setting ``FILER_CANONICAL_URL``, which defaults to ``'canonical/'``. Example::

# settings.py

FILER_CANONICAL_URL = 'sharing/'


debugging and logging
.....................

Expand Down
11 changes: 10 additions & 1 deletion docs/settings.rst
Expand Up @@ -143,4 +143,13 @@ Defaults to ``False``
Defines the dotted path to a custom Image model; please include the model name.
Example: 'my.app.models.CustomImage'

Defaults to ``False``
Defaults to ``False``


``FILER_CANONICAL_URL``
-----------------------

Defines the path element common to all canonical file URLs.

Defaults to ``'canonical/'``

13 changes: 11 additions & 2 deletions filer/admin/fileadmin.py
Expand Up @@ -23,7 +23,7 @@ class FileAdmin(PrimitivePermissionAwareModelAdmin):
list_per_page = 10
search_fields = ['name', 'original_filename', 'sha1', 'description']
raw_id_fields = ('owner',)
readonly_fields = ('sha1',)
readonly_fields = ('sha1', 'display_canonical')

# save_as hack, because without save_as it is impossible to hide the
# save_and_add_another if save_as is False. To show only save_and_continue
Expand All @@ -45,7 +45,7 @@ def build_fieldsets(cls, extra_main_fields=(), extra_advanced_fields=(), extra_f
'fields': ('name', 'owner', 'description',) + extra_main_fields,
}),
(_('Advanced'), {
'fields': ('file', 'sha1',) + extra_advanced_fields,
'fields': ('file', 'sha1', 'display_canonical') + extra_advanced_fields,
'classes': ('collapse',),
}),
) + extra_fieldsets
Expand Down Expand Up @@ -141,4 +141,13 @@ def get_model_perms(self, request):
'delete': False,
}

def display_canonical(self, instance):
canonical = instance.canonical_url
if canonical:
return '<a href="%s">%s</a>' % (canonical, canonical)
else:
return '-'
display_canonical.allow_tags = True
display_canonical.short_description = _('canonical URL')

FileAdmin.fieldsets = FileAdmin.build_fieldsets()
13 changes: 13 additions & 0 deletions filer/models/filemodels.py
Expand Up @@ -240,6 +240,19 @@ def url(self):
r = ''
return r

@property
def canonical_url(self):
url = ''
if self.file and self.is_public:
try:
url = urlresolvers.reverse('canonical', kwargs={
'uploaded_at': self.uploaded_at.strftime('%s'),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use the File.uploaded_at field, which is non-editable, to make the public file's canonical URL a little bit harder to guess.

'file_id': self.id
})
except urlresolvers.NoReverseMatch:
pass # No canonical url, return empty string
return url

@property
def path(self):
try:
Expand Down
3 changes: 3 additions & 0 deletions filer/settings.py
Expand Up @@ -219,3 +219,6 @@ def update_server_settings(settings, defaults, s, t):
FILER_PRIVATEMEDIA_THUMBNAIL_SERVER = load_object(FILER_SERVERS['private']['thumbnails']['ENGINE'])(**FILER_SERVERS['private']['thumbnails']['OPTIONS'])

FILER_DUMP_PAYLOAD = getattr(settings, 'FILER_DUMP_PAYLOAD', False) # Whether the filer shall dump the files payload

FILER_CANONICAL_URL = getattr(settings, 'FILER_CANONICAL_URL', 'canonical/')

1 change: 1 addition & 0 deletions filer/test_utils/templates/404.html
@@ -0,0 +1 @@
404
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is required to make the unit test Django 1.4 compatible.

1 change: 1 addition & 0 deletions filer/test_utils/urls.py
Expand Up @@ -10,4 +10,5 @@
{'document_root': settings.MEDIA_ROOT, 'show_indexes': True}),
url(r'^admin/', include(admin.site.urls)),
url(r'^', include('filer.server.urls')),
url(r'^filer/', include('filer.urls')),
)
44 changes: 44 additions & 0 deletions filer/tests/models.py
Expand Up @@ -223,3 +223,47 @@ def test_custom_model(self):

reloaded = Image.objects.get(pk=image.pk)
self.assertEqual(reloaded.author, image.author)

def test_canonical_url(self):
"""
Check that a public file's canonical url redirects to the file's current version
"""
image = self.create_filer_image()
image.save()
# Private file
image.is_public = False
image.save()
canonical = image.canonical_url
self.assertEqual(self.client.get(canonical).status_code, 404)
# First public version
image.is_public = True
image.save()
canonical = image.canonical_url
file_url_1 = image.file.url
self.assertRedirects(self.client.get(canonical), file_url_1)
# Second public version
img_2 = create_image()
image_name_2 = 'test_file_2.jpg'
filename_2 = os.path.join(settings.FILE_UPLOAD_TEMP_DIR, image_name_2)
img_2.save(filename_2, 'JPEG')
file_2 = DjangoFile(open(filename_2, 'rb'), name=image_name_2)
image.file = file_2
image.save()
file_url_2 = image.file.url
self.assertNotEqual(file_url_1, file_url_2)
self.assertRedirects(self.client.get(canonical), file_url_2)
# No file
image.file = None
image.save()
self.assertEqual(self.client.get(canonical).status_code, 404)
# Teardown
image.file = file_2
image.save()
os.remove(filename_2)

def test_canonical_url_settings(self):
image = self.create_filer_image()
image.save()
canonical = image.canonical_url
self.assertTrue(canonical.startswith('/filer/test-path/'))

13 changes: 13 additions & 0 deletions filer/urls.py
@@ -0,0 +1,13 @@
#-*- coding: utf-8 -*-
from django.conf.urls import url

from filer import settings as filer_settings
from filer import views

urlpatterns = [
url(
filer_settings.FILER_CANONICAL_URL + r'(?P<uploaded_at>[0-9]+)/(?P<file_id>[0-9]+)/$',
views.canonical,
name='canonical'
),
]
16 changes: 13 additions & 3 deletions filer/views.py
Expand Up @@ -5,11 +5,11 @@
from django.contrib.admin import widgets
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.http import HttpResponseRedirect, Http404
from django.shortcuts import render, redirect, get_object_or_404
from django.utils.translation import ugettext_lazy as _

from .models import Folder, Image, Clipboard, tools, FolderRoot
from .models import Folder, File, Image, Clipboard, tools, FolderRoot
from . import settings as filer_settings


Expand Down Expand Up @@ -57,6 +57,16 @@ def _userperms(item, request):
return r


def canonical(request, uploaded_at, file_id):
"""
Redirect to the current url of a public file
"""
filer_file = get_object_or_404(File, pk=file_id, is_public=True)
if uploaded_at != filer_file.uploaded_at.strftime('%s') or not filer_file.file:
raise Http404('No %s matches the given query.' % File._meta.object_name)
return redirect(filer_file.url)


@login_required
def edit_folder(request, folder_id):
# TODO: implement edit_folder view
Expand Down
6 changes: 5 additions & 1 deletion test_settings.py
Expand Up @@ -4,6 +4,8 @@

gettext = lambda s: s

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

HELPER_SETTINGS = {
'NOSE_ARGS': [
'-s',
Expand Down Expand Up @@ -50,7 +52,9 @@
'easy_thumbnails.processors.filters',
),
'FILE_UPLOAD_TEMP_DIR': mkdtemp(),
'FILER_IMAGE_MODEL': False
'FILER_IMAGE_MODEL': False,
'TEMPLATE_DIRS': (os.path.join(BASE_DIR, 'django-filer', 'filer', 'test_utils', 'templates'),),
'FILER_CANONICAL_URL': 'test-path/',

}
if os.environ.get('CUSTOM_IMAGE', False):
Expand Down