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)
 
  84         for doc in documents.itervalues():
 
  85             doc['parts'].sort(key=natural_order(lambda d: d['name']))
 
  87         return {'documents': sorted(document_tree.itervalues(),
 
  88             key=natural_order(lambda d: d['name']) ) }
 
  90     @validate_form(forms.DocumentUploadForm, 'POST')
 
  92     def create(self, request, form, lib):
 
  93         """Create a new document."""       
 
  95         if form.cleaned_data['ocr_data']:
 
  96             data = form.cleaned_data['ocr_data']
 
  98             data = request.FILES['ocr_file'].read().decode('utf-8')
 
 100         if form.cleaned_data['generate_dc']:
 
 101             data = librarian.wrap_text(data, unicode(date.today()))
 
 103         docid = form.cleaned_data['bookname']
 
 109                 doc = lib.document_create(docid)
 
 110                 # document created, but no content yet
 
 113                     doc = doc.quickwrite('xml', data.encode('utf-8'),
 
 114                         '$AUTO$ XML data uploaded.', user=request.user.username)
 
 116                     # rollback branch creation
 
 118                     raise LibraryException("Exception occured:" + repr(e))
 
 120                 url = reverse('document_view', args=[doc.id])
 
 122                 return response.EntityCreated().django_response(\
 
 126                         'revision': doc.revision },
 
 130         except LibraryException, e:
 
 131             return response.InternalError().django_response(\
 
 132                 {'exception': repr(e) })                
 
 133         except DocumentAlreadyExists:
 
 134             # Document is already there
 
 135             return response.EntityConflict().django_response(\
 
 136                 {"reason": "Document %s already exists." % docid})
 
 141 class BasicDocumentHandler(AnonymousBaseHandler):
 
 142     allowed_methods = ('GET',)
 
 145     def read(self, request, docid, lib):
 
 147             doc = lib.document(docid)
 
 148         except RevisionNotFound:
 
 153             'html_url': reverse('dochtml_view', args=[doc.id]),
 
 154             'text_url': reverse('doctext_view', args=[doc.id]),
 
 155             'dc_url': reverse('docdc_view', args=[doc.id]),
 
 156             'public_revision': doc.revision,
 
 164 class DocumentHandler(BaseHandler):
 
 165     allowed_methods = ('GET', 'PUT')
 
 166     anonymous = BasicDocumentHandler
 
 169     def read(self, request, docid, lib):
 
 170         """Read document's meta data"""       
 
 172             doc = lib.document(docid)
 
 173             udoc = doc.take(request.user.username)
 
 174         except RevisionNotFound, e:
 
 175             return response.EntityNotFound().django_response({
 
 176                 'exception': type(e), 'message': e.message})
 
 178         # is_shared = udoc.ancestorof(doc)
 
 179         # is_uptodate = is_shared or shared.ancestorof(document)
 
 183             'html_url': reverse('dochtml_view', args=[udoc.id]),
 
 184             'text_url': reverse('doctext_view', args=[udoc.id]),
 
 185             'dc_url': reverse('docdc_view', args=[udoc.id]),
 
 186             'gallery_url': reverse('docgallery_view', args=[udoc.id]),
 
 187             'merge_url': reverse('docmerge_view', args=[udoc.id]),
 
 188             'user_revision': udoc.revision,
 
 189             'user_timestamp': udoc.revision.timestamp,
 
 190             'public_revision': doc.revision,
 
 191             'public_timestamp': doc.revision.timestamp,
 
 197     def update(self, request, docid, lib):
 
 198         """Update information about the document, like display not"""
 
 203 class DocumentHTMLHandler(BaseHandler):
 
 204     allowed_methods = ('GET')
 
 207     def read(self, request, docid, lib):
 
 208         """Read document as html text"""
 
 210             revision = request.GET.get('revision', 'latest')
 
 212             if revision == 'latest':
 
 213                 document = lib.document(docid)
 
 215                 document = lib.document_for_rev(revision)
 
 217             if document.id != docid:
 
 218                 return response.BadRequest().django_response({'reason': 'name-mismatch',
 
 219                     'message': 'Provided revision refers, to document "%s", but provided "%s"' % (document.id, docid) })
 
 221             return librarian.html.transform(document.data('xml'), is_file=False)
 
 222         except (EntryNotFound, RevisionNotFound), e:
 
 223             return response.EntityNotFound().django_response({
 
 224                 'exception': type(e), 'message': e.message})
 
 230 from django.core.files.storage import FileSystemStorage
 
 232 class DocumentGalleryHandler(BaseHandler):
 
 233     allowed_methods = ('GET')
 
 235     def read(self, request, docid):
 
 236         """Read meta-data about scans for gallery of this document."""
 
 239         for assoc in GalleryForDocument.objects.filter(document=docid):
 
 240             dirpath = os.path.join(settings.MEDIA_ROOT, assoc.subpath)
 
 242             if not os.path.isdir(dirpath):
 
 243                 print u"[WARNING]: missing gallery %s" % dirpath
 
 246             gallery = {'name': assoc.name, 'pages': []}
 
 248             for file in sorted(os.listdir(dirpath), key=natural_order()):
 
 250                 name, ext = os.path.splitext(os.path.basename(file))
 
 252                 if ext.lower() not in ['.png', '.jpeg', '.jpg']:
 
 253                     print "Ignoring:", name, ext
 
 256                 url = settings.MEDIA_URL + assoc.subpath + u'/' + file.decode('utf-8');
 
 257                 gallery['pages'].append(url)
 
 259             galleries.append(gallery)
 
 267 XINCLUDE_REGEXP = r"""<(?:\w+:)?include\s+[^>]*?href=("|')wlrepo://(?P<link>[^\1]+?)\1\s*[^>]*?>"""
 
 271 class DocumentTextHandler(BaseHandler):
 
 272     allowed_methods = ('GET', 'POST')
 
 275     def read(self, request, docid, lib):
 
 276         """Read document as raw text"""
 
 277         revision = request.GET.get('revision', 'latest')
 
 279             if revision == 'latest':
 
 280                 document = lib.document(docid)
 
 282                 document = lib.document_for_rev(revision)
 
 284             if document.id != docid:
 
 285                 return response.BadRequest().django_response({'reason': 'name-mismatch',
 
 286                     'message': 'Provided revision is not valid for this document'})
 
 288             # TODO: some finer-grained access control
 
 289             return document.data('xml')
 
 290         except (EntryNotFound, RevisionNotFound), e:
 
 291             return response.EntityNotFound().django_response({
 
 292                 'exception': type(e), 'message': e.message})
 
 295     def create(self, request, docid, lib):
 
 297             data = request.POST['contents']
 
 298             revision = request.POST['revision']
 
 300             if request.POST.has_key('message'):
 
 301                 msg = u"$USER$ " + request.POST['message']
 
 303                 msg = u"$AUTO$ XML content update."
 
 305             current = lib.document(docid, request.user.username)
 
 306             orig = lib.document_for_rev(revision)
 
 309                 return response.EntityConflict().django_response({
 
 310                         "reason": "out-of-date",
 
 311                         "provided_revision": orig.revision,
 
 312                         "latest_revision": current.revision })
 
 314             # try to find any Xinclude tags
 
 315             includes = [m.groupdict()['link'] for m in (re.finditer(\
 
 316                 XINCLUDE_REGEXP, data, flags=re.UNICODE) or []) ]
 
 318             print "INCLUDES: ", includes
 
 320             # TODO: provide useful routines to make this simpler
 
 321             def xml_update_action(lib, resolve):
 
 323                     f = lib._fileopen(resolve('parts'), 'r')
 
 324                     stored_includes = json.loads(f.read())
 
 329                 if stored_includes != includes:
 
 330                     f = lib._fileopen(resolve('parts'), 'w+')
 
 331                     f.write(json.dumps(includes))
 
 334                     lib._fileadd(resolve('parts'))
 
 336                     # update the parts cache
 
 337                     PartCache.update_cache(docid, current.owner,\
 
 338                         stored_includes, includes)
 
 340                 # now that the parts are ok, write xml
 
 341                 f = lib._fileopen(resolve('xml'), 'w+')
 
 342                 f.write(data.encode('utf-8'))
 
 346             ndoc = current.invoke_and_commit(\
 
 347                 xml_update_action, lambda d: (msg, current.owner) )
 
 350                 # return the new revision number
 
 351                 return response.SuccessAllOk().django_response({
 
 354                     "previous_revision": current.revision,
 
 355                     "revision": ndoc.revision,
 
 356                     'timestamp': ndoc.revision.timestamp,
 
 357                     "url": reverse("doctext_view", args=[ndoc.id])
 
 360                 if ndoc: lib._rollback()
 
 362         except RevisionNotFound, e:
 
 363             return response.EntityNotFound(mimetype="text/plain").\
 
 364                 django_response(e.message)
 
 368 # Dublin Core handlers
 
 370 # @requires librarian
 
 372 class DocumentDublinCoreHandler(BaseHandler):
 
 373     allowed_methods = ('GET', 'POST')
 
 376     def read(self, request, docid, lib):
 
 377         """Read document as raw text"""        
 
 379             revision = request.GET.get('revision', 'latest')
 
 381             if revision == 'latest':
 
 382                 doc = lib.document(docid)
 
 384                 doc = lib.document_for_rev(revision)
 
 387             if document.id != docid:
 
 388                 return response.BadRequest().django_response({'reason': 'name-mismatch',
 
 389                     'message': 'Provided revision is not valid for this document'})
 
 391             bookinfo = dcparser.BookInfo.from_string(doc.data('xml'))
 
 392             return bookinfo.serialize()
 
 393         except (EntryNotFound, RevisionNotFound), e:
 
 394             return response.EntityNotFound().django_response({
 
 395                 'exception': type(e), 'message': e.message})
 
 398     def create(self, request, docid, lib):
 
 400             bi_json = request.POST['contents']
 
 401             revision = request.POST['revision']
 
 403             if request.POST.has_key('message'):
 
 404                 msg = u"$USER$ " + request.PUT['message']
 
 406                 msg = u"$AUTO$ Dublin core update."
 
 408             current = lib.document(docid, request.user.username)
 
 409             orig = lib.document_for_rev(revision)
 
 412                 return response.EntityConflict().django_response({
 
 413                         "reason": "out-of-date",
 
 414                         "provided": orig.revision,
 
 415                         "latest": current.revision })
 
 417             xmldoc = parser.WLDocument.from_string(current.data('xml'))
 
 418             document.book_info = dcparser.BookInfo.from_json(bi_json)
 
 421             ndoc = current.quickwrite('xml', \
 
 422                 document.serialize().encode('utf-8'),\
 
 423                 message=msg, user=request.user.username)
 
 426                 # return the new revision number
 
 430                     "previous_revision": current.revision,
 
 431                     "revision": ndoc.revision,
 
 432                     'timestamp': ndoc.revision.timestamp,
 
 433                     "url": reverse("docdc_view", args=[ndoc.id])
 
 436                 if ndoc: lib._rollback()
 
 438         except RevisionNotFound:
 
 439             return response.EntityNotFound().django_response()
 
 441 class MergeHandler(BaseHandler):
 
 442     allowed_methods = ('POST',)
 
 444     @validate_form(forms.MergeRequestForm, 'POST')
 
 446     def create(self, request, form, docid, lib):
 
 447         """Create a new document revision from the information provided by user"""
 
 449         target_rev = form.cleaned_data['target_revision']
 
 451         doc = lib.document(docid)
 
 452         udoc = doc.take(request.user.username)
 
 454         if target_rev == 'latest':
 
 455             target_rev = udoc.revision
 
 457         if str(udoc.revision) != target_rev:
 
 458             # user think doesn't know he has an old version
 
 461             # Updating is teorericly ok, but we need would
 
 462             # have to force a refresh. Sharing may be not safe,
 
 463             # 'cause it doesn't always result in update.
 
 465             # In other words, we can't lie about the resource's state
 
 466             # So we should just yield and 'out-of-date' conflict
 
 467             # and let the client ask again with updated info.
 
 469             # NOTE: this could result in a race condition, when there
 
 470             # are 2 instances of the same user editing the same document.
 
 471             # Instance "A" trying to update, and instance "B" always changing
 
 472             # the document right before "A". The anwser to this problem is
 
 473             # for the "A" to request a merge from 'latest' and then
 
 474             # check the parent revisions in response, if he actually
 
 475             # merge from where he thinks he should. If not, the client SHOULD
 
 476             # update his internal state.
 
 477             return response.EntityConflict().django_response({
 
 478                     "reason": "out-of-date",
 
 479                     "provided": target_rev,
 
 480                     "latest": udoc.revision })
 
 482         if not request.user.has_perm('explorer.book.can_share'):
 
 483             # User is not permitted to make a merge, right away
 
 484             # So we instead create a pull request in the database
 
 486                 comitter=request.user,
 
 488                 source_revision = str(udoc.revision),
 
 490                 comment = form.cleaned_data['message'] or '$AUTO$ Document shared.'
 
 494             return response.RequestAccepted().django_response(\
 
 495                 ticket_status=prq.status, \
 
 496                 ticket_uri=reverse("pullrequest_view", args=[prq.id]) )
 
 498         if form.cleaned_data['type'] == 'update':
 
 499             # update is always performed from the file branch
 
 501             success, changed = udoc.update(request.user.username)
 
 503         if form.cleaned_data['type'] == 'share':
 
 504             success, changed = udoc.share(form.cleaned_data['message'])
 
 507             return response.EntityConflict().django_response({
 
 508                 'reason': 'merge-failure',
 
 512             return response.SuccessNoContent().django_response()
 
 514         new_udoc = udoc.latest()
 
 516         return response.SuccessAllOk().django_response({
 
 518             "parent_user_resivion": udoc.revision,
 
 519             "parent_revision": doc.revision,
 
 520             "revision": ndoc.revision,
 
 521             'timestamp': ndoc.revision.timestamp,