From: Radek Czajka Date: Mon, 24 Oct 2011 11:32:12 +0000 (+0200) Subject: #880: hide copyrighted stuff, X-Git-Url: https://git.mdrn.pl/redakcja.git/commitdiff_plain/4395564bb09e6b7d74cae3421f6342dd546b5619?ds=inline #880: hide copyrighted stuff, #1777: nicer book create form, USE_CELERY option, some cleaning --- diff --git a/apps/catalogue/forms.py b/apps/catalogue/forms.py index 8cd00c2d..ad1e4d64 100644 --- a/apps/catalogue/forms.py +++ b/apps/catalogue/forms.py @@ -20,8 +20,13 @@ class DocumentCreateForm(forms.ModelForm): class Meta: model = Book - exclude = ['gallery', 'parent', 'parent_number'] - prepopulated_fields = {'slug': ['title']} + exclude = ['parent', 'parent_number'] + + def __init__(self, *args, **kwargs): + super(DocumentCreateForm, self).__init__(*args, **kwargs) + self.fields['slug'].widget.attrs={'class': 'autoslug'} + self.fields['gallery'].widget.attrs={'class': 'autoslug'} + self.fields['title'].widget.attrs={'class': 'autoslug-source'} def clean(self): super(DocumentCreateForm, self).clean() @@ -31,10 +36,10 @@ class DocumentCreateForm(forms.ModelForm): try: self.cleaned_data['text'] = file.read().decode('utf-8') except UnicodeDecodeError: - raise forms.ValidationError("Text file must be UTF-8 encoded.") + raise forms.ValidationError(_("Text file must be UTF-8 encoded.")) if not self.cleaned_data["text"]: - raise forms.ValidationError("You must either enter text or upload a file") + self._errors["file"] = self.error_class([_("You must either enter text or upload a file")]) return self.cleaned_data diff --git a/apps/catalogue/locale/pl/LC_MESSAGES/django.mo b/apps/catalogue/locale/pl/LC_MESSAGES/django.mo index 96e4b32e..b05c6cfa 100644 Binary files a/apps/catalogue/locale/pl/LC_MESSAGES/django.mo and b/apps/catalogue/locale/pl/LC_MESSAGES/django.mo differ diff --git a/apps/catalogue/locale/pl/LC_MESSAGES/django.po b/apps/catalogue/locale/pl/LC_MESSAGES/django.po index 22708f4b..293fd449 100644 --- a/apps/catalogue/locale/pl/LC_MESSAGES/django.po +++ b/apps/catalogue/locale/pl/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Platforma Redakcyjna\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2011-10-21 15:53+0200\n" -"PO-Revision-Date: 2011-10-21 15:53+0100\n" +"POT-Creation-Date: 2011-10-24 13:25+0200\n" +"PO-Revision-Date: 2011-10-24 13:28+0100\n" "Last-Translator: Radek Czajka \n" "Language-Team: Fundacja Nowoczesna Polska \n" "Language: pl\n" @@ -17,109 +17,122 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n" -#: forms.py:46 +#: forms.py:39 +#, fuzzy +msgid "Text file must be UTF-8 encoded." +msgstr "Plik powinien mieć kodowanie UTF-8." + +#: forms.py:42 +msgid "You must either enter text or upload a file" +msgstr "Proszę wpisać tekst albo wybrać plik do załadowania" + +#: forms.py:51 msgid "ZIP file" msgstr "Plik ZIP" -#: forms.py:47 +#: forms.py:52 msgid "Directories are documents in chunks" msgstr "Katalogi zawierają dokumenty w częściach" -#: forms.py:71 +#: forms.py:76 msgid "Assigned to" msgstr "Przypisane do" -#: forms.py:91 -#: forms.py:105 +#: forms.py:96 +#: forms.py:110 msgid "Chunk with this slug already exists" msgstr "Część z tym slugiem już istnieje" -#: forms.py:114 +#: forms.py:119 msgid "Append to" msgstr "Dołącz do" -#: views.py:148 +#: views.py:149 #, python-format msgid "Slug already used for %s" msgstr "Slug taki sam jak dla pliku %s" -#: views.py:150 +#: views.py:151 msgid "Slug already used in repository." msgstr "Dokument o tym slugu już istnieje w repozytorium." -#: views.py:156 +#: views.py:157 msgid "File should be UTF-8 encoded." msgstr "Plik powinien mieć kodowanie UTF-8." #: models/book.py:25 -#: models/chunk.py:24 +#: models/chunk.py:23 msgid "title" msgstr "tytuł" #: models/book.py:26 -#: models/chunk.py:25 +#: models/chunk.py:24 msgid "slug" msgstr "slug" #: models/book.py:27 +msgid "public" +msgstr "publiczna" + +#: models/book.py:28 msgid "scan gallery name" msgstr "nazwa galerii skanów" -#: models/book.py:30 +#: models/book.py:31 msgid "parent" msgstr "rodzic" -#: models/book.py:31 +#: models/book.py:32 msgid "parent number" msgstr "numeracja rodzica" -#: models/book.py:48 -#: models/chunk.py:22 +#: models/book.py:46 +#: models/chunk.py:21 #: models/publish_log.py:17 msgid "book" msgstr "książka" -#: models/book.py:49 +#: models/book.py:47 msgid "books" msgstr "książki" -#: models/book.py:197 +#: models/book.py:198 msgid "No chunks in the book." msgstr "Książka nie ma części." -#: models/book.py:201 +#: models/book.py:202 msgid "Not all chunks have publishable revisions." msgstr "Niektóre części nie są gotowe do publikacji." -#: models/book.py:207 +#: models/book.py:208 msgid "Invalid XML" msgstr "Nieprawidłowy XML" -#: models/book.py:209 +#: models/book.py:210 msgid "No Dublin Core found." msgstr "Brak sekcji Dublin Core." -#: models/book.py:211 +#: models/book.py:212 msgid "Invalid Dublin Core" msgstr "Nieprawidłowy Dublin Core" -#: models/book.py:214 +#: models/book.py:215 msgid "rdf:about is not" msgstr "rdf:about jest różny od" -#: models/chunk.py:23 +#: models/chunk.py:22 msgid "number" msgstr "numer" -#: models/chunk.py:26 +#: models/chunk.py:25 msgid "gallery start" msgstr "początek galerii" -#: models/chunk.py:41 +#: models/chunk.py:40 msgid "chunk" msgstr "część" -#: models/chunk.py:42 +#: models/chunk.py:41 msgid "chunks" msgstr "części" @@ -234,9 +247,13 @@ msgstr "Ustawienia części" msgid "Book" msgstr "Książka" -#: templates/catalogue/document_create_missing.html:9 -msgid "Create document" -msgstr "Utwórz dokument" +#: templates/catalogue/document_create_missing.html:5 +msgid "Create a new book" +msgstr "Utwórz nową książkę" + +#: templates/catalogue/document_create_missing.html:11 +msgid "Create book" +msgstr "Utwórz książkę" #: templates/catalogue/document_upload.html:8 msgid "Bulk documents upload" diff --git a/apps/catalogue/migrations/0006_auto__add_field_book_public.py b/apps/catalogue/migrations/0006_auto__add_field_book_public.py new file mode 100644 index 00000000..fd1cea56 --- /dev/null +++ b/apps/catalogue/migrations/0006_auto__add_field_book_public.py @@ -0,0 +1,126 @@ +# 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.public' + db.add_column('catalogue_book', 'public', self.gf('django.db.models.fields.BooleanField')(default=True, db_index=True), keep_default=False) + + + def backwards(self, orm): + + # Deleting field 'Book.public' + db.delete_column('catalogue_book', 'public') + + + 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': "['title', 'slug']", 'object_name': 'Book'}, + '_new_publishable': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + '_published': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + '_single': ('django.db.models.fields.NullBooleanField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'gallery': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': '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', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}) + }, + 'catalogue.bookpublishrecord': { + 'Meta': {'ordering': "['-timestamp']", 'object_name': 'BookPublishRecord'}, + 'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.Book']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'catalogue.chunk': { + 'Meta': {'ordering': "['number']", 'unique_together': "[['book', 'number'], ['book', 'slug']]", 'object_name': 'Chunk'}, + '_changed': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + '_hidden': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'book': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Book']"}), + 'creator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'created_chunk'", 'null': 'True', 'to': "orm['auth.User']"}), + 'gallery_start': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'head': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['catalogue.ChunkChange']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'number': ('django.db.models.fields.IntegerField', [], {}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}), + 'stage': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ChunkTag']", 'null': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}) + }, + 'catalogue.chunkchange': { + 'Meta': {'ordering': "('created_at',)", 'unique_together': "(['tree', 'revision'],)", 'object_name': 'ChunkChange'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'author_email': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'author_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}), + 'data': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'merge_parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'merge_children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}), + 'publishable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'revision': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'change_set'", 'symmetrical': 'False', 'to': "orm['catalogue.ChunkTag']"}), + 'tree': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'change_set'", 'to': "orm['catalogue.Chunk']"}) + }, + 'catalogue.chunkpublishrecord': { + 'Meta': {'object_name': 'ChunkPublishRecord'}, + 'book_record': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.BookPublishRecord']"}), + 'change': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.ChunkChange']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'catalogue.chunktag': { + 'Meta': {'ordering': "['ordering']", 'object_name': 'ChunkTag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'ordering': ('django.db.models.fields.IntegerField', [], {}), + 'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}) + }, + '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/book.py b/apps/catalogue/models/book.py index 5125532c..94a98335 100755 --- a/apps/catalogue/models/book.py +++ b/apps/catalogue/models/book.py @@ -24,6 +24,7 @@ class Book(models.Model): title = models.CharField(_('title'), max_length=255, db_index=True) slug = models.SlugField(_('slug'), max_length=128, unique=True, db_index=True) + public = models.BooleanField(_('public'), default=True, db_index=True) gallery = models.CharField(_('scan gallery name'), max_length=255, blank=True) #wl_slug = models.CharField(_('title'), max_length=255, null=True, db_index=True, editable=False) @@ -36,9 +37,6 @@ class Book(models.Model): _new_publishable = models.NullBooleanField(editable=False) _published = models.NullBooleanField(editable=False) - # Managers - objects = models.Manager() - class NoTextError(BaseException): pass @@ -78,6 +76,9 @@ class Book(models.Model): # Creating & manipulating # ======================= + def accessible(self, request): + return self.public or request.user.is_authenticated() + @classmethod def create(cls, creator, text, *args, **kwargs): b = cls.objects.create(*args, **kwargs) diff --git a/apps/catalogue/models/chunk.py b/apps/catalogue/models/chunk.py index 9b1dcbae..770ddbf6 100755 --- a/apps/catalogue/models/chunk.py +++ b/apps/catalogue/models/chunk.py @@ -11,7 +11,6 @@ from django.utils.translation import ugettext_lazy as _ from catalogue.helpers import cached_in_field from catalogue.managers import VisibleManager from catalogue.tasks import refresh_instance -from catalogue.xml_tools import GradedText from dvcs import models as dvcs_models @@ -124,6 +123,3 @@ class Chunk(dvcs_models.Document): self.changed self.hidden self.short_html - - def graded(self, master=None): - return GradedText(self.materialize(), master) diff --git a/apps/catalogue/tasks.py b/apps/catalogue/tasks.py index 3e230379..53b867b4 100644 --- a/apps/catalogue/tasks.py +++ b/apps/catalogue/tasks.py @@ -1,20 +1,42 @@ from celery.task import task +from django.utils import translation +from django.conf import settings @task -def refresh_by_pk(cls, pk): - cls._default_manager.get(pk=pk).refresh() - +def _refresh_by_pk(cls, pk, language=None): + prev_language = translation.get_language() + language and translation.activate(language) + try: + cls._default_manager.get(pk=pk).refresh() + finally: + translation.activate(prev_language) -def refresh_instance(instance): - refresh_by_pk.delay(type(instance), instance.pk) +if settings.USE_CELERY: + def refresh_instance(instance): + _refresh_by_pk.delay(type(instance), instance.pk, translation.get_language()) +else: + def refresh_instance(instance): + instance.refresh() @task -def publishable_error(book): +def _publishable_error(book, language=None): + prev_language = translation.get_language() + language and translation.activate(language) try: - book.assert_publishable() + return book.assert_publishable() except AssertionError, e: return e else: return None + finally: + translation.activate(prev_language) + +if settings.USE_CELERY: + def publishable_error(book): + task = _publishable_error.delay(book, translation.get_language()) + return task.wait() +else: + def publishable_error(book): + return _publishable_error(book) diff --git a/apps/catalogue/templates/catalogue/document_create_missing.html b/apps/catalogue/templates/catalogue/document_create_missing.html index dcb61752..47c99f98 100644 --- a/apps/catalogue/templates/catalogue/document_create_missing.html +++ b/apps/catalogue/templates/catalogue/document_create_missing.html @@ -1,14 +1,14 @@ {% extends "catalogue/base.html" %} {% load i18n %} -{% block leftcolumn %} -
- {% csrf_token %} - {{ form.as_p }} - -

-
-{% endblock leftcolumn %} +{% block content %} +

{% trans "Create a new book" %}

-{% block rightcolumn %} -{% endblock rightcolumn %} +
+ {% csrf_token %} + + {{ form.as_table}} + +
+
+{% endblock content %} diff --git a/apps/catalogue/templatetags/book_list.py b/apps/catalogue/templatetags/book_list.py index aee33210..f7e70474 100755 --- a/apps/catalogue/templatetags/book_list.py +++ b/apps/catalogue/templatetags/book_list.py @@ -103,6 +103,9 @@ def document_list_filter(request, **kwargs): chunks = chunks.order_by('book__title', 'book', 'number') + if not request.user.is_authenticated(): + chunks = chunks.filter(book__public=True) + state = arg_or_GET('status') if state in _states_dict: chunks = chunks.filter(_states_dict[state]) diff --git a/apps/catalogue/views.py b/apps/catalogue/views.py index 6fb5f60b..3459fdaf 100644 --- a/apps/catalogue/views.py +++ b/apps/catalogue/views.py @@ -11,7 +11,7 @@ from django.contrib.auth.decorators import login_required, permission_required from django.core.urlresolvers import reverse from django.db.models import Count, Q from django import http -from django.http import Http404 +from django.http import Http404, HttpResponseForbidden from django.shortcuts import get_object_or_404, render from django.utils.encoding import iri_to_uri from django.utils.http import urlquote_plus @@ -28,7 +28,6 @@ from catalogue import helpers from catalogue.helpers import active_tab from catalogue.models import Book, Chunk, BookPublishRecord, ChunkPublishRecord from catalogue.tasks import publishable_error -from catalogue import xml_tools # # Quick hack around caching problems, TODO: use ETags @@ -101,13 +100,15 @@ def create_missing(request, slug=None): creator=creator, slug=form.cleaned_data['slug'], title=form.cleaned_data['title'], + gallery=form.cleaned_data['gallery'], ) - return http.HttpResponseRedirect(reverse("wiki_editor", args=[book.slug])) + return http.HttpResponseRedirect(reverse("catalogue_book", args=[book.slug])) else: form = forms.DocumentCreateForm(initial={ "slug": slug, "title": slug.replace('-', ' ').title(), + "gallery": slug, }) return direct_to_template(request, "catalogue/document_create_missing.html", extra_context={ @@ -185,7 +186,10 @@ def upload(request): @never_cache def book_xml(request, slug): - xml = get_object_or_404(Book, slug=slug).materialize() + book = get_object_or_404(Book, slug=slug) + if not book.accessible(request): + return HttpResponseForbidden("Not authorized.") + xml = book.materialize() response = http.HttpResponse(xml, content_type='application/xml', mimetype='application/wl+xml') response['Content-Disposition'] = 'attachment; filename=%s.xml' % slug @@ -194,7 +198,10 @@ def book_xml(request, slug): @never_cache def book_txt(request, slug): - xml = get_object_or_404(Book, slug=slug).materialize() + book = get_object_or_404(Book, slug=slug) + if not book.accessible(request): + return HttpResponseForbidden("Not authorized.") + xml = book.materialize() output = StringIO() # errors? librarian.text.transform(StringIO(xml), output) @@ -206,7 +213,10 @@ def book_txt(request, slug): @never_cache def book_html(request, slug): - xml = get_object_or_404(Book, slug=slug).materialize() + book = get_object_or_404(Book, slug=slug) + if not book.accessible(request): + return HttpResponseForbidden("Not authorized.") + xml = book.materialize() output = StringIO() # errors? librarian.html.transform(StringIO(xml), output, parse_dublincore=False, @@ -215,18 +225,21 @@ def book_html(request, slug): response = http.HttpResponse(html, content_type='text/html', mimetype='text/html') return response - @never_cache def revision(request, slug, chunk=None): try: doc = Chunk.get(slug, chunk) except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist): raise Http404 + if not doc.book.accessible(request): + return HttpResponseForbidden("Not authorized.") return http.HttpResponse(str(doc.revision())) def book(request, slug): book = get_object_or_404(Book, slug=slug) + if not book.accessible(request): + return HttpResponseForbidden("Not authorized.") if request.user.has_perm('catalogue.change_book'): if request.method == "POST": @@ -241,8 +254,7 @@ def book(request, slug): form = forms.ReadonlyBookForm(instance=book) editable = False - task = publishable_error.delay(book) - publish_error = task.wait() + publish_error = publishable_error(book) publishable = publish_error is None return direct_to_template(request, "catalogue/book_detail.html", extra_context={ @@ -260,6 +272,8 @@ def chunk_add(request, slug, chunk): doc = Chunk.get(slug, chunk) except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist): raise Http404 + if not doc.book.accessible(request): + return HttpResponseForbidden("Not authorized.") if request.method == "POST": form = forms.ChunkAddForm(request.POST, instance=doc) @@ -291,6 +305,9 @@ def chunk_edit(request, slug, chunk): doc = Chunk.get(slug, chunk) except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist): raise Http404 + if not doc.book.accessible(request): + return HttpResponseForbidden("Not authorized.") + if request.method == "POST": form = forms.ChunkForm(request.POST, instance=doc) if form.is_valid(): @@ -318,6 +335,9 @@ def chunk_edit(request, slug, chunk): @permission_required('catalogue.change_book') def book_append(request, slug): book = get_object_or_404(Book, slug=slug) + if not book.accessible(request): + return HttpResponseForbidden("Not authorized.") + if request.method == "POST": form = forms.BookAppendForm(book, request.POST) if form.is_valid(): @@ -338,6 +358,9 @@ def book_append(request, slug): @login_required def publish(request, slug): book = get_object_or_404(Book, slug=slug) + if not book.accessible(request): + return HttpResponseForbidden("Not authorized.") + try: book.publish(request.user) except NotAuthorizedError: diff --git a/apps/catalogue/xml_tools.py b/apps/catalogue/xml_tools.py index d6a9333b..242714b6 100644 --- a/apps/catalogue/xml_tools.py +++ b/apps/catalogue/xml_tools.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- from copy import deepcopy -from functools import wraps import re from lxml import etree @@ -14,110 +13,6 @@ class ParseError(BaseException): pass -def obj_memoized(f): - """ - A decorator that caches return value of object methods. - The cache is kept with the object, in a _obj_memoized property. - """ - @wraps(f) - def wrapper(self, *args, **kwargs): - if not hasattr(self, '_obj_memoized'): - self._obj_memoized = {} - key = (f.__name__,) + args + tuple(sorted(kwargs.iteritems())) - try: - return self._obj_memoized[key] - except TypeError: - return f(self, *args, **kwargs) - except KeyError: - self._obj_memoized[key] = f(self, *args, **kwargs) - return self._obj_memoized[key] - return wrapper - - -class GradedText(object): - _edoc = None - - ROOT = 'utwor' - RDF = '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}RDF' - - def __init__(self, text): - self._text = text - - @obj_memoized - def is_xml(self): - """ - Determines if it's a well-formed XML. - - >>> GradedText("").is_xml() - True - >>> GradedText("").is_xml() - False - """ - try: - self._edoc = etree.fromstring(self._text) - except etree.XMLSyntaxError: - return False - return True - - @obj_memoized - def is_wl(self): - """ - Determines if it's an XML with a and a master tag. - - >>> GradedText("").is_wl() - True - >>> GradedText("").is_wl() - False - """ - if self.is_xml(): - e = self._edoc - # FIXME: there could be comments - ret = e.tag == self.ROOT and ( - len(e) == 1 and e[0].tag in MASTERS or - len(e) == 2 and e[0].tag == self.RDF - and e[1].tag in MASTERS) - if ret: - self._master = e[-1].tag - del self._edoc - return ret - else: - return False - - @obj_memoized - def is_broken_wl(self): - """ - Determines if it at least looks like broken WL file - and not just some untagged text. - - >>> GradedText("<").is_broken_wl() - True - >>> GradedText("some text").is_broken_wl() - False - """ - if self.is_wl(): - return True - text = self._text.strip() - return text.startswith('') and text.endswith('') - - def master(self): - """ - Gets the master tag. - - >>> GradedText("").master() - 'powiesc' - """ - assert self.is_wl() - return self._master - - @obj_memoized - def has_trim_begin(self): - return RE_TRIM_BEGIN.search(self._text) - - @obj_memoized - def has_trim_end(self): - return RE_TRIM_END.search(self._text) - - def _trim(text, trim_begin=True, trim_end=True): """ Cut off everything before RE_TRIM_BEGIN and after RE_TRIM_END, so @@ -153,26 +48,6 @@ def compile_text(parts): return "".join(texts) -def change_master(text, master): - """ - Changes the master tag in a WL document. - """ - e = etree.fromstring(text) - e[-1].tag = master - return unicode(etree.tostring(e, encoding="utf-8"), 'utf-8') - - -def basic_structure(text, master): - e = etree.fromstring(''' - - - -''' % (TRIM_BEGIN, TRIM_END)) - e[0].tag = master - e[0][0].tail = "\n"*3 + text + "\n"*3 - return unicode(etree.tostring(e, encoding="utf-8"), 'utf-8') - - def add_trim_begin(text): trim_tag = etree.Comment(TRIM_BEGIN) e = etree.fromstring(text) diff --git a/apps/wiki/views.py b/apps/wiki/views.py index b137a541..4d9fac9b 100644 --- a/apps/wiki/views.py +++ b/apps/wiki/views.py @@ -5,7 +5,7 @@ import logging from django.conf import settings from django.core.urlresolvers import reverse from django import http -from django.http import Http404 +from django.http import Http404, HttpResponseForbidden from django.middleware.gzip import GZipMiddleware from django.utils.decorators import decorator_from_middleware from django.utils.encoding import smart_unicode @@ -46,6 +46,8 @@ def editor(request, slug, chunk=None, template_name='wiki/document_details.html' return http.HttpResponseRedirect(reverse("catalogue_create_missing", args=[slug])) else: raise Http404 + if not chunk.book.accessible(request): + return HttpResponseForbidden("Not authorized.") access_time = datetime.now() last_books = request.session.get("wiki_last_books", {}) @@ -77,6 +79,8 @@ def editor_readonly(request, slug, chunk=None, template_name='wiki/document_deta revision = request.GET['revision'] except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist, KeyError): raise Http404 + if not chunk.book.accessible(request): + return HttpResponseForbidden("Not authorized.") access_time = datetime.now() last_books = request.session.get("wiki_last_books", {}) @@ -102,6 +106,8 @@ def editor_readonly(request, slug, chunk=None, template_name='wiki/document_deta @decorator_from_middleware(GZipMiddleware) def text(request, chunk_id): doc = get_object_or_404(Chunk, pk=chunk_id) + if not doc.book.accessible(request): + return HttpResponseForbidden("Not authorized.") if request.method == 'POST': form = forms.DocumentTextSaveForm(request.POST, user=request.user, prefix="textsave") @@ -160,6 +166,8 @@ def revert(request, chunk_id): form = forms.DocumentTextRevertForm(request.POST, prefix="textrevert") if form.is_valid(): doc = get_object_or_404(Chunk, pk=chunk_id) + if not doc.book.accessible(request): + return HttpResponseForbidden("Not authorized.") revision = form.cleaned_data['revision'] @@ -205,6 +213,10 @@ def gallery(request, directory): images = [map_to_url(f) for f in map(smart_unicode, os.listdir(base_dir)) if is_image(f)] images.sort() + + if not request.user.is_authenticated(): + return HttpResponseForbidden("Not authorized.") + return JSONResponse(images) except (IndexError, OSError): logger.exception("Unable to fetch gallery") @@ -223,6 +235,9 @@ def diff(request, chunk_id): revB = None doc = get_object_or_404(Chunk, pk=chunk_id) + if not doc.book.accessible(request): + return HttpResponseForbidden("Not authorized.") + # allow diff from the beginning if revA: docA = doc.at_revision(revA).materialize() @@ -237,6 +252,8 @@ def diff(request, chunk_id): @never_cache def revision(request, chunk_id): doc = get_object_or_404(Chunk, pk=chunk_id) + if not doc.book.accessible(request): + return HttpResponseForbidden("Not authorized.") return http.HttpResponse(str(doc.revision())) @@ -244,6 +261,8 @@ def revision(request, chunk_id): def history(request, chunk_id): # TODO: pagination doc = get_object_or_404(Chunk, pk=chunk_id) + if not doc.book.accessible(request): + return HttpResponseForbidden("Not authorized.") changes = [] for change in doc.history().order_by('-created_at'): @@ -264,6 +283,8 @@ def pubmark(request, chunk_id): form = forms.DocumentPubmarkForm(request.POST, prefix="pubmark") if form.is_valid(): doc = get_object_or_404(Chunk, pk=chunk_id) + if not doc.book.accessible(request): + return HttpResponseForbidden("Not authorized.") revision = form.cleaned_data['revision'] publishable = form.cleaned_data['publishable'] diff --git a/redakcja/localsettings.sample b/redakcja/localsettings.sample index c46e1921..b2f2b5cb 100644 --- a/redakcja/localsettings.sample +++ b/redakcja/localsettings.sample @@ -30,3 +30,5 @@ COMPRESS = False APICLIENT_WL_CONSUMER_KEY = None APICLIENT_WL_CONSUMER_SECRET = None + +USE_CELERY = True diff --git a/redakcja/settings/test.py b/redakcja/settings/test.py index 47622577..5fbd59f5 100644 --- a/redakcja/settings/test.py +++ b/redakcja/settings/test.py @@ -20,6 +20,7 @@ DATABASES = { import tempfile CATALOGUE_REPO_PATH = tempfile.mkdtemp(prefix='redakcja-repo') +USE_CELERY = False INSTALLED_APPS += ('django_nose', 'dvcs.tests') diff --git a/redakcja/static/css/filelist.css b/redakcja/static/css/filelist.css index 23198945..4aead730 100644 --- a/redakcja/static/css/filelist.css +++ b/redakcja/static/css/filelist.css @@ -63,8 +63,8 @@ td { .editable td { padding: 1px; } -.editable input, .editable select { - width: 30em; +.editable input, .editable select, .editable textarea { + width: 400px; } #login-box { diff --git a/redakcja/static/js/wiki/wikiapi.js b/redakcja/static/js/wiki/wikiapi.js index 4949f6b7..1234c985 100644 --- a/redakcja/static/js/wiki/wikiapi.js +++ b/redakcja/static/js/wiki/wikiapi.js @@ -204,9 +204,18 @@ self.galleryImages = data; params['success'](self, data); }, - error: function() { + error: function(xhr) { + switch (xhr.status) { + case 403: + var msg = 'Galerie dostępne tylko dla zalogowanych użytkowników.'; + break; + case 404: + var msg = "Nie znaleziono galerii o nazwie: '" + self.galleryLink + "'."; + default: + var msg = "Nie udało się wczytać galerii o nazwie: '" + self.galleryLink + "'."; + } self.galleryImages = []; - params['failure'](self, "

Nie udało się wczytać galerii pod nazwą: '" + self.galleryLink + "'.

"); + params['failure'](self, "

" + msg + "

"); } }); };