+Change.author_desc, minor changes
[redakcja.git] / apps / wiki / views.py
1 import os
2 from StringIO import StringIO
3 import logging
4 logger = logging.getLogger("fnp.wiki")
5
6 from lxml import etree
7
8 from django.conf import settings
9
10 from django.views.generic.simple import direct_to_template
11 from django.views.decorators.http import require_POST, require_GET
12 from django.core.urlresolvers import reverse
13 from wiki.helpers import (JSONResponse, JSONFormInvalid, JSONServerError,
14                 ajax_require_permission, recursive_groupby)
15 from django import http
16 from django.shortcuts import get_object_or_404, redirect
17 from django.http import Http404
18
19 from wiki.models import Book, Chunk, Theme
20 from wiki.forms import DocumentTextSaveForm, DocumentTextRevertForm, DocumentTagForm, DocumentCreateForm, DocumentsUploadForm
21 from datetime import datetime
22 from django.utils.encoding import smart_unicode
23 from django.utils.translation import ugettext_lazy as _
24 from django.utils.decorators import decorator_from_middleware
25 from django.middleware.gzip import GZipMiddleware
26
27 import librarian.html
28 import librarian.text
29
30 #
31 # Quick hack around caching problems, TODO: use ETags
32 #
33 from django.views.decorators.cache import never_cache
34
35 import nice_diff
36 import operator
37
38 MAX_LAST_DOCS = 10
39
40
41 @never_cache
42 def document_list(request):
43     return direct_to_template(request, 'wiki/document_list.html', extra_context={
44         'books': Book.objects.all(),
45         'last_books': sorted(request.session.get("wiki_last_books", {}).items(),
46                         key=lambda x: x[1]['time'], reverse=True),
47     })
48
49
50 @never_cache
51 def editor(request, slug, chunk=None, template_name='wiki/document_details.html'):
52     try:
53         chunk = Chunk.get(slug, chunk)
54     except Chunk.MultipleObjectsReturned:
55         # TODO: choice page
56         raise Http404
57     except Chunk.DoesNotExist:
58         if chunk is None:
59             try:
60                 book = Book.objects.get(slug=slug)
61             except Book.DoesNotExist:
62                 return http.HttpResponseRedirect(reverse("wiki_create_missing", args=[slug]))
63         else:
64             raise Http404
65
66     access_time = datetime.now()
67     last_books = request.session.get("wiki_last_books", {})
68     last_books[slug, chunk.slug] = {
69         'time': access_time,
70         'title': chunk.pretty_name(),
71         }
72
73     if len(last_books) > MAX_LAST_DOCS:
74         oldest_key = min(last_books, key=lambda x: last_books[x]['time'])
75         del last_books[oldest_key]
76     request.session['wiki_last_books'] = last_books
77
78     return direct_to_template(request, template_name, extra_context={
79         'chunk': chunk,
80         'forms': {
81             "text_save": DocumentTextSaveForm(prefix="textsave"),
82             "text_revert": DocumentTextRevertForm(prefix="textrevert"),
83             "add_tag": DocumentTagForm(prefix="addtag"),
84         },
85         'REDMINE_URL': settings.REDMINE_URL,
86     })
87
88
89 @require_GET
90 def editor_readonly(request, slug, chunk=None, template_name='wiki/document_details_readonly.html'):
91     try:
92         chunk = Chunk.get(slug, chunk)
93         revision = request.GET['revision']
94     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist, KeyError):
95         raise Http404
96
97     access_time = datetime.now()
98     last_books = request.session.get("wiki_last_books", {})
99     last_books[slug, chunk.slug] = {
100         'time': access_time,
101         'title': chunk.book.title,
102         }
103
104     if len(last_books) > MAX_LAST_DOCS:
105         oldest_key = min(last_books, key=lambda x: last_books[x]['time'])
106         del last_books[oldest_key]
107     request.session['wiki_last_books'] = last_books
108
109     return direct_to_template(request, template_name, extra_context={
110         'chunk': chunk,
111         'revision': revision,
112         'readonly': True,
113         'REDMINE_URL': settings.REDMINE_URL,
114     })
115
116
117 def create_missing(request, slug):
118     slug = slug.replace(' ', '-')
119
120     if request.method == "POST":
121         form = DocumentCreateForm(request.POST, request.FILES)
122         if form.is_valid():
123             
124             if request.user.is_authenticated():
125                 creator = request.user
126             else:
127                 creator = None
128             book = Book.create(creator=creator,
129                 slug=form.cleaned_data['slug'],
130                 title=form.cleaned_data['title'],
131                 text=form.cleaned_data['text'],
132             )
133
134             return http.HttpResponseRedirect(reverse("wiki_editor", args=[book.slug]))
135     else:
136         form = DocumentCreateForm(initial={
137                 "slug": slug,
138                 "title": slug.replace('-', ' ').title(),
139         })
140
141     return direct_to_template(request, "wiki/document_create_missing.html", extra_context={
142         "slug": slug,
143         "form": form,
144     })
145
146
147 def upload(request):
148     if request.method == "POST":
149         form = DocumentsUploadForm(request.POST, request.FILES)
150         if form.is_valid():
151             import slughifi
152
153             if request.user.is_authenticated():
154                 creator = request.user
155             else:
156                 creator = None
157
158             zip = form.cleaned_data['zip']
159             skipped_list = []
160             ok_list = []
161             error_list = []
162             slugs = {}
163             existing = [book.slug for book in Book.objects.all()]
164             for filename in zip.namelist():
165                 if filename[-1] == '/':
166                     continue
167                 title = os.path.basename(filename)[:-4]
168                 slug = slughifi(title)
169                 if not (slug and filename.endswith('.xml')):
170                     skipped_list.append(filename)
171                 elif slug in slugs:
172                     error_list.append((filename, slug, _('Slug already used for %s' % slugs[slug])))
173                 elif slug in existing:
174                     error_list.append((filename, slug, _('Slug already used in repository.')))
175                 else:
176                     try:
177                         zip.read(filename).decode('utf-8') # test read
178                         ok_list.append((filename, slug, title))
179                     except UnicodeDecodeError:
180                         error_list.append((filename, title, _('File should be UTF-8 encoded.')))
181                     slugs[slug] = filename
182
183             if not error_list:
184                 for filename, slug, title in ok_list:
185                     Book.create(creator=creator,
186                         slug=slug,
187                         title=title,
188                         text=zip.read(filename).decode('utf-8'),
189                     )
190
191             return direct_to_template(request, "wiki/document_upload.html", extra_context={
192                 "form": form,
193                 "ok_list": ok_list,
194                 "skipped_list": skipped_list,
195                 "error_list": error_list,
196             })
197     else:
198         form = DocumentsUploadForm()
199
200     return direct_to_template(request, "wiki/document_upload.html", extra_context={
201         "form": form,
202     })
203
204
205 @never_cache
206 @decorator_from_middleware(GZipMiddleware)
207 def text(request, slug, chunk=None):
208     try:
209         doc = Chunk.get(slug, chunk)
210     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
211         raise Http404
212
213     if request.method == 'POST':
214         form = DocumentTextSaveForm(request.POST, prefix="textsave")
215         if form.is_valid():
216             # TODO:
217             # - stage completion should be stored (as a relation)
218
219             if request.user.is_authenticated():
220                 author = request.user
221             else:
222                 author = None
223             text = form.cleaned_data['text']
224             parent_revision = form.cleaned_data['parent_revision']
225             parent = doc.at_revision(parent_revision)
226             doc.commit(author=author,
227                        text=text,
228                        parent=parent,
229                        description=form.cleaned_data['comment'],
230                        )
231             revision = doc.revision()
232             return JSONResponse({
233                 'text': doc.materialize() if parent_revision != revision else None,
234                 'meta': {},
235                 'revision': revision,
236             })
237         else:
238             return JSONFormInvalid(form)
239     else:
240         revision = request.GET.get("revision", None)
241         
242         try:
243             revision = int(revision)
244         except (ValueError, TypeError):
245             revision = None
246
247         return JSONResponse({
248             'text': doc.at_revision(revision).materialize(),
249             'meta': {},
250             'revision': revision if revision else doc.revision(),
251         })
252
253
254 @never_cache
255 def book_xml(request, slug):
256     xml = get_object_or_404(Book, slug=slug).materialize()
257     
258     response = http.HttpResponse(xml, content_type='application/xml', mimetype='application/wl+xml')
259     response['Content-Disposition'] = 'attachment; filename=%s.xml' % slug
260     return response
261
262
263 @never_cache
264 def book_txt(request, slug):
265     xml = get_object_or_404(Book, slug=slug).materialize()
266     output = StringIO()
267     # errors?
268     librarian.text.transform(StringIO(xml), output)
269     text = output.getvalue()
270     response = http.HttpResponse(text, content_type='text/plain', mimetype='text/plain')
271     response['Content-Disposition'] = 'attachment; filename=%s.txt' % slug
272     return response
273
274
275 @never_cache
276 def book_html(request, slug):
277     xml = get_object_or_404(Book, slug=slug).materialize()
278     output = StringIO()
279     # errors?
280     librarian.html.transform(StringIO(xml), output, parse_dublincore=False,
281                              flags=['full-page'])
282     html = output.getvalue()
283     response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
284     return response
285
286
287 @never_cache
288 @require_POST
289 def revert(request, slug, chunk=None):
290     form = DocumentTextRevertForm(request.POST, prefix="textrevert")
291     if form.is_valid():
292         try:
293             doc = Chunk.get(slug, chunk)
294         except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
295             raise Http404
296
297         revision = form.cleaned_data['revision']
298
299         comment = form.cleaned_data['comment']
300         comment += "\n#revert to %s" % revision
301
302         if request.user.is_authenticated():
303             author = request.user
304         else:
305             author = None
306
307         before = doc.revision()
308         logger.info("Reverting %s to %s", slug, revision)
309         doc.at_revision(revision).revert(author=author, description=comment)
310
311         return JSONResponse({
312             'text': doc.materialize() if before != doc.revision() else None,
313             'meta': {},
314             'revision': doc.revision(),
315         })
316     else:
317         return JSONFormInvalid(form)
318
319
320 @never_cache
321 def gallery(request, directory):
322     try:
323         base_url = ''.join((
324                         smart_unicode(settings.MEDIA_URL),
325                         smart_unicode(settings.FILEBROWSER_DIRECTORY),
326                         smart_unicode(directory)))
327
328         base_dir = os.path.join(
329                     smart_unicode(settings.MEDIA_ROOT),
330                     smart_unicode(settings.FILEBROWSER_DIRECTORY),
331                     smart_unicode(directory))
332
333         def map_to_url(filename):
334             return "%s/%s" % (base_url, smart_unicode(filename))
335
336         def is_image(filename):
337             return os.path.splitext(f)[1].lower() in (u'.jpg', u'.jpeg', u'.png')
338
339         images = [map_to_url(f) for f in map(smart_unicode, os.listdir(base_dir)) if is_image(f)]
340         images.sort()
341         return JSONResponse(images)
342     except (IndexError, OSError):
343         logger.exception("Unable to fetch gallery")
344         raise http.Http404
345
346
347 @never_cache
348 def diff(request, slug, chunk=None):
349     revA = int(request.GET.get('from', 0))
350     revB = int(request.GET.get('to', 0))
351
352     if revA > revB:
353         revA, revB = revB, revA
354
355     if revB == 0:
356         revB = None
357
358     try:
359         doc = Chunk.get(slug, chunk)
360     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
361         raise Http404
362     docA = doc.at_revision(revA).materialize()
363     docB = doc.at_revision(revB).materialize()
364
365     return http.HttpResponse(nice_diff.html_diff_table(docA.splitlines(),
366                                          docB.splitlines(), context=3))
367
368
369 @never_cache
370 def revision(request, slug, chunk=None):
371     try:
372         doc = Chunk.get(slug, chunk)
373     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
374         raise Http404
375     return http.HttpResponse(str(doc.revision()))
376
377
378 @never_cache
379 def history(request, slug, chunk=None):
380     # TODO: pagination
381     try:
382         doc = Chunk.get(slug, chunk)
383     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
384         raise Http404
385
386     changes = []
387     for change in doc.history().order_by('-created_at'):
388         changes.append({
389                 "version": change.revision,
390                 "description": change.description,
391                 "author": change.author_str(),
392                 "date": change.created_at,
393                 "tag": [],
394             })
395     return JSONResponse(changes)
396
397
398 def book(request, slug):
399     book = get_object_or_404(Book, slug=slug)
400
401     return direct_to_template(request, "wiki/book_detail.html", extra_context={
402         "book": book,
403     })
404
405
406
407 """
408 import wlapi
409
410
411 @require_POST
412 @ajax_require_permission('wiki.can_change_tags')
413 def add_tag(request, name):
414     name = normalize_name(name)
415     storage = getstorage()
416
417     form = DocumentTagForm(request.POST, prefix="addtag")
418     if form.is_valid():
419         doc = storage.get_or_404(form.cleaned_data['id'])
420         doc.add_tag(tag=form.cleaned_data['tag'],
421                     revision=form.cleaned_data['revision'],
422                     author=request.user.username)
423         return JSONResponse({"message": _("Tag added")})
424     else:
425         return JSONFormInvalid(form)
426
427
428 @require_POST
429 @ajax_require_permission('wiki.can_publish')
430 def publish(request, name):
431     name = normalize_name(name)
432
433     storage = getstorage()
434     document = storage.get_by_tag(name, "ready_to_publish")
435
436     api = wlapi.WLAPI(**settings.WL_API_CONFIG)
437
438     try:
439         return JSONResponse({"result": api.publish_book(document)})
440     except wlapi.APICallException, e:
441         return JSONServerError({"message": str(e)})
442 """
443
444 def themes(request):
445     prefix = request.GET.get('q', '')
446     return http.HttpResponse('\n'.join([str(t) for t in Theme.objects.filter(name__istartswith=prefix)]))