editing and merging books, adding and editing book chunks,
[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 import forms
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 from wiki.xml_tools import GradedText
30
31 #
32 # Quick hack around caching problems, TODO: use ETags
33 #
34 from django.views.decorators.cache import never_cache
35
36 import nice_diff
37 import operator
38
39 MAX_LAST_DOCS = 10
40
41
42 @never_cache
43 def document_list(request):
44     return direct_to_template(request, 'wiki/document_list.html', extra_context={
45         'books': Book.objects.all(),
46         'last_books': sorted(request.session.get("wiki_last_books", {}).items(),
47                         key=lambda x: x[1]['time'], reverse=True),
48     })
49
50
51 @never_cache
52 def editor(request, slug, chunk=None, template_name='wiki/document_details.html'):
53     try:
54         chunk = Chunk.get(slug, chunk)
55     except Chunk.MultipleObjectsReturned:
56         # TODO: choice page
57         raise Http404
58     except Chunk.DoesNotExist:
59         if chunk is None:
60             try:
61                 book = Book.objects.get(slug=slug)
62             except Book.DoesNotExist:
63                 return http.HttpResponseRedirect(reverse("wiki_create_missing", args=[slug]))
64         else:
65             raise Http404
66
67     access_time = datetime.now()
68     last_books = request.session.get("wiki_last_books", {})
69     last_books[slug, chunk.slug] = {
70         'time': access_time,
71         'title': chunk.pretty_name(),
72         }
73
74     if len(last_books) > MAX_LAST_DOCS:
75         oldest_key = min(last_books, key=lambda x: last_books[x]['time'])
76         del last_books[oldest_key]
77     request.session['wiki_last_books'] = last_books
78
79     return direct_to_template(request, template_name, extra_context={
80         'chunk': chunk,
81         'forms': {
82             "text_save": forms.DocumentTextSaveForm(prefix="textsave"),
83             "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
84             "add_tag": forms.DocumentTagForm(prefix="addtag"),
85         },
86         'REDMINE_URL': settings.REDMINE_URL,
87     })
88
89
90 @require_GET
91 def editor_readonly(request, slug, chunk=None, template_name='wiki/document_details_readonly.html'):
92     try:
93         chunk = Chunk.get(slug, chunk)
94         revision = request.GET['revision']
95     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist, KeyError):
96         raise Http404
97
98     access_time = datetime.now()
99     last_books = request.session.get("wiki_last_books", {})
100     last_books[slug, chunk.slug] = {
101         'time': access_time,
102         'title': chunk.book.title,
103         }
104
105     if len(last_books) > MAX_LAST_DOCS:
106         oldest_key = min(last_books, key=lambda x: last_books[x]['time'])
107         del last_books[oldest_key]
108     request.session['wiki_last_books'] = last_books
109
110     return direct_to_template(request, template_name, extra_context={
111         'chunk': chunk,
112         'revision': revision,
113         'readonly': True,
114         'REDMINE_URL': settings.REDMINE_URL,
115     })
116
117
118 def create_missing(request, slug):
119     slug = slug.replace(' ', '-')
120
121     if request.method == "POST":
122         form = forms.DocumentCreateForm(request.POST, request.FILES)
123         if form.is_valid():
124             
125             if request.user.is_authenticated():
126                 creator = request.user
127             else:
128                 creator = None
129             book = Book.create(creator=creator,
130                 slug=form.cleaned_data['slug'],
131                 title=form.cleaned_data['title'],
132                 text=form.cleaned_data['text'],
133             )
134
135             return http.HttpResponseRedirect(reverse("wiki_editor", args=[book.slug]))
136     else:
137         form = forms.DocumentCreateForm(initial={
138                 "slug": slug,
139                 "title": slug.replace('-', ' ').title(),
140         })
141
142     return direct_to_template(request, "wiki/document_create_missing.html", extra_context={
143         "slug": slug,
144         "form": form,
145     })
146
147
148 def upload(request):
149     if request.method == "POST":
150         form = forms.DocumentsUploadForm(request.POST, request.FILES)
151         if form.is_valid():
152             import slughifi
153
154             if request.user.is_authenticated():
155                 creator = request.user
156             else:
157                 creator = None
158
159             zip = form.cleaned_data['zip']
160             skipped_list = []
161             ok_list = []
162             error_list = []
163             slugs = {}
164             existing = [book.slug for book in Book.objects.all()]
165             for filename in zip.namelist():
166                 if filename[-1] == '/':
167                     continue
168                 title = os.path.basename(filename)[:-4]
169                 slug = slughifi(title)
170                 if not (slug and filename.endswith('.xml')):
171                     skipped_list.append(filename)
172                 elif slug in slugs:
173                     error_list.append((filename, slug, _('Slug already used for %s' % slugs[slug])))
174                 elif slug in existing:
175                     error_list.append((filename, slug, _('Slug already used in repository.')))
176                 else:
177                     try:
178                         zip.read(filename).decode('utf-8') # test read
179                         ok_list.append((filename, slug, title))
180                     except UnicodeDecodeError:
181                         error_list.append((filename, title, _('File should be UTF-8 encoded.')))
182                     slugs[slug] = filename
183
184             if not error_list:
185                 for filename, slug, title in ok_list:
186                     Book.create(creator=creator,
187                         slug=slug,
188                         title=title,
189                         text=zip.read(filename).decode('utf-8'),
190                     )
191
192             return direct_to_template(request, "wiki/document_upload.html", extra_context={
193                 "form": form,
194                 "ok_list": ok_list,
195                 "skipped_list": skipped_list,
196                 "error_list": error_list,
197             })
198     else:
199         form = forms.DocumentsUploadForm()
200
201     return direct_to_template(request, "wiki/document_upload.html", extra_context={
202         "form": form,
203     })
204
205
206 @never_cache
207 @decorator_from_middleware(GZipMiddleware)
208 def text(request, slug, chunk=None):
209     try:
210         doc = Chunk.get(slug, chunk)
211     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
212         raise Http404
213
214     if request.method == 'POST':
215         form = forms.DocumentTextSaveForm(request.POST, prefix="textsave")
216         if form.is_valid():
217             if request.user.is_authenticated():
218                 author = request.user
219             else:
220                 author = None
221             text = form.cleaned_data['text']
222             parent_revision = form.cleaned_data['parent_revision']
223             parent = doc.at_revision(parent_revision)
224             stage = form.cleaned_data['stage_completed']
225             tags = [stage] if stage else []
226             doc.commit(author=author,
227                        text=text,
228                        parent=parent,
229                        description=form.cleaned_data['comment'],
230                        tags=tags,
231                        )
232             revision = doc.revision()
233             return JSONResponse({
234                 'text': doc.materialize() if parent_revision != revision else None,
235                 'meta': {},
236                 'revision': revision,
237             })
238         else:
239             return JSONFormInvalid(form)
240     else:
241         revision = request.GET.get("revision", None)
242         
243         try:
244             revision = int(revision)
245         except (ValueError, TypeError):
246             revision = None
247
248         return JSONResponse({
249             'text': doc.at_revision(revision).materialize(),
250             'meta': {},
251             'revision': revision if revision else doc.revision(),
252         })
253
254
255 @never_cache
256 def book_xml(request, slug):
257     xml = get_object_or_404(Book, slug=slug).materialize(Book.publish_tag())
258
259     response = http.HttpResponse(xml, content_type='application/xml', mimetype='application/wl+xml')
260     response['Content-Disposition'] = 'attachment; filename=%s.xml' % slug
261     return response
262
263
264 @never_cache
265 def book_txt(request, slug):
266     xml = get_object_or_404(Book, slug=slug).materialize(Book.publish_tag())
267     output = StringIO()
268     # errors?
269     librarian.text.transform(StringIO(xml), output)
270     text = output.getvalue()
271     response = http.HttpResponse(text, content_type='text/plain', mimetype='text/plain')
272     response['Content-Disposition'] = 'attachment; filename=%s.txt' % slug
273     return response
274
275
276 @never_cache
277 def book_html(request, slug):
278     xml = get_object_or_404(Book, slug=slug).materialize(Book.publish_tag())
279     output = StringIO()
280     # errors?
281     librarian.html.transform(StringIO(xml), output, parse_dublincore=False,
282                              flags=['full-page'])
283     html = output.getvalue()
284     response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
285     return response
286
287
288 @never_cache
289 @require_POST
290 def revert(request, slug, chunk=None):
291     form = forms.DocumentTextRevertForm(request.POST, prefix="textrevert")
292     if form.is_valid():
293         try:
294             doc = Chunk.get(slug, chunk)
295         except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
296             raise Http404
297
298         revision = form.cleaned_data['revision']
299
300         comment = form.cleaned_data['comment']
301         comment += "\n#revert to %s" % revision
302
303         if request.user.is_authenticated():
304             author = request.user
305         else:
306             author = None
307
308         before = doc.revision()
309         logger.info("Reverting %s to %s", slug, revision)
310         doc.at_revision(revision).revert(author=author, description=comment)
311
312         return JSONResponse({
313             'text': doc.materialize() if before != doc.revision() else None,
314             'meta': {},
315             'revision': doc.revision(),
316         })
317     else:
318         return JSONFormInvalid(form)
319
320
321 @never_cache
322 def gallery(request, directory):
323     try:
324         base_url = ''.join((
325                         smart_unicode(settings.MEDIA_URL),
326                         smart_unicode(settings.FILEBROWSER_DIRECTORY),
327                         smart_unicode(directory)))
328
329         base_dir = os.path.join(
330                     smart_unicode(settings.MEDIA_ROOT),
331                     smart_unicode(settings.FILEBROWSER_DIRECTORY),
332                     smart_unicode(directory))
333
334         def map_to_url(filename):
335             return "%s/%s" % (base_url, smart_unicode(filename))
336
337         def is_image(filename):
338             return os.path.splitext(f)[1].lower() in (u'.jpg', u'.jpeg', u'.png')
339
340         images = [map_to_url(f) for f in map(smart_unicode, os.listdir(base_dir)) if is_image(f)]
341         images.sort()
342         return JSONResponse(images)
343     except (IndexError, OSError):
344         logger.exception("Unable to fetch gallery")
345         raise http.Http404
346
347
348 @never_cache
349 def diff(request, slug, chunk=None):
350     revA = int(request.GET.get('from', 0))
351     revB = int(request.GET.get('to', 0))
352
353     if revA > revB:
354         revA, revB = revB, revA
355
356     if revB == 0:
357         revB = None
358
359     try:
360         doc = Chunk.get(slug, chunk)
361     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
362         raise Http404
363     docA = doc.at_revision(revA).materialize()
364     docB = doc.at_revision(revB).materialize()
365
366     return http.HttpResponse(nice_diff.html_diff_table(docA.splitlines(),
367                                          docB.splitlines(), context=3))
368
369
370 @never_cache
371 def revision(request, slug, chunk=None):
372     try:
373         doc = Chunk.get(slug, chunk)
374     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
375         raise Http404
376     return http.HttpResponse(str(doc.revision()))
377
378
379 @never_cache
380 def history(request, slug, chunk=None):
381     # TODO: pagination
382     try:
383         doc = Chunk.get(slug, chunk)
384     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
385         raise Http404
386
387     changes = []
388     for change in doc.history().order_by('-created_at'):
389         changes.append({
390                 "version": change.revision,
391                 "description": change.description,
392                 "author": change.author_str(),
393                 "date": change.created_at,
394                 "tag": ',\n'.join(unicode(tag) for tag in change.tags.all()),
395             })
396     return JSONResponse(changes)
397
398
399 def book(request, slug):
400     book = get_object_or_404(Book, slug=slug)
401
402     # do we need some automation?
403     some_wl = False
404     first_master = None
405     chunks = []
406
407     for chunk in book:
408         graded = GradedText(chunk.materialize())
409         chunk_dict = {
410             "chunk": chunk,
411             "graded": graded,
412             }
413         if graded.is_wl():
414             some_wl = True
415             master = graded.master()
416             if first_master is None:
417                 first_master = master
418             elif master != first_master:
419                 chunk_dict['bad_master'] = master
420         chunks.append(chunk_dict)
421
422     return direct_to_template(request, "wiki/book_detail.html", extra_context={
423         "book": book,
424         "chunks": chunks,
425         "some_wl": some_wl,
426         "first_master": first_master,
427     })
428
429
430 def chunk_add(request, slug, chunk):
431     try:
432         doc = Chunk.get(slug, chunk)
433     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
434         raise Http404
435
436     if request.method == "POST":
437         form = forms.ChunkAddForm(request.POST, instance=doc)
438         if form.is_valid():
439             if request.user.is_authenticated():
440                 creator = request.user
441             else:
442                 creator = None
443             doc.split(creator=creator,
444                 slug=form.cleaned_data['slug'],
445                 comment=form.cleaned_data['comment'],
446             )
447
448             return http.HttpResponseRedirect(doc.book.get_absolute_url())
449     else:
450         form = forms.ChunkAddForm(initial={
451                 "slug": str(doc.number + 1),
452                 "comment": "cz. %d" % (doc.number + 1, ),
453         })
454
455     return direct_to_template(request, "wiki/chunk_add.html", extra_context={
456         "chunk": doc,
457         "form": form,
458     })
459
460
461 def chunk_edit(request, slug, chunk):
462     try:
463         doc = Chunk.get(slug, chunk)
464     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
465         raise Http404
466     if request.method == "POST":
467         form = forms.ChunkForm(request.POST, instance=doc)
468         if form.is_valid():
469             form.save()
470             return http.HttpResponseRedirect(doc.book.get_absolute_url())
471     else:
472         form = forms.ChunkForm(instance=doc)
473     return direct_to_template(request, "wiki/chunk_edit.html", extra_context={
474         "chunk": doc,
475         "form": form,
476     })
477
478
479 def book_append(request, slug):
480     book = get_object_or_404(Book, slug=slug)
481     if request.method == "POST":
482         form = forms.BookAppendForm(request.POST)
483         if form.is_valid():
484             append_to = form.cleaned_data['append_to']
485             append_to.append(book)
486             return http.HttpResponseRedirect(append_to.get_absolute_url())
487     else:
488         form = forms.BookAppendForm()
489     return direct_to_template(request, "wiki/book_append_to.html", extra_context={
490         "book": book,
491         "form": form,
492     })
493
494
495 def book_edit(request, slug):
496     book = get_object_or_404(Book, slug=slug)
497     if request.method == "POST":
498         form = forms.BookForm(request.POST, instance=book)
499         if form.is_valid():
500             form.save()
501             return http.HttpResponseRedirect(book.get_absolute_url())
502     else:
503         form = forms.BookForm(instance=book)
504     return direct_to_template(request, "wiki/book_edit.html", extra_context={
505         "book": book,
506         "form": form,
507     })
508
509
510 @require_POST
511 @ajax_require_permission('wiki.can_change_tags')
512 def add_tag(request, slug, chunk=None):
513     form = forms.DocumentTagForm(request.POST, prefix="addtag")
514     if form.is_valid():
515         try:
516             doc = Chunk.get(slug, chunk)
517         except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
518             raise Http404
519
520         tag = form.cleaned_data['tag']
521         revision = revision=form.cleaned_data['revision']
522         doc.at_revision(revision).tags.add(tag)
523         return JSONResponse({"message": _("Tag added")})
524     else:
525         return JSONFormInvalid(form)
526
527
528 """
529 import wlapi
530
531
532 @require_POST
533 @ajax_require_permission('wiki.can_publish')
534 def publish(request, name):
535     name = normalize_name(name)
536
537     storage = getstorage()
538     document = storage.get_by_tag(name, "ready_to_publish")
539
540     api = wlapi.WLAPI(**settings.WL_API_CONFIG)
541
542     try:
543         return JSONResponse({"result": api.publish_book(document)})
544     except wlapi.APICallException, e:
545         return JSONServerError({"message": str(e)})
546 """
547
548 def themes(request):
549     prefix = request.GET.get('q', '')
550     return http.HttpResponse('\n'.join([str(t) for t in Theme.objects.filter(name__istartswith=prefix)]))