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.conf import settings
11 from django.urls import reverse
12 from django import http
13 from django.http import Http404, HttpResponseForbidden
14 from django.middleware.gzip import GZipMiddleware
15 from django.utils.decorators import decorator_from_middleware
16 from django.utils.formats import localize
17 from django.utils.translation import gettext as _
18 from django.views.decorators.http import require_POST, require_GET
19 from django.shortcuts import get_object_or_404, render
21 from documents.models import Book, Chunk
22 from . import nice_diff
23 from wiki import forms
24 from wiki.helpers import (JSONResponse, JSONFormInvalid, JSONServerError,
25 ajax_require_permission)
26 from wiki.models import Theme
29 # Quick hack around caching problems, TODO: use ETags
31 from django.views.decorators.cache import never_cache
33 logger = logging.getLogger("fnp.wiki")
39 def editor(request, slug, chunk=None, template_name='wiki/document_details.html'):
41 chunk = Chunk.get(slug, chunk)
42 except Chunk.MultipleObjectsReturned:
45 except Chunk.DoesNotExist:
48 book = Book.objects.get(slug=slug)
49 except Book.DoesNotExist:
50 return http.HttpResponseRedirect(reverse("documents_create_missing", args=[slug]))
53 if not chunk.book.accessible(request):
54 return HttpResponseForbidden("Not authorized.")
56 access_time = datetime.now()
57 last_books = request.session.get("wiki_last_books", {})
58 last_books[reverse(editor, args=[chunk.book.slug, chunk.slug])] = {
59 'time': mktime(access_time.timetuple()),
60 'title': chunk.pretty_name(),
63 if len(last_books) > MAX_LAST_DOCS:
64 oldest_key = min(last_books, key=lambda x: last_books[x]['time'])
65 del last_books[oldest_key]
66 request.session['wiki_last_books'] = last_books
68 return render(request, template_name, {
71 "text_save": forms.DocumentTextSaveForm(user=request.user, prefix="textsave"),
72 "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
73 "pubmark": forms.DocumentPubmarkForm(prefix="pubmark"),
75 'can_pubmark': request.user.has_perm('documents.can_pubmark'),
76 'REDMINE_URL': settings.REDMINE_URL,
80 def editor_user_area(request):
81 return render(request, 'wiki/editor-user-area.html', {
83 "text_save": forms.DocumentTextSaveForm(user=request.user, prefix="textsave"),
84 "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
85 "pubmark": forms.DocumentPubmarkForm(prefix="pubmark"),
87 'can_pubmark': request.user.has_perm('documents.can_pubmark'),
92 def editor_readonly(request, slug, chunk=None, template_name='wiki/document_details_readonly.html'):
94 chunk = Chunk.get(slug, chunk)
95 revision = request.GET['revision']
96 except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist, KeyError):
98 if not chunk.book.accessible(request):
99 return HttpResponseForbidden("Not authorized.")
101 access_time = datetime.now()
102 last_books = request.session.get("wiki_last_books", {})
103 last_books[slug, chunk.slug] = {
104 'time': mktime(access_time.timetuple()),
105 'title': chunk.book.title,
108 if len(last_books) > MAX_LAST_DOCS:
109 oldest_key = min(last_books, key=lambda x: last_books[x]['time'])
110 del last_books[oldest_key]
111 request.session['wiki_last_books'] = last_books
113 return render(request, template_name, {
115 'revision': revision,
117 'REDMINE_URL': settings.REDMINE_URL,
122 @decorator_from_middleware(GZipMiddleware)
123 def text(request, chunk_id):
124 doc = get_object_or_404(Chunk, pk=chunk_id)
125 if not doc.book.accessible(request):
126 return HttpResponseForbidden("Not authorized.")
128 if request.method == 'POST':
129 form = forms.DocumentTextSaveForm(request.POST, user=request.user, prefix="textsave")
131 if request.user.is_authenticated:
132 author = request.user
135 text = form.cleaned_data['text']
136 parent_revision = form.cleaned_data['parent_revision']
137 if parent_revision is not None:
138 parent = doc.at_revision(parent_revision)
141 stage = form.cleaned_data['stage_completed']
142 tags = [stage] if stage else []
143 publishable = (form.cleaned_data['publishable'] and
144 request.user.has_perm('documents.can_pubmark'))
145 doc.commit(author=author,
148 description=form.cleaned_data['comment'],
150 author_name=form.cleaned_data['author_name'],
151 author_email=form.cleaned_data['author_email'],
152 publishable=publishable,
154 revision = doc.revision()
155 return JSONResponse({
156 'text': doc.materialize() if parent_revision != revision else None,
158 'revision': revision,
161 return JSONFormInvalid(form)
163 revision = request.GET.get("revision", None)
166 revision = int(revision)
167 except (ValueError, TypeError):
168 revision = doc.revision()
170 if revision is not None:
171 text = doc.at_revision(revision).materialize()
175 return JSONResponse({
178 'revision': revision,
184 def revert(request, chunk_id):
185 form = forms.DocumentTextRevertForm(request.POST, prefix="textrevert")
187 doc = get_object_or_404(Chunk, pk=chunk_id)
188 if not doc.book.accessible(request):
189 return HttpResponseForbidden("Not authorized.")
191 revision = form.cleaned_data['revision']
193 comment = form.cleaned_data['comment']
194 comment += "\n#revert to %s" % revision
196 if request.user.is_authenticated:
197 author = request.user
201 before = doc.revision()
202 logger.info("Reverting %s to %s", chunk_id, revision)
203 doc.at_revision(revision).revert(author=author, description=comment)
205 return JSONResponse({
206 'text': doc.materialize() if before != doc.revision() else None,
208 'revision': doc.revision(),
211 return JSONFormInvalid(form)
215 def gallery(request, directory):
222 base_dir = os.path.join(
227 def map_to_url(filename):
228 return quote(("%s/%s" % (base_url, filename)))
230 def is_image(filename):
231 return os.path.splitext(filename)[1].lower() in (u'.jpg', u'.jpeg', u'.png')
233 images = [map_to_url(f) for f in os.listdir(base_dir) if is_image(f)]
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.")
241 return JSONResponse(images)
242 except (IndexError, OSError):
243 logger.exception("Unable to fetch gallery")
248 def diff(request, chunk_id):
249 revA = int(request.GET.get('from', 0))
250 revB = int(request.GET.get('to', 0))
253 revA, revB = revB, revA
258 doc = get_object_or_404(Chunk, pk=chunk_id)
259 if not doc.book.accessible(request):
260 return HttpResponseForbidden("Not authorized.")
262 # allow diff from the beginning
264 docA = doc.at_revision(revA).materialize()
267 docB = doc.at_revision(revB).materialize()
269 return http.HttpResponse(nice_diff.html_diff_table(docA.splitlines(),
270 docB.splitlines(), context=3))
274 def revision(request, chunk_id):
275 doc = get_object_or_404(Chunk, pk=chunk_id)
276 if not doc.book.accessible(request):
277 return HttpResponseForbidden("Not authorized.")
278 return http.HttpResponse(str(doc.revision()))
282 def history(request, chunk_id):
284 doc = get_object_or_404(Chunk, pk=chunk_id)
285 if not doc.book.accessible(request):
286 return HttpResponseForbidden("Not authorized.")
289 for change in doc.history().reverse():
291 "version": change.revision,
292 "description": change.description,
293 "author": change.author_str(),
294 "date": localize(change.created_at),
295 "publishable": _("Publishable") + "\n" if change.publishable else "",
296 "tag": ',\n'.join(str(tag) for tag in change.tags.all()),
297 "published": _("Published") + ": " + \
298 localize(change.publish_log.order_by('-book_record__timestamp')[0].book_record.timestamp) \
299 if change.publish_log.exists() else "",
301 return JSONResponse(changes)
305 @ajax_require_permission('documents.can_pubmark')
306 def pubmark(request, chunk_id):
307 form = forms.DocumentPubmarkForm(request.POST, prefix="pubmark")
309 doc = get_object_or_404(Chunk, pk=chunk_id)
310 if not doc.book.accessible(request):
311 return HttpResponseForbidden("Not authorized.")
313 revision = form.cleaned_data['revision']
314 publishable = form.cleaned_data['publishable']
315 change = doc.at_revision(revision)
316 if publishable != change.publishable:
317 change.set_publishable(publishable)
318 return JSONResponse({"message": _("Revision marked")})
320 return JSONResponse({"message": _("Nothing changed")})
322 return JSONFormInvalid(form)
326 prefix = request.GET.get('q', '')
327 return http.HttpResponse('\n'.join([str(t) for t in Theme.objects.filter(name__istartswith=prefix)]))
331 return render(request, 'wiki/back.html')