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
11 from xml.sax.saxutils import escape as escape_xml
13 from django.conf import settings
14 from django.contrib import auth
15 from django.contrib.auth.models import User
16 from django.contrib.auth.decorators import login_required
17 from django.contrib.sites.models import Site
18 from django.core.urlresolvers import reverse
19 from django import http
20 from django.http import Http404, HttpResponse, HttpResponseForbidden
21 from django.shortcuts import get_object_or_404, render, redirect
22 from django.utils.encoding import force_str
23 from django.utils.http import urlquote_plus
24 from django.views.decorators.http import require_POST
25 from unidecode import unidecode
27 from catalogue import forms
28 from catalogue.filters import DocumentFilterSet
29 from catalogue.forms import TagMultipleForm, TagSingleForm
30 from catalogue.helpers import active_tab
31 from catalogue.models import Category
32 from librarian import BuildError
33 from redakcja.utlis import send_notify_email
34 from .constants import STAGES
35 from .models import Document, Plan
36 from dvcs.models import Revision
37 from organizations.models import Organization
38 from fileupload.views import UploadView
41 # Quick hack around caching problems, TODO: use ETags
43 from django.views.decorators.cache import never_cache
44 # from fnpdjango.utils.text.slughifi import slughifi
46 logger = logging.getLogger("fnp.catalogue")
50 def user(request, username):
51 user = get_object_or_404(User, username=username)
52 return render(request, 'catalogue/user_page.html', {"viewed_user": user})
59 return render(request, 'catalogue/my_page.html', {
61 request.session.get("wiki_last_books", {}).items(), key=lambda x: x[1]['time'], reverse=True),
68 def logout_then_redirect(request):
70 return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
75 def create_missing(request):
76 if request.method == "POST":
77 form = forms.DocumentCreateForm(request.POST, request.FILES)
79 (TagMultipleForm if category.multiple else TagSingleForm)(
80 category=category, data=request.POST, prefix=category.dc_tag)
81 for category in Category.objects.all()]
82 if form.is_valid() and all(tag_form.is_valid() for tag_form in tag_forms):
84 if request.user.is_authenticated():
85 creator = request.user
89 title = form.cleaned_data['title']
91 org = request.user.membership_set.get(
92 organization=int(form.cleaned_data['owner_organization'])).organization
93 kwargs = {'owner_organization': org}
95 kwargs = {'owner_user': request.user}
97 doc = Document.objects.create(**kwargs)
99 for tag_form in tag_forms:
100 tag_form.save(instance=doc)
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 cover.name = unidecode(cover.name)
109 dest_path = path + cover.name
110 if not os.path.abspath(dest_path).startswith(os.path.abspath(path)):
112 with open(dest_path, 'w') as destination:
113 for chunk in cover.chunks():
114 destination.write(chunk)
115 cover_url = 'http://milpeer.eu/media/dynamic/' + uppath + cover.name
119 text = '''<section xmlns="http://nowoczesnapolska.org.pl/sst#" xmlns:dc="http://purl.org/dc/elements/1.1/">
121 <dc:publisher>%s</dc:publisher>
122 <dc:description>%s</dc:description>
124 <dc:relation.coverImage.url>%s</dc:relation.coverImage.url>
127 <div class="p"> </div>
129 escape_xml(form.cleaned_data['publisher']),
130 escape_xml(form.cleaned_data['description']),
131 '\n'.join(tag_form.metadata_rows() for tag_form in tag_forms),
132 escape_xml(cover_url),
139 doc.assigned_to = request.user
142 return http.HttpResponseRedirect(reverse("wiki_editor", args=[doc.pk]))
144 org_pk = request.GET.get('organization')
147 org = Organization.objects.get(pk=org_pk)
148 except Organization.DoesNotExist:
151 if not org.is_member(request.user):
158 form = forms.DocumentCreateForm(initial={'owner_organization': org})
161 (TagMultipleForm if category.multiple else TagSingleForm)(
162 category=category, tutorial_no=i, prefix=category.dc_tag)
163 for i, category in enumerate(Category.objects.all(), start=2)]
165 return render(request, "catalogue/document_create_missing.html", {
167 "tag_forms": tag_forms,
174 def book_html(request, pk, rev_pk=None, preview=False):
175 from librarian.document import Document as SST
176 from librarian.formats.html import HtmlFormat
178 doc = get_object_or_404(Document, pk=pk, deleted=False)
181 published_revision = doc.publish_log.all()[0].revision
183 published_revision = None
187 revision = doc.revision
189 if published_revision is not None:
190 revision = published_revision
192 # No published version, fallback to preview mode.
194 revision = doc.revision
196 revision = get_object_or_404(Revision, pk=rev_pk)
198 was_published = revision == published_revision or doc.publish_log.filter(revision=revision).exists()
201 sst = SST.from_string(revision.materialize())
202 except ValueError as e:
205 html = HtmlFormat(sst).build(
206 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk)).get_string()
208 # response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
211 # for fragment in book.fragments.all().iterator():
212 # for theme in fragment.tags.filter(category='theme').iterator():
213 # book_themes.setdefault(theme, []).append(fragment)
215 # book_themes = book_themes.items()
216 # book_themes.sort(key=lambda s: s[0].sort_key)
217 return render(request, 'catalogue/book_text.html', {
220 'revision': revision,
221 'published_revision': published_revision,
222 'specific': rev_pk is not None,
224 'can_edit': doc.can_edit(request.user) if doc else None,
225 'was_published': was_published,
230 def book_pdf(request, pk, rev_pk):
231 from librarian.utils import Context
232 from librarian.document import Document as SST
233 from librarian.formats.pdf import PdfFormat
235 doc = get_object_or_404(Document, pk=pk)
236 rev = get_object_or_404(Revision, pk=rev_pk)
240 sst = SST.from_string(rev.materialize())
241 except ValueError as e:
242 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
245 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
246 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
248 if doc.owner_organization is not None and doc.owner_organization.logo:
249 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
251 pdf_file = PdfFormat(sst).build(ctx)
252 except BuildError as e:
253 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
255 from catalogue.ebook_utils import serve_file
256 return serve_file(pdf_file.get_filename(), '%d.pdf' % doc.pk, 'application/pdf')
260 def book_epub(request, pk, rev_pk):
261 from librarian.utils import Context
262 from librarian.document import Document as SST
263 from librarian.formats.epub import EpubFormat
265 doc = get_object_or_404(Document, pk=pk)
266 rev = get_object_or_404(Revision, pk=rev_pk)
270 sst = SST.from_string(rev.materialize())
271 except ValueError as e:
272 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
275 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
276 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
278 if doc.owner_organization is not None and doc.owner_organization.logo:
279 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
281 epub_file = EpubFormat(sst).build(ctx)
282 except BuildError as e:
283 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
285 from catalogue.ebook_utils import serve_file
286 return serve_file(epub_file.get_filename(), '%d.epub' % doc.pk, 'application/epub+zip')
290 def book_mobi(request, pk, rev_pk):
291 from librarian.utils import Context
292 from librarian.document import Document as SST
293 from librarian.formats.epub import EpubFormat
295 doc = get_object_or_404(Document, pk=pk)
296 rev = get_object_or_404(Revision, pk=rev_pk)
299 sst = SST.from_string(rev.materialize())
300 except ValueError as e:
301 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
304 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
305 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
307 if doc.owner_organization is not None and doc.owner_organization.logo:
308 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
310 epub_file = EpubFormat(sst).build(ctx)
311 except BuildError as e:
312 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
314 output_file = NamedTemporaryFile(prefix='librarian', suffix='.mobi', delete=False)
316 subprocess.check_call(
317 ['ebook-convert', epub_file.get_filename(), output_file.name, '--no-inline-toc'])
319 from catalogue.ebook_utils import serve_file
320 return serve_file(output_file.name, '%d.mobi' % doc.pk, 'application/epub+zip')
324 # def revision(request, slug, chunk=None):
326 # doc = Chunk.get(slug, chunk)
327 # except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
329 # if not doc.book.accessible(request):
330 # return HttpResponseForbidden("Not authorized.")
331 # return http.HttpResponse(str(doc.revision()))
335 def book_schedule(request, pk):
336 book = get_object_or_404(Document, pk=pk, deleted=False)
337 if not book.can_edit(request.user):
338 return HttpResponseForbidden("Not authorized.")
339 if request.method == 'POST':
340 Plan.objects.filter(document=book).delete()
341 for i, (s, name) in enumerate(STAGES):
342 user_id = request.POST.get('s%d-user' % i)
343 deadline = request.POST.get('s%d-deadline' % i) or None
344 Plan.objects.create(document=book, stage=s, user_id=user_id, deadline=deadline)
346 book.set_stage(request.POST.get('stage', ''))
347 return redirect('catalogue_user')
350 for p in Plan.objects.filter(document=book):
351 current[p.stage] = (getattr(p.user, 'pk', None), (p.deadline.isoformat() if p.deadline else None))
353 schedule = [(i, s, current.get(s, ())) for i, (s, name) in enumerate(STAGES)]
355 if book.owner_organization:
356 people = [m.user for m in book.owner_organization.membership_set.exclude(status='pending')]
358 people = [book.owner_user]
359 return render(request, 'catalogue/book_schedule.html', {
361 'schedule': schedule,
367 def book_owner(request, pk):
368 doc = get_object_or_404(Document, pk=pk, deleted=False)
369 if not doc.can_edit(request.user):
370 return HttpResponseForbidden("Not authorized.")
374 if request.method == 'POST':
376 new_org_pk = request.POST.get('owner_organization')
378 doc.owner_organization = None
379 doc.owner_user = request.user
382 org = Organization.objects.get(pk=new_org_pk)
383 if not org.is_member(request.user):
384 error = 'Bad organization'
386 doc.owner_organization = org
387 doc.owner_user = None
390 return redirect('catalogue_user')
392 return render(request, 'catalogue/book_owner.html', {
399 def book_delete(request, pk):
400 doc = get_object_or_404(Document, pk=pk, deleted=False)
401 if not doc.can_edit(request.user):
402 return HttpResponseForbidden("Not authorized.")
404 if request.method == 'POST':
407 return redirect('catalogue_user')
409 return render(request, 'catalogue/book_delete.html', {
416 def publish(request, pk):
417 from wiki import forms
418 from .models import PublishRecord
419 from dvcs.models import Revision
421 doc = get_object_or_404(Document, pk=pk, deleted=False)
422 if not doc.can_edit(request.user):
423 return HttpResponseForbidden("Not authorized.")
424 form = forms.DocumentTextPublishForm(request.POST, prefix="textpublish")
426 rev = Revision.objects.get(pk=form.cleaned_data['revision'])
427 # FIXME: check if in tree
428 # if PublishRecord.objects.filter(revision=rev, document=doc).exists():
429 # return http.HttpResponse('exists')
430 if not doc.published:
431 site = Site.objects.get_current()
433 'New published document in MIL/PEER',
434 '''New published document in MIL/PEER: %s. View it in browser: https://%s%s.
437 MIL/PEER team.''' % (doc.meta()['title'], site.domain, reverse('catalogue_html', args=[doc.pk])))
438 PublishRecord.objects.create(revision=rev, document=doc, user=request.user)
441 if request.is_ajax():
442 return http.HttpResponse('ok')
444 return redirect('catalogue_html', doc.pk)
446 if request.is_ajax():
447 return http.HttpResponse('error')
450 return redirect('catalogue_preview_rev', doc.pk, form.cleaned_data['revision'])
452 return redirect('catalogue_preview', doc.pk)
457 def unpublish(request, pk):
458 doc = get_object_or_404(Document, pk=pk, deleted=False)
459 if not doc.can_edit(request.user):
460 return HttpResponseForbidden("Not authorized.")
462 doc.publish_log.all().delete()
463 if request.is_ajax():
464 return http.HttpResponse('ok')
466 return redirect('catalogue_html', doc.pk)
469 class GalleryMixin(object):
470 def get_directory(self):
471 # return "%s%s/" % (settings.IMAGE_DIR, 'org%d' % self.org.pk if self.org is not None else self.request.user.pk)
472 return "uploads/%d/" % self.doc.pk
475 class GalleryView(GalleryMixin, UploadView):
477 def breadcrumbs(self):
479 (self.doc.meta()['title'], '/documents/%d/' % self.doc.pk),
482 def get_object(self, request, pk=None):
483 self.doc = Document.objects.get(pk=pk, deleted=False)
487 def fork(request, pk):
488 doc = get_object_or_404(Document, pk=pk, deleted=False)
489 if request.method == "POST":
490 form = forms.DocumentForkForm(request.POST, request.FILES)
493 org = request.user.membership_set.get(
494 organization=int(form.cleaned_data['owner_organization'])).organization
495 kwargs = {'owner_organization': org}
497 kwargs = {'owner_user': request.user}
499 new_doc = Document.objects.create(revision=doc.revision, **kwargs)
501 if os.path.isdir(settings.MEDIA_ROOT + "uploads/%d" % doc.pk):
503 settings.MEDIA_ROOT + "uploads/%d" % doc.pk,
504 settings.MEDIA_ROOT + "uploads/%d" % new_doc.pk
507 new_doc.assigned_to = request.user
510 return http.HttpResponseRedirect(reverse("wiki_editor", args=[new_doc.pk]))
512 form = forms.DocumentForkForm()
514 return render(request, "catalogue/document_fork.html", {
521 def upcoming(request):
522 f = DocumentFilterSet(request.GET, queryset=Document.objects.filter(deleted=False).filter(publish_log=None))
523 return render(request, "catalogue/upcoming.html", {
525 'link_url': 'catalogue_preview',
529 def finished(request):
530 f = DocumentFilterSet(request.GET, queryset=Document.objects.filter(deleted=False).exclude(publish_log=None))
531 return render(request, "catalogue/finished.html", {
533 'link_url': 'catalogue_html',