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
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 request.method == 'POST':
323 Plan.objects.filter(document=book).delete()
324 for i, (s, name) in enumerate(STAGES):
325 user_id = request.POST.get('s%d-user' % i)
326 deadline = request.POST.get('s%d-deadline' % i) or None
327 Plan.objects.create(document=book, stage=s, user_id=user_id, deadline=deadline)
329 book.set_stage(request.POST.get('stage', ''))
330 return redirect('catalogue_user')
333 for p in Plan.objects.filter(document=book):
334 current[p.stage] = (getattr(p.user, 'pk', None), (p.deadline.isoformat() if p.deadline else None))
336 schedule = [(i, s, current.get(s, ())) for i, (s, name) in enumerate(STAGES)]
338 if book.owner_organization:
339 people = [m.user for m in book.owner_organization.membership_set.exclude(status='pending')]
341 people = [book.owner_user]
342 return render(request, 'catalogue/book_schedule.html', {
344 'schedule': schedule,
350 def book_owner(request, pk):
351 doc = get_object_or_404(Document, pk=pk, deleted=False)
352 user_is_owner = doc.owner_organization and doc.owner_organization.is_member(request.user)
353 if not (doc.owner_user == request.user or user_is_owner):
358 if request.method == 'POST':
360 new_org_pk = request.POST.get('owner_organization')
362 doc.owner_organization = None
363 doc.owner_user = request.user
366 org = Organization.objects.get(pk=new_org_pk)
367 if not org.is_member(request.user):
368 error = 'Bad organization'
370 doc.owner_organization = org
371 doc.owner_user = None
374 return redirect('catalogue_user')
376 return render(request, 'catalogue/book_owner.html', {
383 def book_delete(request, pk):
384 doc = get_object_or_404(Document, pk=pk, deleted=False)
385 if not (doc.owner_user == request.user or doc.owner_organization.is_member(request.user)):
388 if request.method == 'POST':
391 return redirect('catalogue_user')
393 return render(request, 'catalogue/book_delete.html', {
400 def publish(request, pk):
401 from wiki import forms
402 from .models import PublishRecord
403 from dvcs.models import Revision
405 # FIXME: check permissions
407 doc = get_object_or_404(Document, pk=pk, deleted=False)
408 form = forms.DocumentTextPublishForm(request.POST, prefix="textpublish")
410 rev = Revision.objects.get(pk=form.cleaned_data['revision'])
411 # FIXME: check if in tree
412 # if PublishRecord.objects.filter(revision=rev, document=doc).exists():
413 # return http.HttpResponse('exists')
414 if not doc.published:
415 site = Site.objects.get_current()
417 'New published document in MIL/PEER',
418 '''New published document in MIL/PEER: %s. View it in browser: https://%s%s.
421 MIL/PEER team.''' % (doc.meta()['title'], site.domain, reverse('catalogue_html', args=[doc.pk])))
422 PublishRecord.objects.create(revision=rev, document=doc, user=request.user)
425 if request.is_ajax():
426 return http.HttpResponse('ok')
428 return redirect('catalogue_html', doc.pk)
430 if request.is_ajax():
431 return http.HttpResponse('error')
434 return redirect('catalogue_preview_rev', doc.pk, form.cleaned_data['revision'])
436 return redirect('catalogue_preview', doc.pk)
441 def unpublish(request, pk):
442 # FIXME: check permissions
444 doc = get_object_or_404(Document, pk=pk, deleted=False)
445 doc.publish_log.all().delete()
446 if request.is_ajax():
447 return http.HttpResponse('ok')
449 return redirect('catalogue_html', doc.pk)
452 class GalleryMixin(object):
453 def get_directory(self):
454 # return "%s%s/" % (settings.IMAGE_DIR, 'org%d' % self.org.pk if self.org is not None else self.request.user.pk)
455 return "uploads/%d/" % self.doc.pk
458 class GalleryView(GalleryMixin, UploadView):
460 def breadcrumbs(self):
462 (self.doc.meta()['title'], '/documents/%d/' % self.doc.pk),
465 def get_object(self, request, pk=None):
466 self.doc = Document.objects.get(pk=pk, deleted=False)
470 def fork(request, pk):
471 doc = get_object_or_404(Document, pk=pk, deleted=False)
472 if request.method == "POST":
473 form = forms.DocumentForkForm(request.POST, request.FILES)
476 org = request.user.membership_set.get(
477 organization=int(form.cleaned_data['owner_organization'])).organization
478 kwargs = {'owner_organization': org}
480 kwargs = {'owner_user': request.user}
482 new_doc = Document.objects.create(revision=doc.revision, **kwargs)
484 if os.path.isdir(settings.MEDIA_ROOT + "uploads/%d" % doc.pk):
486 settings.MEDIA_ROOT + "uploads/%d" % doc.pk,
487 settings.MEDIA_ROOT + "uploads/%d" % new_doc.pk
490 new_doc.assigned_to = request.user
493 return http.HttpResponseRedirect(reverse("wiki_editor", args=[new_doc.pk]))
495 form = forms.DocumentForkForm()
497 return render(request, "catalogue/document_fork.html", {
504 def upcoming(request):
505 return render(request, "catalogue/upcoming.html", {
506 'objects_list': Document.objects.filter(deleted=False).filter(publish_log=None),
510 def finished(request):
511 return render(request, "catalogue/finished.html", {
512 'objects_list': Document.objects.filter(deleted=False).exclude(publish_log=None),