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