cover image repo
authorRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Fri, 15 Jun 2012 15:05:19 +0000 (17:05 +0200)
committerRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Mon, 18 Jun 2012 10:51:13 +0000 (12:51 +0200)
15 files changed:
apps/catalogue/constants.py
apps/catalogue/templatetags/catalogue.py
apps/catalogue/templatetags/common_tags.py [new file with mode: 0755]
apps/cover/forms.py [new file with mode: 0755]
apps/cover/migrations/0001_initial.py [new file with mode: 0644]
apps/cover/migrations/__init__.py [new file with mode: 0644]
apps/cover/models.py
apps/cover/templates/cover/add_image.html [new file with mode: 0755]
apps/cover/templates/cover/image_detail.html [new file with mode: 0755]
apps/cover/templates/cover/image_list.html [new file with mode: 0755]
apps/cover/tests.py [new file with mode: 0644]
apps/cover/urls.py
apps/cover/views.py
redakcja/settings/test.py
requirements.txt

index d75d6b4..0c84232 100644 (file)
@@ -1,5 +1,8 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
-
+#
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
 TRIM_BEGIN = " TRIM_BEGIN "
 TRIM_END = " TRIM_END "
 
 TRIM_BEGIN = " TRIM_BEGIN "
 TRIM_END = " TRIM_END "
 
index 3cc7210..8d5ff65 100644 (file)
@@ -34,6 +34,8 @@ def main_tabs(context):
         tabs.append(Tab('create', _('Add'), reverse("catalogue_create_missing")))
         tabs.append(Tab('upload', _('Upload'), reverse("catalogue_upload")))
 
         tabs.append(Tab('create', _('Add'), reverse("catalogue_create_missing")))
         tabs.append(Tab('upload', _('Upload'), reverse("catalogue_upload")))
 
+    tabs.append(Tab('cover', _('Covers'), reverse("cover_image_list")))
+
     return {"tabs": tabs, "active_tab": active}
 
 
     return {"tabs": tabs, "active_tab": active}
 
 
diff --git a/apps/catalogue/templatetags/common_tags.py b/apps/catalogue/templatetags/common_tags.py
new file mode 100755 (executable)
index 0000000..ccaf03b
--- /dev/null
@@ -0,0 +1,7 @@
+from django import template
+register = template.Library()
+
+
+@register.filter
+def build_absolute_uri(uri, request):
+    return request.build_absolute_uri(uri)
diff --git a/apps/cover/forms.py b/apps/cover/forms.py
new file mode 100755 (executable)
index 0000000..754d697
--- /dev/null
@@ -0,0 +1,80 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+import re
+from urllib2 import urlopen
+from django import forms
+from cover.models import Image
+
+class ImageAddForm(forms.ModelForm):
+    class Meta:
+        model = Image
+
+class ImageEditForm(forms.ModelForm):
+    """Form used for editing a Book."""
+    class Meta:
+        model = Image
+        exclude = ['download_url']
+
+
+class ReadonlyImageEditForm(ImageEditForm):
+    """Form used for not editing a Book."""
+
+    def __init__(self, *args, **kwargs):
+        ret = super(ReadonlyImageEditForm, self).__init__(*args, **kwargs)
+        for field in self.fields.values():
+            field.widget.attrs.update({"readonly": True})
+        return ret
+
+    def save(self, *args, **kwargs):
+        raise AssertionError, "ReadonlyImageEditForm should not be saved."
+
+
+class FlickrForm(forms.Form):
+    source_url = forms.URLField()
+
+    def clean_source_url(self):
+        url = self.cleaned_data['source_url']
+        m = re.match(r'(https?://)?(www.)?flickr.com/photos/(?P<author>[^/]+)/(?P<img>\d+)', url)
+        if not m:
+            raise forms.ValidationError("It doesn't look like Flickr URL.")
+        author_slug, img_id = m.group('author'), m.group('img')
+        base_url = "https://www.flickr.com/photos/%s/%s/" % (author_slug, img_id)
+
+        try:
+            html = urlopen(url).read().decode('utf-8')
+        except:
+            raise forms.ValidationError('Error reading page.')
+        match = re.search(r'<a href="([^"]*)" rel="license cc:license">Some rights reserved</a>', html)
+        try:
+            assert match
+            license_url = match.group(1)
+            self.cleaned_data['license_url'] = license_url
+            re_license = re.compile(r'http://creativecommons.org/licenses/([^/]*)/([^/]*)/.*')
+            m = re_license.match(license_url)
+            assert m
+            self.cleaned_data['license_name'] = 'CC %s %s' % (m.group(1).upper(), m.group(2))
+        except AssertionError:
+            raise forms.ValidationError('Error reading license name.')
+
+        m = re.search(r'<strong class="username">By <a href="[^"]*">([^<]*)</a></strong>', html)
+        if m:
+            self.cleaned_data['author'] = "%s@Flickr" % m.group(1)
+        else:
+            raise forms.ValidationError('Error reading author name.')
+
+        m = re.search(r'<h1[^>]*>(.*?)</h1>', html)
+        if not m:
+            raise forms.ValidationError('Error reading image title.')
+        self.cleaned_data['title'] = m.group(1)
+
+        url_size = base_url + "sizes/o/"
+        html = urlopen(url_size).read().decode('utf-8')
+        m = re.search(r'<div id="allsizes-photo">\s*<img src="([^"]*)"', html)
+        if m:
+            self.cleaned_data['download_url'] = m.group(1)
+        else:
+            raise forms.ValidationError('Error reading image URL.')
+        return base_url
diff --git a/apps/cover/migrations/0001_initial.py b/apps/cover/migrations/0001_initial.py
new file mode 100644 (file)
index 0000000..f31c405
--- /dev/null
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding model 'Image'
+        db.create_table('cover_image', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('title', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('author', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('license_name', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('license_url', self.gf('django.db.models.fields.URLField')(max_length=255, blank=True)),
+            ('source_url', self.gf('django.db.models.fields.URLField')(max_length=200)),
+            ('download_url', self.gf('django.db.models.fields.URLField')(unique=True, max_length=200)),
+            ('file', self.gf('django.db.models.fields.files.ImageField')(max_length=100)),
+        ))
+        db.send_create_signal('cover', ['Image'])
+
+
+    def backwards(self, orm):
+        # Deleting model 'Image'
+        db.delete_table('cover_image')
+
+
+    models = {
+        'cover.image': {
+            'Meta': {'object_name': 'Image'},
+            'author': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'download_url': ('django.db.models.fields.URLField', [], {'unique': 'True', 'max_length': '200'}),
+            'file': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'license_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'license_url': ('django.db.models.fields.URLField', [], {'max_length': '255', 'blank': 'True'}),
+            'source_url': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        }
+    }
+
+    complete_apps = ['cover']
\ No newline at end of file
diff --git a/apps/cover/migrations/__init__.py b/apps/cover/migrations/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
index e69de29..0aeddb4 100644 (file)
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+import re
+from urllib2 import urlopen
+from urlparse import urljoin
+from django.conf import settings
+from django.core.files.base import ContentFile
+from django.db import models
+from django.db.models.signals import post_save
+from django.dispatch import receiver
+from django.utils.translation import ugettext_lazy as _
+
+
+class Image(models.Model):
+    title = models.CharField(max_length=255)
+    author = models.CharField(max_length=255)
+    license_name = models.CharField(max_length=255)
+    license_url = models.URLField(max_length=255, blank=True)
+    source_url = models.URLField()
+    download_url = models.URLField(unique=True)
+    file = models.ImageField(upload_to='cover/image', editable=False)
+
+    class Meta:
+        verbose_name = _('cover image')
+        verbose_name_plural = _('cover images')
+
+    def __unicode__(self):
+        return u"%s - %s" % (self.author, self.title)
+
+    @models.permalink
+    def get_absolute_url(self):
+        return ('cover_image', [self.id])
+
+
+@receiver(post_save, sender=Image)
+def download_image(sender, instance, **kwargs):
+    if instance.pk and not instance.file:
+        t = urlopen(instance.download_url).read()
+        instance.file.save("%d.jpg" % instance.pk, ContentFile(t))
+        
+        
\ No newline at end of file
diff --git a/apps/cover/templates/cover/add_image.html b/apps/cover/templates/cover/add_image.html
new file mode 100755 (executable)
index 0000000..8506e61
--- /dev/null
@@ -0,0 +1,21 @@
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+
+
+{% block content %}
+<h1>{% trans "Add image" %}</h1>
+
+<form method="post">
+{% csrf_token %}
+<input type="hidden" name='form_id' value="flickr" />
+{{ ff.as_p }}
+<input type="submit" value="{% trans "Load from Flickr" %}" />
+</form>
+
+
+<form method="post">
+{% csrf_token %}
+{{ form.as_p }}
+<input type="submit" value="{% trans "Add image" %}" />
+</form>
+{% endblock %}
diff --git a/apps/cover/templates/cover/image_detail.html b/apps/cover/templates/cover/image_detail.html
new file mode 100755 (executable)
index 0000000..8ebf988
--- /dev/null
@@ -0,0 +1,29 @@
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+{% load build_absolute_uri from common_tags %}
+
+{% block content %}
+<h1>{% trans "Cover image" %}</h1>
+
+<div style="float: right">
+<a href="{{ object.file.url }}"><img style="width:400px" src="{{ object.file.url }}" /></a>
+<br/><a href="{{ object.source_url }}">{{ object.title }}</a> by {{ object.author }},
+    {% if object.license_url %}<a href="{{ object.license_url }}">{% endif %}
+        {{ object.license_name }}</a>
+    {% if object.license_url %}</a>{% endif %}
+
+<br/>{% trans "source" %}: {{ object.download_url }}
+</div>
+
+<form method="post">
+{% csrf_token %}
+{{ form.as_p }}
+<input type="submit" value="{% trans "Change" %}" />
+</form>
+
+<textarea style="width:100%" rows="5">
+&lt;dc:relation.coverImage.url xmlns:dc="http://purl.org/dc/elements/1.1/">{{ object.file.url|build_absolute_uri:request }}&lt;/dc:relation.coverImage.url>
+&lt;dc:relation.coverImage.attribution xmlns:dc="http://purl.org/dc/elements/1.1/">{{ object.author }}, {{ object.license_name }}&lt;/dc:relation.coverImage.attribution>
+&lt;dc:relation.coverImage.source xmlns:dc="http://purl.org/dc/elements/1.1/">{{ object.source_url }}&lt;/dc:relation.coverImage.source>
+</textarea>
+{% endblock %}
diff --git a/apps/cover/templates/cover/image_list.html b/apps/cover/templates/cover/image_list.html
new file mode 100755 (executable)
index 0000000..5b2b000
--- /dev/null
@@ -0,0 +1,30 @@
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+{% load url from future %}
+{% load thumbnail pagination_tags %}
+
+
+{% block content %}
+<h1>{% trans "Cover images" %}</h1>
+
+{% if can_add %}
+    <a href="{% url 'cover_add_image' %}">{% trans "Add new" %}</a>
+{% endif %}
+
+<ul>
+{% autopaginate object_list 100 %}
+{% for image in object_list %}
+    <a href="{{ image.get_absolute_url }}">
+    
+    <img style="height: 200px"
+        src="{% thumbnail image.file "x200" as thumb %}
+                {{ thumb.url }}
+             {% empty %}
+                {{ image.file.url }}
+             {% endthumbnail %}" />
+        {{ image }}</a>
+{% endfor %}
+{% paginate %}
+</ul>
+
+{% endblock %}
diff --git a/apps/cover/tests.py b/apps/cover/tests.py
new file mode 100644 (file)
index 0000000..d6a1ad7
--- /dev/null
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from nose.tools import *
+from django.test import TestCase
+from cover.forms import FlickrForm
+
+
+class FlickrTests(TestCase):
+    def test_flickr(self):
+        form = FlickrForm({"source_url": "https://www.flickr.com/photos/rczajka/6941928577/in/photostream"})
+        self.assertTrue(form.is_valid())
+        self.assertEqual(form.cleaned_data['source_url'], "https://www.flickr.com/photos/rczajka/6941928577/")
+        self.assertEqual(form.cleaned_data['author'], "rczajka@Flickr")
+        self.assertEqual(form.cleaned_data['title'], u"Pirate Stańczyk")
+        self.assertEqual(form.cleaned_data['license_name'], "CC BY 2.0")
+        self.assertEqual(form.cleaned_data['license_url'], "http://creativecommons.org/licenses/by/2.0/deed.en")
+        self.assertEqual(form.cleaned_data['download_url'], "http://farm8.staticflickr.com/7069/6941928577_2a0306f6a6_b.jpg")
index 5e6ffc5..2337d47 100644 (file)
@@ -1,4 +1,8 @@
-# -*- coding: utf-8
+# -*- coding: utf-8 -*-
+#
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
 from django.conf.urls.defaults import patterns, url
 
 
 from django.conf.urls.defaults import patterns, url
 
 
@@ -10,5 +14,7 @@ urlpatterns = patterns('cover.views',
     url(r'^preview/(?P<book>[^/]+)/(?P<chunk>[^/]+)/(?P<rev>\d+)/$',
             'preview', name='cover_preview'),
 
     url(r'^preview/(?P<book>[^/]+)/(?P<chunk>[^/]+)/(?P<rev>\d+)/$',
             'preview', name='cover_preview'),
 
-    url(r'^flickr/$', 'flickr'),
+    url(r'^image/$', 'image_list', name='cover_image_list'),
+    url(r'^image/(?P<pk>\d+)/?$', 'image', name='cover_image'),
+    url(r'^add_image/$', 'add_image', name='cover_add_image'),
 )
 )
index 0f341ad..0c29209 100644 (file)
@@ -1,11 +1,19 @@
-# Create your views here.
+# -*- coding: utf-8 -*-
+#
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
 import os.path
 from django.conf import settings
 import os.path
 from django.conf import settings
+from django.contrib.auth.decorators import permission_required
 from django.http import HttpResponse, HttpResponseRedirect, Http404
 from django.http import HttpResponse, HttpResponseRedirect, Http404
-from catalogue.models import Chunk
-from django.views.decorators.http import require_POST
+from django.shortcuts import get_object_or_404, render
 from django.views.decorators.csrf import csrf_exempt
 from django.views.decorators.csrf import csrf_exempt
-from django.shortcuts import render
+from django.views.decorators.http import require_POST
+from catalogue.helpers import active_tab
+from catalogue.models import Chunk
+from cover.models import Image
+from cover import forms
 
 
 PREVIEW_SIZE = (216, 300)
 
 
 PREVIEW_SIZE = (216, 300)
@@ -73,36 +81,59 @@ def preview_from_xml(request):
     return HttpResponse(os.path.join(settings.MEDIA_URL, fname))
 
 
     return HttpResponse(os.path.join(settings.MEDIA_URL, fname))
 
 
-def flickr(request):
-    url = request.POST.get('flickr_url')
-    if url:
-        import re
-        from urllib2 import urlopen
+@active_tab('cover')
+def image(request, pk):
+    image = get_object_or_404(Image, pk=pk)
 
 
-        html = urlopen(url).read()
-        match = re.search(r'<a href="([^"]*)" rel="license cc:license">Some rights reserved</a>', html)
-        try:
-            assert match
-            license_url = match.group(1)
-            re_license = re.compile(r'http://creativecommons.org/licenses/([^/]*)/([^/]*)/.*')
-            m = re_license.match(license_url)
-            assert m
-            license_name = 'CC %s %s' % (m.group(1).upper(), m.group(2))
-        except AssertionError:
-            license_name = 'NIEZNANA LICENCJA'
-
-        m = re.search(r'<strong class="username">By <a href="[^"]*">([^<]*)</a></strong>', html)
-        if m:
-            author = m.group(1)
+    if request.user.has_perm('cover.change_image'):
+        if request.method == "POST":
+            form = forms.ImageEditForm(request.POST, instance=image)
+            if form.is_valid():
+                form.save()
+                return HttpResponseRedirect(image.get_absolute_url())
         else:
         else:
-            author = "NIEZNANY AUTOR"
-
-        url_size = url.rstrip('/') + '/sizes/o/'
-        html = urlopen(url_size).read()
-        m = re.search(r'<div id="allsizes-photo">\s*<img src="([^"]*)"', html)
-        if m:
-            image_url = m.group(1)
+            form = forms.ImageEditForm(instance=image)
+        editable = True
     else:
     else:
-        url = ""
-
-    return render(request, 'cover/flickr.html', locals())
+        form = forms.ReadonlyImageEditForm(instance=image)
+        editable = False
+
+    return render(request, "cover/image_detail.html", {
+        "object": image,
+        "form": form,
+        "editable": editable,
+    })
+
+
+@active_tab('cover')
+def image_list(request):
+    objects = Image.objects.all()
+    enable_add = request.user.has_perm('cover.add_image')
+    return render(request, "cover/image_list.html", {
+        'object_list': Image.objects.all(),
+        'can_add': request.user.has_perm('cover.add_image'),
+    })
+
+
+@permission_required('cover.add_image')
+@active_tab('cover')
+def add_image(request):
+    form = ff = None
+    if request.method == 'POST':
+        if request.POST.get('form_id') == 'flickr':
+            ff = forms.FlickrForm(request.POST)
+            if ff.is_valid():
+                form = forms.ImageAddForm(ff.cleaned_data)
+        else:
+            form = forms.ImageAddForm(request.POST)
+            if form.is_valid():
+                obj = form.save()
+                return HttpResponseRedirect(obj.get_absolute_url())
+    if form is None:
+        form = forms.ImageAddForm()
+    if ff is None:
+        ff = forms.FlickrForm()
+    return render(request, 'cover/add_image.html', {
+            'form': form,
+            'ff': ff,
+        })
index dcbbb1f..717de29 100644 (file)
@@ -26,10 +26,8 @@ USE_CELERY = False
 INSTALLED_APPS += ('django_nose', 'dvcs.tests')
 
 TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
 INSTALLED_APPS += ('django_nose', 'dvcs.tests')
 
 TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
-#TEST_MODULES = ('catalogue')
-
-TEST_MODULES = ('catalogue', 'dvcs.tests', 'wiki', 'toolbar')
-COVER_APPS = ('catalogue', 'dvcs', 'wiki', 'toolbar')
+TEST_MODULES = ('catalogue', 'cover', 'dvcs.tests', 'wiki', 'toolbar')
+COVER_APPS = ('catalogue', 'cover', 'dvcs', 'wiki', 'toolbar')
 NOSE_ARGS = (
     '--tests=' + ','.join(TEST_MODULES),
     '--cover-package=' + ','.join(COVER_APPS),
 NOSE_ARGS = (
     '--tests=' + ','.join(TEST_MODULES),
     '--cover-package=' + ','.join(COVER_APPS),
index 5e94254..370f6a9 100644 (file)
@@ -11,7 +11,7 @@ httplib2 # oauth2 dependency
 
 ## Django
 Django>=1.3,<1.4
 
 ## Django
 Django>=1.3,<1.4
-sorl-thumbnail>=3.2
+sorl-thumbnail>=11.09,<12
 django-maintenancemode>=0.9
 django-pagination
 django-gravatar
 django-maintenancemode>=0.9
 django-pagination
 django-gravatar