more error reporting
[redakcja.git] / apps / catalogue / views.py
1 # -*- coding: utf-8 -*-
2 #
3 # This file is part of MIL/PEER, licensed under GNU Affero GPLv3 or later.
4 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
5 #
6 import logging
7 import os
8 import shutil
9 import subprocess
10 from tempfile import NamedTemporaryFile
11
12 from django.conf import settings
13 from django.contrib import auth
14 from django.contrib.auth.models import User
15 from django.contrib.auth.decorators import login_required
16 from django.core.urlresolvers import reverse
17 from django import http
18 from django.http import Http404, HttpResponse
19 from django.shortcuts import get_object_or_404, render, redirect
20 from django.utils.encoding import force_str
21 from django.utils.http import urlquote_plus
22 from django.views.decorators.http import require_POST
23
24 from catalogue import forms
25 from catalogue.helpers import active_tab
26 from librarian import BuildError
27 from .constants import STAGES
28 from .models import Document, Plan
29 from dvcs.models import Revision
30 from organizations.models import Organization
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 # from fnpdjango.utils.text.slughifi import slughifi
38
39 logger = logging.getLogger("fnp.catalogue")
40
41
42 @active_tab('all')
43 @never_cache
44 def document_list(request):
45     return render(request, 'catalogue/document_list.html')
46
47
48 @never_cache
49 def user(request, username):
50     user = get_object_or_404(User, username=username)
51     return render(request, 'catalogue/user_page.html', {"viewed_user": user})
52
53
54 @login_required
55 @active_tab('my')
56 @never_cache
57 def my(request):
58     return render(request, 'catalogue/my_page.html', {
59         'last_books': sorted(
60             request.session.get("wiki_last_books", {}).items(), key=lambda x: x[1]['time'], reverse=True),
61
62         "logout_to": '/',
63         })
64
65
66 @never_cache
67 def logout_then_redirect(request):
68     auth.logout(request)
69     return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
70
71
72 @login_required
73 @active_tab('create')
74 def create_missing(request):
75     if request.method == "POST":
76         form = forms.DocumentCreateForm(request.POST, request.FILES)
77         if form.is_valid():
78             
79             if request.user.is_authenticated():
80                 creator = request.user
81             else:
82                 creator = None
83
84             title = form.cleaned_data['title']
85             try:
86                 org = request.user.membership_set.get(
87                     organization=int(form.cleaned_data['owner_organization'])).organization
88                 kwargs = {'owner_organization': org}
89             except:
90                 kwargs = {'owner_user': request.user}
91
92             doc = Document.objects.create(**kwargs)
93
94             cover = request.FILES.get('cover')
95             if cover:
96                 uppath = 'uploads/%d/' % doc.pk
97                 path = settings.MEDIA_ROOT + uppath
98                 if not os.path.isdir(path):
99                     os.makedirs(path)
100                 dest_path = path + cover.name   # UNSAFE
101                 with open(dest_path, 'w') as destination:
102                     for chunk in cover.chunks():
103                         destination.write(chunk)
104                 cover_url = 'http://milpeer.eu/media/dynamic/' + uppath + cover.name
105             else:
106                 cover_url = ''
107
108             doc.commit(
109                 text='''<section xmlns="http://nowoczesnapolska.org.pl/sst#" xmlns:dc="http://purl.org/dc/elements/1.1/">
110                 <metadata>
111                     <dc:publisher>''' + form.cleaned_data['publisher'] + '''</dc:publisher>
112                     <dc:description>''' + form.cleaned_data['description'] + '''</dc:description>
113                     <dc:language>''' + form.cleaned_data['language'] + '''</dc:language>
114                     <dc:rights>''' + form.cleaned_data['rights'] + '''</dc:rights>
115                     <dc:audience>''' + form.cleaned_data['audience'] + '''</dc:audience>
116                     <dc:relation.coverImage.url>''' + cover_url + '''</dc:relation.coverImage.url>
117                 </metadata>
118                 <header>''' + title + '''</header>
119                 <div class="p"> </div>
120                 </section>''',
121                 author=creator
122             )
123             doc.assigned_to = request.user
124             doc.save()
125
126             return http.HttpResponseRedirect(reverse("wiki_editor", args=[doc.pk]))
127     else:
128         org_pk = request.GET.get('organization')
129         if org_pk:
130             try:
131                 org = Organization.objects.get(pk=org_pk)
132             except Organization.DoesNotExist:
133                 org = None
134             else:
135                 if not org.is_member(request.user):
136                     org = None
137         else:
138             org = None
139         if org is not None:
140             org = org.pk
141
142         form = forms.DocumentCreateForm(initial={'owner_organization': org})
143
144     return render(request, "catalogue/document_create_missing.html", {
145         "form": form,
146
147         "logout_to": '/',
148     })
149
150
151 @never_cache
152 def book_html(request, pk, rev_pk=None, preview=False):
153     from librarian.document import Document as SST
154     from librarian.formats.html import HtmlFormat
155
156     doc = get_object_or_404(Document, pk=pk, deleted=False)
157
158     try:
159         published_revision = doc.publish_log.all()[0].revision
160     except IndexError:
161         published_revision = None
162
163     if rev_pk is None:
164         if preview:
165             revision = doc.revision
166         else:
167             if published_revision is not None:
168                 revision = published_revision
169             else:
170                 # No published version, fallback to preview mode.
171                 preview = True
172                 revision = doc.revision
173     else:
174         revision = get_object_or_404(Revision, pk=rev_pk)
175
176     was_published = revision == published_revision or doc.publish_log.filter(revision=revision).exists()
177
178     try:
179         sst = SST.from_string(revision.materialize())
180     except ValueError as e:
181         html = e
182     else:
183         html = HtmlFormat(sst).build(
184             files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk)).get_string()
185
186     # response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
187     # return response
188     # book_themes = {}
189     # for fragment in book.fragments.all().iterator():
190     #     for theme in fragment.tags.filter(category='theme').iterator():
191     #         book_themes.setdefault(theme, []).append(fragment)
192
193     # book_themes = book_themes.items()
194     # book_themes.sort(key=lambda s: s[0].sort_key)
195     return render(request, 'catalogue/book_text.html', {
196         'doc': doc,
197         'preview': preview,
198         'revision': revision,
199         'published_revision': published_revision,
200         'specific': rev_pk is not None,
201         'html': html,
202         'can_edit': doc.can_edit(request.user) if doc else None,
203         'was_published': was_published,
204     })
205
206
207 @never_cache
208 def book_pdf(request, pk, rev_pk):
209     from librarian.utils import Context
210     from librarian.document import Document as SST
211     from librarian.formats.pdf import PdfFormat
212
213     doc = get_object_or_404(Document, pk=pk)
214     rev = get_object_or_404(Revision, pk=rev_pk)
215     # Test
216
217     try:
218         sst = SST.from_string(rev.materialize())
219     except ValueError as e:
220         return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
221     
222     ctx = Context(
223         files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
224         source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
225     )
226     if doc.owner_organization is not None and doc.owner_organization.logo:
227         ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
228     try:
229         pdf_file = PdfFormat(sst).build(ctx)
230     except BuildError as e:
231         return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
232
233     from catalogue.ebook_utils import serve_file
234     return serve_file(pdf_file.get_filename(), '%d.pdf' % doc.pk, 'application/pdf')
235
236
237 @never_cache
238 def book_epub(request, pk, rev_pk):
239     from librarian.utils import Context
240     from librarian.document import Document as SST
241     from librarian.formats.epub import EpubFormat
242
243     doc = get_object_or_404(Document, pk=pk)
244     rev = get_object_or_404(Revision, pk=rev_pk)
245     # Test
246
247     try:
248         sst = SST.from_string(rev.materialize())
249     except ValueError as e:
250         return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
251
252     ctx = Context(
253         files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
254         source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
255     )
256     if doc.owner_organization is not None and doc.owner_organization.logo:
257         ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
258     try:
259         epub_file = EpubFormat(sst).build(ctx)
260     except BuildError as e:
261         return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
262
263     from catalogue.ebook_utils import serve_file
264     return serve_file(epub_file.get_filename(), '%d.epub' % doc.pk, 'application/epub+zip')
265
266
267 @never_cache
268 def book_mobi(request, pk, rev_pk):
269     from librarian.utils import Context
270     from librarian.document import Document as SST
271     from librarian.formats.epub import EpubFormat
272
273     doc = get_object_or_404(Document, pk=pk)
274     rev = get_object_or_404(Revision, pk=rev_pk)
275
276     try:
277         sst = SST.from_string(rev.materialize())
278     except ValueError as e:
279         return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
280
281     ctx = Context(
282         files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
283         source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
284     )
285     if doc.owner_organization is not None and doc.owner_organization.logo:
286         ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
287     try:
288         epub_file = EpubFormat(sst).build(ctx)
289     except BuildError as e:
290         return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
291
292     output_file = NamedTemporaryFile(prefix='librarian', suffix='.mobi', delete=False)
293     output_file.close()
294     subprocess.check_call(
295         ['ebook-convert', epub_file.get_filename(), output_file.name, '--no-inline-toc'])
296
297     from catalogue.ebook_utils import serve_file
298     return serve_file(output_file.name, '%d.mobi' % doc.pk, 'application/epub+zip')
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 @login_required
313 def book_schedule(request, pk):
314     book = get_object_or_404(Document, pk=pk, deleted=False)
315     if request.method == 'POST':
316         Plan.objects.filter(document=book).delete()
317         for i, s in enumerate(STAGES):
318             user_id = request.POST.get('s%d-user' % i)
319             deadline = request.POST.get('s%d-deadline' % i) or None
320             Plan.objects.create(document=book, stage=s, user_id=user_id, deadline=deadline)
321
322         book.set_stage(request.POST.get('stage', ''))
323         return redirect('catalogue_user')
324
325     current = {}
326     for p in Plan.objects.filter(document=book):
327         current[p.stage] = (getattr(p.user, 'pk', None), (p.deadline.isoformat() if p.deadline else None))
328
329     schedule = [(i, s, current.get(s, ())) for (i, s) in enumerate(STAGES)]
330     
331     if book.owner_organization:
332         people = [m.user for m in book.owner_organization.membership_set.exclude(status='pending')]
333     else:
334         people = [book.owner_user]
335     return render(request, 'catalogue/book_schedule.html', {
336         'book': book,
337         'schedule': schedule,
338         'people': people,
339     })
340
341
342 @login_required
343 def book_owner(request, pk):
344     doc = get_object_or_404(Document, pk=pk, deleted=False)
345     user_is_owner = doc.owner_organization and doc.owner_organization.is_member(request.user)
346     if not (doc.owner_user == request.user or user_is_owner):
347         raise Http404
348
349     error = ''
350
351     if request.method == 'POST':
352         # TODO: real form
353         new_org_pk = request.POST.get('owner_organization')
354         if not new_org_pk:
355             doc.owner_organization = None
356             doc.owner_user = request.user
357             doc.save()
358         else:
359             org = Organization.objects.get(pk=new_org_pk)
360             if not org.is_member(request.user):
361                 error = 'Bad organization'
362             else:
363                 doc.owner_organization = org
364                 doc.owner_user = None
365                 doc.save()
366         if not error:
367             return redirect('catalogue_user')
368
369     return render(request, 'catalogue/book_owner.html', {
370         'doc': doc,
371         'error': error,
372     })
373
374
375 @login_required
376 def book_delete(request, pk):
377     doc = get_object_or_404(Document, pk=pk, deleted=False)
378     if not (doc.owner_user == request.user or doc.owner_organization.is_member(request.user)):
379         raise Http404
380
381     if request.method == 'POST':
382         doc.deleted = True
383         doc.save()
384         return redirect('catalogue_user')
385
386     return render(request, 'catalogue/book_delete.html', {
387         'doc': doc,
388     })
389
390
391 @require_POST
392 @login_required
393 def publish(request, pk):
394     from wiki import forms
395     from .models import PublishRecord
396     from dvcs.models import Revision
397
398     # FIXME: check permissions
399
400     doc = get_object_or_404(Document, pk=pk, deleted=False)
401     form = forms.DocumentTextPublishForm(request.POST, prefix="textpublish")
402     if form.is_valid():
403         rev = Revision.objects.get(pk=form.cleaned_data['revision'])
404         # FIXME: check if in tree
405         # if PublishRecord.objects.filter(revision=rev, document=doc).exists():
406         #     return http.HttpResponse('exists')
407         PublishRecord.objects.create(revision=rev, document=doc, user=request.user)
408         if request.is_ajax():
409             return http.HttpResponse('ok')
410         else:
411             return redirect('catalogue_html', doc.pk)
412     else:
413         if request.is_ajax():
414             return http.HttpResponse('error')
415         else:
416             try:
417                 return redirect('catalogue_preview_rev', doc.pk, form.cleaned_data['revision'])
418             except KeyError:
419                 return redirect('catalogue_preview', doc.pk)
420
421
422 @require_POST
423 @login_required
424 def unpublish(request, pk):
425     # FIXME: check permissions
426
427     doc = get_object_or_404(Document, pk=pk, deleted=False)
428     doc.publish_log.all().delete()
429     if request.is_ajax():
430         return http.HttpResponse('ok')
431     else:
432         return redirect('catalogue_html', doc.pk)
433
434
435 class GalleryMixin(object):
436     def get_directory(self):
437         # return "%s%s/" % (settings.IMAGE_DIR, 'org%d' % self.org.pk if self.org is not None else self.request.user.pk)
438         return "uploads/%d/" % self.doc.pk
439
440
441 class GalleryView(GalleryMixin, UploadView):
442
443     def breadcrumbs(self):
444         return [
445                 (self.doc.meta()['title'], '/documents/%d/' % self.doc.pk),
446             ]
447
448     def get_object(self, request, pk=None):
449         self.doc = Document.objects.get(pk=pk, deleted=False)
450
451
452 @login_required
453 def fork(request, pk):
454     doc = get_object_or_404(Document, pk=pk, deleted=False)
455     if request.method == "POST":
456         form = forms.DocumentForkForm(request.POST, request.FILES)
457         if form.is_valid():
458             try:
459                 org = request.user.membership_set.get(
460                     organization=int(form.cleaned_data['owner_organization'])).organization
461                 kwargs = {'owner_organization': org}
462             except:
463                 kwargs = {'owner_user': request.user}
464
465             new_doc = Document.objects.create(revision=doc.revision, **kwargs)
466
467             if os.path.isdir(settings.MEDIA_ROOT + "uploads/%d" % doc.pk):
468                 shutil.copytree(
469                     settings.MEDIA_ROOT + "uploads/%d" % doc.pk,
470                     settings.MEDIA_ROOT + "uploads/%d" % new_doc.pk
471                 )
472
473             new_doc.assigned_to = request.user
474             new_doc.save()
475
476             return http.HttpResponseRedirect(reverse("wiki_editor", args=[new_doc.pk]))
477     else:
478         form = forms.DocumentForkForm()
479
480     return render(request, "catalogue/document_fork.html", {
481         "form": form,
482
483         "logout_to": '/',
484     })
485
486
487 def upcoming(request):
488     return render(request, "catalogue/upcoming.html", {
489         'objects_list': Document.objects.filter(deleted=False).filter(publish_log=None),
490     })
491
492
493 def finished(request):
494     return render(request, "catalogue/finished.html", {
495         'objects_list': Document.objects.filter(deleted=False).exclude(publish_log=None),
496     })