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