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