2 # -*- encoding: utf-8 -*-
 
   4 __author__= "Ćukasz Rekucki"
 
   5 __date__ = "$2009-09-25 15:49:50$"
 
   6 __doc__ = "Module documentation."
 
   8 from piston.handler import BaseHandler, AnonymousBaseHandler
 
  11 from datetime import date
 
  13 from django.core.urlresolvers import reverse
 
  14 from django.utils import simplejson as json
 
  18 from librarian import dcparser
 
  21 from explorer.models import PullRequest, GalleryForDocument
 
  24 import api.forms as forms
 
  25 import api.response as response
 
  26 from api.utils import validate_form, hglibrary, natural_order
 
  27 from api.models import PartCache
 
  33 # Document List Handlers
 
  35 class BasicLibraryHandler(AnonymousBaseHandler):
 
  36     allowed_methods = ('GET',)
 
  39     def read(self, request, lib):
 
  40         """Return the list of documents."""       
 
  42             'url': reverse('document_view', args=[docid]),
 
  43             'name': docid } for docid in lib.documents() ]
 
  45         return {'documents' : document_list}
 
  48 class LibraryHandler(BaseHandler):
 
  49     allowed_methods = ('GET', 'POST')
 
  50     anonymous = BasicLibraryHandler
 
  53     def read(self, request, lib):
 
  54         """Return the list of documents."""
 
  58         for docid in lib.documents():
 
  59             docid = docid.decode('utf-8')
 
  61                 'url': reverse('document_view', args=[docid]),
 
  66         parts = PartCache.objects.defer('part_id')\
 
  67             .values_list('part_id', 'document_id').distinct()
 
  69         document_tree = dict(documents)
 
  71         for part, docid in parts:
 
  72             # this way, we won't display broken links
 
  73             if not documents.has_key(part):
 
  74                 print "NOT FOUND:", part
 
  77             parent = documents[docid]
 
  78             child = documents[part]
 
  80             # not top-level anymore
 
  81             document_tree.pop(part)
 
  82             parent['parts'].append(child)
 
  87         for doc in documents.itervalues():
 
  88             doc['parts'].sort(key=natural_order(lambda d: d['name']))
 
  90         return {'documents': sorted(document_tree.itervalues(),
 
  91             key=natural_order(lambda d: d['name']) ) }
 
  93     @validate_form(forms.DocumentUploadForm, 'POST')
 
  95     def create(self, request, form, lib):
 
  96         """Create a new document."""       
 
  98         if form.cleaned_data['ocr_data']:
 
  99             data = form.cleaned_data['ocr_data']
 
 101             data = request.FILES['ocr_file'].read().decode('utf-8')
 
 103         if form.cleaned_data['generate_dc']:
 
 104             data = librarian.wrap_text(data, unicode(date.today()))
 
 106         docid = form.cleaned_data['bookname']
 
 112                 doc = lib.document_create(docid)
 
 113                 # document created, but no content yet
 
 116                     doc = doc.quickwrite('xml', data.encode('utf-8'),
 
 117                         '$AUTO$ XML data uploaded.', user=request.user.username)
 
 119                     # rollback branch creation
 
 121                     raise LibraryException("Exception occured:" + repr(e))
 
 123                 url = reverse('document_view', args=[doc.id])
 
 125                 return response.EntityCreated().django_response(\
 
 129                         'revision': doc.revision },
 
 133         except LibraryException, e:
 
 134             return response.InternalError().django_response(\
 
 135                 {'exception': repr(e) })                
 
 136         except DocumentAlreadyExists:
 
 137             # Document is already there
 
 138             return response.EntityConflict().django_response(\
 
 139                 {"reason": "Document %s already exists." % docid})
 
 144 class BasicDocumentHandler(AnonymousBaseHandler):
 
 145     allowed_methods = ('GET',)
 
 148     def read(self, request, docid, lib):
 
 150             doc = lib.document(docid)
 
 151         except RevisionNotFound:
 
 156             'html_url': reverse('dochtml_view', args=[doc.id]),
 
 157             'text_url': reverse('doctext_view', args=[doc.id]),
 
 158             'dc_url': reverse('docdc_view', args=[doc.id]),
 
 159             'public_revision': doc.revision,
 
 167 class DocumentHandler(BaseHandler):
 
 168     allowed_methods = ('GET', 'PUT')
 
 169     anonymous = BasicDocumentHandler
 
 172     def read(self, request, docid, lib):
 
 173         """Read document's meta data"""       
 
 175             doc = lib.document(docid)
 
 176             udoc = doc.take(request.user.username)
 
 177         except RevisionNotFound, e:
 
 178             return response.EntityNotFound().django_response({
 
 179                 'exception': type(e), 'message': e.message})
 
 181         # is_shared = udoc.ancestorof(doc)
 
 182         # is_uptodate = is_shared or shared.ancestorof(document)
 
 186             'html_url': reverse('dochtml_view', args=[udoc.id]),
 
 187             'text_url': reverse('doctext_view', args=[udoc.id]),
 
 188             'dc_url': reverse('docdc_view', args=[udoc.id]),
 
 189             'gallery_url': reverse('docgallery_view', args=[udoc.id]),
 
 190             'merge_url': reverse('docmerge_view', args=[udoc.id]),
 
 191             'user_revision': udoc.revision,
 
 192             'user_timestamp': udoc.revision.timestamp,
 
 193             'public_revision': doc.revision,
 
 194             'public_timestamp': doc.revision.timestamp,
 
 200     def update(self, request, docid, lib):
 
 201         """Update information about the document, like display not"""
 
 206 class DocumentHTMLHandler(BaseHandler):
 
 207     allowed_methods = ('GET')
 
 210     def read(self, request, docid, lib):
 
 211         """Read document as html text"""
 
 213             revision = request.GET.get('revision', 'latest')
 
 215             if revision == 'latest':
 
 216                 document = lib.document(docid)
 
 218                 document = lib.document_for_rev(revision)
 
 220             if document.id != docid:
 
 221                 return response.BadRequest().django_response({'reason': 'name-mismatch',
 
 222                     'message': 'Provided revision refers, to document "%s", but provided "%s"' % (document.id, docid) })
 
 224             return librarian.html.transform(document.data('xml'), is_file=False)
 
 225         except (EntryNotFound, RevisionNotFound), e:
 
 226             return response.EntityNotFound().django_response({
 
 227                 'exception': type(e), 'message': e.message})
 
 233 from django.core.files.storage import FileSystemStorage
 
 235 class DocumentGalleryHandler(BaseHandler):
 
 236     allowed_methods = ('GET')
 
 238     def read(self, request, docid):
 
 239         """Read meta-data about scans for gallery of this document."""
 
 242         for assoc in GalleryForDocument.objects.filter(document=docid):
 
 243             dirpath = os.path.join(settings.MEDIA_ROOT, assoc.subpath)
 
 245             if not os.path.isdir(dirpath):
 
 246                 print u"[WARNING]: missing gallery %s" % dirpath
 
 249             gallery = {'name': assoc.name, 'pages': []}
 
 251             for file in sorted(os.listdir(dirpath), key=natural_order()):
 
 253                 name, ext = os.path.splitext(os.path.basename(file))
 
 255                 if ext.lower() not in ['.png', '.jpeg', '.jpg']:
 
 256                     print "Ignoring:", name, ext
 
 259                 url = settings.MEDIA_URL + assoc.subpath + u'/' + file.decode('utf-8');
 
 260                 gallery['pages'].append(url)
 
 262             galleries.append(gallery)
 
 270 XINCLUDE_REGEXP = r"""<(?:\w+:)?include\s+[^>]*?href=("|')wlrepo://(?P<link>[^\1]+?)\1\s*[^>]*?>"""
 
 274 class DocumentTextHandler(BaseHandler):
 
 275     allowed_methods = ('GET', 'POST')
 
 278     def read(self, request, docid, lib):
 
 279         """Read document as raw text"""
 
 280         revision = request.GET.get('revision', 'latest')
 
 282             if revision == 'latest':
 
 283                 document = lib.document(docid)
 
 285                 document = lib.document_for_rev(revision)
 
 287             if document.id != docid:
 
 288                 return response.BadRequest().django_response({'reason': 'name-mismatch',
 
 289                     'message': 'Provided revision is not valid for this document'})
 
 291             # TODO: some finer-grained access control
 
 292             return document.data('xml')
 
 293         except (EntryNotFound, RevisionNotFound), e:
 
 294             return response.EntityNotFound().django_response({
 
 295                 'exception': type(e), 'message': e.message})
 
 298     def create(self, request, docid, lib):
 
 300             data = request.POST['contents']
 
 301             revision = request.POST['revision']
 
 303             if request.POST.has_key('message'):
 
 304                 msg = u"$USER$ " + request.POST['message']
 
 306                 msg = u"$AUTO$ XML content update."
 
 308             current = lib.document(docid, request.user.username)
 
 309             orig = lib.document_for_rev(revision)
 
 312                 return response.EntityConflict().django_response({
 
 313                         "reason": "out-of-date",
 
 314                         "provided_revision": orig.revision,
 
 315                         "latest_revision": current.revision })
 
 317             # try to find any Xinclude tags
 
 318             includes = [m.groupdict()['link'] for m in (re.finditer(\
 
 319                 XINCLUDE_REGEXP, data, flags=re.UNICODE) or []) ]
 
 321             print "INCLUDES: ", includes
 
 323             # TODO: provide useful routines to make this simpler
 
 324             def xml_update_action(lib, resolve):
 
 326                     f = lib._fileopen(resolve('parts'), 'r')
 
 327                     stored_includes = json.loads(f.read())
 
 332                 if stored_includes != includes:
 
 333                     f = lib._fileopen(resolve('parts'), 'w+')
 
 334                     f.write(json.dumps(includes))
 
 337                     lib._fileadd(resolve('parts'))
 
 339                     # update the parts cache
 
 340                     PartCache.update_cache(docid, current.owner,\
 
 341                         stored_includes, includes)
 
 343                 # now that the parts are ok, write xml
 
 344                 f = lib._fileopen(resolve('xml'), 'w+')
 
 345                 f.write(data.encode('utf-8'))
 
 349             ndoc = current.invoke_and_commit(\
 
 350                 xml_update_action, lambda d: (msg, current.owner) )
 
 353                 # return the new revision number
 
 354                 return response.SuccessAllOk().django_response({
 
 357                     "previous_revision": current.revision,
 
 358                     "revision": ndoc.revision,
 
 359                     'timestamp': ndoc.revision.timestamp,
 
 360                     "url": reverse("doctext_view", args=[ndoc.id])
 
 363                 if ndoc: lib._rollback()
 
 365         except RevisionNotFound, e:
 
 366             return response.EntityNotFound(mimetype="text/plain").\
 
 367                 django_response(e.message)
 
 371 # Dublin Core handlers
 
 373 # @requires librarian
 
 375 class DocumentDublinCoreHandler(BaseHandler):
 
 376     allowed_methods = ('GET', 'POST')
 
 379     def read(self, request, docid, lib):
 
 380         """Read document as raw text"""        
 
 382             revision = request.GET.get('revision', 'latest')
 
 384             if revision == 'latest':
 
 385                 doc = lib.document(docid)
 
 387                 doc = lib.document_for_rev(revision)
 
 390             if document.id != docid:
 
 391                 return response.BadRequest().django_response({'reason': 'name-mismatch',
 
 392                     'message': 'Provided revision is not valid for this document'})
 
 394             bookinfo = dcparser.BookInfo.from_string(doc.data('xml'))
 
 395             return bookinfo.serialize()
 
 396         except (EntryNotFound, RevisionNotFound), e:
 
 397             return response.EntityNotFound().django_response({
 
 398                 'exception': type(e), 'message': e.message})
 
 401     def create(self, request, docid, lib):
 
 403             bi_json = request.POST['contents']
 
 404             revision = request.POST['revision']
 
 406             if request.POST.has_key('message'):
 
 407                 msg = u"$USER$ " + request.PUT['message']
 
 409                 msg = u"$AUTO$ Dublin core update."
 
 411             current = lib.document(docid, request.user.username)
 
 412             orig = lib.document_for_rev(revision)
 
 415                 return response.EntityConflict().django_response({
 
 416                         "reason": "out-of-date",
 
 417                         "provided": orig.revision,
 
 418                         "latest": current.revision })
 
 420             xmldoc = parser.WLDocument.from_string(current.data('xml'))
 
 421             document.book_info = dcparser.BookInfo.from_json(bi_json)
 
 424             ndoc = current.quickwrite('xml', \
 
 425                 document.serialize().encode('utf-8'),\
 
 426                 message=msg, user=request.user.username)
 
 429                 # return the new revision number
 
 433                     "previous_revision": current.revision,
 
 434                     "revision": ndoc.revision,
 
 435                     'timestamp': ndoc.revision.timestamp,
 
 436                     "url": reverse("docdc_view", args=[ndoc.id])
 
 439                 if ndoc: lib._rollback()
 
 441         except RevisionNotFound:
 
 442             return response.EntityNotFound().django_response()
 
 444 class MergeHandler(BaseHandler):
 
 445     allowed_methods = ('POST',)
 
 447     @validate_form(forms.MergeRequestForm, 'POST')
 
 449     def create(self, request, form, docid, lib):
 
 450         """Create a new document revision from the information provided by user"""
 
 452         target_rev = form.cleaned_data['target_revision']
 
 454         doc = lib.document(docid)
 
 455         udoc = doc.take(request.user.username)
 
 457         if target_rev == 'latest':
 
 458             target_rev = udoc.revision
 
 460         if str(udoc.revision) != target_rev:
 
 461             # user think doesn't know he has an old version
 
 464             # Updating is teorericly ok, but we need would
 
 465             # have to force a refresh. Sharing may be not safe,
 
 466             # 'cause it doesn't always result in update.
 
 468             # In other words, we can't lie about the resource's state
 
 469             # So we should just yield and 'out-of-date' conflict
 
 470             # and let the client ask again with updated info.
 
 472             # NOTE: this could result in a race condition, when there
 
 473             # are 2 instances of the same user editing the same document.
 
 474             # Instance "A" trying to update, and instance "B" always changing
 
 475             # the document right before "A". The anwser to this problem is
 
 476             # for the "A" to request a merge from 'latest' and then
 
 477             # check the parent revisions in response, if he actually
 
 478             # merge from where he thinks he should. If not, the client SHOULD
 
 479             # update his internal state.
 
 480             return response.EntityConflict().django_response({
 
 481                     "reason": "out-of-date",
 
 482                     "provided": target_rev,
 
 483                     "latest": udoc.revision })
 
 485         if not request.user.has_perm('explorer.book.can_share'):
 
 486             # User is not permitted to make a merge, right away
 
 487             # So we instead create a pull request in the database
 
 489                 comitter=request.user,
 
 491                 source_revision = str(udoc.revision),
 
 493                 comment = form.cleaned_data['message'] or '$AUTO$ Document shared.'
 
 497             return response.RequestAccepted().django_response(\
 
 498                 ticket_status=prq.status, \
 
 499                 ticket_uri=reverse("pullrequest_view", args=[prq.id]) )
 
 501         if form.cleaned_data['type'] == 'update':
 
 502             # update is always performed from the file branch
 
 504             success, changed = udoc.update(request.user.username)
 
 506         if form.cleaned_data['type'] == 'share':
 
 507             success, changed = udoc.share(form.cleaned_data['message'])
 
 510             return response.EntityConflict().django_response({
 
 511                 'reason': 'merge-failure',
 
 515             return response.SuccessNoContent().django_response()
 
 517         new_udoc = udoc.latest()
 
 519         return response.SuccessAllOk().django_response({
 
 521             "parent_user_resivion": udoc.revision,
 
 522             "parent_revision": doc.revision,
 
 523             "revision": ndoc.revision,
 
 524             'timestamp': ndoc.revision.timestamp,