allow download xml for non-public books (tmp?)
[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     xml = book.materialize(publishable=True)
209     response = http.HttpResponse(xml, content_type='application/xml')
210     response['Content-Disposition'] = 'attachment; filename=%s.xml' % slug
211     return response
212
213
214 @never_cache
215 def book_xml(request, slug):
216     book = get_object_or_404(Book, slug=slug)
217     if not book.accessible(request):
218         return HttpResponseForbidden("Not authorized.")
219     return serve_xml(request, book, slug)
220
221
222 @never_cache
223 def book_xml_dc(request, slug):
224     # no permission check, because non-public books
225     book = get_object_or_404(Book, dc_slug=slug)
226     return serve_xml(request, book, slug)
227
228
229 @never_cache
230 def book_txt(request, slug):
231     book = get_object_or_404(Book, slug=slug)
232     if not book.accessible(request):
233         return HttpResponseForbidden("Not authorized.")
234
235     doc = book.wldocument()
236     text = doc.as_text().get_string()
237     response = http.HttpResponse(text, content_type='text/plain')
238     response['Content-Disposition'] = 'attachment; filename=%s.txt' % slug
239     return response
240
241
242 @never_cache
243 def book_html(request, slug):
244     book = get_object_or_404(Book, slug=slug)
245     if not book.accessible(request):
246         return HttpResponseForbidden("Not authorized.")
247
248     doc = book.wldocument(parse_dublincore=False)
249     html = doc.as_html(options={'gallery': "'%s'" % book.gallery_url()})
250
251     html = html.get_string() if html is not None else ''
252     # response = http.HttpResponse(html, content_type='text/html')
253     # return response
254     # book_themes = {}
255     # for fragment in book.fragments.all().iterator():
256     #     for theme in fragment.tags.filter(category='theme').iterator():
257     #         book_themes.setdefault(theme, []).append(fragment)
258
259     # book_themes = book_themes.items()
260     # book_themes.sort(key=lambda s: s[0].sort_key)
261     return render(request, 'catalogue/book_text.html', locals())
262
263
264 @never_cache
265 def book_pdf(request, slug):
266     book = get_object_or_404(Book, slug=slug)
267     if not book.accessible(request):
268         return HttpResponseForbidden("Not authorized.")
269
270     # TODO: move to celery
271     doc = book.wldocument()
272     # TODO: error handling
273     pdf_file = doc.as_pdf(cover=True, ilustr_path=book.gallery_path())
274     from catalogue.ebook_utils import serve_file
275     return serve_file(pdf_file.get_filename(),
276                 book.slug + '.pdf', 'application/pdf')
277
278
279 @never_cache
280 def book_epub(request, slug):
281     book = get_object_or_404(Book, slug=slug)
282     if not book.accessible(request):
283         return HttpResponseForbidden("Not authorized.")
284
285     # TODO: move to celery
286     doc = book.wldocument()
287     # TODO: error handling
288     epub = doc.as_epub(ilustr_path=book.gallery_path()).get_string()
289     response = HttpResponse(content_type='application/epub+zip')
290     response['Content-Disposition'] = 'attachment; filename=%s' % book.slug + '.epub'
291     response.write(epub)
292     return response
293
294
295 @never_cache
296 def book_mobi(request, slug):
297     book = get_object_or_404(Book, slug=slug)
298     if not book.accessible(request):
299         return HttpResponseForbidden("Not authorized.")
300
301     # TODO: move to celery
302     doc = book.wldocument()
303     # TODO: error handling
304     mobi = doc.as_mobi(ilustr_path=book.gallery_path()).get_string()
305     response = HttpResponse(content_type='application/x-mobipocket-ebook')
306     response['Content-Disposition'] = 'attachment; filename=%s' % book.slug + '.mobi'
307     response.write(mobi)
308     return response
309
310
311 @never_cache
312 def revision(request, slug, chunk=None):
313     try:
314         doc = Chunk.get(slug, chunk)
315     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
316         raise Http404
317     if not doc.book.accessible(request):
318         return HttpResponseForbidden("Not authorized.")
319     return http.HttpResponse(str(doc.revision()))
320
321
322 def book(request, slug):
323     book = get_object_or_404(Book, slug=slug)
324     if not book.accessible(request):
325         return HttpResponseForbidden("Not authorized.")
326
327     if request.user.has_perm('catalogue.change_book'):
328         if request.method == "POST":
329             form = forms.BookForm(request.POST, instance=book)
330             if form.is_valid():
331                 form.save()
332                 return http.HttpResponseRedirect(book.get_absolute_url())
333         else:
334             form = forms.BookForm(instance=book)
335         editable = True
336     else:
337         form = forms.ReadonlyBookForm(instance=book)
338         editable = False
339
340     publish_error = book.publishable_error()
341     publishable = publish_error is None
342
343     return render(request, "catalogue/book_detail.html", {
344         "book": book,
345         "publishable": publishable,
346         "publishable_error": publish_error,
347         "form": form,
348         "editable": editable,
349     })
350
351
352 def image(request, slug):
353     image = get_object_or_404(Image, slug=slug)
354     if not image.accessible(request):
355         return HttpResponseForbidden("Not authorized.")
356
357     if request.user.has_perm('catalogue.change_image'):
358         if request.method == "POST":
359             form = forms.ImageForm(request.POST, instance=image)
360             if form.is_valid():
361                 form.save()
362                 return http.HttpResponseRedirect(image.get_absolute_url())
363         else:
364             form = forms.ImageForm(instance=image)
365         editable = True
366     else:
367         form = forms.ReadonlyImageForm(instance=image)
368         editable = False
369
370     publish_error = image.publishable_error()
371     publishable = publish_error is None
372
373     return render(request, "catalogue/image_detail.html", {
374         "object": image,
375         "publishable": publishable,
376         "publishable_error": publish_error,
377         "form": form,
378         "editable": editable,
379     })
380
381
382 @permission_required('catalogue.add_chunk')
383 def chunk_add(request, slug, chunk):
384     try:
385         doc = Chunk.get(slug, chunk)
386     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
387         raise Http404
388     if not doc.book.accessible(request):
389         return HttpResponseForbidden("Not authorized.")
390
391     if request.method == "POST":
392         form = forms.ChunkAddForm(request.POST, instance=doc)
393         if form.is_valid():
394             if request.user.is_authenticated():
395                 creator = request.user
396             else:
397                 creator = None
398             doc.split(creator=creator,
399                 slug=form.cleaned_data['slug'],
400                 title=form.cleaned_data['title'],
401                 gallery_start=form.cleaned_data['gallery_start'],
402                 user=form.cleaned_data['user'],
403                 stage=form.cleaned_data['stage']
404             )
405
406             return http.HttpResponseRedirect(doc.book.get_absolute_url())
407     else:
408         form = forms.ChunkAddForm(initial={
409                 "slug": str(doc.number + 1),
410                 "title": "cz. %d" % (doc.number + 1, ),
411         })
412
413     return render(request, "catalogue/chunk_add.html", {
414         "chunk": doc,
415         "form": form,
416     })
417
418
419 @login_required
420 def chunk_edit(request, slug, chunk):
421     try:
422         doc = Chunk.get(slug, chunk)
423     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
424         raise Http404
425     if not doc.book.accessible(request):
426         return HttpResponseForbidden("Not authorized.")
427
428     if request.method == "POST":
429         form = forms.ChunkForm(request.POST, instance=doc)
430         if form.is_valid():
431             form.save()
432             go_next = request.GET.get('next', None)
433             if go_next:
434                 go_next = urlquote_plus(unquote(iri_to_uri(go_next)), safe='/?=&')
435             else:
436                 go_next = doc.book.get_absolute_url()
437             return http.HttpResponseRedirect(go_next)
438     else:
439         form = forms.ChunkForm(instance=doc)
440
441     referer = request.META.get('HTTP_REFERER')
442     if referer:
443         parts = urlsplit(referer)
444         parts = ['', ''] + list(parts[2:])
445         go_next = urlquote_plus(urlunsplit(parts))
446     else:
447         go_next = ''
448
449     return render(request, "catalogue/chunk_edit.html", {
450         "chunk": doc,
451         "form": form,
452         "go_next": go_next,
453     })
454
455
456 @transaction.atomic
457 @login_required
458 @require_POST
459 def chunk_mass_edit(request):
460     ids = map(int, filter(lambda i: i.strip()!='', request.POST.get('ids').split(',')))
461     chunks = map(lambda i: Chunk.objects.get(id=i), ids)
462     
463     stage = request.POST.get('stage')
464     if stage:
465         try:
466             stage = Chunk.tag_model.objects.get(slug=stage)
467         except Chunk.DoesNotExist, e:
468             stage = None
469        
470         for c in chunks: c.stage = stage
471
472     username = request.POST.get('user')
473     logger.info("username: %s" % username)
474     logger.info(request.POST)
475     if username:
476         try:
477             user = User.objects.get(username=username)
478         except User.DoesNotExist, e:
479             user = None
480             
481         for c in chunks: c.user = user
482
483     project_id = request.POST.get('project')
484     if project_id:
485         try:
486             project = Project.objects.get(pk=int(project_id))
487         except (Project.DoesNotExist, ValueError), e:
488             project = None
489         for c in chunks:
490             book = c.book
491             book.project = project
492             book.save()
493
494     for c in chunks: c.save()
495
496     return HttpResponse("", content_type="text/plain")
497
498
499 @transaction.atomic
500 @login_required
501 @require_POST
502 def image_mass_edit(request):
503     ids = map(int, filter(lambda i: i.strip()!='', request.POST.get('ids').split(',')))
504     images = map(lambda i: Image.objects.get(id=i), ids)
505     
506     stage = request.POST.get('stage')
507     if stage:
508         try:
509             stage = Image.tag_model.objects.get(slug=stage)
510         except Image.DoesNotExist, e:
511             stage = None
512        
513         for c in images: c.stage = stage
514
515     username = request.POST.get('user')
516     logger.info("username: %s" % username)
517     logger.info(request.POST)
518     if username:
519         try:
520             user = User.objects.get(username=username)
521         except User.DoesNotExist, e:
522             user = None
523             
524         for c in images: c.user = user
525
526     project_id = request.POST.get('project')
527     if project_id:
528         try:
529             project = Project.objects.get(pk=int(project_id))
530         except (Project.DoesNotExist, ValueError), e:
531             project = None
532         for c in images:
533             c.project = project
534
535     for c in images: c.save()
536
537     return HttpResponse("", content_type="text/plain")
538
539
540 @permission_required('catalogue.change_book')
541 def book_append(request, slug):
542     book = get_object_or_404(Book, slug=slug)
543     if not book.accessible(request):
544         return HttpResponseForbidden("Not authorized.")
545
546     if request.method == "POST":
547         form = forms.BookAppendForm(book, request.POST)
548         if form.is_valid():
549             append_to = form.cleaned_data['append_to']
550             append_to.append(book)
551             return http.HttpResponseRedirect(append_to.get_absolute_url())
552     else:
553         form = forms.BookAppendForm(book)
554     return render(request, "catalogue/book_append_to.html", {
555         "book": book,
556         "form": form,
557
558         "logout_to": '/',
559     })
560
561
562 @require_POST
563 @login_required
564 def publish(request, slug):
565     book = get_object_or_404(Book, slug=slug)
566     if not book.accessible(request):
567         return HttpResponseForbidden("Not authorized.")
568
569     try:
570         protocol = 'https://' if request.is_secure() else 'http://'
571         book.publish(request.user, host=protocol + request.get_host())
572     except NotAuthorizedError:
573         return http.HttpResponseRedirect(reverse('apiclient_oauth'))
574     except BaseException, e:
575         return http.HttpResponse(e)
576     else:
577         return http.HttpResponseRedirect(book.get_absolute_url())
578
579
580 @require_POST
581 @login_required
582 def publish_image(request, slug):
583     image = get_object_or_404(Image, slug=slug)
584     if not image.accessible(request):
585         return HttpResponseForbidden("Not authorized.")
586
587     try:
588         image.publish(request.user)
589     except NotAuthorizedError:
590         return http.HttpResponseRedirect(reverse('apiclient_oauth'))
591     except BaseException, e:
592         return http.HttpResponse(e)
593     else:
594         return http.HttpResponseRedirect(image.get_absolute_url())
595
596
597 class GalleryView(UploadView):
598     def get_object(self, request, slug):
599         book = get_object_or_404(Book, slug=slug)
600         if not book.gallery:
601             raise Http404
602         return book
603
604     def breadcrumbs(self):
605         return [
606             (_('books'), reverse('catalogue_document_list')),
607             (self.object.title, self.object.get_absolute_url()),
608             (_('scan gallery'),),
609         ]
610
611     def get_directory(self):
612         return "%s%s/" % (settings.IMAGE_DIR, self.object.gallery)
613
614
615 def active_users_list(request):
616     since = date(date.today().year, 1, 1)
617     by_user = defaultdict(lambda: 0)
618     by_email = defaultdict(lambda: 0)
619     names_by_email = defaultdict(set)
620     for change_model in (Chunk.change_model, Image.change_model):
621         for c in change_model.objects.filter(
622                 created_at__gte=since).order_by(
623                 'author', 'author_email', 'author_name').values(
624                 'author', 'author_name', 'author_email').annotate(
625                 c=Count('author'), ce=Count('author_email')).distinct():
626             if c['author']:
627                 by_user[c['author']] += c['c']
628             else:
629                 by_email[c['author_email']] += c['ce']
630                 if c['author_name'].strip():
631                     names_by_email[c['author_email']].add(c['author_name'])
632     for user in User.objects.filter(pk__in=by_user):
633         by_email[user.email] += by_user[user.pk]
634         names_by_email[user.email].add("%s %s" % (user.first_name, user.last_name))
635
636     active_users = []
637     for email, count in by_email.items():
638         active_users.append((email, names_by_email[email], count))
639     active_users.sort(key=lambda x: -x[2])
640     return render(request, 'catalogue/active_users_list.html', {
641         'users': active_users,
642         'since': since,
643     })
644
645