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.")
356 user_is_owner = doc.owner_organization and doc.owner_organization.is_member(request.user)
357 if not (doc.owner_user == request.user or user_is_owner):
362 if request.method == 'POST':
364 new_org_pk = request.POST.get('owner_organization')
366 doc.owner_organization = None
367 doc.owner_user = request.user
370 org = Organization.objects.get(pk=new_org_pk)
371 if not org.is_member(request.user):
372 error = 'Bad organization'
374 doc.owner_organization = org
375 doc.owner_user = None
378 return redirect('catalogue_user')
380 return render(request, 'catalogue/book_owner.html', {
387 def book_delete(request, pk):
388 doc = get_object_or_404(Document, pk=pk, deleted=False)
389 if not doc.can_edit(request.user):
390 return HttpResponseForbidden("Not authorized.")
392 if request.method == 'POST':
395 return redirect('catalogue_user')
397 return render(request, 'catalogue/book_delete.html', {
404 def publish(request, pk):
405 from wiki import forms
406 from .models import PublishRecord
407 from dvcs.models import Revision
409 doc = get_object_or_404(Document, pk=pk, deleted=False)
410 if not doc.can_edit(request.user):
411 return HttpResponseForbidden("Not authorized.")
412 form = forms.DocumentTextPublishForm(request.POST, prefix="textpublish")
414 rev = Revision.objects.get(pk=form.cleaned_data['revision'])
415 # FIXME: check if in tree
416 # if PublishRecord.objects.filter(revision=rev, document=doc).exists():
417 # return http.HttpResponse('exists')
418 if not doc.published:
419 site = Site.objects.get_current()
421 'New published document in MIL/PEER',
422 '''New published document in MIL/PEER: %s. View it in browser: https://%s%s.
425 MIL/PEER team.''' % (doc.meta()['title'], site.domain, reverse('catalogue_html', args=[doc.pk])))
426 PublishRecord.objects.create(revision=rev, document=doc, user=request.user)
429 if request.is_ajax():
430 return http.HttpResponse('ok')
432 return redirect('catalogue_html', doc.pk)
434 if request.is_ajax():
435 return http.HttpResponse('error')
438 return redirect('catalogue_preview_rev', doc.pk, form.cleaned_data['revision'])
440 return redirect('catalogue_preview', doc.pk)
445 def unpublish(request, pk):
446 doc = get_object_or_404(Document, pk=pk, deleted=False)
447 if not doc.can_edit(request.user):
448 return HttpResponseForbidden("Not authorized.")
450 doc.publish_log.all().delete()
451 if request.is_ajax():
452 return http.HttpResponse('ok')
454 return redirect('catalogue_html', doc.pk)
457 class GalleryMixin(object):
458 def get_directory(self):
459 # return "%s%s/" % (settings.IMAGE_DIR, 'org%d' % self.org.pk if self.org is not None else self.request.user.pk)
460 return "uploads/%d/" % self.doc.pk
463 class GalleryView(GalleryMixin, UploadView):
465 def breadcrumbs(self):
467 (self.doc.meta()['title'], '/documents/%d/' % self.doc.pk),
470 def get_object(self, request, pk=None):
471 self.doc = Document.objects.get(pk=pk, deleted=False)
475 def fork(request, pk):
476 doc = get_object_or_404(Document, pk=pk, deleted=False)
477 if request.method == "POST":
478 form = forms.DocumentForkForm(request.POST, request.FILES)
481 org = request.user.membership_set.get(
482 organization=int(form.cleaned_data['owner_organization'])).organization
483 kwargs = {'owner_organization': org}
485 kwargs = {'owner_user': request.user}
487 new_doc = Document.objects.create(revision=doc.revision, **kwargs)
489 if os.path.isdir(settings.MEDIA_ROOT + "uploads/%d" % doc.pk):
491 settings.MEDIA_ROOT + "uploads/%d" % doc.pk,
492 settings.MEDIA_ROOT + "uploads/%d" % new_doc.pk
495 new_doc.assigned_to = request.user
498 return http.HttpResponseRedirect(reverse("wiki_editor", args=[new_doc.pk]))
500 form = forms.DocumentForkForm()
502 return render(request, "catalogue/document_fork.html", {
509 def upcoming(request):
510 return render(request, "catalogue/upcoming.html", {
511 'objects_list': Document.objects.filter(deleted=False).filter(publish_log=None),
515 def finished(request):
516 return render(request, "catalogue/finished.html", {
517 'objects_list': Document.objects.filter(deleted=False).exclude(publish_log=None),