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