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