1 # -*- encoding: utf-8 -*-
 
   5 __author__= "Ćukasz Rekucki"
 
   6 __date__ = "$2009-09-25 15:49:50$"
 
   7 __doc__ = "Module documentation."
 
   9 from piston.handler import BaseHandler, AnonymousBaseHandler
 
  12 from datetime import date
 
  14 from django.core.urlresolvers import reverse
 
  15 from django.utils import simplejson as json
 
  19 from librarian import dcparser
 
  22 from explorer.models import PullRequest, GalleryForDocument
 
  25 import api.forms as forms
 
  26 import api.response as response
 
  27 from api.utils import validate_form, hglibrary, natural_order
 
  28 from api.models import PartCache
 
  34 log = logging.getLogger('platforma.api')
 
  38 # Document List Handlers
 
  40 class BasicLibraryHandler(AnonymousBaseHandler):
 
  41     allowed_methods = ('GET',)
 
  44     def read(self, request, lib):
 
  45         """Return the list of documents."""       
 
  47             'url': reverse('document_view', args=[docid]),
 
  48             'name': docid } for docid in lib.documents() ]
 
  50         return {'documents' : document_list}
 
  53 class LibraryHandler(BaseHandler):
 
  54     allowed_methods = ('GET', 'POST')
 
  55     anonymous = BasicLibraryHandler
 
  58     def read(self, request, lib):
 
  59         """Return the list of documents."""
 
  63         for docid in lib.documents():            
 
  65                 'url': reverse('document_view', args=[docid]),
 
  70         parts = PartCache.objects.defer('part_id')\
 
  71             .values_list('part_id', 'document_id').distinct()
 
  73         document_tree = dict(documents)
 
  75         for part, docid in parts:
 
  76             # this way, we won't display broken links
 
  77             if not documents.has_key(part):
 
  78                 log.info("NOT FOUND: %s", part)
 
  81             parent = documents[docid]
 
  82             child = documents[part]
 
  84             # not top-level anymore
 
  85             document_tree.pop(part)
 
  86             parent['parts'].append(child)
 
  88         for doc in documents.itervalues():
 
  89             doc['parts'].sort(key=natural_order(lambda d: d['name']))
 
  91         return {'documents': sorted(document_tree.itervalues(),
 
  92             key=natural_order(lambda d: d['name']) ) }
 
  94     @validate_form(forms.DocumentUploadForm, 'POST')
 
  96     def create(self, request, form, lib):
 
  97         """Create a new document."""       
 
  99         if form.cleaned_data['ocr_data']:
 
 100             data = form.cleaned_data['ocr_data']
 
 102             data = request.FILES['ocr_file'].read().decode('utf-8')
 
 105             return response.BadRequest().django_response('You must pass ocr_data or ocr_file.')
 
 107         if form.cleaned_data['generate_dc']:
 
 108             data = librarian.wrap_text(data, unicode(date.today()))
 
 110         docid = form.cleaned_data['bookname']
 
 115                 log.info("DOCID %s", docid)
 
 116                 doc = lib.document_create(docid)
 
 117                 # document created, but no content yet
 
 120                     doc = doc.quickwrite('xml', data.encode('utf-8'),
 
 121                         '$AUTO$ XML data uploaded.', user=request.user.username)
 
 124                     # rollback branch creation
 
 126                     raise LibraryException(traceback.format_exc())
 
 128                 url = reverse('document_view', args=[doc.id])
 
 130                 return response.EntityCreated().django_response(\
 
 134                         'revision': doc.revision },
 
 138         except LibraryException, e:
 
 140             return response.InternalError().django_response(\
 
 141                 {'exception': traceback.format_exc()} )
 
 142         except DocumentAlreadyExists:
 
 143             # Document is already there
 
 144             return response.EntityConflict().django_response(\
 
 145                 {"reason": "Document %s already exists." % docid})
 
 150 class BasicDocumentHandler(AnonymousBaseHandler):
 
 151     allowed_methods = ('GET',)
 
 154     def read(self, request, docid, lib):
 
 156             doc = lib.document(docid)
 
 157         except RevisionNotFound:
 
 162             'html_url': reverse('dochtml_view', args=[doc.id]),
 
 163             'text_url': reverse('doctext_view', args=[doc.id]),
 
 164             'dc_url': reverse('docdc_view', args=[doc.id]),
 
 165             'public_revision': doc.revision,
 
 173 class DocumentHandler(BaseHandler):
 
 174     allowed_methods = ('GET', 'PUT')
 
 175     anonymous = BasicDocumentHandler
 
 178     def read(self, request, docid, lib):
 
 179         """Read document's meta data"""       
 
 180         log.info("Read %s", docid)
 
 182             doc = lib.document(docid)
 
 183             udoc = doc.take(request.user.username)
 
 184         except RevisionNotFound, e:
 
 185             return response.EntityNotFound().django_response({
 
 186                 'exception': type(e), 'message': e.message})
 
 188         # is_shared = udoc.ancestorof(doc)
 
 189         # is_uptodate = is_shared or shared.ancestorof(document)
 
 193             'html_url': reverse('dochtml_view', args=[udoc.id]),
 
 194             'text_url': reverse('doctext_view', args=[udoc.id]),
 
 195             'dc_url': reverse('docdc_view', args=[udoc.id]),
 
 196             'gallery_url': reverse('docgallery_view', args=[udoc.id]),
 
 197             'merge_url': reverse('docmerge_view', args=[udoc.id]),
 
 198             'user_revision': udoc.revision,
 
 199             'user_timestamp': udoc.revision.timestamp,
 
 200             'public_revision': doc.revision,
 
 201             'public_timestamp': doc.revision.timestamp,
 
 207     def update(self, request, docid, lib):
 
 208         """Update information about the document, like display not"""
 
 213 class DocumentHTMLHandler(BaseHandler):
 
 214     allowed_methods = ('GET')
 
 217     def read(self, request, docid, lib):
 
 218         """Read document as html text"""
 
 220             revision = request.GET.get('revision', 'latest')
 
 222             if revision == 'latest':
 
 223                 document = lib.document(docid)
 
 225                 document = lib.document_for_rev(revision)
 
 227             if document.id != docid:
 
 228                 return response.BadRequest().django_response({'reason': 'name-mismatch',
 
 229                     'message': 'Provided revision refers, to document "%s", but provided "%s"' % (document.id, docid) })
 
 231             return librarian.html.transform(document.data('xml'), is_file=False, parse_dublincore=False)
 
 232         except (EntryNotFound, RevisionNotFound), e:
 
 233             return response.EntityNotFound().django_response({
 
 234                 'exception': type(e), 'message': e.message})
 
 241 class DocumentGalleryHandler(BaseHandler):
 
 242     allowed_methods = ('GET')
 
 244     def read(self, request, docid):
 
 245         """Read meta-data about scans for gallery of this document."""
 
 248         for assoc in GalleryForDocument.objects.filter(document=docid):
 
 249             dirpath = os.path.join(settings.MEDIA_ROOT, assoc.subpath)
 
 251             if not os.path.isdir(dirpath):
 
 252                 log.info(u"[WARNING]: missing gallery %s", dirpath)
 
 255             gallery = {'name': assoc.name, 'pages': []}
 
 257             for file in os.listdir(dirpath):
 
 258                 file = file.decode('utf-8')
 
 261                 name, ext = os.path.splitext(os.path.basename(file))
 
 263                 if ext.lower() not in [u'.png', u'.jpeg', u'.jpg']:
 
 264                     log.info("Ignoring: %s %s", name, ext)
 
 267                 url = settings.MEDIA_URL + assoc.subpath + u'/' + file;
 
 268                 gallery['pages'].append(url)
 
 270             gallery['pages'].sort()
 
 271             galleries.append(gallery)
 
 279 XINCLUDE_REGEXP = r"""<(?:\w+:)?include\s+[^>]*?href=("|')wlrepo://(?P<link>[^\1]+?)\1\s*[^>]*?>"""
 
 283 class DocumentTextHandler(BaseHandler):
 
 284     allowed_methods = ('GET', 'POST')
 
 287     def read(self, request, docid, lib):
 
 288         """Read document as raw text"""
 
 289         revision = request.GET.get('revision', 'latest')
 
 291             if revision == 'latest':
 
 292                 document = lib.document(docid)
 
 294                 document = lib.document_for_rev(revision)
 
 296             if document.id != docid:
 
 297                 return response.BadRequest().django_response({'reason': 'name-mismatch',
 
 298                     'message': 'Provided revision is not valid for this document'})
 
 300             # TODO: some finer-grained access control
 
 301             return document.data('xml')
 
 302         except (EntryNotFound, RevisionNotFound), e:
 
 303             return response.EntityNotFound().django_response({
 
 304                 'exception': type(e), 'message': e.message})
 
 307     def create(self, request, docid, lib):
 
 309             data = request.POST['contents']
 
 310             revision = request.POST['revision']
 
 312             if request.POST.has_key('message'):
 
 313                 msg = u"$USER$ " + request.POST['message']
 
 315                 msg = u"$AUTO$ XML content update."
 
 317             current = lib.document(docid, request.user.username)
 
 318             orig = lib.document_for_rev(revision)
 
 321                 return response.EntityConflict().django_response({
 
 322                         "reason": "out-of-date",
 
 323                         "provided_revision": orig.revision,
 
 324                         "latest_revision": current.revision })
 
 326             # try to find any Xinclude tags
 
 327             includes = [m.groupdict()['link'] for m in (re.finditer(\
 
 328                 XINCLUDE_REGEXP, data, flags=re.UNICODE) or []) ]
 
 330             log.info("INCLUDES: %s", includes)
 
 332             # TODO: provide useful routines to make this simpler
 
 333             def xml_update_action(lib, resolve):
 
 335                     f = lib._fileopen(resolve('parts'), 'r')
 
 336                     stored_includes = json.loads(f.read())
 
 341                 if stored_includes != includes:
 
 342                     f = lib._fileopen(resolve('parts'), 'w+')
 
 343                     f.write(json.dumps(includes))
 
 346                     lib._fileadd(resolve('parts'))
 
 348                     # update the parts cache
 
 349                     PartCache.update_cache(docid, current.owner,\
 
 350                         stored_includes, includes)
 
 352                 # now that the parts are ok, write xml
 
 353                 f = lib._fileopen(resolve('xml'), 'w+')
 
 354                 f.write(data.encode('utf-8'))
 
 358             ndoc = current.invoke_and_commit(\
 
 359                 xml_update_action, lambda d: (msg, current.owner) )
 
 362                 # return the new revision number
 
 363                 return response.SuccessAllOk().django_response({
 
 366                     "previous_revision": current.revision,
 
 367                     "revision": ndoc.revision,
 
 368                     'timestamp': ndoc.revision.timestamp,
 
 369                     "url": reverse("doctext_view", args=[ndoc.id])
 
 372                 if ndoc: lib._rollback()
 
 374         except RevisionNotFound, e:
 
 375             return response.EntityNotFound(mimetype="text/plain").\
 
 376                 django_response(e.message)
 
 380 # Dublin Core handlers
 
 382 # @requires librarian
 
 384 class DocumentDublinCoreHandler(BaseHandler):
 
 385     allowed_methods = ('GET', 'POST')
 
 388     def read(self, request, docid, lib):
 
 389         """Read document as raw text"""        
 
 391             revision = request.GET.get('revision', 'latest')
 
 393             if revision == 'latest':
 
 394                 doc = lib.document(docid)
 
 396                 doc = lib.document_for_rev(revision)
 
 399             if document.id != docid:
 
 400                 return response.BadRequest().django_response({'reason': 'name-mismatch',
 
 401                     'message': 'Provided revision is not valid for this document'})
 
 403             bookinfo = dcparser.BookInfo.from_string(doc.data('xml'))
 
 404             return bookinfo.serialize()
 
 405         except (EntryNotFound, RevisionNotFound), e:
 
 406             return response.EntityNotFound().django_response({
 
 407                 'exception': type(e), 'message': e.message})
 
 410     def create(self, request, docid, lib):
 
 412             bi_json = request.POST['contents']
 
 413             revision = request.POST['revision']
 
 415             if request.POST.has_key('message'):
 
 416                 msg = u"$USER$ " + request.PUT['message']
 
 418                 msg = u"$AUTO$ Dublin core update."
 
 420             current = lib.document(docid, request.user.username)
 
 421             orig = lib.document_for_rev(revision)
 
 424                 return response.EntityConflict().django_response({
 
 425                         "reason": "out-of-date",
 
 426                         "provided": orig.revision,
 
 427                         "latest": current.revision })
 
 429             xmldoc = parser.WLDocument.from_string(current.data('xml'))
 
 430             document.book_info = dcparser.BookInfo.from_json(bi_json)
 
 433             ndoc = current.quickwrite('xml', \
 
 434                 document.serialize().encode('utf-8'),\
 
 435                 message=msg, user=request.user.username)
 
 438                 # return the new revision number
 
 442                     "previous_revision": current.revision,
 
 443                     "revision": ndoc.revision,
 
 444                     'timestamp': ndoc.revision.timestamp,
 
 445                     "url": reverse("docdc_view", args=[ndoc.id])
 
 448                 if ndoc: lib._rollback()
 
 450         except RevisionNotFound:
 
 451             return response.EntityNotFound().django_response()
 
 453 class MergeHandler(BaseHandler):
 
 454     allowed_methods = ('POST',)
 
 456     @validate_form(forms.MergeRequestForm, 'POST')
 
 458     def create(self, request, form, docid, lib):
 
 459         """Create a new document revision from the information provided by user"""
 
 461         target_rev = form.cleaned_data['target_revision']
 
 463         doc = lib.document(docid)
 
 464         udoc = doc.take(request.user.username)
 
 466         if target_rev == 'latest':
 
 467             target_rev = udoc.revision
 
 469         if str(udoc.revision) != target_rev:
 
 470             # user think doesn't know he has an old version
 
 473             # Updating is teorericly ok, but we need would
 
 474             # have to force a refresh. Sharing may be not safe,
 
 475             # 'cause it doesn't always result in update.
 
 477             # In other words, we can't lie about the resource's state
 
 478             # So we should just yield and 'out-of-date' conflict
 
 479             # and let the client ask again with updated info.
 
 481             # NOTE: this could result in a race condition, when there
 
 482             # are 2 instances of the same user editing the same document.
 
 483             # Instance "A" trying to update, and instance "B" always changing
 
 484             # the document right before "A". The anwser to this problem is
 
 485             # for the "A" to request a merge from 'latest' and then
 
 486             # check the parent revisions in response, if he actually
 
 487             # merge from where he thinks he should. If not, the client SHOULD
 
 488             # update his internal state.
 
 489             return response.EntityConflict().django_response({
 
 490                     "reason": "out-of-date",
 
 491                     "provided": target_rev,
 
 492                     "latest": udoc.revision })
 
 494         if not request.user.has_perm('explorer.book.can_share'):
 
 495             # User is not permitted to make a merge, right away
 
 496             # So we instead create a pull request in the database
 
 498                 comitter=request.user,
 
 500                 source_revision = str(udoc.revision),
 
 502                 comment = form.cleaned_data['message'] or '$AUTO$ Document shared.'
 
 506             return response.RequestAccepted().django_response(\
 
 507                 ticket_status=prq.status, \
 
 508                 ticket_uri=reverse("pullrequest_view", args=[prq.id]) )
 
 510         if form.cleaned_data['type'] == 'update':
 
 511             # update is always performed from the file branch
 
 513             success, changed = udoc.update(request.user.username)
 
 515         if form.cleaned_data['type'] == 'share':
 
 516             success, changed = udoc.share(form.cleaned_data['message'])
 
 519             return response.EntityConflict().django_response({
 
 520                 'reason': 'merge-failure',
 
 524             return response.SuccessNoContent().django_response()
 
 526         nudoc = udoc.latest()
 
 528         return response.SuccessAllOk().django_response({
 
 530             "parent_user_resivion": udoc.revision,
 
 531             "parent_revision": doc.revision,
 
 532             "revision": nudoc.revision,
 
 533             'timestamp': nudoc.revision.timestamp,