From: Radek Czajka Date: Thu, 19 Jan 2012 14:21:59 +0000 (+0100) Subject: stars and tags instead of shelves, move to social app X-Git-Url: https://git.mdrn.pl/wolnelektury.git/commitdiff_plain/3badd77f743883992829a1174eef7c8d5e851766?ds=sidebyside;hp=73ce961f14509aabfa26536f847afd28111029c6 stars and tags instead of shelves, move to social app 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 --- diff --git a/apps/ajaxable/templates/ajaxable/form.html b/apps/ajaxable/templates/ajaxable/form.html index ba79e4b69..1658b9be8 100755 --- a/apps/ajaxable/templates/ajaxable/form.html +++ b/apps/ajaxable/templates/ajaxable/form.html @@ -3,8 +3,10 @@
    -
    +
    {{ form.as_ul }}
-
\ No newline at end of file + + +{% block extra %}{% endblock %} \ No newline at end of file diff --git a/apps/ajaxable/utils.py b/apps/ajaxable/utils.py index d6f70501d..e32356aee 100755 --- a/apps/ajaxable/utils.py +++ b/apps/ajaxable/utils.py @@ -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. diff --git a/apps/catalogue/fields.py b/apps/catalogue/fields.py index e19df9d37..390fb0359 100644 --- a/apps/catalogue/fields.py +++ b/apps/catalogue/fields.py @@ -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''' - - ''' % { - '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. diff --git a/apps/catalogue/forms.py b/apps/catalogue/forms.py index 655f1eccd..75d9ab997 100644 --- a/apps/catalogue/forms.py +++ b/apps/catalogue/forms.py @@ -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 index 000000000..a46e34d0e --- /dev/null +++ b/apps/catalogue/migrations/0025_auto__add_field_book__related_info.py @@ -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'] diff --git a/apps/catalogue/models.py b/apps/catalogue/models.py index 9a1e71ad0..1f6210d20 100644 --- a/apps/catalogue/models.py +++ b/apps/catalogue/models.py @@ -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) diff --git a/apps/catalogue/templatetags/catalogue_tags.py b/apps/catalogue/templatetags/catalogue_tags.py index df938a624..eeba74ee3 100644 --- a/apps/catalogue/templatetags/catalogue_tags.py +++ b/apps/catalogue/templatetags/catalogue_tags.py @@ -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 diff --git a/apps/catalogue/urls.py b/apps/catalogue/urls.py index db044fc1c..88e622678 100644 --- a/apps/catalogue/urls.py +++ b/apps/catalogue/urls.py @@ -18,17 +18,11 @@ urlpatterns = patterns('picture.views', ) + \ patterns('catalogue.views', url(r'^$', 'catalogue', name='catalogue'), - url(r'^polki/(?P[a-zA-Z0-9-]+)/formaty/$', 'shelf_book_formats', name='shelf_book_formats'), - url(r'^polki/(?P[a-zA-Z0-9-]+)/(?P%s)/usun$' % SLUG, 'remove_from_shelf', name='remove_from_shelf'), - url(r'^polki/$', 'user_shelves', name='user_shelves'), - url(r'^polki/(?P[a-zA-Z0-9-]+)/usun/$', 'delete_shelf', name='delete_shelf'), - url(r'^polki/(?P[a-zA-Z0-9-]+)\.zip$', 'download_shelf', name='download_shelf'), + url(r'^lektury/$', 'book_list', name='book_list'), url(r'^lektury/(?P[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%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'), diff --git a/apps/catalogue/views.py b/apps/catalogue/views.py index f57797da1..eb0e7b95a 100644 --- a/apps/catalogue/views.py +++ b/apps/catalogue/views.py @@ -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(_('

To maintain your shelves you need to be logged in.

')) - - 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":"'+_("

Shelves were sucessfully saved.

")+'", "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":"

Shelf %s was successfully created

"}' % (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(_('

Shelf %s was successfully removed

') % user_set.name) - else: - return HttpResponseRedirect('/') - # ========= # = Admin = diff --git a/apps/dictionary/views.py b/apps/dictionary/views.py index 7b9cd5313..47204fd42 100755 --- a/apps/dictionary/views.py +++ b/apps/dictionary/views.py @@ -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): diff --git a/apps/search/context_processors.py b/apps/search/context_processors.py index c54525aec..cfb2f2eb7 100644 --- a/apps/search/context_processors.py +++ b/apps/search/context_processors.py @@ -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 index 000000000..680e618d9 --- /dev/null +++ b/apps/search/fields.py @@ -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''' + + ''' % { + '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 index 000000000..e7051b86e --- /dev/null +++ b/apps/search/forms.py @@ -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 index 000000000..e69de29bb diff --git a/apps/social/forms.py b/apps/social/forms.py new file mode 100755 index 000000000..bbdc43ce5 --- /dev/null +++ b/apps/social/forms.py @@ -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 index 000000000..e69de29bb diff --git a/apps/social/models.py b/apps/social/models.py new file mode 100644 index 000000000..71a836239 --- /dev/null +++ b/apps/social/models.py @@ -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 index 000000000..c465ab992 --- /dev/null +++ b/apps/social/templates/social/my_shelf.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} +{% load i18n %} +{% load catalogue_tags %} + +{% block titleextra %}{% trans "My shelf" %}{% endblock %} + +{% block logout %}/{% endblock %} + +{% block body %} + +

{% trans "My shelf" %}

+ + {% 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 index 000000000..eff951e13 --- /dev/null +++ b/apps/social/templates/social/sets_form.html @@ -0,0 +1,14 @@ +{% load i18n %} +

{{ title }}

+ +
+ +
+ +
+
    +
    + {{ form.as_ul }} +
  1. +
+
\ No newline at end of file diff --git a/apps/social/templatetags/__init__.py b/apps/social/templatetags/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/apps/social/templatetags/social_tags.py b/apps/social/templatetags/social_tags.py new file mode 100755 index 000000000..1b26d91c4 --- /dev/null +++ b/apps/social/templatetags/social_tags.py @@ -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 index 000000000..9e6de00b1 --- /dev/null +++ b/apps/social/urls.py @@ -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[a-z0-9-]+)/lubie/$', 'like_book', name='social_like_book'), + url(r'^lektura/(?P[a-z0-9-]+)/nie_lubie/$', 'unlike_book', name='social_unlike_book'), + url(r'^lektura/(?P[a-z0-9-]+)/polki/$', ObjectSetsFormView(), name='social_book_sets'), + url(r'^polka/$', 'my_shelf', name='social_my_shelf'), + + #~ url(r'^polki/(?P[a-zA-Z0-9-]+)/formaty/$', 'shelf_book_formats', name='shelf_book_formats'), + #~ url(r'^polki/(?P[a-zA-Z0-9-]+)/(?P%s)/usun$' % SLUG, 'remove_from_shelf', name='remove_from_shelf'), + #~ url(r'^polki/$', 'user_shelves', name='user_shelves'), + #~ url(r'^polki/(?P[a-zA-Z0-9-]+)/usun/$', 'delete_shelf', name='delete_shelf'), + #~ url(r'^polki/(?P[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 index 000000000..96f0ca0c7 --- /dev/null +++ b/apps/social/utils.py @@ -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 index 000000000..6ded289b2 --- /dev/null +++ b/apps/social/views.py @@ -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(_('

To maintain your shelves you need to be logged in.

')) +#~ + #~ 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":"'+_("

Shelves were sucessfully saved.

")+'", "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":"

Shelf %s was successfully created

"}' % (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(_('

Shelf %s was successfully removed

') % user_set.name) + #~ else: + #~ return HttpResponseRedirect('/') diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100755 index 000000000..aec394f4b --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1 @@ +django-debug-toolbar diff --git a/requirements.txt b/requirements.txt index 81cc3936a..a961c4d32 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,7 @@ django-pagination>=1.0 django-rosetta>=0.5.3 django-maintenancemode>=0.9 django-piston +django-jsonfield python-memcached piwik diff --git a/wolnelektury/settings.py b/wolnelektury/settings.py index a69b0508e..f93f9e8ac 100644 --- a/wolnelektury/settings.py +++ b/wolnelektury/settings.py @@ -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/'), diff --git a/wolnelektury/static/css/book_box.css b/wolnelektury/static/css/book_box.css index 12adda1d9..b816fc01a 100755 --- a/wolnelektury/static/css/book_box.css +++ b/wolnelektury/static/css/book_box.css @@ -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 { @@ -145,12 +149,22 @@ 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 { @@ -159,6 +173,7 @@ 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; +} + diff --git a/wolnelektury/static/css/dialogs.css b/wolnelektury/static/css/dialogs.css index 35136e0ca..c3b968bb5 100755 --- a/wolnelektury/static/css/dialogs.css +++ b/wolnelektury/static/css/dialogs.css @@ -40,6 +40,7 @@ background-color: transparent; margin-top: -0.5em; margin-left: 1em; + width: 20em; } .dialog-window div.header { diff --git a/wolnelektury/static/js/dialogs.js b/wolnelektury/static/js/dialogs.js index bf9d94b54..0df6508eb 100755 --- a/wolnelektury/static/js/dialogs.js +++ b/wolnelektury/static/js/dialogs.js @@ -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: '

* ' + gettext("Loading") + '

', target: $('.target', $window)[0], overlay: 60, @@ -23,17 +19,12 @@ 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; } @@ -59,6 +52,103 @@ }); + 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: '

* ' + gettext("Loading") + '

', + 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('' + errors[0] + ''); + }); + $('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(''); + }, + 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('' + errors[0] + ''); + }); + $('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 index 000000000..e262e2e82 --- /dev/null +++ b/wolnelektury/templates/auth/login_register.html @@ -0,0 +1,18 @@ +{% extends "ajaxable/form.html" %} +{% load i18n %} + +{% block extra %} + + +

{% trans "or register" %}:

+ +
+
    +
    + {{ register_form.as_ul }} +
  1. +
+
+ + +{% endblock %} diff --git a/wolnelektury/templates/base.html b/wolnelektury/templates/base.html index 23cb46b56..8674ee16f 100644 --- a/wolnelektury/templates/base.html +++ b/wolnelektury/templates/base.html @@ -1,8 +1,8 @@ - {% load i18n compressed catalogue_tags sponsor_tags %} - {% load reporting_stats %} + {% load cache compressed i18n %} + {% load catalogue_tags reporting_stats sponsor_tags %} @@ -31,25 +31,27 @@
- {% count_books book_count %} - {% url book_list as b %} - {% url book_list as r %} - {% blocktrans count book_count as c %} - {{c}} free reading you have right to - {% plural %} - {{c}} free readings you have right to - {% endblocktrans %} + {% cache 300 tagline %} + {% url book_list as b %} + {% url book_list as r %} + {% count_books book_count %} + {% blocktrans count book_count as c %} + {{c}} free reading you have right to + {% plural %} + {{c}} free readings you have right to + {% endblocktrans %} + {% endcache %}

{% if user.is_authenticated %} {% trans "Welcome" %}, {{ user.username }} - | {% trans "Your shelves" %} + | {% trans "My shelf" %} {% if user.is_staff %} | {% trans "Administration" %} {% endif %} - | {% trans "Logout" %} + | {% trans "Logout" %} {% else %} diff --git a/wolnelektury/templates/catalogue/book_mini_box.html b/wolnelektury/templates/catalogue/book_mini_box.html index 58abaa61d..b7d78d7e9 100755 --- a/wolnelektury/templates/catalogue/book_mini_box.html +++ b/wolnelektury/templates/catalogue/book_mini_box.html @@ -10,12 +10,14 @@ {% endthumbnail %} " alt="Cover" /> {% endif %} - {% for author in authors %} -

- {% endfor %} +
+ + {% for name, url in related.tags.author %} + {{ name }}{% if not forloop.last %}, {% endif %} + {% endfor %} + + {{ book.title }} +
diff --git a/wolnelektury/templates/catalogue/book_short.html b/wolnelektury/templates/catalogue/book_short.html index 7f262374d..2c81b2e98 100644 --- a/wolnelektury/templates/catalogue/book_short.html +++ b/wolnelektury/templates/catalogue/book_short.html @@ -1,7 +1,10 @@ {% load i18n %} +{% load social_tags %} {% load thumbnail %}
-
+
+ + {% if book.cover %} + + +
+ +
+
+ +
+
+
+ +
- {% for author in tags.author %} - {{ author }} - {% endfor %} + {% for name, url in related.tags.author %} + {{ name }}{% if not forloop.last %}, {% endif %} + {% endfor %}
{{ book.title }}
@@ -28,20 +47,20 @@ {% spaceless %} {% trans "Epoch" %}:  - {% for tag in tags.epoch %} - {{ tag.name }} + {% for name, url in related.tags.epoch %} + {{ name }} {% endfor %} {% trans "Kind" %}:  - {% for tag in tags.kind %} - {{ tag.name }} + {% for name, url in related.tags.kind %} + {{ name }} {% endfor %} {% trans "Genre" %}:  - {% for tag in tags.genre %} - {{ tag.name }} + {% for name, url in related.tags.genre %} + {{ name }} {% endfor %} @@ -72,7 +91,7 @@
  • - {% if book.has_mp3_file %} + {% if related.media.mp3 or related.media.ogg %} {% trans "Listen" %} {% endif %}
  • diff --git a/wolnelektury/templates/catalogue/book_wide.html b/wolnelektury/templates/catalogue/book_wide.html index 0506c8ec8..a13f1a634 100644 --- a/wolnelektury/templates/catalogue/book_wide.html +++ b/wolnelektury/templates/catalogue/book_wide.html @@ -10,6 +10,7 @@ Gdy długo spoglądamy w otchłań, otchłań spogląda również w nas.
    +

    {% trans "See" %}

      @@ -31,10 +32,10 @@

      {% trans "Download" %}

      • - {% 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 %}MP3{% endif %}{% if has_media.mp4 and has_media.ogg %},{% endif %} - {% if has_media.ogg %}OGG{% endif %}. + {% if related.media.mp3 %}MP3{% endif %}{% if related.media.mp3 and related.media.ogg %},{% endif %} + {% if related.media.ogg %}OGG{% endif %}. {% endif %}
      • diff --git a/wolnelektury/templates/catalogue/tagged_object_list.html b/wolnelektury/templates/catalogue/tagged_object_list.html index 7d3642fa6..21aef82ce 100644 --- a/wolnelektury/templates/catalogue/tagged_object_list.html +++ b/wolnelektury/templates/catalogue/tagged_object_list.html @@ -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 %}
        @@ -129,25 +128,11 @@ {% endif %} {% if object_list %} - {% spaceless %} -
          - {% for book in object_list %} -
        1. - {% if user_is_owner %} - {% trans "Delete" %} - {% endif %} - {{ book.short_html }}
        2. - {% endfor %} -
        - {% endspaceless %} - {% paginate %} + {% work_list object_list %} {% else %} {% trans "Sorry! Search cirteria did not match any resources." %} {% include "info/join_us.html" %} {% endif %} {% endwith %}
        - {% 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 index 000000000..34ecb5f0a --- /dev/null +++ b/wolnelektury/templates/catalogue/work-list.html @@ -0,0 +1,18 @@ +{% load pagination_tags %} +{% load book_short from catalogue_tags %} + +{% autopaginate object_list 10 %} +{% spaceless %} +
          +{% for item in object_list %} +
        1. + {% if object_type == 'Book' %} + {% book_short item %} + {% else %} + {{ item.short_html }} + {% endif %} +
        2. +{% endfor %} +
        +{% endspaceless %} +{% paginate %} \ No newline at end of file diff --git a/wolnelektury/templates/main_page.html b/wolnelektury/templates/main_page.html index b0de3cf0b..a879b0dcb 100755 --- a/wolnelektury/templates/main_page.html +++ b/wolnelektury/templates/main_page.html @@ -35,9 +35,11 @@

        Ostatnie publikacje

        - {% 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 %}
        @@ -66,8 +68,9 @@

        Informacje

        - - {% infopages_on_main %} + {% cache 300 infopages-on-main LANGUAGE_CODE %} + {% infopages_on_main %} + {% endcache %}