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 for tag_form in tag_forms:
98 tag_form.save(instance=doc)
100 cover = request.FILES.get('cover')
102 uppath = 'uploads/%d/' % doc.pk
103 path = settings.MEDIA_ROOT + uppath
104 if not os.path.isdir(path):
106 cover.name = unidecode(cover.name)
107 dest_path = path + cover.name
108 if not os.path.abspath(dest_path).startswith(os.path.abspath(path)):
110 with open(dest_path, 'w') as destination:
111 for chunk in cover.chunks():
112 destination.write(chunk)
113 cover_url = 'http://milpeer.eu/media/dynamic/' + uppath + cover.name
118 text='''<section xmlns="http://nowoczesnapolska.org.pl/sst#" xmlns:dc="http://purl.org/dc/elements/1.1/">
120 <dc:publisher>''' + form.cleaned_data['publisher'] + '''</dc:publisher>
121 <dc:description>''' + form.cleaned_data['description'] + '''</dc:description>
122 ''' + '\n'.join(tag_form.metadata_rows() for tag_form in tag_forms) + '''
123 <dc:relation.coverImage.url>''' + cover_url + '''</dc:relation.coverImage.url>
125 <header>''' + title + '''</header>
126 <div class="p"> </div>
130 doc.assigned_to = request.user
133 return http.HttpResponseRedirect(reverse("wiki_editor", args=[doc.pk]))
135 org_pk = request.GET.get('organization')
138 org = Organization.objects.get(pk=org_pk)
139 except Organization.DoesNotExist:
142 if not org.is_member(request.user):
149 form = forms.DocumentCreateForm(initial={'owner_organization': org})
152 (TagMultipleForm if category.multiple else TagSingleForm)(
153 category=category, tutorial_no=i, prefix=category.dc_tag)
154 for i, category in enumerate(Category.objects.all(), start=2)]
156 return render(request, "catalogue/document_create_missing.html", {
158 "tag_forms": tag_forms,
165 def book_html(request, pk, rev_pk=None, preview=False):
166 from librarian.document import Document as SST
167 from librarian.formats.html import HtmlFormat
169 doc = get_object_or_404(Document, pk=pk, deleted=False)
172 published_revision = doc.publish_log.all()[0].revision
174 published_revision = None
178 revision = doc.revision
180 if published_revision is not None:
181 revision = published_revision
183 # No published version, fallback to preview mode.
185 revision = doc.revision
187 revision = get_object_or_404(Revision, pk=rev_pk)
189 was_published = revision == published_revision or doc.publish_log.filter(revision=revision).exists()
192 sst = SST.from_string(revision.materialize())
193 except ValueError as e:
196 html = HtmlFormat(sst).build(
197 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk)).get_string()
199 # response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
202 # for fragment in book.fragments.all().iterator():
203 # for theme in fragment.tags.filter(category='theme').iterator():
204 # book_themes.setdefault(theme, []).append(fragment)
206 # book_themes = book_themes.items()
207 # book_themes.sort(key=lambda s: s[0].sort_key)
208 return render(request, 'catalogue/book_text.html', {
211 'revision': revision,
212 'published_revision': published_revision,
213 'specific': rev_pk is not None,
215 'can_edit': doc.can_edit(request.user) if doc else None,
216 'was_published': was_published,
221 def book_pdf(request, pk, rev_pk):
222 from librarian.utils import Context
223 from librarian.document import Document as SST
224 from librarian.formats.pdf import PdfFormat
226 doc = get_object_or_404(Document, pk=pk)
227 rev = get_object_or_404(Revision, pk=rev_pk)
231 sst = SST.from_string(rev.materialize())
232 except ValueError as e:
233 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
236 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
237 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
239 if doc.owner_organization is not None and doc.owner_organization.logo:
240 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
242 pdf_file = PdfFormat(sst).build(ctx)
243 except BuildError as e:
244 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
246 from catalogue.ebook_utils import serve_file
247 return serve_file(pdf_file.get_filename(), '%d.pdf' % doc.pk, 'application/pdf')
251 def book_epub(request, pk, rev_pk):
252 from librarian.utils import Context
253 from librarian.document import Document as SST
254 from librarian.formats.epub import EpubFormat
256 doc = get_object_or_404(Document, pk=pk)
257 rev = get_object_or_404(Revision, pk=rev_pk)
261 sst = SST.from_string(rev.materialize())
262 except ValueError as e:
263 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
266 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
267 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
269 if doc.owner_organization is not None and doc.owner_organization.logo:
270 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
272 epub_file = EpubFormat(sst).build(ctx)
273 except BuildError as e:
274 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
276 from catalogue.ebook_utils import serve_file
277 return serve_file(epub_file.get_filename(), '%d.epub' % doc.pk, 'application/epub+zip')
281 def book_mobi(request, pk, rev_pk):
282 from librarian.utils import Context
283 from librarian.document import Document as SST
284 from librarian.formats.epub import EpubFormat
286 doc = get_object_or_404(Document, pk=pk)
287 rev = get_object_or_404(Revision, pk=rev_pk)
290 sst = SST.from_string(rev.materialize())
291 except ValueError as e:
292 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
295 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
296 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
298 if doc.owner_organization is not None and doc.owner_organization.logo:
299 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
301 epub_file = EpubFormat(sst).build(ctx)
302 except BuildError as e:
303 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
305 output_file = NamedTemporaryFile(prefix='librarian', suffix='.mobi', delete=False)
307 subprocess.check_call(
308 ['ebook-convert', epub_file.get_filename(), output_file.name, '--no-inline-toc'])
310 from catalogue.ebook_utils import serve_file
311 return serve_file(output_file.name, '%d.mobi' % doc.pk, 'application/epub+zip')
315 # def revision(request, slug, chunk=None):
317 # doc = Chunk.get(slug, chunk)
318 # except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
320 # if not doc.book.accessible(request):
321 # return HttpResponseForbidden("Not authorized.")
322 # return http.HttpResponse(str(doc.revision()))
326 def book_schedule(request, pk):
327 book = get_object_or_404(Document, pk=pk, deleted=False)
328 if not book.can_edit(request.user):
329 return HttpResponseForbidden("Not authorized.")
330 if request.method == 'POST':
331 Plan.objects.filter(document=book).delete()
332 for i, (s, name) in enumerate(STAGES):
333 user_id = request.POST.get('s%d-user' % i)
334 deadline = request.POST.get('s%d-deadline' % i) or None
335 Plan.objects.create(document=book, stage=s, user_id=user_id, deadline=deadline)
337 book.set_stage(request.POST.get('stage', ''))
338 return redirect('catalogue_user')
341 for p in Plan.objects.filter(document=book):
342 current[p.stage] = (getattr(p.user, 'pk', None), (p.deadline.isoformat() if p.deadline else None))
344 schedule = [(i, s, current.get(s, ())) for i, (s, name) in enumerate(STAGES)]
346 if book.owner_organization:
347 people = [m.user for m in book.owner_organization.membership_set.exclude(status='pending')]
349 people = [book.owner_user]
350 return render(request, 'catalogue/book_schedule.html', {
352 'schedule': schedule,
358 def book_owner(request, pk):
359 doc = get_object_or_404(Document, pk=pk, deleted=False)
360 if not doc.can_edit(request.user):
361 return HttpResponseForbidden("Not authorized.")
365 if request.method == 'POST':
367 new_org_pk = request.POST.get('owner_organization')
369 doc.owner_organization = None
370 doc.owner_user = request.user
373 org = Organization.objects.get(pk=new_org_pk)
374 if not org.is_member(request.user):
375 error = 'Bad organization'
377 doc.owner_organization = org
378 doc.owner_user = None
381 return redirect('catalogue_user')
383 return render(request, 'catalogue/book_owner.html', {
390 def book_delete(request, pk):
391 doc = get_object_or_404(Document, pk=pk, deleted=False)
392 if not doc.can_edit(request.user):
393 return HttpResponseForbidden("Not authorized.")
395 if request.method == 'POST':
398 return redirect('catalogue_user')
400 return render(request, 'catalogue/book_delete.html', {
407 def publish(request, pk):
408 from wiki import forms
409 from .models import PublishRecord
410 from dvcs.models import Revision
412 doc = get_object_or_404(Document, pk=pk, deleted=False)
413 if not doc.can_edit(request.user):
414 return HttpResponseForbidden("Not authorized.")
415 form = forms.DocumentTextPublishForm(request.POST, prefix="textpublish")
417 rev = Revision.objects.get(pk=form.cleaned_data['revision'])
418 # FIXME: check if in tree
419 # if PublishRecord.objects.filter(revision=rev, document=doc).exists():
420 # return http.HttpResponse('exists')
421 if not doc.published:
422 site = Site.objects.get_current()
424 'New published document in MIL/PEER',
425 '''New published document in MIL/PEER: %s. View it in browser: https://%s%s.
428 MIL/PEER team.''' % (doc.meta()['title'], site.domain, reverse('catalogue_html', args=[doc.pk])))
429 PublishRecord.objects.create(revision=rev, document=doc, user=request.user)
432 if request.is_ajax():
433 return http.HttpResponse('ok')
435 return redirect('catalogue_html', doc.pk)
437 if request.is_ajax():
438 return http.HttpResponse('error')
441 return redirect('catalogue_preview_rev', doc.pk, form.cleaned_data['revision'])
443 return redirect('catalogue_preview', doc.pk)
448 def unpublish(request, pk):
449 doc = get_object_or_404(Document, pk=pk, deleted=False)
450 if not doc.can_edit(request.user):
451 return HttpResponseForbidden("Not authorized.")
453 doc.publish_log.all().delete()
454 if request.is_ajax():
455 return http.HttpResponse('ok')
457 return redirect('catalogue_html', doc.pk)
460 class GalleryMixin(object):
461 def get_directory(self):
462 # return "%s%s/" % (settings.IMAGE_DIR, 'org%d' % self.org.pk if self.org is not None else self.request.user.pk)
463 return "uploads/%d/" % self.doc.pk
466 class GalleryView(GalleryMixin, UploadView):
468 def breadcrumbs(self):
470 (self.doc.meta()['title'], '/documents/%d/' % self.doc.pk),
473 def get_object(self, request, pk=None):
474 self.doc = Document.objects.get(pk=pk, deleted=False)
478 def fork(request, pk):
479 doc = get_object_or_404(Document, pk=pk, deleted=False)
480 if request.method == "POST":
481 form = forms.DocumentForkForm(request.POST, request.FILES)
484 org = request.user.membership_set.get(
485 organization=int(form.cleaned_data['owner_organization'])).organization
486 kwargs = {'owner_organization': org}
488 kwargs = {'owner_user': request.user}
490 new_doc = Document.objects.create(revision=doc.revision, **kwargs)
492 if os.path.isdir(settings.MEDIA_ROOT + "uploads/%d" % doc.pk):
494 settings.MEDIA_ROOT + "uploads/%d" % doc.pk,
495 settings.MEDIA_ROOT + "uploads/%d" % new_doc.pk
498 new_doc.assigned_to = request.user
501 return http.HttpResponseRedirect(reverse("wiki_editor", args=[new_doc.pk]))
503 form = forms.DocumentForkForm()
505 return render(request, "catalogue/document_fork.html", {
512 def upcoming(request):
513 return render(request, "catalogue/upcoming.html", {
514 'objects_list': Document.objects.filter(deleted=False).filter(publish_log=None),
518 def finished(request):
519 return render(request, "catalogue/finished.html", {
520 'objects_list': Document.objects.filter(deleted=False).exclude(publish_log=None),