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
 
   7 from time import mktime
 
   8 from urllib.parse import quote
 
  10 from django.apps import apps
 
  11 from django.conf import settings
 
  12 from django.urls import reverse
 
  13 from django import http
 
  14 from django.http import Http404, HttpResponseForbidden
 
  15 from django.middleware.gzip import GZipMiddleware
 
  16 from django.utils.decorators import decorator_from_middleware
 
  17 from django.utils.formats import localize
 
  18 from django.utils.translation import gettext as _
 
  19 from django.views.decorators.http import require_POST, require_GET
 
  20 from django.shortcuts import get_object_or_404, render
 
  21 from sorl.thumbnail import get_thumbnail
 
  23 from documents.models import Book, Chunk
 
  25 from . import nice_diff
 
  26 from wiki import forms
 
  27 from wiki.helpers import (JSONResponse, JSONFormInvalid, JSONServerError,
 
  28                 ajax_require_permission)
 
  29 from wiki.models import Theme
 
  32 # Quick hack around caching problems, TODO: use ETags
 
  34 from django.views.decorators.cache import never_cache
 
  36 logger = logging.getLogger("fnp.wiki")
 
  42 def editor(request, slug, chunk=None, template_name='wiki/document_details.html'):
 
  44         chunk = Chunk.get(slug, chunk)
 
  45     except Chunk.MultipleObjectsReturned:
 
  48     except Chunk.DoesNotExist:
 
  51                 book = Book.objects.get(slug=slug)
 
  52             except Book.DoesNotExist:
 
  53                 return http.HttpResponseRedirect(reverse("documents_create_missing", args=[slug]))
 
  56     if not chunk.book.accessible(request):
 
  57         return HttpResponseForbidden("Not authorized.")
 
  59     access_time = datetime.now()
 
  60     last_books = request.session.get("wiki_last_books", {})
 
  61     last_books[reverse(editor, args=[chunk.book.slug, chunk.slug])] = {
 
  62         'time': mktime(access_time.timetuple()),
 
  63         'title': chunk.pretty_name(),
 
  66     if len(last_books) > MAX_LAST_DOCS:
 
  67         oldest_key = min(last_books, key=lambda x: last_books[x]['time'])
 
  68         del last_books[oldest_key]
 
  69     request.session['wiki_last_books'] = last_books
 
  71     return render(request, template_name, {
 
  74             "text_save": forms.DocumentTextSaveForm(user=request.user, prefix="textsave"),
 
  75             "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
 
  76             "pubmark": forms.DocumentPubmarkForm(prefix="pubmark"),
 
  78         'can_pubmark': request.user.has_perm('documents.can_pubmark'),
 
  79         'REDMINE_URL': settings.REDMINE_URL,
 
  83 def editor_user_area(request):
 
  84     return render(request, 'wiki/editor-user-area.html', {
 
  86             "text_save": forms.DocumentTextSaveForm(user=request.user, prefix="textsave"),
 
  87             "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
 
  88             "pubmark": forms.DocumentPubmarkForm(prefix="pubmark"),
 
  90         'can_pubmark': request.user.has_perm('documents.can_pubmark'),
 
  95 def editor_readonly(request, slug, chunk=None, template_name='wiki/document_details_readonly.html'):
 
  97         chunk = Chunk.get(slug, chunk)
 
  98         revision = request.GET['revision']
 
  99     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist, KeyError):
 
 101     if not chunk.book.accessible(request):
 
 102         return HttpResponseForbidden("Not authorized.")
 
 104     access_time = datetime.now()
 
 105     last_books = request.session.get("wiki_last_books", {})
 
 106     last_books[slug, chunk.slug] = {
 
 107         'time': mktime(access_time.timetuple()),
 
 108         'title': chunk.book.title,
 
 111     if len(last_books) > MAX_LAST_DOCS:
 
 112         oldest_key = min(last_books, key=lambda x: last_books[x]['time'])
 
 113         del last_books[oldest_key]
 
 114     request.session['wiki_last_books'] = last_books
 
 116     return render(request, template_name, {
 
 118         'revision': revision,
 
 120         'REDMINE_URL': settings.REDMINE_URL,
 
 125 @decorator_from_middleware(GZipMiddleware)
 
 126 def text(request, chunk_id):
 
 127     doc = get_object_or_404(Chunk, pk=chunk_id)
 
 128     if not doc.book.accessible(request):
 
 129         return HttpResponseForbidden("Not authorized.")
 
 131     if request.method == 'POST':
 
 132         form = forms.DocumentTextSaveForm(request.POST, user=request.user, prefix="textsave")
 
 134             if request.user.is_authenticated:
 
 135                 author = request.user
 
 138             text = form.cleaned_data['text']
 
 139             parent_revision = form.cleaned_data['parent_revision']
 
 140             if parent_revision is not None:
 
 141                 parent = doc.at_revision(parent_revision)
 
 144             stage = form.cleaned_data['stage_completed']
 
 145             tags = [stage] if stage else []
 
 146             publishable = (form.cleaned_data['publishable'] and
 
 147                     request.user.has_perm('documents.can_pubmark'))
 
 148             doc.commit(author=author,
 
 151                        description=form.cleaned_data['comment'],
 
 153                        author_name=form.cleaned_data['author_name'],
 
 154                        author_email=form.cleaned_data['author_email'],
 
 155                        publishable=publishable,
 
 157             revision = doc.revision()
 
 158             return JSONResponse({
 
 159                 'text': doc.materialize() if parent_revision != revision else None,
 
 161                 'revision': revision,
 
 164             return JSONFormInvalid(form)
 
 166         revision = request.GET.get("revision", None)
 
 169             revision = int(revision)
 
 170         except (ValueError, TypeError):
 
 171             revision = doc.revision()
 
 173         if revision is not None:
 
 174             text = doc.at_revision(revision).materialize()
 
 178         return JSONResponse({
 
 181             'revision': revision,
 
 187 def revert(request, chunk_id):
 
 188     form = forms.DocumentTextRevertForm(request.POST, prefix="textrevert")
 
 190         doc = get_object_or_404(Chunk, pk=chunk_id)
 
 191         if not doc.book.accessible(request):
 
 192             return HttpResponseForbidden("Not authorized.")
 
 194         revision = form.cleaned_data['revision']
 
 196         comment = form.cleaned_data['comment']
 
 197         comment += "\n#revert to %s" % revision
 
 199         if request.user.is_authenticated:
 
 200             author = request.user
 
 204         before = doc.revision()
 
 205         logger.info("Reverting %s to %s", chunk_id, revision)
 
 206         doc.at_revision(revision).revert(author=author, description=comment)
 
 208         return JSONResponse({
 
 209             'text': doc.materialize() if before != doc.revision() else None,
 
 211             'revision': doc.revision(),
 
 214         return JSONFormInvalid(form)
 
 218 def gallery(request, directory):
 
 225         base_dir = os.path.join(
 
 230         def map_to_url(filename):
 
 231             return quote(("%s/%s" % (base_url, filename)))
 
 233         def is_image(filename):
 
 234             return os.path.splitext(filename)[1].lower() in (u'.jpg', u'.jpeg', u'.png')
 
 236         books = Book.objects.filter(gallery=directory)
 
 238         if not all(book.public for book in books) and not request.user.is_authenticated:
 
 239             return HttpResponseForbidden("Not authorized.")
 
 243                 "url": map_to_url(f),
 
 244                 "thumb": get_thumbnail(os.path.join(base_dir, f), '120x120').url
 
 245             } for f in sorted(os.listdir(base_dir)) if is_image(f)
 
 248         return JSONResponse(images)
 
 249     except (IndexError, OSError):
 
 250         logger.exception("Unable to fetch gallery")
 
 255 def scans_list(request, pk):
 
 256     bs = get_object_or_404(sources.models.BookSource, pk=pk)
 
 257     def map_to_url(filename):
 
 258         return quote(("%s/%s" % (settings.MEDIA_URL, filename)))
 
 261             "url": map_to_url(f),
 
 262         } for f in bs.get_view_files()
 
 264     return JSONResponse(images)
 
 268 def diff(request, chunk_id):
 
 269     revA = int(request.GET.get('from', 0))
 
 270     revB = int(request.GET.get('to', 0))
 
 273         revA, revB = revB, revA
 
 278     doc = get_object_or_404(Chunk, pk=chunk_id)
 
 279     if not doc.book.accessible(request):
 
 280         return HttpResponseForbidden("Not authorized.")
 
 282     # allow diff from the beginning
 
 284         docA = doc.at_revision(revA).materialize()
 
 287     docB = doc.at_revision(revB).materialize()
 
 289     return http.HttpResponse(nice_diff.html_diff_table(docA.splitlines(),
 
 290                                          docB.splitlines(), context=3))
 
 294 def revision(request, chunk_id):
 
 295     doc = get_object_or_404(Chunk, pk=chunk_id)
 
 296     if not doc.book.accessible(request):
 
 297         return HttpResponseForbidden("Not authorized.")
 
 298     Presence = apps.get_model('team', 'Presence')
 
 299     Presence.report(request.user, doc, request.GET.get('a') == 'true')
 
 300     return http.HttpResponse(str(doc.revision()))
 
 304 def history(request, chunk_id):
 
 306     doc = get_object_or_404(Chunk, pk=chunk_id)
 
 307     if not doc.book.accessible(request):
 
 308         return HttpResponseForbidden("Not authorized.")
 
 310     history = doc.history()
 
 312         before = int(request.GET.get('before'))
 
 316         history = history.filter(revision__lt=before)
 
 318     for change in history.reverse()[:20]:
 
 320                 "version": change.revision,
 
 321                 "description": change.description,
 
 322                 "author": change.author_str(),
 
 323                 "date": localize(change.created_at),
 
 324                 "publishable": _("Publishable") + "\n" if change.publishable else "",
 
 325                 "tag": ',\n'.join(str(tag) for tag in change.tags.all()),
 
 326                 "published": _("Published") + ": " + \
 
 327                     localize(change.publish_log.order_by('-book_record__timestamp')[0].book_record.timestamp) \
 
 328                     if change.publish_log.exists() else "",
 
 330     return JSONResponse(changes)
 
 334 @ajax_require_permission('documents.can_pubmark')
 
 335 def pubmark(request, chunk_id):
 
 336     form = forms.DocumentPubmarkForm(request.POST, prefix="pubmark")
 
 338         doc = get_object_or_404(Chunk, pk=chunk_id)
 
 339         if not doc.book.accessible(request):
 
 340             return HttpResponseForbidden("Not authorized.")
 
 342         revision = form.cleaned_data['revision']
 
 343         publishable = form.cleaned_data['publishable']
 
 344         change = doc.at_revision(revision)
 
 345         if publishable != change.publishable:
 
 346             change.set_publishable(publishable)
 
 347             return JSONResponse({"message": _("Revision marked")})
 
 349             return JSONResponse({"message": _("Nothing changed")})
 
 351         return JSONFormInvalid(form)
 
 355 @ajax_require_permission('documents.book_edit')
 
 356 def set_gallery(request, chunk_id):
 
 357     doc = get_object_or_404(Chunk, pk=chunk_id)
 
 359     book.gallery = request.POST['gallery']
 
 360     book.save(update_fields=['gallery'])
 
 361     return JSONResponse({})
 
 364 @ajax_require_permission('documents.chunk_edit')
 
 365 def set_gallery_start(request, chunk_id):
 
 366     doc = get_object_or_404(Chunk, pk=chunk_id)
 
 367     doc.gallery_start = request.POST['start']
 
 368     doc.save(update_fields=['gallery_start'])
 
 369     return JSONResponse({})
 
 371 @ajax_require_permission('documents.chunk_edit')
 
 372 def galleries(request):
 
 385     prefix = request.GET.get('q', '')
 
 386     return http.HttpResponse('\n'.join([str(t) for t in Theme.objects.filter(name__istartswith=prefix)]))
 
 390     return render(request, 'wiki/back.html')