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
24 from . import nice_diff
25 from wiki import forms
26 from wiki.helpers import (JSONResponse, JSONFormInvalid, JSONServerError,
27 ajax_require_permission)
28 from wiki.models import Theme
31 # Quick hack around caching problems, TODO: use ETags
33 from django.views.decorators.cache import never_cache
35 logger = logging.getLogger("fnp.wiki")
41 def editor(request, slug, chunk=None, template_name='wiki/document_details.html'):
43 chunk = Chunk.get(slug, chunk)
44 except Chunk.MultipleObjectsReturned:
47 except Chunk.DoesNotExist:
50 book = Book.objects.get(slug=slug)
51 except Book.DoesNotExist:
52 return http.HttpResponseRedirect(reverse("documents_create_missing", args=[slug]))
55 if not chunk.book.accessible(request):
56 return HttpResponseForbidden("Not authorized.")
58 access_time = datetime.now()
59 last_books = request.session.get("wiki_last_books", {})
60 last_books[reverse(editor, args=[chunk.book.slug, chunk.slug])] = {
61 'time': mktime(access_time.timetuple()),
62 'title': chunk.pretty_name(),
65 if len(last_books) > MAX_LAST_DOCS:
66 oldest_key = min(last_books, key=lambda x: last_books[x]['time'])
67 del last_books[oldest_key]
68 request.session['wiki_last_books'] = last_books
70 return render(request, template_name, {
73 "text_save": forms.DocumentTextSaveForm(user=request.user, prefix="textsave"),
74 "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
75 "pubmark": forms.DocumentPubmarkForm(prefix="pubmark"),
77 'can_pubmark': request.user.has_perm('documents.can_pubmark'),
78 'REDMINE_URL': settings.REDMINE_URL,
82 def editor_user_area(request):
83 return render(request, 'wiki/editor-user-area.html', {
85 "text_save": forms.DocumentTextSaveForm(user=request.user, prefix="textsave"),
86 "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
87 "pubmark": forms.DocumentPubmarkForm(prefix="pubmark"),
89 'can_pubmark': request.user.has_perm('documents.can_pubmark'),
94 def editor_readonly(request, slug, chunk=None, template_name='wiki/document_details_readonly.html'):
96 chunk = Chunk.get(slug, chunk)
97 revision = request.GET['revision']
98 except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist, KeyError):
100 if not chunk.book.accessible(request):
101 return HttpResponseForbidden("Not authorized.")
103 access_time = datetime.now()
104 last_books = request.session.get("wiki_last_books", {})
105 last_books[slug, chunk.slug] = {
106 'time': mktime(access_time.timetuple()),
107 'title': chunk.book.title,
110 if len(last_books) > MAX_LAST_DOCS:
111 oldest_key = min(last_books, key=lambda x: last_books[x]['time'])
112 del last_books[oldest_key]
113 request.session['wiki_last_books'] = last_books
115 return render(request, template_name, {
117 'revision': revision,
119 'REDMINE_URL': settings.REDMINE_URL,
124 @decorator_from_middleware(GZipMiddleware)
125 def text(request, chunk_id):
126 doc = get_object_or_404(Chunk, pk=chunk_id)
127 if not doc.book.accessible(request):
128 return HttpResponseForbidden("Not authorized.")
130 if request.method == 'POST':
131 form = forms.DocumentTextSaveForm(request.POST, user=request.user, prefix="textsave")
133 if request.user.is_authenticated:
134 author = request.user
137 text = form.cleaned_data['text']
138 parent_revision = form.cleaned_data['parent_revision']
139 if parent_revision is not None:
140 parent = doc.at_revision(parent_revision)
143 stage = form.cleaned_data['stage_completed']
144 tags = [stage] if stage else []
145 publishable = (form.cleaned_data['publishable'] and
146 request.user.has_perm('documents.can_pubmark'))
147 doc.commit(author=author,
150 description=form.cleaned_data['comment'],
152 author_name=form.cleaned_data['author_name'],
153 author_email=form.cleaned_data['author_email'],
154 publishable=publishable,
156 revision = doc.revision()
157 return JSONResponse({
158 'text': doc.materialize() if parent_revision != revision else None,
160 'revision': revision,
163 return JSONFormInvalid(form)
165 revision = request.GET.get("revision", None)
168 revision = int(revision)
169 except (ValueError, TypeError):
170 revision = doc.revision()
172 if revision is not None:
173 text = doc.at_revision(revision).materialize()
177 return JSONResponse({
180 'revision': revision,
186 def revert(request, chunk_id):
187 form = forms.DocumentTextRevertForm(request.POST, prefix="textrevert")
189 doc = get_object_or_404(Chunk, pk=chunk_id)
190 if not doc.book.accessible(request):
191 return HttpResponseForbidden("Not authorized.")
193 revision = form.cleaned_data['revision']
195 comment = form.cleaned_data['comment']
196 comment += "\n#revert to %s" % revision
198 if request.user.is_authenticated:
199 author = request.user
203 before = doc.revision()
204 logger.info("Reverting %s to %s", chunk_id, revision)
205 doc.at_revision(revision).revert(author=author, description=comment)
207 return JSONResponse({
208 'text': doc.materialize() if before != doc.revision() else None,
210 'revision': doc.revision(),
213 return JSONFormInvalid(form)
217 def gallery(request, directory):
224 base_dir = os.path.join(
229 def map_to_url(filename):
230 return quote(("%s/%s" % (base_url, filename)))
232 def is_image(filename):
233 return os.path.splitext(filename)[1].lower() in (u'.jpg', u'.jpeg', u'.png')
235 books = Book.objects.filter(gallery=directory)
237 if not all(book.public for book in books) and not request.user.is_authenticated:
238 return HttpResponseForbidden("Not authorized.")
242 "url": map_to_url(f),
243 "thumb": get_thumbnail(os.path.join(base_dir, f), '120x120').url
244 } for f in sorted(os.listdir(base_dir)) if is_image(f)
247 return JSONResponse(images)
248 except (IndexError, OSError):
249 logger.exception("Unable to fetch gallery")
254 def diff(request, chunk_id):
255 revA = int(request.GET.get('from', 0))
256 revB = int(request.GET.get('to', 0))
259 revA, revB = revB, revA
264 doc = get_object_or_404(Chunk, pk=chunk_id)
265 if not doc.book.accessible(request):
266 return HttpResponseForbidden("Not authorized.")
268 # allow diff from the beginning
270 docA = doc.at_revision(revA).materialize()
273 docB = doc.at_revision(revB).materialize()
275 return http.HttpResponse(nice_diff.html_diff_table(docA.splitlines(),
276 docB.splitlines(), context=3))
280 def revision(request, chunk_id):
281 doc = get_object_or_404(Chunk, pk=chunk_id)
282 if not doc.book.accessible(request):
283 return HttpResponseForbidden("Not authorized.")
284 Presence = apps.get_model('team', 'Presence')
285 Presence.report(request.user, doc, request.GET.get('a') == 'true')
286 return http.HttpResponse(str(doc.revision()))
290 def history(request, chunk_id):
292 doc = get_object_or_404(Chunk, pk=chunk_id)
293 if not doc.book.accessible(request):
294 return HttpResponseForbidden("Not authorized.")
296 history = doc.history()
298 before = int(request.GET.get('before'))
302 history = history.filter(revision__lt=before)
304 for change in history.reverse()[:20]:
306 "version": change.revision,
307 "description": change.description,
308 "author": change.author_str(),
309 "date": localize(change.created_at),
310 "publishable": _("Publishable") + "\n" if change.publishable else "",
311 "tag": ',\n'.join(str(tag) for tag in change.tags.all()),
312 "published": _("Published") + ": " + \
313 localize(change.publish_log.order_by('-book_record__timestamp')[0].book_record.timestamp) \
314 if change.publish_log.exists() else "",
316 return JSONResponse(changes)
320 @ajax_require_permission('documents.can_pubmark')
321 def pubmark(request, chunk_id):
322 form = forms.DocumentPubmarkForm(request.POST, prefix="pubmark")
324 doc = get_object_or_404(Chunk, pk=chunk_id)
325 if not doc.book.accessible(request):
326 return HttpResponseForbidden("Not authorized.")
328 revision = form.cleaned_data['revision']
329 publishable = form.cleaned_data['publishable']
330 change = doc.at_revision(revision)
331 if publishable != change.publishable:
332 change.set_publishable(publishable)
333 return JSONResponse({"message": _("Revision marked")})
335 return JSONResponse({"message": _("Nothing changed")})
337 return JSONFormInvalid(form)
341 @ajax_require_permission('documents.book_edit')
342 def set_gallery(request, chunk_id):
343 doc = get_object_or_404(Chunk, pk=chunk_id)
345 book.gallery = request.POST['gallery']
346 book.save(update_fields=['gallery'])
347 return JSONResponse({})
350 @ajax_require_permission('documents.chunk_edit')
351 def set_gallery_start(request, chunk_id):
352 doc = get_object_or_404(Chunk, pk=chunk_id)
353 doc.gallery_start = request.POST['start']
354 doc.save(update_fields=['gallery_start'])
355 return JSONResponse({})
357 @ajax_require_permission('documents.chunk_edit')
358 def galleries(request):
371 prefix = request.GET.get('q', '')
372 return http.HttpResponse('\n'.join([str(t) for t in Theme.objects.filter(name__istartswith=prefix)]))
376 return render(request, 'wiki/back.html')