Minor upgrades.
[redakcja.git] / src / catalogue / views.py
1 from collections import defaultdict
2 from datetime import datetime, date, timedelta
3 import logging
4 import os
5 from urllib.parse import unquote, urlsplit, urlunsplit
6
7 from django.conf import settings
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.db import transaction
14 from django import http
15 from django.http import Http404, HttpResponse, HttpResponseForbidden
16 from django.http.response import HttpResponseRedirect
17 from django.shortcuts import get_object_or_404, render
18 from django.utils.encoding import iri_to_uri
19 from django.utils.http import urlquote_plus
20 from django.utils.translation import ugettext_lazy as _
21 from django.views.decorators.http import require_POST
22 from django_cas_ng.decorators import user_passes_test
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_bytes()
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_bytes() 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, mobile=False):
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     customizations = ['26pt', 'nothemes', 'nomargins', 'notoc'] if mobile else None
273     pdf_file = doc.as_pdf(cover=True, ilustr_path=book.gallery_path(), customizations=customizations)
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_bytes()
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_bytes()
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         publish_options_form = forms.PublishOptionsForm()
336         editable = True
337     else:
338         form = forms.ReadonlyBookForm(instance=book)
339         publish_options_form = forms.PublishOptionsForm()
340         editable = False
341
342     publish_error = book.publishable_error()
343     publishable = publish_error is None
344
345     return render(request, "catalogue/book_detail.html", {
346         "book": book,
347         "publishable": publishable,
348         "publishable_error": publish_error,
349         "form": form,
350         "publish_options_form": publish_options_form,
351         "editable": editable,
352     })
353
354
355 def image(request, slug):
356     image = get_object_or_404(Image, slug=slug)
357     if not image.accessible(request):
358         return HttpResponseForbidden("Not authorized.")
359
360     if request.user.has_perm('catalogue.change_image'):
361         if request.method == "POST":
362             form = forms.ImageForm(request.POST, instance=image)
363             if form.is_valid():
364                 form.save()
365                 return http.HttpResponseRedirect(image.get_absolute_url())
366         else:
367             form = forms.ImageForm(instance=image)
368         editable = True
369     else:
370         form = forms.ReadonlyImageForm(instance=image)
371         editable = False
372
373     publish_error = image.publishable_error()
374     publishable = publish_error is None
375
376     return render(request, "catalogue/image_detail.html", {
377         "object": image,
378         "publishable": publishable,
379         "publishable_error": publish_error,
380         "form": form,
381         "editable": editable,
382     })
383
384
385 @permission_required('catalogue.add_chunk')
386 def chunk_add(request, slug, chunk):
387     try:
388         doc = Chunk.get(slug, chunk)
389     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
390         raise Http404
391     if not doc.book.accessible(request):
392         return HttpResponseForbidden("Not authorized.")
393
394     if request.method == "POST":
395         form = forms.ChunkAddForm(request.POST, instance=doc)
396         if form.is_valid():
397             if request.user.is_authenticated:
398                 creator = request.user
399             else:
400                 creator = None
401             doc.split(creator=creator,
402                 slug=form.cleaned_data['slug'],
403                 title=form.cleaned_data['title'],
404                 gallery_start=form.cleaned_data['gallery_start'],
405                 user=form.cleaned_data['user'],
406                 stage=form.cleaned_data['stage']
407             )
408
409             return http.HttpResponseRedirect(doc.book.get_absolute_url())
410     else:
411         form = forms.ChunkAddForm(initial={
412                 "slug": str(doc.number + 1),
413                 "title": "cz. %d" % (doc.number + 1, ),
414         })
415
416     return render(request, "catalogue/chunk_add.html", {
417         "chunk": doc,
418         "form": form,
419     })
420
421
422 @login_required
423 def chunk_edit(request, slug, chunk):
424     try:
425         doc = Chunk.get(slug, chunk)
426     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
427         raise Http404
428     if not doc.book.accessible(request):
429         return HttpResponseForbidden("Not authorized.")
430
431     if request.method == "POST":
432         form = forms.ChunkForm(request.POST, instance=doc)
433         if form.is_valid():
434             form.save()
435             go_next = request.GET.get('next', None)
436             if go_next:
437                 go_next = urlquote_plus(unquote(iri_to_uri(go_next)), safe='/?=&')
438             else:
439                 go_next = doc.book.get_absolute_url()
440             return http.HttpResponseRedirect(go_next)
441     else:
442         form = forms.ChunkForm(instance=doc)
443
444     referer = request.META.get('HTTP_REFERER')
445     if referer:
446         parts = urlsplit(referer)
447         parts = ['', ''] + list(parts[2:])
448         go_next = urlquote_plus(urlunsplit(parts))
449     else:
450         go_next = ''
451
452     return render(request, "catalogue/chunk_edit.html", {
453         "chunk": doc,
454         "form": form,
455         "go_next": go_next,
456     })
457
458
459 @transaction.atomic
460 @login_required
461 @require_POST
462 def chunk_mass_edit(request):
463     ids = [int(i) for i in request.POST.get('ids').split(',') if i.strip()]
464     chunks = list(Chunk.objects.filter(id__in=ids))
465     
466     stage = request.POST.get('stage')
467     if stage:
468         try:
469             stage = Chunk.tag_model.objects.get(slug=stage)
470         except Chunk.DoesNotExist as e:
471             stage = None
472        
473         for c in chunks: c.stage = stage
474
475     username = request.POST.get('user')
476     logger.info("username: %s" % username)
477     logger.info(request.POST)
478     if username:
479         try:
480             user = User.objects.get(username=username)
481         except User.DoesNotExist as e:
482             user = None
483             
484         for c in chunks: c.user = user
485
486     project_id = request.POST.get('project')
487     if project_id:
488         try:
489             project = Project.objects.get(pk=int(project_id))
490         except (Project.DoesNotExist, ValueError) as e:
491             project = None
492         for c in chunks:
493             book = c.book
494             book.project = project
495             book.save()
496
497     for c in chunks: c.save()
498
499     return HttpResponse("", content_type="text/plain")
500
501
502 @transaction.atomic
503 @login_required
504 @require_POST
505 def image_mass_edit(request):
506     ids = map(int, filter(lambda i: i.strip()!='', request.POST.get('ids').split(',')))
507     images = map(lambda i: Image.objects.get(id=i), ids)
508     
509     stage = request.POST.get('stage')
510     if stage:
511         try:
512             stage = Image.tag_model.objects.get(slug=stage)
513         except Image.DoesNotExist as e:
514             stage = None
515        
516         for c in images: c.stage = stage
517
518     username = request.POST.get('user')
519     logger.info("username: %s" % username)
520     logger.info(request.POST)
521     if username:
522         try:
523             user = User.objects.get(username=username)
524         except User.DoesNotExist as e:
525             user = None
526             
527         for c in images: c.user = user
528
529     project_id = request.POST.get('project')
530     if project_id:
531         try:
532             project = Project.objects.get(pk=int(project_id))
533         except (Project.DoesNotExist, ValueError) as e:
534             project = None
535         for c in images:
536             c.project = project
537
538     for c in images: c.save()
539
540     return HttpResponse("", content_type="text/plain")
541
542
543 @permission_required('catalogue.change_book')
544 def book_append(request, slug):
545     book = get_object_or_404(Book, slug=slug)
546     if not book.accessible(request):
547         return HttpResponseForbidden("Not authorized.")
548
549     if request.method == "POST":
550         form = forms.BookAppendForm(book, request.POST)
551         if form.is_valid():
552             append_to = form.cleaned_data['append_to']
553             append_to.append(book)
554             return http.HttpResponseRedirect(append_to.get_absolute_url())
555     else:
556         form = forms.BookAppendForm(book)
557     return render(request, "catalogue/book_append_to.html", {
558         "book": book,
559         "form": form,
560
561         "logout_to": '/',
562     })
563
564
565 @require_POST
566 @login_required
567 def publish(request, slug):
568     form = forms.PublishOptionsForm(request.POST)
569     if form.is_valid():
570         days = form.cleaned_data['days']
571         beta = form.cleaned_data['beta']
572     else:
573         days = 0
574         beta = False
575     book = get_object_or_404(Book, slug=slug)
576     if not book.accessible(request):
577         return HttpResponseForbidden("Not authorized.")
578
579     try:
580         protocol = 'https://' if request.is_secure() else 'http://'
581         book.publish(request.user, host=protocol + request.get_host(), days=days, beta=beta)
582     except NotAuthorizedError:
583         return http.HttpResponseRedirect(reverse('apiclient_oauth' if not beta else 'apiclient_beta_oauth'))
584     except BaseException as e:
585         return http.HttpResponse(repr(e))
586     else:
587         return http.HttpResponseRedirect(book.get_absolute_url())
588
589
590 @require_POST
591 @login_required
592 def publish_image(request, slug):
593     image = get_object_or_404(Image, slug=slug)
594     if not image.accessible(request):
595         return HttpResponseForbidden("Not authorized.")
596
597     try:
598         image.publish(request.user)
599     except NotAuthorizedError:
600         return http.HttpResponseRedirect(reverse('apiclient_oauth'))
601     except BaseException as e:
602         return http.HttpResponse(e)
603     else:
604         return http.HttpResponseRedirect(image.get_absolute_url())
605
606
607 class GalleryView(UploadView):
608     def get_object(self, request, slug):
609         book = get_object_or_404(Book, slug=slug)
610         if not book.gallery:
611             raise Http404
612         return book
613
614     def breadcrumbs(self):
615         return [
616             (_('books'), reverse('catalogue_document_list')),
617             (self.object.title, self.object.get_absolute_url()),
618             (_('scan gallery'),),
619         ]
620
621     def get_directory(self):
622         return "%s%s/" % (settings.IMAGE_DIR, self.object.gallery)
623
624
625 def active_users_list(request):
626     year = int(request.GET.get('y', date.today().year))
627     by_user = defaultdict(lambda: 0)
628     by_email = defaultdict(lambda: 0)
629     names_by_email = defaultdict(set)
630     for change_model in (Chunk.change_model, Image.change_model):
631         for c in change_model.objects.filter(
632                 created_at__year=year).order_by(
633                 'author', 'author_email', 'author_name').values(
634                 'author', 'author_name', 'author_email').annotate(
635                 c=Count('author'), ce=Count('author_email')).distinct():
636             if c['author']:
637                 by_user[c['author']] += c['c']
638             else:
639                 by_email[c['author_email']] += c['ce']
640                 if (c['author_name'] or '').strip():
641                     names_by_email[c['author_email']].add(c['author_name'])
642     for user in User.objects.filter(pk__in=by_user):
643         by_email[user.email] += by_user[user.pk]
644         names_by_email[user.email].add("%s %s" % (user.first_name, user.last_name))
645
646     active_users = []
647     for email, count in by_email.items():
648         active_users.append((email, names_by_email[email], count))
649     active_users.sort(key=lambda x: -x[2])
650     return render(request, 'catalogue/active_users_list.html', {
651         'users': active_users,
652         'year': year,
653     })
654
655
656 @user_passes_test(lambda u: u.is_superuser)
657 def mark_final(request):
658     if request.method == 'POST':
659         form = forms.MarkFinalForm(data=request.POST)
660         if form.is_valid():
661             form.save()
662             return HttpResponseRedirect(reverse('mark_final_completed'))
663     else:
664         form = forms.MarkFinalForm()
665     return render(request, 'catalogue/mark_final.html', {'form': form})
666
667
668 def mark_final_completed(request):
669     return render(request, 'catalogue/mark_final_completed.html')