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