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()
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)
217 sst = SST.from_string(rev.materialize())
220 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
221 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
223 if doc.owner_organization is not None and doc.owner_organization.logo:
224 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
226 pdf_file = PdfFormat(sst).build(ctx)
227 except BuildError as e:
228 from django.http import HttpResponse
229 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
231 from catalogue.ebook_utils import serve_file
232 return serve_file(pdf_file.get_filename(), '%d.pdf' % doc.pk, 'application/pdf')
236 def book_epub(request, pk, rev_pk):
237 from librarian.utils import Context
238 from librarian.document import Document as SST
239 from librarian.formats.epub import EpubFormat
241 doc = get_object_or_404(Document, pk=pk)
242 rev = get_object_or_404(Revision, pk=rev_pk)
245 sst = SST.from_string(rev.materialize())
248 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
249 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
251 if doc.owner_organization is not None and doc.owner_organization.logo:
252 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
254 epub_file = EpubFormat(sst).build(ctx)
255 except BuildError as e:
256 from django.http import HttpResponse
257 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
259 from catalogue.ebook_utils import serve_file
260 return serve_file(epub_file.get_filename(), '%d.epub' % doc.pk, 'application/epub+zip')
264 def book_mobi(request, pk, rev_pk):
265 from librarian.utils import Context
266 from librarian.document import Document as SST
267 from librarian.formats.epub import EpubFormat
269 doc = get_object_or_404(Document, pk=pk)
270 rev = get_object_or_404(Revision, pk=rev_pk)
272 sst = SST.from_string(rev.materialize())
275 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
276 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
278 if doc.owner_organization is not None and doc.owner_organization.logo:
279 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
281 epub_file = EpubFormat(sst).build(ctx)
282 except BuildError as e:
283 from django.http import HttpResponse
284 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
286 output_file = NamedTemporaryFile(prefix='librarian', suffix='.mobi', delete=False)
288 subprocess.check_call(
289 ['ebook-convert', epub_file.get_filename(), output_file.name, '--no-inline-toc'])
291 from catalogue.ebook_utils import serve_file
292 return serve_file(output_file.name, '%d.mobi' % doc.pk, 'application/epub+zip')
296 # def revision(request, slug, chunk=None):
298 # doc = Chunk.get(slug, chunk)
299 # except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
301 # if not doc.book.accessible(request):
302 # return HttpResponseForbidden("Not authorized.")
303 # return http.HttpResponse(str(doc.revision()))
307 def book_schedule(request, pk):
308 book = get_object_or_404(Document, pk=pk, deleted=False)
309 if request.method == 'POST':
310 Plan.objects.filter(document=book).delete()
311 for i, s in enumerate(STAGES):
312 user_id = request.POST.get('s%d-user' % i)
313 deadline = request.POST.get('s%d-deadline' % i) or None
314 Plan.objects.create(document=book, stage=s, user_id=user_id, deadline=deadline)
316 book.set_stage(request.POST.get('stage', ''))
317 return redirect('catalogue_user')
320 for p in Plan.objects.filter(document=book):
321 current[p.stage] = (getattr(p.user, 'pk', None), (p.deadline.isoformat() if p.deadline else None))
323 schedule = [(i, s, current.get(s, ())) for (i, s) in enumerate(STAGES)]
325 if book.owner_organization:
326 people = [m.user for m in book.owner_organization.membership_set.exclude(status='pending')]
328 people = [book.owner_user]
329 return render(request, 'catalogue/book_schedule.html', {
331 'schedule': schedule,
337 def book_owner(request, pk):
338 doc = get_object_or_404(Document, pk=pk, deleted=False)
339 user_is_owner = doc.owner_organization and doc.owner_organization.is_member(request.user)
340 if not (doc.owner_user == request.user or user_is_owner):
345 if request.method == 'POST':
347 new_org_pk = request.POST.get('owner_organization')
349 doc.owner_organization = None
350 doc.owner_user = request.user
353 org = Organization.objects.get(pk=new_org_pk)
354 if not org.is_member(request.user):
355 error = 'Bad organization'
357 doc.owner_organization = org
358 doc.owner_user = None
361 return redirect('catalogue_user')
363 return render(request, 'catalogue/book_owner.html', {
370 def book_delete(request, pk):
371 doc = get_object_or_404(Document, pk=pk, deleted=False)
372 if not (doc.owner_user == request.user or doc.owner_organization.is_member(request.user)):
375 if request.method == 'POST':
378 return redirect('catalogue_user')
380 return render(request, 'catalogue/book_delete.html', {
387 def publish(request, pk):
388 from wiki import forms
389 from .models import PublishRecord
390 from dvcs.models import Revision
392 # FIXME: check permissions
394 doc = get_object_or_404(Document, pk=pk, deleted=False)
395 form = forms.DocumentTextPublishForm(request.POST, prefix="textpublish")
397 rev = Revision.objects.get(pk=form.cleaned_data['revision'])
398 # FIXME: check if in tree
399 # if PublishRecord.objects.filter(revision=rev, document=doc).exists():
400 # return http.HttpResponse('exists')
401 PublishRecord.objects.create(revision=rev, document=doc, user=request.user)
402 if request.is_ajax():
403 return http.HttpResponse('ok')
405 return redirect('catalogue_html', doc.pk)
407 if request.is_ajax():
408 return http.HttpResponse('error')
411 return redirect('catalogue_preview_rev', doc.pk, form.cleaned_data['revision'])
413 return redirect('catalogue_preview', doc.pk)
418 def unpublish(request, pk):
419 # FIXME: check permissions
421 doc = get_object_or_404(Document, pk=pk, deleted=False)
422 doc.publish_log.all().delete()
423 if request.is_ajax():
424 return http.HttpResponse('ok')
426 return redirect('catalogue_html', doc.pk)
429 class GalleryMixin(object):
430 def get_directory(self):
431 # return "%s%s/" % (settings.IMAGE_DIR, 'org%d' % self.org.pk if self.org is not None else self.request.user.pk)
432 return "uploads/%d/" % self.doc.pk
435 class GalleryView(GalleryMixin, UploadView):
437 def breadcrumbs(self):
439 (self.doc.meta()['title'], '/documents/%d/' % self.doc.pk),
442 def get_object(self, request, pk=None):
443 self.doc = Document.objects.get(pk=pk, deleted=False)
447 def fork(request, pk):
448 doc = get_object_or_404(Document, pk=pk, deleted=False)
449 if request.method == "POST":
450 form = forms.DocumentForkForm(request.POST, request.FILES)
453 org = request.user.membership_set.get(
454 organization=int(form.cleaned_data['owner_organization'])).organization
455 kwargs = {'owner_organization': org}
457 kwargs = {'owner_user': request.user}
459 new_doc = Document.objects.create(revision=doc.revision, **kwargs)
461 if os.path.isdir(settings.MEDIA_ROOT + "uploads/%d" % doc.pk):
463 settings.MEDIA_ROOT + "uploads/%d" % doc.pk,
464 settings.MEDIA_ROOT + "uploads/%d" % new_doc.pk
467 new_doc.assigned_to = request.user
470 return http.HttpResponseRedirect(reverse("wiki_editor", args=[new_doc.pk]))
472 form = forms.DocumentForkForm()
474 return render(request, "catalogue/document_fork.html", {
481 def upcoming(request):
482 return render(request, "catalogue/upcoming.html", {
483 'objects_list': Document.objects.filter(deleted=False).filter(publish_log=None),
487 def finished(request):
488 return render(request, "catalogue/finished.html", {
489 'objects_list': Document.objects.filter(deleted=False).exclude(publish_log=None),