changeset tagging in dvcs,
[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         ChunkFormSet)
22 from datetime import datetime
23 from django.utils.encoding import smart_unicode
24 from django.utils.translation import ugettext_lazy as _
25 from django.utils.decorators import decorator_from_middleware
26 from django.middleware.gzip import GZipMiddleware
27
28 import librarian.html
29 import librarian.text
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": DocumentTextSaveForm(prefix="textsave"),
83             "text_revert": DocumentTextRevertForm(prefix="textrevert"),
84             "add_tag": 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 = 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 = 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 = 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 = 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 = 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 = 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     return direct_to_template(request, "wiki/book_detail.html", extra_context={
403         "book": book,
404     })
405
406
407 @require_POST
408 @ajax_require_permission('wiki.can_change_tags')
409 def add_tag(request, slug, chunk=None):
410     form = DocumentTagForm(request.POST, prefix="addtag")
411     if form.is_valid():
412         try:
413             doc = Chunk.get(slug, chunk)
414         except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
415             raise Http404
416
417         tag = form.cleaned_data['tag']
418         revision = revision=form.cleaned_data['revision']
419         doc.at_revision(revision).tags.add(tag)
420         return JSONResponse({"message": _("Tag added")})
421     else:
422         return JSONFormInvalid(form)
423
424
425 """
426 import wlapi
427
428
429 @require_POST
430 @ajax_require_permission('wiki.can_publish')
431 def publish(request, name):
432     name = normalize_name(name)
433
434     storage = getstorage()
435     document = storage.get_by_tag(name, "ready_to_publish")
436
437     api = wlapi.WLAPI(**settings.WL_API_CONFIG)
438
439     try:
440         return JSONResponse({"result": api.publish_book(document)})
441     except wlapi.APICallException, e:
442         return JSONServerError({"message": str(e)})
443 """
444
445 def themes(request):
446     prefix = request.GET.get('q', '')
447     return http.HttpResponse('\n'.join([str(t) for t in Theme.objects.filter(name__istartswith=prefix)]))