X-Git-Url: https://git.mdrn.pl/redakcja.git/blobdiff_plain/9d313b5b47b31bf6fd82987b967923c4eca011ac..1f49ab5c41b30ea9ce3db87b72999b845681cd64:/apps/catalogue/views.py?ds=sidebyside diff --git a/apps/catalogue/views.py b/apps/catalogue/views.py index e436b0ba..4204e770 100644 --- a/apps/catalogue/views.py +++ b/apps/catalogue/views.py @@ -1,29 +1,46 @@ # -*- coding: utf-8 -*- -from datetime import date, timedelta +# +# This file is part of MIL/PEER, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# import logging import os import shutil +import subprocess +from tempfile import NamedTemporaryFile +from xml.sax.saxutils import escape as escape_xml from django.conf import settings from django.contrib import auth from django.contrib.auth.models import User from django.contrib.auth.decorators import login_required +from django.contrib.sites.models import Site from django.core.urlresolvers import reverse -from django.db.models import Count from django import http -from django.http import Http404 +from django.http import Http404, HttpResponse, HttpResponseForbidden from django.shortcuts import get_object_or_404, render, redirect +from django.utils.encoding import force_str from django.utils.http import urlquote_plus from django.views.decorators.http import require_POST +from unidecode import unidecode from catalogue import forms -from catalogue import helpers +from catalogue.filters import DocumentFilterSet +from catalogue.forms import TagMultipleForm, TagSingleForm from catalogue.helpers import active_tab +from catalogue.models import Category +from librarian import BuildError +from librarian.utils import Context +from librarian.document import Document as SST +from librarian.formats.html import HtmlFormat +from librarian.formats.pdf import PdfFormat +from librarian.formats.epub import EpubFormat +from redakcja.utlis import send_notify_email from .constants import STAGES from .models import Document, Plan from dvcs.models import Revision from organizations.models import Organization -from fileupload.views import UploadView, PackageView +from fileupload.views import UploadView # # Quick hack around caching problems, TODO: use ETags @@ -34,12 +51,6 @@ from django.views.decorators.cache import never_cache logger = logging.getLogger("fnp.catalogue") -@active_tab('all') -@never_cache -def document_list(request): - return render(request, 'catalogue/document_list.html') - - @never_cache def user(request, username): user = get_object_or_404(User, username=username) @@ -58,44 +69,22 @@ def my(request): }) -@active_tab('users') -def users(request): - return render(request, 'catalogue/user_list.html', { - 'users': User.objects.all().annotate(count=Count('chunk')).order_by( - '-count', 'last_name', 'first_name'), - }) - - -@active_tab('activity') -def activity(request, isodate=None): - today = date.today() - try: - day = helpers.parse_isodate(isodate) - except ValueError: - day = today - - if day > today: - raise Http404 - if day != today: - next_day = day + timedelta(1) - prev_day = day - timedelta(1) - - return render(request, 'catalogue/activity.html', locals()) - - @never_cache def logout_then_redirect(request): auth.logout(request) return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?=')) -# @permission_required('catalogue.add_book') @login_required @active_tab('create') def create_missing(request): if request.method == "POST": form = forms.DocumentCreateForm(request.POST, request.FILES) - if form.is_valid(): + tag_forms = [ + (TagMultipleForm if category.multiple else TagSingleForm)( + category=category, data=request.POST, prefix=category.dc_tag) + for category in Category.objects.all()] + if form.is_valid() and all(tag_form.is_valid() for tag_form in tag_forms): if request.user.is_authenticated(): creator = request.user @@ -112,13 +101,19 @@ def create_missing(request): doc = Document.objects.create(**kwargs) + for tag_form in tag_forms: + tag_form.save(instance=doc) + cover = request.FILES.get('cover') if cover: uppath = 'uploads/%d/' % doc.pk path = settings.MEDIA_ROOT + uppath if not os.path.isdir(path): os.makedirs(path) - dest_path = path + cover.name # UNSAFE + cover.name = unidecode(cover.name) + dest_path = path + cover.name + if not os.path.abspath(dest_path).startswith(os.path.abspath(path)): + raise Http404 with open(dest_path, 'w') as destination: for chunk in cover.chunks(): destination.write(chunk) @@ -126,19 +121,24 @@ def create_missing(request): else: cover_url = '' - doc.commit( - text='''
+ text = '''
- ''' + form.cleaned_data['publisher'] + ''' - ''' + form.cleaned_data['description'] + ''' - ''' + form.cleaned_data['language'] + ''' - ''' + form.cleaned_data['rights'] + ''' - ''' + form.cleaned_data['audience'] + ''' - ''' + cover_url + ''' + %s + %s + %s + %s -
''' + title + '''
+
%s
-
''', +
''' % ( + escape_xml(form.cleaned_data['publisher']), + escape_xml(form.cleaned_data['description']), + '\n'.join(tag_form.metadata_rows() for tag_form in tag_forms), + escape_xml(cover_url), + escape_xml(title)) + + doc.commit( + text=text, author=creator ) doc.assigned_to = request.user @@ -162,108 +162,21 @@ def create_missing(request): form = forms.DocumentCreateForm(initial={'owner_organization': org}) + tag_forms = [ + (TagMultipleForm if category.multiple else TagSingleForm)( + category=category, tutorial_no=i, prefix=category.dc_tag) + for i, category in enumerate(Category.objects.all(), start=2)] + return render(request, "catalogue/document_create_missing.html", { "form": form, + "tag_forms": tag_forms, "logout_to": '/', }) -# @permission_required('catalogue.add_book') -# @active_tab('upload') -# def upload(request): -# if request.method == "POST": -# form = forms.DocumentsUploadForm(request.POST, request.FILES) -# if form.is_valid(): -# import slughifi -# -# if request.user.is_authenticated(): -# creator = request.user -# else: -# creator = None -# -# zip = form.cleaned_data['zip'] -# skipped_list = [] -# ok_list = [] -# error_list = [] -# slugs = {} -# existing = [book.slug for book in Book.objects.all()] -# for filename in zip.namelist(): -# if filename[-1] == '/': -# continue -# title = os.path.basename(filename)[:-4] -# slug = slughifi(title) -# if not (slug and filename.endswith('.xml')): -# skipped_list.append(filename) -# elif slug in slugs: -# error_list.append((filename, slug, _('Slug already used for %s' % slugs[slug]))) -# elif slug in existing: -# error_list.append((filename, slug, _('Slug already used in repository.'))) -# else: -# try: -# zip.read(filename).decode('utf-8') # test read -# ok_list.append((filename, slug, title)) -# except UnicodeDecodeError: -# error_list.append((filename, title, _('File should be UTF-8 encoded.'))) -# slugs[slug] = filename -# -# if not error_list: -# for filename, slug, title in ok_list: -# book = Book.create( -# text=zip.read(filename).decode('utf-8'), -# creator=creator, -# slug=slug, -# title=title, -# ) -# -# return render(request, "catalogue/document_upload.html", { -# "form": form, -# "ok_list": ok_list, -# "skipped_list": skipped_list, -# "error_list": error_list, -# -# "logout_to": '/', -# }) -# else: -# form = forms.DocumentsUploadForm() -# -# return render(request, "catalogue/document_upload.html", { -# "form": form, -# -# "logout_to": '/', -# }) - - -# @never_cache -# def book_xml(request, slug): -# 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 -# return response - - -# @never_cache -# def book_txt(request, slug): -# book = get_object_or_404(Book, slug=slug) -# if not book.accessible(request): -# return HttpResponseForbidden("Not authorized.") -# -# doc = book.wldocument() -# text = doc.as_text().get_string() -# response = http.HttpResponse(text, content_type='text/plain', mimetype='text/plain') -# response['Content-Disposition'] = 'attachment; filename=%s.txt' % slug -# return response - - @never_cache def book_html(request, pk, rev_pk=None, preview=False): - from librarian.document import Document as SST - from librarian.formats.html import HtmlFormat - doc = get_object_or_404(Document, pk=pk, deleted=False) try: @@ -286,9 +199,13 @@ def book_html(request, pk, rev_pk=None, preview=False): was_published = revision == published_revision or doc.publish_log.filter(revision=revision).exists() - sst = SST.from_string(revision.materialize()) - html = HtmlFormat(sst).build( - files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk)).get_string() + try: + sst = SST.from_string(revision.materialize()) + except ValueError as e: + html = e + else: + html = HtmlFormat(sst).build( + files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk)).get_string() # response = http.HttpResponse(html, content_type='text/html', mimetype='text/html') # return response @@ -313,23 +230,26 @@ def book_html(request, pk, rev_pk=None, preview=False): @never_cache def book_pdf(request, pk, rev_pk): - from librarian.utils import Context - from librarian.document import Document as SST - from librarian.formats.pdf import PdfFormat - doc = get_object_or_404(Document, pk=pk) rev = get_object_or_404(Revision, pk=rev_pk) # Test - sst = SST.from_string(rev.materialize()) + try: + sst = SST.from_string(rev.materialize()) + except ValueError as e: + return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400') ctx = Context( files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk), source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])), + organization=doc.owner_organization.name if doc.owner_organization else doc.owner_user.get_full_name(), ) if doc.owner_organization is not None and doc.owner_organization.logo: ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url) - pdf_file = PdfFormat(sst).build(ctx) + try: + pdf_file = PdfFormat(sst).build(ctx) + except BuildError as e: + return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400') from catalogue.ebook_utils import serve_file return serve_file(pdf_file.get_filename(), '%d.pdf' % doc.pk, 'application/pdf') @@ -337,15 +257,14 @@ def book_pdf(request, pk, rev_pk): @never_cache def book_epub(request, pk, rev_pk): - from librarian.utils import Context - from librarian.document import Document as SST - from librarian.formats.epub import EpubFormat - doc = get_object_or_404(Document, pk=pk) rev = get_object_or_404(Revision, pk=rev_pk) # Test - sst = SST.from_string(rev.materialize()) + try: + sst = SST.from_string(rev.materialize()) + except ValueError as e: + return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400') ctx = Context( files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk), @@ -353,12 +272,45 @@ def book_epub(request, pk, rev_pk): ) if doc.owner_organization is not None and doc.owner_organization.logo: ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url) - epub_file = EpubFormat(sst).build() + try: + epub_file = EpubFormat(sst).build(ctx) + except BuildError as e: + return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400') from catalogue.ebook_utils import serve_file return serve_file(epub_file.get_filename(), '%d.epub' % doc.pk, 'application/epub+zip') +@never_cache +def book_mobi(request, pk, rev_pk): + doc = get_object_or_404(Document, pk=pk) + rev = get_object_or_404(Revision, pk=rev_pk) + + try: + sst = SST.from_string(rev.materialize()) + except ValueError as e: + return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400') + + ctx = Context( + files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk), + source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])), + ) + if doc.owner_organization is not None and doc.owner_organization.logo: + ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url) + try: + epub_file = EpubFormat(sst).build(ctx) + except BuildError as e: + return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400') + + output_file = NamedTemporaryFile(prefix='librarian', suffix='.mobi', delete=False) + output_file.close() + subprocess.check_call( + ['ebook-convert', epub_file.get_filename(), output_file.name, '--no-inline-toc']) + + from catalogue.ebook_utils import serve_file + return serve_file(output_file.name, '%d.mobi' % doc.pk, 'application/epub+zip') + + # @never_cache # def revision(request, slug, chunk=None): # try: @@ -373,9 +325,11 @@ def book_epub(request, pk, rev_pk): @login_required def book_schedule(request, pk): book = get_object_or_404(Document, pk=pk, deleted=False) + if not book.can_edit(request.user): + return HttpResponseForbidden("Not authorized.") if request.method == 'POST': Plan.objects.filter(document=book).delete() - for i, s in enumerate(STAGES): + for i, (s, name) in enumerate(STAGES): user_id = request.POST.get('s%d-user' % i) deadline = request.POST.get('s%d-deadline' % i) or None Plan.objects.create(document=book, stage=s, user_id=user_id, deadline=deadline) @@ -387,7 +341,7 @@ def book_schedule(request, pk): for p in Plan.objects.filter(document=book): current[p.stage] = (getattr(p.user, 'pk', None), (p.deadline.isoformat() if p.deadline else None)) - schedule = [(i, s, current.get(s, ())) for (i, s) in enumerate(STAGES)] + schedule = [(i, s, current.get(s, ())) for i, (s, name) in enumerate(STAGES)] if book.owner_organization: people = [m.user for m in book.owner_organization.membership_set.exclude(status='pending')] @@ -403,9 +357,8 @@ def book_schedule(request, pk): @login_required def book_owner(request, pk): doc = get_object_or_404(Document, pk=pk, deleted=False) - user_is_owner = doc.owner_organization and doc.owner_organization.is_member(request.user) - if not (doc.owner_user == request.user or user_is_owner): - raise Http404 + if not doc.can_edit(request.user): + return HttpResponseForbidden("Not authorized.") error = '' @@ -436,8 +389,8 @@ def book_owner(request, pk): @login_required def book_delete(request, pk): doc = get_object_or_404(Document, pk=pk, deleted=False) - if not (doc.owner_user == request.user or doc.owner_organization.is_member(request.user)): - raise Http404 + if not doc.can_edit(request.user): + return HttpResponseForbidden("Not authorized.") if request.method == 'POST': doc.deleted = True @@ -449,192 +402,6 @@ def book_delete(request, pk): }) -# 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": -# form = forms.BookForm(request.POST, instance=book) -# if form.is_valid(): -# form.save() -# return http.HttpResponseRedirect(book.get_absolute_url()) -# else: -# form = forms.BookForm(instance=book) -# editable = True -# else: -# form = forms.ReadonlyBookForm(instance=book) -# editable = False -# -# publish_error = book.publishable_error() -# publishable = publish_error is None -# -# return render(request, "catalogue/book_detail.html", { -# "book": book, -# "publishable": publishable, -# "publishable_error": publish_error, -# "form": form, -# "editable": editable, -# }) - - -# @permission_required('catalogue.add_chunk') -# def chunk_add(request, slug, chunk): -# try: -# 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) -# if form.is_valid(): -# if request.user.is_authenticated(): -# creator = request.user -# else: -# creator = None -# doc.split(creator=creator, -# slug=form.cleaned_data['slug'], -# title=form.cleaned_data['title'], -# gallery_start=form.cleaned_data['gallery_start'], -# user=form.cleaned_data['user'], -# stage=form.cleaned_data['stage'] -# ) -# -# return http.HttpResponseRedirect(doc.book.get_absolute_url()) -# else: -# form = forms.ChunkAddForm(initial={ -# "slug": str(doc.number + 1), -# "title": "cz. %d" % (doc.number + 1, ), -# }) -# -# return render(request, "catalogue/chunk_add.html", { -# "chunk": doc, -# "form": form, -# }) - - -# @login_required -# def chunk_edit(request, slug, chunk): -# try: -# 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(): -# form.save() -# go_next = request.GET.get('next', None) -# if go_next: -# go_next = urlquote_plus(unquote(iri_to_uri(go_next)), safe='/?=&') -# else: -# go_next = doc.book.get_absolute_url() -# return http.HttpResponseRedirect(go_next) -# else: -# form = forms.ChunkForm(instance=doc) -# -# referer = request.META.get('HTTP_REFERER') -# if referer: -# parts = urlsplit(referer) -# parts = ['', ''] + list(parts[2:]) -# go_next = urlquote_plus(urlunsplit(parts)) -# else: -# go_next = '' -# -# return render(request, "catalogue/chunk_edit.html", { -# "chunk": doc, -# "form": form, -# "go_next": go_next, -# }) - - -# @transaction.atomic -# @login_required -# def chunk_mass_edit(request): -# if request.method == 'POST': -# ids = map(int, filter(lambda i: i.strip()!='', request.POST.get('ids').split(','))) -# chunks = map(lambda i: Chunk.objects.get(id=i), ids) -# -# stage = request.POST.get('stage') -# if stage: -# try: -# stage = Chunk.tag_model.objects.get(slug=stage) -# except Chunk.DoesNotExist, e: -# stage = None -# -# for c in chunks: c.stage = stage -# -# username = request.POST.get('user') -# logger.info("username: %s" % username) -# logger.info(request.POST) -# if username: -# try: -# user = User.objects.get(username=username) -# except User.DoesNotExist, e: -# user = None -# -# for c in chunks: c.user = user -# -# status = request.POST.get('status') -# if status: -# books_affected = set() -# for c in chunks: -# if status == 'publish': -# c.head.publishable = True -# c.head.save() -# elif status == 'unpublish': -# c.head.publishable = False -# c.head.save() -# c.touch() # cache -# books_affected.add(c.book) -# for b in books_affected: -# b.touch() # cache -# -# project_id = request.POST.get('project') -# if project_id: -# try: -# project = Project.objects.get(pk=int(project_id)) -# except (Project.DoesNotExist, ValueError), e: -# project = None -# for c in chunks: -# book = c.book -# book.project = project -# book.save() -# -# for c in chunks: c.save() -# -# return HttpResponse("", content_type="text/plain") -# else: -# raise Http404 - - -# @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(): -# append_to = form.cleaned_data['append_to'] -# append_to.append(book) -# return http.HttpResponseRedirect(append_to.get_absolute_url()) -# else: -# form = forms.BookAppendForm(book) -# return render(request, "catalogue/book_append_to.html", { -# "book": book, -# "form": form, -# -# "logout_to": '/', -# }) - - @require_POST @login_required def publish(request, pk): @@ -642,16 +409,26 @@ def publish(request, pk): from .models import PublishRecord from dvcs.models import Revision - # FIXME: check permissions - doc = get_object_or_404(Document, pk=pk, deleted=False) + if not doc.can_edit(request.user): + return HttpResponseForbidden("Not authorized.") form = forms.DocumentTextPublishForm(request.POST, prefix="textpublish") if form.is_valid(): rev = Revision.objects.get(pk=form.cleaned_data['revision']) # FIXME: check if in tree # if PublishRecord.objects.filter(revision=rev, document=doc).exists(): # return http.HttpResponse('exists') + if not doc.published: + site = Site.objects.get_current() + send_notify_email( + 'New published document in MIL/PEER', + '''New published document in MIL/PEER: %s. View it in browser: https://%s%s. + +-- +MIL/PEER team.''' % (doc.meta()['title'], site.domain, reverse('catalogue_html', args=[doc.pk]))) PublishRecord.objects.create(revision=rev, document=doc, user=request.user) + doc.published = True + doc.save() if request.is_ajax(): return http.HttpResponse('ok') else: @@ -669,9 +446,10 @@ def publish(request, pk): @require_POST @login_required def unpublish(request, pk): - # FIXME: check permissions - doc = get_object_or_404(Document, pk=pk, deleted=False) + if not doc.can_edit(request.user): + return HttpResponseForbidden("Not authorized.") + doc.publish_log.all().delete() if request.is_ajax(): return http.HttpResponse('ok') @@ -696,12 +474,6 @@ class GalleryView(GalleryMixin, UploadView): self.doc = Document.objects.get(pk=pk, deleted=False) -class GalleryPackageView(GalleryMixin, PackageView): - - def get_redirect_url(self, slug): - return reverse('catalogue_book_gallery', kwargs=dict(slug=slug)) - - @login_required def fork(request, pk): doc = get_object_or_404(Document, pk=pk, deleted=False) @@ -738,12 +510,16 @@ def fork(request, pk): def upcoming(request): + f = DocumentFilterSet(request.GET, queryset=Document.objects.filter(deleted=False).filter(publish_log=None)) return render(request, "catalogue/upcoming.html", { - 'objects_list': Document.objects.filter(deleted=False).filter(publish_log=None), + 'filter_set': f, + 'link_url': 'catalogue_preview', }) def finished(request): + f = DocumentFilterSet(request.GET, queryset=Document.objects.filter(deleted=False).exclude(publish_log=None)) return render(request, "catalogue/finished.html", { - 'objects_list': Document.objects.filter(deleted=False).exclude(publish_log=None), + 'filter_set': f, + 'link_url': 'catalogue_html', })