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