1 # -*- encoding: utf-8 -*-
5 log = logging.getLogger('platforma.api.library')
7 from piston.handler import BaseHandler, AnonymousBaseHandler
8 from piston.utils import rc
10 from datetime import date
12 from django.core.urlresolvers import reverse
13 from django.db import IntegrityError
20 from explorer.models import GalleryForDocument
23 import api.forms as forms
24 import api.response as response
25 from api.utils import validate_form, hglibrary, natural_order
26 from api.models import PartCache, PullRequest
28 from pygments import highlight
29 from pygments.lexers import DiffLexer
30 from pygments.formatters import HtmlFormatter
37 return username.startswith('$prq-')
39 def prq_for_user(username):
41 return PullRequest.objects.get(id=int(username[5:]))
45 def check_user(request, user):
46 log.info("user: %r, perm: %r" % (request.user, request.user.get_all_permissions()) )
49 if not request.user.has_perm('api.view_prq'):
50 yield response.AccessDenied().django_response({
51 'reason': 'access-denied',
52 'message': "You don't have enough priviliges to view pull requests."
55 elif request.user.username != user:
56 if not request.user.has_perm('api.view_other_document'):
57 yield response.AccessDenied().django_response({
58 'reason': 'access-denied',
59 'message': "You don't have enough priviliges to view other people's document."
64 # Document List Handlers
66 # TODO: security check
67 class BasicLibraryHandler(AnonymousBaseHandler):
68 allowed_methods = ('GET',)
71 def read(self, request, lib):
72 """Return the list of documents."""
74 'url': reverse('document_view', args=[docid]),
75 'name': docid } for docid in lib.documents() ]
76 return {'documents' : document_list}
79 # This handler controlls the document collection
81 class LibraryHandler(BaseHandler):
82 allowed_methods = ('GET', 'POST')
83 anonymous = BasicLibraryHandler
86 def read(self, request, lib):
87 """Return the list of documents."""
91 for docid in lib.documents():
93 'url': reverse('document_view', args=[docid]),
98 parts = PartCache.objects.defer('part_id')\
99 .values_list('part_id', 'document_id').distinct()
101 document_tree = dict(documents)
103 for part, docid in parts:
104 # this way, we won't display broken links
105 if not documents.has_key(part):
106 log.info("NOT FOUND: %s", part)
109 parent = documents[docid]
110 child = documents[part]
112 # not top-level anymore
113 document_tree.pop(part)
114 parent['parts'].append(child)
116 for doc in documents.itervalues():
117 doc['parts'].sort(key=natural_order(lambda d: d['name']))
119 return {'documents': sorted(document_tree.itervalues(),
120 key=natural_order(lambda d: d['name']) ) }
123 @validate_form(forms.DocumentUploadForm, 'POST')
125 def create(self, request, form, lib):
126 """Create a new document."""
128 if form.cleaned_data['ocr_data']:
129 data = form.cleaned_data['ocr_data']
131 data = request.FILES['ocr_file'].read().decode('utf-8')
134 return response.BadRequest().django_response('You must pass ocr_data or ocr_file.')
136 if form.cleaned_data['generate_dc']:
137 data = librarian.wrap_text(data, unicode(date.today()))
139 docid = form.cleaned_data['bookname']
144 log.info("DOCID %s", docid)
145 doc = lib.document_create(docid)
146 # document created, but no content yet
148 doc = doc.quickwrite('xml', data.encode('utf-8'),
149 '$AUTO$ XML data uploaded.', user=request.user.username)
152 # rollback branch creation
154 raise wlrepo.LibraryException(traceback.format_exc())
156 url = reverse('document_view', args=[doc.id])
158 return response.EntityCreated().django_response(\
162 'revision': doc.revision },
166 except wlrepo.LibraryException, e:
168 return response.InternalError().django_response({
169 "reason": traceback.format_exc()
171 except wlrepo.DocumentAlreadyExists:
172 # Document is already there
173 return response.EntityConflict().django_response({
174 "reason": "already-exists",
175 "message": "Document already exists." % docid
182 class DiffHandler(BaseHandler):
183 allowed_methods = ('GET',)
186 def read(self, request, docid, lib):
187 '''Return diff between source_revision and target_revision)'''
188 revision = request.GET.get('revision')
191 source_document = lib.document(docid)
192 target_document = lib.document_for_revision(revision)
193 print source_document, target_document
195 diff = difflib.unified_diff(
196 source_document.data('xml').splitlines(True),
197 target_document.data('xml').splitlines(True),
201 s = ''.join(list(diff))
202 return highlight(s, DiffLexer(), HtmlFormatter(cssclass="pastie"))
208 class DocumentHandler(BaseHandler):
209 allowed_methods = ('GET', 'PUT')
211 @validate_form(forms.DocumentRetrieveForm, 'GET')
213 def read(self, request, form, docid, lib):
214 """Read document's meta data"""
215 log.info(u"User '%s' wants to edit %s(%s) as %s" % \
216 (request.user.username, docid, form.cleaned_data['revision'], form.cleaned_data['user']) )
218 user = form.cleaned_data['user'] or request.user.username
219 rev = form.cleaned_data['revision'] or 'latest'
221 for error in check_user(request, user):
225 doc = lib.document(docid, user, rev=rev)
226 except wlrepo.RevisionMismatch, e:
227 # the document exists, but the revision is bad
228 return response.EntityNotFound().django_response({
229 'reason': 'revision-mismatch',
230 'message': e.message,
234 except wlrepo.RevisionNotFound, e:
235 # the user doesn't have this document checked out
236 # or some other weird error occured
237 # try to do the checkout
239 if user == request.user.username:
240 mdoc = lib.document(docid)
241 doc = mdoc.take(user)
243 prq = prq_for_user(user)
244 # commiter's document
245 prq_doc = lib.document_for_revision(prq.source_revision)
246 doc = prq_doc.take(user)
248 return response.EntityNotFound().django_response({
249 'reason': 'document-not-found',
250 'message': e.message,
254 except wlrepo.RevisionNotFound, e:
255 return response.EntityNotFound().django_response({
256 'reason': 'document-not-found',
257 'message': e.message,
265 'html_url': reverse('dochtml_view', args=[doc.id]),
266 'text_url': reverse('doctext_view', args=[doc.id]),
267 # 'dc_url': reverse('docdc_view', args=[doc.id]),
268 'gallery_url': reverse('docgallery_view', args=[doc.id]),
269 'merge_url': reverse('docmerge_view', args=[doc.id]),
270 'revision': doc.revision,
271 'timestamp': doc.revision.timestamp,
272 # 'public_revision': doc.revision,
273 # 'public_timestamp': doc.revision.timestamp,
278 # def update(self, request, docid, lib):
279 # """Update information about the document, like display not"""
284 class DocumentHTMLHandler(BaseHandler):
285 allowed_methods = ('GET')
287 @validate_form(forms.DocumentRetrieveForm, 'GET')
289 def read(self, request, form, docid, lib, stylesheet='full'):
290 """Read document as html text"""
292 revision = form.cleaned_data['revision']
293 user = form.cleaned_data['user'] or request.user.username
294 document = lib.document_for_revision(revision)
296 if document.id != docid:
297 return response.BadRequest().django_response({
298 'reason': 'name-mismatch',
299 'message': 'Provided revision is not valid for this document'
302 if document.owner != user:
303 return response.BadRequest().django_response({
304 'reason': 'user-mismatch',
305 'message': "Provided revision doesn't belong to user %s" % user
308 for error in check_user(request, user):
311 return librarian.html.transform(document.data('xml'), is_file=False, \
312 parse_dublincore=False, stylesheet=stylesheet,\
314 "with-paths": 'boolean(1)',
317 except (wlrepo.EntryNotFound, wlrepo.RevisionNotFound), e:
318 return response.EntityNotFound().django_response({
319 'reason': 'not-found', 'message': e.message})
320 except librarian.ValidationError, e:
321 return response.InternalError().django_response({
322 'reason': 'xml-non-valid', 'message': e.message or u''})
323 except librarian.ParseError, e:
324 return response.InternalError().django_response({
325 'reason': 'xml-parse-error', 'message': e.message or u'' })
331 class DocumentGalleryHandler(BaseHandler):
332 allowed_methods = ('GET', 'POST')
335 def read(self, request, docid):
336 """Read meta-data about scans for gallery of this document."""
338 from urllib import quote
340 for assoc in GalleryForDocument.objects.filter(document=docid):
341 dirpath = os.path.join(settings.MEDIA_ROOT, assoc.subpath)
343 if not os.path.isdir(dirpath):
344 log.warn(u"[WARNING]: missing gallery %s", dirpath)
347 gallery = {'name': assoc.name, 'pages': []}
349 for file in os.listdir(dirpath):
350 if not isinstance(file, unicode):
352 file = file.decode('utf-8')
354 log.warn(u"File %r in gallery %r is not unicode. Ommiting."\
359 name, ext = os.path.splitext(os.path.basename(file))
361 if ext.lower() not in [u'.png', u'.jpeg', u'.jpg']:
362 log.warn(u"Ignoring: %s %s", name, ext)
365 url = settings.MEDIA_URL + assoc.subpath + u'/' + file
368 url = settings.MEDIA_URL + u'/missing.png'
370 gallery['pages'].append( quote(url.encode('utf-8')) )
372 gallery['pages'].sort()
373 galleries.append(gallery)
377 def create(self, request, docid):
378 if not request.user.is_superuser:
381 new_path = request.POST.get('path')
384 gallery, created = GalleryForDocument.objects.get_or_create(
392 gallery.subpath = new_path
397 return rc.BAD_REQUEST
400 # Dublin Core handlers
402 # @requires librarian
404 #class DocumentDublinCoreHandler(BaseHandler):
405 # allowed_methods = ('GET', 'POST')
408 # def read(self, request, docid, lib):
409 # """Read document as raw text"""
411 # revision = request.GET.get('revision', 'latest')
413 # if revision == 'latest':
414 # doc = lib.document(docid)
416 # doc = lib.document_for_revision(revision)
419 # if document.id != docid:
420 # return response.BadRequest().django_response({'reason': 'name-mismatch',
421 # 'message': 'Provided revision is not valid for this document'})
423 # bookinfo = dcparser.BookInfo.from_string(doc.data('xml'))
424 # return bookinfo.serialize()
425 # except (EntryNotFound, RevisionNotFound), e:
426 # return response.EntityNotFound().django_response({
427 # 'exception': type(e), 'message': e.message})
430 # def create(self, request, docid, lib):
432 # bi_json = request.POST['contents']
433 # revision = request.POST['revision']
435 # if request.POST.has_key('message'):
436 # msg = u"$USER$ " + request.PUT['message']
438 # msg = u"$AUTO$ Dublin core update."
440 # current = lib.document(docid, request.user.username)
441 # orig = lib.document_for_revision(revision)
443 # if current != orig:
444 # return response.EntityConflict().django_response({
445 # "reason": "out-of-date",
446 # "provided": orig.revision,
447 # "latest": current.revision })
449 # xmldoc = parser.WLDocument.from_string(current.data('xml'))
450 # document.book_info = dcparser.BookInfo.from_json(bi_json)
453 # ndoc = current.quickwrite('xml', \
454 # document.serialize().encode('utf-8'),\
455 # message=msg, user=request.user.username)
458 # # return the new revision number
460 # "document": ndoc.id,
462 # "previous_revision": current.revision,
463 # "revision": ndoc.revision,
464 # 'timestamp': ndoc.revision.timestamp,
465 # "url": reverse("docdc_view", args=[ndoc.id])
467 # except Exception, e:
468 # if ndoc: lib._rollback()
470 # except RevisionNotFound:
471 # return response.EntityNotFound().django_response()
473 class MergeHandler(BaseHandler):
474 allowed_methods = ('POST',)
476 @validate_form(forms.MergeRequestForm, 'POST')
478 def create(self, request, form, docid, lib):
479 """Create a new document revision from the information provided by user"""
481 revision = form.cleaned_data['revision']
483 # fetch the main branch document
484 doc = lib.document(docid)
486 # fetch the base document
487 user_doc = lib.document_for_revision(revision)
488 base_doc = user_doc.latest()
490 if base_doc != user_doc:
491 return response.EntityConflict().django_response({
492 "reason": "out-of-date",
493 "provided": str(user_doc.revision),
494 "latest": str(base_doc.revision)
497 if form.cleaned_data['type'] == 'update':
498 # update is always performed from the file branch
500 user_doc_new = base_doc.update(request.user.username)
502 if user_doc_new == user_doc:
503 return response.SuccessAllOk().django_response({
507 # shared document is the same
510 if form.cleaned_data['type'] == 'share':
511 if not base_doc.up_to_date():
512 return response.BadRequest().django_response({
513 "reason": "not-fast-forward",
514 "message": "You must first update your branch to the latest version."
517 anwser, info = base_doc.would_share()
520 return response.SuccessAllOk().django_response({
521 "result": "no-op", "message": info
524 # check for unresolved conflicts
525 if base_doc.has_conflict_marks():
526 return response.BadRequest().django_response({
527 "reason": "unresolved-conflicts",
528 "message": "There are unresolved conflicts in your file. Fix them, and try again."
531 if not request.user.has_perm('api.share_document'):
532 # User is not permitted to make a merge, right away
533 # So we instead create a pull request in the database
535 prq, created = PullRequest.objects.get_or_create(
536 comitter = request.user,
540 'source_revision': str(base_doc.revision),
541 'comment': form.cleaned_data['message'] or '$AUTO$ Document shared.',
545 # there can't be 2 pending request from same user
546 # for the same document
548 prq.source_revision = str(base_doc.revision)
549 prq.comment = prq.comment + 'u\n\n' + (form.cleaned_data['message'] or u'')
552 return response.RequestAccepted().django_response(\
553 ticket_status=prq.status, \
554 ticket_uri=reverse("pullrequest_view", args=[prq.id]) )
555 except IntegrityError:
556 return response.EntityConflict().django_response({
557 'reason': 'request-already-exist'
560 changed = base_doc.share(form.cleaned_data['message'])
562 # update shared version if needed
564 doc_new = doc.latest()
568 # the user wersion is the same
569 user_doc_new = base_doc
571 # The client can compare parent_revision to revision
572 # to see if he needs to update user's view
573 # Same goes for shared view
575 return response.SuccessAllOk().django_response({
577 "name": user_doc_new.id,
578 "user": user_doc_new.owner,
580 "revision": user_doc_new.revision,
581 'timestamp': user_doc_new.revision.timestamp,
583 "parent_revision": user_doc.revision,
584 "parent_timestamp": user_doc.revision.timestamp,
586 "shared_revision": doc_new.revision,
587 "shared_timestamp": doc_new.revision.timestamp,
589 "shared_parent_revision": doc.revision,
590 "shared_parent_timestamp": doc.revision.timestamp,
592 except wlrepo.OutdatedException, e:
593 return response.BadRequest().django_response({
594 "reason": "not-fast-forward",
597 except wlrepo.LibraryException, e:
598 return response.InternalError().django_response({
599 "reason": "merge-error",