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
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     sst = SST.from_string(rev.materialize())
218     
219     ctx = Context(
220         files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
221         source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
222     )
223     if doc.owner_organization is not None and doc.owner_organization.logo:
224         ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
225     try:
226         pdf_file = PdfFormat(sst).build(ctx)
227     except BuildError as e:
228         from django.http import HttpResponse
229         return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
230
231     from catalogue.ebook_utils import serve_file
232     return serve_file(pdf_file.get_filename(), '%d.pdf' % doc.pk, 'application/pdf')
233
234
235 @never_cache
236 def book_epub(request, pk, rev_pk):
237     from librarian.utils import Context
238     from librarian.document import Document as SST
239     from librarian.formats.epub import EpubFormat
240
241     doc = get_object_or_404(Document, pk=pk)
242     rev = get_object_or_404(Revision, pk=rev_pk)
243     # Test
244
245     sst = SST.from_string(rev.materialize())
246
247     ctx = Context(
248         files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
249         source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
250     )
251     if doc.owner_organization is not None and doc.owner_organization.logo:
252         ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
253     try:
254         epub_file = EpubFormat(sst).build(ctx)
255     except BuildError as e:
256         from django.http import HttpResponse
257         return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
258
259     from catalogue.ebook_utils import serve_file
260     return serve_file(epub_file.get_filename(), '%d.epub' % doc.pk, 'application/epub+zip')
261
262
263 @never_cache
264 def book_mobi(request, pk, rev_pk):
265     from librarian.utils import Context
266     from librarian.document import Document as SST
267     from librarian.formats.epub import EpubFormat
268
269     doc = get_object_or_404(Document, pk=pk)
270     rev = get_object_or_404(Revision, pk=rev_pk)
271
272     sst = SST.from_string(rev.materialize())
273
274     ctx = Context(
275         files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
276         source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
277     )
278     if doc.owner_organization is not None and doc.owner_organization.logo:
279         ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
280     try:
281         epub_file = EpubFormat(sst).build(ctx)
282     except BuildError as e:
283         from django.http import HttpResponse
284         return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
285
286     output_file = NamedTemporaryFile(prefix='librarian', suffix='.mobi', delete=False)
287     output_file.close()
288     subprocess.check_call(
289         ['ebook-convert', epub_file.get_filename(), output_file.name, '--no-inline-toc'])
290
291     from catalogue.ebook_utils import serve_file
292     return serve_file(output_file.name, '%d.mobi' % doc.pk, 'application/epub+zip')
293
294
295 # @never_cache
296 # def revision(request, slug, chunk=None):
297 #     try:
298 #         doc = Chunk.get(slug, chunk)
299 #     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
300 #         raise Http404
301 #     if not doc.book.accessible(request):
302 #         return HttpResponseForbidden("Not authorized.")
303 #     return http.HttpResponse(str(doc.revision()))
304
305
306 @login_required
307 def book_schedule(request, pk):
308     book = get_object_or_404(Document, pk=pk, deleted=False)
309     if request.method == 'POST':
310         Plan.objects.filter(document=book).delete()
311         for i, s in enumerate(STAGES):
312             user_id = request.POST.get('s%d-user' % i)
313             deadline = request.POST.get('s%d-deadline' % i) or None
314             Plan.objects.create(document=book, stage=s, user_id=user_id, deadline=deadline)
315
316         book.set_stage(request.POST.get('stage', ''))
317         return redirect('catalogue_user')
318
319     current = {}
320     for p in Plan.objects.filter(document=book):
321         current[p.stage] = (getattr(p.user, 'pk', None), (p.deadline.isoformat() if p.deadline else None))
322
323     schedule = [(i, s, current.get(s, ())) for (i, s) in enumerate(STAGES)]
324     
325     if book.owner_organization:
326         people = [m.user for m in book.owner_organization.membership_set.exclude(status='pending')]
327     else:
328         people = [book.owner_user]
329     return render(request, 'catalogue/book_schedule.html', {
330         'book': book,
331         'schedule': schedule,
332         'people': people,
333     })
334
335
336 @login_required
337 def book_owner(request, pk):
338     doc = get_object_or_404(Document, pk=pk, deleted=False)
339     user_is_owner = doc.owner_organization and doc.owner_organization.is_member(request.user)
340     if not (doc.owner_user == request.user or user_is_owner):
341         raise Http404
342
343     error = ''
344
345     if request.method == 'POST':
346         # TODO: real form
347         new_org_pk = request.POST.get('owner_organization')
348         if not new_org_pk:
349             doc.owner_organization = None
350             doc.owner_user = request.user
351             doc.save()
352         else:
353             org = Organization.objects.get(pk=new_org_pk)
354             if not org.is_member(request.user):
355                 error = 'Bad organization'
356             else:
357                 doc.owner_organization = org
358                 doc.owner_user = None
359                 doc.save()
360         if not error:
361             return redirect('catalogue_user')
362
363     return render(request, 'catalogue/book_owner.html', {
364         'doc': doc,
365         'error': error,
366     })
367
368
369 @login_required
370 def book_delete(request, pk):
371     doc = get_object_or_404(Document, pk=pk, deleted=False)
372     if not (doc.owner_user == request.user or doc.owner_organization.is_member(request.user)):
373         raise Http404
374
375     if request.method == 'POST':
376         doc.deleted = True
377         doc.save()
378         return redirect('catalogue_user')
379
380     return render(request, 'catalogue/book_delete.html', {
381         'doc': doc,
382     })
383
384
385 @require_POST
386 @login_required
387 def publish(request, pk):
388     from wiki import forms
389     from .models import PublishRecord
390     from dvcs.models import Revision
391
392     # FIXME: check permissions
393
394     doc = get_object_or_404(Document, pk=pk, deleted=False)
395     form = forms.DocumentTextPublishForm(request.POST, prefix="textpublish")
396     if form.is_valid():
397         rev = Revision.objects.get(pk=form.cleaned_data['revision'])
398         # FIXME: check if in tree
399         # if PublishRecord.objects.filter(revision=rev, document=doc).exists():
400         #     return http.HttpResponse('exists')
401         PublishRecord.objects.create(revision=rev, document=doc, user=request.user)
402         if request.is_ajax():
403             return http.HttpResponse('ok')
404         else:
405             return redirect('catalogue_html', doc.pk)
406     else:
407         if request.is_ajax():
408             return http.HttpResponse('error')
409         else:
410             try:
411                 return redirect('catalogue_preview_rev', doc.pk, form.cleaned_data['revision'])
412             except KeyError:
413                 return redirect('catalogue_preview', doc.pk)
414
415
416 @require_POST
417 @login_required
418 def unpublish(request, pk):
419     # FIXME: check permissions
420
421     doc = get_object_or_404(Document, pk=pk, deleted=False)
422     doc.publish_log.all().delete()
423     if request.is_ajax():
424         return http.HttpResponse('ok')
425     else:
426         return redirect('catalogue_html', doc.pk)
427
428
429 class GalleryMixin(object):
430     def get_directory(self):
431         # return "%s%s/" % (settings.IMAGE_DIR, 'org%d' % self.org.pk if self.org is not None else self.request.user.pk)
432         return "uploads/%d/" % self.doc.pk
433
434
435 class GalleryView(GalleryMixin, UploadView):
436
437     def breadcrumbs(self):
438         return [
439                 (self.doc.meta()['title'], '/documents/%d/' % self.doc.pk),
440             ]
441
442     def get_object(self, request, pk=None):
443         self.doc = Document.objects.get(pk=pk, deleted=False)
444
445
446 @login_required
447 def fork(request, pk):
448     doc = get_object_or_404(Document, pk=pk, deleted=False)
449     if request.method == "POST":
450         form = forms.DocumentForkForm(request.POST, request.FILES)
451         if form.is_valid():
452             try:
453                 org = request.user.membership_set.get(
454                     organization=int(form.cleaned_data['owner_organization'])).organization
455                 kwargs = {'owner_organization': org}
456             except:
457                 kwargs = {'owner_user': request.user}
458
459             new_doc = Document.objects.create(revision=doc.revision, **kwargs)
460
461             if os.path.isdir(settings.MEDIA_ROOT + "uploads/%d" % doc.pk):
462                 shutil.copytree(
463                     settings.MEDIA_ROOT + "uploads/%d" % doc.pk,
464                     settings.MEDIA_ROOT + "uploads/%d" % new_doc.pk
465                 )
466
467             new_doc.assigned_to = request.user
468             new_doc.save()
469
470             return http.HttpResponseRedirect(reverse("wiki_editor", args=[new_doc.pk]))
471     else:
472         form = forms.DocumentForkForm()
473
474     return render(request, "catalogue/document_fork.html", {
475         "form": form,
476
477         "logout_to": '/',
478     })
479
480
481 def upcoming(request):
482     return render(request, "catalogue/upcoming.html", {
483         'objects_list': Document.objects.filter(deleted=False).filter(publish_log=None),
484     })
485
486
487 def finished(request):
488     return render(request, "catalogue/finished.html", {
489         'objects_list': Document.objects.filter(deleted=False).exclude(publish_log=None),
490     })