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