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])),
245 organization=doc.owner_organization.name,
247 if doc.owner_organization is not None and doc.owner_organization.logo:
248 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
250 pdf_file = PdfFormat(sst).build(ctx)
251 except BuildError as e:
252 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
254 from catalogue.ebook_utils import serve_file
255 return serve_file(pdf_file.get_filename(), '%d.pdf' % doc.pk, 'application/pdf')
259 def book_epub(request, pk, rev_pk):
260 doc = get_object_or_404(Document, pk=pk)
261 rev = get_object_or_404(Revision, pk=rev_pk)
265 sst = SST.from_string(rev.materialize())
266 except ValueError as e:
267 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
270 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
271 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
273 if doc.owner_organization is not None and doc.owner_organization.logo:
274 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
276 epub_file = EpubFormat(sst).build(ctx)
277 except BuildError as e:
278 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
280 from catalogue.ebook_utils import serve_file
281 return serve_file(epub_file.get_filename(), '%d.epub' % doc.pk, 'application/epub+zip')
285 def book_mobi(request, pk, rev_pk):
286 doc = get_object_or_404(Document, pk=pk)
287 rev = get_object_or_404(Revision, pk=rev_pk)
290 sst = SST.from_string(rev.materialize())
291 except ValueError as e:
292 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
295 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
296 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
298 if doc.owner_organization is not None and doc.owner_organization.logo:
299 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
301 epub_file = EpubFormat(sst).build(ctx)
302 except BuildError as e:
303 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
305 output_file = NamedTemporaryFile(prefix='librarian', suffix='.mobi', delete=False)
307 subprocess.check_call(
308 ['ebook-convert', epub_file.get_filename(), output_file.name, '--no-inline-toc'])
310 from catalogue.ebook_utils import serve_file
311 return serve_file(output_file.name, '%d.mobi' % doc.pk, 'application/epub+zip')
315 # def revision(request, slug, chunk=None):
317 # doc = Chunk.get(slug, chunk)
318 # except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
320 # if not doc.book.accessible(request):
321 # return HttpResponseForbidden("Not authorized.")
322 # return http.HttpResponse(str(doc.revision()))
326 def book_schedule(request, pk):
327 book = get_object_or_404(Document, pk=pk, deleted=False)
328 if not book.can_edit(request.user):
329 return HttpResponseForbidden("Not authorized.")
330 if request.method == 'POST':
331 Plan.objects.filter(document=book).delete()
332 for i, (s, name) in enumerate(STAGES):
333 user_id = request.POST.get('s%d-user' % i)
334 deadline = request.POST.get('s%d-deadline' % i) or None
335 Plan.objects.create(document=book, stage=s, user_id=user_id, deadline=deadline)
337 book.set_stage(request.POST.get('stage', ''))
338 return redirect('catalogue_user')
341 for p in Plan.objects.filter(document=book):
342 current[p.stage] = (getattr(p.user, 'pk', None), (p.deadline.isoformat() if p.deadline else None))
344 schedule = [(i, s, current.get(s, ())) for i, (s, name) in enumerate(STAGES)]
346 if book.owner_organization:
347 people = [m.user for m in book.owner_organization.membership_set.exclude(status='pending')]
349 people = [book.owner_user]
350 return render(request, 'catalogue/book_schedule.html', {
352 'schedule': schedule,
358 def book_owner(request, pk):
359 doc = get_object_or_404(Document, pk=pk, deleted=False)
360 if not doc.can_edit(request.user):
361 return HttpResponseForbidden("Not authorized.")
365 if request.method == 'POST':
367 new_org_pk = request.POST.get('owner_organization')
369 doc.owner_organization = None
370 doc.owner_user = request.user
373 org = Organization.objects.get(pk=new_org_pk)
374 if not org.is_member(request.user):
375 error = 'Bad organization'
377 doc.owner_organization = org
378 doc.owner_user = None
381 return redirect('catalogue_user')
383 return render(request, 'catalogue/book_owner.html', {
390 def book_delete(request, pk):
391 doc = get_object_or_404(Document, pk=pk, deleted=False)
392 if not doc.can_edit(request.user):
393 return HttpResponseForbidden("Not authorized.")
395 if request.method == 'POST':
398 return redirect('catalogue_user')
400 return render(request, 'catalogue/book_delete.html', {
407 def publish(request, pk):
408 from wiki import forms
409 from .models import PublishRecord
410 from dvcs.models import Revision
412 doc = get_object_or_404(Document, pk=pk, deleted=False)
413 if not doc.can_edit(request.user):
414 return HttpResponseForbidden("Not authorized.")
415 form = forms.DocumentTextPublishForm(request.POST, prefix="textpublish")
417 rev = Revision.objects.get(pk=form.cleaned_data['revision'])
418 # FIXME: check if in tree
419 # if PublishRecord.objects.filter(revision=rev, document=doc).exists():
420 # return http.HttpResponse('exists')
421 if not doc.published:
422 site = Site.objects.get_current()
424 'New published document in MIL/PEER',
425 '''New published document in MIL/PEER: %s. View it in browser: https://%s%s.
428 MIL/PEER team.''' % (doc.meta()['title'], site.domain, reverse('catalogue_html', args=[doc.pk])))
429 PublishRecord.objects.create(revision=rev, document=doc, user=request.user)
432 if request.is_ajax():
433 return http.HttpResponse('ok')
435 return redirect('catalogue_html', doc.pk)
437 if request.is_ajax():
438 return http.HttpResponse('error')
441 return redirect('catalogue_preview_rev', doc.pk, form.cleaned_data['revision'])
443 return redirect('catalogue_preview', doc.pk)
448 def unpublish(request, pk):
449 doc = get_object_or_404(Document, pk=pk, deleted=False)
450 if not doc.can_edit(request.user):
451 return HttpResponseForbidden("Not authorized.")
453 doc.publish_log.all().delete()
454 if request.is_ajax():
455 return http.HttpResponse('ok')
457 return redirect('catalogue_html', doc.pk)
460 class GalleryMixin(object):
461 def get_directory(self):
462 # return "%s%s/" % (settings.IMAGE_DIR, 'org%d' % self.org.pk if self.org is not None else self.request.user.pk)
463 return "uploads/%d/" % self.doc.pk
466 class GalleryView(GalleryMixin, UploadView):
468 def breadcrumbs(self):
470 (self.doc.meta()['title'], '/documents/%d/' % self.doc.pk),
473 def get_object(self, request, pk=None):
474 self.doc = Document.objects.get(pk=pk, deleted=False)
478 def fork(request, pk):
479 doc = get_object_or_404(Document, pk=pk, deleted=False)
480 if request.method == "POST":
481 form = forms.DocumentForkForm(request.POST, request.FILES)
484 org = request.user.membership_set.get(
485 organization=int(form.cleaned_data['owner_organization'])).organization
486 kwargs = {'owner_organization': org}
488 kwargs = {'owner_user': request.user}
490 new_doc = Document.objects.create(revision=doc.revision, **kwargs)
492 if os.path.isdir(settings.MEDIA_ROOT + "uploads/%d" % doc.pk):
494 settings.MEDIA_ROOT + "uploads/%d" % doc.pk,
495 settings.MEDIA_ROOT + "uploads/%d" % new_doc.pk
498 new_doc.assigned_to = request.user
501 return http.HttpResponseRedirect(reverse("wiki_editor", args=[new_doc.pk]))
503 form = forms.DocumentForkForm()
505 return render(request, "catalogue/document_fork.html", {
512 def upcoming(request):
513 f = DocumentFilterSet(request.GET, queryset=Document.objects.filter(deleted=False).filter(publish_log=None))
514 return render(request, "catalogue/upcoming.html", {
516 'link_url': 'catalogue_preview',
520 def finished(request):
521 f = DocumentFilterSet(request.GET, queryset=Document.objects.filter(deleted=False).exclude(publish_log=None))
522 return render(request, "catalogue/finished.html", {
524 'link_url': 'catalogue_html',