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 ugettext 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,
81 def editor_readonly(request, slug, chunk=None, template_name='wiki/document_details_readonly.html'):
83 chunk = Chunk.get(slug, chunk)
84 revision = request.GET['revision']
85 except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist, KeyError):
87 if not chunk.book.accessible(request):
88 return HttpResponseForbidden("Not authorized.")
90 access_time = datetime.now()
91 last_books = request.session.get("wiki_last_books", {})
92 last_books[slug, chunk.slug] = {
93 'time': mktime(access_time.timetuple()),
94 'title': chunk.book.title,
97 if len(last_books) > MAX_LAST_DOCS:
98 oldest_key = min(last_books, key=lambda x: last_books[x]['time'])
99 del last_books[oldest_key]
100 request.session['wiki_last_books'] = last_books
102 return render(request, template_name, {
104 'revision': revision,
106 'REDMINE_URL': settings.REDMINE_URL,
111 @decorator_from_middleware(GZipMiddleware)
112 def text(request, chunk_id):
113 doc = get_object_or_404(Chunk, pk=chunk_id)
114 if not doc.book.accessible(request):
115 return HttpResponseForbidden("Not authorized.")
117 if request.method == 'POST':
118 form = forms.DocumentTextSaveForm(request.POST, user=request.user, prefix="textsave")
120 if request.user.is_authenticated:
121 author = request.user
124 text = form.cleaned_data['text']
125 parent_revision = form.cleaned_data['parent_revision']
126 if parent_revision is not None:
127 parent = doc.at_revision(parent_revision)
130 stage = form.cleaned_data['stage_completed']
131 tags = [stage] if stage else []
132 publishable = (form.cleaned_data['publishable'] and
133 request.user.has_perm('documents.can_pubmark'))
134 doc.commit(author=author,
137 description=form.cleaned_data['comment'],
139 author_name=form.cleaned_data['author_name'],
140 author_email=form.cleaned_data['author_email'],
141 publishable=publishable,
143 revision = doc.revision()
144 return JSONResponse({
145 'text': doc.materialize() if parent_revision != revision else None,
147 'revision': revision,
150 return JSONFormInvalid(form)
152 revision = request.GET.get("revision", None)
155 revision = int(revision)
156 except (ValueError, TypeError):
157 revision = doc.revision()
159 if revision is not None:
160 text = doc.at_revision(revision).materialize()
164 return JSONResponse({
167 'revision': revision,
173 def revert(request, chunk_id):
174 form = forms.DocumentTextRevertForm(request.POST, prefix="textrevert")
176 doc = get_object_or_404(Chunk, pk=chunk_id)
177 if not doc.book.accessible(request):
178 return HttpResponseForbidden("Not authorized.")
180 revision = form.cleaned_data['revision']
182 comment = form.cleaned_data['comment']
183 comment += "\n#revert to %s" % revision
185 if request.user.is_authenticated:
186 author = request.user
190 before = doc.revision()
191 logger.info("Reverting %s to %s", chunk_id, revision)
192 doc.at_revision(revision).revert(author=author, description=comment)
194 return JSONResponse({
195 'text': doc.materialize() if before != doc.revision() else None,
197 'revision': doc.revision(),
200 return JSONFormInvalid(form)
204 def gallery(request, directory):
211 base_dir = os.path.join(
216 def map_to_url(filename):
217 return quote(("%s/%s" % (base_url, filename)))
219 def is_image(filename):
220 return os.path.splitext(filename)[1].lower() in (u'.jpg', u'.jpeg', u'.png')
222 images = [map_to_url(f) for f in os.listdir(base_dir) if is_image(f)]
225 books = Book.objects.filter(gallery=directory)
227 if not all(book.public for book in books) and not request.user.is_authenticated:
228 return HttpResponseForbidden("Not authorized.")
230 return JSONResponse(images)
231 except (IndexError, OSError):
232 logger.exception("Unable to fetch gallery")
237 def diff(request, chunk_id):
238 revA = int(request.GET.get('from', 0))
239 revB = int(request.GET.get('to', 0))
242 revA, revB = revB, revA
247 doc = get_object_or_404(Chunk, pk=chunk_id)
248 if not doc.book.accessible(request):
249 return HttpResponseForbidden("Not authorized.")
251 # allow diff from the beginning
253 docA = doc.at_revision(revA).materialize()
256 docB = doc.at_revision(revB).materialize()
258 return http.HttpResponse(nice_diff.html_diff_table(docA.splitlines(),
259 docB.splitlines(), context=3))
263 def revision(request, chunk_id):
264 doc = get_object_or_404(Chunk, pk=chunk_id)
265 if not doc.book.accessible(request):
266 return HttpResponseForbidden("Not authorized.")
267 return http.HttpResponse(str(doc.revision()))
271 def history(request, chunk_id):
273 doc = get_object_or_404(Chunk, pk=chunk_id)
274 if not doc.book.accessible(request):
275 return HttpResponseForbidden("Not authorized.")
278 for change in doc.history().reverse():
280 "version": change.revision,
281 "description": change.description,
282 "author": change.author_str(),
283 "date": localize(change.created_at),
284 "publishable": _("Publishable") + "\n" if change.publishable else "",
285 "tag": ',\n'.join(str(tag) for tag in change.tags.all()),
286 "published": _("Published") + ": " + \
287 localize(change.publish_log.order_by('-book_record__timestamp')[0].book_record.timestamp) \
288 if change.publish_log.exists() else "",
290 return JSONResponse(changes)
294 @ajax_require_permission('documents.can_pubmark')
295 def pubmark(request, chunk_id):
296 form = forms.DocumentPubmarkForm(request.POST, prefix="pubmark")
298 doc = get_object_or_404(Chunk, pk=chunk_id)
299 if not doc.book.accessible(request):
300 return HttpResponseForbidden("Not authorized.")
302 revision = form.cleaned_data['revision']
303 publishable = form.cleaned_data['publishable']
304 change = doc.at_revision(revision)
305 if publishable != change.publishable:
306 change.set_publishable(publishable)
307 return JSONResponse({"message": _("Revision marked")})
309 return JSONResponse({"message": _("Nothing changed")})
311 return JSONFormInvalid(form)
315 prefix = request.GET.get('q', '')
316 return http.HttpResponse('\n'.join([str(t) for t in Theme.objects.filter(name__istartswith=prefix)]))