1 # -*- coding: utf-8 -*-
2 from datetime import datetime, date, timedelta
6 from StringIO import StringIO
7 from urllib import unquote
8 from urlparse import urlsplit, urlunsplit
10 from django.conf import settings
11 from django.contrib import auth
12 from django.contrib.auth.models import User
13 from django.contrib.auth.decorators import login_required, permission_required
14 from django.core.urlresolvers import reverse
15 from django.db.models import Count, Q
16 from django.db import transaction
17 from django import http
18 from django.http import Http404, HttpResponse, HttpResponseForbidden
19 from django.shortcuts import get_object_or_404, render, render_to_response, redirect
20 from django.utils.encoding import iri_to_uri
21 from django.utils.http import urlquote_plus
22 from django.utils.translation import ugettext_lazy as _
23 from django.views.decorators.http import require_POST
24 from django.template import RequestContext
26 from catalogue import forms
27 from catalogue import helpers
28 from catalogue.helpers import active_tab, sstdocument
29 from .constants import STAGES
30 from .models import Document, Plan
31 from dvcs.models import Revision
32 from organizations.models import Organization
33 from fileupload.views import UploadView, PackageView
36 # Quick hack around caching problems, TODO: use ETags
38 from django.views.decorators.cache import never_cache
39 #from fnpdjango.utils.text.slughifi import slughifi
41 logger = logging.getLogger("fnp.catalogue")
46 def document_list(request):
47 return render(request, 'catalogue/document_list.html')
51 def user(request, username):
52 user = get_object_or_404(User, username=username)
53 return render(request, 'catalogue/user_page.html', {"viewed_user": user})
60 return render(request, 'catalogue/my_page.html', {
61 'last_books': sorted(request.session.get("wiki_last_books", {}).items(),
62 key=lambda x: x[1]['time'], reverse=True),
70 return render(request, 'catalogue/user_list.html', {
71 'users': User.objects.all().annotate(count=Count('chunk')).order_by(
72 '-count', 'last_name', 'first_name'),
76 @active_tab('activity')
77 def activity(request, isodate=None):
80 day = helpers.parse_isodate(isodate)
87 next_day = day + timedelta(1)
88 prev_day = day - timedelta(1)
90 return render(request, 'catalogue/activity.html', locals())
94 def logout_then_redirect(request):
96 return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
99 #@permission_required('catalogue.add_book')
101 @active_tab('create')
102 def create_missing(request):
103 if request.method == "POST":
104 form = forms.DocumentCreateForm(request.POST, request.FILES)
107 if request.user.is_authenticated():
108 creator = request.user
112 title = form.cleaned_data['title']
114 org = request.user.membership_set.get(
115 organization=int(form.cleaned_data['owner_organization'])).organization
116 kwargs = {'owner_organization': org}
118 kwargs = {'owner_user': request.user}
120 doc = Document.objects.create(**kwargs)
122 cover = request.FILES.get('cover')
124 uppath = 'uploads/%d/' % doc.pk
125 path = settings.MEDIA_ROOT + uppath
126 if not os.path.isdir(path):
128 dest_path = path + cover.name # UNSAFE
129 with open(dest_path, 'w') as destination:
130 for chunk in cover.chunks():
131 destination.write(chunk)
132 cover_url = 'http://milpeer.eu/media/dynamic/' + uppath + cover.name
137 text = '''<section xmlns="http://nowoczesnapolska.org.pl/sst#" xmlns:dc="http://purl.org/dc/elements/1.1/">
139 <dc:publisher>''' + form.cleaned_data['publisher'] + '''</dc:publisher>
140 <dc:description>''' + form.cleaned_data['description'] + '''</dc:description>
141 <dc:language>''' + form.cleaned_data['language'] + '''</dc:language>
142 <dc:rights>''' + form.cleaned_data['rights'] + '''</dc:rights>
143 <dc:audience>''' + form.cleaned_data['audience'] + '''</dc:audience>
144 <dc:relation.coverImage.url>''' + cover_url + '''</dc:relation.coverImage.url>
146 <header>''' + title + '''</header>
147 <div class="p"> </div>
151 doc.assigned_to = request.user
154 return http.HttpResponseRedirect(reverse("wiki_editor", args=[doc.pk]))
156 org_pk = request.GET.get('organization')
159 org = Organization.objects.get(pk=org_pk)
160 except Organization.DoesNotExist:
163 if not org.is_member(request.user):
170 form = forms.DocumentCreateForm(initial={'owner_organization': org})
172 return render(request, "catalogue/document_create_missing.html", {
179 @permission_required('catalogue.add_book')
180 @active_tab('upload')
182 if request.method == "POST":
183 form = forms.DocumentsUploadForm(request.POST, request.FILES)
187 if request.user.is_authenticated():
188 creator = request.user
192 zip = form.cleaned_data['zip']
197 existing = [book.slug for book in Book.objects.all()]
198 for filename in zip.namelist():
199 if filename[-1] == '/':
201 title = os.path.basename(filename)[:-4]
202 slug = slughifi(title)
203 if not (slug and filename.endswith('.xml')):
204 skipped_list.append(filename)
206 error_list.append((filename, slug, _('Slug already used for %s' % slugs[slug])))
207 elif slug in existing:
208 error_list.append((filename, slug, _('Slug already used in repository.')))
211 zip.read(filename).decode('utf-8') # test read
212 ok_list.append((filename, slug, title))
213 except UnicodeDecodeError:
214 error_list.append((filename, title, _('File should be UTF-8 encoded.')))
215 slugs[slug] = filename
218 for filename, slug, title in ok_list:
220 text=zip.read(filename).decode('utf-8'),
226 return render(request, "catalogue/document_upload.html", {
229 "skipped_list": skipped_list,
230 "error_list": error_list,
235 form = forms.DocumentsUploadForm()
237 return render(request, "catalogue/document_upload.html", {
245 def book_xml(request, slug):
246 book = get_object_or_404(Book, slug=slug)
247 if not book.accessible(request):
248 return HttpResponseForbidden("Not authorized.")
249 xml = book.materialize()
251 response = http.HttpResponse(xml, content_type='application/xml', mimetype='application/wl+xml')
252 response['Content-Disposition'] = 'attachment; filename=%s.xml' % slug
257 def book_txt(request, slug):
258 book = get_object_or_404(Book, slug=slug)
259 if not book.accessible(request):
260 return HttpResponseForbidden("Not authorized.")
262 doc = book.wldocument()
263 text = doc.as_text().get_string()
264 response = http.HttpResponse(text, content_type='text/plain', mimetype='text/plain')
265 response['Content-Disposition'] = 'attachment; filename=%s.txt' % slug
270 def book_html(request, pk, rev_pk=None, preview=False):
271 from librarian.document import Document as SST
272 from librarian.formats.html import HtmlFormat
274 doc = get_object_or_404(Document, pk=pk, deleted=False)
277 published_revision = doc.publish_log.all()[0].revision
279 published_revision = None
283 revision = doc.revision
285 if published_revision is not None:
286 revision = published_revision
288 # No published version, fallback to preview mode.
290 revision = doc.revision
292 revision = get_object_or_404(Revision, pk=rev_pk)
294 was_published = revision == published_revision or doc.publish_log.filter(revision=revision).exists()
296 sst = SST.from_string(revision.materialize())
297 html = HtmlFormat(sst).build(files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk)).get_string()
299 # response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
302 # for fragment in book.fragments.all().iterator():
303 # for theme in fragment.tags.filter(category='theme').iterator():
304 # book_themes.setdefault(theme, []).append(fragment)
306 # book_themes = book_themes.items()
307 # book_themes.sort(key=lambda s: s[0].sort_key)
308 return render(request, 'catalogue/book_text.html', {
311 'revision': revision,
312 'published_revision': published_revision,
313 'specific': rev_pk is not None,
315 'can_edit': doc.can_edit(request.user) if doc else None,
316 'was_published': was_published,
321 def book_pdf(request, pk, rev_pk):
322 from librarian.utils import Context
323 from librarian.document import Document as SST
324 from librarian.formats.pdf import PdfFormat
326 doc = get_object_or_404(Document, pk=pk)
327 rev = get_object_or_404(Revision, pk=rev_pk)
330 sst = SST.from_string(rev.materialize())
333 files_path = 'http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
334 source_url = 'http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
336 if doc.owner_organization is not None and doc.owner_organization.logo:
337 ctx.cover_logo = 'http://%s%s' %(request.get_host(), doc.owner_organization.logo.url)
338 pdf_file = PdfFormat(sst).build(ctx)
340 from catalogue.ebook_utils import serve_file
341 return serve_file(pdf_file.get_filename(),
342 '%d.pdf' % doc.pk, 'application/pdf')
346 def book_epub(request, slug):
347 book = get_object_or_404(Book, slug=slug)
348 if not book.accessible(request):
349 return HttpResponseForbidden("Not authorized.")
351 # TODO: move to celery
352 doc = book.wldocument()
353 # TODO: error handling
354 epub = doc.as_epub().get_string()
355 response = HttpResponse(mimetype='application/epub+zip')
356 response['Content-Disposition'] = 'attachment; filename=%s' % book.slug + '.epub'
362 def revision(request, slug, chunk=None):
364 doc = Chunk.get(slug, chunk)
365 except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
367 if not doc.book.accessible(request):
368 return HttpResponseForbidden("Not authorized.")
369 return http.HttpResponse(str(doc.revision()))
373 def book_schedule(request, pk):
374 book = get_object_or_404(Document, pk=pk, deleted=False)
375 if request.method == 'POST':
376 Plan.objects.filter(document=book).delete()
377 for i, s in enumerate(STAGES):
378 user_id = request.POST.get('s%d-user' % i)
379 deadline = request.POST.get('s%d-deadline' % i) or None
380 Plan.objects.create(document=book, stage=s, user_id=user_id, deadline=deadline)
382 book.set_stage(request.POST.get('stage', ''))
383 return redirect('catalogue_user')
386 for p in Plan.objects.filter(document=book):
387 current[p.stage] = (getattr(p.user, 'pk', None), (p.deadline.isoformat() if p.deadline else None))
389 schedule = [(i, s, current.get(s, ())) for (i, s) in enumerate(STAGES)]
391 if book.owner_organization:
392 people = [m.user for m in book.owner_organization.membership_set.exclude(status='pending')]
394 people = [book.owner_user]
395 return render(request, 'catalogue/book_schedule.html', {
397 'schedule': schedule,
403 def book_owner(request, pk):
404 doc = get_object_or_404(Document, pk=pk, deleted=False)
405 if not (doc.owner_user == request.user or doc.owner_organization.is_member(request.user)):
410 if request.method == 'POST':
412 new_org_pk = request.POST.get('owner_organization')
414 doc.owner_organization = None
415 doc.owner_user = request.user
418 org = Organization.objects.get(pk=new_org_pk)
419 if not org.is_member(request.user):
420 error = 'Bad organization'
422 doc.owner_organization = org
423 doc.owner_user = None
426 return redirect('catalogue_user')
428 return render(request, 'catalogue/book_owner.html', {
435 def book_delete(request, pk):
436 doc = get_object_or_404(Document, pk=pk, deleted=False)
437 if not (doc.owner_user == request.user or doc.owner_organization.is_member(request.user)):
440 if request.method == 'POST':
443 return redirect('catalogue_user')
445 return render(request, 'catalogue/book_delete.html', {
451 def book(request, slug):
452 book = get_object_or_404(Book, slug=slug)
453 if not book.accessible(request):
454 return HttpResponseForbidden("Not authorized.")
456 if request.user.has_perm('catalogue.change_book'):
457 if request.method == "POST":
458 form = forms.BookForm(request.POST, instance=book)
461 return http.HttpResponseRedirect(book.get_absolute_url())
463 form = forms.BookForm(instance=book)
466 form = forms.ReadonlyBookForm(instance=book)
469 publish_error = book.publishable_error()
470 publishable = publish_error is None
472 return render(request, "catalogue/book_detail.html", {
474 "publishable": publishable,
475 "publishable_error": publish_error,
477 "editable": editable,
481 @permission_required('catalogue.add_chunk')
482 def chunk_add(request, slug, chunk):
484 doc = Chunk.get(slug, chunk)
485 except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
487 if not doc.book.accessible(request):
488 return HttpResponseForbidden("Not authorized.")
490 if request.method == "POST":
491 form = forms.ChunkAddForm(request.POST, instance=doc)
493 if request.user.is_authenticated():
494 creator = request.user
497 doc.split(creator=creator,
498 slug=form.cleaned_data['slug'],
499 title=form.cleaned_data['title'],
500 gallery_start=form.cleaned_data['gallery_start'],
501 user=form.cleaned_data['user'],
502 stage=form.cleaned_data['stage']
505 return http.HttpResponseRedirect(doc.book.get_absolute_url())
507 form = forms.ChunkAddForm(initial={
508 "slug": str(doc.number + 1),
509 "title": "cz. %d" % (doc.number + 1, ),
512 return render(request, "catalogue/chunk_add.html", {
519 def chunk_edit(request, slug, chunk):
521 doc = Chunk.get(slug, chunk)
522 except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
524 if not doc.book.accessible(request):
525 return HttpResponseForbidden("Not authorized.")
527 if request.method == "POST":
528 form = forms.ChunkForm(request.POST, instance=doc)
531 go_next = request.GET.get('next', None)
533 go_next = urlquote_plus(unquote(iri_to_uri(go_next)), safe='/?=&')
535 go_next = doc.book.get_absolute_url()
536 return http.HttpResponseRedirect(go_next)
538 form = forms.ChunkForm(instance=doc)
540 referer = request.META.get('HTTP_REFERER')
542 parts = urlsplit(referer)
543 parts = ['', ''] + list(parts[2:])
544 go_next = urlquote_plus(urlunsplit(parts))
548 return render(request, "catalogue/chunk_edit.html", {
557 def chunk_mass_edit(request):
558 if request.method == 'POST':
559 ids = map(int, filter(lambda i: i.strip()!='', request.POST.get('ids').split(',')))
560 chunks = map(lambda i: Chunk.objects.get(id=i), ids)
562 stage = request.POST.get('stage')
565 stage = Chunk.tag_model.objects.get(slug=stage)
566 except Chunk.DoesNotExist, e:
569 for c in chunks: c.stage = stage
571 username = request.POST.get('user')
572 logger.info("username: %s" % username)
573 logger.info(request.POST)
576 user = User.objects.get(username=username)
577 except User.DoesNotExist, e:
580 for c in chunks: c.user = user
582 status = request.POST.get('status')
584 books_affected = set()
586 if status == 'publish':
587 c.head.publishable = True
589 elif status == 'unpublish':
590 c.head.publishable = False
593 books_affected.add(c.book)
594 for b in books_affected:
597 project_id = request.POST.get('project')
600 project = Project.objects.get(pk=int(project_id))
601 except (Project.DoesNotExist, ValueError), e:
605 book.project = project
608 for c in chunks: c.save()
610 return HttpResponse("", content_type="text/plain")
615 @permission_required('catalogue.change_book')
616 def book_append(request, slug):
617 book = get_object_or_404(Book, slug=slug)
618 if not book.accessible(request):
619 return HttpResponseForbidden("Not authorized.")
621 if request.method == "POST":
622 form = forms.BookAppendForm(book, request.POST)
624 append_to = form.cleaned_data['append_to']
625 append_to.append(book)
626 return http.HttpResponseRedirect(append_to.get_absolute_url())
628 form = forms.BookAppendForm(book)
629 return render(request, "catalogue/book_append_to.html", {
639 def publish(request, pk):
640 from wiki import forms
641 from .models import PublishRecord
642 from dvcs.models import Revision
644 # FIXME: check permissions
646 doc = get_object_or_404(Document, pk=pk, deleted=False)
647 form = forms.DocumentTextPublishForm(request.POST, prefix="textpublish")
649 rev = Revision.objects.get(pk=form.cleaned_data['revision'])
650 # FIXME: check if in tree
651 #if PublishRecord.objects.filter(revision=rev, document=doc).exists():
652 # return http.HttpResponse('exists')
653 pr = PublishRecord.objects.create(revision=rev, document=doc, user=request.user)
654 if request.is_ajax():
655 return http.HttpResponse('ok')
657 return redirect('catalogue_html', doc.pk)
659 if request.is_ajax():
660 return http.HttpResponse('error')
663 return redirect('catalogue_preview_rev', doc.pk, form.cleaned_data['revision'])
665 return redirect('catalogue_preview', doc.pk)
670 def unpublish(request, pk):
671 from wiki import forms
672 from .models import PublishRecord
673 from dvcs.models import Revision
675 # FIXME: check permissions
677 doc = get_object_or_404(Document, pk=pk, deleted=False)
678 doc.publish_log.all().delete()
679 if request.is_ajax():
680 return http.HttpResponse('ok')
682 return redirect('catalogue_html', doc.pk)
686 class GalleryMixin(object):
687 def get_directory(self):
688 #return "%s%s/" % (settings.IMAGE_DIR, 'org%d' % self.org.pk if self.org is not None else self.request.user.pk)
689 return "uploads/%d/" % (self.doc.pk)
692 class GalleryView(GalleryMixin, UploadView):
694 def breadcrumbs(self):
696 (self.doc.meta()['title'], '/documents/%d/' % self.doc.pk),
699 def get_object(self, request, pk=None):
700 self.doc = Document.objects.get(pk=pk, deleted=False)
703 class GalleryPackageView(GalleryMixin, PackageView):
705 def get_redirect_url(self, slug):
706 return reverse('catalogue_book_gallery', kwargs = dict(slug=slug))
709 def fork(request, pk):
710 doc = get_object_or_404(Document, pk=pk, deleted=False)
711 if request.method == "POST":
712 form = forms.DocumentForkForm(request.POST, request.FILES)
715 if request.user.is_authenticated():
716 creator = request.user
721 org = request.user.membership_set.get(
722 organization=int(form.cleaned_data['owner_organization'])).organization
723 kwargs = {'owner_organization': org}
725 kwargs = {'owner_user': request.user}
727 new_doc = Document.objects.create(revision=doc.revision, **kwargs)
729 if os.path.isdir(settings.MEDIA_ROOT + "uploads/%d" % (doc.pk)):
731 settings.MEDIA_ROOT + "uploads/%d" % (doc.pk),
732 settings.MEDIA_ROOT + "uploads/%d" % (new_doc.pk)
735 new_doc.assigned_to = request.user
738 return http.HttpResponseRedirect(reverse("wiki_editor", args=[new_doc.pk]))
740 form = forms.DocumentForkForm()
742 return render(request, "catalogue/document_fork.html", {
749 def upcoming(request):
750 return render(request, "catalogue/upcoming.html", {
751 'objects_list': Document.objects.filter(deleted=False).filter(publish_log=None),
754 def finished(request):
755 return render(request, "catalogue/finished.html", {
756 'objects_list': Document.objects.filter(deleted=False).exclude(publish_log=None),