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
13 from datetime import date
15 from django.core.urlresolvers import reverse
16 from django.db import IntegrityError
22 from api.models import PullRequest
23 from explorer.models import GalleryForDocument
26 import api.forms as forms
27 import api.response as response
28 from api.utils import validate_form, hglibrary, natural_order
29 from api.models import PartCache, PullRequest
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 BasicDocumentHandler(AnonymousBaseHandler):
181 allowed_methods = ('GET',)
184 def read(self, request, docid, lib):
186 doc = lib.document(docid)
187 except RevisionNotFound:
192 'html_url': reverse('dochtml_view', args=[doc.id]),
193 'text_url': reverse('doctext_view', args=[doc.id]),
194 'dc_url': reverse('docdc_view', args=[doc.id]),
195 'public_revision': doc.revision,
203 class DocumentHandler(BaseHandler):
204 allowed_methods = ('GET', 'PUT')
205 anonymous = BasicDocumentHandler
207 @validate_form(forms.DocumentRetrieveForm, 'GET')
209 def read(self, request, form, docid, lib):
210 """Read document's meta data"""
211 log.info(u"User '%s' wants to %s(%s) as %s" % \
212 (request.user.username, docid, form.cleaned_data['revision'], form.cleaned_data['user']) )
214 user = form.cleaned_data['user'] or request.user.username
215 rev = form.cleaned_data['revision'] or 'latest'
217 for error in check_user(request, user):
221 doc = lib.document(docid, user, rev=rev)
222 except RevisionMismatch, e:
223 # the document exists, but the revision is bad
224 return response.EntityNotFound().django_response({
225 'reason': 'revision-mismatch',
226 'message': e.message,
230 except RevisionNotFound, e:
231 # the user doesn't have this document checked out
232 # or some other weird error occured
233 # try to do the checkout
235 if user == request.user.username:
236 mdoc = lib.document(docid)
237 doc = mdoc.take(user)
239 prq = prq_for_user(user)
240 # commiter's document
241 prq_doc = lib.document_for_rev(prq.source_revision)
242 doc = prq_doc.take(user)
244 return response.EntityNotFound().django_response({
245 'reason': 'document-not-found',
246 'message': e.message,
250 except RevisionNotFound, e:
251 return response.EntityNotFound().django_response({
252 'reason': 'document-not-found',
253 'message': e.message,
261 'html_url': reverse('dochtml_view', args=[doc.id]),
262 'text_url': reverse('doctext_view', args=[doc.id]),
263 # 'dc_url': reverse('docdc_view', args=[doc.id]),
264 'gallery_url': reverse('docgallery_view', args=[doc.id]),
265 'merge_url': reverse('docmerge_view', args=[doc.id]),
266 'revision': doc.revision,
267 'timestamp': doc.revision.timestamp,
268 # 'public_revision': doc.revision,
269 # 'public_timestamp': doc.revision.timestamp,
274 # def update(self, request, docid, lib):
275 # """Update information about the document, like display not"""
280 class DocumentHTMLHandler(BaseHandler):
281 allowed_methods = ('GET')
283 @validate_form(forms.DocumentRetrieveForm, 'GET')
285 def read(self, request, form, docid, lib, stylesheet='partial'):
286 """Read document as html text"""
288 revision = form.cleaned_data['revision']
289 user = form.cleaned_data['user'] or request.user.username
290 document = lib.document_for_rev(revision)
292 if document.id != docid:
293 return response.BadRequest().django_response({
294 'reason': 'name-mismatch',
295 'message': 'Provided revision is not valid for this document'
298 if document.owner != user:
299 return response.BadRequest().django_response({
300 'reason': 'user-mismatch',
301 'message': "Provided revision doesn't belong to user %s" % user
304 for error in check_user(request, user):
307 return librarian.html.transform(document.data('xml'), is_file=False, \
308 parse_dublincore=False, stylesheet=stylesheet,\
310 "with-paths": 'boolean(1)',
313 except (EntryNotFound, RevisionNotFound), e:
314 return response.EntityNotFound().django_response({
315 'reason': 'not-found', 'message': e.message})
316 except librarian.ParseError, e:
317 return response.InternalError().django_response({
318 'reason': 'xml-parse-error', 'message': e.message })
324 class DocumentGalleryHandler(BaseHandler):
325 allowed_methods = ('GET')
328 def read(self, request, docid):
329 """Read meta-data about scans for gallery of this document."""
331 from urllib import quote
333 for assoc in GalleryForDocument.objects.filter(document=docid):
334 dirpath = os.path.join(settings.MEDIA_ROOT, assoc.subpath)
336 if not os.path.isdir(dirpath):
337 log.warn(u"[WARNING]: missing gallery %s", dirpath)
340 gallery = {'name': assoc.name, 'pages': []}
342 for file in os.listdir(dirpath):
343 if not isinstance(file, unicode):
345 file = file.decode('utf-8')
347 log.warn(u"File %r in gallery %r is not unicode. Ommiting."\
352 name, ext = os.path.splitext(os.path.basename(file))
354 if ext.lower() not in [u'.png', u'.jpeg', u'.jpg']:
355 log.warn(u"Ignoring: %s %s", name, ext)
358 url = settings.MEDIA_URL + assoc.subpath + u'/' + file
361 url = settings.MEDIA_URL + u'/missing.png'
363 gallery['pages'].append( quote(url.encode('utf-8')) )
365 # gallery['pages'].sort()
366 galleries.append(gallery)
373 # Dublin Core handlers
375 # @requires librarian
377 #class DocumentDublinCoreHandler(BaseHandler):
378 # allowed_methods = ('GET', 'POST')
381 # def read(self, request, docid, lib):
382 # """Read document as raw text"""
384 # revision = request.GET.get('revision', 'latest')
386 # if revision == 'latest':
387 # doc = lib.document(docid)
389 # doc = lib.document_for_rev(revision)
392 # if document.id != docid:
393 # return response.BadRequest().django_response({'reason': 'name-mismatch',
394 # 'message': 'Provided revision is not valid for this document'})
396 # bookinfo = dcparser.BookInfo.from_string(doc.data('xml'))
397 # return bookinfo.serialize()
398 # except (EntryNotFound, RevisionNotFound), e:
399 # return response.EntityNotFound().django_response({
400 # 'exception': type(e), 'message': e.message})
403 # def create(self, request, docid, lib):
405 # bi_json = request.POST['contents']
406 # revision = request.POST['revision']
408 # if request.POST.has_key('message'):
409 # msg = u"$USER$ " + request.PUT['message']
411 # msg = u"$AUTO$ Dublin core update."
413 # current = lib.document(docid, request.user.username)
414 # orig = lib.document_for_rev(revision)
416 # if current != orig:
417 # return response.EntityConflict().django_response({
418 # "reason": "out-of-date",
419 # "provided": orig.revision,
420 # "latest": current.revision })
422 # xmldoc = parser.WLDocument.from_string(current.data('xml'))
423 # document.book_info = dcparser.BookInfo.from_json(bi_json)
426 # ndoc = current.quickwrite('xml', \
427 # document.serialize().encode('utf-8'),\
428 # message=msg, user=request.user.username)
431 # # return the new revision number
433 # "document": ndoc.id,
435 # "previous_revision": current.revision,
436 # "revision": ndoc.revision,
437 # 'timestamp': ndoc.revision.timestamp,
438 # "url": reverse("docdc_view", args=[ndoc.id])
440 # except Exception, e:
441 # if ndoc: lib._rollback()
443 # except RevisionNotFound:
444 # return response.EntityNotFound().django_response()
446 class MergeHandler(BaseHandler):
447 allowed_methods = ('POST',)
449 @validate_form(forms.MergeRequestForm, 'POST')
451 def create(self, request, form, docid, lib):
452 """Create a new document revision from the information provided by user"""
453 revision = form.cleaned_data['revision']
455 # fetch the main branch document
456 doc = lib.document(docid)
458 # fetch the base document
459 user_doc = lib.document_for_rev(revision)
460 base_doc = user_doc.latest()
462 if base_doc != user_doc:
463 return response.EntityConflict().django_response({
464 "reason": "out-of-date",
465 "provided": str(user_doc.revision),
466 "latest": str(base_doc.revision)
469 if form.cleaned_data['type'] == 'update':
470 # update is always performed from the file branch
472 user_doc_new = base_doc.update(request.user.username)
474 if user_doc_new == user_doc:
475 return response.SuccessAllOk().django_response({
479 # shared document is the same
482 if form.cleaned_data['type'] == 'share':
483 if not base_doc.up_to_date():
484 return response.BadRequest().django_response({
485 "reason": "not-fast-forward",
486 "message": "You must first update your branch to the latest version."
489 if base_doc.parentof(doc) or base_doc.has_parent_from(doc):
490 return response.SuccessAllOk().django_response({
494 # check for unresolved conflicts
495 if base_doc.has_conflict_marks():
496 return response.BadRequest().django_response({
497 "reason": "unresolved-conflicts",
498 "message": "There are unresolved conflicts in your file. Fix them, and try again."
501 if not request.user.has_perm('api.share_document'):
502 # User is not permitted to make a merge, right away
503 # So we instead create a pull request in the database
505 prq, created = PullRequest.objects.get_or_create(
506 comitter = request.user,
510 'source_revision': str(base_doc.revision),
511 'comment': form.cleaned_data['message'] or '$AUTO$ Document shared.',
515 # there can't be 2 pending request from same user
516 # for the same document
518 prq.source_revision = str(base_doc.revision)
519 prq.comment = prq.comment + 'u\n\n' + (form.cleaned_data['message'] or u'')
522 return response.RequestAccepted().django_response(\
523 ticket_status=prq.status, \
524 ticket_uri=reverse("pullrequest_view", args=[prq.id]) )
525 except IntegrityError:
526 return response.EntityConflict().django_response({
527 'reason': 'request-already-exist'
530 changed = base_doc.share(form.cleaned_data['message'])
532 # update shared version if needed
534 doc_new = doc.latest()
538 # the user wersion is the same
539 user_doc_new = base_doc
541 # The client can compare parent_revision to revision
542 # to see if he needs to update user's view
543 # Same goes for shared view
545 return response.SuccessAllOk().django_response({
547 "name": user_doc_new.id,
548 "user": user_doc_new.owner,
550 "revision": user_doc_new.revision,
551 'timestamp': user_doc_new.revision.timestamp,
553 "parent_revision": user_doc.revision,
554 "parent_timestamp": user_doc.revision.timestamp,
556 "shared_revision": doc_new.revision,
557 "shared_timestamp": doc_new.revision.timestamp,
559 "shared_parent_revision": doc.revision,
560 "shared_parent_timestamp": doc.revision.timestamp,