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.contrib.sites.models import Site
17 from django.core.urlresolvers import reverse
18 from django import http
19 from django.http import Http404, HttpResponse, HttpResponseForbidden
20 from django.shortcuts import get_object_or_404, render, redirect
21 from django.utils.encoding import force_str
22 from django.utils.http import urlquote_plus
23 from django.views.decorators.http import require_POST
24 from unidecode import unidecode
26 from catalogue import forms
27 from catalogue.forms import TagMultipleForm, TagSingleForm
28 from catalogue.helpers import active_tab
29 from catalogue.models import Category
30 from librarian import BuildError
31 from redakcja.utlis import send_notify_email
32 from .constants import STAGES
33 from .models import Document, Plan
34 from dvcs.models import Revision
35 from organizations.models import Organization
36 from fileupload.views import UploadView
39 # Quick hack around caching problems, TODO: use ETags
41 from django.views.decorators.cache import never_cache
42 # from fnpdjango.utils.text.slughifi import slughifi
44 logger = logging.getLogger("fnp.catalogue")
48 def user(request, username):
49 user = get_object_or_404(User, username=username)
50 return render(request, 'catalogue/user_page.html', {"viewed_user": user})
57 return render(request, 'catalogue/my_page.html', {
59 request.session.get("wiki_last_books", {}).items(), key=lambda x: x[1]['time'], reverse=True),
66 def logout_then_redirect(request):
68 return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
73 def create_missing(request):
74 if request.method == "POST":
75 form = forms.DocumentCreateForm(request.POST, request.FILES)
77 (TagMultipleForm if category.multiple else TagSingleForm)(
78 category=category, data=request.POST, prefix=category.dc_tag)
79 for category in Category.objects.all()]
80 if form.is_valid() and all(tag_form.is_valid() for tag_form in tag_forms):
82 if request.user.is_authenticated():
83 creator = request.user
87 title = form.cleaned_data['title']
89 org = request.user.membership_set.get(
90 organization=int(form.cleaned_data['owner_organization'])).organization
91 kwargs = {'owner_organization': org}
93 kwargs = {'owner_user': request.user}
95 doc = Document.objects.create(**kwargs)
97 cover = request.FILES.get('cover')
99 uppath = 'uploads/%d/' % doc.pk
100 path = settings.MEDIA_ROOT + uppath
101 if not os.path.isdir(path):
103 cover.name = unidecode(cover.name)
104 dest_path = path + cover.name
105 if not os.path.abspath(dest_path).startswith(os.path.abspath(path)):
107 with open(dest_path, 'w') as destination:
108 for chunk in cover.chunks():
109 destination.write(chunk)
110 cover_url = 'http://milpeer.eu/media/dynamic/' + uppath + cover.name
115 text='''<section xmlns="http://nowoczesnapolska.org.pl/sst#" xmlns:dc="http://purl.org/dc/elements/1.1/">
117 <dc:publisher>''' + form.cleaned_data['publisher'] + '''</dc:publisher>
118 <dc:description>''' + form.cleaned_data['description'] + '''</dc:description>
119 <dc:language>''' + form.cleaned_data['language'] + '''</dc:language>
120 <dc:rights>''' + form.cleaned_data['rights'] + '''</dc:rights>
121 <dc:audience>''' + form.cleaned_data['audience'] + '''</dc:audience>
122 <dc:relation.coverImage.url>''' + cover_url + '''</dc:relation.coverImage.url>
124 <header>''' + title + '''</header>
125 <div class="p"> </div>
129 doc.assigned_to = request.user
132 return http.HttpResponseRedirect(reverse("wiki_editor", args=[doc.pk]))
134 org_pk = request.GET.get('organization')
137 org = Organization.objects.get(pk=org_pk)
138 except Organization.DoesNotExist:
141 if not org.is_member(request.user):
148 form = forms.DocumentCreateForm(initial={'owner_organization': org})
151 (TagMultipleForm if category.multiple else TagSingleForm)(category=category, prefix=category.dc_tag)
152 for category in Category.objects.all()]
154 return render(request, "catalogue/document_create_missing.html", {
156 "tag_forms": tag_forms,
163 def book_html(request, pk, rev_pk=None, preview=False):
164 from librarian.document import Document as SST
165 from librarian.formats.html import HtmlFormat
167 doc = get_object_or_404(Document, pk=pk, deleted=False)
170 published_revision = doc.publish_log.all()[0].revision
172 published_revision = None
176 revision = doc.revision
178 if published_revision is not None:
179 revision = published_revision
181 # No published version, fallback to preview mode.
183 revision = doc.revision
185 revision = get_object_or_404(Revision, pk=rev_pk)
187 was_published = revision == published_revision or doc.publish_log.filter(revision=revision).exists()
190 sst = SST.from_string(revision.materialize())
191 except ValueError as e:
194 html = HtmlFormat(sst).build(
195 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk)).get_string()
197 # response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
200 # for fragment in book.fragments.all().iterator():
201 # for theme in fragment.tags.filter(category='theme').iterator():
202 # book_themes.setdefault(theme, []).append(fragment)
204 # book_themes = book_themes.items()
205 # book_themes.sort(key=lambda s: s[0].sort_key)
206 return render(request, 'catalogue/book_text.html', {
209 'revision': revision,
210 'published_revision': published_revision,
211 'specific': rev_pk is not None,
213 'can_edit': doc.can_edit(request.user) if doc else None,
214 'was_published': was_published,
219 def book_pdf(request, pk, rev_pk):
220 from librarian.utils import Context
221 from librarian.document import Document as SST
222 from librarian.formats.pdf import PdfFormat
224 doc = get_object_or_404(Document, pk=pk)
225 rev = get_object_or_404(Revision, pk=rev_pk)
229 sst = SST.from_string(rev.materialize())
230 except ValueError as e:
231 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
234 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
235 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
237 if doc.owner_organization is not None and doc.owner_organization.logo:
238 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
240 pdf_file = PdfFormat(sst).build(ctx)
241 except BuildError as e:
242 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
244 from catalogue.ebook_utils import serve_file
245 return serve_file(pdf_file.get_filename(), '%d.pdf' % doc.pk, 'application/pdf')
249 def book_epub(request, pk, rev_pk):
250 from librarian.utils import Context
251 from librarian.document import Document as SST
252 from librarian.formats.epub import EpubFormat
254 doc = get_object_or_404(Document, pk=pk)
255 rev = get_object_or_404(Revision, pk=rev_pk)
259 sst = SST.from_string(rev.materialize())
260 except ValueError as e:
261 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
264 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
265 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
267 if doc.owner_organization is not None and doc.owner_organization.logo:
268 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
270 epub_file = EpubFormat(sst).build(ctx)
271 except BuildError as e:
272 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
274 from catalogue.ebook_utils import serve_file
275 return serve_file(epub_file.get_filename(), '%d.epub' % doc.pk, 'application/epub+zip')
279 def book_mobi(request, pk, rev_pk):
280 from librarian.utils import Context
281 from librarian.document import Document as SST
282 from librarian.formats.epub import EpubFormat
284 doc = get_object_or_404(Document, pk=pk)
285 rev = get_object_or_404(Revision, pk=rev_pk)
288 sst = SST.from_string(rev.materialize())
289 except ValueError as e:
290 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
293 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
294 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
296 if doc.owner_organization is not None and doc.owner_organization.logo:
297 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
299 epub_file = EpubFormat(sst).build(ctx)
300 except BuildError as e:
301 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
303 output_file = NamedTemporaryFile(prefix='librarian', suffix='.mobi', delete=False)
305 subprocess.check_call(
306 ['ebook-convert', epub_file.get_filename(), output_file.name, '--no-inline-toc'])
308 from catalogue.ebook_utils import serve_file
309 return serve_file(output_file.name, '%d.mobi' % doc.pk, 'application/epub+zip')
313 # def revision(request, slug, chunk=None):
315 # doc = Chunk.get(slug, chunk)
316 # except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
318 # if not doc.book.accessible(request):
319 # return HttpResponseForbidden("Not authorized.")
320 # return http.HttpResponse(str(doc.revision()))
324 def book_schedule(request, pk):
325 book = get_object_or_404(Document, pk=pk, deleted=False)
326 if not book.can_edit(request.user):
327 return HttpResponseForbidden("Not authorized.")
328 if request.method == 'POST':
329 Plan.objects.filter(document=book).delete()
330 for i, (s, name) in enumerate(STAGES):
331 user_id = request.POST.get('s%d-user' % i)
332 deadline = request.POST.get('s%d-deadline' % i) or None
333 Plan.objects.create(document=book, stage=s, user_id=user_id, deadline=deadline)
335 book.set_stage(request.POST.get('stage', ''))
336 return redirect('catalogue_user')
339 for p in Plan.objects.filter(document=book):
340 current[p.stage] = (getattr(p.user, 'pk', None), (p.deadline.isoformat() if p.deadline else None))
342 schedule = [(i, s, current.get(s, ())) for i, (s, name) in enumerate(STAGES)]
344 if book.owner_organization:
345 people = [m.user for m in book.owner_organization.membership_set.exclude(status='pending')]
347 people = [book.owner_user]
348 return render(request, 'catalogue/book_schedule.html', {
350 'schedule': schedule,
356 def book_owner(request, pk):
357 doc = get_object_or_404(Document, pk=pk, deleted=False)
358 if not doc.can_edit(request.user):
359 return HttpResponseForbidden("Not authorized.")
363 if request.method == 'POST':
365 new_org_pk = request.POST.get('owner_organization')
367 doc.owner_organization = None
368 doc.owner_user = request.user
371 org = Organization.objects.get(pk=new_org_pk)
372 if not org.is_member(request.user):
373 error = 'Bad organization'
375 doc.owner_organization = org
376 doc.owner_user = None
379 return redirect('catalogue_user')
381 return render(request, 'catalogue/book_owner.html', {
388 def book_delete(request, pk):
389 doc = get_object_or_404(Document, pk=pk, deleted=False)
390 if not doc.can_edit(request.user):
391 return HttpResponseForbidden("Not authorized.")
393 if request.method == 'POST':
396 return redirect('catalogue_user')
398 return render(request, 'catalogue/book_delete.html', {
405 def publish(request, pk):
406 from wiki import forms
407 from .models import PublishRecord
408 from dvcs.models import Revision
410 doc = get_object_or_404(Document, pk=pk, deleted=False)
411 if not doc.can_edit(request.user):
412 return HttpResponseForbidden("Not authorized.")
413 form = forms.DocumentTextPublishForm(request.POST, prefix="textpublish")
415 rev = Revision.objects.get(pk=form.cleaned_data['revision'])
416 # FIXME: check if in tree
417 # if PublishRecord.objects.filter(revision=rev, document=doc).exists():
418 # return http.HttpResponse('exists')
419 if not doc.published:
420 site = Site.objects.get_current()
422 'New published document in MIL/PEER',
423 '''New published document in MIL/PEER: %s. View it in browser: https://%s%s.
426 MIL/PEER team.''' % (doc.meta()['title'], site.domain, reverse('catalogue_html', args=[doc.pk])))
427 PublishRecord.objects.create(revision=rev, document=doc, user=request.user)
430 if request.is_ajax():
431 return http.HttpResponse('ok')
433 return redirect('catalogue_html', doc.pk)
435 if request.is_ajax():
436 return http.HttpResponse('error')
439 return redirect('catalogue_preview_rev', doc.pk, form.cleaned_data['revision'])
441 return redirect('catalogue_preview', doc.pk)
446 def unpublish(request, pk):
447 doc = get_object_or_404(Document, pk=pk, deleted=False)
448 if not doc.can_edit(request.user):
449 return HttpResponseForbidden("Not authorized.")
451 doc.publish_log.all().delete()
452 if request.is_ajax():
453 return http.HttpResponse('ok')
455 return redirect('catalogue_html', doc.pk)
458 class GalleryMixin(object):
459 def get_directory(self):
460 # return "%s%s/" % (settings.IMAGE_DIR, 'org%d' % self.org.pk if self.org is not None else self.request.user.pk)
461 return "uploads/%d/" % self.doc.pk
464 class GalleryView(GalleryMixin, UploadView):
466 def breadcrumbs(self):
468 (self.doc.meta()['title'], '/documents/%d/' % self.doc.pk),
471 def get_object(self, request, pk=None):
472 self.doc = Document.objects.get(pk=pk, deleted=False)
476 def fork(request, pk):
477 doc = get_object_or_404(Document, pk=pk, deleted=False)
478 if request.method == "POST":
479 form = forms.DocumentForkForm(request.POST, request.FILES)
482 org = request.user.membership_set.get(
483 organization=int(form.cleaned_data['owner_organization'])).organization
484 kwargs = {'owner_organization': org}
486 kwargs = {'owner_user': request.user}
488 new_doc = Document.objects.create(revision=doc.revision, **kwargs)
490 if os.path.isdir(settings.MEDIA_ROOT + "uploads/%d" % doc.pk):
492 settings.MEDIA_ROOT + "uploads/%d" % doc.pk,
493 settings.MEDIA_ROOT + "uploads/%d" % new_doc.pk
496 new_doc.assigned_to = request.user
499 return http.HttpResponseRedirect(reverse("wiki_editor", args=[new_doc.pk]))
501 form = forms.DocumentForkForm()
503 return render(request, "catalogue/document_fork.html", {
510 def upcoming(request):
511 return render(request, "catalogue/upcoming.html", {
512 'objects_list': Document.objects.filter(deleted=False).filter(publish_log=None),
516 def finished(request):
517 return render(request, "catalogue/finished.html", {
518 'objects_list': Document.objects.filter(deleted=False).exclude(publish_log=None),