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