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
11 from xml.sax.saxutils import escape as escape_xml
13 from django.conf import settings
14 from django.contrib import auth
15 from django.contrib.auth.models import User
16 from django.contrib.auth.decorators import login_required
17 from django.contrib.sites.models import Site
18 from django.core.urlresolvers import reverse
19 from django import http
20 from django.http import Http404, HttpResponse, HttpResponseForbidden
21 from django.shortcuts import get_object_or_404, render, redirect
22 from django.utils.encoding import force_str
23 from django.utils.http import urlquote_plus
24 from django.views.decorators.http import require_POST
25 from unidecode import unidecode
27 from catalogue import forms
28 from catalogue.filters import DocumentFilterSet
29 from catalogue.forms import TagMultipleForm, TagSingleForm
30 from catalogue.helpers import active_tab
31 from catalogue.models import Category
32 from librarian import BuildError
33 from librarian.utils import Context
34 from librarian.document import Document as SST
35 from librarian.formats.html import HtmlFormat
36 from librarian.formats.pdf import PdfFormat
37 from librarian.formats.epub import EpubFormat
38 from redakcja.utlis import send_notify_email
39 from .constants import STAGES
40 from .models import Document, Plan
41 from dvcs.models import Revision
42 from organizations.models import Organization
43 from fileupload.views import UploadView
46 # Quick hack around caching problems, TODO: use ETags
48 from django.views.decorators.cache import never_cache
49 # from fnpdjango.utils.text.slughifi import slughifi
51 logger = logging.getLogger("fnp.catalogue")
55 def user(request, username):
56 user = get_object_or_404(User, username=username)
57 return render(request, 'catalogue/user_page.html', {"viewed_user": user})
64 return render(request, 'catalogue/my_page.html', {
66 request.session.get("wiki_last_books", {}).items(), key=lambda x: x[1]['time'], reverse=True),
73 def logout_then_redirect(request):
75 return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
80 def create_missing(request):
81 if request.method == "POST":
82 form = forms.DocumentCreateForm(request.POST, request.FILES)
84 (TagMultipleForm if category.multiple else TagSingleForm)(
85 category=category, data=request.POST, prefix=category.dc_tag)
86 for category in Category.objects.all()]
87 if form.is_valid() and all(tag_form.is_valid() for tag_form in tag_forms):
89 if request.user.is_authenticated():
90 creator = request.user
94 title = form.cleaned_data['title']
96 org = request.user.membership_set.get(
97 organization=int(form.cleaned_data['owner_organization'])).organization
98 kwargs = {'owner_organization': org}
100 kwargs = {'owner_user': request.user}
102 doc = Document.objects.create(**kwargs)
104 for tag_form in tag_forms:
105 tag_form.save(instance=doc)
107 cover = request.FILES.get('cover')
109 uppath = 'uploads/%d/' % doc.pk
110 path = settings.MEDIA_ROOT + uppath
111 if not os.path.isdir(path):
113 cover.name = unidecode(cover.name)
114 dest_path = path + cover.name
115 if not os.path.abspath(dest_path).startswith(os.path.abspath(path)):
117 with open(dest_path, 'w') as destination:
118 for chunk in cover.chunks():
119 destination.write(chunk)
120 cover_url = 'http://milpeer.eu/media/dynamic/' + uppath + cover.name
124 text = '''<section xmlns="http://nowoczesnapolska.org.pl/sst#" xmlns:dc="http://purl.org/dc/elements/1.1/">
126 <dc:publisher>%s</dc:publisher>
127 <dc:description>%s</dc:description>
129 <dc:relation.coverImage.url>%s</dc:relation.coverImage.url>
132 <div class="p"> </div>
134 escape_xml(form.cleaned_data['publisher']),
135 escape_xml(form.cleaned_data['description']),
136 '\n'.join(tag_form.metadata_rows() for tag_form in tag_forms),
137 escape_xml(cover_url),
144 doc.assigned_to = request.user
147 return http.HttpResponseRedirect(reverse("wiki_editor", args=[doc.pk]))
149 org_pk = request.GET.get('organization')
152 org = Organization.objects.get(pk=org_pk)
153 except Organization.DoesNotExist:
156 if not org.is_member(request.user):
163 form = forms.DocumentCreateForm(initial={'owner_organization': org})
166 (TagMultipleForm if category.multiple else TagSingleForm)(
167 category=category, tutorial_no=i, prefix=category.dc_tag)
168 for i, category in enumerate(Category.objects.all(), start=2)]
170 return render(request, "catalogue/document_create_missing.html", {
172 "tag_forms": tag_forms,
179 def book_html(request, pk, rev_pk=None, preview=False):
180 doc = get_object_or_404(Document, pk=pk, deleted=False)
183 published_revision = doc.publish_log.all()[0].revision
185 published_revision = None
189 revision = doc.revision
191 if published_revision is not None:
192 revision = published_revision
194 # No published version, fallback to preview mode.
196 revision = doc.revision
198 revision = get_object_or_404(Revision, pk=rev_pk)
200 was_published = revision == published_revision or doc.publish_log.filter(revision=revision).exists()
203 sst = SST.from_string(revision.materialize())
204 except ValueError as e:
207 html = HtmlFormat(sst).build(
208 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk)).get_string()
210 # response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
213 # for fragment in book.fragments.all().iterator():
214 # for theme in fragment.tags.filter(category='theme').iterator():
215 # book_themes.setdefault(theme, []).append(fragment)
217 # book_themes = book_themes.items()
218 # book_themes.sort(key=lambda s: s[0].sort_key)
219 return render(request, 'catalogue/book_text.html', {
222 'revision': revision,
223 'published_revision': published_revision,
224 'specific': rev_pk is not None,
226 'can_edit': doc.can_edit(request.user) if doc else None,
227 'was_published': was_published,
232 def book_pdf(request, pk, rev_pk):
233 doc = get_object_or_404(Document, pk=pk)
234 rev = get_object_or_404(Revision, pk=rev_pk)
238 sst = SST.from_string(rev.materialize())
239 except ValueError as e:
240 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
243 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
244 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
246 if doc.owner_organization is not None and doc.owner_organization.logo:
247 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
249 pdf_file = PdfFormat(sst).build(ctx)
250 except BuildError as e:
251 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
253 from catalogue.ebook_utils import serve_file
254 return serve_file(pdf_file.get_filename(), '%d.pdf' % doc.pk, 'application/pdf')
258 def book_epub(request, pk, rev_pk):
259 doc = get_object_or_404(Document, pk=pk)
260 rev = get_object_or_404(Revision, pk=rev_pk)
264 sst = SST.from_string(rev.materialize())
265 except ValueError as e:
266 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
269 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
270 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
272 if doc.owner_organization is not None and doc.owner_organization.logo:
273 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
275 epub_file = EpubFormat(sst).build(ctx)
276 except BuildError as e:
277 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
279 from catalogue.ebook_utils import serve_file
280 return serve_file(epub_file.get_filename(), '%d.epub' % doc.pk, 'application/epub+zip')
284 def book_mobi(request, pk, rev_pk):
285 doc = get_object_or_404(Document, pk=pk)
286 rev = get_object_or_404(Revision, pk=rev_pk)
289 sst = SST.from_string(rev.materialize())
290 except ValueError as e:
291 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
294 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
295 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
297 if doc.owner_organization is not None and doc.owner_organization.logo:
298 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
300 epub_file = EpubFormat(sst).build(ctx)
301 except BuildError as e:
302 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
304 output_file = NamedTemporaryFile(prefix='librarian', suffix='.mobi', delete=False)
306 subprocess.check_call(
307 ['ebook-convert', epub_file.get_filename(), output_file.name, '--no-inline-toc'])
309 from catalogue.ebook_utils import serve_file
310 return serve_file(output_file.name, '%d.mobi' % doc.pk, 'application/epub+zip')
314 # def revision(request, slug, chunk=None):
316 # doc = Chunk.get(slug, chunk)
317 # except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
319 # if not doc.book.accessible(request):
320 # return HttpResponseForbidden("Not authorized.")
321 # return http.HttpResponse(str(doc.revision()))
325 def book_schedule(request, pk):
326 book = get_object_or_404(Document, pk=pk, deleted=False)
327 if not book.can_edit(request.user):
328 return HttpResponseForbidden("Not authorized.")
329 if request.method == 'POST':
330 Plan.objects.filter(document=book).delete()
331 for i, (s, name) in enumerate(STAGES):
332 user_id = request.POST.get('s%d-user' % i)
333 deadline = request.POST.get('s%d-deadline' % i) or None
334 Plan.objects.create(document=book, stage=s, user_id=user_id, deadline=deadline)
336 book.set_stage(request.POST.get('stage', ''))
337 return redirect('catalogue_user')
340 for p in Plan.objects.filter(document=book):
341 current[p.stage] = (getattr(p.user, 'pk', None), (p.deadline.isoformat() if p.deadline else None))
343 schedule = [(i, s, current.get(s, ())) for i, (s, name) in enumerate(STAGES)]
345 if book.owner_organization:
346 people = [m.user for m in book.owner_organization.membership_set.exclude(status='pending')]
348 people = [book.owner_user]
349 return render(request, 'catalogue/book_schedule.html', {
351 'schedule': schedule,
357 def book_owner(request, pk):
358 doc = get_object_or_404(Document, pk=pk, deleted=False)
359 if not doc.can_edit(request.user):
360 return HttpResponseForbidden("Not authorized.")
364 if request.method == 'POST':
366 new_org_pk = request.POST.get('owner_organization')
368 doc.owner_organization = None
369 doc.owner_user = request.user
372 org = Organization.objects.get(pk=new_org_pk)
373 if not org.is_member(request.user):
374 error = 'Bad organization'
376 doc.owner_organization = org
377 doc.owner_user = None
380 return redirect('catalogue_user')
382 return render(request, 'catalogue/book_owner.html', {
389 def book_delete(request, pk):
390 doc = get_object_or_404(Document, pk=pk, deleted=False)
391 if not doc.can_edit(request.user):
392 return HttpResponseForbidden("Not authorized.")
394 if request.method == 'POST':
397 return redirect('catalogue_user')
399 return render(request, 'catalogue/book_delete.html', {
406 def publish(request, pk):
407 from wiki import forms
408 from .models import PublishRecord
409 from dvcs.models import Revision
411 doc = get_object_or_404(Document, pk=pk, deleted=False)
412 if not doc.can_edit(request.user):
413 return HttpResponseForbidden("Not authorized.")
414 form = forms.DocumentTextPublishForm(request.POST, prefix="textpublish")
416 rev = Revision.objects.get(pk=form.cleaned_data['revision'])
417 # FIXME: check if in tree
418 # if PublishRecord.objects.filter(revision=rev, document=doc).exists():
419 # return http.HttpResponse('exists')
420 if not doc.published:
421 site = Site.objects.get_current()
423 'New published document in MIL/PEER',
424 '''New published document in MIL/PEER: %s. View it in browser: https://%s%s.
427 MIL/PEER team.''' % (doc.meta()['title'], site.domain, reverse('catalogue_html', args=[doc.pk])))
428 PublishRecord.objects.create(revision=rev, document=doc, user=request.user)
431 if request.is_ajax():
432 return http.HttpResponse('ok')
434 return redirect('catalogue_html', doc.pk)
436 if request.is_ajax():
437 return http.HttpResponse('error')
440 return redirect('catalogue_preview_rev', doc.pk, form.cleaned_data['revision'])
442 return redirect('catalogue_preview', doc.pk)
447 def unpublish(request, pk):
448 doc = get_object_or_404(Document, pk=pk, deleted=False)
449 if not doc.can_edit(request.user):
450 return HttpResponseForbidden("Not authorized.")
452 doc.publish_log.all().delete()
453 if request.is_ajax():
454 return http.HttpResponse('ok')
456 return redirect('catalogue_html', doc.pk)
459 class GalleryMixin(object):
460 def get_directory(self):
461 # return "%s%s/" % (settings.IMAGE_DIR, 'org%d' % self.org.pk if self.org is not None else self.request.user.pk)
462 return "uploads/%d/" % self.doc.pk
465 class GalleryView(GalleryMixin, UploadView):
467 def breadcrumbs(self):
469 (self.doc.meta()['title'], '/documents/%d/' % self.doc.pk),
472 def get_object(self, request, pk=None):
473 self.doc = Document.objects.get(pk=pk, deleted=False)
477 def fork(request, pk):
478 doc = get_object_or_404(Document, pk=pk, deleted=False)
479 if request.method == "POST":
480 form = forms.DocumentForkForm(request.POST, request.FILES)
483 org = request.user.membership_set.get(
484 organization=int(form.cleaned_data['owner_organization'])).organization
485 kwargs = {'owner_organization': org}
487 kwargs = {'owner_user': request.user}
489 new_doc = Document.objects.create(revision=doc.revision, **kwargs)
491 if os.path.isdir(settings.MEDIA_ROOT + "uploads/%d" % doc.pk):
493 settings.MEDIA_ROOT + "uploads/%d" % doc.pk,
494 settings.MEDIA_ROOT + "uploads/%d" % new_doc.pk
497 new_doc.assigned_to = request.user
500 return http.HttpResponseRedirect(reverse("wiki_editor", args=[new_doc.pk]))
502 form = forms.DocumentForkForm()
504 return render(request, "catalogue/document_fork.html", {
511 def upcoming(request):
512 f = DocumentFilterSet(request.GET, queryset=Document.objects.filter(deleted=False).filter(publish_log=None))
513 return render(request, "catalogue/upcoming.html", {
515 'link_url': 'catalogue_preview',
519 def finished(request):
520 f = DocumentFilterSet(request.GET, queryset=Document.objects.filter(deleted=False).exclude(publish_log=None))
521 return render(request, "catalogue/finished.html", {
523 'link_url': 'catalogue_html',