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
24 from unidecode import unidecode
26 from catalogue import forms
27 from catalogue.forms import TagMultipleForm, TagSingleForm
28 from catalogue.helpers import active_tab
29 from catalogue.models import Category
30 from librarian import BuildError
31 from redakcja.utlis import send_notify_email
32 from .constants import STAGES
33 from .models import Document, Plan
34 from dvcs.models import Revision
35 from organizations.models import Organization
36 from fileupload.views import UploadView
39 # Quick hack around caching problems, TODO: use ETags
41 from django.views.decorators.cache import never_cache
42 # from fnpdjango.utils.text.slughifi import slughifi
44 logger = logging.getLogger("fnp.catalogue")
48 def user(request, username):
49 user = get_object_or_404(User, username=username)
50 return render(request, 'catalogue/user_page.html', {"viewed_user": user})
57 return render(request, 'catalogue/my_page.html', {
59 request.session.get("wiki_last_books", {}).items(), key=lambda x: x[1]['time'], reverse=True),
66 def logout_then_redirect(request):
68 return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
73 def create_missing(request):
74 if request.method == "POST":
75 form = forms.DocumentCreateForm(request.POST, request.FILES)
77 (TagMultipleForm if category.multiple else TagSingleForm)(
78 category=category, data=request.POST, prefix=category.dc_tag)
79 for category in Category.objects.all()]
80 if form.is_valid() and all(tag_form.is_valid() for tag_form in tag_forms):
82 if request.user.is_authenticated():
83 creator = request.user
87 title = form.cleaned_data['title']
89 org = request.user.membership_set.get(
90 organization=int(form.cleaned_data['owner_organization'])).organization
91 kwargs = {'owner_organization': org}
93 kwargs = {'owner_user': request.user}
95 doc = Document.objects.create(**kwargs)
97 cover = request.FILES.get('cover')
99 uppath = 'uploads/%d/' % doc.pk
100 path = settings.MEDIA_ROOT + uppath
101 if not os.path.isdir(path):
103 cover.name = unidecode(cover.name)
104 dest_path = path + cover.name
105 if not os.path.abspath(dest_path).startswith(os.path.abspath(path)):
107 with open(dest_path, 'w') as destination:
108 for chunk in cover.chunks():
109 destination.write(chunk)
110 cover_url = 'http://milpeer.eu/media/dynamic/' + uppath + cover.name
115 text='''<section xmlns="http://nowoczesnapolska.org.pl/sst#" xmlns:dc="http://purl.org/dc/elements/1.1/">
117 <dc:publisher>''' + form.cleaned_data['publisher'] + '''</dc:publisher>
118 <dc:description>''' + form.cleaned_data['description'] + '''</dc:description>
119 <dc:language>''' + form.cleaned_data['language'] + '''</dc:language>
120 <dc:rights>''' + form.cleaned_data['rights'] + '''</dc:rights>
121 <dc:audience>''' + form.cleaned_data['audience'] + '''</dc:audience>
122 <dc:relation.coverImage.url>''' + cover_url + '''</dc:relation.coverImage.url>
124 <header>''' + title + '''</header>
125 <div class="p"> </div>
129 doc.assigned_to = request.user
132 return http.HttpResponseRedirect(reverse("wiki_editor", args=[doc.pk]))
134 org_pk = request.GET.get('organization')
137 org = Organization.objects.get(pk=org_pk)
138 except Organization.DoesNotExist:
141 if not org.is_member(request.user):
148 form = forms.DocumentCreateForm(initial={'owner_organization': org})
151 (TagMultipleForm if category.multiple else TagSingleForm)(
152 category=category, tutorial_no=i, prefix=category.dc_tag)
153 for i, category in enumerate(Category.objects.all(), start=2)]
155 return render(request, "catalogue/document_create_missing.html", {
157 "tag_forms": tag_forms,
164 def book_html(request, pk, rev_pk=None, preview=False):
165 from librarian.document import Document as SST
166 from librarian.formats.html import HtmlFormat
168 doc = get_object_or_404(Document, pk=pk, deleted=False)
171 published_revision = doc.publish_log.all()[0].revision
173 published_revision = None
177 revision = doc.revision
179 if published_revision is not None:
180 revision = published_revision
182 # No published version, fallback to preview mode.
184 revision = doc.revision
186 revision = get_object_or_404(Revision, pk=rev_pk)
188 was_published = revision == published_revision or doc.publish_log.filter(revision=revision).exists()
191 sst = SST.from_string(revision.materialize())
192 except ValueError as e:
195 html = HtmlFormat(sst).build(
196 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk)).get_string()
198 # response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
201 # for fragment in book.fragments.all().iterator():
202 # for theme in fragment.tags.filter(category='theme').iterator():
203 # book_themes.setdefault(theme, []).append(fragment)
205 # book_themes = book_themes.items()
206 # book_themes.sort(key=lambda s: s[0].sort_key)
207 return render(request, 'catalogue/book_text.html', {
210 'revision': revision,
211 'published_revision': published_revision,
212 'specific': rev_pk is not None,
214 'can_edit': doc.can_edit(request.user) if doc else None,
215 'was_published': was_published,
220 def book_pdf(request, pk, rev_pk):
221 from librarian.utils import Context
222 from librarian.document import Document as SST
223 from librarian.formats.pdf import PdfFormat
225 doc = get_object_or_404(Document, pk=pk)
226 rev = get_object_or_404(Revision, pk=rev_pk)
230 sst = SST.from_string(rev.materialize())
231 except ValueError as e:
232 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
235 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
236 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
238 if doc.owner_organization is not None and doc.owner_organization.logo:
239 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
241 pdf_file = PdfFormat(sst).build(ctx)
242 except BuildError as e:
243 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
245 from catalogue.ebook_utils import serve_file
246 return serve_file(pdf_file.get_filename(), '%d.pdf' % doc.pk, 'application/pdf')
250 def book_epub(request, pk, rev_pk):
251 from librarian.utils import Context
252 from librarian.document import Document as SST
253 from librarian.formats.epub import EpubFormat
255 doc = get_object_or_404(Document, pk=pk)
256 rev = get_object_or_404(Revision, pk=rev_pk)
260 sst = SST.from_string(rev.materialize())
261 except ValueError as e:
262 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
265 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
266 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
268 if doc.owner_organization is not None and doc.owner_organization.logo:
269 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
271 epub_file = EpubFormat(sst).build(ctx)
272 except BuildError as e:
273 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
275 from catalogue.ebook_utils import serve_file
276 return serve_file(epub_file.get_filename(), '%d.epub' % doc.pk, 'application/epub+zip')
280 def book_mobi(request, pk, rev_pk):
281 from librarian.utils import Context
282 from librarian.document import Document as SST
283 from librarian.formats.epub import EpubFormat
285 doc = get_object_or_404(Document, pk=pk)
286 rev = get_object_or_404(Revision, pk=rev_pk)
289 sst = SST.from_string(rev.materialize())
290 except ValueError as e:
291 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
294 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
295 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
297 if doc.owner_organization is not None and doc.owner_organization.logo:
298 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
300 epub_file = EpubFormat(sst).build(ctx)
301 except BuildError as e:
302 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
304 output_file = NamedTemporaryFile(prefix='librarian', suffix='.mobi', delete=False)
306 subprocess.check_call(
307 ['ebook-convert', epub_file.get_filename(), output_file.name, '--no-inline-toc'])
309 from catalogue.ebook_utils import serve_file
310 return serve_file(output_file.name, '%d.mobi' % doc.pk, 'application/epub+zip')
314 # def revision(request, slug, chunk=None):
316 # doc = Chunk.get(slug, chunk)
317 # except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
319 # if not doc.book.accessible(request):
320 # return HttpResponseForbidden("Not authorized.")
321 # return http.HttpResponse(str(doc.revision()))
325 def book_schedule(request, pk):
326 book = get_object_or_404(Document, pk=pk, deleted=False)
327 if not book.can_edit(request.user):
328 return HttpResponseForbidden("Not authorized.")
329 if request.method == 'POST':
330 Plan.objects.filter(document=book).delete()
331 for i, (s, name) in enumerate(STAGES):
332 user_id = request.POST.get('s%d-user' % i)
333 deadline = request.POST.get('s%d-deadline' % i) or None
334 Plan.objects.create(document=book, stage=s, user_id=user_id, deadline=deadline)
336 book.set_stage(request.POST.get('stage', ''))
337 return redirect('catalogue_user')
340 for p in Plan.objects.filter(document=book):
341 current[p.stage] = (getattr(p.user, 'pk', None), (p.deadline.isoformat() if p.deadline else None))
343 schedule = [(i, s, current.get(s, ())) for i, (s, name) in enumerate(STAGES)]
345 if book.owner_organization:
346 people = [m.user for m in book.owner_organization.membership_set.exclude(status='pending')]
348 people = [book.owner_user]
349 return render(request, 'catalogue/book_schedule.html', {
351 'schedule': schedule,
357 def book_owner(request, pk):
358 doc = get_object_or_404(Document, pk=pk, deleted=False)
359 if not doc.can_edit(request.user):
360 return HttpResponseForbidden("Not authorized.")
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.can_edit(request.user):
392 return HttpResponseForbidden("Not authorized.")
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 doc = get_object_or_404(Document, pk=pk, deleted=False)
412 if not doc.can_edit(request.user):
413 return HttpResponseForbidden("Not authorized.")
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 doc = get_object_or_404(Document, pk=pk, deleted=False)
449 if not doc.can_edit(request.user):
450 return HttpResponseForbidden("Not authorized.")
452 doc.publish_log.all().delete()
453 if request.is_ajax():
454 return http.HttpResponse('ok')
456 return redirect('catalogue_html', doc.pk)
459 class GalleryMixin(object):
460 def get_directory(self):
461 # return "%s%s/" % (settings.IMAGE_DIR, 'org%d' % self.org.pk if self.org is not None else self.request.user.pk)
462 return "uploads/%d/" % self.doc.pk
465 class GalleryView(GalleryMixin, UploadView):
467 def breadcrumbs(self):
469 (self.doc.meta()['title'], '/documents/%d/' % self.doc.pk),
472 def get_object(self, request, pk=None):
473 self.doc = Document.objects.get(pk=pk, deleted=False)
477 def fork(request, pk):
478 doc = get_object_or_404(Document, pk=pk, deleted=False)
479 if request.method == "POST":
480 form = forms.DocumentForkForm(request.POST, request.FILES)
483 org = request.user.membership_set.get(
484 organization=int(form.cleaned_data['owner_organization'])).organization
485 kwargs = {'owner_organization': org}
487 kwargs = {'owner_user': request.user}
489 new_doc = Document.objects.create(revision=doc.revision, **kwargs)
491 if os.path.isdir(settings.MEDIA_ROOT + "uploads/%d" % doc.pk):
493 settings.MEDIA_ROOT + "uploads/%d" % doc.pk,
494 settings.MEDIA_ROOT + "uploads/%d" % new_doc.pk
497 new_doc.assigned_to = request.user
500 return http.HttpResponseRedirect(reverse("wiki_editor", args=[new_doc.pk]))
502 form = forms.DocumentForkForm()
504 return render(request, "catalogue/document_fork.html", {
511 def upcoming(request):
512 return render(request, "catalogue/upcoming.html", {
513 'objects_list': Document.objects.filter(deleted=False).filter(publish_log=None),
517 def finished(request):
518 return render(request, "catalogue/finished.html", {
519 'objects_list': Document.objects.filter(deleted=False).exclude(publish_log=None),