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, pk):
275 bs = get_object_or_404(sources.models.BookSource, pk=pk)
276 def map_to_url(filename):
277 return quote(("%s/%s" % (settings.MEDIA_URL, filename)))
280 "url": map_to_url(f),
281 } for f in bs.get_view_files()
283 return JSONResponse(images)
287 def diff(request, chunk_id):
288 revA = int(request.GET.get('from', 0))
289 revB = int(request.GET.get('to', 0))
292 revA, revB = revB, revA
297 doc = get_object_or_404(Chunk, pk=chunk_id)
298 if not doc.book.accessible(request):
299 return HttpResponseForbidden("Not authorized.")
301 # allow diff from the beginning
303 docA = doc.at_revision(revA).materialize()
306 docB = doc.at_revision(revB).materialize()
308 return http.HttpResponse(nice_diff.html_diff_table(docA.splitlines(),
309 docB.splitlines(), context=3))
313 def revision(request, chunk_id):
314 doc = get_object_or_404(Chunk, pk=chunk_id)
315 if not doc.book.accessible(request):
316 return HttpResponseForbidden("Not authorized.")
317 Presence = apps.get_model('team', 'Presence')
318 Presence.report(request.user, doc, request.GET.get('a') == 'true')
319 return http.HttpResponse(str(doc.revision()))
323 def history(request, chunk_id):
325 doc = get_object_or_404(Chunk, pk=chunk_id)
326 if not doc.book.accessible(request):
327 return HttpResponseForbidden("Not authorized.")
329 history = doc.history()
331 before = int(request.GET.get('before'))
335 history = history.filter(revision__lt=before)
337 for change in history.reverse()[:20]:
339 "version": change.revision,
340 "description": change.description,
341 "author": change.author_str(),
342 "date": localize(change.created_at),
343 "publishable": change.publishable,
344 "tag": ',\n'.join(str(tag) for tag in change.tags.all()),
345 "published": _("Published") + ": " + \
346 localize(change.publish_log.order_by('-book_record__timestamp')[0].book_record.timestamp) \
347 if change.publish_log.exists() else "",
349 return JSONResponse(changes)
353 @ajax_require_permission('documents.can_pubmark')
354 def pubmark(request, chunk_id):
355 form = forms.DocumentPubmarkForm(request.POST, prefix="pubmark")
357 doc = get_object_or_404(Chunk, pk=chunk_id)
358 if not doc.book.accessible(request):
359 return HttpResponseForbidden("Not authorized.")
361 revision = form.cleaned_data['revision']
362 publishable = form.cleaned_data['publishable']
363 change = doc.at_revision(revision)
364 if publishable != change.publishable:
365 change.set_publishable(publishable)
366 return JSONResponse({"message": _("Revision marked")})
368 return JSONResponse({"message": _("Nothing changed")})
370 return JSONFormInvalid(form)
374 @ajax_require_permission('documents.book_edit')
375 def set_gallery(request, chunk_id):
376 doc = get_object_or_404(Chunk, pk=chunk_id)
378 book.gallery = request.POST['gallery']
379 book.save(update_fields=['gallery'])
380 return JSONResponse({})
383 @ajax_require_permission('documents.chunk_edit')
384 def set_gallery_start(request, chunk_id):
385 doc = get_object_or_404(Chunk, pk=chunk_id)
386 doc.gallery_start = request.POST['start']
387 doc.save(update_fields=['gallery_start'])
388 return JSONResponse({})
390 @ajax_require_permission('documents.chunk_edit')
391 def galleries(request):
404 prefix = request.GET.get('q', '')
405 return http.HttpResponse('\n'.join([str(t) for t in Theme.objects.filter(name__istartswith=prefix)]))
409 return render(request, 'wiki/back.html')