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.helpers import active_tab
27 from librarian import BuildError
28 from redakcja.utlis import send_notify_email
29 from .constants import STAGES
30 from .models import Document, Plan
31 from dvcs.models import Revision
32 from organizations.models import Organization
33 from fileupload.views import UploadView
36 # Quick hack around caching problems, TODO: use ETags
38 from django.views.decorators.cache import never_cache
39 # from fnpdjango.utils.text.slughifi import slughifi
41 logger = logging.getLogger("fnp.catalogue")
46 def document_list(request):
47 return render(request, 'catalogue/document_list.html')
51 def user(request, username):
52 user = get_object_or_404(User, username=username)
53 return render(request, 'catalogue/user_page.html', {"viewed_user": user})
60 return render(request, 'catalogue/my_page.html', {
62 request.session.get("wiki_last_books", {}).items(), key=lambda x: x[1]['time'], reverse=True),
69 def logout_then_redirect(request):
71 return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
76 def create_missing(request):
77 if request.method == "POST":
78 form = forms.DocumentCreateForm(request.POST, request.FILES)
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})
146 return render(request, "catalogue/document_create_missing.html", {
154 def book_html(request, pk, rev_pk=None, preview=False):
155 from librarian.document import Document as SST
156 from librarian.formats.html import HtmlFormat
158 doc = get_object_or_404(Document, pk=pk, deleted=False)
161 published_revision = doc.publish_log.all()[0].revision
163 published_revision = None
167 revision = doc.revision
169 if published_revision is not None:
170 revision = published_revision
172 # No published version, fallback to preview mode.
174 revision = doc.revision
176 revision = get_object_or_404(Revision, pk=rev_pk)
178 was_published = revision == published_revision or doc.publish_log.filter(revision=revision).exists()
181 sst = SST.from_string(revision.materialize())
182 except ValueError as e:
185 html = HtmlFormat(sst).build(
186 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk)).get_string()
188 # response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
191 # for fragment in book.fragments.all().iterator():
192 # for theme in fragment.tags.filter(category='theme').iterator():
193 # book_themes.setdefault(theme, []).append(fragment)
195 # book_themes = book_themes.items()
196 # book_themes.sort(key=lambda s: s[0].sort_key)
197 return render(request, 'catalogue/book_text.html', {
200 'revision': revision,
201 'published_revision': published_revision,
202 'specific': rev_pk is not None,
204 'can_edit': doc.can_edit(request.user) if doc else None,
205 'was_published': was_published,
210 def book_pdf(request, pk, rev_pk):
211 from librarian.utils import Context
212 from librarian.document import Document as SST
213 from librarian.formats.pdf import PdfFormat
215 doc = get_object_or_404(Document, pk=pk)
216 rev = get_object_or_404(Revision, pk=rev_pk)
220 sst = SST.from_string(rev.materialize())
221 except ValueError as e:
222 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
225 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
226 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
228 if doc.owner_organization is not None and doc.owner_organization.logo:
229 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
231 pdf_file = PdfFormat(sst).build(ctx)
232 except BuildError as e:
233 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
235 from catalogue.ebook_utils import serve_file
236 return serve_file(pdf_file.get_filename(), '%d.pdf' % doc.pk, 'application/pdf')
240 def book_epub(request, pk, rev_pk):
241 from librarian.utils import Context
242 from librarian.document import Document as SST
243 from librarian.formats.epub import EpubFormat
245 doc = get_object_or_404(Document, pk=pk)
246 rev = get_object_or_404(Revision, pk=rev_pk)
250 sst = SST.from_string(rev.materialize())
251 except ValueError as e:
252 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
255 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
256 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
258 if doc.owner_organization is not None and doc.owner_organization.logo:
259 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
261 epub_file = EpubFormat(sst).build(ctx)
262 except BuildError as e:
263 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
265 from catalogue.ebook_utils import serve_file
266 return serve_file(epub_file.get_filename(), '%d.epub' % doc.pk, 'application/epub+zip')
270 def book_mobi(request, pk, rev_pk):
271 from librarian.utils import Context
272 from librarian.document import Document as SST
273 from librarian.formats.epub import EpubFormat
275 doc = get_object_or_404(Document, pk=pk)
276 rev = get_object_or_404(Revision, pk=rev_pk)
279 sst = SST.from_string(rev.materialize())
280 except ValueError as e:
281 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
284 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
285 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
287 if doc.owner_organization is not None and doc.owner_organization.logo:
288 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
290 epub_file = EpubFormat(sst).build(ctx)
291 except BuildError as e:
292 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
294 output_file = NamedTemporaryFile(prefix='librarian', suffix='.mobi', delete=False)
296 subprocess.check_call(
297 ['ebook-convert', epub_file.get_filename(), output_file.name, '--no-inline-toc'])
299 from catalogue.ebook_utils import serve_file
300 return serve_file(output_file.name, '%d.mobi' % doc.pk, 'application/epub+zip')
304 # def revision(request, slug, chunk=None):
306 # doc = Chunk.get(slug, chunk)
307 # except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
309 # if not doc.book.accessible(request):
310 # return HttpResponseForbidden("Not authorized.")
311 # return http.HttpResponse(str(doc.revision()))
315 def book_schedule(request, pk):
316 book = get_object_or_404(Document, pk=pk, deleted=False)
317 if request.method == 'POST':
318 Plan.objects.filter(document=book).delete()
319 for i, s in enumerate(STAGES):
320 user_id = request.POST.get('s%d-user' % i)
321 deadline = request.POST.get('s%d-deadline' % i) or None
322 Plan.objects.create(document=book, stage=s, user_id=user_id, deadline=deadline)
324 book.set_stage(request.POST.get('stage', ''))
325 return redirect('catalogue_user')
328 for p in Plan.objects.filter(document=book):
329 current[p.stage] = (getattr(p.user, 'pk', None), (p.deadline.isoformat() if p.deadline else None))
331 schedule = [(i, s, current.get(s, ())) for (i, s) in enumerate(STAGES)]
333 if book.owner_organization:
334 people = [m.user for m in book.owner_organization.membership_set.exclude(status='pending')]
336 people = [book.owner_user]
337 return render(request, 'catalogue/book_schedule.html', {
339 'schedule': schedule,
345 def book_owner(request, pk):
346 doc = get_object_or_404(Document, pk=pk, deleted=False)
347 user_is_owner = doc.owner_organization and doc.owner_organization.is_member(request.user)
348 if not (doc.owner_user == request.user or user_is_owner):
353 if request.method == 'POST':
355 new_org_pk = request.POST.get('owner_organization')
357 doc.owner_organization = None
358 doc.owner_user = request.user
361 org = Organization.objects.get(pk=new_org_pk)
362 if not org.is_member(request.user):
363 error = 'Bad organization'
365 doc.owner_organization = org
366 doc.owner_user = None
369 return redirect('catalogue_user')
371 return render(request, 'catalogue/book_owner.html', {
378 def book_delete(request, pk):
379 doc = get_object_or_404(Document, pk=pk, deleted=False)
380 if not (doc.owner_user == request.user or doc.owner_organization.is_member(request.user)):
383 if request.method == 'POST':
386 return redirect('catalogue_user')
388 return render(request, 'catalogue/book_delete.html', {
395 def publish(request, pk):
396 from wiki import forms
397 from .models import PublishRecord
398 from dvcs.models import Revision
400 # FIXME: check permissions
402 doc = get_object_or_404(Document, pk=pk, deleted=False)
403 form = forms.DocumentTextPublishForm(request.POST, prefix="textpublish")
405 rev = Revision.objects.get(pk=form.cleaned_data['revision'])
406 # FIXME: check if in tree
407 # if PublishRecord.objects.filter(revision=rev, document=doc).exists():
408 # return http.HttpResponse('exists')
409 if not doc.published:
410 site = Site.objects.get_current()
412 'New published document in MIL/PEER',
413 '''New published document in MIL/PEER: %s. View it in browser: https://%s%s.
416 MIL/PEER team.''' % (doc.meta()['title'], site.domain, reverse('catalogue_html', args=[doc.pk])))
417 PublishRecord.objects.create(revision=rev, document=doc, user=request.user)
420 if request.is_ajax():
421 return http.HttpResponse('ok')
423 return redirect('catalogue_html', doc.pk)
425 if request.is_ajax():
426 return http.HttpResponse('error')
429 return redirect('catalogue_preview_rev', doc.pk, form.cleaned_data['revision'])
431 return redirect('catalogue_preview', doc.pk)
436 def unpublish(request, pk):
437 # FIXME: check permissions
439 doc = get_object_or_404(Document, pk=pk, deleted=False)
440 doc.publish_log.all().delete()
441 if request.is_ajax():
442 return http.HttpResponse('ok')
444 return redirect('catalogue_html', doc.pk)
447 class GalleryMixin(object):
448 def get_directory(self):
449 # return "%s%s/" % (settings.IMAGE_DIR, 'org%d' % self.org.pk if self.org is not None else self.request.user.pk)
450 return "uploads/%d/" % self.doc.pk
453 class GalleryView(GalleryMixin, UploadView):
455 def breadcrumbs(self):
457 (self.doc.meta()['title'], '/documents/%d/' % self.doc.pk),
460 def get_object(self, request, pk=None):
461 self.doc = Document.objects.get(pk=pk, deleted=False)
465 def fork(request, pk):
466 doc = get_object_or_404(Document, pk=pk, deleted=False)
467 if request.method == "POST":
468 form = forms.DocumentForkForm(request.POST, request.FILES)
471 org = request.user.membership_set.get(
472 organization=int(form.cleaned_data['owner_organization'])).organization
473 kwargs = {'owner_organization': org}
475 kwargs = {'owner_user': request.user}
477 new_doc = Document.objects.create(revision=doc.revision, **kwargs)
479 if os.path.isdir(settings.MEDIA_ROOT + "uploads/%d" % doc.pk):
481 settings.MEDIA_ROOT + "uploads/%d" % doc.pk,
482 settings.MEDIA_ROOT + "uploads/%d" % new_doc.pk
485 new_doc.assigned_to = request.user
488 return http.HttpResponseRedirect(reverse("wiki_editor", args=[new_doc.pk]))
490 form = forms.DocumentForkForm()
492 return render(request, "catalogue/document_fork.html", {
499 def upcoming(request):
500 return render(request, "catalogue/upcoming.html", {
501 'objects_list': Document.objects.filter(deleted=False).filter(publish_log=None),
505 def finished(request):
506 return render(request, "catalogue/finished.html", {
507 'objects_list': Document.objects.filter(deleted=False).exclude(publish_log=None),