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
22 from documents.models import Book, Chunk
23 from . import nice_diff
24 from wiki import forms
25 from wiki.helpers import (JSONResponse, JSONFormInvalid, JSONServerError,
26 ajax_require_permission)
27 from wiki.models import Theme
30 # Quick hack around caching problems, TODO: use ETags
32 from django.views.decorators.cache import never_cache
34 logger = logging.getLogger("fnp.wiki")
40 def editor(request, slug, chunk=None, template_name='wiki/document_details.html'):
42 chunk = Chunk.get(slug, chunk)
43 except Chunk.MultipleObjectsReturned:
46 except Chunk.DoesNotExist:
49 book = Book.objects.get(slug=slug)
50 except Book.DoesNotExist:
51 return http.HttpResponseRedirect(reverse("documents_create_missing", args=[slug]))
54 if not chunk.book.accessible(request):
55 return HttpResponseForbidden("Not authorized.")
57 access_time = datetime.now()
58 last_books = request.session.get("wiki_last_books", {})
59 last_books[reverse(editor, args=[chunk.book.slug, chunk.slug])] = {
60 'time': mktime(access_time.timetuple()),
61 'title': chunk.pretty_name(),
64 if len(last_books) > MAX_LAST_DOCS:
65 oldest_key = min(last_books, key=lambda x: last_books[x]['time'])
66 del last_books[oldest_key]
67 request.session['wiki_last_books'] = last_books
69 return render(request, template_name, {
72 "text_save": forms.DocumentTextSaveForm(user=request.user, prefix="textsave"),
73 "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
74 "pubmark": forms.DocumentPubmarkForm(prefix="pubmark"),
76 'can_pubmark': request.user.has_perm('documents.can_pubmark'),
77 'REDMINE_URL': settings.REDMINE_URL,
81 def editor_user_area(request):
82 return render(request, 'wiki/editor-user-area.html', {
84 "text_save": forms.DocumentTextSaveForm(user=request.user, prefix="textsave"),
85 "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
86 "pubmark": forms.DocumentPubmarkForm(prefix="pubmark"),
88 'can_pubmark': request.user.has_perm('documents.can_pubmark'),
93 def editor_readonly(request, slug, chunk=None, template_name='wiki/document_details_readonly.html'):
95 chunk = Chunk.get(slug, chunk)
96 revision = request.GET['revision']
97 except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist, KeyError):
99 if not chunk.book.accessible(request):
100 return HttpResponseForbidden("Not authorized.")
102 access_time = datetime.now()
103 last_books = request.session.get("wiki_last_books", {})
104 last_books[slug, chunk.slug] = {
105 'time': mktime(access_time.timetuple()),
106 'title': chunk.book.title,
109 if len(last_books) > MAX_LAST_DOCS:
110 oldest_key = min(last_books, key=lambda x: last_books[x]['time'])
111 del last_books[oldest_key]
112 request.session['wiki_last_books'] = last_books
114 return render(request, template_name, {
116 'revision': revision,
118 'REDMINE_URL': settings.REDMINE_URL,
123 @decorator_from_middleware(GZipMiddleware)
124 def text(request, chunk_id):
125 doc = get_object_or_404(Chunk, pk=chunk_id)
126 if not doc.book.accessible(request):
127 return HttpResponseForbidden("Not authorized.")
129 if request.method == 'POST':
130 form = forms.DocumentTextSaveForm(request.POST, user=request.user, prefix="textsave")
132 if request.user.is_authenticated:
133 author = request.user
136 text = form.cleaned_data['text']
137 parent_revision = form.cleaned_data['parent_revision']
138 if parent_revision is not None:
139 parent = doc.at_revision(parent_revision)
142 stage = form.cleaned_data['stage_completed']
143 tags = [stage] if stage else []
144 publishable = (form.cleaned_data['publishable'] and
145 request.user.has_perm('documents.can_pubmark'))
146 doc.commit(author=author,
149 description=form.cleaned_data['comment'],
151 author_name=form.cleaned_data['author_name'],
152 author_email=form.cleaned_data['author_email'],
153 publishable=publishable,
155 revision = doc.revision()
156 return JSONResponse({
157 'text': doc.materialize() if parent_revision != revision else None,
159 'revision': revision,
162 return JSONFormInvalid(form)
164 revision = request.GET.get("revision", None)
167 revision = int(revision)
168 except (ValueError, TypeError):
169 revision = doc.revision()
171 if revision is not None:
172 text = doc.at_revision(revision).materialize()
176 return JSONResponse({
179 'revision': revision,
185 def revert(request, chunk_id):
186 form = forms.DocumentTextRevertForm(request.POST, prefix="textrevert")
188 doc = get_object_or_404(Chunk, pk=chunk_id)
189 if not doc.book.accessible(request):
190 return HttpResponseForbidden("Not authorized.")
192 revision = form.cleaned_data['revision']
194 comment = form.cleaned_data['comment']
195 comment += "\n#revert to %s" % revision
197 if request.user.is_authenticated:
198 author = request.user
202 before = doc.revision()
203 logger.info("Reverting %s to %s", chunk_id, revision)
204 doc.at_revision(revision).revert(author=author, description=comment)
206 return JSONResponse({
207 'text': doc.materialize() if before != doc.revision() else None,
209 'revision': doc.revision(),
212 return JSONFormInvalid(form)
216 def gallery(request, directory):
223 base_dir = os.path.join(
228 def map_to_url(filename):
229 return quote(("%s/%s" % (base_url, filename)))
231 def is_image(filename):
232 return os.path.splitext(filename)[1].lower() in (u'.jpg', u'.jpeg', u'.png')
234 images = [map_to_url(f) for f in os.listdir(base_dir) if is_image(f)]
237 books = Book.objects.filter(gallery=directory)
239 if not all(book.public for book in books) and not request.user.is_authenticated:
240 return HttpResponseForbidden("Not authorized.")
242 return JSONResponse(images)
243 except (IndexError, OSError):
244 logger.exception("Unable to fetch gallery")
249 def diff(request, chunk_id):
250 revA = int(request.GET.get('from', 0))
251 revB = int(request.GET.get('to', 0))
254 revA, revB = revB, revA
259 doc = get_object_or_404(Chunk, pk=chunk_id)
260 if not doc.book.accessible(request):
261 return HttpResponseForbidden("Not authorized.")
263 # allow diff from the beginning
265 docA = doc.at_revision(revA).materialize()
268 docB = doc.at_revision(revB).materialize()
270 return http.HttpResponse(nice_diff.html_diff_table(docA.splitlines(),
271 docB.splitlines(), context=3))
275 def revision(request, chunk_id):
276 doc = get_object_or_404(Chunk, pk=chunk_id)
277 if not doc.book.accessible(request):
278 return HttpResponseForbidden("Not authorized.")
279 Presence = apps.get_model('team', 'Presence')
280 Presence.report(request.user, doc, request.GET.get('a') == 'true')
281 return http.HttpResponse(str(doc.revision()))
285 def history(request, chunk_id):
287 doc = get_object_or_404(Chunk, pk=chunk_id)
288 if not doc.book.accessible(request):
289 return HttpResponseForbidden("Not authorized.")
292 for change in doc.history().reverse():
294 "version": change.revision,
295 "description": change.description,
296 "author": change.author_str(),
297 "date": localize(change.created_at),
298 "publishable": _("Publishable") + "\n" if change.publishable else "",
299 "tag": ',\n'.join(str(tag) for tag in change.tags.all()),
300 "published": _("Published") + ": " + \
301 localize(change.publish_log.order_by('-book_record__timestamp')[0].book_record.timestamp) \
302 if change.publish_log.exists() else "",
304 return JSONResponse(changes)
308 @ajax_require_permission('documents.can_pubmark')
309 def pubmark(request, chunk_id):
310 form = forms.DocumentPubmarkForm(request.POST, prefix="pubmark")
312 doc = get_object_or_404(Chunk, pk=chunk_id)
313 if not doc.book.accessible(request):
314 return HttpResponseForbidden("Not authorized.")
316 revision = form.cleaned_data['revision']
317 publishable = form.cleaned_data['publishable']
318 change = doc.at_revision(revision)
319 if publishable != change.publishable:
320 change.set_publishable(publishable)
321 return JSONResponse({"message": _("Revision marked")})
323 return JSONResponse({"message": _("Nothing changed")})
325 return JSONFormInvalid(form)
329 prefix = request.GET.get('q', '')
330 return http.HttpResponse('\n'.join([str(t) for t in Theme.objects.filter(name__istartswith=prefix)]))
334 return render(request, 'wiki/back.html')