1 # -*- encoding: utf-8 -*-
5 log = logging.getLogger('platforma.api.library')
7 from piston.handler import BaseHandler, AnonymousBaseHandler
9 from datetime import date
11 from django.core.urlresolvers import reverse
12 from django.db import IntegrityError
19 from explorer.models import GalleryForDocument
22 import api.forms as forms
23 import api.response as response
24 from api.utils import validate_form, hglibrary, natural_order
25 from api.models import PartCache, PullRequest
27 from pygments import highlight
28 from pygments.lexers import DiffLexer
29 from pygments.formatters import HtmlFormatter
36 return username.startswith('$prq-')
38 def prq_for_user(username):
40 return PullRequest.objects.get(id=int(username[5:]))
44 def check_user(request, user):
45 log.info("user: %r, perm: %r" % (request.user, request.user.get_all_permissions()) )
48 if not request.user.has_perm('api.view_prq'):
49 yield response.AccessDenied().django_response({
50 'reason': 'access-denied',
51 'message': "You don't have enough priviliges to view pull requests."
54 elif request.user.username != user:
55 if not request.user.has_perm('api.view_other_document'):
56 yield response.AccessDenied().django_response({
57 'reason': 'access-denied',
58 'message': "You don't have enough priviliges to view other people's document."
63 # Document List Handlers
65 # TODO: security check
66 class BasicLibraryHandler(AnonymousBaseHandler):
67 allowed_methods = ('GET',)
70 def read(self, request, lib):
71 """Return the list of documents."""
73 'url': reverse('document_view', args=[docid]),
74 'name': docid } for docid in lib.documents() ]
75 return {'documents' : document_list}
78 # This handler controlls the document collection
80 class LibraryHandler(BaseHandler):
81 allowed_methods = ('GET', 'POST')
82 anonymous = BasicLibraryHandler
85 def read(self, request, lib):
86 """Return the list of documents."""
90 for docid in lib.documents():
92 'url': reverse('document_view', args=[docid]),
97 parts = PartCache.objects.defer('part_id')\
98 .values_list('part_id', 'document_id').distinct()
100 document_tree = dict(documents)
102 for part, docid in parts:
103 # this way, we won't display broken links
104 if not documents.has_key(part):
105 log.info("NOT FOUND: %s", part)
108 parent = documents[docid]
109 child = documents[part]
111 # not top-level anymore
112 document_tree.pop(part)
113 parent['parts'].append(child)
115 for doc in documents.itervalues():
116 doc['parts'].sort(key=natural_order(lambda d: d['name']))
118 return {'documents': sorted(document_tree.itervalues(),
119 key=natural_order(lambda d: d['name']) ) }
122 @validate_form(forms.DocumentUploadForm, 'POST')
124 def create(self, request, form, lib):
125 """Create a new document."""
127 if form.cleaned_data['ocr_data']:
128 data = form.cleaned_data['ocr_data']
130 data = request.FILES['ocr_file'].read().decode('utf-8')
133 return response.BadRequest().django_response('You must pass ocr_data or ocr_file.')
135 if form.cleaned_data['generate_dc']:
136 data = librarian.wrap_text(data, unicode(date.today()))
138 docid = form.cleaned_data['bookname']
143 log.info("DOCID %s", docid)
144 doc = lib.document_create(docid)
145 # document created, but no content yet
147 doc = doc.quickwrite('xml', data.encode('utf-8'),
148 '$AUTO$ XML data uploaded.', user=request.user.username)
151 # rollback branch creation
153 raise LibraryException(traceback.format_exc())
155 url = reverse('document_view', args=[doc.id])
157 return response.EntityCreated().django_response(\
161 'revision': doc.revision },
165 except LibraryException, e:
167 return response.InternalError().django_response({
168 "reason": traceback.format_exc()
170 except DocumentAlreadyExists:
171 # Document is already there
172 return response.EntityConflict().django_response({
173 "reason": "already-exists",
174 "message": "Document already exists." % docid
180 class DiffHandler(BaseHandler):
181 allowed_methods = ('GET',)
184 def read(self, request, docid, lib):
185 '''Return diff between source_revision and target_revision)'''
186 revision = request.GET.get('revision')
189 source_document = lib.document(docid)
190 target_document = lib.document_for_revision(revision)
191 print source_document, target_document
193 diff = difflib.unified_diff(
194 source_document.data('xml').splitlines(True),
195 target_document.data('xml').splitlines(True),
199 s = ''.join(list(diff))
200 return highlight(s, DiffLexer(), HtmlFormatter(cssclass="pastie"))
206 class DocumentHandler(BaseHandler):
207 allowed_methods = ('GET', 'PUT')
209 @validate_form(forms.DocumentRetrieveForm, 'GET')
211 def read(self, request, form, docid, lib):
212 """Read document's meta data"""
213 log.info(u"User '%s' wants to edit %s(%s) as %s" % \
214 (request.user.username, docid, form.cleaned_data['revision'], form.cleaned_data['user']) )
216 user = form.cleaned_data['user'] or request.user.username
217 rev = form.cleaned_data['revision'] or 'latest'
219 for error in check_user(request, user):
223 doc = lib.document(docid, user, rev=rev)
224 except RevisionMismatch, e:
225 # the document exists, but the revision is bad
226 return response.EntityNotFound().django_response({
227 'reason': 'revision-mismatch',
228 'message': e.message,
232 except RevisionNotFound, e:
233 # the user doesn't have this document checked out
234 # or some other weird error occured
235 # try to do the checkout
237 if user == request.user.username:
238 mdoc = lib.document(docid)
239 doc = mdoc.take(user)
241 prq = prq_for_user(user)
242 # commiter's document
243 prq_doc = lib.document_for_revision(prq.source_revision)
244 doc = prq_doc.take(user)
246 return response.EntityNotFound().django_response({
247 'reason': 'document-not-found',
248 'message': e.message,
252 except RevisionNotFound, e:
253 return response.EntityNotFound().django_response({
254 'reason': 'document-not-found',
255 'message': e.message,
263 'html_url': reverse('dochtml_view', args=[doc.id]),
264 'text_url': reverse('doctext_view', args=[doc.id]),
265 # 'dc_url': reverse('docdc_view', args=[doc.id]),
266 'gallery_url': reverse('docgallery_view', args=[doc.id]),
267 'merge_url': reverse('docmerge_view', args=[doc.id]),
268 'revision': doc.revision,
269 'timestamp': doc.revision.timestamp,
270 # 'public_revision': doc.revision,
271 # 'public_timestamp': doc.revision.timestamp,
276 # def update(self, request, docid, lib):
277 # """Update information about the document, like display not"""
282 class DocumentHTMLHandler(BaseHandler):
283 allowed_methods = ('GET')
285 @validate_form(forms.DocumentRetrieveForm, 'GET')
287 def read(self, request, form, docid, lib, stylesheet='full'):
288 """Read document as html text"""
290 revision = form.cleaned_data['revision']
291 user = form.cleaned_data['user'] or request.user.username
292 document = lib.document_for_revision(revision)
294 if document.id != docid:
295 return response.BadRequest().django_response({
296 'reason': 'name-mismatch',
297 'message': 'Provided revision is not valid for this document'
300 if document.owner != user:
301 return response.BadRequest().django_response({
302 'reason': 'user-mismatch',
303 'message': "Provided revision doesn't belong to user %s" % user
306 for error in check_user(request, user):
309 return librarian.html.transform(document.data('xml'), is_file=False, \
310 parse_dublincore=False, stylesheet=stylesheet,\
312 "with-paths": 'boolean(1)',
315 except (EntryNotFound, RevisionNotFound), e:
316 return response.EntityNotFound().django_response({
317 'reason': 'not-found', 'message': e.message})
318 except librarian.ValidationError, e:
319 return response.InternalError().django_response({
320 'reason': 'xml-non-valid', 'message': e.message or u''})
321 except librarian.ParseError, e:
322 return response.InternalError().django_response({
323 'reason': 'xml-parse-error', 'message': e.message or u'' })
329 class DocumentGalleryHandler(BaseHandler):
330 allowed_methods = ('GET')
333 def read(self, request, docid):
334 """Read meta-data about scans for gallery of this document."""
336 from urllib import quote
338 for assoc in GalleryForDocument.objects.filter(document=docid):
339 dirpath = os.path.join(settings.MEDIA_ROOT, assoc.subpath)
341 if not os.path.isdir(dirpath):
342 log.warn(u"[WARNING]: missing gallery %s", dirpath)
345 gallery = {'name': assoc.name, 'pages': []}
347 for file in os.listdir(dirpath):
348 if not isinstance(file, unicode):
350 file = file.decode('utf-8')
352 log.warn(u"File %r in gallery %r is not unicode. Ommiting."\
357 name, ext = os.path.splitext(os.path.basename(file))
359 if ext.lower() not in [u'.png', u'.jpeg', u'.jpg']:
360 log.warn(u"Ignoring: %s %s", name, ext)
363 url = settings.MEDIA_URL + assoc.subpath + u'/' + file
366 url = settings.MEDIA_URL + u'/missing.png'
368 gallery['pages'].append( quote(url.encode('utf-8')) )
370 gallery['pages'].sort()
371 galleries.append(gallery)
378 # Dublin Core handlers
380 # @requires librarian
382 #class DocumentDublinCoreHandler(BaseHandler):
383 # allowed_methods = ('GET', 'POST')
386 # def read(self, request, docid, lib):
387 # """Read document as raw text"""
389 # revision = request.GET.get('revision', 'latest')
391 # if revision == 'latest':
392 # doc = lib.document(docid)
394 # doc = lib.document_for_revision(revision)
397 # if document.id != docid:
398 # return response.BadRequest().django_response({'reason': 'name-mismatch',
399 # 'message': 'Provided revision is not valid for this document'})
401 # bookinfo = dcparser.BookInfo.from_string(doc.data('xml'))
402 # return bookinfo.serialize()
403 # except (EntryNotFound, RevisionNotFound), e:
404 # return response.EntityNotFound().django_response({
405 # 'exception': type(e), 'message': e.message})
408 # def create(self, request, docid, lib):
410 # bi_json = request.POST['contents']
411 # revision = request.POST['revision']
413 # if request.POST.has_key('message'):
414 # msg = u"$USER$ " + request.PUT['message']
416 # msg = u"$AUTO$ Dublin core update."
418 # current = lib.document(docid, request.user.username)
419 # orig = lib.document_for_revision(revision)
421 # if current != orig:
422 # return response.EntityConflict().django_response({
423 # "reason": "out-of-date",
424 # "provided": orig.revision,
425 # "latest": current.revision })
427 # xmldoc = parser.WLDocument.from_string(current.data('xml'))
428 # document.book_info = dcparser.BookInfo.from_json(bi_json)
431 # ndoc = current.quickwrite('xml', \
432 # document.serialize().encode('utf-8'),\
433 # message=msg, user=request.user.username)
436 # # return the new revision number
438 # "document": ndoc.id,
440 # "previous_revision": current.revision,
441 # "revision": ndoc.revision,
442 # 'timestamp': ndoc.revision.timestamp,
443 # "url": reverse("docdc_view", args=[ndoc.id])
445 # except Exception, e:
446 # if ndoc: lib._rollback()
448 # except RevisionNotFound:
449 # return response.EntityNotFound().django_response()
451 class MergeHandler(BaseHandler):
452 allowed_methods = ('POST',)
454 @validate_form(forms.MergeRequestForm, 'POST')
456 def create(self, request, form, docid, lib):
457 """Create a new document revision from the information provided by user"""
459 revision = form.cleaned_data['revision']
461 # fetch the main branch document
462 doc = lib.document(docid)
464 # fetch the base document
465 user_doc = lib.document_for_revision(revision)
466 base_doc = user_doc.latest()
468 if base_doc != user_doc:
469 return response.EntityConflict().django_response({
470 "reason": "out-of-date",
471 "provided": str(user_doc.revision),
472 "latest": str(base_doc.revision)
475 if form.cleaned_data['type'] == 'update':
476 # update is always performed from the file branch
478 user_doc_new = base_doc.update(request.user.username)
480 if user_doc_new == user_doc:
481 return response.SuccessAllOk().django_response({
485 # shared document is the same
488 if form.cleaned_data['type'] == 'share':
489 if not base_doc.up_to_date():
490 return response.BadRequest().django_response({
491 "reason": "not-fast-forward",
492 "message": "You must first update your branch to the latest version."
495 anwser, info = base_doc.would_share()
498 return response.SuccessAllOk().django_response({
499 "result": "no-op", "message": info
502 # check for unresolved conflicts
503 if base_doc.has_conflict_marks():
504 return response.BadRequest().django_response({
505 "reason": "unresolved-conflicts",
506 "message": "There are unresolved conflicts in your file. Fix them, and try again."
509 if not request.user.has_perm('api.share_document'):
510 # User is not permitted to make a merge, right away
511 # So we instead create a pull request in the database
513 prq, created = PullRequest.objects.get_or_create(
514 comitter = request.user,
518 'source_revision': str(base_doc.revision),
519 'comment': form.cleaned_data['message'] or '$AUTO$ Document shared.',
523 # there can't be 2 pending request from same user
524 # for the same document
526 prq.source_revision = str(base_doc.revision)
527 prq.comment = prq.comment + 'u\n\n' + (form.cleaned_data['message'] or u'')
530 return response.RequestAccepted().django_response(\
531 ticket_status=prq.status, \
532 ticket_uri=reverse("pullrequest_view", args=[prq.id]) )
533 except IntegrityError:
534 return response.EntityConflict().django_response({
535 'reason': 'request-already-exist'
538 changed = base_doc.share(form.cleaned_data['message'])
540 # update shared version if needed
542 doc_new = doc.latest()
546 # the user wersion is the same
547 user_doc_new = base_doc
549 # The client can compare parent_revision to revision
550 # to see if he needs to update user's view
551 # Same goes for shared view
553 return response.SuccessAllOk().django_response({
555 "name": user_doc_new.id,
556 "user": user_doc_new.owner,
558 "revision": user_doc_new.revision,
559 'timestamp': user_doc_new.revision.timestamp,
561 "parent_revision": user_doc.revision,
562 "parent_timestamp": user_doc.revision.timestamp,
564 "shared_revision": doc_new.revision,
565 "shared_timestamp": doc_new.revision.timestamp,
567 "shared_parent_revision": doc.revision,
568 "shared_parent_timestamp": doc.revision.timestamp,
570 except wlrepo.OutdatedException, e:
571 return response.BadRequest().django_response({
572 "reason": "not-fast-forward",
575 except wlrepo.LibraryException, e:
576 return response.InternalError().django_response({
577 "reason": "merge-error",