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