stars and tags instead of shelves, move to social app
authorRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Thu, 19 Jan 2012 14:21:59 +0000 (15:21 +0100)
committerRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Thu, 19 Jan 2012 14:25:45 +0000 (15:25 +0100)
Book.related_info cache for related tags and media instead of short_html cache
ajaxable: add Vary header for ajax, support for login-only forms and prefixing,
and we don't need the ?ajax nonsense
move SearchForm to search

40 files changed:
apps/ajaxable/templates/ajaxable/form.html
apps/ajaxable/utils.py
apps/catalogue/fields.py
apps/catalogue/forms.py
apps/catalogue/migrations/0025_auto__add_field_book__related_info.py [new file with mode: 0644]
apps/catalogue/models.py
apps/catalogue/templatetags/catalogue_tags.py
apps/catalogue/urls.py
apps/catalogue/views.py
apps/dictionary/views.py
apps/search/context_processors.py
apps/search/fields.py [new file with mode: 0755]
apps/search/forms.py [new file with mode: 0755]
apps/social/__init__.py [new file with mode: 0644]
apps/social/forms.py [new file with mode: 0755]
apps/social/migrations/__init__.py [new file with mode: 0755]
apps/social/models.py [new file with mode: 0644]
apps/social/templates/social/my_shelf.html [new file with mode: 0755]
apps/social/templates/social/sets_form.html [new file with mode: 0755]
apps/social/templatetags/__init__.py [new file with mode: 0755]
apps/social/templatetags/social_tags.py [new file with mode: 0755]
apps/social/urls.py [new file with mode: 0755]
apps/social/utils.py [new file with mode: 0755]
apps/social/views.py [new file with mode: 0644]
requirements-dev.txt [new file with mode: 0755]
requirements.txt
wolnelektury/settings.py
wolnelektury/static/css/book_box.css
wolnelektury/static/css/dialogs.css
wolnelektury/static/js/dialogs.js
wolnelektury/templates/auth/login_register.html [new file with mode: 0755]
wolnelektury/templates/base.html
wolnelektury/templates/catalogue/book_mini_box.html
wolnelektury/templates/catalogue/book_short.html
wolnelektury/templates/catalogue/book_wide.html
wolnelektury/templates/catalogue/tagged_object_list.html
wolnelektury/templates/catalogue/work-list.html [new file with mode: 0755]
wolnelektury/templates/main_page.html
wolnelektury/urls.py
wolnelektury/views.py

index ba79e4b..1658b9b 100755 (executable)
@@ -3,8 +3,10 @@
 
 <form action="{{ request.get_full_path }}" method="post" accept-charset="utf-8" class="cuteform">
 <ol>
-    <div id="id___all__"></div>
+    <div id="id_{% if form_prefix %}{{ form_prefix }}-{% endif %}__all__"></div>
     {{ form.as_ul }}
     <li><input type="submit" value="{{ submit }}"/></li>
 </ol>
-</form>
\ No newline at end of file
+</form>
+
+{% block extra %}{% endblock %}
\ No newline at end of file
index d6f7050..e32356a 100755 (executable)
@@ -1,11 +1,16 @@
-from django.http import HttpResponse, HttpResponseRedirect
+from functools import wraps
+
+from django.http import (HttpResponse, HttpResponseRedirect,
+        HttpResponseForbidden)
 from django.shortcuts import render_to_response
 from django.template import RequestContext
+from django.utils.cache import patch_vary_headers
 from django.utils.encoding import force_unicode
 from django.utils.functional import Promise
 from django.utils.http import urlquote_plus
 from django.utils import simplejson
 from django.utils.translation import ugettext_lazy as _
+from django.views.decorators.vary import vary_on_headers
 
 
 class LazyEncoder(simplejson.JSONEncoder):
@@ -25,6 +30,28 @@ class JSONResponse(HttpResponse):
         super(JSONResponse, self).__init__(data, mimetype="application/json", **kwargs)
 
 
+def method_decorator(function_decorator):
+    """Converts a function decorator to a method decorator.
+
+    It just makes it ignore first argument.
+    """
+    def decorator(method):
+        @wraps(method)
+        def wrapped_method(self, *args, **kwargs):
+            def function(*fargs, **fkwargs):
+                return method(self, *fargs, **fkwargs)
+            return function_decorator(function)(*args, **kwargs)
+        return wrapped_method
+    return decorator
+
+
+def require_login(request):
+    """Return 403 if request is AJAX. Redirect to login page if not."""
+    if request.is_ajax():
+        return HttpResponseForbidden('Not logged in')
+    else:
+        return HttpResponseRedirect('/uzytkownicy/zaloguj')# next?=request.build_full_path())
+
 
 class AjaxableFormView(object):
     """Subclass this to create an ajaxable view for any form.
@@ -39,40 +66,76 @@ class AjaxableFormView(object):
     
     title = ''
     success_message = ''
+    POST_login = False
     formname = "form"
+    form_prefix = None
     full_template = "ajaxable/form_on_page.html"
 
-    def __call__(self, request):
-        """A view displaying a form, or JSON if `ajax' GET param is set."""
-        ajax = request.GET.get('ajax', False)
+    @method_decorator(vary_on_headers('X-Requested-With'))
+    def __call__(self, request, *args, **kwargs):
+        """A view displaying a form, or JSON if request is AJAX."""
+        form_args, form_kwargs = self.form_args(request, *args, **kwargs)
+        if self.form_prefix:
+            form_kwargs['prefix'] = self.form_prefix
+
         if request.method == "POST":
-            form = self.form_class(data=request.POST)
+            # do I need to be logged in?
+            if self.POST_login and not request.user.is_authenticated():
+                return require_login(request)
+
+            form_kwargs['data'] = request.POST
+            form = self.form_class(*form_args, **form_kwargs)
             if form.is_valid():
-                self.success(form, request)
+                add_args = self.success(form, request)
                 redirect = request.GET.get('next')
-                if not ajax and redirect:
+                if not request.is_ajax() and redirect:
                     return HttpResponseRedirect(urlquote_plus(
                             redirect, safe='/?=&'))
                 response_data = {'success': True, 
                     'message': self.success_message, 'redirect': redirect}
-            else:
-                response_data = {'success': False, 'errors': form.errors}
-            if ajax:
+                if add_args:
+                    response_data.update(add_args)
+            elif request.is_ajax():
+                # Form was sent with errors. Send them back.
+                if self.form_prefix:
+                    errors = {}
+                    for key, value in form.errors.items():
+                        errors["%s-%s" % (self.form_prefix, key)] = value
+                else:
+                    errors = form.errors
+                response_data = {'success': False, 'errors': errors}
+            if request.is_ajax():
                 return HttpResponse(LazyEncoder(ensure_ascii=False).encode(response_data))
         else:
-            form = self.form_class()
+            if (self.POST_login and not request.user.is_authenticated()
+                    and not request.is_ajax()):
+                return require_login(request)
+
+            form = self.form_class(*form_args, **form_kwargs)
             response_data = None
 
-        template = self.template if ajax else self.full_template
-        return render_to_response(template, {
+        template = self.template if request.is_ajax() else self.full_template
+        context = {
                 self.formname: form, 
                 "title": self.title,
                 "submit": self.submit,
                 "response_data": response_data,
                 "ajax_template": self.template,
-            },
+                "view_args": args,
+                "view_kwargs": kwargs,
+            }
+        context.update(self.extra_context())
+        return render_to_response(template, context,
             context_instance=RequestContext(request))
 
+    def form_args(self, request, *args, **kwargs):
+        """Override to parse view args and give additional args to the form."""
+        return (), {}
+
+    def extra_context(self):
+        """Override to pass something to template."""
+        return {}
+
     def success(self, form, request):
         """What to do when the form is valid.
         
index e19df9d..390fb03 100644 (file)
@@ -3,18 +3,12 @@
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 import datetime
-from functools import wraps
 
 from django.conf import settings
 from django.db import models
 from django.db.models.fields.files import FieldFile
-from django.db.models import signals
 from django import forms
-from django.forms.widgets import flatatt
-from django.utils.encoding import smart_unicode
 from django.utils import simplejson as json
-from django.utils.html import escape
-from django.utils.safestring import mark_safe
 from django.utils.translation import ugettext_lazy as _
 
 
@@ -72,58 +66,6 @@ class JSONField(models.TextField):
         setattr(cls, 'set_%s_value' % self.name, set_value)
 
 
-class JQueryAutoCompleteWidget(forms.TextInput):
-    def __init__(self, options, *args, **kwargs):
-        self.options = dumps(options)
-        super(JQueryAutoCompleteWidget, self).__init__(*args, **kwargs)
-
-    def render_js(self, field_id, options):
-        return u'$(\'#%s\').autocomplete(%s).result(autocomplete_result_handler);' % (field_id, options)
-
-    def render(self, name, value=None, attrs=None):
-        final_attrs = self.build_attrs(attrs, name=name)
-        if value:
-            final_attrs['value'] = smart_unicode(value)
-
-        if not self.attrs.has_key('id'):
-            final_attrs['id'] = 'id_%s' % name
-
-        html = u'''<input type="text" %(attrs)s/>
-            <script type="text/javascript">//<!--
-            %(js)s//--></script>
-            ''' % {
-                'attrs': flatatt(final_attrs),
-                'js' : self.render_js(final_attrs['id'], self.options),
-            }
-
-        return mark_safe(html)
-
-
-class JQueryAutoCompleteSearchWidget(JQueryAutoCompleteWidget):
-    def __init__(self, *args, **kwargs):
-        super(JQueryAutoCompleteSearchWidget, self).__init__(*args, **kwargs)
-
-    def render_js(self, field_id, options):
-        return u""
-    
-
-class JQueryAutoCompleteField(forms.CharField):
-    def __init__(self, source, options={}, *args, **kwargs):
-        if 'widget' not in kwargs:
-            options['source'] = source
-            kwargs['widget'] = JQueryAutoCompleteWidget(options)
-
-        super(JQueryAutoCompleteField, self).__init__(*args, **kwargs)
-
-
-class JQueryAutoCompleteSearchField(forms.CharField):
-    def __init__(self, options={}, *args, **kwargs):
-        if 'widget' not in kwargs:
-            kwargs['widget'] = JQueryAutoCompleteSearchWidget(options)
-
-        super(JQueryAutoCompleteSearchField, self).__init__(*args, **kwargs)
-
-
 class OverwritingFieldFile(FieldFile):
     """
         Deletes the old file before saving the new one.
index 655f1ec..75d9ab9 100644 (file)
@@ -4,11 +4,8 @@
 #
 from django import forms
 from django.utils.translation import ugettext_lazy as _
-from slughifi import slughifi
 
-from catalogue.models import Tag, Book
-from catalogue.fields import JQueryAutoCompleteSearchField
-from catalogue import utils
+from catalogue.models import Book
 
 
 class BookImportForm(forms.Form):
@@ -30,55 +27,6 @@ class BookImportForm(forms.Form):
         return Book.from_xml_file(self.cleaned_data['book_xml_file'], overwrite=True, **kwargs)
 
 
-class SearchForm(forms.Form):
-    q = JQueryAutoCompleteSearchField()  # {'minChars': 2, 'selectFirst': True, 'cacheLength': 50, 'matchContains': "word"})
-
-    def __init__(self, source, *args, **kwargs):
-        kwargs['auto_id'] = False
-        super(SearchForm, self).__init__(*args, **kwargs)
-        self.fields['q'].widget.attrs['id'] = _('search')
-        self.fields['q'].widget.attrs['autocomplete'] = _('off')
-        self.fields['q'].widget.attrs['data-source'] = _(source)
-        if not 'q' in self.data:
-            self.fields['q'].widget.attrs['title'] = _('title, author, theme/topic, epoch, kind, genre, phrase')
-
-
-class UserSetsForm(forms.Form):
-    def __init__(self, book, user, *args, **kwargs):
-        super(UserSetsForm, self).__init__(*args, **kwargs)
-        self.fields['set_ids'] = forms.ChoiceField(
-            choices=[(tag.id, tag.name) for tag in Tag.objects.filter(category='set', user=user)],
-        )
-
-
-class ObjectSetsForm(forms.Form):
-    def __init__(self, obj, user, *args, **kwargs):
-        super(ObjectSetsForm, self).__init__(*args, **kwargs)
-        self.fields['set_ids'] = forms.MultipleChoiceField(
-            label=_('Shelves'),
-            required=False,
-            choices=[(tag.id, "%s (%s)" % (tag.name, tag.book_count)) for tag in Tag.objects.filter(category='set', user=user)],
-            initial=[tag.id for tag in obj.tags.filter(category='set', user=user)],
-            widget=forms.CheckboxSelectMultiple
-        )
-
-
-class NewSetForm(forms.Form):
-    name = forms.CharField(max_length=50, required=True)
-
-    def __init__(self, *args, **kwargs):
-        super(NewSetForm, self).__init__(*args, **kwargs)
-        self.fields['name'].widget.attrs['title'] = _('Name of the new shelf')
-
-    def save(self, user, commit=True):
-        name = self.cleaned_data['name']
-        new_set = Tag(name=name, slug=utils.get_random_hash(name), sort_key=name.lower(),
-            category='set', user=user)
-
-        new_set.save()
-        return new_set
-
-
 FORMATS = [(f, f.upper()) for f in Book.ebook_formats]
 
 
diff --git a/apps/catalogue/migrations/0025_auto__add_field_book__related_info.py b/apps/catalogue/migrations/0025_auto__add_field_book__related_info.py
new file mode 100644 (file)
index 0000000..a46e34d
--- /dev/null
@@ -0,0 +1,133 @@
+# encoding: 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 field 'Book._related_info'
+        db.add_column('catalogue_book', '_related_info', self.gf('jsonfield.fields.JSONField')(null=True, blank=True), keep_default=False)
+
+
+    def backwards(self, orm):
+        
+        # Deleting field 'Book._related_info'
+        db.delete_column('catalogue_book', '_related_info')
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'catalogue.book': {
+            'Meta': {'ordering': "('sort_key',)", 'object_name': 'Book'},
+            '_related_info': ('jsonfield.fields.JSONField', [], {'null': 'True', 'blank': 'True'}),
+            'changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'common_slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'db_index': 'True'}),
+            'cover': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
+            'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'epub_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}),
+            'extra_info': ('catalogue.fields.JSONField', [], {'default': "'{}'"}),
+            'gazeta_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}),
+            'html_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'language': ('django.db.models.fields.CharField', [], {'default': "'pol'", 'max_length': '3', 'db_index': 'True'}),
+            'mobi_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['catalogue.Book']"}),
+            'parent_number': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'pdf_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '120', 'db_index': 'True'}),
+            'sort_key': ('django.db.models.fields.CharField', [], {'max_length': '120', 'db_index': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '120'}),
+            'txt_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}),
+            'wiki_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}),
+            'xml_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'})
+        },
+        'catalogue.bookmedia': {
+            'Meta': {'ordering': "('type', 'name')", 'object_name': 'BookMedia'},
+            'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'media'", 'to': "orm['catalogue.Book']"}),
+            'extra_info': ('catalogue.fields.JSONField', [], {'default': "'{}'"}),
+            'file': ('catalogue.fields.OverwritingFileField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}),
+            'source_sha1': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}),
+            'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+        },
+        'catalogue.collection': {
+            'Meta': {'ordering': "('title',)", 'object_name': 'Collection'},
+            'book_slugs': ('django.db.models.fields.TextField', [], {}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'primary_key': 'True', 'db_index': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '120', 'db_index': 'True'})
+        },
+        'catalogue.fragment': {
+            'Meta': {'ordering': "('book', 'anchor')", 'object_name': 'Fragment'},
+            'anchor': ('django.db.models.fields.CharField', [], {'max_length': '120'}),
+            'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'fragments'", 'to': "orm['catalogue.Book']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'short_text': ('django.db.models.fields.TextField', [], {}),
+            'text': ('django.db.models.fields.TextField', [], {})
+        },
+        'catalogue.tag': {
+            'Meta': {'ordering': "('sort_key',)", 'unique_together': "(('slug', 'category'),)", 'object_name': 'Tag'},
+            'book_count': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'category': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
+            'changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'gazeta_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'db_index': 'True'}),
+            'sort_key': ('django.db.models.fields.CharField', [], {'max_length': '120', 'db_index': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+            'wiki_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'})
+        },
+        'catalogue.tagrelation': {
+            'Meta': {'unique_together': "(('tag', 'content_type', 'object_id'),)", 'object_name': 'TagRelation', 'db_table': "'catalogue_tag_relation'"},
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'items'", 'to': "orm['catalogue.Tag']"})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        }
+    }
+
+    complete_apps = ['catalogue']
index 9a1e71a..1f6210d 100644 (file)
@@ -7,7 +7,7 @@ from collections import namedtuple
 from django.db import models
 from django.db.models import permalink, Q
 import django.dispatch
-from django.core.cache import cache
+from django.core.cache import get_cache
 from django.core.files.storage import DefaultStorage
 from django.utils.translation import ugettext_lazy as _
 from django.contrib.auth.models import User
@@ -17,6 +17,7 @@ from django.utils.safestring import mark_safe
 from django.utils.translation import get_language
 from django.core.urlresolvers import reverse
 from django.db.models.signals import post_save, m2m_changed, pre_delete
+import jsonfield
 
 from django.conf import settings
 
@@ -30,7 +31,6 @@ from glob import glob
 import re
 from os import path
 
-
 import search
 
 # Those are hard-coded here so that makemessages sees them.
@@ -44,8 +44,8 @@ TAG_CATEGORIES = (
     ('book', _('book')),
 )
 
-# not quite, but Django wants you to set a timeout
-CACHE_FOREVER = 2419200  # 28 days
+
+permanent_cache = get_cache('permanent')
 
 
 class TagSubcategoryManager(models.Manager):
@@ -372,6 +372,9 @@ class Book(models.Model):
     formats = ebook_formats + ['html', 'xml']
 
     parent        = models.ForeignKey('self', blank=True, null=True, related_name='children')
+
+    _related_info = jsonfield.JSONField(blank=True, null=True, editable=False)
+
     objects  = models.Manager()
     tagged   = managers.ModelTaggedItemManager(Tag)
     tags     = managers.TagDescriptor(Tag)
@@ -450,58 +453,11 @@ class Book(models.Model):
         if self.id is None:
             return
 
-        cache_key = "Book.short_html/%d/%s"
-        for lang, langname in settings.LANGUAGES:
-            cache.delete(cache_key % (self.id, lang))
-        cache.delete("Book.mini_box/%d" % (self.id, ))
+        type(self).objects.filter(pk=self.pk).update(_related_info=None)
         # Fragment.short_html relies on book's tags, so reset it here too
         for fragm in self.fragments.all():
             fragm.reset_short_html()
 
-    def short_html(self):
-        if self.id:
-            cache_key = "Book.short_html/%d/%s" % (self.id, get_language())
-            short_html = cache.get(cache_key)
-        else:
-            short_html = None
-
-        if short_html is not None:
-            return mark_safe(short_html)
-        else:
-            tags = self.tags.filter(category__in=('author', 'kind', 'genre', 'epoch'))
-            tags = split_tags(tags)
-
-            formats = {}
-            # files generated during publication
-            for ebook_format in self.ebook_formats:
-                if self.has_media(ebook_format):
-                    formats[ebook_format] = self.get_media(ebook_format)
-
-
-            short_html = unicode(render_to_string('catalogue/book_short.html',
-                {'book': self, 'tags': tags, 'formats': formats}))
-
-            if self.id:
-                cache.set(cache_key, short_html, CACHE_FOREVER)
-            return mark_safe(short_html)
-
-    def mini_box(self):
-        if self.id:
-            cache_key = "Book.mini_box/%d" % (self.id, )
-            short_html = cache.get(cache_key)
-        else:
-            short_html = None
-
-        if short_html is None:
-            authors = self.tags.filter(category='author')
-
-            short_html = unicode(render_to_string('catalogue/book_mini_box.html',
-                {'book': self, 'authors': authors, 'STATIC_URL': settings.STATIC_URL}))
-
-            if self.id:
-                cache.set(cache_key, short_html, CACHE_FOREVER)
-        return mark_safe(short_html)
-
     def has_description(self):
         return len(self.description) > 0
     has_description.short_description = _('description')
@@ -818,12 +774,30 @@ class Book(models.Model):
         cls.published.send(sender=book)
         return book
 
+    def related_info(self):
+        """Keeps info about related objects (tags, media) in cache field."""
+        if self._related_info is not None:
+            return self._related_info
+        else:
+            rel = {'tags': {}, 'media': {}}
+            tags = self.tags.filter(category__in=(
+                    'author', 'kind', 'genre', 'epoch'))
+            tags = split_tags(tags)
+            for category in tags:
+                rel['tags'][category] = [
+                        (t.name, t.get_absolute_url()) for t in tags[category]]
+            for media_format in BookMedia.formats:
+                rel['media'][media_format] = self.has_media(media_format)
+            if self.pk:
+                type(self).objects.filter(pk=self.pk).update(_related_info=rel)
+            return rel
+
     def reset_tag_counter(self):
         if self.id is None:
             return
 
         cache_key = "Book.tag_counter/%d" % self.id
-        cache.delete(cache_key)
+        permanent_cache.delete(cache_key)
         if self.parent:
             self.parent.reset_tag_counter()
 
@@ -831,7 +805,7 @@ class Book(models.Model):
     def tag_counter(self):
         if self.id:
             cache_key = "Book.tag_counter/%d" % self.id
-            tags = cache.get(cache_key)
+            tags = permanent_cache.get(cache_key)
         else:
             tags = None
 
@@ -844,7 +818,7 @@ class Book(models.Model):
                 tags[tag.pk] = 1
 
             if self.id:
-                cache.set(cache_key, tags, CACHE_FOREVER)
+                permanent_cache.set(cache_key, tags)
         return tags
 
     def reset_theme_counter(self):
@@ -852,7 +826,7 @@ class Book(models.Model):
             return
 
         cache_key = "Book.theme_counter/%d" % self.id
-        cache.delete(cache_key)
+        permanent_cache.delete(cache_key)
         if self.parent:
             self.parent.reset_theme_counter()
 
@@ -860,7 +834,7 @@ class Book(models.Model):
     def theme_counter(self):
         if self.id:
             cache_key = "Book.theme_counter/%d" % self.id
-            tags = cache.get(cache_key)
+            tags = permanent_cache.get(cache_key)
         else:
             tags = None
 
@@ -871,7 +845,7 @@ class Book(models.Model):
                     tags[tag.pk] = tags.get(tag.pk, 0) + 1
 
             if self.id:
-                cache.set(cache_key, tags, CACHE_FOREVER)
+                permanent_cache.set(cache_key, tags)
         return tags
 
     def pretty_title(self, html_links=False):
@@ -1003,12 +977,12 @@ class Fragment(models.Model):
 
         cache_key = "Fragment.short_html/%d/%s"
         for lang, langname in settings.LANGUAGES:
-            cache.delete(cache_key % (self.id, lang))
+            permanent_cache.delete(cache_key % (self.id, lang))
 
     def short_html(self):
         if self.id:
             cache_key = "Fragment.short_html/%d/%s" % (self.id, get_language())
-            short_html = cache.get(cache_key)
+            short_html = permanent_cache.get(cache_key)
         else:
             short_html = None
 
@@ -1018,7 +992,7 @@ class Fragment(models.Model):
             short_html = unicode(render_to_string('catalogue/fragment_short.html',
                 {'fragment': self}))
             if self.id:
-                cache.set(cache_key, short_html, CACHE_FOREVER)
+                permanent_cache.set(cache_key, short_html)
             return mark_safe(short_html)
 
 
index df938a6..eeba74e 100644 (file)
@@ -8,16 +8,15 @@ import datetime
 from django import template
 from django.template import Node, Variable
 from django.utils.encoding import smart_str
+from django.core.cache import get_cache
 from django.core.urlresolvers import reverse
 from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
 from django.db.models import Q
 from django.conf import settings
 from django.utils.translation import ugettext as _
 
-from catalogue.forms import SearchForm
 from catalogue.utils import split_tags
 
-
 register = template.Library()
 
 
@@ -278,21 +277,43 @@ def book_info(book):
     return locals()
 
 
-@register.inclusion_tag('catalogue/book_wide.html')
-def book_wide(book):
-    tags = book.tags.filter(category__in=('author', 'kind', 'genre', 'epoch'))
-    tags = split_tags(tags)
-
+@register.inclusion_tag('catalogue/book_wide.html', takes_context=True)
+def book_wide(context, book):
     formats = {}
     # files generated during publication
     for ebook_format in book.ebook_formats:
         if book.has_media(ebook_format):
             formats[ebook_format] = book.get_media(ebook_format)
 
-    extra_info = book.get_extra_info_value()
-
-    has_media = {}
-    for media_format in ['mp3', 'ogg']:
-        has_media[media_format] = book.has_media(media_format)
-
-    return locals()
+    return {
+        'related': book.related_info(),
+        'book': book,
+        'formats': formats,
+        'extra_info': book.get_extra_info_value(),
+        'request': context.get('request'),
+    }
+
+
+@register.inclusion_tag('catalogue/book_short.html', takes_context=True)
+def book_short(context, book):
+    return {
+        'related': book.related_info(),
+        'book': book,
+        'request': context.get('request'),
+    }
+
+
+@register.inclusion_tag('catalogue/book_mini_box.html')
+def book_mini(book):
+    return {
+        'related': book.related_info(),
+        'book': book,
+    }
+
+
+@register.inclusion_tag('catalogue/work-list.html', takes_context=True)
+def work_list(context, object_list):
+    request = context.get('request')
+    if object_list:
+        object_type = type(object_list[0]).__name__
+    return locals()
\ No newline at end of file
index db044fc..88e6226 100644 (file)
@@ -18,17 +18,11 @@ urlpatterns = patterns('picture.views',
         ) + \
     patterns('catalogue.views',
     url(r'^$', 'catalogue', name='catalogue'),
-    url(r'^polki/(?P<shelf>[a-zA-Z0-9-]+)/formaty/$', 'shelf_book_formats', name='shelf_book_formats'),
-    url(r'^polki/(?P<shelf>[a-zA-Z0-9-]+)/(?P<slug>%s)/usun$' % SLUG, 'remove_from_shelf', name='remove_from_shelf'),
-    url(r'^polki/$', 'user_shelves', name='user_shelves'),
-    url(r'^polki/(?P<slug>[a-zA-Z0-9-]+)/usun/$', 'delete_shelf', name='delete_shelf'),
-    url(r'^polki/(?P<slug>[a-zA-Z0-9-]+)\.zip$', 'download_shelf', name='download_shelf'),
+
     url(r'^lektury/$', 'book_list', name='book_list'),
     url(r'^lektury/(?P<slug>[a-zA-Z0-9-]+)/$', 'collection', name='collection'),
     url(r'^audiobooki/$', 'audiobook_list', name='audiobook_list'),
     url(r'^daisy/$', 'daisy_list', name='daisy_list'),
-    url(r'^lektura/(?P<book>%s)/polki/' % SLUG, 'book_sets', name='book_shelves'),
-    url(r'^polki/nowa/$', 'new_set', name='new_set'),
     url(r'^tags/$', 'tags_starting_with', name='hint'),
     url(r'^jtags/$', 'json_tags_starting_with', name='jhint'),
     url(r'^szukaj/$', 'search', name='old_search'),
index f57797d..eb0e7b9 100644 (file)
@@ -502,169 +502,6 @@ def json_tags_starting_with(request, callback=None):
         result = {"matches": tags_list}
     return JSONResponse(result, callback)
 
-# ====================
-# = Shelf management =
-# ====================
-@login_required
-@cache.never_cache
-def user_shelves(request):
-    shelves = models.Tag.objects.filter(category='set', user=request.user)
-    new_set_form = forms.NewSetForm()
-    return render_to_response('catalogue/user_shelves.html', locals(),
-            context_instance=RequestContext(request))
-
-@cache.never_cache
-def book_sets(request, slug):
-    if not request.user.is_authenticated():
-        return HttpResponse(_('<p>To maintain your shelves you need to be logged in.</p>'))
-
-    book = get_object_or_404(models.Book, slug=slug)
-
-    user_sets = models.Tag.objects.filter(category='set', user=request.user)
-    book_sets = book.tags.filter(category='set', user=request.user)
-
-    if request.method == 'POST':
-        form = forms.ObjectSetsForm(book, request.user, request.POST)
-        if form.is_valid():
-            old_shelves = list(book.tags.filter(category='set'))
-            new_shelves = [models.Tag.objects.get(pk=id) for id in form.cleaned_data['set_ids']]
-
-            for shelf in [shelf for shelf in old_shelves if shelf not in new_shelves]:
-                touch_tag(shelf)
-
-            for shelf in [shelf for shelf in new_shelves if shelf not in old_shelves]:
-                touch_tag(shelf)
-
-            book.tags = new_shelves + list(book.tags.filter(~Q(category='set') | ~Q(user=request.user)))
-            if request.is_ajax():
-                return JSONResponse('{"msg":"'+_("<p>Shelves were sucessfully saved.</p>")+'", "after":"close"}')
-            else:
-                return HttpResponseRedirect('/')
-    else:
-        form = forms.ObjectSetsForm(book, request.user)
-        new_set_form = forms.NewSetForm()
-
-    return render_to_response('catalogue/book_sets.html', locals(),
-        context_instance=RequestContext(request))
-
-
-@login_required
-@require_POST
-@cache.never_cache
-def remove_from_shelf(request, shelf, slug):
-    book = get_object_or_404(models.Book, slug=slug)
-
-    shelf = get_object_or_404(models.Tag, slug=shelf, category='set', user=request.user)
-
-    if shelf in book.tags:
-        models.Tag.objects.remove_tag(book, shelf)
-        touch_tag(shelf)
-
-        return HttpResponse(_('Book was successfully removed from the shelf'))
-    else:
-        return HttpResponse(_('This book is not on the shelf'))
-
-
-def collect_books(books):
-    """
-    Returns all real books in collection.
-    """
-    result = []
-    for book in books:
-        if len(book.children.all()) == 0:
-            result.append(book)
-        else:
-            result += collect_books(book.children.all())
-    return result
-
-
-@cache.never_cache
-def download_shelf(request, slug):
-    """"
-    Create a ZIP archive on disk and transmit it in chunks of 8KB,
-    without loading the whole file into memory. A similar approach can
-    be used for large dynamic PDF files.
-    """
-    from slughifi import slughifi
-    import tempfile
-    import zipfile
-
-    shelf = get_object_or_404(models.Tag, slug=slug, category='set')
-
-    formats = []
-    form = forms.DownloadFormatsForm(request.GET)
-    if form.is_valid():
-        formats = form.cleaned_data['formats']
-    if len(formats) == 0:
-        formats = models.Book.ebook_formats
-
-    # Create a ZIP archive
-    temp = tempfile.TemporaryFile()
-    archive = zipfile.ZipFile(temp, 'w')
-
-    for book in collect_books(models.Book.tagged.with_all(shelf)):
-        for ebook_format in models.Book.ebook_formats:
-            if ebook_format in formats and book.has_media(ebook_format):
-                filename = book.get_media(ebook_format).path
-                archive.write(filename, str('%s.%s' % (book.slug, ebook_format)))
-    archive.close()
-
-    response = HttpResponse(content_type='application/zip', mimetype='application/x-zip-compressed')
-    response['Content-Disposition'] = 'attachment; filename=%s.zip' % slughifi(shelf.name)
-    response['Content-Length'] = temp.tell()
-
-    temp.seek(0)
-    response.write(temp.read())
-    return response
-
-
-@cache.never_cache
-def shelf_book_formats(request, shelf):
-    """"
-    Returns a list of formats of books in shelf.
-    """
-    shelf = get_object_or_404(models.Tag, slug=shelf, category='set')
-
-    formats = {}
-    for ebook_format in models.Book.ebook_formats:
-        formats[ebook_format] = False
-
-    for book in collect_books(models.Book.tagged.with_all(shelf)):
-        for ebook_format in models.Book.ebook_formats:
-            if book.has_media(ebook_format):
-                formats[ebook_format] = True
-
-    return HttpResponse(LazyEncoder().encode(formats))
-
-
-@login_required
-@require_POST
-@cache.never_cache
-def new_set(request):
-    new_set_form = forms.NewSetForm(request.POST)
-    if new_set_form.is_valid():
-        new_set = new_set_form.save(request.user)
-
-        if request.is_ajax():
-            return JSONResponse('{"id":"%d", "name":"%s", "msg":"<p>Shelf <strong>%s</strong> was successfully created</p>"}' % (new_set.id, new_set.name, new_set))
-        else:
-            return HttpResponseRedirect('/')
-
-    return HttpResponseRedirect('/')
-
-
-@login_required
-@require_POST
-@cache.never_cache
-def delete_shelf(request, slug):
-    user_set = get_object_or_404(models.Tag, slug=slug, category='set', user=request.user)
-    user_set.delete()
-
-    if request.is_ajax():
-        return HttpResponse(_('<p>Shelf <strong>%s</strong> was successfully removed</p>') % user_set.name)
-    else:
-        return HttpResponseRedirect('/')
-
 
 # =========
 # = Admin =
index 7b9cd53..47204fd 100755 (executable)
@@ -3,7 +3,6 @@
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from django.views.generic.list_detail import object_list
-from catalogue.forms import SearchForm
 from dictionary.models import Note
 
 def letter_notes(request, letter=None):
index c54525a..cfb2f2e 100644 (file)
@@ -1,6 +1,6 @@
 
-from catalogue.forms import SearchForm
 from django.core.urlresolvers import reverse
+from search.forms import SearchForm
 
 
 def search_form(request):
diff --git a/apps/search/fields.py b/apps/search/fields.py
new file mode 100755 (executable)
index 0000000..680e618
--- /dev/null
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django import forms
+from django.forms.widgets import flatatt
+from django.utils.encoding import smart_unicode
+from django.utils.safestring import mark_safe
+from catalogue.fields import dumps
+
+
+class JQueryAutoCompleteWidget(forms.TextInput):
+    def __init__(self, options, *args, **kwargs):
+        self.options = dumps(options)
+        super(JQueryAutoCompleteWidget, self).__init__(*args, **kwargs)
+
+    def render_js(self, field_id, options):
+        return u'$(\'#%s\').autocomplete(%s).result(autocomplete_result_handler);' % (field_id, options)
+
+    def render(self, name, value=None, attrs=None):
+        final_attrs = self.build_attrs(attrs, name=name)
+        if value:
+            final_attrs['value'] = smart_unicode(value)
+
+        if not self.attrs.has_key('id'):
+            final_attrs['id'] = 'id_%s' % name
+
+        html = u'''<input type="text" %(attrs)s/>
+            <script type="text/javascript">//<!--
+            %(js)s//--></script>
+            ''' % {
+                'attrs': flatatt(final_attrs),
+                'js' : self.render_js(final_attrs['id'], self.options),
+            }
+
+        return mark_safe(html)
+
+
+class JQueryAutoCompleteSearchWidget(JQueryAutoCompleteWidget):
+    def __init__(self, *args, **kwargs):
+        super(JQueryAutoCompleteSearchWidget, self).__init__(*args, **kwargs)
+
+    def render_js(self, field_id, options):
+        return u""
+    
+
+class JQueryAutoCompleteField(forms.CharField):
+    def __init__(self, source, options={}, *args, **kwargs):
+        if 'widget' not in kwargs:
+            options['source'] = source
+            kwargs['widget'] = JQueryAutoCompleteWidget(options)
+
+        super(JQueryAutoCompleteField, self).__init__(*args, **kwargs)
+
+
+class JQueryAutoCompleteSearchField(forms.CharField):
+    def __init__(self, options={}, *args, **kwargs):
+        if 'widget' not in kwargs:
+            kwargs['widget'] = JQueryAutoCompleteSearchWidget(options)
+
+        super(JQueryAutoCompleteSearchField, self).__init__(*args, **kwargs)
diff --git a/apps/search/forms.py b/apps/search/forms.py
new file mode 100755 (executable)
index 0000000..e7051b8
--- /dev/null
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+
+from search.fields import JQueryAutoCompleteSearchField
+
+
+class SearchForm(forms.Form):
+    q = JQueryAutoCompleteSearchField()  # {'minChars': 2, 'selectFirst': True, 'cacheLength': 50, 'matchContains': "word"})
+
+    def __init__(self, source, *args, **kwargs):
+        kwargs['auto_id'] = False
+        super(SearchForm, self).__init__(*args, **kwargs)
+        self.fields['q'].widget.attrs['id'] = _('search')
+        self.fields['q'].widget.attrs['autocomplete'] = _('off')
+        self.fields['q'].widget.attrs['data-source'] = _(source)
+        if not 'q' in self.data:
+            self.fields['q'].widget.attrs['title'] = _('title, author, theme/topic, epoch, kind, genre, phrase')
diff --git a/apps/social/__init__.py b/apps/social/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/apps/social/forms.py b/apps/social/forms.py
new file mode 100755 (executable)
index 0000000..bbdc43c
--- /dev/null
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+
+from catalogue.models import Tag
+from catalogue import utils
+from social.utils import get_set, set_sets
+
+
+class UserSetsForm(forms.Form):
+    def __init__(self, book, user, *args, **kwargs):
+        super(UserSetsForm, self).__init__(*args, **kwargs)
+        self.fields['set_ids'] = forms.ChoiceField(
+            choices=[(tag.id, tag.name) for tag in Tag.objects.filter(category='set', user=user)],
+        )
+
+
+class ObjectSetsForm(forms.Form):
+    tags = forms.CharField(label=_('Tags (comma-separated)'), required=False)
+
+    def __init__(self, obj, user, *args, **kwargs):
+        self._obj = obj
+        self._user = user
+        data = kwargs.setdefault('data', {})
+        if 'tags' not in data and user.is_authenticated():
+            data['tags'] = ', '.join(t.name
+                for t in obj.tags.filter(category='set', user=user) if t.name)
+        super(ObjectSetsForm, self).__init__(*args, **kwargs)
+
+    def save(self, request):
+        tags = [get_set(self._user, tag_name.strip())
+                    for tag_name in self.cleaned_data['tags'].split(',')]
+        set_sets(self._user, self._obj, tags)
+        return {"like": True}
+
+
+class NewSetForm(forms.Form):
+    name = forms.CharField(max_length=50, required=True)
+
+    def __init__(self, *args, **kwargs):
+        super(NewSetForm, self).__init__(*args, **kwargs)
+        self.fields['name'].widget.attrs['title'] = _('Name of the new shelf')
+
+    def save(self, user, commit=True):
+        name = self.cleaned_data['name']
+        new_set = Tag(name=name, slug=utils.get_random_hash(name), sort_key=name.lower(),
+            category='set', user=user)
+
+        new_set.save()
+        return new_set
diff --git a/apps/social/migrations/__init__.py b/apps/social/migrations/__init__.py
new file mode 100755 (executable)
index 0000000..e69de29
diff --git a/apps/social/models.py b/apps/social/models.py
new file mode 100644 (file)
index 0000000..71a8362
--- /dev/null
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/apps/social/templates/social/my_shelf.html b/apps/social/templates/social/my_shelf.html
new file mode 100755 (executable)
index 0000000..c465ab9
--- /dev/null
@@ -0,0 +1,15 @@
+{% extends "base.html" %}
+{% load i18n %}
+{% load catalogue_tags %}
+
+{% block titleextra %}{% trans "My shelf" %}{% endblock %}
+
+{% block logout %}/{% endblock %}
+
+{% block body %}
+
+    <h1>{% trans "My shelf" %}</h1>
+
+    {% work_list books %}
+
+{% endblock %}
diff --git a/apps/social/templates/social/sets_form.html b/apps/social/templates/social/sets_form.html
new file mode 100755 (executable)
index 0000000..eff951e
--- /dev/null
@@ -0,0 +1,14 @@
+{% load i18n %}
+<h1>{{ title }}</h1>
+
+<form action="{% url social_unlike_book view_kwargs.slug %}" method="post" accept-charset="utf-8" class="cuteform">
+    <input type="submit" value="{% trans "Remove from my shelf" %}"/>
+</form>
+
+<form action="{{ request.get_full_path }}" method="post" accept-charset="utf-8" class="cuteform">
+<ol>
+    <div id="id___all__"></div>
+    {{ form.as_ul }}
+    <li><input type="submit" value="{{ submit }}"/></li>
+</ol>
+</form>
\ No newline at end of file
diff --git a/apps/social/templatetags/__init__.py b/apps/social/templatetags/__init__.py
new file mode 100755 (executable)
index 0000000..e69de29
diff --git a/apps/social/templatetags/social_tags.py b/apps/social/templatetags/social_tags.py
new file mode 100755 (executable)
index 0000000..1b26d91
--- /dev/null
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django import template
+from social.utils import likes
+
+register = template.Library()
+
+register.filter('likes', likes)
diff --git a/apps/social/urls.py b/apps/social/urls.py
new file mode 100755 (executable)
index 0000000..9e6de00
--- /dev/null
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.conf.urls.defaults import *
+from social.views import ObjectSetsFormView
+
+urlpatterns = patterns('social.views',
+    url(r'^lektura/(?P<slug>[a-z0-9-]+)/lubie/$', 'like_book', name='social_like_book'),
+    url(r'^lektura/(?P<slug>[a-z0-9-]+)/nie_lubie/$', 'unlike_book', name='social_unlike_book'),
+    url(r'^lektura/(?P<slug>[a-z0-9-]+)/polki/$', ObjectSetsFormView(), name='social_book_sets'),
+    url(r'^polka/$', 'my_shelf', name='social_my_shelf'),
+
+    #~ url(r'^polki/(?P<shelf>[a-zA-Z0-9-]+)/formaty/$', 'shelf_book_formats', name='shelf_book_formats'),
+    #~ url(r'^polki/(?P<shelf>[a-zA-Z0-9-]+)/(?P<slug>%s)/usun$' % SLUG, 'remove_from_shelf', name='remove_from_shelf'),
+    #~ url(r'^polki/$', 'user_shelves', name='user_shelves'),
+    #~ url(r'^polki/(?P<slug>[a-zA-Z0-9-]+)/usun/$', 'delete_shelf', name='delete_shelf'),
+    #~ url(r'^polki/(?P<slug>[a-zA-Z0-9-]+)\.zip$', 'download_shelf', name='download_shelf'),
+    #~ url(r'^polki/nowa/$', 'new_set', name='new_set'),
+) 
diff --git a/apps/social/utils.py b/apps/social/utils.py
new file mode 100755 (executable)
index 0000000..96f0ca0
--- /dev/null
@@ -0,0 +1,35 @@
+from django.db.models import Q
+from catalogue.models import Tag
+from catalogue import utils
+from catalogue.tasks import touch_tag
+
+
+def likes(user, work):
+    return user.is_authenticated() and work.tags.filter(category='set', user=user).exists()
+
+
+def get_set(user, name):
+    """Returns a tag for use by the user. Creates it, if necessary."""
+    try:
+        tag = Tag.objects.get(category='set', user=user, name=name)
+    except Tag.DoesNotExist:
+        tag = Tag.objects.create(category='set', user=user, name=name,
+                slug=utils.get_random_hash(name), sort_key=name.lower())
+    return tag
+
+
+def set_sets(user, work, sets):
+    """Set tags used for given work by a given user."""
+
+    old_sets = list(work.tags.filter(category='set', user=user))
+
+    work.tags = sets + list(
+            work.tags.filter(~Q(category='set') | ~Q(user=user)))
+
+    for shelf in [shelf for shelf in old_sets if shelf not in sets]:
+        touch_tag(shelf)
+    for shelf in [shelf for shelf in sets if shelf not in old_sets]:
+        touch_tag(shelf)
+
+    # delete empty tags
+    Tag.objects.filter(category='set', user=user, book_count=0).delete()
diff --git a/apps/social/views.py b/apps/social/views.py
new file mode 100644 (file)
index 0000000..6ded289
--- /dev/null
@@ -0,0 +1,218 @@
+# -*- coding: utf-8 -*-
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.shortcuts import render, get_object_or_404, redirect
+from django.http import HttpResponseForbidden
+from django.contrib.auth.decorators import login_required
+#~ from django.utils.datastructures import SortedDict
+from django.views.decorators.http import require_POST
+#~ from django.contrib import auth
+#~ from django.views.decorators import cache
+from django.utils.translation import ugettext as _
+
+from ajaxable.utils import LazyEncoder, JSONResponse, AjaxableFormView
+
+from catalogue.models import Book, Tag
+from social import forms
+from social.utils import get_set, likes, set_sets
+
+
+# ====================
+# = Shelf management =
+# ====================
+
+
+@require_POST
+def like_book(request, slug):
+    if not request.user.is_authenticated():
+        return HttpResponseForbidden('Login required.')
+    book = get_object_or_404(Book, slug=slug)
+    if not likes(request.user, book):
+        tag = get_set(request.user, '')
+        set_sets(request.user, book, [tag])
+
+    if request.is_ajax():
+        return JSONResponse({"success": True, "msg": "ok", "like": True})
+    else:
+        return redirect(book)
+
+
+@login_required
+def my_shelf(request):
+    books = Book.tagged.with_any(request.user.tag_set.all())
+    return render(request, 'social/my_shelf.html', locals())
+
+
+class ObjectSetsFormView(AjaxableFormView):
+    form_class = forms.ObjectSetsForm
+    template = 'social/sets_form.html'
+    ajax_redirect = True
+    POST_login = True
+
+    def form_args(self, request, slug):
+        book = get_object_or_404(Book, slug=slug)
+        return (book, request.user), {}
+
+
+def unlike_book(request, slug):
+    book = get_object_or_404(Book, slug=slug)
+    if likes(request.user, book):
+        set_sets(request.user, book, [])
+
+    if request.is_ajax():
+        return JSONResponse({"success": True, "msg": "ok", "like": False})
+    else:
+        return redirect(book)
+
+
+#~ @login_required
+#~ @cache.never_cache
+#~ def user_shelves(request):
+    #~ shelves = models.Tag.objects.filter(category='set', user=request.user)
+    #~ new_set_form = forms.NewSetForm()
+    #~ return render_to_response('social/user_shelves.html', locals(),
+            #~ context_instance=RequestContext(request))
+#~ 
+#~ @cache.never_cache
+#~ def book_sets(request, slug):
+    #~ if not request.user.is_authenticated():
+        #~ return HttpResponse(_('<p>To maintain your shelves you need to be logged in.</p>'))
+#~ 
+    #~ book = get_object_or_404(models.Book, slug=slug)
+#~ 
+    #~ user_sets = models.Tag.objects.filter(category='set', user=request.user)
+    #~ book_sets = book.tags.filter(category='set', user=request.user)
+#~ 
+    #~ if request.method == 'POST':
+        #~ form = forms.ObjectSetsForm(book, request.user, request.POST)
+        #~ if form.is_valid():
+            #~ DONE!
+            #~ if request.is_ajax():
+                #~ return JSONResponse('{"msg":"'+_("<p>Shelves were sucessfully saved.</p>")+'", "after":"close"}')
+            #~ else:
+                #~ return HttpResponseRedirect('/')
+    #~ else:
+        #~ form = forms.ObjectSetsForm(book, request.user)
+        #~ new_set_form = forms.NewSetForm()
+#~ 
+    #~ return render_to_response('social/book_sets.html', locals(),
+        #~ context_instance=RequestContext(request))
+#~ 
+#~ 
+#~ @login_required
+#~ @require_POST
+#~ @cache.never_cache
+#~ def remove_from_shelf(request, shelf, slug):
+    #~ book = get_object_or_404(models.Book, slug=slug)
+#~ 
+    #~ shelf = get_object_or_404(models.Tag, slug=shelf, category='set', user=request.user)
+#~ 
+    #~ if shelf in book.tags:
+        #~ models.Tag.objects.remove_tag(book, shelf)
+        #~ touch_tag(shelf)
+#~ 
+        #~ return HttpResponse(_('Book was successfully removed from the shelf'))
+    #~ else:
+        #~ return HttpResponse(_('This book is not on the shelf'))
+#~ 
+#~ 
+#~ def collect_books(books):
+    #~ """
+    #~ Returns all real books in collection.
+    #~ """
+    #~ result = []
+    #~ for book in books:
+        #~ if len(book.children.all()) == 0:
+            #~ result.append(book)
+        #~ else:
+            #~ result += collect_books(book.children.all())
+    #~ return result
+#~ 
+#~ 
+#~ @cache.never_cache
+#~ def download_shelf(request, slug):
+    #~ """"
+    #~ Create a ZIP archive on disk and transmit it in chunks of 8KB,
+    #~ without loading the whole file into memory. A similar approach can
+    #~ be used for large dynamic PDF files.
+    #~ """
+    #~ from slughifi import slughifi
+    #~ import tempfile
+    #~ import zipfile
+#~ 
+    #~ shelf = get_object_or_404(models.Tag, slug=slug, category='set')
+#~ 
+    #~ formats = []
+    #~ form = forms.DownloadFormatsForm(request.GET)
+    #~ if form.is_valid():
+        #~ formats = form.cleaned_data['formats']
+    #~ if len(formats) == 0:
+        #~ formats = models.Book.ebook_formats
+#~ 
+    #~ # Create a ZIP archive
+    #~ temp = tempfile.TemporaryFile()
+    #~ archive = zipfile.ZipFile(temp, 'w')
+#~ 
+    #~ for book in collect_books(models.Book.tagged.with_all(shelf)):
+        #~ for ebook_format in models.Book.ebook_formats:
+            #~ if ebook_format in formats and book.has_media(ebook_format):
+                #~ filename = book.get_media(ebook_format).path
+                #~ archive.write(filename, str('%s.%s' % (book.slug, ebook_format)))
+    #~ archive.close()
+#~ 
+    #~ response = HttpResponse(content_type='application/zip', mimetype='application/x-zip-compressed')
+    #~ response['Content-Disposition'] = 'attachment; filename=%s.zip' % slughifi(shelf.name)
+    #~ response['Content-Length'] = temp.tell()
+#~ 
+    #~ temp.seek(0)
+    #~ response.write(temp.read())
+    #~ return response
+#~ 
+#~ 
+#~ @cache.never_cache
+#~ def shelf_book_formats(request, shelf):
+    #~ """"
+    #~ Returns a list of formats of books in shelf.
+    #~ """
+    #~ shelf = get_object_or_404(models.Tag, slug=shelf, category='set')
+#~ 
+    #~ formats = {}
+    #~ for ebook_format in models.Book.ebook_formats:
+        #~ formats[ebook_format] = False
+#~ 
+    #~ for book in collect_books(models.Book.tagged.with_all(shelf)):
+        #~ for ebook_format in models.Book.ebook_formats:
+            #~ if book.has_media(ebook_format):
+                #~ formats[ebook_format] = True
+#~ 
+    #~ return HttpResponse(LazyEncoder().encode(formats))
+#~ 
+#~ 
+#~ @login_required
+#~ @require_POST
+#~ @cache.never_cache
+#~ def new_set(request):
+    #~ new_set_form = forms.NewSetForm(request.POST)
+    #~ if new_set_form.is_valid():
+        #~ new_set = new_set_form.save(request.user)
+#~ 
+        #~ if request.is_ajax():
+            #~ return JSONResponse('{"id":"%d", "name":"%s", "msg":"<p>Shelf <strong>%s</strong> was successfully created</p>"}' % (new_set.id, new_set.name, new_set))
+        #~ else:
+            #~ return HttpResponseRedirect('/')
+#~ 
+    #~ return HttpResponseRedirect('/')
+#~ 
+#~ 
+#~ @login_required
+#~ @require_POST
+#~ @cache.never_cache
+#~ def delete_shelf(request, slug):
+    #~ user_set = get_object_or_404(models.Tag, slug=slug, category='set', user=request.user)
+    #~ user_set.delete()
+#~ 
+    #~ if request.is_ajax():
+        #~ return HttpResponse(_('<p>Shelf <strong>%s</strong> was successfully removed</p>') % user_set.name)
+    #~ else:
+        #~ return HttpResponseRedirect('/')
diff --git a/requirements-dev.txt b/requirements-dev.txt
new file mode 100755 (executable)
index 0000000..aec394f
--- /dev/null
@@ -0,0 +1 @@
+django-debug-toolbar
index 81cc393..a961c4d 100644 (file)
@@ -7,6 +7,7 @@ django-pagination>=1.0
 django-rosetta>=0.5.3
 django-maintenancemode>=0.9
 django-piston
+django-jsonfield
 
 python-memcached
 piwik
index a69b050..f93f9e8 100644 (file)
@@ -156,6 +156,7 @@ INSTALLED_APPS = [
     'suggest',
     'picture',
     'search',
+    'social',
 ]
 
 CACHES = {
@@ -165,6 +166,13 @@ CACHES = {
             '127.0.0.1:11211',
         ]
     },
+    'permanent': {
+        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
+        'TIMEOUT': 2419200,
+        'LOCATION': [
+            '127.0.0.1:11211',
+        ]
+    },
     'api': {
         'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
         'LOCATION': path.join(PROJECT_DIR, 'django_cache/'),
index 12adda1..b816fc0 100755 (executable)
@@ -1,5 +1,8 @@
-.book-wide-box, .book-mini-box, .book-box {
+.book-mini-box, .Book-item {
     display: inline-block;
+}
+
+.book-wide-box, .book-box {
     margin: 0;
     vertical-align: top;
 }
@@ -74,6 +77,7 @@
 .book-box-body {
     height: 17em;
     overflow: hidden;
+    position: relative;
 }
 
 .book-wide-box .book-box-body {
     margin-left: 14em;
 }
 
-.book-box-tools a.downarrow:before {
+.book-box-read a:before {
     content: "\2609";
     font-family: WL-Nav;
     font-size: 2.25em;
     margin-right: .15em;
     vertical-align: middle;
+    font-weight: normal;
+}
+
+.book-box-download a:before {
+    content: "\21E9";
+    font-family: WL-Nav;
+    font-size: 2.25em;
+    margin-right: .15em;
+    vertical-align: middle;
+    font-weight: normal;
 }
 
 .book-box-audiobook a:before {
     font-size: 2.25em;
     margin-right: .15em;
     vertical-align: middle;
+    font-weight: normal;
 }
 
 ul.book-box-tools {
@@ -223,3 +238,39 @@ ul.inline-items li {
     margin: 6em 1.5em 0em 1.5em
 }
 
+
+
+
+.star {
+    font-size: 2.25em;
+    margin-right: .5em;
+    position: absolute;
+    right: 0;
+}
+.star button::-moz-focus-inner {
+    padding: 0;
+    border: 0
+}
+.if-unlike button {
+    font-size: 1em;
+    font-family: inherit;
+    border: 0;
+    background: none;
+    margin: 0;
+    padding: 0;
+}
+
+.if-like a {
+    display:block;
+    text-align:right;
+    padding: 0;
+}
+
+.like .if-unlike {
+    display: none;
+}
+
+.unlike .if-like {
+    display: none;
+}
+
index 35136e0..c3b968b 100755 (executable)
@@ -40,6 +40,7 @@
     background-color: transparent;
     margin-top: -0.5em;
     margin-left: 1em;
+    width: 20em;
 }
 
 .dialog-window div.header {
index bf9d94b..0df6508 100755 (executable)
@@ -7,15 +7,11 @@
             $window.attr("id", this.id + "-window");
             $('body').append($window);
 
+            var $trigger = $(this)
             var trigger = '#' + this.id;
 
-            var href = $(this).attr('href');
-            if (href.search('\\?') != -1)
-                href += '&ajax=1';
-            else href += '?ajax=1';
-
             $window.jqm({
-                ajax: href,
+                ajax: '@href',
                 ajaxText: '<p><img src="' + STATIC_URL + 'img/indicator.gif" alt="*"/> ' + gettext("Loading") + '</p>',
                 target: $('.target', $window)[0],
                 overlay: 60,
                 onShow: function(hash) {
                     var offset = $(hash.t).offset();
                     hash.w.css({position: 'absolute', left: offset.left - hash.w.width() + $(hash.t).width(), top: offset.top});
-                    $('.header', hash.w).css({width: $(hash.t).width()});
+                    var width = $(hash.t).width();
+                    width = width > 50 ? width : 50;
+                    $('.header', hash.w).css({width: width});
                     hash.w.show();
                 },
                 onLoad: function(hash) {
-                    $('form', hash.w).each(function() {
-                        if (this.action.search('[\\?&]ajax=1') != -1)
-                            return;
-                        if (this.action.search('\\?') != -1)
-                            this.action += '&ajax=1';
-                        else this.action += '?ajax=1';
-                    });
                     $('form', hash.w).ajaxForm({
                         dataType: 'json',
                         target: $('.target', $window),
@@ -41,6 +32,8 @@
                             if (response.success) {
                                 $('.target', $window).text(response.message);
                                 setTimeout(function() { $window.jqmHide() }, 1000);
+                                callback = ajaxable_callbacks[$trigger.attr('data-callback')];
+                                callback && callback($trigger, response);
                                 if (response.redirect)
                                     window.location = response.redirect;
                             }
         });
 
 
+        var login_and_retry = function($form) {
+            var $window = $("#ajaxable-window").clone();
+            $window.attr("id", "context-login-window");
+            $('body').append($window);
+
+            $window.jqm({
+                ajax: '/uzytkownicy/zaloguj-utworz/',
+                ajaxText: '<p><img src="' + STATIC_URL + 'img/indicator.gif" alt="*"/> ' + gettext("Loading") + '</p>',
+                target: $('.target', $window)[0],
+                overlay: 60,
+                onShow: function(hash) {
+                    var offset = $form.offset();
+                    hash.w.css({position: 'absolute', left: offset.left - hash.w.width() + $form.width(), top: offset.top});
+                    var width = $form.width();
+                    width = width > 50 ? width : 50;
+                    $('.header', hash.w).css({width: width});
+                    hash.w.show();
+                },
+                onLoad: function(hash) {
+                    $('form', hash.w).ajaxForm({
+                        dataType: 'json',
+                        target: $('.target', $window),
+                        success: function(response) {
+                            if (response.success) {
+                                $('.target', $window).text(response.message);
+                                setTimeout(function() { $window.jqmHide() }, 1000);
+                                $form.submit();
+                            }
+                            else {
+                                $('.error', $window).remove();
+                                $.each(response.errors, function(id, errors) {
+                                    $('#id_' + id, $window).before('<span class="error">' + errors[0] + '</span>');
+                                });
+                                $('input[type=submit]', $window).removeAttr('disabled');
+                                return false;
+                            }
+                        }
+                    });
+                }
+            }).jqmShow();
+            
+        };
+
+
+        $('.ajax-form').each(function() {
+            var $form = $(this);
+            $form.ajaxForm({
+                dataType: 'json',
+                beforeSubmit: function() {
+                    $('input[type=submit]', $form)
+                        .attr('disabled', 'disabled')
+                        .after('<img src="/static/img/indicator.gif" style="margin-left: 0.5em"/>');
+                },
+                error: function(response) {
+                        if (response.status == 403)
+                            login_and_retry($form);
+                    },
+                success: function(response) {
+                    if (response.success) {
+                        callback = ajax_form_callbacks[$form.attr('data-callback')];
+                        callback && callback($form, response);
+
+                    } else {
+                        $('span.error', $form).remove();
+                        $.each(response.errors, function(id, errors) {
+                            $('#id_' + id, $form).before('<span class="error">' + errors[0] + '</span>');
+                        });
+                        $('input[type=submit]', $form).removeAttr('disabled');
+                        $('img', $form).remove();
+                    }
+                }
+            });
+        });
+
+
+        var update_star = function($elem, response) {
+            /* updates the star after successful ajax */
+            var $star = $elem.closest('.star');
+            if (response.like) {
+                $star.addClass('like');
+                $star.removeClass('unlike');
+            }
+            else {
+                $star.addClass('unlike');
+                $star.removeClass('like');
+            }
+        };
+
+        var ajax_form_callbacks = {
+            'social-like-book': update_star
+        };
+
+        var ajaxable_callbacks = {
+            'social-book-sets': update_star
+        };
+
+
     });
 })(jQuery)
 
diff --git a/wolnelektury/templates/auth/login_register.html b/wolnelektury/templates/auth/login_register.html
new file mode 100755 (executable)
index 0000000..e262e2e
--- /dev/null
@@ -0,0 +1,18 @@
+{% extends "ajaxable/form.html" %}
+{% load i18n %}
+
+{% block extra %}
+
+
+<h1>{% trans "or register" %}:</h1>
+
+<form action="{% url register %}" method="post" accept-charset="utf-8" class="cuteform">
+<ol>
+    <div id="id_register-__all__"></div>
+    {{ register_form.as_ul }}
+    <li><input type="submit" value="{{ register_submit }}"/></li>
+</ol>
+</form>
+
+
+{% endblock %}
index 23cb46b..8674ee1 100644 (file)
@@ -1,8 +1,8 @@
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 <html xmlns="http://www.w3.org/1999/xhtml">
-       {% load i18n compressed catalogue_tags sponsor_tags %}
-    {% load reporting_stats %}
+       {% load cache compressed i18n %}
+    {% load catalogue_tags reporting_stats sponsor_tags %}
     <head>
         <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
         <meta http-equiv="Content-Style-Type" content="text/css" />
 
             <div id="tagline">
                 <span>
-                {% count_books book_count %}
-                {% url book_list as b %}
-                {% url book_list as r %}
-                {% blocktrans count book_count as c %}
-                <a href='{{b}}'>{{c}}</a> free reading you have <a href='{{r}}'>right to</a>
-                {% plural %}
-                <a href='{{b}}'>{{c}}</a> free readings you have <a href='{{r}}'>right to</a>
-                {% endblocktrans %}
+                {% cache 300 tagline %}
+                    {% url book_list as b %}
+                    {% url book_list as r %}
+                        {% count_books book_count %}
+                    {% blocktrans count book_count as c %}
+                    <a href='{{b}}'>{{c}}</a> free reading you have <a href='{{r}}'>right to</a>
+                    {% plural %}
+                    <a href='{{b}}'>{{c}}</a> free readings you have <a href='{{r}}'>right to</a>
+                    {% endblocktrans %}
+                {% endcache %}
                 </span>
             </div>
 
             <p id="user-info" class="mono">
                 {% if user.is_authenticated %}
                     {% trans "Welcome" %}, <strong>{{ user.username }}</strong>
-                    | <a href="{% url user_shelves %}" id="user-shelves-link">{% trans "Your shelves" %}</a>
+                    | <a href="{% url social_my_shelf %}" id="user-shelves-link">{% trans "My shelf" %}</a>
                     {% if user.is_staff %}
                     | <a href="/admin/">{% trans "Administration" %}</a>
                     {% endif %}
-                    | <a href="{% url logout %}?next={{ request.get_full_path }}">{% trans "Logout" %}</a>
+                    | <a href="{% url logout %}?next={% block logout %}{{ request.get_full_path }}{% endblock %}">{% trans "Logout" %}</a>
                 {% else %}
                     <a href="{% url login %}?next={{ request.path }}"
                         id="login" class="ajaxable">
index 58abaa6..b7d78d7 100755 (executable)
                 {% endthumbnail %}
             " alt="Cover" />
         {% endif %}
-        {% for author in authors %}
-            <div class="desc">
-                <span class="mono author">{{ author }}</span>
-                <span class="title">{{ book.title }}</span>
-            </div>
-        {% endfor %}
+        <div class="desc">
+            <span class="mono author">
+                {% for name, url in related.tags.author %}
+                    {{ name }}{% if not forloop.last %}, {% endif %}
+                {% endfor %}
+            </span>
+            <span class="title">{{ book.title }}</span>
+        </div>
     </a>
 </div>
 
index 7f26237..2c81b2e 100644 (file)
@@ -1,7 +1,10 @@
 {% load i18n %}
+{% load social_tags %}
 {% load thumbnail %}
 <div class="{% block box-class %}book-box{% endblock %}">
-<div class="book-box-inner">
+<div class="book-box-inner" style="position: relative;">
+
+
     <a href="{{ book.get_absolute_url }}">
         {% if book.cover %}
             <img src="
     {% block right-column %}
     {% endblock %}
     <div class="book-box-body">
+
+
+<div class="star {% if not request.user|likes:book %}un{% endif %}like">
+    <div class="if-like" >
+        <a id="social-book-sets-{{ book.slug }}" data-callback='social-book-sets' class='ajaxable' href='{% url social_book_sets book.slug %}'>
+            ★
+        </a>
+    </div>
+    <div class="if-unlike">
+        <form id="social-like-book-{{ book.slug }}" data-callback='social-like-book' method='post' class='ajax-form' action='{% url social_like_book book.slug %}'>
+            <button type='submit'>☆</button>
+        </form>
+    </div>
+</div>
+
+
         <div class="book-box-head">
             <div class="mono author">
-            {% for author in tags.author %}
-                {{ author }}
-            {% endfor %}
+                {% for name, url in related.tags.author %}
+                    {{ name }}{% if not forloop.last %}, {% endif %}
+                {% endfor %}
             </div>
             <div class="title">{{ book.title }}</div>
         </div>
             {% spaceless %}
 
             <span class="mono">{% trans "Epoch" %}:</span>&nbsp;<span class="book-box-tag">
-                {% for tag in tags.epoch %}
-                    <a href="{{ tag.get_absolute_url }}">{{ tag.name }} </a>
+                {% for name, url in related.tags.epoch %}
+                    <a href="{{ url }}">{{ name }} </a>
                 {% endfor %}
             </span>
 
             <span class="mono">{% trans "Kind" %}:</span>&nbsp;<span class="book-box-tag">
-                {% for tag in tags.kind %}
-                    <a href="{{ tag.get_absolute_url }}">{{ tag.name }} </a>
+                {% for name, url in related.tags.kind %}
+                    <a href="{{ url }}">{{ name }} </a>
                 {% endfor %}
             </span>
 
             <span class="mono">{% trans "Genre" %}:</span>&nbsp;<span class="book-box-tag">
-                {% for tag in tags.genre %}
-                    <a href="{{ tag.get_absolute_url }}">{{ tag.name }} </a>
+                {% for name, url in related.tags.genre %}
+                    <a href="{{ url }}">{{ name }} </a>
                 {% endfor %}
             </span>
 
@@ -72,7 +91,7 @@
             </div>
         </li>
         <li class="book-box-audiobook">
-        {% if book.has_mp3_file %}
+        {% if related.media.mp3 or related.media.ogg %}
             <a href="{% url book_player book.slug %}" class="open-player mono downarrow">{% trans "Listen" %}</a>
         {% endif %}
         </li>
index 0506c8e..a13f1a6 100644 (file)
@@ -10,6 +10,7 @@
     Gdy długo spoglądamy w otchłań, otchłań spogląda również w nas.</div>
   </blockquote>
 
+
   <div id="other-tools">
     <h2 class="mono">{% trans "See" %}</h2>
     <ul class="inline-items">
     <h2 class="mono">{% trans "Download" %}</h2>
     <ul class="inline-items">
       <li>
-       {% if has_media.mp3 or has_media.ogg %}
+       {% if related.media.mp3 or related.media.ogg %}
        {% trans "Download all audiobooks for this book" %}: 
-       {% if has_media.mp3 %}<a href="{% url download_zip_mp3 book.slug %}">MP3</a>{% endif %}{% if has_media.mp4 and has_media.ogg %},{% endif %}
-       {% if has_media.ogg %}<a href="{% url download_zip_ogg book.slug %}">OGG</a>{% endif %}.
+       {% if related.media.mp3 %}<a href="{% url download_zip_mp3 book.slug %}">MP3</a>{% endif %}{% if related.media.mp3 and related.media.ogg %},{% endif %}
+       {% if related.media.ogg %}<a href="{% url download_zip_ogg book.slug %}">OGG</a>{% endif %}.
        {% endif %}
       </li>
       <li>
index 7d3642f..21aef82 100644 (file)
@@ -1,6 +1,6 @@
 {% extends "base.html" %}
 {% load i18n %}
-{% load catalogue_tags pagination_tags switch_tag %}
+{% load catalogue_tags switch_tag %}
 
 {% block titleextra %}{% title_from_tags tags %}{% endblock %}
 
@@ -91,7 +91,6 @@
 
 
 
-    {% autopaginate object_list 10 %}
     <div id="books-list">
 
 
         {% endif %}
 
         {% if object_list %}
-            {% spaceless %}
-            <ol class='work-list'>
-            {% for book in object_list %}
-                <li class='work-item'>
-                    {% if user_is_owner %}
-                        <a href="{% url remove_from_shelf last_tag.slug book.slug %}" class="remove-from-shelf">{% trans "Delete" %}</a>
-                    {% endif %}
-                    {{ book.short_html }}</li>
-            {% endfor %}
-            </ol>
-            {% endspaceless %}
-            {% paginate %}
+            {% work_list object_list %}
         {% else %}
             {% trans "Sorry! Search cirteria did not match any resources." %}
             {% include "info/join_us.html" %}
         {% endif %}
         {% endwith %}
     </div>
-       {% if object_list %}
-       {% comment %} If we didn't find anything there will be nothing on the right side as well {% endcomment %}
-       {% endif %}
 {% endblock %}
diff --git a/wolnelektury/templates/catalogue/work-list.html b/wolnelektury/templates/catalogue/work-list.html
new file mode 100755 (executable)
index 0000000..34ecb5f
--- /dev/null
@@ -0,0 +1,18 @@
+{% load pagination_tags %}
+{% load book_short from catalogue_tags %}
+
+{% autopaginate object_list 10 %}
+{% spaceless %}
+<ol class='work-list'>
+{% for item in object_list %}
+    <li class='{{ object_type }}-item'>
+        {% if object_type == 'Book' %}
+            {% book_short item %}
+        {% else %}
+            {{ item.short_html }}
+        {% endif %}
+    </li>
+{% endfor %}
+</ol>
+{% endspaceless %}
+{% paginate %}
\ No newline at end of file
index b0de3cf..a879b0d 100755 (executable)
 
 
     <h2 class="main-last"><span class="mono">Ostatnie publikacje</span></h2>
-        {% for book in last_published %}
-            {{ book.mini_box }}
-        {% endfor %}
+        {% cache 300 last-published-on-main %}
+            {% for book in last_published %}
+                {% book_mini book %}
+            {% endfor %}
+        {% endcache %}
 
     <div class="clearboth"></div>
 
@@ -66,8 +68,9 @@
 
     <div class="infopages-box">
         <h2><span class='mono'>Informacje</span></h2>
-
-        {% infopages_on_main %}
+        {% cache 300 infopages-on-main LANGUAGE_CODE %}
+            {% infopages_on_main %}
+        {% endcache %}
 
         <div class="social-links">
             <a href="http://pl-pl.facebook.com/pages/Wolne-Lektury/203084073268"><img src="{{ STATIC_URL }}img/social/facebook.png" alt="WolneLektury @ Facebook" /></a>
index 90895a5..f6cd8d9 100644 (file)
@@ -19,6 +19,7 @@ urlpatterns = patterns('wolnelektury.views',
     url(r'^uzytkownicy/zaloguj/$', views.LoginFormView(), name='login'),
     url(r'^uzytkownicy/utworz/$', views.RegisterFormView(), name='register'),
     url(r'^uzytkownicy/wyloguj/$', 'logout_then_redirect', name='logout'),
+    url(r'^uzytkownicy/zaloguj-utworz/$', views.LoginRegisterFormView(), name='login_register'),
 )
 
 urlpatterns += patterns('',
@@ -30,6 +31,7 @@ urlpatterns += patterns('',
     url(r'^przypisy/', include('dictionary.urls')),
     url(r'^raporty/', include('reporting.urls')),
     url(r'^info/', include('infopages.urls')),
+    url(r'^ludzie/', include('social.urls')),
 
     # Admin panel
     url(r'^admin/catalogue/book/import$', 'catalogue.views.import_book', name='import_book'),
index c594732..8732079 100755 (executable)
@@ -43,6 +43,7 @@ class RegisterFormView(AjaxableFormView):
     title = _('Register')
     submit = _('Register')
     ajax_redirect = True
+    form_prefix = 'register'
 
     def __call__(self, request):
         if request.user.is_authenticated():
@@ -58,6 +59,16 @@ class RegisterFormView(AjaxableFormView):
         auth.login(request, user)
 
 
+class LoginRegisterFormView(LoginFormView):
+    template = 'auth/login_register.html'
+
+    def extra_context(self):
+        return {
+            "register_form": UserCreationForm(prefix='register'),
+            "register_submit": _('Register'),
+        }
+
+
 @never_cache
 def logout_then_redirect(request):
     auth.logout(request)