From bd2f4130a81e68a2bb3c8d88448540ec60fe7be5 Mon Sep 17 00:00:00 2001 From: =?utf8?q?=C5=81ukasz=20Rekucki?= Date: Tue, 20 Oct 2009 14:39:52 +0200 Subject: [PATCH] Client message reporting refactor. Better pull/push handling. --- apps/api/forms.py | 28 +++- apps/api/handlers/library_handlers.py | 174 ++------------------- apps/api/handlers/text_handler.py | 184 +++++++++++++++++++++++ apps/api/resources.py | 4 +- apps/api/response.py | 1 + lib/wlrepo/__init__.py | 7 +- lib/wlrepo/mercurial_backend/__init__.py | 94 +----------- lib/wlrepo/mercurial_backend/document.py | 96 ++++++------ lib/wlrepo/mercurial_backend/library.py | 2 +- lib/wlrepo/mercurial_backend/revision.py | 103 +++++++++++++ platforma/static/css/html.css | 14 +- platforma/static/js/app.js | 34 ++++- platforma/static/js/models.js | 150 ++++++++++-------- platforma/static/js/views/editor.js | 44 +++--- platforma/urls.py | 2 +- 15 files changed, 542 insertions(+), 395 deletions(-) create mode 100644 apps/api/handlers/text_handler.py create mode 100644 lib/wlrepo/mercurial_backend/revision.py diff --git a/apps/api/forms.py b/apps/api/forms.py index 2c9ae66a..7532e179 100644 --- a/apps/api/forms.py +++ b/apps/api/forms.py @@ -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'] diff --git a/apps/api/handlers/library_handlers.py b/apps/api/handlers/library_handlers.py index 06358618..aef1e9c1 100644 --- a/apps/api/handlers/library_handlers.py +++ b/apps/api/handlers/library_handlers.py @@ -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[^\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 index 00000000..5e34ab4d --- /dev/null +++ b/apps/api/handlers/text_handler.py @@ -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[^\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) diff --git a/apps/api/resources.py b/apps/api/resources.py index a124a2fa..1d55aa9e 100644 --- a/apps/api/resources.py +++ b/apps/api/resources.py @@ -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) diff --git a/apps/api/response.py b/apps/api/response.py index b5513c4e..46750158 100644 --- a/apps/api/response.py +++ b/apps/api/response.py @@ -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 }) diff --git a/lib/wlrepo/__init__.py b/lib/wlrepo/__init__.py index 0f373d40..f8c07c68 100644 --- a/lib/wlrepo/__init__.py +++ b/lib/wlrepo/__init__.py @@ -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 diff --git a/lib/wlrepo/mercurial_backend/__init__.py b/lib/wlrepo/mercurial_backend/__init__.py index 22a9d520..f797d394 100644 --- a/lib/wlrepo/mercurial_backend/__init__.py +++ b/lib/wlrepo/mercurial_backend/__init__.py @@ -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 diff --git a/lib/wlrepo/mercurial_backend/document.py b/lib/wlrepo/mercurial_backend/document.py index 3911d042..f7da32d2 100644 --- a/lib/wlrepo/mercurial_backend/document.py +++ b/lib/wlrepo/mercurial_backend/document.py @@ -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() diff --git a/lib/wlrepo/mercurial_backend/library.py b/lib/wlrepo/mercurial_backend/library.py index 84e76540..cde7af9f 100644 --- a/lib/wlrepo/mercurial_backend/library.py +++ b/lib/wlrepo/mercurial_backend/library.py @@ -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 index 00000000..f05637d2 --- /dev/null +++ b/lib/wlrepo/mercurial_backend/revision.py @@ -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() diff --git a/platforma/static/css/html.css b/platforma/static/css/html.css index 336a365e..fcc111e9 100644 --- a/platforma/static/css/html.css +++ b/platforma/static/css/html.css @@ -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; } @@ -225,3 +225,15 @@ 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 diff --git a/platforma/static/js/app.js b/platforma/static/js/app.js index 8dae56e4..ec19ee3c 100644 --- a/platforma/static/js/app.js +++ b/platforma/static/js/app.js @@ -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, "$1$2$3$4"); + + 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; diff --git a/platforma/static/js/models.js b/platforma/static/js/models.js index c57f4f0f..d1486402 100644 --- a/platforma/static/js/models.js +++ b/platforma/static/js/models.js @@ -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', '

Błąd przy ładowaniu XML

'+message+'

'); @@ -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', '

Nie udało się wczytać widoku HTML:

' + message); + this.set('error', '

Nie udało się wczytać widoku HTML:

' + 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', '

Nie udało się wczytać dokumentu

'+message+"

"); + var err = parseXHRError(response); + this.set('error', '

Nie udało się wczytać dokumentu

'+err.error_message+"

"); 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 diff --git a/platforma/static/js/views/editor.js b/platforma/static/js/views/editor.js index d43caaaa..cfd18166 100644 --- a/platforma/static/js/views/editor.js +++ b/platforma/static/js/views/editor.js @@ -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); diff --git a/platforma/urls.py b/platforma/urls.py index e107635e..03056c88 100644 --- a/platforma/urls.py +++ b/platforma/urls.py @@ -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[^/]+)/$', 'explorer.views.folder_images', name='folder_image'), # url(r'^images/$', 'explorer.views.folder_images', {'folder': '.'}, name='folder_image_ajax'), -- 2.20.1