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