wider html book view
[redakcja.git] / apps / wiki / views.py
1 from datetime import datetime
2 import os
3 import logging
4 from time import mktime
5 import urllib
6
7 from django.conf import settings
8 from django.core.urlresolvers import reverse
9 from django import http
10 from django.http import Http404, HttpResponseForbidden
11 from django.middleware.gzip import GZipMiddleware
12 from django.utils.decorators import decorator_from_middleware
13 from django.utils.encoding import smart_unicode
14 from django.utils.formats import localize
15 from django.utils.translation import ugettext as _
16 from django.views.decorators.http import require_POST, require_GET
17 from django.shortcuts import get_object_or_404, render
18
19 from catalogue.models import Book, Chunk
20 import nice_diff
21 from wiki import forms
22 from wiki.helpers import (JSONResponse, JSONFormInvalid, JSONServerError,
23                 ajax_require_permission)
24 from wiki.models import Theme
25
26 #
27 # Quick hack around caching problems, TODO: use ETags
28 #
29 from django.views.decorators.cache import never_cache
30
31 logger = logging.getLogger("fnp.wiki")
32
33 MAX_LAST_DOCS = 10
34
35
36 @never_cache
37 def editor(request, slug, chunk=None, template_name='wiki/document_details.html'):
38     try:
39         chunk = Chunk.get(slug, chunk)
40     except Chunk.MultipleObjectsReturned:
41         # TODO: choice page
42         raise Http404
43     except Chunk.DoesNotExist:
44         if chunk is None:
45             try:
46                 book = Book.objects.get(slug=slug)
47             except Book.DoesNotExist:
48                 return http.HttpResponseRedirect(reverse("catalogue_create_missing", args=[slug]))
49         else:
50             raise Http404
51     if not chunk.book.accessible(request):
52         return HttpResponseForbidden("Not authorized.")
53
54     access_time = datetime.now()
55     last_books = request.session.get("wiki_last_books", {})
56     last_books[reverse(editor, args=[chunk.book.slug, chunk.slug])] = {
57         'time': mktime(access_time.timetuple()),
58         'title': chunk.pretty_name(),
59         }
60
61     if len(last_books) > MAX_LAST_DOCS:
62         oldest_key = min(last_books, key=lambda x: last_books[x]['time'])
63         del last_books[oldest_key]
64     request.session['wiki_last_books'] = last_books
65
66     return render(request, template_name, {
67         'chunk': chunk,
68         'forms': {
69             "text_save": forms.DocumentTextSaveForm(user=request.user, prefix="textsave"),
70             "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
71             "pubmark": forms.DocumentPubmarkForm(prefix="pubmark"),
72         },
73         'can_pubmark': request.user.has_perm('catalogue.can_pubmark'),
74         'REDMINE_URL': settings.REDMINE_URL,
75     })
76
77
78 @require_GET
79 def editor_readonly(request, slug, chunk=None, template_name='wiki/document_details_readonly.html'):
80     try:
81         chunk = Chunk.get(slug, chunk)
82         revision = request.GET['revision']
83     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist, KeyError):
84         raise Http404
85     if not chunk.book.accessible(request):
86         return HttpResponseForbidden("Not authorized.")
87
88     access_time = datetime.now()
89     last_books = request.session.get("wiki_last_books", {})
90     last_books[slug, chunk.slug] = {
91         'time': mktime(access_time.timetuple()),
92         'title': chunk.book.title,
93         }
94
95     if len(last_books) > MAX_LAST_DOCS:
96         oldest_key = min(last_books, key=lambda x: last_books[x]['time'])
97         del last_books[oldest_key]
98     request.session['wiki_last_books'] = last_books
99
100     return render(request, template_name, {
101         'chunk': chunk,
102         'revision': revision,
103         'readonly': True,
104         'REDMINE_URL': settings.REDMINE_URL,
105     })
106
107
108 @never_cache
109 @decorator_from_middleware(GZipMiddleware)
110 def text(request, chunk_id):
111     doc = get_object_or_404(Chunk, pk=chunk_id)
112     if not doc.book.accessible(request):
113         return HttpResponseForbidden("Not authorized.")
114
115     if request.method == 'POST':
116         form = forms.DocumentTextSaveForm(request.POST, user=request.user, prefix="textsave")
117         if form.is_valid():
118             if request.user.is_authenticated():
119                 author = request.user
120             else:
121                 author = None
122             text = form.cleaned_data['text']
123             parent_revision = form.cleaned_data['parent_revision']
124             if parent_revision is not None:
125                 parent = doc.at_revision(parent_revision)
126             else:
127                 parent = None
128             stage = form.cleaned_data['stage_completed']
129             tags = [stage] if stage else []
130             publishable = (form.cleaned_data['publishable'] and
131                     request.user.has_perm('catalogue.can_pubmark'))
132             doc.commit(author=author,
133                        text=text,
134                        parent=parent,
135                        description=form.cleaned_data['comment'],
136                        tags=tags,
137                        author_name=form.cleaned_data['author_name'],
138                        author_email=form.cleaned_data['author_email'],
139                        publishable=publishable,
140                        )
141             revision = doc.revision()
142             return JSONResponse({
143                 'text': doc.materialize() if parent_revision != revision else None,
144                 'meta': {},
145                 'revision': revision,
146             })
147         else:
148             return JSONFormInvalid(form)
149     else:
150         revision = request.GET.get("revision", None)
151         
152         try:
153             revision = int(revision)
154         except (ValueError, TypeError):
155             revision = doc.revision()
156
157         if revision is not None:
158             text = doc.at_revision(revision).materialize()
159         else:
160             text = ''
161
162         return JSONResponse({
163             'text': text,
164             'meta': {},
165             'revision': revision,
166         })
167
168
169 @never_cache
170 @require_POST
171 def revert(request, chunk_id):
172     form = forms.DocumentTextRevertForm(request.POST, prefix="textrevert")
173     if form.is_valid():
174         doc = get_object_or_404(Chunk, pk=chunk_id)
175         if not doc.book.accessible(request):
176             return HttpResponseForbidden("Not authorized.")
177
178         revision = form.cleaned_data['revision']
179
180         comment = form.cleaned_data['comment']
181         comment += "\n#revert to %s" % revision
182
183         if request.user.is_authenticated():
184             author = request.user
185         else:
186             author = None
187
188         before = doc.revision()
189         logger.info("Reverting %s to %s", chunk_id, revision)
190         doc.at_revision(revision).revert(author=author, description=comment)
191
192         return JSONResponse({
193             'text': doc.materialize() if before != doc.revision() else None,
194             'meta': {},
195             'revision': doc.revision(),
196         })
197     else:
198         return JSONFormInvalid(form)
199
200
201 @never_cache
202 def gallery(request, directory):
203     try:
204         base_url = ''.join((
205                         smart_unicode(settings.MEDIA_URL),
206                         smart_unicode(settings.IMAGE_DIR),
207                         smart_unicode(directory)))
208
209         base_dir = os.path.join(
210                     smart_unicode(settings.MEDIA_ROOT),
211                     smart_unicode(settings.IMAGE_DIR),
212                     smart_unicode(directory))
213
214         def map_to_url(filename):
215             return urllib.quote("%s/%s" % (base_url, smart_unicode(filename)))
216
217         def is_image(filename):
218             return os.path.splitext(f)[1].lower() in (u'.jpg', u'.jpeg', u'.png')
219
220         images = [map_to_url(f) for f in map(smart_unicode, os.listdir(base_dir)) if is_image(f)]
221         images.sort()
222
223         book = Book.objects.get(gallery=directory)
224
225         if not book.public and not request.user.is_authenticated():
226             return HttpResponseForbidden("Not authorized.")
227
228         return JSONResponse(images)
229     except (IndexError, OSError):
230         logger.exception("Unable to fetch gallery")
231         raise http.Http404
232
233
234 @never_cache
235 def diff(request, chunk_id):
236     revA = int(request.GET.get('from', 0))
237     revB = int(request.GET.get('to', 0))
238
239     if revA > revB:
240         revA, revB = revB, revA
241
242     if revB == 0:
243         revB = None
244
245     doc = get_object_or_404(Chunk, pk=chunk_id)
246     if not doc.book.accessible(request):
247         return HttpResponseForbidden("Not authorized.")
248
249     # allow diff from the beginning
250     if revA:
251         docA = doc.at_revision(revA).materialize()
252     else:
253         docA = ""
254     docB = doc.at_revision(revB).materialize()
255
256     return http.HttpResponse(nice_diff.html_diff_table(docA.splitlines(),
257                                          docB.splitlines(), context=3))
258
259
260 @never_cache
261 def revision(request, chunk_id):
262     doc = get_object_or_404(Chunk, pk=chunk_id)
263     if not doc.book.accessible(request):
264         return HttpResponseForbidden("Not authorized.")
265     return http.HttpResponse(str(doc.revision()))
266
267
268 @never_cache
269 def history(request, chunk_id):
270     # TODO: pagination
271     doc = get_object_or_404(Chunk, pk=chunk_id)
272     if not doc.book.accessible(request):
273         return HttpResponseForbidden("Not authorized.")
274
275     changes = []
276     for change in doc.history().reverse():
277         changes.append({
278                 "version": change.revision,
279                 "description": change.description,
280                 "author": change.author_str(),
281                 "date": localize(change.created_at),
282                 "publishable": _("Publishable") + "\n" if change.publishable else "",
283                 "tag": ',\n'.join(unicode(tag) for tag in change.tags.all()),
284                 "published": _("Published") + ": " + \
285                     localize(change.publish_log.order_by('-book_record__timestamp')[0].book_record.timestamp) \
286                     if change.publish_log.exists() else "",
287             })
288     return JSONResponse(changes)
289
290
291 @require_POST
292 @ajax_require_permission('catalogue.can_pubmark')
293 def pubmark(request, chunk_id):
294     form = forms.DocumentPubmarkForm(request.POST, prefix="pubmark")
295     if form.is_valid():
296         doc = get_object_or_404(Chunk, pk=chunk_id)
297         if not doc.book.accessible(request):
298             return HttpResponseForbidden("Not authorized.")
299
300         revision = form.cleaned_data['revision']
301         publishable = form.cleaned_data['publishable']
302         change = doc.at_revision(revision)
303         if publishable != change.publishable:
304             change.set_publishable(publishable)
305             return JSONResponse({"message": _("Revision marked")})
306         else:
307             return JSONResponse({"message": _("Nothing changed")})
308     else:
309         return JSONFormInvalid(form)
310
311
312 def themes(request):
313     prefix = request.GET.get('q', '')
314     return http.HttpResponse('\n'.join([str(t) for t in Theme.objects.filter(name__istartswith=prefix)]))