d07303f4fe43917aff0a133f3eed22f9b11b9772
[redakcja.git] / apps / catalogue / views.py
1 # -*- coding: utf-8 -*-
2 from datetime import date, timedelta
3 import logging
4 import os
5 from urllib import unquote
6 from urlparse import 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
14 from django.db import transaction
15 from django import http
16 from django.http import Http404, HttpResponse, HttpResponseForbidden
17 from django.shortcuts import get_object_or_404, render, render_to_response
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.template import RequestContext
23
24 from apiclient import NotAuthorizedError
25 from catalogue import forms
26 from catalogue import helpers
27 from catalogue.helpers import active_tab, ajax
28 from catalogue.models import Book, Chunk, Project
29 from fileupload.views import UploadView, PackageView
30
31 #
32 # Quick hack around caching problems, TODO: use ETags
33 #
34 from django.views.decorators.cache import never_cache
35
36 logger = logging.getLogger("fnp.catalogue")
37
38
39 @active_tab('all')
40 @never_cache
41 def document_list(request):
42     return render(request, 'catalogue/document_list.html')
43
44
45 @never_cache
46 def user(request, username):
47     user = get_object_or_404(User, username=username)
48     return render(request, 'catalogue/user_page.html', {"viewed_user": user})
49
50
51 @login_required
52 @active_tab('my')
53 @never_cache
54 def my(request):
55     return render(request, 'catalogue/my_page.html', {
56         'last_books': sorted(
57             request.session.get("wiki_last_books", {}).items(),
58             key=lambda x: x[1]['time'], reverse=True),
59         'logout_to': '/',
60     })
61
62
63 @active_tab('users')
64 def users(request):
65     return render(request, 'catalogue/user_list.html', {
66         'users': User.objects.all().annotate(count=Count('chunk')).order_by(
67             '-count', 'last_name', 'first_name'),
68     })
69
70
71 @active_tab('activity')
72 def activity(request, isodate=None):
73     today = date.today()
74     try:
75         day = helpers.parse_isodate(isodate)
76     except ValueError:
77         day = today
78
79     if day > today:
80         raise Http404
81     if day != today:
82         next_day = day + timedelta(1)
83     prev_day = day - timedelta(1)
84
85     return render(request, 'catalogue/activity.html', locals())
86
87
88 @never_cache
89 def logout_then_redirect(request):
90     auth.logout(request)
91     return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
92
93
94 @permission_required('catalogue.add_book')
95 @active_tab('create')
96 def create_missing(request, slug=None):
97     if slug is None:
98         slug = ''
99     slug = slug.replace(' ', '-')
100
101     if request.method == "POST":
102         form = forms.DocumentCreateForm(request.POST, request.FILES)
103         if form.is_valid():
104             
105             if request.user.is_authenticated():
106                 creator = request.user
107             else:
108                 creator = None
109             book = Book.create(
110                 text=form.cleaned_data['text'],
111                 creator=creator,
112                 slug=form.cleaned_data['slug'],
113                 title=form.cleaned_data['title'],
114                 gallery=form.cleaned_data['gallery'],
115             )
116
117             return http.HttpResponseRedirect(reverse("wiki_editor", args=[book.slug]))
118     else:
119         form = forms.DocumentCreateForm(initial={
120                 "slug": slug,
121                 "title": slug.replace('-', ' ').title(),
122         })
123
124     return render(request, "catalogue/document_create_missing.html", {
125         "slug": slug,
126         "form": form,
127
128         "logout_to": '/',
129     })
130
131
132 @permission_required('catalogue.add_book')
133 @active_tab('upload')
134 def upload(request):
135     if request.method == "POST":
136         form = forms.DocumentsUploadForm(request.POST, request.FILES)
137         if form.is_valid():
138             from slughifi import slughifi
139
140             if request.user.is_authenticated():
141                 creator = request.user
142             else:
143                 creator = None
144
145             zip = form.cleaned_data['zip']
146             skipped_list = []
147             ok_list = []
148             error_list = []
149             slugs = {}
150             existing = [book.slug for book in Book.objects.all()]
151             for filename in zip.namelist():
152                 if filename[-1] == '/':
153                     continue
154                 title = os.path.basename(filename)[:-4]
155                 slug = slughifi(title)
156                 if not (slug and filename.endswith('.xml')):
157                     skipped_list.append(filename)
158                 elif slug in slugs:
159                     error_list.append((filename, slug, _('Slug already used for %s' % slugs[slug])))
160                 elif slug in existing:
161                     error_list.append((filename, slug, _('Slug already used in repository.')))
162                 else:
163                     try:
164                         zip.read(filename).decode('utf-8')  # test read
165                         ok_list.append((filename, slug, title))
166                     except UnicodeDecodeError:
167                         error_list.append((filename, title, _('File should be UTF-8 encoded.')))
168                     slugs[slug] = filename
169
170             if not error_list:
171                 for filename, slug, title in ok_list:
172                     book = Book.create(
173                         text=zip.read(filename).decode('utf-8'),
174                         creator=creator,
175                         slug=slug,
176                         title=title,
177                     )
178
179             return render(request, "catalogue/document_upload.html", {
180                 "form": form,
181                 "ok_list": ok_list,
182                 "skipped_list": skipped_list,
183                 "error_list": error_list,
184
185                 "logout_to": '/',
186             })
187     else:
188         form = forms.DocumentsUploadForm()
189
190     return render(request, "catalogue/document_upload.html", {
191         "form": form,
192
193         "logout_to": '/',
194     })
195
196
197 @never_cache
198 def book_xml(request, slug):
199     book = get_object_or_404(Book, slug=slug)
200     if not book.accessible(request):
201         return HttpResponseForbidden("Not authorized.")
202     xml = book.materialize()
203
204     response = http.HttpResponse(xml, content_type='application/xml', mimetype='application/wl+xml')
205     response['Content-Disposition'] = 'attachment; filename=%s.xml' % slug
206     return response
207
208
209 @never_cache
210 def book_txt(request, slug):
211     book = get_object_or_404(Book, slug=slug)
212     if not book.accessible(request):
213         return HttpResponseForbidden("Not authorized.")
214
215     doc = book.wldocument()
216     text = doc.as_text().get_string()
217     response = http.HttpResponse(text, content_type='text/plain', mimetype='text/plain')
218     response['Content-Disposition'] = 'attachment; filename=%s.txt' % slug
219     return response
220
221
222 @never_cache
223 def book_html(request, slug):
224     book = get_object_or_404(Book, slug=slug)
225     if not book.accessible(request):
226         return HttpResponseForbidden("Not authorized.")
227
228     doc = book.wldocument(parse_dublincore=False)
229     html = doc.as_html()
230
231     html = html.get_string() if html is not None else ''
232     # response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
233     # return response
234     # book_themes = {}
235     # for fragment in book.fragments.all().iterator():
236     #     for theme in fragment.tags.filter(category='theme').iterator():
237     #         book_themes.setdefault(theme, []).append(fragment)
238
239     # book_themes = book_themes.items()
240     # book_themes.sort(key=lambda s: s[0].sort_key)
241     return render_to_response(
242         'catalogue/book_text.html', locals(),
243         context_instance=RequestContext(request))
244
245
246 @never_cache
247 def book_pdf(request, slug):
248     book = get_object_or_404(Book, slug=slug)
249     if not book.accessible(request):
250         return HttpResponseForbidden("Not authorized.")
251
252     # TODO: move to celery
253     doc = book.wldocument()
254     # TODO: error handling
255     pdf_file = doc.as_pdf()
256     from catalogue.ebook_utils import serve_file
257     return serve_file(pdf_file.get_filename(), book.slug + '.pdf', 'application/pdf')
258
259
260 @never_cache
261 def book_epub(request, slug):
262     book = get_object_or_404(Book, slug=slug)
263     if not book.accessible(request):
264         return HttpResponseForbidden("Not authorized.")
265
266     # TODO: move to celery
267     doc = book.wldocument()
268     # TODO: error handling
269     epub = doc.as_epub().get_string()
270     response = HttpResponse(mimetype='application/epub+zip')
271     response['Content-Disposition'] = 'attachment; filename=%s' % book.slug + '.epub'
272     response.write(epub)
273     return response
274
275
276 @never_cache
277 def revision(request, slug, chunk=None):
278     try:
279         doc = Chunk.get(slug, chunk)
280     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
281         raise Http404
282     if not doc.book.accessible(request):
283         return HttpResponseForbidden("Not authorized.")
284     return http.HttpResponse(str(doc.revision()))
285
286
287 def book(request, slug):
288     book = get_object_or_404(Book, slug=slug)
289     if not book.accessible(request):
290         return HttpResponseForbidden("Not authorized.")
291
292     if request.user.has_perm('catalogue.change_book'):
293         if request.method == "POST":
294             form = forms.BookForm(request.POST, instance=book)
295             if form.is_valid():
296                 form.save()
297                 return http.HttpResponseRedirect(book.get_absolute_url())
298         else:
299             form = forms.BookForm(instance=book)
300         editable = True
301     else:
302         form = forms.ReadonlyBookForm(instance=book)
303         editable = False
304
305     publish_error = book.publishable_error()
306     publishable = publish_error is None
307
308     return render(request, "catalogue/book_detail.html", {
309         "book": book,
310         "publishable": publishable,
311         "publishable_error": publish_error,
312         "form": form,
313         "editable": editable,
314     })
315
316
317 @permission_required('catalogue.add_chunk')
318 def chunk_add(request, slug, chunk):
319     try:
320         doc = Chunk.get(slug, chunk)
321     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
322         raise Http404
323     if not doc.book.accessible(request):
324         return HttpResponseForbidden("Not authorized.")
325
326     if request.method == "POST":
327         form = forms.ChunkAddForm(request.POST, instance=doc)
328         if form.is_valid():
329             if request.user.is_authenticated():
330                 creator = request.user
331             else:
332                 creator = None
333             doc.split(
334                 creator=creator,
335                 slug=form.cleaned_data['slug'],
336                 title=form.cleaned_data['title'],
337                 gallery_start=form.cleaned_data['gallery_start'],
338                 user=form.cleaned_data['user'],
339                 stage=form.cleaned_data['stage']
340             )
341
342             return http.HttpResponseRedirect(doc.book.get_absolute_url())
343     else:
344         form = forms.ChunkAddForm(initial={
345                 "slug": str(doc.number + 1),
346                 "title": "cz. %d" % (doc.number + 1, ),
347         })
348
349     return render(request, "catalogue/chunk_add.html", {
350         "chunk": doc,
351         "form": form,
352     })
353
354
355 @login_required
356 def chunk_edit(request, slug, chunk):
357     try:
358         doc = Chunk.get(slug, chunk)
359     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
360         raise Http404
361     if not doc.book.accessible(request):
362         return HttpResponseForbidden("Not authorized.")
363
364     if request.method == "POST":
365         form = forms.ChunkForm(request.POST, instance=doc)
366         if form.is_valid():
367             form.save()
368             go_next = request.GET.get('next', None)
369             if go_next:
370                 go_next = urlquote_plus(unquote(iri_to_uri(go_next)), safe='/?=&')
371             else:
372                 go_next = doc.book.get_absolute_url()
373             return http.HttpResponseRedirect(go_next)
374     else:
375         form = forms.ChunkForm(instance=doc)
376
377     referer = request.META.get('HTTP_REFERER')
378     if referer:
379         parts = urlsplit(referer)
380         parts = ['', ''] + list(parts[2:])
381         go_next = urlquote_plus(urlunsplit(parts))
382     else:
383         go_next = ''
384
385     return render(request, "catalogue/chunk_edit.html", {
386         "chunk": doc,
387         "form": form,
388         "go_next": go_next,
389     })
390
391
392 @transaction.commit_on_success
393 @login_required
394 def chunk_mass_edit(request):
395     if request.method == 'POST':
396         ids = map(int, filter(lambda i: i.strip() != '', request.POST.get('ids').split(',')))
397         chunks = map(lambda i: Chunk.objects.get(id=i), ids)
398         
399         stage = request.POST.get('stage')
400         if stage:
401             try:
402                 stage = Chunk.tag_model.objects.get(slug=stage)
403             except Chunk.DoesNotExist:
404                 stage = None
405            
406             for c in chunks:
407                 c.stage = stage
408
409         username = request.POST.get('user')
410         logger.info("username: %s" % username)
411         logger.info(request.POST)
412         if username:
413             try:
414                 user = User.objects.get(username=username)
415             except User.DoesNotExist, e:
416                 user = None
417                 
418             for c in chunks:
419                 c.user = user
420
421         status = request.POST.get('status')
422         if status:
423             books_affected = set()
424             for c in chunks:
425                 if status == 'publish':
426                     c.head.publishable = True
427                     c.head.save()
428                 elif status == 'unpublish':
429                     c.head.publishable = False
430                     c.head.save()
431                 c.touch()  # cache
432                 books_affected.add(c.book)
433             for b in books_affected:
434                 b.touch()  # cache
435
436         project_id = request.POST.get('project')
437         if project_id:
438             try:
439                 project = Project.objects.get(pk=int(project_id))
440             except (Project.DoesNotExist, ValueError), e:
441                 project = None
442             for c in chunks:
443                 book = c.book
444                 book.project = project
445                 book.save()
446
447         for c in chunks:
448             c.save()
449
450         return HttpResponse("", content_type="text/plain")
451     else:
452         raise Http404
453
454
455 @permission_required('catalogue.change_book')
456 def book_append(request, slug):
457     book = get_object_or_404(Book, slug=slug)
458     if not book.accessible(request):
459         return HttpResponseForbidden("Not authorized.")
460
461     if request.method == "POST":
462         form = forms.BookAppendForm(book, request.POST)
463         if form.is_valid():
464             append_to = form.cleaned_data['append_to']
465             append_to.append(book)
466             return http.HttpResponseRedirect(append_to.get_absolute_url())
467     else:
468         form = forms.BookAppendForm(book)
469     return render(request, "catalogue/book_append_to.html", {
470         "book": book,
471         "form": form,
472
473         "logout_to": '/',
474     })
475
476
477 @require_POST
478 @login_required
479 def publish(request, slug):
480     book = get_object_or_404(Book, slug=slug)
481     if not book.accessible(request):
482         return HttpResponseForbidden("Not authorized.")
483
484     try:
485         book.publish(request.user)
486     except NotAuthorizedError:
487         return http.HttpResponseRedirect(reverse('apiclient_oauth'))
488     except BaseException, e:
489         return http.HttpResponse(e)
490     else:
491         return http.HttpResponseRedirect(book.get_absolute_url())
492
493
494 class GalleryMixin(object):
495     def get_directory(self):
496         return "%s%s/" % (settings.IMAGE_DIR, self.object.gallery)
497
498     @staticmethod
499     def get_object(request, slug):
500         book = get_object_or_404(Book, slug=slug)
501         if not book.gallery:
502             raise Http404
503         return book
504
505
506 class GalleryView(GalleryMixin, UploadView):
507
508     def breadcrumbs(self):
509         return [
510             (u'moduły', reverse('catalogue_document_list')),
511             (self.object.title, self.object.get_absolute_url()),
512             (u'materiały',),
513         ]
514
515
516 class GalleryPackageView(GalleryMixin, PackageView):
517
518     def get_redirect_url(self, slug):
519         return reverse('catalogue_book_gallery', kwargs={'slug': slug})
520
521
522 @ajax(method='get')
523 def lessons_for_cybernauts(request):
524     books = Book.objects.filter(for_cybernauts=True)
525     data = []
526     for book in books:
527         try:
528             changes = book.get_current_changes()
529             time_changed = max(change.created_at for change in changes)
530             xml_url = reverse('catalogue_book_xml', args=[book.slug])
531             data.append({
532                 'slug': book.slug,
533                 'url': xml_url,
534                 'time_changed': time_changed.isoformat(),
535             })
536         except Book.NoTextError:
537             pass
538     return {'lessons': data}