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')