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
25 from catalogue import forms
26 from catalogue.forms import TagMultipleForm, TagSingleForm
27 from catalogue.helpers import active_tab
28 from catalogue.models import Category
29 from librarian import BuildError
30 from redakcja.utlis import send_notify_email
31 from .constants import STAGES
32 from .models import Document, Plan
33 from dvcs.models import Revision
34 from organizations.models import Organization
35 from fileupload.views import UploadView
38 # Quick hack around caching problems, TODO: use ETags
40 from django.views.decorators.cache import never_cache
41 # from fnpdjango.utils.text.slughifi import slughifi
43 logger = logging.getLogger("fnp.catalogue")
47 def user(request, username):
48 user = get_object_or_404(User, username=username)
49 return render(request, 'catalogue/user_page.html', {"viewed_user": user})
56 return render(request, 'catalogue/my_page.html', {
58 request.session.get("wiki_last_books", {}).items(), key=lambda x: x[1]['time'], reverse=True),
65 def logout_then_redirect(request):
67 return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
72 def create_missing(request):
73 if request.method == "POST":
74 form = forms.DocumentCreateForm(request.POST, request.FILES)
76 # (TagMultipleForm if category.multiple else TagSingleForm)(
77 # category=category, data=request.POST, prefix=category.dc_tag)
78 # for category in Category.objects.all()]
79 if form.is_valid(): # and all(tag_form.is_valid() for tag_form in tag_forms):
81 if request.user.is_authenticated():
82 creator = request.user
86 title = form.cleaned_data['title']
88 org = request.user.membership_set.get(
89 organization=int(form.cleaned_data['owner_organization'])).organization
90 kwargs = {'owner_organization': org}
92 kwargs = {'owner_user': request.user}
94 doc = Document.objects.create(**kwargs)
96 cover = request.FILES.get('cover')
98 uppath = 'uploads/%d/' % doc.pk
99 path = settings.MEDIA_ROOT + uppath
100 if not os.path.isdir(path):
102 dest_path = path + cover.name # UNSAFE
103 with open(dest_path, 'w') as destination:
104 for chunk in cover.chunks():
105 destination.write(chunk)
106 cover_url = 'http://milpeer.eu/media/dynamic/' + uppath + cover.name
111 text='''<section xmlns="http://nowoczesnapolska.org.pl/sst#" xmlns:dc="http://purl.org/dc/elements/1.1/">
113 <dc:publisher>''' + form.cleaned_data['publisher'] + '''</dc:publisher>
114 <dc:description>''' + form.cleaned_data['description'] + '''</dc:description>
115 <dc:language>''' + form.cleaned_data['language'] + '''</dc:language>
116 <dc:rights>''' + form.cleaned_data['rights'] + '''</dc:rights>
117 <dc:audience>''' + form.cleaned_data['audience'] + '''</dc:audience>
118 <dc:relation.coverImage.url>''' + cover_url + '''</dc:relation.coverImage.url>
120 <header>''' + title + '''</header>
121 <div class="p"> </div>
125 doc.assigned_to = request.user
128 return http.HttpResponseRedirect(reverse("wiki_editor", args=[doc.pk]))
130 org_pk = request.GET.get('organization')
133 org = Organization.objects.get(pk=org_pk)
134 except Organization.DoesNotExist:
137 if not org.is_member(request.user):
144 form = forms.DocumentCreateForm(initial={'owner_organization': org})
147 # (TagMultipleForm if category.multiple else TagSingleForm)(category=category, prefix=category.dc_tag)
148 # for category in Category.objects.all()]
150 return render(request, "catalogue/document_create_missing.html", {
152 # "tag_forms": tag_forms,
159 def book_html(request, pk, rev_pk=None, preview=False):
160 from librarian.document import Document as SST
161 from librarian.formats.html import HtmlFormat
163 doc = get_object_or_404(Document, pk=pk, deleted=False)
166 published_revision = doc.publish_log.all()[0].revision
168 published_revision = None
172 revision = doc.revision
174 if published_revision is not None:
175 revision = published_revision
177 # No published version, fallback to preview mode.
179 revision = doc.revision
181 revision = get_object_or_404(Revision, pk=rev_pk)
183 was_published = revision == published_revision or doc.publish_log.filter(revision=revision).exists()
186 sst = SST.from_string(revision.materialize())
187 except ValueError as e:
190 html = HtmlFormat(sst).build(
191 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk)).get_string()
193 # response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
196 # for fragment in book.fragments.all().iterator():
197 # for theme in fragment.tags.filter(category='theme').iterator():
198 # book_themes.setdefault(theme, []).append(fragment)
200 # book_themes = book_themes.items()
201 # book_themes.sort(key=lambda s: s[0].sort_key)
202 return render(request, 'catalogue/book_text.html', {
205 'revision': revision,
206 'published_revision': published_revision,
207 'specific': rev_pk is not None,
209 'can_edit': doc.can_edit(request.user) if doc else None,
210 'was_published': was_published,
215 def book_pdf(request, pk, rev_pk):
216 from librarian.utils import Context
217 from librarian.document import Document as SST
218 from librarian.formats.pdf import PdfFormat
220 doc = get_object_or_404(Document, pk=pk)
221 rev = get_object_or_404(Revision, pk=rev_pk)
225 sst = SST.from_string(rev.materialize())
226 except ValueError as e:
227 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
230 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
231 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
233 if doc.owner_organization is not None and doc.owner_organization.logo:
234 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
236 pdf_file = PdfFormat(sst).build(ctx)
237 except BuildError as e:
238 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
240 from catalogue.ebook_utils import serve_file
241 return serve_file(pdf_file.get_filename(), '%d.pdf' % doc.pk, 'application/pdf')
245 def book_epub(request, pk, rev_pk):
246 from librarian.utils import Context
247 from librarian.document import Document as SST
248 from librarian.formats.epub import EpubFormat
250 doc = get_object_or_404(Document, pk=pk)
251 rev = get_object_or_404(Revision, pk=rev_pk)
255 sst = SST.from_string(rev.materialize())
256 except ValueError as e:
257 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
260 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
261 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
263 if doc.owner_organization is not None and doc.owner_organization.logo:
264 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
266 epub_file = EpubFormat(sst).build(ctx)
267 except BuildError as e:
268 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
270 from catalogue.ebook_utils import serve_file
271 return serve_file(epub_file.get_filename(), '%d.epub' % doc.pk, 'application/epub+zip')
275 def book_mobi(request, pk, rev_pk):
276 from librarian.utils import Context
277 from librarian.document import Document as SST
278 from librarian.formats.epub import EpubFormat
280 doc = get_object_or_404(Document, pk=pk)
281 rev = get_object_or_404(Revision, pk=rev_pk)
284 sst = SST.from_string(rev.materialize())
285 except ValueError as e:
286 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
289 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
290 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
292 if doc.owner_organization is not None and doc.owner_organization.logo:
293 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
295 epub_file = EpubFormat(sst).build(ctx)
296 except BuildError as e:
297 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
299 output_file = NamedTemporaryFile(prefix='librarian', suffix='.mobi', delete=False)
301 subprocess.check_call(
302 ['ebook-convert', epub_file.get_filename(), output_file.name, '--no-inline-toc'])
304 from catalogue.ebook_utils import serve_file
305 return serve_file(output_file.name, '%d.mobi' % doc.pk, 'application/epub+zip')
309 # def revision(request, slug, chunk=None):
311 # doc = Chunk.get(slug, chunk)
312 # except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
314 # if not doc.book.accessible(request):
315 # return HttpResponseForbidden("Not authorized.")
316 # return http.HttpResponse(str(doc.revision()))
320 def book_schedule(request, pk):
321 book = get_object_or_404(Document, pk=pk, deleted=False)
322 if not book.can_edit(request.user):
323 return HttpResponseForbidden("Not authorized.")
324 if request.method == 'POST':
325 Plan.objects.filter(document=book).delete()
326 for i, (s, name) in enumerate(STAGES):
327 user_id = request.POST.get('s%d-user' % i)
328 deadline = request.POST.get('s%d-deadline' % i) or None
329 Plan.objects.create(document=book, stage=s, user_id=user_id, deadline=deadline)
331 book.set_stage(request.POST.get('stage', ''))
332 return redirect('catalogue_user')
335 for p in Plan.objects.filter(document=book):
336 current[p.stage] = (getattr(p.user, 'pk', None), (p.deadline.isoformat() if p.deadline else None))
338 schedule = [(i, s, current.get(s, ())) for i, (s, name) in enumerate(STAGES)]
340 if book.owner_organization:
341 people = [m.user for m in book.owner_organization.membership_set.exclude(status='pending')]
343 people = [book.owner_user]
344 return render(request, 'catalogue/book_schedule.html', {
346 'schedule': schedule,
352 def book_owner(request, pk):
353 doc = get_object_or_404(Document, pk=pk, deleted=False)
354 if not doc.can_edit(request.user):
355 return HttpResponseForbidden("Not authorized.")
359 if request.method == 'POST':
361 new_org_pk = request.POST.get('owner_organization')
363 doc.owner_organization = None
364 doc.owner_user = request.user
367 org = Organization.objects.get(pk=new_org_pk)
368 if not org.is_member(request.user):
369 error = 'Bad organization'
371 doc.owner_organization = org
372 doc.owner_user = None
375 return redirect('catalogue_user')
377 return render(request, 'catalogue/book_owner.html', {
384 def book_delete(request, pk):
385 doc = get_object_or_404(Document, pk=pk, deleted=False)
386 if not doc.can_edit(request.user):
387 return HttpResponseForbidden("Not authorized.")
389 if request.method == 'POST':
392 return redirect('catalogue_user')
394 return render(request, 'catalogue/book_delete.html', {
401 def publish(request, pk):
402 from wiki import forms
403 from .models import PublishRecord
404 from dvcs.models import Revision
406 doc = get_object_or_404(Document, pk=pk, deleted=False)
407 if not doc.can_edit(request.user):
408 return HttpResponseForbidden("Not authorized.")
409 form = forms.DocumentTextPublishForm(request.POST, prefix="textpublish")
411 rev = Revision.objects.get(pk=form.cleaned_data['revision'])
412 # FIXME: check if in tree
413 # if PublishRecord.objects.filter(revision=rev, document=doc).exists():
414 # return http.HttpResponse('exists')
415 if not doc.published:
416 site = Site.objects.get_current()
418 'New published document in MIL/PEER',
419 '''New published document in MIL/PEER: %s. View it in browser: https://%s%s.
422 MIL/PEER team.''' % (doc.meta()['title'], site.domain, reverse('catalogue_html', args=[doc.pk])))
423 PublishRecord.objects.create(revision=rev, document=doc, user=request.user)
426 if request.is_ajax():
427 return http.HttpResponse('ok')
429 return redirect('catalogue_html', doc.pk)
431 if request.is_ajax():
432 return http.HttpResponse('error')
435 return redirect('catalogue_preview_rev', doc.pk, form.cleaned_data['revision'])
437 return redirect('catalogue_preview', doc.pk)
442 def unpublish(request, pk):
443 doc = get_object_or_404(Document, pk=pk, deleted=False)
444 if not doc.can_edit(request.user):
445 return HttpResponseForbidden("Not authorized.")
447 doc.publish_log.all().delete()
448 if request.is_ajax():
449 return http.HttpResponse('ok')
451 return redirect('catalogue_html', doc.pk)
454 class GalleryMixin(object):
455 def get_directory(self):
456 # return "%s%s/" % (settings.IMAGE_DIR, 'org%d' % self.org.pk if self.org is not None else self.request.user.pk)
457 return "uploads/%d/" % self.doc.pk
460 class GalleryView(GalleryMixin, UploadView):
462 def breadcrumbs(self):
464 (self.doc.meta()['title'], '/documents/%d/' % self.doc.pk),
467 def get_object(self, request, pk=None):
468 self.doc = Document.objects.get(pk=pk, deleted=False)
472 def fork(request, pk):
473 doc = get_object_or_404(Document, pk=pk, deleted=False)
474 if request.method == "POST":
475 form = forms.DocumentForkForm(request.POST, request.FILES)
478 org = request.user.membership_set.get(
479 organization=int(form.cleaned_data['owner_organization'])).organization
480 kwargs = {'owner_organization': org}
482 kwargs = {'owner_user': request.user}
484 new_doc = Document.objects.create(revision=doc.revision, **kwargs)
486 if os.path.isdir(settings.MEDIA_ROOT + "uploads/%d" % doc.pk):
488 settings.MEDIA_ROOT + "uploads/%d" % doc.pk,
489 settings.MEDIA_ROOT + "uploads/%d" % new_doc.pk
492 new_doc.assigned_to = request.user
495 return http.HttpResponseRedirect(reverse("wiki_editor", args=[new_doc.pk]))
497 form = forms.DocumentForkForm()
499 return render(request, "catalogue/document_fork.html", {
506 def upcoming(request):
507 return render(request, "catalogue/upcoming.html", {
508 'objects_list': Document.objects.filter(deleted=False).filter(publish_log=None),
512 def finished(request):
513 return render(request, "catalogue/finished.html", {
514 'objects_list': Document.objects.filter(deleted=False).exclude(publish_log=None),