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