Unverified Commit fbb35b96 authored by Joost Rijneveld's avatar Joost Rijneveld
Browse files

Merge branch 'master' into 28-extra-controls-for-photos

Conflicts:
	website/photos/models.py
	website/photos/templates/photos/album.html
parents e4cb4e28 27794969
......@@ -3,3 +3,4 @@ django-localflavor==1.3
Pillow
django-static-precompiler>=1.4,<2
django-sendfile==0.3.10
django-template-check # This should be in dev-requirements somehow
......@@ -6,6 +6,7 @@ skipsdist = True
changedir={toxinidir}/website
commands =
python manage.py check
python manage.py templatecheck
python manage.py makemigrations --no-input --check
python -Wall manage.py test
deps = -r{toxinidir}/requirements.txt
......
{% extends "base.html" %}
{% load i18n %}
{% load i18n thumbnail %}
{% block title %}{% trans 'Committees' %} — {{ block.super }}{% endblock %}
{% block page_title %}{% trans 'Committees' %}{% endblock %}
......@@ -15,7 +15,7 @@
<div class="post-inner">
<div class="inner-img">
<img src="{{ thumbnail committee.photo '1024x600' fit=False }}" alt="{{ committee.name }}">
<img src="{% thumbnail committee.photo '1024x600' fit=False %}" alt="{{ committee.name }}">
</div>
<div class="post-overlay">
......
......@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-07-27 19:15+0200\n"
"PO-Revision-Date: 2016-07-27 19:17+0100\n"
"POT-Creation-Date: 2016-08-11 16:00+0200\n"
"PO-Revision-Date: 2016-08-11 16:02+0100\n"
"Last-Translator: Joost Rijneveld <joost@joostrijneveld.nl>\n"
"Language-Team: \n"
"Language: nl\n"
......@@ -96,8 +96,9 @@ msgstr ""
"is."
#: templates/documents/meetingyear.html:4
msgid "General Meetings of"
msgstr "ALV's van"
#, python-format
msgid "General Meetings of %(year)s-%(nextyear)s:"
msgstr "ALV's van %(year)s-%(nextyear)s:"
#: templates/documents/meetingyear.html:12
msgid "Meeting"
......@@ -113,5 +114,8 @@ msgstr "ALV-stukken zijn alleen in te zien<br>door ingelogde gebruikers."
#: templates/documents/meetingyear.html:53
#, python-format
msgid "There are no General Meetings available for %(year)s-%(year | add:1)s"
msgstr "Er zijn geen ALV's beschikbaar voor %(year)s-%(year | add:1)s"
msgid "There are no General Meetings available for %(year)s-%(nextyear)s"
msgstr "Er zijn geen ALV's beschikbaar van %(year)s-%(nextyear)s"
#~ msgid "General Meetings of"
#~ msgstr "ALV's van"
{% load i18n %}
{% load filename %}
<h2 style="margin-bottom: 0px;">{% trans "General Meetings of" %} {{ year }}-{{ year|add:1 }}:</span></h2>
<h2 style="margin-bottom: 0px;">{% blocktrans with year|add:1 as nextyear %}General Meetings of {{ year }}-{{ nextyear }}:{% endblocktrans %}</span></h2>
{% for meeting in meetings %}
<div class="gw-go gw-go-clearfix gw-go-{% if meetings|length < 3 %}3{% else %}{{ meetings | length }}{% endif %}cols">
......@@ -50,5 +50,5 @@
</div>
</div>
{% empty %}
<div style="text-align:center;margin-top:10px;">{% blocktrans %}There are no General Meetings available for {{ year }}-{{ year | add:1 }}{% endblocktrans %}</div>
<div style="text-align:center;margin-top:10px;">{% blocktrans with year|add:1 as nextyear %}There are no General Meetings available for {{ year }}-{{ nextyear }}{% endblocktrans %}</div>
{% endfor %}
\ No newline at end of file
{% extends "base.html" %}
{% load i18n %}
{% block title %}Hello World - {{ block.super }}{% endblock %}
......
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-08-06 14:33
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('photos', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='album',
name='shareable',
field=models.BooleanField(default=False),
),
]
......@@ -3,6 +3,7 @@ from django.conf import settings
from django.utils.functional import cached_property
from django.db import models
import hashlib
import os
import random
......@@ -35,6 +36,7 @@ class Album(models.Model):
hidden = models.BooleanField(default=False)
_cover = models.OneToOneField(Photo, on_delete=models.SET_NULL, blank=True,
null=True, related_name='covered_album')
shareable = models.BooleanField(default=False)
photosdir = 'photos'
photospath = os.path.join(settings.MEDIA_ROOT, photosdir)
......@@ -59,3 +61,8 @@ class Album(models.Model):
if self.pk is None:
self.dirname = self.slug
super(Album, self).save(*args, **kwargs)
@property
def access_token(self):
return hashlib.sha256('{}album{}'.format(settings.SECRET_KEY, self.pk)
.encode('utf-8')).hexdigest()
{% extends 'base.html' %}
{% load staticfiles i18n thumbnail %}
{% load staticfiles i18n thumbnail shared_thumbnail %}
{% block page_title %}{% trans "Photos" %}{% endblock %}
......@@ -11,14 +11,31 @@
{% block body %}
<h1>{{ album.title }}</h1>
<h2>{{ album.date|date:"d-m-Y" }}</h2>
{% if album.shareable %}
<p class="tcenter">
{% trans "Note: This album can be shared with people outside the association by sending them the following link:" %}<br>
<small><a href="{% url 'photos:shared_album' album.slug album.access_token %}">
{{ request.get_host }}{% url 'photos:shared_album' album.slug album.access_token %}
</a></small>
</p>
{% endif %}
<div class="gallery">
<ul class="gallery-photos row">
{% for photo in photos %}
<li class="post gallery-photo span3 has-overlay {% if forloop.counter0|divisibleby:4 %}first-child{% endif %}">
<a data-fancybox-rotation="{{ photo.rotation }}" class="gallery-box" rel="gallery" data-download="{% url 'photos:download' photo.file %}" href="{% thumbnail photo.file '1024x768' fit=False %}">
<a data-fancybox-rotation="{{ photo.rotation }}" class="gallery-box" rel="gallery"
{% if album.shareable %}
data-download="{% url 'photos:shared-download' album.slug album.access_token photo %}" href="{% shared_thumbnail album.slug album.access_token photo '1024x768' fit=False %}"
{% else %}
data-download="{% url 'photos:download' photo %}" href="{% thumbnail photo '1024x768' fit=False %}"
{% endif %}>
<span class="post-inner">
<span class="inner-img">
<img class="rotate{{ photo.rotation }}" src="{% thumbnail photo.file '220x220' %}" alt="" />
{% if album.shareable %}
<img class="rotate{{ photo.rotation }}" src="{% shared_thumbnail album.slug album.access_token photo '220x220' %}" alt="" />
{% else %}
<img class="rotate{{ photo.rotation }}" src="{% thumbnail photo '220x220' %}" alt="" />
{% endif %}
</span>
<span class="post-overlay">
</span>
......
from django import template
from django.core.urlresolvers import resolve, reverse
from utils.templatetags.thumbnail import thumbnail
register = template.Library()
@register.simple_tag
def shared_thumbnail(slug, token, path, size, fit=True):
thumb = resolve(thumbnail(path, size, fit))
args = [slug, token, thumb.kwargs['size_fit'], thumb.kwargs['path']]
return reverse('photos:shared-thumbnail', args=args)
......@@ -5,6 +5,9 @@ from . import views
urlpatterns = [
url(r'^download/(?P<path>.*)', views.download, name='download'),
url(r'^shared-download/(?P<slug>[-\w]+)/(?P<token>[a-zA-Z0-9]+)/(?P<path>.*)', views.shared_download, name='shared-download'),
url(r'^shared-thumbnail/(?P<slug>[-\w]+)/(?P<token>[a-zA-Z0-9]+)/(?P<size_fit>\d+x\d+_[01])/(?P<path>.*)', views.shared_thumbnail, name='shared-thumbnail'),
url(r'^(?P<slug>[-\w]+)/$', views.album, name='album'),
url(r'^(?P<slug>[-\w]+)/(?P<token>[a-zA-Z0-9]+)$', views.shared_album, name='shared_album'),
url(r'^$', views.index, name='index'),
]
......@@ -6,6 +6,7 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from .models import Album
from utils.snippets import sanitize_path
from utils.views import _private_thumbnails_unauthed
from sendfile import sendfile
import os
......@@ -37,10 +38,37 @@ def album(request, slug):
return render(request, 'photos/album.html', context)
@login_required
def download(request, path):
def _checked_shared_album(slug, token):
album = get_object_or_404(Album, slug=slug)
if token != album.access_token:
raise Http404("Invalid token.")
return album
def shared_album(request, slug, token):
album = _checked_shared_album(slug, token)
return render(request, 'photos/album.html', {'album': album})
def _download(request, path):
"""This function provides a layer of indirection for shared albums"""
path = sanitize_path(path)
path = os.path.join(settings.MEDIA_ROOT, 'photos', *path.split('/')[1:])
if not os.path.isfile(path):
raise Http404("Photo not found.")
return sendfile(request, path, attachment=True)
@login_required
def download(request, path):
return _download(request, path)
def shared_download(request, slug, token, path):
_checked_shared_album(slug, token)
return _download(request, path)
def shared_thumbnail(request, slug, token, size_fit, path):
_checked_shared_album(slug, token)
return _private_thumbnails_unauthed(request, size_fit, path)
......@@ -13,7 +13,8 @@ main = [
{'title': _('Become a Member'), 'name': 'become-a-member'},
{'title': _('Thabloid'), 'name': '#'},
]},
{'title': _('For Members'), 'name': '#', 'submenu': [
{'title': _('For Members'), 'name': '#', 'authenticated': True,
'submenu': [
{'title': _('Photos'), 'name': 'photos:index'},
{'title': _('Statistics'), 'name': '#'},
{'title': _('Become Active'), 'name': 'become-active'},
......
......@@ -50,6 +50,7 @@ INSTALLED_APPS = [
'utils',
'mailinglists',
'merchandise',
'django_template_check', # This is only necessary in development
]
MIDDLEWARE = [
......
......@@ -6,6 +6,7 @@
<!-- menu -->
<ul class="nav">
{% for item in menu %}
{% if not item.authenticated or request.user.is_authenticated %}
{% url item.name as path %}
<li {% if request.path == path %}class="nav-path-selected"{% endif %}>
<a href="{{ path }}" target="_self" {% if request.path == path %}class="nav-path-selected"{% endif %}>
......@@ -15,6 +16,7 @@
{% include 'menu/submenu.html' with submenu=item.submenu %}
{% endif %}
</li>
{% endif %}
{% endfor %}
</ul>
<!-- /menu -->
......
......@@ -47,6 +47,18 @@ class TestTranslateMeta(TestCase):
self.assertIn('Text', fr)
self.assertEqual(len({nl, en, fr}), 3)
def test_no_verbose_name(self):
class TestItem3b(models.Model, metaclass=ModelTranslateMeta):
text = MultilingualField(models.TextField)
nl = TestItem3b._meta.get_field('text_nl').verbose_name
en = TestItem3b._meta.get_field('text_en').verbose_name
fr = TestItem3b._meta.get_field('text_fr').verbose_name
self.assertEqual('text (NL)', nl)
self.assertEqual('text (EN)', en)
self.assertEqual('text (FR)', fr)
self.assertEqual(len({nl, en, fr}), 3)
def test_other_kwargs(self):
class TestItem4(models.Model, metaclass=ModelTranslateMeta):
text = MultilingualField(models.CharField, 'Text', max_length=100)
......
from django.contrib import admin
from django.db import models
from django.db.models.fields.related import RelatedField
from django.conf import settings
......@@ -20,6 +21,15 @@ See the following usage example;
name = MultilingualField(models.CharField, max_length=100)
description = MultilingualField(models.TextField)
In order to use the fields in ModelAdmin configuration (such as in the
fields, fieldsets or prepopulated_fields attributes), subclass the Admin object
from TranslatedModelAdmin instead;
from utils.translation import TranslatedModelAdmin
class SomeItemAdmin(TranslatedModelAdmin):
fields = (name, description)
"""
......@@ -51,28 +61,75 @@ def _i18n_attr_accessor(attr):
class ModelTranslateMeta(models.base.ModelBase):
def __new__(cls, name, bases, dct):
field_i18n = {'default': {}, 'fields': {}}
for attr, field in list(dct.items()):
if isinstance(field, MultilingualField):
# ForeignKey, OneToOneField and ManyToManyField do not have
# a verbose name as first positional argument.
# But those are not translatable (see above).
if len(field.args) > 0:
verbose_base = ('args', field.args[0])
if not isinstance(field, MultilingualField):
continue
# ForeignKey, OneToOneField and ManyToManyField do not have
# a verbose name as first positional argument.
# But those are not translatable (see above).
if len(field.args) > 0:
verbose_base = ('args', field.args[0])
else:
verbose_base = ('kwargs', field.kwargs.get('verbose_name',
attr))
fields = []
for lang in settings.LANGUAGES:
attr_i18n = I18N_FIELD_FORMAT.format(attr, lang[0])
verbose_name = '{} ({})'.format(verbose_base[1],
lang[0].upper())
if verbose_base[0] == 'args':
field.args = (verbose_name,) + field.args[1:]
else:
verbose_base = ('kwargs', field.kwargs.get('verbose_name',
None))
for lang in settings.LANGUAGES:
attr_i18n = I18N_FIELD_FORMAT.format(attr, lang[0])
if verbose_base is not None:
verbose_name = '{} ({})'.format(verbose_base[1],
lang[0].upper())
if verbose_base[0] == 'args':
field.args = (verbose_name,) + field.args[1:]
else:
field.kwargs['verbose_name'] = verbose_name
if attr_i18n in dct:
raise FieldError("Explicit field {} is shadowed "
"by TranslateMeta.".format(attr_i18n))
dct[attr_i18n] = field.cls(*field.args, **field.kwargs)
dct[attr] = property(_i18n_attr_accessor(attr))
return super(ModelTranslateMeta, cls).__new__(cls, name, bases, dct)
field.kwargs['verbose_name'] = verbose_name
if attr_i18n in dct:
raise FieldError("Explicit field {} is shadowed "
"by TranslateMeta.".format(attr_i18n))
dct[attr_i18n] = field.cls(*field.args, **field.kwargs)
fields.append(attr_i18n)
dct[attr] = property(_i18n_attr_accessor(attr))
default = I18N_FIELD_FORMAT.format(attr, settings.LANGUAGE_CODE)
if default not in dct:
raise ImproperlyConfigured("LANGUAGE_CODE not in LANGUAGES.")
field_i18n['default'][attr] = default
field_i18n['fields'][attr] = fields
model = super(ModelTranslateMeta, cls).__new__(cls, name, bases, dct)
if hasattr(model._meta, '_field_i18n'):
raise FieldError("TranslateMeta map already exists!")
model._meta._field_i18n = field_i18n
return model
class TranslatedModelAdmin(admin.ModelAdmin):
"""This class should be used when the ModelAdmin is used with a
translated model and one refers to such a field in the `fields`
or `fieldsets` attributes, or in `prepopulated_fields`.
This works because admin.ModelAdmin has an empty metaclass; we can hook
in to __init__ and modify the attributes when model is known."""
def __init__(self, model, admin_site):
for key, fields in list(type(self).prepopulated_fields.items()):
# Replace translated fields in `fields`
fields = tuple(model._meta._field_i18n['default'].get(field, field)
for field in fields)
# ..and in `key`
del type(self).prepopulated_fields[key]
key = model._meta._field_i18n['default'].get(key, key)
type(self).prepopulated_fields[key] = fields
def trans_fields(fields):
if fields is None:
return None
fields = [model._meta._field_i18n['fields']
.get(field, (field, )) for field in fields]
return tuple(field for fieldset in fields for field in fieldset)
# In fields, we replace a translated field by all resulting fields.
type(self).fields = trans_fields(type(self).fields)
type(self).exclude = trans_fields(type(self).exclude)
if type(self).fieldsets is not None:
for fieldset in type(self).fieldsets:
fieldset[1]['fields'] = trans_fields(fieldset[1]['fields'])
super(TranslatedModelAdmin, self).__init__(model, admin_site)
......@@ -8,10 +8,17 @@ import os
from .snippets import sanitize_path
@login_required
def private_thumbnails(request, size_fit, path):
def _private_thumbnails_unauthed(request, size_fit, path):
"""This layer of indirection makes it possible to make exceptions
to the authentication requirements for thumbnails, e.g. when sharing
photo albums with external parties using access tokens."""
path = sanitize_path(path)
path = os.path.join(settings.MEDIA_ROOT, 'thumbnails', size_fit, path)
if not os.path.isfile(path):
raise Http404("Thumbnail not found.")
return sendfile(request, path)
@login_required
def private_thumbnails(request, size_fit, path):
return _private_thumbnails_unauthed(request, size_fit, path)
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment