opt-in presence record
[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
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
28
29 #
30 # Quick hack around caching problems, TODO: use ETags
31 #
32 from django.views.decorators.cache import never_cache
33
34 logger = logging.getLogger("fnp.wiki")
35
36 MAX_LAST_DOCS = 10
37
38
39 @never_cache
40 def editor(request, slug, chunk=None, template_name='wiki/document_details.html'):
41     try:
42         chunk = Chunk.get(slug, chunk)
43     except Chunk.MultipleObjectsReturned:
44         # TODO: choice page
45         raise Http404
46     except Chunk.DoesNotExist:
47         if chunk is None:
48             try:
49                 book = Book.objects.get(slug=slug)
50             except Book.DoesNotExist:
51                 return http.HttpResponseRedirect(reverse("documents_create_missing", args=[slug]))
52         else:
53             raise Http404
54     if not chunk.book.accessible(request):
55         return HttpResponseForbidden("Not authorized.")
56
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(),
62         }
63
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
68
69     return render(request, template_name, {
70         'chunk': chunk,
71         'forms': {
72             "text_save": forms.DocumentTextSaveForm(user=request.user, prefix="textsave"),
73             "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
74             "pubmark": forms.DocumentPubmarkForm(prefix="pubmark"),
75         },
76         'can_pubmark': request.user.has_perm('documents.can_pubmark'),
77         'REDMINE_URL': settings.REDMINE_URL,
78     })
79
80
81 def editor_user_area(request):
82     return render(request, 'wiki/editor-user-area.html', {
83         'forms': {
84             "text_save": forms.DocumentTextSaveForm(user=request.user, prefix="textsave"),
85             "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
86             "pubmark": forms.DocumentPubmarkForm(prefix="pubmark"),
87         },
88         'can_pubmark': request.user.has_perm('documents.can_pubmark'),
89     })
90
91
92 @require_GET
93 def editor_readonly(request, slug, chunk=None, template_name='wiki/document_details_readonly.html'):
94     try:
95         chunk = Chunk.get(slug, chunk)
96         revision = request.GET['revision']
97     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist, KeyError):
98         raise Http404
99     if not chunk.book.accessible(request):
100         return HttpResponseForbidden("Not authorized.")
101
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,
107         }
108
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
113
114     return render(request, template_name, {
115         'chunk': chunk,
116         'revision': revision,
117         'readonly': True,
118         'REDMINE_URL': settings.REDMINE_URL,
119     })
120
121
122 @never_cache
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.")
128
129     if request.method == 'POST':
130         form = forms.DocumentTextSaveForm(request.POST, user=request.user, prefix="textsave")
131         if form.is_valid():
132             if request.user.is_authenticated:
133                 author = request.user
134             else:
135                 author = None
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)
140             else:
141                 parent = None
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,
147                        text=text,
148                        parent=parent,
149                        description=form.cleaned_data['comment'],
150                        tags=tags,
151                        author_name=form.cleaned_data['author_name'],
152                        author_email=form.cleaned_data['author_email'],
153                        publishable=publishable,
154                        )
155             revision = doc.revision()
156             return JSONResponse({
157                 'text': doc.materialize() if parent_revision != revision else None,
158                 'meta': {},
159                 'revision': revision,
160             })
161         else:
162             return JSONFormInvalid(form)
163     else:
164         revision = request.GET.get("revision", None)
165         
166         try:
167             revision = int(revision)
168         except (ValueError, TypeError):
169             revision = doc.revision()
170
171         if revision is not None:
172             text = doc.at_revision(revision).materialize()
173         else:
174             text = ''
175
176         return JSONResponse({
177             'text': text,
178             'meta': {},
179             'revision': revision,
180         })
181
182
183 @never_cache
184 @require_POST
185 def revert(request, chunk_id):
186     form = forms.DocumentTextRevertForm(request.POST, prefix="textrevert")
187     if form.is_valid():
188         doc = get_object_or_404(Chunk, pk=chunk_id)
189         if not doc.book.accessible(request):
190             return HttpResponseForbidden("Not authorized.")
191
192         revision = form.cleaned_data['revision']
193
194         comment = form.cleaned_data['comment']
195         comment += "\n#revert to %s" % revision
196
197         if request.user.is_authenticated:
198             author = request.user
199         else:
200             author = None
201
202         before = doc.revision()
203         logger.info("Reverting %s to %s", chunk_id, revision)
204         doc.at_revision(revision).revert(author=author, description=comment)
205
206         return JSONResponse({
207             'text': doc.materialize() if before != doc.revision() else None,
208             'meta': {},
209             'revision': doc.revision(),
210         })
211     else:
212         return JSONFormInvalid(form)
213
214
215 @never_cache
216 def gallery(request, directory):
217     try:
218         base_url = ''.join((
219                         settings.MEDIA_URL,
220                         settings.IMAGE_DIR,
221                         directory))
222
223         base_dir = os.path.join(
224                     settings.MEDIA_ROOT,
225                     settings.IMAGE_DIR,
226                     directory)
227
228         def map_to_url(filename):
229             return quote(("%s/%s" % (base_url, filename)))
230
231         def is_image(filename):
232             return os.path.splitext(filename)[1].lower() in (u'.jpg', u'.jpeg', u'.png')
233
234         images = [map_to_url(f) for f in os.listdir(base_dir) if is_image(f)]
235         images.sort()
236
237         books = Book.objects.filter(gallery=directory)
238
239         if not all(book.public for book in books) and not request.user.is_authenticated:
240             return HttpResponseForbidden("Not authorized.")
241
242         return JSONResponse(images)
243     except (IndexError, OSError):
244         logger.exception("Unable to fetch gallery")
245         raise http.Http404
246
247
248 @never_cache
249 def diff(request, chunk_id):
250     revA = int(request.GET.get('from', 0))
251     revB = int(request.GET.get('to', 0))
252
253     if revA > revB:
254         revA, revB = revB, revA
255
256     if revB == 0:
257         revB = None
258
259     doc = get_object_or_404(Chunk, pk=chunk_id)
260     if not doc.book.accessible(request):
261         return HttpResponseForbidden("Not authorized.")
262
263     # allow diff from the beginning
264     if revA:
265         docA = doc.at_revision(revA).materialize()
266     else:
267         docA = ""
268     docB = doc.at_revision(revB).materialize()
269
270     return http.HttpResponse(nice_diff.html_diff_table(docA.splitlines(),
271                                          docB.splitlines(), context=3))
272
273
274 @never_cache
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()))
282
283
284 @never_cache
285 def history(request, chunk_id):
286     # TODO: pagination
287     doc = get_object_or_404(Chunk, pk=chunk_id)
288     if not doc.book.accessible(request):
289         return HttpResponseForbidden("Not authorized.")
290
291     changes = []
292     for change in doc.history().reverse():
293         changes.append({
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 "",
303             })
304     return JSONResponse(changes)
305
306
307 @require_POST
308 @ajax_require_permission('documents.can_pubmark')
309 def pubmark(request, chunk_id):
310     form = forms.DocumentPubmarkForm(request.POST, prefix="pubmark")
311     if form.is_valid():
312         doc = get_object_or_404(Chunk, pk=chunk_id)
313         if not doc.book.accessible(request):
314             return HttpResponseForbidden("Not authorized.")
315
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")})
322         else:
323             return JSONResponse({"message": _("Nothing changed")})
324     else:
325         return JSONFormInvalid(form)
326
327
328 def themes(request):
329     prefix = request.GET.get('q', '')
330     return http.HttpResponse('\n'.join([str(t) for t in Theme.objects.filter(name__istartswith=prefix)]))
331
332
333 def back(request):
334     return render(request, 'wiki/back.html')