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
 
  12 from django.http import HttpResponse
 
  14 from datetime import date
 
  16 from django.core.urlresolvers import reverse
 
  17 from django.db import IntegrityError
 
  22 from librarian import dcparser, parser
 
  25 from api.models import PullRequest
 
  26 from explorer.models import GalleryForDocument
 
  29 import api.forms as forms
 
  30 import api.response as response
 
  31 from api.utils import validate_form, hglibrary, natural_order
 
  32 from api.models import PartCache, PullRequest
 
  34 from pygments import highlight
 
  35 from pygments.lexers import DiffLexer
 
  36 from pygments.formatters import HtmlFormatter
 
  43     return username.startswith('$prq-')
 
  45 def prq_for_user(username):
 
  47         return PullRequest.objects.get(id=int(username[5:]))
 
  51 def check_user(request, user):
 
  52     log.info("user: %r, perm: %r" % (request.user, request.user.get_all_permissions()) )
 
  55         if not request.user.has_perm('api.view_prq'):
 
  56             yield response.AccessDenied().django_response({
 
  57                 'reason': 'access-denied',
 
  58                 'message': "You don't have enough priviliges to view pull requests."
 
  61     elif request.user.username != user:
 
  62         if not request.user.has_perm('api.view_other_document'):
 
  63             yield response.AccessDenied().django_response({
 
  64                 'reason': 'access-denied',
 
  65                 'message': "You don't have enough priviliges to view other people's document."
 
  70 # Document List Handlers
 
  72 # TODO: security check
 
  73 class BasicLibraryHandler(AnonymousBaseHandler):
 
  74     allowed_methods = ('GET',)
 
  77     def read(self, request, lib):
 
  78         """Return the list of documents."""       
 
  80             'url': reverse('document_view', args=[docid]),
 
  81             'name': docid } for docid in lib.documents() ]
 
  82         return {'documents' : document_list}
 
  85 # This handler controlls the document collection
 
  87 class LibraryHandler(BaseHandler):
 
  88     allowed_methods = ('GET', 'POST')
 
  89     anonymous = BasicLibraryHandler
 
  92     def read(self, request, lib):
 
  93         """Return the list of documents."""
 
  97         for docid in lib.documents():            
 
  99                 'url': reverse('document_view', args=[docid]),
 
 104         parts = PartCache.objects.defer('part_id')\
 
 105             .values_list('part_id', 'document_id').distinct()
 
 107         document_tree = dict(documents)
 
 109         for part, docid in parts:
 
 110             # this way, we won't display broken links
 
 111             if not documents.has_key(part):
 
 112                 log.info("NOT FOUND: %s", part)
 
 115             parent = documents[docid]
 
 116             child = documents[part]
 
 118             # not top-level anymore
 
 119             document_tree.pop(part)
 
 120             parent['parts'].append(child)
 
 122         for doc in documents.itervalues():
 
 123             doc['parts'].sort(key=natural_order(lambda d: d['name']))
 
 125         return {'documents': sorted(document_tree.itervalues(),
 
 126             key=natural_order(lambda d: d['name']) ) }
 
 129     @validate_form(forms.DocumentUploadForm, 'POST')
 
 131     def create(self, request, form, lib):
 
 132         """Create a new document."""       
 
 134         if form.cleaned_data['ocr_data']:
 
 135             data = form.cleaned_data['ocr_data']
 
 137             data = request.FILES['ocr_file'].read().decode('utf-8')
 
 140             return response.BadRequest().django_response('You must pass ocr_data or ocr_file.')
 
 142         if form.cleaned_data['generate_dc']:
 
 143             data = librarian.wrap_text(data, unicode(date.today()))
 
 145         docid = form.cleaned_data['bookname']
 
 150                 log.info("DOCID %s", docid)
 
 151                 doc = lib.document_create(docid)
 
 152                 # document created, but no content yet
 
 154                     doc = doc.quickwrite('xml', data.encode('utf-8'),
 
 155                         '$AUTO$ XML data uploaded.', user=request.user.username)
 
 158                     # rollback branch creation
 
 160                     raise LibraryException(traceback.format_exc())
 
 162                 url = reverse('document_view', args=[doc.id])
 
 164                 return response.EntityCreated().django_response(\
 
 168                         'revision': doc.revision },
 
 172         except LibraryException, e:
 
 174             return response.InternalError().django_response({
 
 175                 "reason": traceback.format_exc()
 
 177         except DocumentAlreadyExists:
 
 178             # Document is already there
 
 179             return response.EntityConflict().django_response({
 
 180                 "reason": "already-exists",
 
 181                 "message": "Document already exists." % docid
 
 187 class BasicDocumentHandler(AnonymousBaseHandler):
 
 188     allowed_methods = ('GET',)
 
 191     def read(self, request, docid, lib):
 
 193             doc = lib.document(docid)
 
 194         except RevisionNotFound:
 
 199             'html_url': reverse('dochtml_view', args=[doc.id]),
 
 200             'text_url': reverse('doctext_view', args=[doc.id]),
 
 201             'dc_url': reverse('docdc_view', args=[doc.id]),
 
 202             'public_revision': doc.revision,
 
 208 class DiffHandler(BaseHandler):
 
 209     allowed_methods = ('GET',)
 
 212     def read(self, request, docid, lib):
 
 213         '''Return diff between source_revision and target_revision)'''        
 
 214         revision = request.GET.get('revision')
 
 217         source_document = lib.document(docid)
 
 218         target_document = lib.document_for_revision(revision)
 
 219         print source_document, target_document
 
 221         diff = difflib.unified_diff(
 
 222             source_document.data('xml').splitlines(True),
 
 223             target_document.data('xml').splitlines(True),
 
 227         s =  ''.join(list(diff))
 
 228         return highlight(s, DiffLexer(), HtmlFormatter(cssclass="pastie"))
 
 234 class DocumentHandler(BaseHandler):
 
 235     allowed_methods = ('GET', 'PUT')
 
 236     anonymous = BasicDocumentHandler
 
 238     @validate_form(forms.DocumentRetrieveForm, 'GET')
 
 240     def read(self, request, form, docid, lib):
 
 241         """Read document's meta data"""       
 
 242         log.info(u"User '%s' wants to edit %s(%s) as %s" % \
 
 243             (request.user.username, docid, form.cleaned_data['revision'], form.cleaned_data['user']) )
 
 245         user = form.cleaned_data['user'] or request.user.username
 
 246         rev = form.cleaned_data['revision'] or 'latest'
 
 248         for error in check_user(request, user):
 
 252             doc = lib.document(docid, user, rev=rev)
 
 253         except RevisionMismatch, e:
 
 254             # the document exists, but the revision is bad
 
 255             return response.EntityNotFound().django_response({
 
 256                 'reason': 'revision-mismatch',
 
 257                 'message': e.message,
 
 261         except RevisionNotFound, e:
 
 262             # the user doesn't have this document checked out
 
 263             # or some other weird error occured
 
 264             # try to do the checkout
 
 266                 if user == request.user.username:
 
 267                     mdoc = lib.document(docid)
 
 268                     doc = mdoc.take(user)
 
 270                     prq = prq_for_user(user)
 
 271                     # commiter's document
 
 272                     prq_doc = lib.document_for_revision(prq.source_revision)
 
 273                     doc = prq_doc.take(user)
 
 275                     return response.EntityNotFound().django_response({
 
 276                         'reason': 'document-not-found',
 
 277                         'message': e.message,
 
 281             except RevisionNotFound, e:
 
 282                 return response.EntityNotFound().django_response({
 
 283                     'reason': 'document-not-found',
 
 284                     'message': e.message,
 
 292             'html_url': reverse('dochtml_view', args=[doc.id]),
 
 293             'text_url': reverse('doctext_view', args=[doc.id]),
 
 294             # 'dc_url': reverse('docdc_view', args=[doc.id]),
 
 295             'gallery_url': reverse('docgallery_view', args=[doc.id]),
 
 296             'merge_url': reverse('docmerge_view', args=[doc.id]),
 
 297             'revision': doc.revision,
 
 298             'timestamp': doc.revision.timestamp,
 
 299             # 'public_revision': doc.revision,
 
 300             # 'public_timestamp': doc.revision.timestamp,
 
 305 #    def update(self, request, docid, lib):
 
 306 #        """Update information about the document, like display not"""
 
 311 class DocumentHTMLHandler(BaseHandler):
 
 312     allowed_methods = ('GET')
 
 314     @validate_form(forms.DocumentRetrieveForm, 'GET')
 
 316     def read(self, request, form, docid, lib, stylesheet='partial'):
 
 317         """Read document as html text"""
 
 319             revision = form.cleaned_data['revision']
 
 320             user = form.cleaned_data['user'] or request.user.username
 
 321             document = lib.document_for_revision(revision)
 
 323             if document.id != docid:
 
 324                 return response.BadRequest().django_response({
 
 325                     'reason': 'name-mismatch',
 
 326                     'message': 'Provided revision is not valid for this document'
 
 329             if document.owner != user:
 
 330                 return response.BadRequest().django_response({
 
 331                     'reason': 'user-mismatch',
 
 332                     'message': "Provided revision doesn't belong to user %s" % user
 
 335             for error in check_user(request, user):
 
 338             return librarian.html.transform(document.data('xml'), is_file=False, \
 
 339                 parse_dublincore=False, stylesheet='full',\
 
 341                     "with-paths": 'boolean(1)',                    
 
 344         except (EntryNotFound, RevisionNotFound), e:
 
 345             return response.EntityNotFound().django_response({
 
 346                 'reason': 'not-found', 'message': e.message})
 
 347         except librarian.ValidationError, e:
 
 348             return response.InternalError().django_response({
 
 349                 'reason': 'xml-non-valid', 'message': e.message or u''})
 
 350         except librarian.ParseError, e:
 
 351             return response.InternalError().django_response({
 
 352                 'reason': 'xml-parse-error', 'message': e.message or u'' })
 
 358 class DocumentGalleryHandler(BaseHandler):
 
 359     allowed_methods = ('GET')
 
 362     def read(self, request, docid):
 
 363         """Read meta-data about scans for gallery of this document."""
 
 365         from urllib import quote
 
 367         for assoc in GalleryForDocument.objects.filter(document=docid):
 
 368             dirpath = os.path.join(settings.MEDIA_ROOT, assoc.subpath)
 
 370             if not os.path.isdir(dirpath):
 
 371                 log.warn(u"[WARNING]: missing gallery %s", dirpath)
 
 374             gallery = {'name': assoc.name, 'pages': []}
 
 376             for file in os.listdir(dirpath):
 
 377                 if not isinstance(file, unicode):
 
 379                         file = file.decode('utf-8')
 
 381                         log.warn(u"File %r in gallery %r is not unicode. Ommiting."\
 
 386                     name, ext = os.path.splitext(os.path.basename(file))
 
 388                     if ext.lower() not in [u'.png', u'.jpeg', u'.jpg']:
 
 389                         log.warn(u"Ignoring: %s %s", name, ext)
 
 392                     url = settings.MEDIA_URL + assoc.subpath + u'/' + file
 
 395                     url = settings.MEDIA_URL + u'/missing.png'
 
 397                 gallery['pages'].append( quote(url.encode('utf-8')) )
 
 399             gallery['pages'].sort()
 
 400             galleries.append(gallery)
 
 407 # Dublin Core handlers
 
 409 # @requires librarian
 
 411 #class DocumentDublinCoreHandler(BaseHandler):
 
 412 #    allowed_methods = ('GET', 'POST')
 
 415 #    def read(self, request, docid, lib):
 
 416 #        """Read document as raw text"""
 
 418 #            revision = request.GET.get('revision', 'latest')
 
 420 #            if revision == 'latest':
 
 421 #                doc = lib.document(docid)
 
 423 #                doc = lib.document_for_revision(revision)
 
 426 #            if document.id != docid:
 
 427 #                return response.BadRequest().django_response({'reason': 'name-mismatch',
 
 428 #                    'message': 'Provided revision is not valid for this document'})
 
 430 #            bookinfo = dcparser.BookInfo.from_string(doc.data('xml'))
 
 431 #            return bookinfo.serialize()
 
 432 #        except (EntryNotFound, RevisionNotFound), e:
 
 433 #            return response.EntityNotFound().django_response({
 
 434 #                'exception': type(e), 'message': e.message})
 
 437 #    def create(self, request, docid, lib):
 
 439 #            bi_json = request.POST['contents']
 
 440 #            revision = request.POST['revision']
 
 442 #            if request.POST.has_key('message'):
 
 443 #                msg = u"$USER$ " + request.PUT['message']
 
 445 #                msg = u"$AUTO$ Dublin core update."
 
 447 #            current = lib.document(docid, request.user.username)
 
 448 #            orig = lib.document_for_revision(revision)
 
 450 #            if current != orig:
 
 451 #                return response.EntityConflict().django_response({
 
 452 #                        "reason": "out-of-date",
 
 453 #                        "provided": orig.revision,
 
 454 #                        "latest": current.revision })
 
 456 #            xmldoc = parser.WLDocument.from_string(current.data('xml'))
 
 457 #            document.book_info = dcparser.BookInfo.from_json(bi_json)
 
 460 #            ndoc = current.quickwrite('xml', \
 
 461 #                document.serialize().encode('utf-8'),\
 
 462 #                message=msg, user=request.user.username)
 
 465 #                # return the new revision number
 
 467 #                    "document": ndoc.id,
 
 469 #                    "previous_revision": current.revision,
 
 470 #                    "revision": ndoc.revision,
 
 471 #                    'timestamp': ndoc.revision.timestamp,
 
 472 #                    "url": reverse("docdc_view", args=[ndoc.id])
 
 474 #            except Exception, e:
 
 475 #                if ndoc: lib._rollback()
 
 477 #        except RevisionNotFound:
 
 478 #            return response.EntityNotFound().django_response()
 
 480 class MergeHandler(BaseHandler):
 
 481     allowed_methods = ('POST',)
 
 483     @validate_form(forms.MergeRequestForm, 'POST')
 
 485     def create(self, request, form, docid, lib):
 
 486         """Create a new document revision from the information provided by user"""
 
 487         revision = form.cleaned_data['revision']
 
 489         # fetch the main branch document
 
 490         doc = lib.document(docid)
 
 492         # fetch the base document
 
 493         user_doc = lib.document_for_revision(revision)
 
 494         base_doc = user_doc.latest()
 
 496         if base_doc != user_doc:
 
 497             return response.EntityConflict().django_response({
 
 498                 "reason": "out-of-date",
 
 499                 "provided": str(user_doc.revision),
 
 500                 "latest": str(base_doc.revision)
 
 503         if form.cleaned_data['type'] == 'update':
 
 504             # update is always performed from the file branch
 
 506             user_doc_new = base_doc.update(request.user.username)
 
 508             if user_doc_new == user_doc:
 
 509                 return response.SuccessAllOk().django_response({
 
 513             # shared document is the same
 
 516         if form.cleaned_data['type'] == 'share':
 
 517             if not base_doc.up_to_date():
 
 518                 return response.BadRequest().django_response({
 
 519                     "reason": "not-fast-forward",
 
 520                     "message": "You must first update your branch to the latest version."
 
 523             anwser, info = base_doc.would_share()
 
 526                 return response.SuccessAllOk().django_response({
 
 527                     "result": "no-op", "message": info
 
 530             # check for unresolved conflicts            
 
 531             if base_doc.has_conflict_marks():
 
 532                 return response.BadRequest().django_response({                    
 
 533                     "reason": "unresolved-conflicts",
 
 534                     "message": "There are unresolved conflicts in your file. Fix them, and try again."
 
 537             if not request.user.has_perm('api.share_document'):
 
 538                 # User is not permitted to make a merge, right away
 
 539                 # So we instead create a pull request in the database
 
 541                     prq, created = PullRequest.objects.get_or_create(
 
 542                         comitter = request.user,
 
 546                             'source_revision': str(base_doc.revision),
 
 547                             'comment': form.cleaned_data['message'] or '$AUTO$ Document shared.',
 
 551                     # there can't be 2 pending request from same user
 
 552                     # for the same document
 
 554                         prq.source_revision = str(base_doc.revision)
 
 555                         prq.comment = prq.comment + 'u\n\n' + (form.cleaned_data['message'] or u'')
 
 558                     return response.RequestAccepted().django_response(\
 
 559                         ticket_status=prq.status, \
 
 560                         ticket_uri=reverse("pullrequest_view", args=[prq.id]) )
 
 561                 except IntegrityError:
 
 562                     return response.EntityConflict().django_response({
 
 563                         'reason': 'request-already-exist'
 
 566             changed = base_doc.share(form.cleaned_data['message'])
 
 568             # update shared version if needed
 
 570                 doc_new = doc.latest()
 
 574             # the user wersion is the same
 
 575             user_doc_new = base_doc
 
 577         # The client can compare parent_revision to revision
 
 578         # to see if he needs to update user's view        
 
 579         # Same goes for shared view
 
 581         return response.SuccessAllOk().django_response({
 
 583             "name": user_doc_new.id,
 
 584             "user": user_doc_new.owner,
 
 586             "revision": user_doc_new.revision,
 
 587             'timestamp': user_doc_new.revision.timestamp,
 
 589             "parent_revision": user_doc.revision,
 
 590             "parent_timestamp": user_doc.revision.timestamp,
 
 592             "shared_revision": doc_new.revision,
 
 593             "shared_timestamp": doc_new.revision.timestamp,
 
 595             "shared_parent_revision": doc.revision,
 
 596             "shared_parent_timestamp": doc.revision.timestamp,