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.core.urlresolvers import reverse
 
  17 from django import http
 
  18 from django.http import Http404, HttpResponse
 
  19 from django.shortcuts import get_object_or_404, render, redirect
 
  20 from django.utils.encoding import force_str
 
  21 from django.utils.http import urlquote_plus
 
  22 from django.views.decorators.http import require_POST
 
  24 from catalogue import forms
 
  25 from catalogue.helpers import active_tab
 
  26 from librarian import BuildError
 
  27 from .constants import STAGES
 
  28 from .models import Document, Plan
 
  29 from dvcs.models import Revision
 
  30 from organizations.models import Organization
 
  31 from fileupload.views import UploadView
 
  34 # Quick hack around caching problems, TODO: use ETags
 
  36 from django.views.decorators.cache import never_cache
 
  37 # from fnpdjango.utils.text.slughifi import slughifi
 
  39 logger = logging.getLogger("fnp.catalogue")
 
  44 def document_list(request):
 
  45     return render(request, 'catalogue/document_list.html')
 
  49 def user(request, username):
 
  50     user = get_object_or_404(User, username=username)
 
  51     return render(request, 'catalogue/user_page.html', {"viewed_user": user})
 
  58     return render(request, 'catalogue/my_page.html', {
 
  60             request.session.get("wiki_last_books", {}).items(), key=lambda x: x[1]['time'], reverse=True),
 
  67 def logout_then_redirect(request):
 
  69     return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
 
  74 def create_missing(request):
 
  75     if request.method == "POST":
 
  76         form = forms.DocumentCreateForm(request.POST, request.FILES)
 
  79             if request.user.is_authenticated():
 
  80                 creator = request.user
 
  84             title = form.cleaned_data['title']
 
  86                 org = request.user.membership_set.get(
 
  87                     organization=int(form.cleaned_data['owner_organization'])).organization
 
  88                 kwargs = {'owner_organization': org}
 
  90                 kwargs = {'owner_user': request.user}
 
  92             doc = Document.objects.create(**kwargs)
 
  94             cover = request.FILES.get('cover')
 
  96                 uppath = 'uploads/%d/' % doc.pk
 
  97                 path = settings.MEDIA_ROOT + uppath
 
  98                 if not os.path.isdir(path):
 
 100                 dest_path = path + cover.name   # UNSAFE
 
 101                 with open(dest_path, 'w') as destination:
 
 102                     for chunk in cover.chunks():
 
 103                         destination.write(chunk)
 
 104                 cover_url = 'http://milpeer.eu/media/dynamic/' + uppath + cover.name
 
 109                 text='''<section xmlns="http://nowoczesnapolska.org.pl/sst#" xmlns:dc="http://purl.org/dc/elements/1.1/">
 
 111                     <dc:publisher>''' + form.cleaned_data['publisher'] + '''</dc:publisher>
 
 112                     <dc:description>''' + form.cleaned_data['description'] + '''</dc:description>
 
 113                     <dc:language>''' + form.cleaned_data['language'] + '''</dc:language>
 
 114                     <dc:rights>''' + form.cleaned_data['rights'] + '''</dc:rights>
 
 115                     <dc:audience>''' + form.cleaned_data['audience'] + '''</dc:audience>
 
 116                     <dc:relation.coverImage.url>''' + cover_url + '''</dc:relation.coverImage.url>
 
 118                 <header>''' + title + '''</header>
 
 119                 <div class="p"> </div>
 
 123             doc.assigned_to = request.user
 
 126             return http.HttpResponseRedirect(reverse("wiki_editor", args=[doc.pk]))
 
 128         org_pk = request.GET.get('organization')
 
 131                 org = Organization.objects.get(pk=org_pk)
 
 132             except Organization.DoesNotExist:
 
 135                 if not org.is_member(request.user):
 
 142         form = forms.DocumentCreateForm(initial={'owner_organization': org})
 
 144     return render(request, "catalogue/document_create_missing.html", {
 
 152 def book_html(request, pk, rev_pk=None, preview=False):
 
 153     from librarian.document import Document as SST
 
 154     from librarian.formats.html import HtmlFormat
 
 156     doc = get_object_or_404(Document, pk=pk, deleted=False)
 
 159         published_revision = doc.publish_log.all()[0].revision
 
 161         published_revision = None
 
 165             revision = doc.revision
 
 167             if published_revision is not None:
 
 168                 revision = published_revision
 
 170                 # No published version, fallback to preview mode.
 
 172                 revision = doc.revision
 
 174         revision = get_object_or_404(Revision, pk=rev_pk)
 
 176     was_published = revision == published_revision or doc.publish_log.filter(revision=revision).exists()
 
 179         sst = SST.from_string(revision.materialize())
 
 180     except ValueError as e:
 
 183         html = HtmlFormat(sst).build(
 
 184             files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk)).get_string()
 
 186     # response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
 
 189     # for fragment in book.fragments.all().iterator():
 
 190     #     for theme in fragment.tags.filter(category='theme').iterator():
 
 191     #         book_themes.setdefault(theme, []).append(fragment)
 
 193     # book_themes = book_themes.items()
 
 194     # book_themes.sort(key=lambda s: s[0].sort_key)
 
 195     return render(request, 'catalogue/book_text.html', {
 
 198         'revision': revision,
 
 199         'published_revision': published_revision,
 
 200         'specific': rev_pk is not None,
 
 202         'can_edit': doc.can_edit(request.user) if doc else None,
 
 203         'was_published': was_published,
 
 208 def book_pdf(request, pk, rev_pk):
 
 209     from librarian.utils import Context
 
 210     from librarian.document import Document as SST
 
 211     from librarian.formats.pdf import PdfFormat
 
 213     doc = get_object_or_404(Document, pk=pk)
 
 214     rev = get_object_or_404(Revision, pk=rev_pk)
 
 218         sst = SST.from_string(rev.materialize())
 
 219     except ValueError as e:
 
 220         return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
 
 223         files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
 
 224         source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
 
 226     if doc.owner_organization is not None and doc.owner_organization.logo:
 
 227         ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
 
 229         pdf_file = PdfFormat(sst).build(ctx)
 
 230     except BuildError as e:
 
 231         return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
 
 233     from catalogue.ebook_utils import serve_file
 
 234     return serve_file(pdf_file.get_filename(), '%d.pdf' % doc.pk, 'application/pdf')
 
 238 def book_epub(request, pk, rev_pk):
 
 239     from librarian.utils import Context
 
 240     from librarian.document import Document as SST
 
 241     from librarian.formats.epub import EpubFormat
 
 243     doc = get_object_or_404(Document, pk=pk)
 
 244     rev = get_object_or_404(Revision, pk=rev_pk)
 
 248         sst = SST.from_string(rev.materialize())
 
 249     except ValueError as e:
 
 250         return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
 
 253         files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
 
 254         source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
 
 256     if doc.owner_organization is not None and doc.owner_organization.logo:
 
 257         ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
 
 259         epub_file = EpubFormat(sst).build(ctx)
 
 260     except BuildError as e:
 
 261         return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
 
 263     from catalogue.ebook_utils import serve_file
 
 264     return serve_file(epub_file.get_filename(), '%d.epub' % doc.pk, 'application/epub+zip')
 
 268 def book_mobi(request, pk, rev_pk):
 
 269     from librarian.utils import Context
 
 270     from librarian.document import Document as SST
 
 271     from librarian.formats.epub import EpubFormat
 
 273     doc = get_object_or_404(Document, pk=pk)
 
 274     rev = get_object_or_404(Revision, pk=rev_pk)
 
 277         sst = SST.from_string(rev.materialize())
 
 278     except ValueError as e:
 
 279         return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
 
 282         files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
 
 283         source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
 
 285     if doc.owner_organization is not None and doc.owner_organization.logo:
 
 286         ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
 
 288         epub_file = EpubFormat(sst).build(ctx)
 
 289     except BuildError as e:
 
 290         return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
 
 292     output_file = NamedTemporaryFile(prefix='librarian', suffix='.mobi', delete=False)
 
 294     subprocess.check_call(
 
 295         ['ebook-convert', epub_file.get_filename(), output_file.name, '--no-inline-toc'])
 
 297     from catalogue.ebook_utils import serve_file
 
 298     return serve_file(output_file.name, '%d.mobi' % doc.pk, 'application/epub+zip')
 
 302 # def revision(request, slug, chunk=None):
 
 304 #         doc = Chunk.get(slug, chunk)
 
 305 #     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
 
 307 #     if not doc.book.accessible(request):
 
 308 #         return HttpResponseForbidden("Not authorized.")
 
 309 #     return http.HttpResponse(str(doc.revision()))
 
 313 def book_schedule(request, pk):
 
 314     book = get_object_or_404(Document, pk=pk, deleted=False)
 
 315     if request.method == 'POST':
 
 316         Plan.objects.filter(document=book).delete()
 
 317         for i, s in enumerate(STAGES):
 
 318             user_id = request.POST.get('s%d-user' % i)
 
 319             deadline = request.POST.get('s%d-deadline' % i) or None
 
 320             Plan.objects.create(document=book, stage=s, user_id=user_id, deadline=deadline)
 
 322         book.set_stage(request.POST.get('stage', ''))
 
 323         return redirect('catalogue_user')
 
 326     for p in Plan.objects.filter(document=book):
 
 327         current[p.stage] = (getattr(p.user, 'pk', None), (p.deadline.isoformat() if p.deadline else None))
 
 329     schedule = [(i, s, current.get(s, ())) for (i, s) in enumerate(STAGES)]
 
 331     if book.owner_organization:
 
 332         people = [m.user for m in book.owner_organization.membership_set.exclude(status='pending')]
 
 334         people = [book.owner_user]
 
 335     return render(request, 'catalogue/book_schedule.html', {
 
 337         'schedule': schedule,
 
 343 def book_owner(request, pk):
 
 344     doc = get_object_or_404(Document, pk=pk, deleted=False)
 
 345     user_is_owner = doc.owner_organization and doc.owner_organization.is_member(request.user)
 
 346     if not (doc.owner_user == request.user or user_is_owner):
 
 351     if request.method == 'POST':
 
 353         new_org_pk = request.POST.get('owner_organization')
 
 355             doc.owner_organization = None
 
 356             doc.owner_user = request.user
 
 359             org = Organization.objects.get(pk=new_org_pk)
 
 360             if not org.is_member(request.user):
 
 361                 error = 'Bad organization'
 
 363                 doc.owner_organization = org
 
 364                 doc.owner_user = None
 
 367             return redirect('catalogue_user')
 
 369     return render(request, 'catalogue/book_owner.html', {
 
 376 def book_delete(request, pk):
 
 377     doc = get_object_or_404(Document, pk=pk, deleted=False)
 
 378     if not (doc.owner_user == request.user or doc.owner_organization.is_member(request.user)):
 
 381     if request.method == 'POST':
 
 384         return redirect('catalogue_user')
 
 386     return render(request, 'catalogue/book_delete.html', {
 
 393 def publish(request, pk):
 
 394     from wiki import forms
 
 395     from .models import PublishRecord
 
 396     from dvcs.models import Revision
 
 398     # FIXME: check permissions
 
 400     doc = get_object_or_404(Document, pk=pk, deleted=False)
 
 401     form = forms.DocumentTextPublishForm(request.POST, prefix="textpublish")
 
 403         rev = Revision.objects.get(pk=form.cleaned_data['revision'])
 
 404         # FIXME: check if in tree
 
 405         # if PublishRecord.objects.filter(revision=rev, document=doc).exists():
 
 406         #     return http.HttpResponse('exists')
 
 407         PublishRecord.objects.create(revision=rev, document=doc, user=request.user)
 
 408         if request.is_ajax():
 
 409             return http.HttpResponse('ok')
 
 411             return redirect('catalogue_html', doc.pk)
 
 413         if request.is_ajax():
 
 414             return http.HttpResponse('error')
 
 417                 return redirect('catalogue_preview_rev', doc.pk, form.cleaned_data['revision'])
 
 419                 return redirect('catalogue_preview', doc.pk)
 
 424 def unpublish(request, pk):
 
 425     # FIXME: check permissions
 
 427     doc = get_object_or_404(Document, pk=pk, deleted=False)
 
 428     doc.publish_log.all().delete()
 
 429     if request.is_ajax():
 
 430         return http.HttpResponse('ok')
 
 432         return redirect('catalogue_html', doc.pk)
 
 435 class GalleryMixin(object):
 
 436     def get_directory(self):
 
 437         # return "%s%s/" % (settings.IMAGE_DIR, 'org%d' % self.org.pk if self.org is not None else self.request.user.pk)
 
 438         return "uploads/%d/" % self.doc.pk
 
 441 class GalleryView(GalleryMixin, UploadView):
 
 443     def breadcrumbs(self):
 
 445                 (self.doc.meta()['title'], '/documents/%d/' % self.doc.pk),
 
 448     def get_object(self, request, pk=None):
 
 449         self.doc = Document.objects.get(pk=pk, deleted=False)
 
 453 def fork(request, pk):
 
 454     doc = get_object_or_404(Document, pk=pk, deleted=False)
 
 455     if request.method == "POST":
 
 456         form = forms.DocumentForkForm(request.POST, request.FILES)
 
 459                 org = request.user.membership_set.get(
 
 460                     organization=int(form.cleaned_data['owner_organization'])).organization
 
 461                 kwargs = {'owner_organization': org}
 
 463                 kwargs = {'owner_user': request.user}
 
 465             new_doc = Document.objects.create(revision=doc.revision, **kwargs)
 
 467             if os.path.isdir(settings.MEDIA_ROOT + "uploads/%d" % doc.pk):
 
 469                     settings.MEDIA_ROOT + "uploads/%d" % doc.pk,
 
 470                     settings.MEDIA_ROOT + "uploads/%d" % new_doc.pk
 
 473             new_doc.assigned_to = request.user
 
 476             return http.HttpResponseRedirect(reverse("wiki_editor", args=[new_doc.pk]))
 
 478         form = forms.DocumentForkForm()
 
 480     return render(request, "catalogue/document_fork.html", {
 
 487 def upcoming(request):
 
 488     return render(request, "catalogue/upcoming.html", {
 
 489         'objects_list': Document.objects.filter(deleted=False).filter(publish_log=None),
 
 493 def finished(request):
 
 494     return render(request, "catalogue/finished.html", {
 
 495         'objects_list': Document.objects.filter(deleted=False).exclude(publish_log=None),