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
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()
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()
182 # response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
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)
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', {
194 'revision': revision,
195 'published_revision': published_revision,
196 'specific': rev_pk is not None,
198 'can_edit': doc.can_edit(request.user) if doc else None,
199 'was_published': was_published,
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
209 doc = get_object_or_404(Document, pk=pk)
210 rev = get_object_or_404(Revision, pk=rev_pk)
213 sst = SST.from_string(rev.materialize())
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])),
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)
223 from catalogue.ebook_utils import serve_file
224 return serve_file(pdf_file.get_filename(), '%d.pdf' % doc.pk, 'application/pdf')
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
233 doc = get_object_or_404(Document, pk=pk)
234 rev = get_object_or_404(Revision, pk=rev_pk)
237 sst = SST.from_string(rev.materialize())
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])),
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)
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')
251 from catalogue.ebook_utils import serve_file
252 return serve_file(epub_file.get_filename(), '%d.epub' % doc.pk, 'application/epub+zip')
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
261 doc = get_object_or_404(Document, pk=pk)
262 rev = get_object_or_404(Revision, pk=rev_pk)
264 sst = SST.from_string(rev.materialize())
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])),
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)
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')
278 output_file = NamedTemporaryFile(prefix='librarian', suffix='.mobi', delete=False)
280 subprocess.check_call(
281 ['ebook-convert', epub_file.get_filename(), output_file.name, '--no-inline-toc'])
283 from catalogue.ebook_utils import serve_file
284 return serve_file(output_file.name, '%d.mobi' % doc.pk, 'application/epub+zip')
288 # def revision(request, slug, chunk=None):
290 # doc = Chunk.get(slug, chunk)
291 # except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
293 # if not doc.book.accessible(request):
294 # return HttpResponseForbidden("Not authorized.")
295 # return http.HttpResponse(str(doc.revision()))
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)
308 book.set_stage(request.POST.get('stage', ''))
309 return redirect('catalogue_user')
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))
315 schedule = [(i, s, current.get(s, ())) for (i, s) in enumerate(STAGES)]
317 if book.owner_organization:
318 people = [m.user for m in book.owner_organization.membership_set.exclude(status='pending')]
320 people = [book.owner_user]
321 return render(request, 'catalogue/book_schedule.html', {
323 'schedule': schedule,
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):
337 if request.method == 'POST':
339 new_org_pk = request.POST.get('owner_organization')
341 doc.owner_organization = None
342 doc.owner_user = request.user
345 org = Organization.objects.get(pk=new_org_pk)
346 if not org.is_member(request.user):
347 error = 'Bad organization'
349 doc.owner_organization = org
350 doc.owner_user = None
353 return redirect('catalogue_user')
355 return render(request, 'catalogue/book_owner.html', {
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)):
367 if request.method == 'POST':
370 return redirect('catalogue_user')
372 return render(request, 'catalogue/book_delete.html', {
379 def publish(request, pk):
380 from wiki import forms
381 from .models import PublishRecord
382 from dvcs.models import Revision
384 # FIXME: check permissions
386 doc = get_object_or_404(Document, pk=pk, deleted=False)
387 form = forms.DocumentTextPublishForm(request.POST, prefix="textpublish")
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')
397 return redirect('catalogue_html', doc.pk)
399 if request.is_ajax():
400 return http.HttpResponse('error')
403 return redirect('catalogue_preview_rev', doc.pk, form.cleaned_data['revision'])
405 return redirect('catalogue_preview', doc.pk)
410 def unpublish(request, pk):
411 # FIXME: check permissions
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')
418 return redirect('catalogue_html', doc.pk)
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
427 class GalleryView(GalleryMixin, UploadView):
429 def breadcrumbs(self):
431 (self.doc.meta()['title'], '/documents/%d/' % self.doc.pk),
434 def get_object(self, request, pk=None):
435 self.doc = Document.objects.get(pk=pk, deleted=False)
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)
445 org = request.user.membership_set.get(
446 organization=int(form.cleaned_data['owner_organization'])).organization
447 kwargs = {'owner_organization': org}
449 kwargs = {'owner_user': request.user}
451 new_doc = Document.objects.create(revision=doc.revision, **kwargs)
453 if os.path.isdir(settings.MEDIA_ROOT + "uploads/%d" % doc.pk):
455 settings.MEDIA_ROOT + "uploads/%d" % doc.pk,
456 settings.MEDIA_ROOT + "uploads/%d" % new_doc.pk
459 new_doc.assigned_to = request.user
462 return http.HttpResponseRedirect(reverse("wiki_editor", args=[new_doc.pk]))
464 form = forms.DocumentForkForm()
466 return render(request, "catalogue/document_fork.html", {
473 def upcoming(request):
474 return render(request, "catalogue/upcoming.html", {
475 'objects_list': Document.objects.filter(deleted=False).filter(publish_log=None),
479 def finished(request):
480 return render(request, "catalogue/finished.html", {
481 'objects_list': Document.objects.filter(deleted=False).exclude(publish_log=None),