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