2 from StringIO import StringIO
4 logger = logging.getLogger("fnp.wiki")
8 from django.conf import settings
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
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
29 from wiki import xml_tools
32 # Quick hack around caching problems, TODO: use ETags
34 from django.views.decorators.cache import 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),
52 def editor(request, slug, chunk=None, template_name='wiki/document_details.html'):
54 chunk = Chunk.get(slug, chunk)
55 except Chunk.MultipleObjectsReturned:
58 except Chunk.DoesNotExist:
61 book = Book.objects.get(slug=slug)
62 except Book.DoesNotExist:
63 return http.HttpResponseRedirect(reverse("wiki_create_missing", args=[slug]))
67 access_time = datetime.now()
68 last_books = request.session.get("wiki_last_books", {})
69 last_books[slug, chunk.slug] = {
71 'title': chunk.pretty_name(),
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
79 return direct_to_template(request, template_name, extra_context={
82 "text_save": forms.DocumentTextSaveForm(prefix="textsave"),
83 "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
84 "add_tag": forms.DocumentTagForm(prefix="addtag"),
86 'REDMINE_URL': settings.REDMINE_URL,
91 def editor_readonly(request, slug, chunk=None, template_name='wiki/document_details_readonly.html'):
93 chunk = Chunk.get(slug, chunk)
94 revision = request.GET['revision']
95 except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist, KeyError):
98 access_time = datetime.now()
99 last_books = request.session.get("wiki_last_books", {})
100 last_books[slug, chunk.slug] = {
102 'title': chunk.book.title,
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
110 return direct_to_template(request, template_name, extra_context={
112 'revision': revision,
114 'REDMINE_URL': settings.REDMINE_URL,
118 def create_missing(request, slug):
119 slug = slug.replace(' ', '-')
121 if request.method == "POST":
122 form = forms.DocumentCreateForm(request.POST, request.FILES)
125 if request.user.is_authenticated():
126 creator = request.user
129 book = Book.create(creator=creator,
130 slug=form.cleaned_data['slug'],
131 title=form.cleaned_data['title'],
132 text=form.cleaned_data['text'],
135 return http.HttpResponseRedirect(reverse("wiki_editor", args=[book.slug]))
137 form = forms.DocumentCreateForm(initial={
139 "title": slug.replace('-', ' ').title(),
142 return direct_to_template(request, "wiki/document_create_missing.html", extra_context={
149 if request.method == "POST":
150 form = forms.DocumentsUploadForm(request.POST, request.FILES)
154 if request.user.is_authenticated():
155 creator = request.user
159 zip = form.cleaned_data['zip']
164 existing = [book.slug for book in Book.objects.all()]
165 for filename in zip.namelist():
166 if filename[-1] == '/':
168 title = os.path.basename(filename)[:-4]
169 slug = slughifi(title)
170 if not (slug and filename.endswith('.xml')):
171 skipped_list.append(filename)
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.')))
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
185 for filename, slug, title in ok_list:
186 Book.create(creator=creator,
189 text=zip.read(filename).decode('utf-8'),
192 return direct_to_template(request, "wiki/document_upload.html", extra_context={
195 "skipped_list": skipped_list,
196 "error_list": error_list,
199 form = forms.DocumentsUploadForm()
201 return direct_to_template(request, "wiki/document_upload.html", extra_context={
207 @decorator_from_middleware(GZipMiddleware)
208 def text(request, slug, chunk=None):
210 doc = Chunk.get(slug, chunk)
211 except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
214 if request.method == 'POST':
215 form = forms.DocumentTextSaveForm(request.POST, prefix="textsave")
217 if request.user.is_authenticated():
218 author = request.user
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,
229 description=form.cleaned_data['comment'],
232 revision = doc.revision()
233 return JSONResponse({
234 'text': doc.materialize() if parent_revision != revision else None,
236 'revision': revision,
239 return JSONFormInvalid(form)
241 revision = request.GET.get("revision", None)
244 revision = int(revision)
245 except (ValueError, TypeError):
248 return JSONResponse({
249 'text': doc.at_revision(revision).materialize(),
251 'revision': revision if revision else doc.revision(),
256 def book_xml(request, slug):
257 xml = get_object_or_404(Book, slug=slug).materialize(Book.publish_tag())
259 response = http.HttpResponse(xml, content_type='application/xml', mimetype='application/wl+xml')
260 response['Content-Disposition'] = 'attachment; filename=%s.xml' % slug
265 def book_txt(request, slug):
266 xml = get_object_or_404(Book, slug=slug).materialize(Book.publish_tag())
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
277 def book_html(request, slug):
278 xml = get_object_or_404(Book, slug=slug).materialize(Book.publish_tag())
281 librarian.html.transform(StringIO(xml), output, parse_dublincore=False,
283 html = output.getvalue()
284 response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
290 def revert(request, slug, chunk=None):
291 form = forms.DocumentTextRevertForm(request.POST, prefix="textrevert")
294 doc = Chunk.get(slug, chunk)
295 except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
298 revision = form.cleaned_data['revision']
300 comment = form.cleaned_data['comment']
301 comment += "\n#revert to %s" % revision
303 if request.user.is_authenticated():
304 author = request.user
308 before = doc.revision()
309 logger.info("Reverting %s to %s", slug, revision)
310 doc.at_revision(revision).revert(author=author, description=comment)
312 return JSONResponse({
313 'text': doc.materialize() if before != doc.revision() else None,
315 'revision': doc.revision(),
318 return JSONFormInvalid(form)
322 def gallery(request, directory):
325 smart_unicode(settings.MEDIA_URL),
326 smart_unicode(settings.FILEBROWSER_DIRECTORY),
327 smart_unicode(directory)))
329 base_dir = os.path.join(
330 smart_unicode(settings.MEDIA_ROOT),
331 smart_unicode(settings.FILEBROWSER_DIRECTORY),
332 smart_unicode(directory))
334 def map_to_url(filename):
335 return "%s/%s" % (base_url, smart_unicode(filename))
337 def is_image(filename):
338 return os.path.splitext(f)[1].lower() in (u'.jpg', u'.jpeg', u'.png')
340 images = [map_to_url(f) for f in map(smart_unicode, os.listdir(base_dir)) if is_image(f)]
342 return JSONResponse(images)
343 except (IndexError, OSError):
344 logger.exception("Unable to fetch gallery")
349 def diff(request, slug, chunk=None):
350 revA = int(request.GET.get('from', 0))
351 revB = int(request.GET.get('to', 0))
354 revA, revB = revB, revA
360 doc = Chunk.get(slug, chunk)
361 except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
363 docA = doc.at_revision(revA).materialize()
364 docB = doc.at_revision(revB).materialize()
366 return http.HttpResponse(nice_diff.html_diff_table(docA.splitlines(),
367 docB.splitlines(), context=3))
371 def revision(request, slug, chunk=None):
373 doc = Chunk.get(slug, chunk)
374 except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
376 return http.HttpResponse(str(doc.revision()))
380 def history(request, slug, chunk=None):
383 doc = Chunk.get(slug, chunk)
384 except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
388 for change in doc.history().order_by('-created_at'):
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()),
396 return JSONResponse(changes)
399 def book(request, slug):
400 book = get_object_or_404(Book, slug=slug)
402 # TODO: most of this should go somewhere else
404 # do we need some automation?
408 choose_master = False
411 for i, chunk in enumerate(book):
417 graded = xml_tools.GradedText(chunk.materialize())
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')
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')
430 if chunk_dict['fix']:
431 chunk_dict['grade'] = 'wl-fix'
433 chunk_dict['grade'] = 'wl'
435 elif graded.is_broken_wl():
436 chunk_dict['grade'] = 'wl-broken'
437 elif graded.is_xml():
438 chunk_dict['grade'] = 'xml'
440 chunk_dict['grade'] = 'plain'
441 chunk_dict['fix'].append('wl')
444 if chunk_dict['fix']:
446 chunks.append(chunk_dict)
448 if first_master or not need_fixing:
449 choose_master = False
451 if request.method == "POST":
452 form = forms.ChooseMasterForm(request.POST)
453 if not choose_master or form.is_valid():
455 first_master = form.cleaned_data['master']
457 # do the actual fixing
462 text = c['chunk'].materialize()
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)
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)
477 return http.HttpResponseRedirect(book.get_absolute_url())
479 form = forms.ChooseMasterForm()
483 return direct_to_template(request, "wiki/book_detail.html", extra_context={
486 "need_fixing": need_fixing,
487 "choose_master": choose_master,
488 "first_master": first_master,
493 def chunk_add(request, slug, chunk):
495 doc = Chunk.get(slug, chunk)
496 except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
499 if request.method == "POST":
500 form = forms.ChunkAddForm(request.POST, instance=doc)
502 if request.user.is_authenticated():
503 creator = request.user
506 doc.split(creator=creator,
507 slug=form.cleaned_data['slug'],
508 comment=form.cleaned_data['comment'],
511 return http.HttpResponseRedirect(doc.book.get_absolute_url())
513 form = forms.ChunkAddForm(initial={
514 "slug": str(doc.number + 1),
515 "comment": "cz. %d" % (doc.number + 1, ),
518 return direct_to_template(request, "wiki/chunk_add.html", extra_context={
524 def chunk_edit(request, slug, chunk):
526 doc = Chunk.get(slug, chunk)
527 except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
529 if request.method == "POST":
530 form = forms.ChunkForm(request.POST, instance=doc)
533 return http.HttpResponseRedirect(doc.book.get_absolute_url())
535 form = forms.ChunkForm(instance=doc)
536 return direct_to_template(request, "wiki/chunk_edit.html", extra_context={
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)
547 append_to = form.cleaned_data['append_to']
548 append_to.append(book)
549 return http.HttpResponseRedirect(append_to.get_absolute_url())
551 form = forms.BookAppendForm()
552 return direct_to_template(request, "wiki/book_append_to.html", extra_context={
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)
564 return http.HttpResponseRedirect(book.get_absolute_url())
566 form = forms.BookForm(instance=book)
567 return direct_to_template(request, "wiki/book_edit.html", extra_context={
574 @ajax_require_permission('wiki.can_change_tags')
575 def add_tag(request, slug, chunk=None):
576 form = forms.DocumentTagForm(request.POST, prefix="addtag")
579 doc = Chunk.get(slug, chunk)
580 except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
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")})
588 return JSONFormInvalid(form)
596 @ajax_require_permission('wiki.can_publish')
597 def publish(request, name):
598 name = normalize_name(name)
600 storage = getstorage()
601 document = storage.get_by_tag(name, "ready_to_publish")
603 api = wlapi.WLAPI(**settings.WL_API_CONFIG)
606 return JSONResponse({"result": api.publish_book(document)})
607 except wlapi.APICallException, e:
608 return JSONServerError({"message": str(e)})
612 prefix = request.GET.get('q', '')
613 return http.HttpResponse('\n'.join([str(t) for t in Theme.objects.filter(name__istartswith=prefix)]))