Merge master into img-playground. Image support with new management features. Missing...
[redakcja.git] / apps / catalogue / views.py
1 from datetime import datetime, date, timedelta
2 import logging
3 import os
4 from StringIO import StringIO
5 from urllib import unquote
6 from urlparse import urlsplit, urlunsplit
7
8 from django.contrib import auth
9 from django.contrib.auth.models import User
10 from django.contrib.auth.decorators import login_required, permission_required
11 from django.core.urlresolvers import reverse
12 from django.db.models import Count, Q
13 from django import http
14 from django.http import Http404, HttpResponse, HttpResponseForbidden
15 from django.shortcuts import get_object_or_404, render
16 from django.utils.encoding import iri_to_uri
17 from django.utils.http import urlquote_plus
18 from django.utils.translation import ugettext_lazy as _
19 from django.views.decorators.http import require_POST
20 from django.views.generic.simple import direct_to_template
21
22 from apiclient import NotAuthorizedError
23 from catalogue import forms
24 from catalogue import helpers
25 from catalogue.helpers import active_tab
26 from catalogue.models import Book, Chunk, BookPublishRecord, ChunkPublishRecord
27 from catalogue.tasks import publishable_error
28
29 #
30 # Quick hack around caching problems, TODO: use ETags
31 #
32 from django.views.decorators.cache import never_cache
33
34 logger = logging.getLogger("fnp.catalogue")
35
36
37 @active_tab('all')
38 @never_cache
39 def document_list(request):
40     return render(request, 'catalogue/document_list.html')
41
42
43 @active_tab('images')
44 @never_cache
45 def image_list(request, user=None):
46     return render(request, 'catalogue/image_list.html')
47
48
49 @never_cache
50 def user(request, username):
51     user = get_object_or_404(User, username=username)
52     return render(request, 'catalogue/user_page.html', {"viewed_user": user})
53
54
55 @login_required
56 @active_tab('my')
57 @never_cache
58 def my(request):
59     return render(request, 'catalogue/my_page.html', {
60         'last_books': sorted(request.session.get("wiki_last_books", {}).items(),
61                         key=lambda x: x[1]['time'], reverse=True),
62
63         "logout_to": '/',
64         })
65
66
67 @active_tab('users')
68 def users(request):
69     return direct_to_template(request, 'catalogue/user_list.html', extra_context={
70         'users': User.objects.all().annotate(count=Count('chunk')).order_by(
71             '-count', 'last_name', 'first_name'),
72     })
73
74
75 @active_tab('activity')
76 def activity(request, isodate=None):
77     today = date.today()
78     try:
79         day = helpers.parse_isodate(isodate)
80     except ValueError:
81         day = today
82
83     if day > today:
84         raise Http404
85     if day != today:
86         next_day = day + timedelta(1)
87     prev_day = day - timedelta(1)
88
89     return render(request, 'catalogue/activity.html', locals())
90
91
92 @never_cache
93 def logout_then_redirect(request):
94     auth.logout(request)
95     return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
96
97
98 @permission_required('catalogue.add_book')
99 @active_tab('create')
100 def create_missing(request, slug=None):
101     if slug is None:
102         slug = ''
103     slug = slug.replace(' ', '-')
104
105     if request.method == "POST":
106         form = forms.DocumentCreateForm(request.POST, request.FILES)
107         if form.is_valid():
108             
109             if request.user.is_authenticated():
110                 creator = request.user
111             else:
112                 creator = None
113             book = Book.create(
114                 text=form.cleaned_data['text'],
115                 creator=creator,
116                 slug=form.cleaned_data['slug'],
117                 title=form.cleaned_data['title'],
118                 gallery=form.cleaned_data['gallery'],
119             )
120
121             return http.HttpResponseRedirect(reverse("catalogue_book", args=[book.slug]))
122     else:
123         form = forms.DocumentCreateForm(initial={
124                 "slug": slug,
125                 "title": slug.replace('-', ' ').title(),
126                 "gallery": slug,
127         })
128
129     return direct_to_template(request, "catalogue/document_create_missing.html", extra_context={
130         "slug": slug,
131         "form": form,
132
133         "logout_to": '/',
134     })
135
136
137 @permission_required('catalogue.add_book')
138 @active_tab('upload')
139 def upload(request):
140     if request.method == "POST":
141         form = forms.DocumentsUploadForm(request.POST, request.FILES)
142         if form.is_valid():
143             import slughifi
144
145             if request.user.is_authenticated():
146                 creator = request.user
147             else:
148                 creator = None
149
150             zip = form.cleaned_data['zip']
151             skipped_list = []
152             ok_list = []
153             error_list = []
154             slugs = {}
155             existing = [book.slug for book in Book.objects.all()]
156             for filename in zip.namelist():
157                 if filename[-1] == '/':
158                     continue
159                 title = os.path.basename(filename)[:-4]
160                 slug = slughifi(title)
161                 if not (slug and filename.endswith('.xml')):
162                     skipped_list.append(filename)
163                 elif slug in slugs:
164                     error_list.append((filename, slug, _('Slug already used for %s' % slugs[slug])))
165                 elif slug in existing:
166                     error_list.append((filename, slug, _('Slug already used in repository.')))
167                 else:
168                     try:
169                         zip.read(filename).decode('utf-8') # test read
170                         ok_list.append((filename, slug, title))
171                     except UnicodeDecodeError:
172                         error_list.append((filename, title, _('File should be UTF-8 encoded.')))
173                     slugs[slug] = filename
174
175             if not error_list:
176                 for filename, slug, title in ok_list:
177                     book = Book.create(
178                         text=zip.read(filename).decode('utf-8'),
179                         creator=creator,
180                         slug=slug,
181                         title=title,
182                     )
183
184             return direct_to_template(request, "catalogue/document_upload.html", extra_context={
185                 "form": form,
186                 "ok_list": ok_list,
187                 "skipped_list": skipped_list,
188                 "error_list": error_list,
189
190                 "logout_to": '/',
191             })
192     else:
193         form = forms.DocumentsUploadForm()
194
195     return direct_to_template(request, "catalogue/document_upload.html", extra_context={
196         "form": form,
197
198         "logout_to": '/',
199     })
200
201
202 @never_cache
203 def book_xml(request, slug):
204     book = get_object_or_404(Book, slug=slug)
205     if not book.accessible(request):
206         return HttpResponseForbidden("Not authorized.")
207     xml = book.materialize()
208
209     response = http.HttpResponse(xml, content_type='application/xml', mimetype='application/wl+xml')
210     response['Content-Disposition'] = 'attachment; filename=%s.xml' % slug
211     return response
212
213
214 @never_cache
215 def book_txt(request, slug):
216     book = get_object_or_404(Book, slug=slug)
217     if not book.accessible(request):
218         return HttpResponseForbidden("Not authorized.")
219     xml = book.materialize()
220     output = StringIO()
221     # errors?
222
223     import librarian.text
224     librarian.text.transform(StringIO(xml), output)
225     text = output.getvalue()
226     response = http.HttpResponse(text, content_type='text/plain', mimetype='text/plain')
227     response['Content-Disposition'] = 'attachment; filename=%s.txt' % slug
228     return response
229
230
231 @never_cache
232 def book_html(request, slug):
233     book = get_object_or_404(Book, slug=slug)
234     if not book.accessible(request):
235         return HttpResponseForbidden("Not authorized.")
236     xml = book.materialize()
237     output = StringIO()
238     # errors?
239
240     import librarian.html
241     librarian.html.transform(StringIO(xml), output, parse_dublincore=False,
242                              flags=['full-page'])
243     html = output.getvalue()
244     response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
245     return response
246
247
248 @never_cache
249 def book_pdf(request, slug):
250     book = get_object_or_404(Book, slug=slug)
251     if not book.accessible(request):
252         return HttpResponseForbidden("Not authorized.")
253
254     from tempfile import NamedTemporaryFile
255     from os import unlink
256     from librarian import pdf
257     from catalogue.ebook_utils import RedakcjaDocProvider, serve_file
258
259     xml = book.materialize()
260     xml_file = NamedTemporaryFile()
261     xml_file.write(xml.encode('utf-8'))
262     xml_file.flush()
263
264     try:
265         pdf_file = NamedTemporaryFile(delete=False)
266         pdf.transform(RedakcjaDocProvider(publishable=True),
267                   file_path=xml_file.name,
268                   output_file=pdf_file,
269                   )
270         return serve_file(pdf_file.name, book.slug + '.pdf', 'application/pdf')
271     finally:
272         unlink(pdf_file.name)
273
274
275 @never_cache
276 def book_epub(request, slug):
277     book = get_object_or_404(Book, slug=slug)
278     if not book.accessible(request):
279         return HttpResponseForbidden("Not authorized.")
280
281     from StringIO import StringIO
282     from tempfile import NamedTemporaryFile
283     from librarian import epub
284     from catalogue.ebook_utils import RedakcjaDocProvider
285
286     xml = book.materialize()
287     xml_file = NamedTemporaryFile()
288     xml_file.write(xml.encode('utf-8'))
289     xml_file.flush()
290
291     epub_file = StringIO()
292     epub.transform(RedakcjaDocProvider(publishable=True),
293             file_path=xml_file.name,
294             output_file=epub_file)
295     response = HttpResponse(mimetype='application/epub+zip')
296     response['Content-Disposition'] = 'attachment; filename=%s' % book.slug + '.epub'
297     response.write(epub_file.getvalue())
298     return response
299
300
301 @never_cache
302 def revision(request, slug, chunk=None):
303     try:
304         doc = Chunk.get(slug, chunk)
305     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
306         raise Http404
307     if not doc.book.accessible(request):
308         return HttpResponseForbidden("Not authorized.")
309     return http.HttpResponse(str(doc.revision()))
310
311
312 def book(request, slug):
313     book = get_object_or_404(Book, slug=slug)
314     if not book.accessible(request):
315         return HttpResponseForbidden("Not authorized.")
316
317     if request.user.has_perm('catalogue.change_book'):
318         if request.method == "POST":
319             form = forms.BookForm(request.POST, instance=book)
320             if form.is_valid():
321                 form.save()
322                 return http.HttpResponseRedirect(book.get_absolute_url())
323         else:
324             form = forms.BookForm(instance=book)
325             editable = True
326     else:
327         form = forms.ReadonlyBookForm(instance=book)
328         editable = False
329
330     publish_error = publishable_error(book)
331     publishable = publish_error is None
332
333     return direct_to_template(request, "catalogue/book_detail.html", extra_context={
334         "book": book,
335         "publishable": publishable,
336         "publishable_error": publish_error,
337         "form": form,
338         "editable": editable,
339     })
340
341
342 def image(request, slug):
343     image = get_object_or_404(Image, slug=slug)
344     if not image.accessible(request):
345         return HttpResponseForbidden("Not authorized.")
346
347     if request.user.has_perm('catalogue.change_image'):
348         if request.method == "POST":
349             form = forms.ImageForm(request.POST, instance=image)
350             if form.is_valid():
351                 form.save()
352                 return http.HttpResponseRedirect(image.get_absolute_url())
353         else:
354             form = forms.ImageForm(instance=image)
355             editable = True
356     else:
357         form = forms.ReadonlyImageForm(instance=image)
358         editable = False
359
360     #publish_error = publishable_error(book)
361     publish_error = 'Publishing not implemented yet.'
362     publishable = publish_error is None
363
364     return direct_to_template(request, "catalogue/image_detail.html", extra_context={
365         "object": image,
366         "publishable": publishable,
367         "publishable_error": publish_error,
368         "form": form,
369         "editable": editable,
370     })
371
372
373 @permission_required('catalogue.add_chunk')
374 def chunk_add(request, slug, chunk):
375     try:
376         doc = Chunk.get(slug, chunk)
377     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
378         raise Http404
379     if not doc.book.accessible(request):
380         return HttpResponseForbidden("Not authorized.")
381
382     if request.method == "POST":
383         form = forms.ChunkAddForm(request.POST, instance=doc)
384         if form.is_valid():
385             if request.user.is_authenticated():
386                 creator = request.user
387             else:
388                 creator = None
389             doc.split(creator=creator,
390                 slug=form.cleaned_data['slug'],
391                 title=form.cleaned_data['title'],
392                 gallery_start=form.cleaned_data['gallery_start'],
393                 user=form.cleaned_data['user'],
394                 stage=form.cleaned_data['stage']
395             )
396
397             return http.HttpResponseRedirect(doc.book.get_absolute_url())
398     else:
399         form = forms.ChunkAddForm(initial={
400                 "slug": str(doc.number + 1),
401                 "title": "cz. %d" % (doc.number + 1, ),
402         })
403
404     return direct_to_template(request, "catalogue/chunk_add.html", extra_context={
405         "chunk": doc,
406         "form": form,
407     })
408
409
410 def chunk_edit(request, slug, chunk):
411     try:
412         doc = Chunk.get(slug, chunk)
413     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
414         raise Http404
415     if not doc.book.accessible(request):
416         return HttpResponseForbidden("Not authorized.")
417
418     if request.method == "POST":
419         form = forms.ChunkForm(request.POST, instance=doc)
420         if form.is_valid():
421             form.save()
422             go_next = request.GET.get('next', None)
423             if go_next:
424                 go_next = urlquote_plus(unquote(iri_to_uri(go_next)), safe='/?=&')
425             else:
426                 go_next = doc.book.get_absolute_url()
427             return http.HttpResponseRedirect(go_next)
428     else:
429         form = forms.ChunkForm(instance=doc)
430
431     referer = request.META.get('HTTP_REFERER')
432     if referer:
433         parts = urlsplit(referer)
434         parts = ['', ''] + list(parts[2:])
435         go_next = urlquote_plus(urlunsplit(parts))
436     else:
437         go_next = ''
438
439     return direct_to_template(request, "catalogue/chunk_edit.html", extra_context={
440         "chunk": doc,
441         "form": form,
442         "go_next": go_next,
443     })
444
445
446 @permission_required('catalogue.change_book')
447 def book_append(request, slug):
448     book = get_object_or_404(Book, slug=slug)
449     if not book.accessible(request):
450         return HttpResponseForbidden("Not authorized.")
451
452     if request.method == "POST":
453         form = forms.BookAppendForm(book, request.POST)
454         if form.is_valid():
455             append_to = form.cleaned_data['append_to']
456             append_to.append(book)
457             return http.HttpResponseRedirect(append_to.get_absolute_url())
458     else:
459         form = forms.BookAppendForm(book)
460     return direct_to_template(request, "catalogue/book_append_to.html", extra_context={
461         "book": book,
462         "form": form,
463
464         "logout_to": '/',
465     })
466
467
468 @require_POST
469 @login_required
470 def publish(request, slug):
471     book = get_object_or_404(Book, slug=slug)
472     if not book.accessible(request):
473         return HttpResponseForbidden("Not authorized.")
474
475     try:
476         book.publish(request.user)
477     except NotAuthorizedError:
478         return http.HttpResponseRedirect(reverse('apiclient_oauth'))
479     except BaseException, e:
480         return http.HttpResponse(e)
481     else:
482         return http.HttpResponseRedirect(book.get_absolute_url())