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")
48 def document_list(request):
49 return render(request, 'catalogue/document_list.html')
53 def user(request, username):
54 user = get_object_or_404(User, username=username)
55 return render(request, 'catalogue/user_page.html', {"viewed_user": user})
62 return render(request, 'catalogue/my_page.html', {
64 request.session.get("wiki_last_books", {}).items(), key=lambda x: x[1]['time'], reverse=True),
71 def logout_then_redirect(request):
73 return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
78 def create_missing(request):
79 if request.method == "POST":
80 form = forms.DocumentCreateForm(request.POST, request.FILES)
82 # (TagMultipleForm if category.multiple else TagSingleForm)(
83 # category=category, data=request.POST, prefix=category.dc_tag)
84 # for category in Category.objects.all()]
85 if form.is_valid(): # and all(tag_form.is_valid() for tag_form in tag_forms):
87 if request.user.is_authenticated():
88 creator = request.user
92 title = form.cleaned_data['title']
94 org = request.user.membership_set.get(
95 organization=int(form.cleaned_data['owner_organization'])).organization
96 kwargs = {'owner_organization': org}
98 kwargs = {'owner_user': request.user}
100 doc = Document.objects.create(**kwargs)
102 cover = request.FILES.get('cover')
104 uppath = 'uploads/%d/' % doc.pk
105 path = settings.MEDIA_ROOT + uppath
106 if not os.path.isdir(path):
108 dest_path = path + cover.name # UNSAFE
109 with open(dest_path, 'w') as destination:
110 for chunk in cover.chunks():
111 destination.write(chunk)
112 cover_url = 'http://milpeer.eu/media/dynamic/' + uppath + cover.name
117 text='''<section xmlns="http://nowoczesnapolska.org.pl/sst#" xmlns:dc="http://purl.org/dc/elements/1.1/">
119 <dc:publisher>''' + form.cleaned_data['publisher'] + '''</dc:publisher>
120 <dc:description>''' + form.cleaned_data['description'] + '''</dc:description>
121 <dc:language>''' + form.cleaned_data['language'] + '''</dc:language>
122 <dc:rights>''' + form.cleaned_data['rights'] + '''</dc:rights>
123 <dc:audience>''' + form.cleaned_data['audience'] + '''</dc:audience>
124 <dc:relation.coverImage.url>''' + cover_url + '''</dc:relation.coverImage.url>
126 <header>''' + title + '''</header>
127 <div class="p"> </div>
131 doc.assigned_to = request.user
134 return http.HttpResponseRedirect(reverse("wiki_editor", args=[doc.pk]))
136 org_pk = request.GET.get('organization')
139 org = Organization.objects.get(pk=org_pk)
140 except Organization.DoesNotExist:
143 if not org.is_member(request.user):
150 form = forms.DocumentCreateForm(initial={'owner_organization': org})
153 # (TagMultipleForm if category.multiple else TagSingleForm)(category=category, prefix=category.dc_tag)
154 # for category in Category.objects.all()]
156 return render(request, "catalogue/document_create_missing.html", {
158 # "tag_forms": tag_forms,
165 def book_html(request, pk, rev_pk=None, preview=False):
166 from librarian.document import Document as SST
167 from librarian.formats.html import HtmlFormat
169 doc = get_object_or_404(Document, pk=pk, deleted=False)
172 published_revision = doc.publish_log.all()[0].revision
174 published_revision = None
178 revision = doc.revision
180 if published_revision is not None:
181 revision = published_revision
183 # No published version, fallback to preview mode.
185 revision = doc.revision
187 revision = get_object_or_404(Revision, pk=rev_pk)
189 was_published = revision == published_revision or doc.publish_log.filter(revision=revision).exists()
192 sst = SST.from_string(revision.materialize())
193 except ValueError as e:
196 html = HtmlFormat(sst).build(
197 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk)).get_string()
199 # response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
202 # for fragment in book.fragments.all().iterator():
203 # for theme in fragment.tags.filter(category='theme').iterator():
204 # book_themes.setdefault(theme, []).append(fragment)
206 # book_themes = book_themes.items()
207 # book_themes.sort(key=lambda s: s[0].sort_key)
208 return render(request, 'catalogue/book_text.html', {
211 'revision': revision,
212 'published_revision': published_revision,
213 'specific': rev_pk is not None,
215 'can_edit': doc.can_edit(request.user) if doc else None,
216 'was_published': was_published,
221 def book_pdf(request, pk, rev_pk):
222 from librarian.utils import Context
223 from librarian.document import Document as SST
224 from librarian.formats.pdf import PdfFormat
226 doc = get_object_or_404(Document, pk=pk)
227 rev = get_object_or_404(Revision, pk=rev_pk)
231 sst = SST.from_string(rev.materialize())
232 except ValueError as e:
233 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
236 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
237 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
239 if doc.owner_organization is not None and doc.owner_organization.logo:
240 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
242 pdf_file = PdfFormat(sst).build(ctx)
243 except BuildError as e:
244 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
246 from catalogue.ebook_utils import serve_file
247 return serve_file(pdf_file.get_filename(), '%d.pdf' % doc.pk, 'application/pdf')
251 def book_epub(request, pk, rev_pk):
252 from librarian.utils import Context
253 from librarian.document import Document as SST
254 from librarian.formats.epub import EpubFormat
256 doc = get_object_or_404(Document, pk=pk)
257 rev = get_object_or_404(Revision, pk=rev_pk)
261 sst = SST.from_string(rev.materialize())
262 except ValueError as e:
263 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
266 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
267 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
269 if doc.owner_organization is not None and doc.owner_organization.logo:
270 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
272 epub_file = EpubFormat(sst).build(ctx)
273 except BuildError as e:
274 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
276 from catalogue.ebook_utils import serve_file
277 return serve_file(epub_file.get_filename(), '%d.epub' % doc.pk, 'application/epub+zip')
281 def book_mobi(request, pk, rev_pk):
282 from librarian.utils import Context
283 from librarian.document import Document as SST
284 from librarian.formats.epub import EpubFormat
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 request.method == 'POST':
329 Plan.objects.filter(document=book).delete()
330 for i, s 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) 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 user_is_owner = doc.owner_organization and doc.owner_organization.is_member(request.user)
359 if not (doc.owner_user == request.user or user_is_owner):
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.owner_user == request.user or doc.owner_organization.is_member(request.user)):
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 # FIXME: check permissions
413 doc = get_object_or_404(Document, pk=pk, deleted=False)
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 # FIXME: check permissions
450 doc = get_object_or_404(Document, pk=pk, deleted=False)
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),