1 # -*- encoding: utf-8 -*-
 
   3 __author__= "Ćukasz Rekucki"
 
   4 __date__ = "$2009-09-25 15:49:50$"
 
   5 __doc__ = "Module documentation."
 
   7 from piston.handler import BaseHandler, AnonymousBaseHandler
 
  10 from datetime import date
 
  12 from django.core.urlresolvers import reverse
 
  13 from django.utils import simplejson as json
 
  17 from librarian import dcparser
 
  19 from wlrepo import RevisionNotFound, LibraryException, DocumentAlreadyExists
 
  20 from explorer.models import PullRequest
 
  23 import api.forms as forms
 
  24 import api.response as response
 
  25 from api.utils import validate_form, hglibrary
 
  26 from api.models import PartCache
 
  29 # Document List Handlers
 
  31 class BasicLibraryHandler(AnonymousBaseHandler):
 
  32     allowed_methods = ('GET',)
 
  35     def read(self, request, lib):
 
  36         """Return the list of documents."""       
 
  38             'url': reverse('document_view', args=[docid]),
 
  39             'name': docid } for docid in lib.documents() ]
 
  41         return {'documents' : document_list}
 
  44 class LibraryHandler(BaseHandler):
 
  45     allowed_methods = ('GET', 'POST')
 
  46     anonymous = BasicLibraryHandler
 
  49     def read(self, request, lib):
 
  50         """Return the list of documents."""
 
  54         for docid in lib.documents():
 
  56                 'url': reverse('document_view', args=[docid]),
 
  61         related = PartCache.objects.defer('part_id')\
 
  62             .values_list('part_id', 'document_id').distinct()
 
  64         for part, docid in related:
 
  65             # this way, we won't display broken links
 
  66             if not documents.has_key(part):
 
  69             child = documents[part]
 
  70             parent = documents[docid]
 
  72             if isinstance(parent, dict): # the parent is top-level
 
  74                 parent['parts'].append(child)
 
  75                 documents[part] = child['parts']
 
  80             'documents': [d for d in documents.itervalues() if isinstance(d, dict)]
 
  83     @validate_form(forms.DocumentUploadForm, 'POST')
 
  85     def create(self, request, form, lib):
 
  86         """Create a new document."""       
 
  88         if form.cleaned_data['ocr_data']:
 
  89             data = form.cleaned_data['ocr_data']
 
  91             data = request.FILES['ocr_file'].read().decode('utf-8')
 
  93         if form.cleaned_data['generate_dc']:
 
  94             data = librarian.wrap_text(data, unicode(date.today()))
 
  96         docid = form.cleaned_data['bookname']
 
 102                 doc = lib.document_create(docid)
 
 103                 # document created, but no content yet
 
 106                     doc = doc.quickwrite('xml', data.encode('utf-8'),
 
 107                         '$AUTO$ XML data uploaded.', user=request.user.username)
 
 109                     # rollback branch creation
 
 111                     raise LibraryException("Exception occured:" + repr(e))
 
 113                 url = reverse('document_view', args=[doc.id])
 
 115                 return response.EntityCreated().django_response(\
 
 119                         'revision': doc.revision },
 
 123         except LibraryException, e:
 
 124             return response.InternalError().django_response(\
 
 125                 {'exception': repr(e) })                
 
 126         except DocumentAlreadyExists:
 
 127             # Document is already there
 
 128             return response.EntityConflict().django_response(\
 
 129                 {"reason": "Document %s already exists." % docid})
 
 134 class BasicDocumentHandler(AnonymousBaseHandler):
 
 135     allowed_methods = ('GET',)
 
 138     def read(self, request, docid, lib):
 
 140             doc = lib.document(docid)
 
 141         except RevisionNotFound:
 
 146             'html_url': reverse('dochtml_view', args=[doc.id,doc.revision]),
 
 147             'text_url': reverse('doctext_view', args=[doc.id,doc.revision]),
 
 148             'dc_url': reverse('docdc_view', args=[doc.id,doc.revision]),
 
 149             'public_revision': doc.revision,
 
 157 class DocumentHandler(BaseHandler):
 
 158     allowed_methods = ('GET', 'PUT')
 
 159     anonymous = BasicDocumentHandler
 
 162     def read(self, request, docid, lib):
 
 163         """Read document's meta data"""       
 
 165             doc = lib.document(docid)
 
 166             udoc = doc.take(request.user.username)
 
 167         except RevisionNotFound:
 
 168             return request.EnityNotFound().django_response()
 
 170         # is_shared = udoc.ancestorof(doc)
 
 171         # is_uptodate = is_shared or shared.ancestorof(document)
 
 175             'html_url': reverse('dochtml_view', args=[udoc.id,udoc.revision]),
 
 176             'text_url': reverse('doctext_view', args=[udoc.id,udoc.revision]),
 
 177             'dc_url': reverse('docdc_view', args=[udoc.id,udoc.revision]),
 
 178             'user_revision': udoc.revision,
 
 179             'public_revision': doc.revision,            
 
 185     def update(self, request, docid, lib):
 
 186         """Update information about the document, like display not"""
 
 191 class DocumentHTMLHandler(BaseHandler):
 
 192     allowed_methods = ('GET', 'PUT')
 
 195     def read(self, request, docid, revision, lib):
 
 196         """Read document as html text"""
 
 198             if revision == 'latest':
 
 199                 document = lib.document(docid)
 
 201                 document = lib.document_for_rev(revision)
 
 203             return librarian.html.transform(document.data('xml'), is_file=False)
 
 204         except RevisionNotFound:
 
 205             return response.EntityNotFound().django_response()
 
 214 XINCLUDE_REGEXP = r"""<(?:\w+:)?include\s+[^>]*?href=("|')wlrepo://(?P<link>[^\1]+?)\1\s*[^>]*?>"""
 
 217 class DocumentTextHandler(BaseHandler):
 
 218     allowed_methods = ('GET', 'PUT')
 
 221     def read(self, request, docid, revision, lib):
 
 222         """Read document as raw text"""               
 
 224             if revision == 'latest':
 
 225                 document = lib.document(docid)
 
 227                 document = lib.document_for_rev(revision)
 
 229             # TODO: some finer-grained access control
 
 230             return document.data('xml')
 
 231         except RevisionNotFound:
 
 232             return response.EntityNotFound().django_response()
 
 235     def update(self, request, docid, revision, lib):
 
 237             data = request.PUT['contents']            
 
 239             if request.PUT.has_key('message'):
 
 240                 msg = u"$USER$ " + request.PUT['message']
 
 242                 msg = u"$AUTO$ XML content update."
 
 244             current = lib.document(docid, request.user.username)
 
 245             orig = lib.document_for_rev(revision)
 
 248                 return response.EntityConflict().django_response({
 
 249                         "reason": "out-of-date",
 
 250                         "provided_revision": orig.revision,
 
 251                         "latest_revision": current.revision })
 
 253             # try to find any Xinclude tags
 
 254             includes = [m.groupdict()['link'] for m in (re.finditer(\
 
 255                 XINCLUDE_REGEXP, data, flags=re.UNICODE) or []) ]
 
 257             # TODO: provide useful routines to make this simpler
 
 258             def xml_update_action(lib, resolve):
 
 260                     f = lib._fileopen(resolve('parts'), 'r')
 
 261                     stored_includes = json.loads(f.read())
 
 266                 if stored_includes != includes:
 
 267                     f = lib._fileopen(resolve('parts'), 'w+')
 
 268                     f.write(json.dumps(includes))
 
 271                     lib._fileadd(resolve('parts'))
 
 273                     # update the parts cache
 
 274                     PartCache.update_cache(docid, current.owner,\
 
 275                         stored_includes, includes)
 
 277                 # now that the parts are ok, write xml
 
 278                 f = lib._fileopen(resolve('xml'), 'w+')
 
 279                 f.write(data.encode('utf-8'))
 
 283             ndoc = current.invoke_and_commit(\
 
 284                 xml_update_action, lambda d: (msg, current.owner) )
 
 287                 # return the new revision number
 
 288                 return response.SuccessAllOk().django_response({
 
 291                     "previous_revision": current.revision,
 
 292                     "updated_revision": ndoc.revision,
 
 293                     "url": reverse("doctext_view", args=[ndoc.id, ndoc.revision])
 
 296                 if ndoc: lib._rollback()
 
 298         except RevisionNotFound, e:
 
 299             return response.EntityNotFound().django_response(e)
 
 302 # Dublin Core handlers
 
 304 # @requires librarian
 
 306 class DocumentDublinCoreHandler(BaseHandler):
 
 307     allowed_methods = ('GET', 'PUT')
 
 310     def read(self, request, docid, revision, lib):
 
 311         """Read document as raw text"""        
 
 313             if revision == 'latest':
 
 314                 doc = lib.document(docid)
 
 316                 doc = lib.document_for_rev(revision)
 
 318             bookinfo = dcparser.BookInfo.from_string(doc.data('xml'))
 
 319             return bookinfo.serialize()
 
 320         except RevisionNotFound:
 
 321             return response.EntityNotFound().django_response()
 
 324     def update(self, request, docid, revision, lib):
 
 326             bi_json = request.PUT['contents']            
 
 327             if request.PUT.has_key('message'):
 
 328                 msg = u"$USER$ " + request.PUT['message']
 
 330                 msg = u"$AUTO$ Dublin core update."
 
 332             current = lib.document(docid, request.user.username)
 
 333             orig = lib.document_for_rev(revision)
 
 336                 return response.EntityConflict().django_response({
 
 337                         "reason": "out-of-date",
 
 338                         "provided": orig.revision,
 
 339                         "latest": current.revision })
 
 341             xmldoc = parser.WLDocument.from_string(current.data('xml'))
 
 342             document.book_info = dcparser.BookInfo.from_json(bi_json)
 
 345             ndoc = current.quickwrite('xml', \
 
 346                 document.serialize().encode('utf-8'),\
 
 347                 message=msg, user=request.user.username)
 
 350                 # return the new revision number
 
 354                     "previous_revision": current.revision,
 
 355                     "updated_revision": ndoc.revision
 
 360         except RevisionNotFound:
 
 361             return response.EntityNotFound().django_response()
 
 365 class MergeHandler(BaseHandler):
 
 366     allowed_methods = ('POST',)
 
 368     @validate_form(forms.MergeRequestForm, 'POST')
 
 370     def create(self, request, form, docid, lib):
 
 371         """Create a new document revision from the information provided by user"""
 
 373         target_rev = form.cleaned_data['target_revision']
 
 375         doc = lib.document(docid)
 
 376         udoc = doc.take(request.user.username)
 
 378         if target_rev == 'latest':
 
 379             target_rev = udoc.revision
 
 381         if str(udoc.revision) != target_rev:
 
 382             # user think doesn't know he has an old version
 
 385             # Updating is teorericly ok, but we need would
 
 386             # have to force a refresh. Sharing may be not safe,
 
 387             # 'cause it doesn't always result in update.
 
 389             # In other words, we can't lie about the resource's state
 
 390             # So we should just yield and 'out-of-date' conflict
 
 391             # and let the client ask again with updated info.
 
 393             # NOTE: this could result in a race condition, when there
 
 394             # are 2 instances of the same user editing the same document.
 
 395             # Instance "A" trying to update, and instance "B" always changing
 
 396             # the document right before "A". The anwser to this problem is
 
 397             # for the "A" to request a merge from 'latest' and then
 
 398             # check the parent revisions in response, if he actually
 
 399             # merge from where he thinks he should. If not, the client SHOULD
 
 400             # update his internal state.
 
 401             return response.EntityConflict().django_response({
 
 402                     "reason": "out-of-date",
 
 403                     "provided": target_rev,
 
 404                     "latest": udoc.revision })
 
 406         if not request.user.has_perm('explorer.book.can_share'):
 
 407             # User is not permitted to make a merge, right away
 
 408             # So we instead create a pull request in the database
 
 410                 comitter=request.user,
 
 412                 source_revision = str(udoc.revision),
 
 414                 comment = form.cleaned_data['comment'] or '$AUTO$ Document shared.'
 
 418             return response.RequestAccepted().django_response(\
 
 419                 ticket_status=prq.status, \
 
 420                 ticket_uri=reverse("pullrequest_view", args=[prq.id]) )
 
 422         if form.cleaned_data['type'] == 'update':
 
 423             # update is always performed from the file branch
 
 425             success, changed = udoc.update(request.user.username)
 
 427         if form.cleaned_data['type'] == 'share':
 
 428             success, changed = udoc.share(form.cleaned_data['comment'])
 
 431             return response.EntityConflict().django_response()
 
 434             return response.SuccessNoContent().django_response()
 
 436         new_udoc = udoc.latest()
 
 438         return response.SuccessAllOk().django_response({
 
 440             "parent_user_resivion": udoc.revision,
 
 441             "parent_revision": doc.revision,
 
 442             "revision": udoc.revision,