Client message reporting refactor.
authorŁukasz Rekucki <lrekucki@gmail.com>
Tue, 20 Oct 2009 12:39:52 +0000 (14:39 +0200)
committerŁukasz Rekucki <lrekucki@gmail.com>
Tue, 20 Oct 2009 12:39:52 +0000 (14:39 +0200)
Better pull/push handling.

15 files changed:
apps/api/forms.py
apps/api/handlers/library_handlers.py
apps/api/handlers/text_handler.py [new file with mode: 0644]
apps/api/resources.py
apps/api/response.py
lib/wlrepo/__init__.py
lib/wlrepo/mercurial_backend/__init__.py
lib/wlrepo/mercurial_backend/document.py
lib/wlrepo/mercurial_backend/library.py
lib/wlrepo/mercurial_backend/revision.py [new file with mode: 0644]
platforma/static/css/html.css
platforma/static/js/app.js
platforma/static/js/models.js
platforma/static/js/views/editor.js
platforma/urls.py

index 2c9ae66..7532e17 100644 (file)
@@ -11,7 +11,6 @@ from django.contrib.auth.models import User
 import re
 from django.utils import simplejson as json
 
-
 class MergeRequestForm(forms.Form):
     # should the target document revision be updated or shared
     type = forms.ChoiceField(choices=(('update', 'Update'), ('share', 'Share')) )
@@ -63,9 +62,9 @@ PRQ_USER_RE = re.compile(r"^\$prq-(\d{1,10})$", re.UNICODE)
 class DocumentRetrieveForm(forms.Form):    
     revision = forms.RegexField(regex=r'latest|[0-9a-z]{40}', required=False)
     user = forms.CharField(required=False)
-
+        
     def clean_user(self):
-        # why, oh why does django doesn't implement this!!!
+        # why, oh why doesn't django implement this!!!
         # value = super(DocumentRetrieveForm, self).clean_user()
         value = self.cleaned_data['user']        
         
@@ -89,13 +88,34 @@ class DocumentRetrieveForm(forms.Form):
            
 
 class TextRetrieveForm(DocumentRetrieveForm):
-    part = forms.CharField(required=False)
+    chunk = forms.CharField(required=False)
+    format = forms.CharField(required=False)
+
+    def clean_format(self):
+        value = self.cleaned_data['format']
+        if not value:
+            return 'raw'
+
+        if value not in ('nl', 'raw'):
+            raise forms.ValidationError("Invalid text format")
+        return value
 
 class TextUpdateForm(DocumentRetrieveForm):
     message = forms.CharField(required=False)
     contents = forms.CharField(required=False)
     chunks = forms.CharField(required=False)
 
+    format = forms.CharField(required=False)
+
+    def clean_format(self):
+        value = self.cleaned_data['format']
+        if not value:
+            return 'raw'
+
+        if value not in ('nl', 'raw'):
+            raise forms.ValidationError("Invalid text format")
+        return value
+
     def clean_message(self):
         value = self.cleaned_data['message']
 
index 0635861..aef1e9c 100644 (file)
@@ -10,16 +10,13 @@ __doc__ = "Module documentation."
 
 from piston.handler import BaseHandler, AnonymousBaseHandler
 
-import re
 from datetime import date
 
 from django.core.urlresolvers import reverse
-from django.utils import simplejson as json
 from django.db import IntegrityError
 
 import librarian
 import librarian.html
-from librarian import dcparser, parser
 
 from wlrepo import *
 from api.models import PullRequest
@@ -370,162 +367,6 @@ class DocumentGalleryHandler(BaseHandler):
 
         return galleries
 
-#
-# Document Text View
-#
-
-XINCLUDE_REGEXP = r"""<(?:\w+:)?include\s+[^>]*?href=("|')wlrepo://(?P<link>[^\1]+?)\1\s*[^>]*?>"""
-#
-#
-#
-
-class DocumentTextHandler(BaseHandler):
-    allowed_methods = ('GET', 'POST')
-
-    @validate_form(forms.TextRetrieveForm, 'GET')
-    @hglibrary
-    def read(self, request, form, docid, lib):
-        """Read document as raw text"""        
-        try:
-            revision = form.cleaned_data['revision']
-            part = form.cleaned_data['part']
-            user = form.cleaned_data['user'] or request.user.username            
-            
-            document = lib.document_for_rev(revision)
-            
-            if document.id != docid:
-                return response.BadRequest().django_response({
-                    'reason': 'name-mismatch',
-                    'message': 'Provided revision is not valid for this document'
-                })
-
-            if document.owner != user:
-                return response.BadRequest().django_response({
-                    'reason': 'user-mismatch',
-                    'message': "Provided revision doesn't belong to user %s" % user
-                })
-
-            for error in check_user(request, user):
-                return error
-            
-            if not part:                
-                return document.data('xml')
-            
-            xdoc = parser.WLDocument.from_string(document.data('xml'),\
-                parse_dublincore=False)
-            ptext = xdoc.part_as_text(part)
-
-            if ptext is None:
-                return response.EntityNotFound().django_response({
-                      'reason': 'no-part-in-document'                     
-                })
-
-            return ptext
-        except librarian.ParseError, e:
-            return response.EntityNotFound().django_response({
-                'reason': 'invalid-document-state',
-                'exception': type(e),
-                'message': e.message
-            })
-        except (EntryNotFound, RevisionNotFound), e:
-            return response.EntityNotFound().django_response({
-                'reason': 'not-found',
-                'exception': type(e), 'message': e.message
-            })   
-
-    @validate_form(forms.TextUpdateForm, 'POST')
-    @hglibrary
-    def create(self, request, form, docid, lib):
-        try:
-            revision = form.cleaned_data['revision']
-            msg = form.cleaned_data['message']
-            user = form.cleaned_data['user'] or request.user.username
-
-            # do not allow changing not owned documents
-            # (for now... )
-            
-            
-            if user != request.user.username:
-                return response.AccessDenied().django_response({
-                    'reason': 'insufficient-priviliges',
-                })
-            
-            current = lib.document(docid, user)
-            orig = lib.document_for_rev(revision)
-
-            if current != orig:
-                return response.EntityConflict().django_response({
-                        "reason": "out-of-date",
-                        "provided_revision": orig.revision,
-                        "latest_revision": current.revision })
-            
-            if form.cleaned_data.has_key('contents'):
-                data = form.cleaned_data['contents']
-            else:                               
-                chunks = form.cleaned_data['chunks']
-                xdoc = parser.WLDocument.from_string(current.data('xml'))
-                errors = xdoc.merge_chunks(chunks)
-
-                if len(errors):
-                    return response.EntityConflict().django_response({
-                            "reason": "invalid-chunks",
-                            "message": "Unable to merge following parts into the document: %s " % ",".join(errors)
-                    })
-
-                data = xdoc.serialize()
-
-            # try to find any Xinclude tags
-            includes = [m.groupdict()['link'] for m in (re.finditer(\
-                XINCLUDE_REGEXP, data, flags=re.UNICODE) or []) ]
-
-            log.info("INCLUDES: %s", includes)
-
-            # TODO: provide useful routines to make this simpler
-            def xml_update_action(lib, resolve):
-                try:
-                    f = lib._fileopen(resolve('parts'), 'r')
-                    stored_includes = json.loads(f.read())
-                    f.close()
-                except:
-                    stored_includes = []
-                
-                if stored_includes != includes:
-                    f = lib._fileopen(resolve('parts'), 'w+')
-                    f.write(json.dumps(includes))
-                    f.close()
-
-                    lib._fileadd(resolve('parts'))
-
-                    # update the parts cache
-                    PartCache.update_cache(docid, current.owner,\
-                        stored_includes, includes)
-
-                # now that the parts are ok, write xml
-                f = lib._fileopen(resolve('xml'), 'w+')
-                f.write(data.encode('utf-8'))
-                f.close()
-
-            ndoc = None
-            ndoc = current.invoke_and_commit(\
-                xml_update_action, lambda d: (msg, user) )
-
-            try:
-                # return the new revision number
-                return response.SuccessAllOk().django_response({
-                    "document": ndoc.id,
-                    "user": user,
-                    "subview": "xml",
-                    "previous_revision": current.revision,
-                    "revision": ndoc.revision,
-                    'timestamp': ndoc.revision.timestamp,
-                    "url": reverse("doctext_view", args=[ndoc.id])
-                })
-            except Exception, e:
-                if ndoc: lib._rollback()
-                raise e        
-        except RevisionNotFound, e:
-            return response.EntityNotFound(mimetype="text/plain").\
-                django_response(e.message)
 
 
 #
@@ -629,6 +470,11 @@ class MergeHandler(BaseHandler):
             # update is always performed from the file branch
             # to the user branch
             user_doc_new = base_doc.update(request.user.username)
+
+            if user_doc_new == user_doc:
+                return response.SuccessAllOk().django_response({
+                    "result": "no-op"
+                })
                 
             # shared document is the same
             doc_new = doc
@@ -637,12 +483,17 @@ class MergeHandler(BaseHandler):
             if not base_doc.up_to_date():
                 return response.BadRequest().django_response({
                     "reason": "not-fast-forward",
-                    "message": "You must first update yout branch to the latest version."
+                    "message": "You must first update your branch to the latest version."
+                })
+
+            if base_doc.parentof(doc) or base_doc.has_parent_from(doc):
+                return response.SuccessAllOk().django_response({
+                    "result": "no-op"
                 })
 
             # check for unresolved conflicts            
             if base_doc.has_conflict_marks():
-                return response.BadRequest().django_response({
+                return response.BadRequest().django_response({                    
                     "reason": "unresolved-conflicts",
                     "message": "There are unresolved conflicts in your file. Fix them, and try again."
                 })
@@ -692,6 +543,7 @@ class MergeHandler(BaseHandler):
         # Same goes for shared view
         
         return response.SuccessAllOk().django_response({
+            "result": "success",
             "name": user_doc_new.id,
             "user": user_doc_new.owner,
 
diff --git a/apps/api/handlers/text_handler.py b/apps/api/handlers/text_handler.py
new file mode 100644 (file)
index 0000000..5e34ab4
--- /dev/null
@@ -0,0 +1,184 @@
+# -*- encoding: utf-8 -*-
+
+__author__= "Łukasz Rekucki"
+__date__ = "$2009-10-19 14:34:42$"
+__doc__ = "Module documentation."
+
+#import api.forms as forms
+#import api.response as response
+#
+#from api.utils import validate_form, hglibrary
+#from api.models import PartCache
+#
+
+#
+#from piston.handler import BaseHandler
+#
+#from wlrepo import *
+
+import re
+from library_handlers import *
+
+import librarian
+from librarian import parser
+
+#
+# Document Text View
+#
+
+XINCLUDE_REGEXP = r"""<(?:\w+:)?include\s+[^>]*?href=("|')wlrepo://(?P<link>[^\1]+?)\1\s*[^>]*?>"""
+#
+#
+#
+
+class DocumentTextHandler(BaseHandler):
+    allowed_methods = ('GET', 'POST')
+
+    @validate_form(forms.TextRetrieveForm, 'GET')
+    @hglibrary
+    def read(self, request, form, docid, lib):
+        """Read document as raw text"""
+        try:
+            revision = form.cleaned_data['revision']
+            chunk = form.cleaned_data['chunk']
+            user = form.cleaned_data['user'] or request.user.username
+            format = form.cleaned_data['format']
+
+            document = lib.document_for_rev(revision)
+
+            if document.id != docid:
+                return response.BadRequest().django_response({
+                    'reason': 'name-mismatch',
+                    'message': 'Provided revision is not valid for this document'
+                })
+
+            if document.owner != user:
+                return response.BadRequest().django_response({
+                    'reason': 'user-mismatch',
+                    'message': "Provided revision doesn't belong to user %s" % user
+                })
+
+            for error in check_user(request, user):
+                return error
+
+            if not chunk:
+                return document.data('xml')
+
+            xdoc = parser.WLDocument.from_string(document.data('xml'),\
+                parse_dublincore=False)
+
+            xchunk = xdoc.chunk(chunk)
+
+            if xchunk is None:
+                return response.EntityNotFound().django_response({
+                      'reason': 'no-part-in-document',
+                      'path': chunk
+                })
+
+            return librarian.serialize_children(xchunk, format=format)
+
+        except librarian.ParseError, e:
+            return response.EntityNotFound().django_response({
+                'reason': 'invalid-document-state',
+                'exception': type(e),
+                'message': e.message
+            })
+        except (EntryNotFound, RevisionNotFound), e:
+            return response.EntityNotFound().django_response({
+                'reason': 'not-found',
+                'exception': type(e), 'message': e.message
+            })
+
+    @validate_form(forms.TextUpdateForm, 'POST')
+    @hglibrary
+    def create(self, request, form, docid, lib):
+        try:
+            revision = form.cleaned_data['revision']
+            msg = form.cleaned_data['message']
+            user = form.cleaned_data['user'] or request.user.username
+
+            # do not allow changing not owned documents
+            # (for now... )
+
+
+            if user != request.user.username:
+                return response.AccessDenied().django_response({
+                    'reason': 'insufficient-priviliges',
+                })
+
+            current = lib.document(docid, user)
+            orig = lib.document_for_rev(revision)
+
+            if current != orig:
+                return response.EntityConflict().django_response({
+                        "reason": "out-of-date",
+                        "provided_revision": orig.revision,
+                        "latest_revision": current.revision })
+
+            if form.cleaned_data.has_key('contents'):
+                data = form.cleaned_data['contents']
+            else:
+                chunks = form.cleaned_data['chunks']
+                xdoc = parser.WLDocument.from_string(current.data('xml'))
+                errors = xdoc.merge_chunks(chunks)
+
+                if len(errors):
+                    return response.EntityConflict().django_response({
+                            "reason": "invalid-chunks",
+                            "message": "Unable to merge following parts into the document: %s " % ",".join(errors)
+                    })
+
+                data = xdoc.serialize()
+
+            # try to find any Xinclude tags
+            includes = [m.groupdict()['link'] for m in (re.finditer(\
+                XINCLUDE_REGEXP, data, flags=re.UNICODE) or []) ]
+
+            log.info("INCLUDES: %s", includes)
+
+            # TODO: provide useful routines to make this simpler
+            def xml_update_action(lib, resolve):
+                try:
+                    f = lib._fileopen(resolve('parts'), 'r')
+                    stored_includes = json.loads(f.read())
+                    f.close()
+                except:
+                    stored_includes = []
+
+                if stored_includes != includes:
+                    f = lib._fileopen(resolve('parts'), 'w+')
+                    f.write(json.dumps(includes))
+                    f.close()
+
+                    lib._fileadd(resolve('parts'))
+
+                    # update the parts cache
+                    PartCache.update_cache(docid, current.owner,\
+                        stored_includes, includes)
+
+                # now that the parts are ok, write xml
+                f = lib._fileopen(resolve('xml'), 'w+')
+                f.write(data.encode('utf-8'))
+                f.close()
+
+            ndoc = None
+            ndoc = current.invoke_and_commit(\
+                xml_update_action, lambda d: (msg, user) )
+
+            try:
+                # return the new revision number
+                return response.SuccessAllOk().django_response({
+                    "document": ndoc.id,
+                    "user": user,
+                    "subview": "xml",
+                    "previous_revision": current.revision,
+                    "revision": ndoc.revision,
+                    'timestamp': ndoc.revision.timestamp,
+                    "url": reverse("doctext_view", args=[ndoc.id])
+                })
+            except Exception, e:
+                if ndoc: lib._rollback()
+                raise e
+        except RevisionNotFound, e:
+            return response.EntityNotFound(mimetype="text/plain").\
+                django_response(e.message)
index a124a2f..1d55aa9 100644 (file)
@@ -14,9 +14,11 @@ authdata = {'authentication': DjangoAuth()}
 #
 
 import api.handlers.library_handlers as dh
+from api.handlers.text_handler import DocumentTextHandler
+
 library_resource = Resource(dh.LibraryHandler, **authdata)
 document_resource = Resource(dh.DocumentHandler, **authdata)
-document_text_resource = Resource(dh.DocumentTextHandler, **authdata)
+document_text_resource = Resource(DocumentTextHandler, **authdata)
 document_html_resource = Resource(dh.DocumentHTMLHandler, **authdata)
 # document_dc_resource = Resource(dh.DocumentDublinCoreHandler, **authdata)
 document_gallery = Resource(dh.DocumentGalleryHandler, **authdata)
index b5513c4..4675015 100644 (file)
@@ -49,6 +49,7 @@ class RequestAccepted(ResponseObject):
 
     def django_response(self, ticket_status, ticket_uri):
         return ResponseObject.django_response(self, {
+            'result': 'accepted',
             'status': ticket_status,
             'refer_to': ticket_uri })     
         
index 0f373d4..f8c07c6 100644 (file)
@@ -73,6 +73,9 @@ class Document(object):
     def parentof(self, other):
         return self._revision.parentof(other._revision)
 
+    def has_parent_from(self, other):
+        return self._revision.has_parent_from(other._revision)
+
     def ancestorof(self, other):
         return self._revision.ancestorof(other._revision)
 
@@ -129,7 +132,7 @@ class DocumentAlreadyExists(LibraryException):
 
 def open_library(path, proto, *args, **kwargs):
     if proto == 'hg':
-        import wlrepo.mercurial_backend
-        return wlrepo.mercurial_backend.MercurialLibrary(path, *args, **kwargs)
+        import wlrepo.mercurial_backend.library
+        return wlrepo.mercurial_backend.library.MercurialLibrary(path, *args, **kwargs)
 
     raise NotImplemented()
\ No newline at end of file
index 22a9d52..f797d39 100644 (file)
@@ -8,97 +8,7 @@ __date__ = "$2009-09-25 09:20:22$"
 __doc__ = "Module documentation."
 
 import wlrepo
-from mercurial.node import nullid
-
-from mercurial import encoding
-encoding.encoding = 'utf-8'
-
-class MercurialRevision(wlrepo.Revision):
-
-    def __init__(self, lib, changectx):
-        super(MercurialRevision, self).__init__(lib)
-        self._changectx = changectx
-        
-        branchname = self._changectx.branch()
-        if branchname.startswith("$doc:"):
-            self._docname = branchname[5:]
-            self._username = None
-        elif branchname.startswith("$user:"):
-            idx = branchname.find("$doc:")
-            if(idx < 0):
-                raise ValueError("Revision %s is not a valid document revision." % changectx.hex());
-            self._username = branchname[6:idx]
-            self._docname = branchname[idx+5:]
-        else:
-            raise ValueError("Revision %s is not a valid document revision." % changectx.hex());
-        
-    @property
-    def document_name(self):
-        return self._docname and self._docname.decode('utf-8')
-
-    @property
-    def user_name(self):
-        return self._username and self._username.decode('utf-8')
-
-    def hgrev(self):
-        return self._changectx.node()
-        
-    def hgcontext(self):
-        return self._changectx
-
-    def hgbranch(self):
-        return self._changectx.branch()
-
-    @property
-    def timestamp(self):
-        return self._changectx.date()[0]
-
-    def __unicode__(self):
-        return u"%s" % self._changectx.hex()
-
-    def __str__(self):
-        return self.__unicode__().encode('utf-8')
-
-    def __repr__(self):
-        return "%s" % self._changectx.hex()
-
-    def ancestorof(self, other):
-        nodes = list(other._changectx._parents)
-        while nodes[0].node() != nullid:
-            v = nodes.pop(0)
-            if v == self._changectx:
-                return True
-            nodes.extend( v._parents )
-        return False
-
-    def parentof(self, other):
-        return self._changectx in other._changectx._parents
-
-    def has_common_ancestor(self, other):
-        a = self._changectx.ancestor(other._changectx)       
-        return (a.branch() == self._changectx.branch())
-
-    def has_children(self):
-        return bool(self._library._hgrepo.changelog.children(self.hgrev()))
-
-    def merge_with(self, other, user, message):
-        message = self._library._sanitize_string(message)
-        lock = self._library.lock(True)
-        try:
-            self._library._checkout(self._changectx.node())
-            status = self._library._merge(other._changectx.node())
-            if status.isclean():
-                self._library._commit(user=user, message=message)
-                return True
-            else:
-                return False
-        finally:
-            lock.release()
-
-    def __eq__(self, other):
-        return self._changectx.node() == other._changectx.node()
-
-
-from wlrepo.mercurial_backend.library import MercurialLibrary
 
 
+from mercurial import encoding
+encoding.encoding = 'utf-8'
\ No newline at end of file
index 3911d04..f7da32d 100644 (file)
@@ -106,10 +106,19 @@ class MercurialDocument(wlrepo.Document):
         return self._library.document_for_rev(fullid)
 
     def up_to_date(self):
-        return self.ismain() or (\
-            self.shared().ancestorof(self) )
+        if self.ismain():
+            return True
+
+        shared = self.shared()
         
-            
+        if shared.parentof(self):
+            return True
+
+        if shared.has_parent_from(self):
+            return True
+
+        return False
+                    
     def update(self, user):
         """Update parts of the document."""
         lock = self.library.lock()
@@ -117,78 +126,69 @@ class MercurialDocument(wlrepo.Document):
             if self.ismain():
                 # main revision of the document
                 return self
-            
-            if self._revision.has_children():                
-                raise UpdateException("Revision has children.")
 
-            sv = self.shared()
+            # check for children in this branch
+            if self._revision.has_children(limit_branch=True):
+                raise wlrepo.UpdateException("Revision %s has children." % self.revision)
 
-            if self.parentof(sv):
+            shared = self.shared()
+
+            # the shared version comes from our version
+            if self.parentof(self.shared()):
+                return self
+
+            # no changes since last update
+            if shared.ancestorof(self):
                 return self
 
-            if sv.ancestorof(self):
+            # last share was from this branch
+            if shared.has_parent_from(self):
                 return self
 
             if self._revision.merge_with(sv._revision, user=user,\
                 message="$AUTO$ Personal branch update."):
                 return self.latest()
             else:
-                raise UpdateException("Merge failed.")
+                raise wlrepo.UpdateException("Merge failed.")
         finally:
             lock.release()  
 
     def share(self, message):
         lock = self.library.lock()
         try:            
+
+            # nothing to do
             if self.ismain():
-                return False # always shared
+                return self
 
-            user = self._revision.user_name
-            main = self.shared()._revision
-            local = self._revision            
+            shared = self.shared()
 
-            # Case 1:
+            # we just did this - move on
+            if self.parentof(shared):
+                return shared
+
+            # No changes since update
+            if shared.parentof(self):
+                return shared
+
+            # The good situation
+            #
             #         * local
             #         |
-            #         * <- can also be here!
-            #        /|
+            #        >* 
+            #        ||
             #       / |
             # main *  *
             #      |  |
-            # The local branch has been recently updated,
-            # so we don't need to update yet again, but we need to
-            # merge down to default branch, even if there was
-            # no commit's since last update
-            #
-            # This is actually the only good case!
-            if main.ancestorof(local):
-                success, changed = main.merge_with(local, user=user, message=message)
+            if shared.ancestorof(self):               
+                success = shared._revision.merge_with(self._revision, user=self.owner, message=message)
 
                 if not success:
-                    raise LibraryException("Merge failed.")
-
-                return changed
-            
-            # Case 2:
-            # main *
-            #      |
-            #      * <- this case overlaps with previos one
-            #      |\
-            #      | \
-            #      |  * local
-            #      |  |
-            #
-            # There was a recent merge to the defaul branch and
-            # no changes to local branch recently.
-            #
-            # Nothing to do
-            elif local.ancestorof(main):                
-                return False
+                    raise wlrepo.LibraryException("Merge failed.")
+                
+                return shared.latest()
 
-            # In all other cases, the local needs an update
-            # and possibly conflict resolution, so fail
-            raise LibraryExcepton("Document not prepared for sharing.")
-        
+            raise wlrepo.LibraryException("Unrecognized share-state.")
         finally:
             lock.release()
 
index 84e7654..cde7af9 100644 (file)
@@ -13,7 +13,7 @@ from mercurial import ui as hgui
 from mercurial import error
 import wlrepo
 
-from wlrepo.mercurial_backend import MercurialRevision
+from wlrepo.mercurial_backend.revision import MercurialRevision
 from wlrepo.mercurial_backend.document import MercurialDocument
 
 class MergeStatus(object):
diff --git a/lib/wlrepo/mercurial_backend/revision.py b/lib/wlrepo/mercurial_backend/revision.py
new file mode 100644 (file)
index 0000000..f05637d
--- /dev/null
@@ -0,0 +1,103 @@
+# -*- encoding: utf-8 -*-
+
+__author__= "Łukasz Rekucki"
+__date__ = "$2009-10-20 12:31:48$"
+__doc__ = "Module documentation."
+
+import wlrepo
+from mercurial.node import nullid
+
+class MercurialRevision(wlrepo.Revision):
+    def __init__(self, lib, changectx):
+        super(MercurialRevision, self).__init__(lib)
+        self._changectx = changectx
+
+        branchname = self._changectx.branch()
+        if branchname.startswith("$doc:"):
+            self._docname = branchname[5:]
+            self._username = None
+        elif branchname.startswith("$user:"):
+            idx = branchname.find("$doc:")
+            if(idx < 0):
+                raise ValueError("Revision %s is not a valid document revision." % changectx.hex());
+            self._username = branchname[6:idx]
+            self._docname = branchname[idx+5:]
+        else:
+            raise ValueError("Revision %s is not a valid document revision." % changectx.hex());
+
+    @property
+    def document_name(self):
+        return self._docname and self._docname.decode('utf-8')
+
+    @property
+    def user_name(self):
+        return self._username and self._username.decode('utf-8')
+
+    def hgrev(self):
+        return self._changectx.node()
+
+    def hgcontext(self):
+        return self._changectx
+
+    def hgbranch(self):
+        return self._changectx.branch()
+
+    @property
+    def timestamp(self):
+        return self._changectx.date()[0]
+
+    def __unicode__(self):
+        return u"%s" % self._changectx.hex()
+
+    def __str__(self):
+        return self.__unicode__().encode('utf-8')
+
+    def __repr__(self):
+        return "%s" % self._changectx.hex()
+
+    def ancestorof(self, other):
+        nodes = list(other._changectx._parents)
+        while nodes[0].node() != nullid:
+            v = nodes.pop(0)
+            if v == self._changectx:
+                return True
+            nodes.extend( v._parents )
+        return False
+
+    def parentof(self, other):
+        return self._changectx in other._changectx._parents
+
+    def has_common_ancestor(self, other):
+        a = self._changectx.ancestor(other._changectx)
+        return (a.branch() == self._changectx.branch())
+
+    def has_children(self, limit_branch=False):
+        for child in self._changectx.children():
+            cbranch = child.branch()
+            if (not limit_branch) or (cbranch == self.hgbranch()):
+                return True
+        return False
+
+    def has_parent_from(self, rev):
+        branch = rev.hgbranch()        
+        for parent in self._changectx.parents():
+            if parent.branch() == branch:
+                return True            
+        return False
+
+    def merge_with(self, other, user, message):
+        message = self._library._sanitize_string(message)
+        lock = self._library.lock(True)
+        try:
+            self._library._checkout(self._changectx.node())
+            status = self._library._merge(other._changectx.node())
+            if status.isclean():
+                self._library._commit(user=user, message=message)
+                return True
+            else:
+                return False
+        finally:
+            lock.release()
+
+    def __eq__(self, other):
+        return self._changectx.node() == other._changectx.node()
index 336a365..fcc111e 100644 (file)
@@ -1,7 +1,7 @@
 /* Style widoku HTML. Nie należy tu ustawiać position ani marginesów */
 .htmlview {
     font-size: 16px;
-    font: Georgia, "Times New Roman", serif;
+    font-family: "Georgia", "Times New Roman", serif;
     line-height: 1.5em;
     padding: 3em;    
 }
     font-style: normal;
     font-variant: small-caps;
 }
+
+.htmlview .parse-warning {
+    display: block;
+    font-size: 10pt;
+    background: #C0C0C0;
+    margin: 1em;
+}
+
+.parse-warning .message {
+    color: purple;
+    font-weight: bold;
+}
\ No newline at end of file
index 8dae56e..ec19ee3 100644 (file)
@@ -183,7 +183,9 @@ Editor.Object = Class.extend({
 // Handle JSON error responses in uniform way
 function parseXHRError(response)
 {
-    var message = ""
+    var message = "";
+    var level = "";
+    
     try {
       var json = $.evalJSON(response.responseText);
 
@@ -193,16 +195,38 @@ function parseXHRError(response)
 
           message = message.replace(/(line\s+)(\d+)(\,\s*column\s+)(\d+)/i,
             "<a class='xml-editor-ref' href='#xml-$2-$4'>$1$2$3$4</a>");
+
+          level = "warning";
       }
       else {
-        message = json.message || json.reason || "Nieznany błąd :((";
+         message = json.message || json.reason || "Nieznany błąd :((";
+         level = "error";
       }
-
     } catch(e) {
         // not a valid JSON response
-        message = response.statusText;
+        message = response.statusText || 'Brak połączenia z serwerem';
+        level = "error";
+    }
+    
+    return {error_message: message, error_level: level};
+}
+
+function parseXHRResponse(xhr) {
+    var response = {}
+    
+    if(xhr.status >= 200 && xhr.status < 300) 
+    {
+        response.success = true;
+        try {
+            response.data = $.evalJSON(xhr.responseText);
+        } catch(e) {
+            response.data = {};
+        }
+
+        return response;
     }
-    return message;
+
+    return parseXHRError(xhr);
 }
 
 Editor.Object._lastGuid = 0;
index c57f4f0..d148640 100644 (file)
@@ -88,10 +88,12 @@ Editor.XMLModel = Editor.Model.extend({
         messageCenter.addMessage('success', 'xmlload', 'Wczytałem XML :-)');
     },
   
-    loadingFailed: function() {
+    loadingFailed: function(response)
+    {
         if (this.get('state') != 'loading') {
             alert('erroneous state:', this.get('state'));
         }
+        
         var message = parseXHRError(response);
         
         this.set('error', '<h2>Błąd przy ładowaniu XML</h2><p>'+message+'</p>');
@@ -218,9 +220,9 @@ Editor.HTMLModel = Editor.Model.extend({
             alert('erroneous state:', this.get('state'));
         }
         
-        var message = parseXHRError(response);
+        var err = parseXHRError(response);
         
-        this.set('error', '<p>Nie udało się wczytać widoku HTML: </p>' + message);
+        this.set('error', '<p>Nie udało się wczytać widoku HTML: </p>' + err.error_message);
         this.set('state', 'error');        
     },
 
@@ -244,10 +246,12 @@ Editor.HTMLModel = Editor.Model.extend({
             data: {
                 revision: this.get('revision'),
                 user: this.document.get('user'),
-                part: path
+                chunk: path,
+                format: 'nl'
             },
             success: function(data) {
                 self.xmlParts[path] = data;
+                console.log(data);
                 callback(path, data);
             },
             // TODO: error handling
@@ -273,7 +277,8 @@ Editor.HTMLModel = Editor.Model.extend({
             dataType: 'text; charset=utf-8',
             data: {
                 fragment: data,
-                part: path
+                chunk: path,
+                format: 'nl'
             },
             success: function(htmldata) {
                 elem.replaceWith(htmldata);
@@ -462,8 +467,8 @@ Editor.DocumentModel = Editor.Model.extend({
             alert('erroneous state:', this.get('state'));
         }
         
-        var message = parseXHRError(response);        
-        this.set('error', '<h2>Nie udało się wczytać dokumentu</h2><p>'+message+"</p>");
+        var err = parseXHRError(response);
+        this.set('error', '<h2>Nie udało się wczytać dokumentu</h2><p>'+err.error_message+"</p>");
         this.set('state', 'error');
     },
   
@@ -517,53 +522,58 @@ Editor.DocumentModel = Editor.Model.extend({
                 revision: this.get('revision'),
                 user: this.get('user')
             },
-            complete: this.updateCompleted.bind(this),
-            success: function(data) {
-                this.set('updateData', data);
-                console.log("new data:", data)
-            }.bind(this)
+            complete: this.updateCompleted.bind(this),            
         });
     },
   
-    updateCompleted: function(xhr, textStatus) {
-        console.log(xhr.status, textStatus);
-        
-        if (xhr.status == 200) 
+    updateCompleted: function(xhr, textStatus)
+    {
+        console.log(xhr.status, xhr.responseText);
+        var response = parseXHRResponse(xhr);
+        if(response.success)
         {
-            var udata = this.get('updateData');
-            if(udata.timestamp == udata.parent_timestamp)
+            if( (response.data.result == 'no-op')
+             || (response.data.timestamp == response.data.parent_timestamp))
             {
-                // no change
+                if( (response.data.revision) && (response.data.revision != this.get('revision')) )
+                {
+                    // we're out of sync
+                    this.set('state', 'unsynced');
+                    return;
+                }
+                
                 messageCenter.addMessage('info', 'doc_update',
-                    'Nic się nie zmieniło od ostatniej aktualizacji. Po co mam uaktualniać?');
+                    'Już posiadasz najbardziej aktualną wersję.');
+                    this.set('state', 'synced');
+                return;
+            }
+
+            // result: success
+            this.set('revision', response.data.revision);
+            this.set('user', response.data.user);
 
+            messageCenter.addMessage('info', 'doc_update',
+                'Uaktualnienie dokumentu do wersji ' + response.data.revision);
+
+            for (var key in this.contentModels) {
+                this.contentModels[key].set('revision', this.get('revision') );
+                this.contentModels[key].set('state', 'empty');
             }
-            else {
-                this.set('revision', udata.revision);
-                this.set('user', udata.user);
-                messageCenter.addMessage('info', 'doc_update', 
-                    'Uaktualnienie dokumentu do wersji ' + udata.revision);
-
-                for (var key in this.contentModels) {
-                    this.contentModels[key].set('revision', this.get('revision') );
-                    this.contentModels[key].set('state', 'empty');
-                }
-            }        
-        } else if (xhr.status == 409) { // Konflikt podczas operacji
-            messageCenter.addMessage('error', 'doc_update',
-                'Wystąpił konflikt podczas aktualizacji. Pędź po programistów! :-(');
-        } else {
-            messageCenter.addMessage('critical', 'doc_update',
-                'Nieoczekiwany błąd. Pędź po programistów! :-(');
+
+            this.set('state', 'synced');
+            return;
         }
+
+        // no success means trouble
+        messageCenter.addMessage(response.error_level, 'doc_update', 
+            response.error_message);       
         
-        this.set('state', 'synced');
-        this.set('updateData', null);
+        this.set('state', 'unsynced');
     },
   
     merge: function(message) {
         this.set('state', 'loading');
-        messageCenter.addMessage('info', null, 
+        messageCenter.addMessage('info', 'doc_merge',
             'Scalam dokument z głównym repozytorium...');
             
         $.ajax({
@@ -584,28 +594,50 @@ Editor.DocumentModel = Editor.Model.extend({
     },
   
     mergeCompleted: function(xhr, textStatus) {
-        console.log(xhr.status, textStatus);
-        if (xhr.status == 200) { // Sukces
-            this.set('revision', this.get('updateData').revision);
-            this.set('user', this.get('updateData').user);
-            
-            for (var key in this.contentModels) {
-                this.contentModels[key].set('revision', this.get('revision'));
-                this.contentModels[key].set('state', 'empty');
+        console.log(xhr.status, xhr.responseText);
+        var response = parseXHRResponse(xhr);
+        if(response.success)
+        {
+            if( (response.data.result == 'no-op')
+             || (response.data.shared_timestamp == response.data.shared_parent_timestamp))
+            {
+                if( (response.data.revision) && (response.data.revision != this.get('revision')) )
+                {
+                    // we're out of sync
+                    this.set('state', 'unsynced');
+                    return;
+                }
+
+                messageCenter.addMessage('info', 'doc_merge',
+                    'Twoja aktualna wersja nie różni się od ostatnio zatwierdzonej.');
+                this.set('state', 'synced');
+                return;
+            }
+
+            if( response.data.result == 'accepted')
+            {
+                messageCenter.addMessage('info', 'doc_merge',
+                    'Prośba o zatwierdzenie została przyjęta i oczekuję na przyjęcie.');
+                this.set('state', 'synced');
+                return;
             }
 
-            messageCenter.addMessage('success', null, 'Scaliłem dokument z głównym repozytorium :-)');
-        } else if (xhr.status == 202) { // Wygenerowano PullRequest
-            messageCenter.addMessage('success', null, 'Wysłałem prośbę o scalenie dokumentu z głównym repozytorium.');
-        } else if (xhr.status == 204) { // Nic nie zmieniono
-            messageCenter.addMessage('info', null, 'Nic się nie zmieniło od ostatniego scalenia. Po co mam scalać?');
-        } else if (xhr.status == 409) { // Konflikt podczas operacji
-            messageCenter.addMessage('error', null, 'Wystąpił konflikt podczas scalania. Pędź po programistów! :-(');
-        } else if (xhr.status == 500) {
-            messageCenter.addMessage('critical', null, 'Błąd serwera. Pędź po programistów! :-(');
+            // result: success
+            this.set('revision', response.data.revision);
+            this.set('user', response.data.user);
+
+            messageCenter.addMessage('info', 'doc_merge',
+                'Twoja wersja dokumentu została zatwierdzona.');
+            
+            this.set('state', 'synced');
+            return;
         }
-        this.set('state', 'synced');
-        this.set('mergeData', null);
+
+        // no success means trouble
+        messageCenter.addMessage(response.error_level, 'doc_merge',
+            response.error_message);
+
+        this.set('state', 'unsynced');
     },
   
     // For debbuging
index d43caaa..cfd1816 100644 (file)
@@ -30,6 +30,27 @@ var EditorView = View.extend({
             $('#commit-dialog').jqmHide();
         });
         
+        $('#commit-dialog-save-button').click(function(event, data)
+        {
+            if ($('#commit-dialog-message').val().match(/^\s*$/)) {
+                $('#commit-dialog-error-empty-message').fadeIn();
+            } else {
+                $('#commit-dialog-error-empty-message').hide();
+                $('#commit-dialog').jqmHide();
+
+                var message = $('#commit-dialog-message').val();
+                $('#commit-dialog-related-issues input:checked')
+                .each(function() {
+                    message += ' refs #' + $(this).val();
+                });
+                
+                var ctx = $('#commit-dialog').data('context');
+                console.log("COMMIT APROVED", ctx);
+                ctx.callback(message);
+            }
+            return false;
+        });
+        
     
         // $('#split-dialog').jqm({
         //      modal: true,
@@ -71,25 +92,8 @@ var EditorView = View.extend({
     loadRelatedIssues: function(hash) {
         var self = this;
         var c = $('#commit-dialog-related-issues');
-
-        $('#commit-dialog-save-button').click(function(event, data)
-        {
-            if ($('#commit-dialog-message').val().match(/^\s*$/)) {
-                $('#commit-dialog-error-empty-message').fadeIn();
-            } else {
-                $('#commit-dialog-error-empty-message').hide();
-                $('#commit-dialog').jqmHide();
-
-                var message = $('#commit-dialog-message').val();
-                $('#commit-dialog-related-issues input:checked')
-                .each(function() {
-                    message += ' refs #' + $(this).val();
-                });
-                console.log("COMMIT APROVED", hash.t);
-                hash.t.callback(message);
-            }
-            return false;
-        });
+        
+        $('#commit-dialog').data('context', hash.t);
 
         $("div.loading-box", c).show();
         $("div.fatal-error-box", c).hide();
@@ -118,7 +122,7 @@ var EditorView = View.extend({
             this.commitButton.attr('disabled', null);
             this.updateButton.attr('disabled', 'disabled');
             this.mergeButton.attr('disabled', 'disabled');
-        } else if (value == 'synced') {            
+        } else if (value == 'synced' || value == 'unsynced') {
             this.quickSaveButton.attr('disabled', 'disabled');
             this.commitButton.attr('disabled', 'disabled');
             this.updateButton.attr('disabled', null);
index e107635..03056c8 100644 (file)
@@ -14,7 +14,7 @@ urlpatterns = patterns('',
     url(r'^file/upload', 'explorer.views.file_upload', name='file_upload'),
 
 
-    url(r'^managment/pull-requests$', 'explorer.views.pull_requests'),
+    url(r'^management/pull-requests$', 'explorer.views.pull_requests'),
     
 #    url(r'^images/(?P<folder>[^/]+)/$', 'explorer.views.folder_images', name='folder_image'),
 #    url(r'^images/$', 'explorer.views.folder_images', {'folder': '.'}, name='folder_image_ajax'),