plwiki for books
[redakcja.git] / src / wiki / views.py
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.
3 #
4 from datetime import datetime
5 import os
6 import logging
7 from time import mktime
8 from urllib.parse import quote
9
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
22
23 from documents.models import Book, Chunk
24 import sources.models
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
30
31 #
32 # Quick hack around caching problems, TODO: use ETags
33 #
34 from django.views.decorators.cache import never_cache
35
36 logger = logging.getLogger("fnp.wiki")
37
38 MAX_LAST_DOCS = 10
39
40
41 @never_cache
42 def editor(request, slug, chunk=None, template_name='wiki/document_details.html'):
43     try:
44         chunk = Chunk.get(slug, chunk)
45     except Chunk.MultipleObjectsReturned:
46         # TODO: choice page
47         raise Http404
48     except Chunk.DoesNotExist:
49         if chunk is None:
50             try:
51                 book = Book.objects.get(slug=slug)
52             except Book.DoesNotExist:
53                 return http.HttpResponseRedirect(reverse("documents_create_missing", args=[slug]))
54         else:
55             raise Http404
56     if not chunk.book.accessible(request):
57         return HttpResponseForbidden("Not authorized.")
58
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(),
64         }
65
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
70
71     return render(request, template_name, {
72         'chunk': chunk,
73         'forms': {
74             "text_save": forms.DocumentTextSaveForm(user=request.user, prefix="textsave"),
75             "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
76             "pubmark": forms.DocumentPubmarkForm(prefix="pubmark"),
77         },
78         'can_pubmark': request.user.has_perm('documents.can_pubmark'),
79         'REDMINE_URL': settings.REDMINE_URL,
80     })
81
82
83 def editor_user_area(request):
84     return render(request, 'wiki/editor-user-area.html', {
85         'forms': {
86             "text_save": forms.DocumentTextSaveForm(user=request.user, prefix="textsave"),
87             "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
88             "pubmark": forms.DocumentPubmarkForm(prefix="pubmark"),
89         },
90         'can_pubmark': request.user.has_perm('documents.can_pubmark'),
91     })
92
93
94 @require_GET
95 def editor_readonly(request, slug, chunk=None, template_name='wiki/document_details_readonly.html'):
96     try:
97         chunk = Chunk.get(slug, chunk)
98         revision = request.GET['revision']
99     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist, KeyError):
100         raise Http404
101     if not chunk.book.accessible(request):
102         return HttpResponseForbidden("Not authorized.")
103
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,
109         }
110
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
115
116     return render(request, template_name, {
117         'chunk': chunk,
118         'revision': revision,
119         'readonly': True,
120         'REDMINE_URL': settings.REDMINE_URL,
121     })
122
123
124 @never_cache
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.")
130
131     if request.method == 'POST':
132         form = forms.DocumentTextSaveForm(request.POST, user=request.user, prefix="textsave")
133         if form.is_valid():
134             if request.user.is_authenticated:
135                 author = request.user
136             else:
137                 author = None
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)
142             else:
143                 parent = None
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,
149                        text=text,
150                        parent=parent,
151                        description=form.cleaned_data['comment'],
152                        tags=tags,
153                        author_name=form.cleaned_data['author_name'],
154                        author_email=form.cleaned_data['author_email'],
155                        publishable=publishable,
156                        )
157             revision = doc.revision()
158             return JSONResponse({
159                 'text': doc.materialize() if parent_revision != revision else None,
160                 'meta': {},
161                 'revision': revision,
162             })
163         else:
164             return JSONFormInvalid(form)
165     else:
166         revision = request.GET.get("revision", None)
167         
168         try:
169             revision = int(revision)
170         except (ValueError, TypeError):
171             revision = doc.revision()
172
173         if revision is not None:
174             text = doc.at_revision(revision).materialize()
175         else:
176             text = ''
177
178         return JSONResponse({
179             'text': text,
180             'meta': {},
181             'revision': revision,
182         })
183
184
185 @never_cache
186 @require_POST
187 def revert(request, chunk_id):
188     form = forms.DocumentTextRevertForm(request.POST, prefix="textrevert")
189     if form.is_valid():
190         doc = get_object_or_404(Chunk, pk=chunk_id)
191         if not doc.book.accessible(request):
192             return HttpResponseForbidden("Not authorized.")
193
194         revision = form.cleaned_data['revision']
195
196         comment = form.cleaned_data['comment']
197         comment += "\n#revert to %s" % revision
198
199         if request.user.is_authenticated:
200             author = request.user
201         else:
202             author = None
203
204         before = doc.revision()
205         logger.info("Reverting %s to %s", chunk_id, revision)
206         doc.at_revision(revision).revert(author=author, description=comment)
207
208         return JSONResponse({
209             'text': doc.materialize() if before != doc.revision() else None,
210             'meta': {},
211             'revision': doc.revision(),
212         })
213     else:
214         return JSONFormInvalid(form)
215
216
217 @never_cache
218 def gallery(request, directory):
219     try:
220         base_url = ''.join((
221                         settings.MEDIA_URL,
222                         settings.IMAGE_DIR,
223                         directory))
224
225         base_dir = os.path.join(
226                     settings.MEDIA_ROOT,
227                     settings.IMAGE_DIR,
228                     directory)
229
230         def map_to_url(filename):
231             return quote(("%s/%s" % (base_url, filename)))
232
233         def is_image(filename):
234             return os.path.splitext(filename)[1].lower() in (u'.jpg', u'.jpeg', u'.png')
235
236         books = Book.objects.filter(gallery=directory)
237
238         if not all(book.public for book in books) and not request.user.is_authenticated:
239             return HttpResponseForbidden("Not authorized.")
240
241         images = [
242             {
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)
246         ]
247
248         return JSONResponse(images)
249     except (IndexError, OSError):
250         logger.exception("Unable to fetch gallery")
251         raise http.Http404
252
253
254 @never_cache
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)))
259     images = [
260         {
261             "url": map_to_url(f),
262         } for f in bs.get_view_files()
263     ]
264     return JSONResponse(images)
265
266
267 @never_cache
268 def diff(request, chunk_id):
269     revA = int(request.GET.get('from', 0))
270     revB = int(request.GET.get('to', 0))
271
272     if revA > revB:
273         revA, revB = revB, revA
274
275     if revB == 0:
276         revB = None
277
278     doc = get_object_or_404(Chunk, pk=chunk_id)
279     if not doc.book.accessible(request):
280         return HttpResponseForbidden("Not authorized.")
281
282     # allow diff from the beginning
283     if revA:
284         docA = doc.at_revision(revA).materialize()
285     else:
286         docA = ""
287     docB = doc.at_revision(revB).materialize()
288
289     return http.HttpResponse(nice_diff.html_diff_table(docA.splitlines(),
290                                          docB.splitlines(), context=3))
291
292
293 @never_cache
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()))
301
302
303 @never_cache
304 def history(request, chunk_id):
305     # TODO: pagination
306     doc = get_object_or_404(Chunk, pk=chunk_id)
307     if not doc.book.accessible(request):
308         return HttpResponseForbidden("Not authorized.")
309
310     history = doc.history()
311     try:
312         before = int(request.GET.get('before'))
313     except:
314         pass
315     else:
316         history = history.filter(revision__lt=before)
317     changes = []
318     for change in history.reverse()[:20]:
319         changes.append({
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 "",
329             })
330     return JSONResponse(changes)
331
332
333 @require_POST
334 @ajax_require_permission('documents.can_pubmark')
335 def pubmark(request, chunk_id):
336     form = forms.DocumentPubmarkForm(request.POST, prefix="pubmark")
337     if form.is_valid():
338         doc = get_object_or_404(Chunk, pk=chunk_id)
339         if not doc.book.accessible(request):
340             return HttpResponseForbidden("Not authorized.")
341
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")})
348         else:
349             return JSONResponse({"message": _("Nothing changed")})
350     else:
351         return JSONFormInvalid(form)
352
353
354 @require_POST
355 @ajax_require_permission('documents.book_edit')
356 def set_gallery(request, chunk_id):
357     doc = get_object_or_404(Chunk, pk=chunk_id)
358     book = doc.book
359     book.gallery = request.POST['gallery']
360     book.save(update_fields=['gallery'])
361     return JSONResponse({})
362
363 @require_POST
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({})
370
371 @ajax_require_permission('documents.chunk_edit')
372 def galleries(request):
373     return JSONResponse(
374         sorted(
375             os.listdir(
376                 os.path.join(
377                     settings.MEDIA_ROOT,
378                     settings.IMAGE_DIR,
379                 )
380             )
381         )
382     )
383
384 def themes(request):
385     prefix = request.GET.get('q', '')
386     return http.HttpResponse('\n'.join([str(t) for t in Theme.objects.filter(name__istartswith=prefix)]))
387
388
389 def back(request):
390     return render(request, 'wiki/back.html')