X-Git-Url: https://git.mdrn.pl/redakcja.git/blobdiff_plain/5913c54d19b8f6775633176032161d49f9b2f1aa..18948ce1d84878c6307f2d0b732a9dbd03778cfb:/src/wiki/views.py?ds=sidebyside diff --git a/src/wiki/views.py b/src/wiki/views.py index e1ef6aed..3e6fedbf 100644 --- a/src/wiki/views.py +++ b/src/wiki/views.py @@ -1,24 +1,31 @@ -# -*- coding: utf-8 -*- +# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# from datetime import datetime +import json import os import logging from time import mktime -import urllib +from urllib.parse import quote +from django.apps import apps from django.conf import settings -from django.core.urlresolvers import reverse +from django.urls import reverse from django import http -from django.http import Http404, HttpResponseForbidden +from django.http import Http404, HttpResponse, HttpResponseForbidden, HttpResponseBadRequest from django.middleware.gzip import GZipMiddleware from django.utils.decorators import decorator_from_middleware -from django.utils.encoding import smart_unicode from django.utils.formats import localize -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.views.decorators.http import require_POST, require_GET from django.shortcuts import get_object_or_404, render +from django_gravatar.helpers import get_gravatar_url +from sorl.thumbnail import get_thumbnail -from catalogue.models import Book, Chunk -import nice_diff +from documents.models import Book, Chunk +import sources.models +from . import nice_diff +from team.models import Presence from wiki import forms from wiki.helpers import (JSONResponse, JSONFormInvalid, JSONServerError, ajax_require_permission) @@ -34,6 +41,10 @@ logger = logging.getLogger("fnp.wiki") MAX_LAST_DOCS = 10 +class HttpResponseLengthRequired(HttpResponse): + status_code = 411 + + @never_cache def editor(request, slug, chunk=None, template_name='wiki/document_details.html'): try: @@ -46,7 +57,7 @@ def editor(request, slug, chunk=None, template_name='wiki/document_details.html' try: book = Book.objects.get(slug=slug) except Book.DoesNotExist: - return http.HttpResponseRedirect(reverse("catalogue_create_missing", args=[slug])) + return http.HttpResponseRedirect(reverse("documents_create_missing", args=[slug])) else: raise Http404 if not chunk.book.accessible(request): @@ -71,11 +82,22 @@ def editor(request, slug, chunk=None, template_name='wiki/document_details.html' "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"), "pubmark": forms.DocumentPubmarkForm(prefix="pubmark"), }, - 'can_pubmark': request.user.has_perm('catalogue.can_pubmark'), + 'can_pubmark': request.user.has_perm('documents.can_pubmark'), 'REDMINE_URL': settings.REDMINE_URL, }) +def editor_user_area(request): + return render(request, 'wiki/editor-user-area.html', { + 'forms': { + "text_save": forms.DocumentTextSaveForm(user=request.user, prefix="textsave"), + "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"), + "pubmark": forms.DocumentPubmarkForm(prefix="pubmark"), + }, + 'can_pubmark': request.user.has_perm('documents.can_pubmark'), + }) + + @require_GET def editor_readonly(request, slug, chunk=None, template_name='wiki/document_details_readonly.html'): try: @@ -114,9 +136,23 @@ def text(request, chunk_id): return HttpResponseForbidden("Not authorized.") if request.method == 'POST': + # Check length to reject broken request. + try: + expected_cl = int(request.META['CONTENT_LENGTH']) + except: + return HttpResponseLengthRequired(json.dumps( + {"__message": _("Content length required.")} + )) + # 411 if missing + cl = len(request.body) + if cl != expected_cl: + return HttpResponseBadRequest(json.dumps( + {"__message": _("Wrong content length, request probably interrupted.")} + )) + form = forms.DocumentTextSaveForm(request.POST, user=request.user, prefix="textsave") if form.is_valid(): - if request.user.is_authenticated(): + if request.user.is_authenticated: author = request.user else: author = None @@ -129,7 +165,7 @@ def text(request, chunk_id): stage = form.cleaned_data['stage_completed'] tags = [stage] if stage else [] publishable = (form.cleaned_data['publishable'] and - request.user.has_perm('catalogue.can_pubmark')) + request.user.has_perm('documents.can_pubmark')) doc.commit(author=author, text=text, parent=parent, @@ -181,7 +217,7 @@ def revert(request, chunk_id): comment = form.cleaned_data['comment'] comment += "\n#revert to %s" % revision - if request.user.is_authenticated(): + if request.user.is_authenticated: author = request.user else: author = None @@ -203,35 +239,58 @@ def revert(request, chunk_id): def gallery(request, directory): try: base_url = ''.join(( - smart_unicode(settings.MEDIA_URL), - smart_unicode(settings.IMAGE_DIR), - smart_unicode(directory))) + settings.MEDIA_URL, + settings.IMAGE_DIR, + directory)) base_dir = os.path.join( - smart_unicode(settings.MEDIA_ROOT), - smart_unicode(settings.IMAGE_DIR), - smart_unicode(directory)) + settings.MEDIA_ROOT, + settings.IMAGE_DIR, + directory) def map_to_url(filename): - return urllib.quote(("%s/%s" % (base_url, smart_unicode(filename))).encode('utf-8')) + return quote(("%s/%s" % (base_url, filename))) def is_image(filename): return os.path.splitext(filename)[1].lower() in (u'.jpg', u'.jpeg', u'.png') - images = [map_to_url(f) for f in map(smart_unicode, os.listdir(base_dir)) if is_image(f)] - images.sort() - books = Book.objects.filter(gallery=directory) - if not all(book.public for book in books) and not request.user.is_authenticated(): + if not all(book.public for book in books) and not request.user.is_authenticated: return HttpResponseForbidden("Not authorized.") + images = [ + { + "url": map_to_url(f), + "thumb": get_thumbnail(os.path.join(base_dir, f), '120x120').url + } for f in sorted(os.listdir(base_dir)) if is_image(f) + ] + return JSONResponse(images) except (IndexError, OSError): logger.exception("Unable to fetch gallery") raise http.Http404 +@never_cache +def scans_list(request, pks): + pks = pks.split(',') + bss = [ + get_object_or_404(sources.models.BookSource, pk=pk) + for pk in pks + ] + def map_to_url(filename): + return quote(("%s/%s" % (settings.MEDIA_URL, filename))) + images = [] + for bs in bss: + images.extend([ + { + "url": map_to_url(f), + } for f in bs.get_view_files() + ]) + return JSONResponse(images) + + @never_cache def diff(request, chunk_id): revA = int(request.GET.get('from', 0)) @@ -260,10 +319,34 @@ def diff(request, chunk_id): @never_cache def revision(request, chunk_id): + if not request.session.session_key: + return HttpResponseForbidden("Not authorized.") 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())) + + Presence.report( + request.user, request.session.session_key, + doc, + request.GET.get('a') == 'true' + ) + + # Temporary compat for unreloaded clients. + if not request.GET.get('new'): + return http.HttpResponse(str(doc.revision())) + + return JSONResponse({ + 'rev': doc.revision(), + 'people': list([ + { + 'name': (p.user.first_name + ' ' + p.user.last_name) if p.user is not None else '?', + 'gravatar': get_gravatar_url(p.user.email if p.user is not None else '-', size=26), + 'since': p.since.strftime('%H:%M'), + 'active': p.active, + } + for p in Presence.get_current(request.session.session_key, doc) + ]), + }) @never_cache @@ -273,15 +356,22 @@ def history(request, chunk_id): if not doc.book.accessible(request): return HttpResponseForbidden("Not authorized.") + history = doc.history() + try: + before = int(request.GET.get('before')) + except: + pass + else: + history = history.filter(revision__lt=before) changes = [] - for change in doc.history().reverse(): + for change in history.reverse()[:20]: changes.append({ "version": change.revision, "description": change.description, "author": change.author_str(), "date": localize(change.created_at), - "publishable": _("Publishable") + "\n" if change.publishable else "", - "tag": ',\n'.join(unicode(tag) for tag in change.tags.all()), + "publishable": change.publishable, + "tag": ',\n'.join(str(tag) for tag in change.tags.all()), "published": _("Published") + ": " + \ localize(change.publish_log.order_by('-book_record__timestamp')[0].book_record.timestamp) \ if change.publish_log.exists() else "", @@ -290,7 +380,7 @@ def history(request, chunk_id): @require_POST -@ajax_require_permission('catalogue.can_pubmark') +@ajax_require_permission('documents.can_pubmark') def pubmark(request, chunk_id): form = forms.DocumentPubmarkForm(request.POST, prefix="pubmark") if form.is_valid(): @@ -310,6 +400,40 @@ def pubmark(request, chunk_id): return JSONFormInvalid(form) +@require_POST +@ajax_require_permission('documents.book_edit') +def set_gallery(request, chunk_id): + doc = get_object_or_404(Chunk, pk=chunk_id) + book = doc.book + book.gallery = request.POST['gallery'] + book.save(update_fields=['gallery']) + return JSONResponse({}) + +@require_POST +@ajax_require_permission('documents.chunk_edit') +def set_gallery_start(request, chunk_id): + doc = get_object_or_404(Chunk, pk=chunk_id) + doc.gallery_start = request.POST['start'] + doc.save(update_fields=['gallery_start']) + return JSONResponse({}) + +@ajax_require_permission('documents.chunk_edit') +def galleries(request): + return JSONResponse( + sorted( + os.listdir( + os.path.join( + settings.MEDIA_ROOT, + settings.IMAGE_DIR, + ) + ) + ) + ) + def themes(request): prefix = request.GET.get('q', '') return http.HttpResponse('\n'.join([str(t) for t in Theme.objects.filter(name__istartswith=prefix)])) + + +def back(request): + return render(request, 'wiki/back.html')