1 # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
 
   2 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 
   4 from datetime import datetime
 
   8 from time import mktime
 
   9 from urllib.parse import quote
 
  11 from django.apps import apps
 
  12 from django.conf import settings
 
  13 from django.urls import reverse
 
  14 from django import http
 
  15 from django.http import Http404, HttpResponse, HttpResponseForbidden, HttpResponseBadRequest
 
  16 from django.middleware.gzip import GZipMiddleware
 
  17 from django.utils.decorators import decorator_from_middleware
 
  18 from django.utils.formats import localize
 
  19 from django.utils.translation import gettext as _
 
  20 from django.views.decorators.http import require_POST, require_GET
 
  21 from django.shortcuts import get_object_or_404, render
 
  22 from django_gravatar.helpers import get_gravatar_url
 
  23 from sorl.thumbnail import get_thumbnail
 
  25 from documents.models import Book, Chunk
 
  27 from . import nice_diff
 
  28 from team.models import Presence
 
  29 from wiki import forms
 
  30 from wiki.helpers import (JSONResponse, JSONFormInvalid, JSONServerError,
 
  31                 ajax_require_permission)
 
  32 from wiki.models import Theme
 
  35 # Quick hack around caching problems, TODO: use ETags
 
  37 from django.views.decorators.cache import never_cache
 
  39 logger = logging.getLogger("fnp.wiki")
 
  44 class HttpResponseLengthRequired(HttpResponse):
 
  49 def editor(request, slug, chunk=None, template_name='wiki/document_details.html'):
 
  51         chunk = Chunk.get(slug, chunk)
 
  52     except Chunk.MultipleObjectsReturned:
 
  55     except Chunk.DoesNotExist:
 
  58                 book = Book.objects.get(slug=slug)
 
  59             except Book.DoesNotExist:
 
  60                 return http.HttpResponseRedirect(reverse("documents_create_missing", args=[slug]))
 
  63     if not chunk.book.accessible(request):
 
  64         return HttpResponseForbidden("Not authorized.")
 
  66     access_time = datetime.now()
 
  67     last_books = request.session.get("wiki_last_books", {})
 
  68     last_books[reverse(editor, args=[chunk.book.slug, chunk.slug])] = {
 
  69         'time': mktime(access_time.timetuple()),
 
  70         'title': chunk.pretty_name(),
 
  73     if len(last_books) > MAX_LAST_DOCS:
 
  74         oldest_key = min(last_books, key=lambda x: last_books[x]['time'])
 
  75         del last_books[oldest_key]
 
  76     request.session['wiki_last_books'] = last_books
 
  78     return render(request, template_name, {
 
  81             "text_save": forms.DocumentTextSaveForm(user=request.user, prefix="textsave"),
 
  82             "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
 
  83             "pubmark": forms.DocumentPubmarkForm(prefix="pubmark"),
 
  85         'can_pubmark': request.user.has_perm('documents.can_pubmark'),
 
  86         'REDMINE_URL': settings.REDMINE_URL,
 
  90 def editor_user_area(request):
 
  91     return render(request, 'wiki/editor-user-area.html', {
 
  93             "text_save": forms.DocumentTextSaveForm(user=request.user, prefix="textsave"),
 
  94             "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
 
  95             "pubmark": forms.DocumentPubmarkForm(prefix="pubmark"),
 
  97         'can_pubmark': request.user.has_perm('documents.can_pubmark'),
 
 102 def editor_readonly(request, slug, chunk=None, template_name='wiki/document_details_readonly.html'):
 
 104         chunk = Chunk.get(slug, chunk)
 
 105         revision = request.GET['revision']
 
 106     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist, KeyError):
 
 108     if not chunk.book.accessible(request):
 
 109         return HttpResponseForbidden("Not authorized.")
 
 111     access_time = datetime.now()
 
 112     last_books = request.session.get("wiki_last_books", {})
 
 113     last_books[slug, chunk.slug] = {
 
 114         'time': mktime(access_time.timetuple()),
 
 115         'title': chunk.book.title,
 
 118     if len(last_books) > MAX_LAST_DOCS:
 
 119         oldest_key = min(last_books, key=lambda x: last_books[x]['time'])
 
 120         del last_books[oldest_key]
 
 121     request.session['wiki_last_books'] = last_books
 
 123     return render(request, template_name, {
 
 125         'revision': revision,
 
 127         'REDMINE_URL': settings.REDMINE_URL,
 
 132 @decorator_from_middleware(GZipMiddleware)
 
 133 def text(request, chunk_id):
 
 134     doc = get_object_or_404(Chunk, pk=chunk_id)
 
 135     if not doc.book.accessible(request):
 
 136         return HttpResponseForbidden("Not authorized.")
 
 138     if request.method == 'POST':
 
 139         # Check length to reject broken request.
 
 141             expected_cl = int(request.META['CONTENT_LENGTH'])
 
 143             return HttpResponseLengthRequired(json.dumps(
 
 144                 {"__message": _("Content length required.")}
 
 147         cl = len(request.body)
 
 148         if cl != expected_cl:
 
 149             return HttpResponseBadRequest(json.dumps(
 
 150                 {"__message": _("Wrong content length, request probably interrupted.")}
 
 153         form = forms.DocumentTextSaveForm(request.POST, user=request.user, prefix="textsave")
 
 155             if request.user.is_authenticated:
 
 156                 author = request.user
 
 159             text = form.cleaned_data['text']
 
 160             parent_revision = form.cleaned_data['parent_revision']
 
 161             if parent_revision is not None:
 
 162                 parent = doc.at_revision(parent_revision)
 
 165             stage = form.cleaned_data['stage_completed']
 
 166             tags = [stage] if stage else []
 
 167             publishable = (form.cleaned_data['publishable'] and
 
 168                     request.user.has_perm('documents.can_pubmark'))
 
 169             doc.commit(author=author,
 
 172                        description=form.cleaned_data['comment'],
 
 174                        author_name=form.cleaned_data['author_name'],
 
 175                        author_email=form.cleaned_data['author_email'],
 
 176                        publishable=publishable,
 
 178             revision = doc.revision()
 
 179             return JSONResponse({
 
 180                 'text': doc.materialize() if parent_revision != revision else None,
 
 182                 'revision': revision,
 
 185             return JSONFormInvalid(form)
 
 187         revision = request.GET.get("revision", None)
 
 190             revision = int(revision)
 
 191         except (ValueError, TypeError):
 
 192             revision = doc.revision()
 
 194         if revision is not None:
 
 195             text = doc.at_revision(revision).materialize()
 
 199         return JSONResponse({
 
 202             'revision': revision,
 
 208 def revert(request, chunk_id):
 
 209     form = forms.DocumentTextRevertForm(request.POST, prefix="textrevert")
 
 211         doc = get_object_or_404(Chunk, pk=chunk_id)
 
 212         if not doc.book.accessible(request):
 
 213             return HttpResponseForbidden("Not authorized.")
 
 215         revision = form.cleaned_data['revision']
 
 217         comment = form.cleaned_data['comment']
 
 218         comment += "\n#revert to %s" % revision
 
 220         if request.user.is_authenticated:
 
 221             author = request.user
 
 225         before = doc.revision()
 
 226         logger.info("Reverting %s to %s", chunk_id, revision)
 
 227         doc.at_revision(revision).revert(author=author, description=comment)
 
 229         return JSONResponse({
 
 230             'text': doc.materialize() if before != doc.revision() else None,
 
 232             'revision': doc.revision(),
 
 235         return JSONFormInvalid(form)
 
 239 def gallery(request, directory):
 
 246         base_dir = os.path.join(
 
 251         def map_to_url(filename):
 
 252             return quote(("%s/%s" % (base_url, filename)))
 
 254         def is_image(filename):
 
 255             return os.path.splitext(filename)[1].lower() in (u'.jpg', u'.jpeg', u'.png')
 
 257         books = Book.objects.filter(gallery=directory)
 
 259         if not all(book.public for book in books) and not request.user.is_authenticated:
 
 260             return HttpResponseForbidden("Not authorized.")
 
 264                 "url": map_to_url(f),
 
 265                 "thumb": get_thumbnail(os.path.join(base_dir, f), '120x120').url
 
 266             } for f in sorted(os.listdir(base_dir)) if is_image(f)
 
 269         return JSONResponse(images)
 
 270     except (IndexError, OSError):
 
 271         logger.exception("Unable to fetch gallery")
 
 276 def scans_list(request, pks):
 
 279         get_object_or_404(sources.models.BookSource, pk=pk)
 
 282     def map_to_url(filename):
 
 283         return quote(("%s/%s" % (settings.MEDIA_URL, filename)))
 
 288                 "url": map_to_url(f),
 
 289             } for f in bs.get_view_files()
 
 291     return JSONResponse(images)
 
 295 def diff(request, chunk_id):
 
 296     revA = int(request.GET.get('from', 0))
 
 297     revB = int(request.GET.get('to', 0))
 
 300         revA, revB = revB, revA
 
 305     doc = get_object_or_404(Chunk, pk=chunk_id)
 
 306     if not doc.book.accessible(request):
 
 307         return HttpResponseForbidden("Not authorized.")
 
 309     # allow diff from the beginning
 
 311         docA = doc.at_revision(revA).materialize()
 
 314     docB = doc.at_revision(revB).materialize()
 
 316     return http.HttpResponse(nice_diff.html_diff_table(docA.splitlines(),
 
 317                                          docB.splitlines(), context=3))
 
 321 def revision(request, chunk_id):
 
 322     if not request.session.session_key:
 
 323         return HttpResponseForbidden("Not authorized.")
 
 324     doc = get_object_or_404(Chunk, pk=chunk_id)
 
 325     if not doc.book.accessible(request):
 
 326         return HttpResponseForbidden("Not authorized.")
 
 329         request.user, request.session.session_key,
 
 331         request.GET.get('a') == 'true'
 
 334     # Temporary compat for unreloaded clients.
 
 335     if not request.GET.get('new'):
 
 336         return http.HttpResponse(str(doc.revision()))
 
 338     return JSONResponse({
 
 339         'rev': doc.revision(),
 
 342                 'name': (p.user.first_name + ' ' + p.user.last_name) if p.user is not None else '?',
 
 343                 'gravatar': get_gravatar_url(p.user.email if p.user is not None else '-', size=26),
 
 344                 'since': p.since.strftime('%H:%M'),
 
 347             for p in Presence.get_current(request.session.session_key, doc)
 
 353 def history(request, chunk_id):
 
 355     doc = get_object_or_404(Chunk, pk=chunk_id)
 
 356     if not doc.book.accessible(request):
 
 357         return HttpResponseForbidden("Not authorized.")
 
 359     history = doc.history()
 
 361         before = int(request.GET.get('before'))
 
 365         history = history.filter(revision__lt=before)
 
 367     for change in history.reverse()[:20]:
 
 369                 "version": change.revision,
 
 370                 "description": change.description,
 
 371                 "author": change.author_str(),
 
 372                 "date": localize(change.created_at),
 
 373                 "publishable": change.publishable,
 
 374                 "tag": ',\n'.join(str(tag) for tag in change.tags.all()),
 
 375                 "published": _("Published") + ": " + \
 
 376                     localize(change.publish_log.order_by('-book_record__timestamp')[0].book_record.timestamp) \
 
 377                     if change.publish_log.exists() else "",
 
 379     return JSONResponse(changes)
 
 383 @ajax_require_permission('documents.can_pubmark')
 
 384 def pubmark(request, chunk_id):
 
 385     form = forms.DocumentPubmarkForm(request.POST, prefix="pubmark")
 
 387         doc = get_object_or_404(Chunk, pk=chunk_id)
 
 388         if not doc.book.accessible(request):
 
 389             return HttpResponseForbidden("Not authorized.")
 
 391         revision = form.cleaned_data['revision']
 
 392         publishable = form.cleaned_data['publishable']
 
 393         change = doc.at_revision(revision)
 
 394         if publishable != change.publishable:
 
 395             change.set_publishable(publishable)
 
 396             return JSONResponse({"message": _("Revision marked")})
 
 398             return JSONResponse({"message": _("Nothing changed")})
 
 400         return JSONFormInvalid(form)
 
 404 @ajax_require_permission('documents.book_edit')
 
 405 def set_gallery(request, chunk_id):
 
 406     doc = get_object_or_404(Chunk, pk=chunk_id)
 
 408     book.gallery = request.POST['gallery']
 
 409     book.save(update_fields=['gallery'])
 
 410     return JSONResponse({})
 
 413 @ajax_require_permission('documents.chunk_edit')
 
 414 def set_gallery_start(request, chunk_id):
 
 415     doc = get_object_or_404(Chunk, pk=chunk_id)
 
 416     doc.gallery_start = request.POST['start']
 
 417     doc.save(update_fields=['gallery_start'])
 
 418     return JSONResponse({})
 
 420 @ajax_require_permission('documents.chunk_edit')
 
 421 def galleries(request):
 
 434     prefix = request.GET.get('q', '')
 
 435     return http.HttpResponse('\n'.join([str(t) for t in Theme.objects.filter(name__istartswith=prefix)]))
 
 439     return render(request, 'wiki/back.html')