wider html book view
[redakcja.git] / apps / catalogue / views.py
1 from collections import defaultdict
2 from datetime import datetime, date, timedelta
3 import logging
4 import os
5 from StringIO import StringIO
6 from urllib import unquote
7 from urlparse import urlsplit, urlunsplit
8
9 from django.conf import settings
10 from django.contrib import auth
11 from django.contrib.auth.models import User
12 from django.contrib.auth.decorators import login_required, permission_required
13 from django.core.urlresolvers import reverse
14 from django.db.models import Count, Q
15 from django.db import transaction
16 from django import http
17 from django.http import Http404, HttpResponse, HttpResponseForbidden
18 from django.shortcuts import get_object_or_404, render
19 from django.utils.encoding import iri_to_uri
20 from django.utils.http import urlquote_plus
21 from django.utils.translation import ugettext_lazy as _
22 from django.views.decorators.http import require_POST
23
24 from apiclient import NotAuthorizedError
25 from catalogue import forms
26 from catalogue import helpers
27 from catalogue.helpers import active_tab
28 from catalogue.models import (Book, Chunk, Image, BookPublishRecord, 
29         ChunkPublishRecord, ImagePublishRecord, Project)
30 from fileupload.views import UploadView
31
32 #
33 # Quick hack around caching problems, TODO: use ETags
34 #
35 from django.views.decorators.cache import never_cache
36
37 logger = logging.getLogger("fnp.catalogue")
38
39
40 @active_tab('all')
41 @never_cache
42 def document_list(request):
43     return render(request, 'catalogue/document_list.html')
44
45
46 @active_tab('images')
47 @never_cache
48 def image_list(request, user=None):
49     return render(request, 'catalogue/image_list.html')
50
51
52 @never_cache
53 def user(request, username):
54     user = get_object_or_404(User, username=username)
55     return render(request, 'catalogue/user_page.html', {"viewed_user": user})
56
57
58 @login_required
59 @active_tab('my')
60 @never_cache
61 def my(request):
62     last_books = sorted(request.session.get("wiki_last_books", {}).items(),
63         key=lambda x: x[1]['time'], reverse=True)
64     for k, v in last_books:
65         v['time'] = datetime.fromtimestamp(v['time'])
66     return render(request, 'catalogue/my_page.html', {
67         'last_books': last_books,
68         "logout_to": '/',
69         })
70
71
72 @active_tab('users')
73 def users(request):
74     return render(request, 'catalogue/user_list.html', {
75         'users': User.objects.all().annotate(count=Count('chunk')).order_by(
76             '-count', 'last_name', 'first_name'),
77     })
78
79
80 @active_tab('activity')
81 def activity(request, isodate=None):
82     today = date.today()
83     try:
84         day = helpers.parse_isodate(isodate)
85     except ValueError:
86         day = today
87
88     if day > today:
89         raise Http404
90     if day != today:
91         next_day = day + timedelta(1)
92     prev_day = day - timedelta(1)
93
94     return render(request, 'catalogue/activity.html', locals())
95
96
97 @never_cache
98 def logout_then_redirect(request):
99     auth.logout(request)
100     return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
101
102
103 @permission_required('catalogue.add_book')
104 @active_tab('create')
105 def create_missing(request, slug=None):
106     if slug is None:
107         slug = ''
108     slug = slug.replace(' ', '-')
109
110     if request.method == "POST":
111         form = forms.DocumentCreateForm(request.POST, request.FILES)
112         if form.is_valid():
113             
114             if request.user.is_authenticated():
115                 creator = request.user
116             else:
117                 creator = None
118             book = Book.create(
119                 text=form.cleaned_data['text'],
120                 creator=creator,
121                 slug=form.cleaned_data['slug'],
122                 title=form.cleaned_data['title'],
123                 gallery=form.cleaned_data['gallery'],
124             )
125
126             return http.HttpResponseRedirect(reverse("catalogue_book", args=[book.slug]))
127     else:
128         form = forms.DocumentCreateForm(initial={
129                 "slug": slug,
130                 "title": slug.replace('-', ' ').title(),
131                 "gallery": slug,
132         })
133
134     return render(request, "catalogue/document_create_missing.html", {
135         "slug": slug,
136         "form": form,
137
138         "logout_to": '/',
139     })
140
141
142 @permission_required('catalogue.add_book')
143 @active_tab('upload')
144 def upload(request):
145     if request.method == "POST":
146         form = forms.DocumentsUploadForm(request.POST, request.FILES)
147         if form.is_valid():
148             from slugify import slugify
149
150             if request.user.is_authenticated():
151                 creator = request.user
152             else:
153                 creator = None
154
155             zip = form.cleaned_data['zip']
156             skipped_list = []
157             ok_list = []
158             error_list = []
159             slugs = {}
160             existing = [book.slug for book in Book.objects.all()]
161             for filename in zip.namelist():
162                 if filename[-1] == '/':
163                     continue
164                 title = os.path.basename(filename)[:-4]
165                 slug = slugify(title)
166                 if not (slug and filename.endswith('.xml')):
167                     skipped_list.append(filename)
168                 elif slug in slugs:
169                     error_list.append((filename, slug, _('Slug already used for %s' % slugs[slug])))
170                 elif slug in existing:
171                     error_list.append((filename, slug, _('Slug already used in repository.')))
172                 else:
173                     try:
174                         zip.read(filename).decode('utf-8') # test read
175                         ok_list.append((filename, slug, title))
176                     except UnicodeDecodeError:
177                         error_list.append((filename, title, _('File should be UTF-8 encoded.')))
178                     slugs[slug] = filename
179
180             if not error_list:
181                 for filename, slug, title in ok_list:
182                     book = Book.create(
183                         text=zip.read(filename).decode('utf-8'),
184                         creator=creator,
185                         slug=slug,
186                         title=title,
187                     )
188
189             return render(request, "catalogue/document_upload.html", {
190                 "form": form,
191                 "ok_list": ok_list,
192                 "skipped_list": skipped_list,
193                 "error_list": error_list,
194
195                 "logout_to": '/',
196             })
197     else:
198         form = forms.DocumentsUploadForm()
199
200     return render(request, "catalogue/document_upload.html", {
201         "form": form,
202
203         "logout_to": '/',
204     })
205
206
207 def serve_xml(request, book, slug):
208     if not book.accessible(request):
209         return HttpResponseForbidden("Not authorized.")
210     xml = book.materialize(publishable=True)
211     response = http.HttpResponse(xml, content_type='application/xml')
212     response['Content-Disposition'] = 'attachment; filename=%s.xml' % slug
213     return response
214
215
216 @never_cache
217 def book_xml(request, slug):
218     book = get_object_or_404(Book, slug=slug)
219     return serve_xml(request, book, slug)
220
221
222 @never_cache
223 def book_xml_dc(request, slug):
224     book = get_object_or_404(Book, dc_slug=slug)
225     return serve_xml(request, book, slug)
226
227
228 @never_cache
229 def book_txt(request, slug):
230     book = get_object_or_404(Book, slug=slug)
231     if not book.accessible(request):
232         return HttpResponseForbidden("Not authorized.")
233
234     doc = book.wldocument()
235     text = doc.as_text().get_string()
236     response = http.HttpResponse(text, content_type='text/plain')
237     response['Content-Disposition'] = 'attachment; filename=%s.txt' % slug
238     return response
239
240
241 @never_cache
242 def book_html(request, slug):
243     book = get_object_or_404(Book, slug=slug)
244     if not book.accessible(request):
245         return HttpResponseForbidden("Not authorized.")
246
247     doc = book.wldocument(parse_dublincore=False)
248     html = doc.as_html(options={'gallery': "'%s'" % book.gallery_url()})
249
250     html = html.get_string() if html is not None else ''
251     # response = http.HttpResponse(html, content_type='text/html')
252     # return response
253     # book_themes = {}
254     # for fragment in book.fragments.all().iterator():
255     #     for theme in fragment.tags.filter(category='theme').iterator():
256     #         book_themes.setdefault(theme, []).append(fragment)
257
258     # book_themes = book_themes.items()
259     # book_themes.sort(key=lambda s: s[0].sort_key)
260     return render(request, 'catalogue/book_text.html', locals())
261
262
263 @never_cache
264 def book_pdf(request, slug):
265     book = get_object_or_404(Book, slug=slug)
266     if not book.accessible(request):
267         return HttpResponseForbidden("Not authorized.")
268
269     # TODO: move to celery
270     doc = book.wldocument()
271     # TODO: error handling
272     pdf_file = doc.as_pdf(cover=True, ilustr_path=book.gallery_path())
273     from catalogue.ebook_utils import serve_file
274     return serve_file(pdf_file.get_filename(),
275                 book.slug + '.pdf', 'application/pdf')
276
277
278 @never_cache
279 def book_epub(request, slug):
280     book = get_object_or_404(Book, slug=slug)
281     if not book.accessible(request):
282         return HttpResponseForbidden("Not authorized.")
283
284     # TODO: move to celery
285     doc = book.wldocument()
286     # TODO: error handling
287     epub = doc.as_epub(ilustr_path=book.gallery_path()).get_string()
288     response = HttpResponse(content_type='application/epub+zip')
289     response['Content-Disposition'] = 'attachment; filename=%s' % book.slug + '.epub'
290     response.write(epub)
291     return response
292
293
294 @never_cache
295 def book_mobi(request, slug):
296     book = get_object_or_404(Book, slug=slug)
297     if not book.accessible(request):
298         return HttpResponseForbidden("Not authorized.")
299
300     # TODO: move to celery
301     doc = book.wldocument()
302     # TODO: error handling
303     mobi = doc.as_mobi(ilustr_path=book.gallery_path()).get_string()
304     response = HttpResponse(content_type='application/x-mobipocket-ebook')
305     response['Content-Disposition'] = 'attachment; filename=%s' % book.slug + '.mobi'
306     response.write(mobi)
307     return response
308
309
310 @never_cache
311 def revision(request, slug, chunk=None):
312     try:
313         doc = Chunk.get(slug, chunk)
314     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
315         raise Http404
316     if not doc.book.accessible(request):
317         return HttpResponseForbidden("Not authorized.")
318     return http.HttpResponse(str(doc.revision()))
319
320
321 def book(request, slug):
322     book = get_object_or_404(Book, slug=slug)
323     if not book.accessible(request):
324         return HttpResponseForbidden("Not authorized.")
325
326     if request.user.has_perm('catalogue.change_book'):
327         if request.method == "POST":
328             form = forms.BookForm(request.POST, instance=book)
329             if form.is_valid():
330                 form.save()
331                 return http.HttpResponseRedirect(book.get_absolute_url())
332         else:
333             form = forms.BookForm(instance=book)
334         editable = True
335     else:
336         form = forms.ReadonlyBookForm(instance=book)
337         editable = False
338
339     publish_error = book.publishable_error()
340     publishable = publish_error is None
341
342     return render(request, "catalogue/book_detail.html", {
343         "book": book,
344         "publishable": publishable,
345         "publishable_error": publish_error,
346         "form": form,
347         "editable": editable,
348     })
349
350
351 def image(request, slug):
352     image = get_object_or_404(Image, slug=slug)
353     if not image.accessible(request):
354         return HttpResponseForbidden("Not authorized.")
355
356     if request.user.has_perm('catalogue.change_image'):
357         if request.method == "POST":
358             form = forms.ImageForm(request.POST, instance=image)
359             if form.is_valid():
360                 form.save()
361                 return http.HttpResponseRedirect(image.get_absolute_url())
362         else:
363             form = forms.ImageForm(instance=image)
364         editable = True
365     else:
366         form = forms.ReadonlyImageForm(instance=image)
367         editable = False
368
369     publish_error = image.publishable_error()
370     publishable = publish_error is None
371
372     return render(request, "catalogue/image_detail.html", {
373         "object": image,
374         "publishable": publishable,
375         "publishable_error": publish_error,
376         "form": form,
377         "editable": editable,
378     })
379
380
381 @permission_required('catalogue.add_chunk')
382 def chunk_add(request, slug, chunk):
383     try:
384         doc = Chunk.get(slug, chunk)
385     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
386         raise Http404
387     if not doc.book.accessible(request):
388         return HttpResponseForbidden("Not authorized.")
389
390     if request.method == "POST":
391         form = forms.ChunkAddForm(request.POST, instance=doc)
392         if form.is_valid():
393             if request.user.is_authenticated():
394                 creator = request.user
395             else:
396                 creator = None
397             doc.split(creator=creator,
398                 slug=form.cleaned_data['slug'],
399                 title=form.cleaned_data['title'],
400                 gallery_start=form.cleaned_data['gallery_start'],
401                 user=form.cleaned_data['user'],
402                 stage=form.cleaned_data['stage']
403             )
404
405             return http.HttpResponseRedirect(doc.book.get_absolute_url())
406     else:
407         form = forms.ChunkAddForm(initial={
408                 "slug": str(doc.number + 1),
409                 "title": "cz. %d" % (doc.number + 1, ),
410         })
411
412     return render(request, "catalogue/chunk_add.html", {
413         "chunk": doc,
414         "form": form,
415     })
416
417
418 @login_required
419 def chunk_edit(request, slug, chunk):
420     try:
421         doc = Chunk.get(slug, chunk)
422     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
423         raise Http404
424     if not doc.book.accessible(request):
425         return HttpResponseForbidden("Not authorized.")
426
427     if request.method == "POST":
428         form = forms.ChunkForm(request.POST, instance=doc)
429         if form.is_valid():
430             form.save()
431             go_next = request.GET.get('next', None)
432             if go_next:
433                 go_next = urlquote_plus(unquote(iri_to_uri(go_next)), safe='/?=&')
434             else:
435                 go_next = doc.book.get_absolute_url()
436             return http.HttpResponseRedirect(go_next)
437     else:
438         form = forms.ChunkForm(instance=doc)
439
440     referer = request.META.get('HTTP_REFERER')
441     if referer:
442         parts = urlsplit(referer)
443         parts = ['', ''] + list(parts[2:])
444         go_next = urlquote_plus(urlunsplit(parts))
445     else:
446         go_next = ''
447
448     return render(request, "catalogue/chunk_edit.html", {
449         "chunk": doc,
450         "form": form,
451         "go_next": go_next,
452     })
453
454
455 @transaction.atomic
456 @login_required
457 @require_POST
458 def chunk_mass_edit(request):
459     ids = map(int, filter(lambda i: i.strip()!='', request.POST.get('ids').split(',')))
460     chunks = map(lambda i: Chunk.objects.get(id=i), ids)
461     
462     stage = request.POST.get('stage')
463     if stage:
464         try:
465             stage = Chunk.tag_model.objects.get(slug=stage)
466         except Chunk.DoesNotExist, e:
467             stage = None
468        
469         for c in chunks: c.stage = stage
470
471     username = request.POST.get('user')
472     logger.info("username: %s" % username)
473     logger.info(request.POST)
474     if username:
475         try:
476             user = User.objects.get(username=username)
477         except User.DoesNotExist, e:
478             user = None
479             
480         for c in chunks: c.user = user
481
482     project_id = request.POST.get('project')
483     if project_id:
484         try:
485             project = Project.objects.get(pk=int(project_id))
486         except (Project.DoesNotExist, ValueError), e:
487             project = None
488         for c in chunks:
489             book = c.book
490             book.project = project
491             book.save()
492
493     for c in chunks: c.save()
494
495     return HttpResponse("", content_type="text/plain")
496
497
498 @transaction.atomic
499 @login_required
500 @require_POST
501 def image_mass_edit(request):
502     ids = map(int, filter(lambda i: i.strip()!='', request.POST.get('ids').split(',')))
503     images = map(lambda i: Image.objects.get(id=i), ids)
504     
505     stage = request.POST.get('stage')
506     if stage:
507         try:
508             stage = Image.tag_model.objects.get(slug=stage)
509         except Image.DoesNotExist, e:
510             stage = None
511        
512         for c in images: c.stage = stage
513
514     username = request.POST.get('user')
515     logger.info("username: %s" % username)
516     logger.info(request.POST)
517     if username:
518         try:
519             user = User.objects.get(username=username)
520         except User.DoesNotExist, e:
521             user = None
522             
523         for c in images: c.user = user
524
525     project_id = request.POST.get('project')
526     if project_id:
527         try:
528             project = Project.objects.get(pk=int(project_id))
529         except (Project.DoesNotExist, ValueError), e:
530             project = None
531         for c in images:
532             c.project = project
533
534     for c in images: c.save()
535
536     return HttpResponse("", content_type="text/plain")
537
538
539 @permission_required('catalogue.change_book')
540 def book_append(request, slug):
541     book = get_object_or_404(Book, slug=slug)
542     if not book.accessible(request):
543         return HttpResponseForbidden("Not authorized.")
544
545     if request.method == "POST":
546         form = forms.BookAppendForm(book, request.POST)
547         if form.is_valid():
548             append_to = form.cleaned_data['append_to']
549             append_to.append(book)
550             return http.HttpResponseRedirect(append_to.get_absolute_url())
551     else:
552         form = forms.BookAppendForm(book)
553     return render(request, "catalogue/book_append_to.html", {
554         "book": book,
555         "form": form,
556
557         "logout_to": '/',
558     })
559
560
561 @require_POST
562 @login_required
563 def publish(request, slug):
564     book = get_object_or_404(Book, slug=slug)
565     if not book.accessible(request):
566         return HttpResponseForbidden("Not authorized.")
567
568     try:
569         protocol = 'https://' if request.is_secure() else 'http://'
570         book.publish(request.user, host=protocol + request.get_host())
571     except NotAuthorizedError:
572         return http.HttpResponseRedirect(reverse('apiclient_oauth'))
573     except BaseException, e:
574         return http.HttpResponse(e)
575     else:
576         return http.HttpResponseRedirect(book.get_absolute_url())
577
578
579 @require_POST
580 @login_required
581 def publish_image(request, slug):
582     image = get_object_or_404(Image, slug=slug)
583     if not image.accessible(request):
584         return HttpResponseForbidden("Not authorized.")
585
586     try:
587         image.publish(request.user)
588     except NotAuthorizedError:
589         return http.HttpResponseRedirect(reverse('apiclient_oauth'))
590     except BaseException, e:
591         return http.HttpResponse(e)
592     else:
593         return http.HttpResponseRedirect(image.get_absolute_url())
594
595
596 class GalleryView(UploadView):
597     def get_object(self, request, slug):
598         book = get_object_or_404(Book, slug=slug)
599         if not book.gallery:
600             raise Http404
601         return book
602
603     def breadcrumbs(self):
604         return [
605             (_('books'), reverse('catalogue_document_list')),
606             (self.object.title, self.object.get_absolute_url()),
607             (_('scan gallery'),),
608         ]
609
610     def get_directory(self):
611         return "%s%s/" % (settings.IMAGE_DIR, self.object.gallery)
612
613
614 def active_users_list(request):
615     since = date(date.today().year, 1, 1)
616     by_user = defaultdict(lambda: 0)
617     by_email = defaultdict(lambda: 0)
618     names_by_email = defaultdict(set)
619     for change_model in (Chunk.change_model, Image.change_model):
620         for c in change_model.objects.filter(
621                 created_at__gte=since).order_by(
622                 'author', 'author_email', 'author_name').values(
623                 'author', 'author_name', 'author_email').annotate(
624                 c=Count('author'), ce=Count('author_email')).distinct():
625             if c['author']:
626                 by_user[c['author']] += c['c']
627             else:
628                 by_email[c['author_email']] += c['ce']
629                 if c['author_name'].strip():
630                     names_by_email[c['author_email']].add(c['author_name'])
631     for user in User.objects.filter(pk__in=by_user):
632         by_email[user.email] += by_user[user.pk]
633         names_by_email[user.email].add("%s %s" % (user.first_name, user.last_name))
634
635     active_users = []
636     for email, count in by_email.items():
637         active_users.append((email, names_by_email[email], count))
638     active_users.sort(key=lambda x: -x[2])
639     return render(request, 'catalogue/active_users_list.html', {
640         'users': active_users,
641         'since': since,
642     })
643
644