1 # -*- coding: utf-8 -*-
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.
10 from tempfile import NamedTemporaryFile
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, HttpResponse
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
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
34 # Quick hack around caching problems, TODO: use ETags
36 from django.views.decorators.cache import never_cache
37 # from fnpdjango.utils.text.slughifi import slughifi
39 logger = logging.getLogger("fnp.catalogue")
44 def document_list(request):
45 return render(request, 'catalogue/document_list.html')
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})
58 return render(request, 'catalogue/my_page.html', {
60 request.session.get("wiki_last_books", {}).items(), key=lambda x: x[1]['time'], reverse=True),
67 def logout_then_redirect(request):
69 return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
74 def create_missing(request):
75 if request.method == "POST":
76 form = forms.DocumentCreateForm(request.POST, request.FILES)
79 if request.user.is_authenticated():
80 creator = request.user
84 title = form.cleaned_data['title']
86 org = request.user.membership_set.get(
87 organization=int(form.cleaned_data['owner_organization'])).organization
88 kwargs = {'owner_organization': org}
90 kwargs = {'owner_user': request.user}
92 doc = Document.objects.create(**kwargs)
94 cover = request.FILES.get('cover')
96 uppath = 'uploads/%d/' % doc.pk
97 path = settings.MEDIA_ROOT + uppath
98 if not os.path.isdir(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
109 text='''<section xmlns="http://nowoczesnapolska.org.pl/sst#" xmlns:dc="http://purl.org/dc/elements/1.1/">
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>
118 <header>''' + title + '''</header>
119 <div class="p"> </div>
123 doc.assigned_to = request.user
126 return http.HttpResponseRedirect(reverse("wiki_editor", args=[doc.pk]))
128 org_pk = request.GET.get('organization')
131 org = Organization.objects.get(pk=org_pk)
132 except Organization.DoesNotExist:
135 if not org.is_member(request.user):
142 form = forms.DocumentCreateForm(initial={'owner_organization': org})
144 return render(request, "catalogue/document_create_missing.html", {
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
156 doc = get_object_or_404(Document, pk=pk, deleted=False)
159 published_revision = doc.publish_log.all()[0].revision
161 published_revision = None
165 revision = doc.revision
167 if published_revision is not None:
168 revision = published_revision
170 # No published version, fallback to preview mode.
172 revision = doc.revision
174 revision = get_object_or_404(Revision, pk=rev_pk)
176 was_published = revision == published_revision or doc.publish_log.filter(revision=revision).exists()
179 sst = SST.from_string(revision.materialize())
180 except ValueError as e:
183 html = HtmlFormat(sst).build(
184 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk)).get_string()
186 # response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
189 # for fragment in book.fragments.all().iterator():
190 # for theme in fragment.tags.filter(category='theme').iterator():
191 # book_themes.setdefault(theme, []).append(fragment)
193 # book_themes = book_themes.items()
194 # book_themes.sort(key=lambda s: s[0].sort_key)
195 return render(request, 'catalogue/book_text.html', {
198 'revision': revision,
199 'published_revision': published_revision,
200 'specific': rev_pk is not None,
202 'can_edit': doc.can_edit(request.user) if doc else None,
203 'was_published': was_published,
208 def book_pdf(request, pk, rev_pk):
209 from librarian.utils import Context
210 from librarian.document import Document as SST
211 from librarian.formats.pdf import PdfFormat
213 doc = get_object_or_404(Document, pk=pk)
214 rev = get_object_or_404(Revision, pk=rev_pk)
218 sst = SST.from_string(rev.materialize())
219 except ValueError as e:
220 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
223 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
224 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
226 if doc.owner_organization is not None and doc.owner_organization.logo:
227 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
229 pdf_file = PdfFormat(sst).build(ctx)
230 except BuildError as e:
231 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
233 from catalogue.ebook_utils import serve_file
234 return serve_file(pdf_file.get_filename(), '%d.pdf' % doc.pk, 'application/pdf')
238 def book_epub(request, pk, rev_pk):
239 from librarian.utils import Context
240 from librarian.document import Document as SST
241 from librarian.formats.epub import EpubFormat
243 doc = get_object_or_404(Document, pk=pk)
244 rev = get_object_or_404(Revision, pk=rev_pk)
248 sst = SST.from_string(rev.materialize())
249 except ValueError as e:
250 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
253 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
254 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
256 if doc.owner_organization is not None and doc.owner_organization.logo:
257 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
259 epub_file = EpubFormat(sst).build(ctx)
260 except BuildError as e:
261 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
263 from catalogue.ebook_utils import serve_file
264 return serve_file(epub_file.get_filename(), '%d.epub' % doc.pk, 'application/epub+zip')
268 def book_mobi(request, pk, rev_pk):
269 from librarian.utils import Context
270 from librarian.document import Document as SST
271 from librarian.formats.epub import EpubFormat
273 doc = get_object_or_404(Document, pk=pk)
274 rev = get_object_or_404(Revision, pk=rev_pk)
277 sst = SST.from_string(rev.materialize())
278 except ValueError as e:
279 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
282 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
283 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
285 if doc.owner_organization is not None and doc.owner_organization.logo:
286 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
288 epub_file = EpubFormat(sst).build(ctx)
289 except BuildError as e:
290 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
292 output_file = NamedTemporaryFile(prefix='librarian', suffix='.mobi', delete=False)
294 subprocess.check_call(
295 ['ebook-convert', epub_file.get_filename(), output_file.name, '--no-inline-toc'])
297 from catalogue.ebook_utils import serve_file
298 return serve_file(output_file.name, '%d.mobi' % doc.pk, 'application/epub+zip')
302 # def revision(request, slug, chunk=None):
304 # doc = Chunk.get(slug, chunk)
305 # except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
307 # if not doc.book.accessible(request):
308 # return HttpResponseForbidden("Not authorized.")
309 # return http.HttpResponse(str(doc.revision()))
313 def book_schedule(request, pk):
314 book = get_object_or_404(Document, pk=pk, deleted=False)
315 if request.method == 'POST':
316 Plan.objects.filter(document=book).delete()
317 for i, s in enumerate(STAGES):
318 user_id = request.POST.get('s%d-user' % i)
319 deadline = request.POST.get('s%d-deadline' % i) or None
320 Plan.objects.create(document=book, stage=s, user_id=user_id, deadline=deadline)
322 book.set_stage(request.POST.get('stage', ''))
323 return redirect('catalogue_user')
326 for p in Plan.objects.filter(document=book):
327 current[p.stage] = (getattr(p.user, 'pk', None), (p.deadline.isoformat() if p.deadline else None))
329 schedule = [(i, s, current.get(s, ())) for (i, s) in enumerate(STAGES)]
331 if book.owner_organization:
332 people = [m.user for m in book.owner_organization.membership_set.exclude(status='pending')]
334 people = [book.owner_user]
335 return render(request, 'catalogue/book_schedule.html', {
337 'schedule': schedule,
343 def book_owner(request, pk):
344 doc = get_object_or_404(Document, pk=pk, deleted=False)
345 user_is_owner = doc.owner_organization and doc.owner_organization.is_member(request.user)
346 if not (doc.owner_user == request.user or user_is_owner):
351 if request.method == 'POST':
353 new_org_pk = request.POST.get('owner_organization')
355 doc.owner_organization = None
356 doc.owner_user = request.user
359 org = Organization.objects.get(pk=new_org_pk)
360 if not org.is_member(request.user):
361 error = 'Bad organization'
363 doc.owner_organization = org
364 doc.owner_user = None
367 return redirect('catalogue_user')
369 return render(request, 'catalogue/book_owner.html', {
376 def book_delete(request, pk):
377 doc = get_object_or_404(Document, pk=pk, deleted=False)
378 if not (doc.owner_user == request.user or doc.owner_organization.is_member(request.user)):
381 if request.method == 'POST':
384 return redirect('catalogue_user')
386 return render(request, 'catalogue/book_delete.html', {
393 def publish(request, pk):
394 from wiki import forms
395 from .models import PublishRecord
396 from dvcs.models import Revision
398 # FIXME: check permissions
400 doc = get_object_or_404(Document, pk=pk, deleted=False)
401 form = forms.DocumentTextPublishForm(request.POST, prefix="textpublish")
403 rev = Revision.objects.get(pk=form.cleaned_data['revision'])
404 # FIXME: check if in tree
405 # if PublishRecord.objects.filter(revision=rev, document=doc).exists():
406 # return http.HttpResponse('exists')
407 PublishRecord.objects.create(revision=rev, document=doc, user=request.user)
408 if request.is_ajax():
409 return http.HttpResponse('ok')
411 return redirect('catalogue_html', doc.pk)
413 if request.is_ajax():
414 return http.HttpResponse('error')
417 return redirect('catalogue_preview_rev', doc.pk, form.cleaned_data['revision'])
419 return redirect('catalogue_preview', doc.pk)
424 def unpublish(request, pk):
425 # FIXME: check permissions
427 doc = get_object_or_404(Document, pk=pk, deleted=False)
428 doc.publish_log.all().delete()
429 if request.is_ajax():
430 return http.HttpResponse('ok')
432 return redirect('catalogue_html', doc.pk)
435 class GalleryMixin(object):
436 def get_directory(self):
437 # return "%s%s/" % (settings.IMAGE_DIR, 'org%d' % self.org.pk if self.org is not None else self.request.user.pk)
438 return "uploads/%d/" % self.doc.pk
441 class GalleryView(GalleryMixin, UploadView):
443 def breadcrumbs(self):
445 (self.doc.meta()['title'], '/documents/%d/' % self.doc.pk),
448 def get_object(self, request, pk=None):
449 self.doc = Document.objects.get(pk=pk, deleted=False)
453 def fork(request, pk):
454 doc = get_object_or_404(Document, pk=pk, deleted=False)
455 if request.method == "POST":
456 form = forms.DocumentForkForm(request.POST, request.FILES)
459 org = request.user.membership_set.get(
460 organization=int(form.cleaned_data['owner_organization'])).organization
461 kwargs = {'owner_organization': org}
463 kwargs = {'owner_user': request.user}
465 new_doc = Document.objects.create(revision=doc.revision, **kwargs)
467 if os.path.isdir(settings.MEDIA_ROOT + "uploads/%d" % doc.pk):
469 settings.MEDIA_ROOT + "uploads/%d" % doc.pk,
470 settings.MEDIA_ROOT + "uploads/%d" % new_doc.pk
473 new_doc.assigned_to = request.user
476 return http.HttpResponseRedirect(reverse("wiki_editor", args=[new_doc.pk]))
478 form = forms.DocumentForkForm()
480 return render(request, "catalogue/document_fork.html", {
487 def upcoming(request):
488 return render(request, "catalogue/upcoming.html", {
489 'objects_list': Document.objects.filter(deleted=False).filter(publish_log=None),
493 def finished(request):
494 return render(request, "catalogue/finished.html", {
495 'objects_list': Document.objects.filter(deleted=False).exclude(publish_log=None),