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 sorl.thumbnail import get_thumbnail
24 from documents.models import Book, Chunk
26 from . import nice_diff
27 from wiki import forms
28 from wiki.helpers import (JSONResponse, JSONFormInvalid, JSONServerError,
29 ajax_require_permission)
30 from wiki.models import Theme
33 # Quick hack around caching problems, TODO: use ETags
35 from django.views.decorators.cache import never_cache
37 logger = logging.getLogger("fnp.wiki")
42 class HttpResponseLengthRequired(HttpResponse):
47 def editor(request, slug, chunk=None, template_name='wiki/document_details.html'):
49 chunk = Chunk.get(slug, chunk)
50 except Chunk.MultipleObjectsReturned:
53 except Chunk.DoesNotExist:
56 book = Book.objects.get(slug=slug)
57 except Book.DoesNotExist:
58 return http.HttpResponseRedirect(reverse("documents_create_missing", args=[slug]))
61 if not chunk.book.accessible(request):
62 return HttpResponseForbidden("Not authorized.")
64 access_time = datetime.now()
65 last_books = request.session.get("wiki_last_books", {})
66 last_books[reverse(editor, args=[chunk.book.slug, chunk.slug])] = {
67 'time': mktime(access_time.timetuple()),
68 'title': chunk.pretty_name(),
71 if len(last_books) > MAX_LAST_DOCS:
72 oldest_key = min(last_books, key=lambda x: last_books[x]['time'])
73 del last_books[oldest_key]
74 request.session['wiki_last_books'] = last_books
76 return render(request, template_name, {
79 "text_save": forms.DocumentTextSaveForm(user=request.user, prefix="textsave"),
80 "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
81 "pubmark": forms.DocumentPubmarkForm(prefix="pubmark"),
83 'can_pubmark': request.user.has_perm('documents.can_pubmark'),
84 'REDMINE_URL': settings.REDMINE_URL,
88 def editor_user_area(request):
89 return render(request, 'wiki/editor-user-area.html', {
91 "text_save": forms.DocumentTextSaveForm(user=request.user, prefix="textsave"),
92 "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
93 "pubmark": forms.DocumentPubmarkForm(prefix="pubmark"),
95 'can_pubmark': request.user.has_perm('documents.can_pubmark'),
100 def editor_readonly(request, slug, chunk=None, template_name='wiki/document_details_readonly.html'):
102 chunk = Chunk.get(slug, chunk)
103 revision = request.GET['revision']
104 except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist, KeyError):
106 if not chunk.book.accessible(request):
107 return HttpResponseForbidden("Not authorized.")
109 access_time = datetime.now()
110 last_books = request.session.get("wiki_last_books", {})
111 last_books[slug, chunk.slug] = {
112 'time': mktime(access_time.timetuple()),
113 'title': chunk.book.title,
116 if len(last_books) > MAX_LAST_DOCS:
117 oldest_key = min(last_books, key=lambda x: last_books[x]['time'])
118 del last_books[oldest_key]
119 request.session['wiki_last_books'] = last_books
121 return render(request, template_name, {
123 'revision': revision,
125 'REDMINE_URL': settings.REDMINE_URL,
130 @decorator_from_middleware(GZipMiddleware)
131 def text(request, chunk_id):
132 doc = get_object_or_404(Chunk, pk=chunk_id)
133 if not doc.book.accessible(request):
134 return HttpResponseForbidden("Not authorized.")
136 if request.method == 'POST':
137 # Check length to reject broken request.
139 expected_cl = int(request.META['CONTENT_LENGTH'])
141 return HttpResponseLengthRequired(json.dumps(
142 {"__message": _("Content length required.")}
145 cl = len(request.body)
146 if cl != expected_cl:
147 return HttpResponseBadRequest(json.dumps(
148 {"__message": _("Wrong content length, request probably interrupted.")}
151 form = forms.DocumentTextSaveForm(request.POST, user=request.user, prefix="textsave")
153 if request.user.is_authenticated:
154 author = request.user
157 text = form.cleaned_data['text']
158 parent_revision = form.cleaned_data['parent_revision']
159 if parent_revision is not None:
160 parent = doc.at_revision(parent_revision)
163 stage = form.cleaned_data['stage_completed']
164 tags = [stage] if stage else []
165 publishable = (form.cleaned_data['publishable'] and
166 request.user.has_perm('documents.can_pubmark'))
167 doc.commit(author=author,
170 description=form.cleaned_data['comment'],
172 author_name=form.cleaned_data['author_name'],
173 author_email=form.cleaned_data['author_email'],
174 publishable=publishable,
176 revision = doc.revision()
177 return JSONResponse({
178 'text': doc.materialize() if parent_revision != revision else None,
180 'revision': revision,
183 return JSONFormInvalid(form)
185 revision = request.GET.get("revision", None)
188 revision = int(revision)
189 except (ValueError, TypeError):
190 revision = doc.revision()
192 if revision is not None:
193 text = doc.at_revision(revision).materialize()
197 return JSONResponse({
200 'revision': revision,
206 def revert(request, chunk_id):
207 form = forms.DocumentTextRevertForm(request.POST, prefix="textrevert")
209 doc = get_object_or_404(Chunk, pk=chunk_id)
210 if not doc.book.accessible(request):
211 return HttpResponseForbidden("Not authorized.")
213 revision = form.cleaned_data['revision']
215 comment = form.cleaned_data['comment']
216 comment += "\n#revert to %s" % revision
218 if request.user.is_authenticated:
219 author = request.user
223 before = doc.revision()
224 logger.info("Reverting %s to %s", chunk_id, revision)
225 doc.at_revision(revision).revert(author=author, description=comment)
227 return JSONResponse({
228 'text': doc.materialize() if before != doc.revision() else None,
230 'revision': doc.revision(),
233 return JSONFormInvalid(form)
237 def gallery(request, directory):
244 base_dir = os.path.join(
249 def map_to_url(filename):
250 return quote(("%s/%s" % (base_url, filename)))
252 def is_image(filename):
253 return os.path.splitext(filename)[1].lower() in (u'.jpg', u'.jpeg', u'.png')
255 books = Book.objects.filter(gallery=directory)
257 if not all(book.public for book in books) and not request.user.is_authenticated:
258 return HttpResponseForbidden("Not authorized.")
262 "url": map_to_url(f),
263 "thumb": get_thumbnail(os.path.join(base_dir, f), '120x120').url
264 } for f in sorted(os.listdir(base_dir)) if is_image(f)
267 return JSONResponse(images)
268 except (IndexError, OSError):
269 logger.exception("Unable to fetch gallery")
274 def scans_list(request, pks):
277 get_object_or_404(sources.models.BookSource, pk=pk)
280 def map_to_url(filename):
281 return quote(("%s/%s" % (settings.MEDIA_URL, filename)))
286 "url": map_to_url(f),
287 } for f in bs.get_view_files()
289 return JSONResponse(images)
293 def diff(request, chunk_id):
294 revA = int(request.GET.get('from', 0))
295 revB = int(request.GET.get('to', 0))
298 revA, revB = revB, revA
303 doc = get_object_or_404(Chunk, pk=chunk_id)
304 if not doc.book.accessible(request):
305 return HttpResponseForbidden("Not authorized.")
307 # allow diff from the beginning
309 docA = doc.at_revision(revA).materialize()
312 docB = doc.at_revision(revB).materialize()
314 return http.HttpResponse(nice_diff.html_diff_table(docA.splitlines(),
315 docB.splitlines(), context=3))
319 def revision(request, chunk_id):
320 doc = get_object_or_404(Chunk, pk=chunk_id)
321 if not doc.book.accessible(request):
322 return HttpResponseForbidden("Not authorized.")
323 Presence = apps.get_model('team', 'Presence')
324 Presence.report(request.user, doc, request.GET.get('a') == 'true')
325 return http.HttpResponse(str(doc.revision()))
329 def history(request, chunk_id):
331 doc = get_object_or_404(Chunk, pk=chunk_id)
332 if not doc.book.accessible(request):
333 return HttpResponseForbidden("Not authorized.")
335 history = doc.history()
337 before = int(request.GET.get('before'))
341 history = history.filter(revision__lt=before)
343 for change in history.reverse()[:20]:
345 "version": change.revision,
346 "description": change.description,
347 "author": change.author_str(),
348 "date": localize(change.created_at),
349 "publishable": change.publishable,
350 "tag": ',\n'.join(str(tag) for tag in change.tags.all()),
351 "published": _("Published") + ": " + \
352 localize(change.publish_log.order_by('-book_record__timestamp')[0].book_record.timestamp) \
353 if change.publish_log.exists() else "",
355 return JSONResponse(changes)
359 @ajax_require_permission('documents.can_pubmark')
360 def pubmark(request, chunk_id):
361 form = forms.DocumentPubmarkForm(request.POST, prefix="pubmark")
363 doc = get_object_or_404(Chunk, pk=chunk_id)
364 if not doc.book.accessible(request):
365 return HttpResponseForbidden("Not authorized.")
367 revision = form.cleaned_data['revision']
368 publishable = form.cleaned_data['publishable']
369 change = doc.at_revision(revision)
370 if publishable != change.publishable:
371 change.set_publishable(publishable)
372 return JSONResponse({"message": _("Revision marked")})
374 return JSONResponse({"message": _("Nothing changed")})
376 return JSONFormInvalid(form)
380 @ajax_require_permission('documents.book_edit')
381 def set_gallery(request, chunk_id):
382 doc = get_object_or_404(Chunk, pk=chunk_id)
384 book.gallery = request.POST['gallery']
385 book.save(update_fields=['gallery'])
386 return JSONResponse({})
389 @ajax_require_permission('documents.chunk_edit')
390 def set_gallery_start(request, chunk_id):
391 doc = get_object_or_404(Chunk, pk=chunk_id)
392 doc.gallery_start = request.POST['start']
393 doc.save(update_fields=['gallery_start'])
394 return JSONResponse({})
396 @ajax_require_permission('documents.chunk_edit')
397 def galleries(request):
410 prefix = request.GET.get('q', '')
411 return http.HttpResponse('\n'.join([str(t) for t in Theme.objects.filter(name__istartswith=prefix)]))
415 return render(request, 'wiki/back.html')