1 # -*- encoding: utf-8 -*-
5 log = logging.getLogger('platforma.api.library')
7 __author__= "Łukasz Rekucki"
8 __date__ = "$2009-09-25 15:49:50$"
9 __doc__ = "Module documentation."
11 from piston.handler import BaseHandler, AnonymousBaseHandler
12 from django.http import HttpResponse
14 from datetime import date
16 from django.core.urlresolvers import reverse
17 from django.db import IntegrityError
22 from librarian import dcparser, parser
25 from api.models import PullRequest
26 from explorer.models import GalleryForDocument
29 import api.forms as forms
30 import api.response as response
31 from api.utils import validate_form, hglibrary, natural_order
32 from api.models import PartCache, PullRequest
34 from pygments import highlight
35 from pygments.lexers import DiffLexer
36 from pygments.formatters import HtmlFormatter
43 return username.startswith('$prq-')
45 def prq_for_user(username):
47 return PullRequest.objects.get(id=int(username[5:]))
51 def check_user(request, user):
52 log.info("user: %r, perm: %r" % (request.user, request.user.get_all_permissions()) )
55 if not request.user.has_perm('api.view_prq'):
56 yield response.AccessDenied().django_response({
57 'reason': 'access-denied',
58 'message': "You don't have enough priviliges to view pull requests."
61 elif request.user.username != user:
62 if not request.user.has_perm('api.view_other_document'):
63 yield response.AccessDenied().django_response({
64 'reason': 'access-denied',
65 'message': "You don't have enough priviliges to view other people's document."
70 # Document List Handlers
72 # TODO: security check
73 class BasicLibraryHandler(AnonymousBaseHandler):
74 allowed_methods = ('GET',)
77 def read(self, request, lib):
78 """Return the list of documents."""
80 'url': reverse('document_view', args=[docid]),
81 'name': docid } for docid in lib.documents() ]
82 return {'documents' : document_list}
85 # This handler controlls the document collection
87 class LibraryHandler(BaseHandler):
88 allowed_methods = ('GET', 'POST')
89 anonymous = BasicLibraryHandler
92 def read(self, request, lib):
93 """Return the list of documents."""
97 for docid in lib.documents():
99 'url': reverse('document_view', args=[docid]),
104 parts = PartCache.objects.defer('part_id')\
105 .values_list('part_id', 'document_id').distinct()
107 document_tree = dict(documents)
109 for part, docid in parts:
110 # this way, we won't display broken links
111 if not documents.has_key(part):
112 log.info("NOT FOUND: %s", part)
115 parent = documents[docid]
116 child = documents[part]
118 # not top-level anymore
119 document_tree.pop(part)
120 parent['parts'].append(child)
122 for doc in documents.itervalues():
123 doc['parts'].sort(key=natural_order(lambda d: d['name']))
125 return {'documents': sorted(document_tree.itervalues(),
126 key=natural_order(lambda d: d['name']) ) }
129 @validate_form(forms.DocumentUploadForm, 'POST')
131 def create(self, request, form, lib):
132 """Create a new document."""
134 if form.cleaned_data['ocr_data']:
135 data = form.cleaned_data['ocr_data']
137 data = request.FILES['ocr_file'].read().decode('utf-8')
140 return response.BadRequest().django_response('You must pass ocr_data or ocr_file.')
142 if form.cleaned_data['generate_dc']:
143 data = librarian.wrap_text(data, unicode(date.today()))
145 docid = form.cleaned_data['bookname']
150 log.info("DOCID %s", docid)
151 doc = lib.document_create(docid)
152 # document created, but no content yet
154 doc = doc.quickwrite('xml', data.encode('utf-8'),
155 '$AUTO$ XML data uploaded.', user=request.user.username)
158 # rollback branch creation
160 raise LibraryException(traceback.format_exc())
162 url = reverse('document_view', args=[doc.id])
164 return response.EntityCreated().django_response(\
168 'revision': doc.revision },
172 except LibraryException, e:
174 return response.InternalError().django_response({
175 "reason": traceback.format_exc()
177 except DocumentAlreadyExists:
178 # Document is already there
179 return response.EntityConflict().django_response({
180 "reason": "already-exists",
181 "message": "Document already exists." % docid
187 class BasicDocumentHandler(AnonymousBaseHandler):
188 allowed_methods = ('GET',)
191 def read(self, request, docid, lib):
193 doc = lib.document(docid)
194 except RevisionNotFound:
199 'html_url': reverse('dochtml_view', args=[doc.id]),
200 'text_url': reverse('doctext_view', args=[doc.id]),
201 'dc_url': reverse('docdc_view', args=[doc.id]),
202 'public_revision': doc.revision,
208 class DiffHandler(BaseHandler):
209 allowed_methods = ('GET',)
212 def read(self, request, docid, lib):
213 '''Return diff between source_revision and target_revision)'''
214 revision = request.GET.get('revision')
217 source_document = lib.document(docid)
218 target_document = lib.document_for_revision(revision)
219 print source_document, target_document
221 diff = difflib.unified_diff(
222 source_document.data('xml').splitlines(True),
223 target_document.data('xml').splitlines(True),
227 s = ''.join(list(diff))
228 return highlight(s, DiffLexer(), HtmlFormatter(cssclass="pastie"))
234 class DocumentHandler(BaseHandler):
235 allowed_methods = ('GET', 'PUT')
236 anonymous = BasicDocumentHandler
238 @validate_form(forms.DocumentRetrieveForm, 'GET')
240 def read(self, request, form, docid, lib):
241 """Read document's meta data"""
242 log.info(u"User '%s' wants to edit %s(%s) as %s" % \
243 (request.user.username, docid, form.cleaned_data['revision'], form.cleaned_data['user']) )
245 user = form.cleaned_data['user'] or request.user.username
246 rev = form.cleaned_data['revision'] or 'latest'
248 for error in check_user(request, user):
252 doc = lib.document(docid, user, rev=rev)
253 except RevisionMismatch, e:
254 # the document exists, but the revision is bad
255 return response.EntityNotFound().django_response({
256 'reason': 'revision-mismatch',
257 'message': e.message,
261 except RevisionNotFound, e:
262 # the user doesn't have this document checked out
263 # or some other weird error occured
264 # try to do the checkout
266 if user == request.user.username:
267 mdoc = lib.document(docid)
268 doc = mdoc.take(user)
270 prq = prq_for_user(user)
271 # commiter's document
272 prq_doc = lib.document_for_revision(prq.source_revision)
273 doc = prq_doc.take(user)
275 return response.EntityNotFound().django_response({
276 'reason': 'document-not-found',
277 'message': e.message,
281 except RevisionNotFound, e:
282 return response.EntityNotFound().django_response({
283 'reason': 'document-not-found',
284 'message': e.message,
292 'html_url': reverse('dochtml_view', args=[doc.id]),
293 'text_url': reverse('doctext_view', args=[doc.id]),
294 # 'dc_url': reverse('docdc_view', args=[doc.id]),
295 'gallery_url': reverse('docgallery_view', args=[doc.id]),
296 'merge_url': reverse('docmerge_view', args=[doc.id]),
297 'revision': doc.revision,
298 'timestamp': doc.revision.timestamp,
299 # 'public_revision': doc.revision,
300 # 'public_timestamp': doc.revision.timestamp,
305 # def update(self, request, docid, lib):
306 # """Update information about the document, like display not"""
311 class DocumentHTMLHandler(BaseHandler):
312 allowed_methods = ('GET')
314 @validate_form(forms.DocumentRetrieveForm, 'GET')
316 def read(self, request, form, docid, lib, stylesheet='full'):
317 """Read document as html text"""
319 revision = form.cleaned_data['revision']
320 user = form.cleaned_data['user'] or request.user.username
321 document = lib.document_for_revision(revision)
323 if document.id != docid:
324 return response.BadRequest().django_response({
325 'reason': 'name-mismatch',
326 'message': 'Provided revision is not valid for this document'
329 if document.owner != user:
330 return response.BadRequest().django_response({
331 'reason': 'user-mismatch',
332 'message': "Provided revision doesn't belong to user %s" % user
335 for error in check_user(request, user):
338 return librarian.html.transform(document.data('xml'), is_file=False, \
339 parse_dublincore=False, stylesheet=stylesheet,\
341 "with-paths": 'boolean(1)',
344 except (EntryNotFound, RevisionNotFound), e:
345 return response.EntityNotFound().django_response({
346 'reason': 'not-found', 'message': e.message})
347 except librarian.ValidationError, e:
348 return response.InternalError().django_response({
349 'reason': 'xml-non-valid', 'message': e.message or u''})
350 except librarian.ParseError, e:
351 return response.InternalError().django_response({
352 'reason': 'xml-parse-error', 'message': e.message or u'' })
358 class DocumentGalleryHandler(BaseHandler):
359 allowed_methods = ('GET')
362 def read(self, request, docid):
363 """Read meta-data about scans for gallery of this document."""
365 from urllib import quote
367 for assoc in GalleryForDocument.objects.filter(document=docid):
368 dirpath = os.path.join(settings.MEDIA_ROOT, assoc.subpath)
370 if not os.path.isdir(dirpath):
371 log.warn(u"[WARNING]: missing gallery %s", dirpath)
374 gallery = {'name': assoc.name, 'pages': []}
376 for file in os.listdir(dirpath):
377 if not isinstance(file, unicode):
379 file = file.decode('utf-8')
381 log.warn(u"File %r in gallery %r is not unicode. Ommiting."\
386 name, ext = os.path.splitext(os.path.basename(file))
388 if ext.lower() not in [u'.png', u'.jpeg', u'.jpg']:
389 log.warn(u"Ignoring: %s %s", name, ext)
392 url = settings.MEDIA_URL + assoc.subpath + u'/' + file
395 url = settings.MEDIA_URL + u'/missing.png'
397 gallery['pages'].append( quote(url.encode('utf-8')) )
399 gallery['pages'].sort()
400 galleries.append(gallery)
407 # Dublin Core handlers
409 # @requires librarian
411 #class DocumentDublinCoreHandler(BaseHandler):
412 # allowed_methods = ('GET', 'POST')
415 # def read(self, request, docid, lib):
416 # """Read document as raw text"""
418 # revision = request.GET.get('revision', 'latest')
420 # if revision == 'latest':
421 # doc = lib.document(docid)
423 # doc = lib.document_for_revision(revision)
426 # if document.id != docid:
427 # return response.BadRequest().django_response({'reason': 'name-mismatch',
428 # 'message': 'Provided revision is not valid for this document'})
430 # bookinfo = dcparser.BookInfo.from_string(doc.data('xml'))
431 # return bookinfo.serialize()
432 # except (EntryNotFound, RevisionNotFound), e:
433 # return response.EntityNotFound().django_response({
434 # 'exception': type(e), 'message': e.message})
437 # def create(self, request, docid, lib):
439 # bi_json = request.POST['contents']
440 # revision = request.POST['revision']
442 # if request.POST.has_key('message'):
443 # msg = u"$USER$ " + request.PUT['message']
445 # msg = u"$AUTO$ Dublin core update."
447 # current = lib.document(docid, request.user.username)
448 # orig = lib.document_for_revision(revision)
450 # if current != orig:
451 # return response.EntityConflict().django_response({
452 # "reason": "out-of-date",
453 # "provided": orig.revision,
454 # "latest": current.revision })
456 # xmldoc = parser.WLDocument.from_string(current.data('xml'))
457 # document.book_info = dcparser.BookInfo.from_json(bi_json)
460 # ndoc = current.quickwrite('xml', \
461 # document.serialize().encode('utf-8'),\
462 # message=msg, user=request.user.username)
465 # # return the new revision number
467 # "document": ndoc.id,
469 # "previous_revision": current.revision,
470 # "revision": ndoc.revision,
471 # 'timestamp': ndoc.revision.timestamp,
472 # "url": reverse("docdc_view", args=[ndoc.id])
474 # except Exception, e:
475 # if ndoc: lib._rollback()
477 # except RevisionNotFound:
478 # return response.EntityNotFound().django_response()
480 class MergeHandler(BaseHandler):
481 allowed_methods = ('POST',)
483 @validate_form(forms.MergeRequestForm, 'POST')
485 def create(self, request, form, docid, lib):
486 """Create a new document revision from the information provided by user"""
488 revision = form.cleaned_data['revision']
490 # fetch the main branch document
491 doc = lib.document(docid)
493 # fetch the base document
494 user_doc = lib.document_for_revision(revision)
495 base_doc = user_doc.latest()
497 if base_doc != user_doc:
498 return response.EntityConflict().django_response({
499 "reason": "out-of-date",
500 "provided": str(user_doc.revision),
501 "latest": str(base_doc.revision)
504 if form.cleaned_data['type'] == 'update':
505 # update is always performed from the file branch
507 user_doc_new = base_doc.update(request.user.username)
509 if user_doc_new == user_doc:
510 return response.SuccessAllOk().django_response({
514 # shared document is the same
517 if form.cleaned_data['type'] == 'share':
518 if not base_doc.up_to_date():
519 return response.BadRequest().django_response({
520 "reason": "not-fast-forward",
521 "message": "You must first update your branch to the latest version."
524 anwser, info = base_doc.would_share()
527 return response.SuccessAllOk().django_response({
528 "result": "no-op", "message": info
531 # check for unresolved conflicts
532 if base_doc.has_conflict_marks():
533 return response.BadRequest().django_response({
534 "reason": "unresolved-conflicts",
535 "message": "There are unresolved conflicts in your file. Fix them, and try again."
538 if not request.user.has_perm('api.share_document'):
539 # User is not permitted to make a merge, right away
540 # So we instead create a pull request in the database
542 prq, created = PullRequest.objects.get_or_create(
543 comitter = request.user,
547 'source_revision': str(base_doc.revision),
548 'comment': form.cleaned_data['message'] or '$AUTO$ Document shared.',
552 # there can't be 2 pending request from same user
553 # for the same document
555 prq.source_revision = str(base_doc.revision)
556 prq.comment = prq.comment + 'u\n\n' + (form.cleaned_data['message'] or u'')
559 return response.RequestAccepted().django_response(\
560 ticket_status=prq.status, \
561 ticket_uri=reverse("pullrequest_view", args=[prq.id]) )
562 except IntegrityError:
563 return response.EntityConflict().django_response({
564 'reason': 'request-already-exist'
567 changed = base_doc.share(form.cleaned_data['message'])
569 # update shared version if needed
571 doc_new = doc.latest()
575 # the user wersion is the same
576 user_doc_new = base_doc
578 # The client can compare parent_revision to revision
579 # to see if he needs to update user's view
580 # Same goes for shared view
582 return response.SuccessAllOk().django_response({
584 "name": user_doc_new.id,
585 "user": user_doc_new.owner,
587 "revision": user_doc_new.revision,
588 'timestamp': user_doc_new.revision.timestamp,
590 "parent_revision": user_doc.revision,
591 "parent_timestamp": user_doc.revision.timestamp,
593 "shared_revision": doc_new.revision,
594 "shared_timestamp": doc_new.revision.timestamp,
596 "shared_parent_revision": doc.revision,
597 "shared_parent_timestamp": doc.revision.timestamp,
599 except wlrepo.OutdatedException, e:
600 return response.BadRequest().django_response({
601 "reason": "not-fast-forward",
604 except wlrepo.LibraryException, e:
605 return response.InternalError().django_response({
606 "reason": "merge-error",