some chunk management automation
[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 import xml_tools
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     # TODO: most of this should go somewhere else
403
404     # do we need some automation?
405     first_master = None
406     chunks = []
407     need_fixing = False
408     choose_master = False
409
410     length = len(book)
411     for i, chunk in enumerate(book):
412         chunk_dict = {
413             "chunk": chunk,
414             "fix": [],
415             "grade": ""
416             }
417         graded = xml_tools.GradedText(chunk.materialize())
418         if graded.is_wl():
419             master = graded.master()
420             if first_master is None:
421                 first_master = master
422             elif master != first_master:
423                 chunk_dict['fix'].append('bad-master')
424
425             if i > 0 and not graded.has_trim_begin():
426                 chunk_dict['fix'].append('trim-begin')
427             if i < length - 1 and not graded.has_trim_end():
428                 chunk_dict['fix'].append('trim-end')
429
430             if chunk_dict['fix']:
431                 chunk_dict['grade'] = 'wl-fix'
432             else:
433                 chunk_dict['grade'] = 'wl'
434
435         elif graded.is_broken_wl():
436             chunk_dict['grade'] = 'wl-broken'
437         elif graded.is_xml():
438             chunk_dict['grade'] = 'xml'
439         else:
440             chunk_dict['grade'] = 'plain'
441             chunk_dict['fix'].append('wl')
442             choose_master = True
443
444         if chunk_dict['fix']:
445             need_fixing = True
446         chunks.append(chunk_dict)
447
448     if first_master or not need_fixing:
449         choose_master = False
450
451     if request.method == "POST":
452         form = forms.ChooseMasterForm(request.POST)
453         if not choose_master or form.is_valid():
454             if choose_master:
455                 first_master = form.cleaned_data['master']
456
457             # do the actual fixing
458             for c in chunks:
459                 if not c['fix']:
460                     continue
461
462                 text = c['chunk'].materialize()
463                 for fix in c['fix']:
464                     if fix == 'bad-master':
465                         text = xml_tools.change_master(text, first_master)
466                     elif fix == 'trim-begin':
467                         text = xml_tools.add_trim_begin(text)
468                     elif fix == 'trim-end':
469                         text = xml_tools.add_trim_end(text)
470                     elif fix == 'wl':
471                         text = xml_tools.basic_structure(text, first_master)
472                 author = request.user if request.user.is_authenticated() else None
473                 description = "auto-fix: " + ", ".join(c['fix'])
474                 c['chunk'].commit(text=text, author=author, 
475                     description=description)
476
477             return http.HttpResponseRedirect(book.get_absolute_url())
478     elif choose_master:
479         form = forms.ChooseMasterForm()
480     else:
481         form = None
482
483     return direct_to_template(request, "wiki/book_detail.html", extra_context={
484         "book": book,
485         "chunks": chunks,
486         "need_fixing": need_fixing,
487         "choose_master": choose_master,
488         "first_master": first_master,
489         "form": form,
490     })
491
492
493 def chunk_add(request, slug, chunk):
494     try:
495         doc = Chunk.get(slug, chunk)
496     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
497         raise Http404
498
499     if request.method == "POST":
500         form = forms.ChunkAddForm(request.POST, instance=doc)
501         if form.is_valid():
502             if request.user.is_authenticated():
503                 creator = request.user
504             else:
505                 creator = None
506             doc.split(creator=creator,
507                 slug=form.cleaned_data['slug'],
508                 comment=form.cleaned_data['comment'],
509             )
510
511             return http.HttpResponseRedirect(doc.book.get_absolute_url())
512     else:
513         form = forms.ChunkAddForm(initial={
514                 "slug": str(doc.number + 1),
515                 "comment": "cz. %d" % (doc.number + 1, ),
516         })
517
518     return direct_to_template(request, "wiki/chunk_add.html", extra_context={
519         "chunk": doc,
520         "form": form,
521     })
522
523
524 def chunk_edit(request, slug, chunk):
525     try:
526         doc = Chunk.get(slug, chunk)
527     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
528         raise Http404
529     if request.method == "POST":
530         form = forms.ChunkForm(request.POST, instance=doc)
531         if form.is_valid():
532             form.save()
533             return http.HttpResponseRedirect(doc.book.get_absolute_url())
534     else:
535         form = forms.ChunkForm(instance=doc)
536     return direct_to_template(request, "wiki/chunk_edit.html", extra_context={
537         "chunk": doc,
538         "form": form,
539     })
540
541
542 def book_append(request, slug):
543     book = get_object_or_404(Book, slug=slug)
544     if request.method == "POST":
545         form = forms.BookAppendForm(request.POST)
546         if form.is_valid():
547             append_to = form.cleaned_data['append_to']
548             append_to.append(book)
549             return http.HttpResponseRedirect(append_to.get_absolute_url())
550     else:
551         form = forms.BookAppendForm()
552     return direct_to_template(request, "wiki/book_append_to.html", extra_context={
553         "book": book,
554         "form": form,
555     })
556
557
558 def book_edit(request, slug):
559     book = get_object_or_404(Book, slug=slug)
560     if request.method == "POST":
561         form = forms.BookForm(request.POST, instance=book)
562         if form.is_valid():
563             form.save()
564             return http.HttpResponseRedirect(book.get_absolute_url())
565     else:
566         form = forms.BookForm(instance=book)
567     return direct_to_template(request, "wiki/book_edit.html", extra_context={
568         "book": book,
569         "form": form,
570     })
571
572
573 @require_POST
574 @ajax_require_permission('wiki.can_change_tags')
575 def add_tag(request, slug, chunk=None):
576     form = forms.DocumentTagForm(request.POST, prefix="addtag")
577     if form.is_valid():
578         try:
579             doc = Chunk.get(slug, chunk)
580         except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
581             raise Http404
582
583         tag = form.cleaned_data['tag']
584         revision = revision=form.cleaned_data['revision']
585         doc.at_revision(revision).tags.add(tag)
586         return JSONResponse({"message": _("Tag added")})
587     else:
588         return JSONFormInvalid(form)
589
590
591 """
592 import wlapi
593
594
595 @require_POST
596 @ajax_require_permission('wiki.can_publish')
597 def publish(request, name):
598     name = normalize_name(name)
599
600     storage = getstorage()
601     document = storage.get_by_tag(name, "ready_to_publish")
602
603     api = wlapi.WLAPI(**settings.WL_API_CONFIG)
604
605     try:
606         return JSONResponse({"result": api.publish_book(document)})
607     except wlapi.APICallException, e:
608         return JSONServerError({"message": str(e)})
609 """
610
611 def themes(request):
612     prefix = request.GET.get('q', '')
613     return http.HttpResponse('\n'.join([str(t) for t in Theme.objects.filter(name__istartswith=prefix)]))