From: Łukasz Rekucki Date: Thu, 15 Oct 2009 16:28:32 +0000 (+0200) Subject: Merge branch 'master' of stigma:platforma X-Git-Url: https://git.mdrn.pl/redakcja.git/commitdiff_plain/39d26aa0f2cb893f19282657b2fddd2a494f2263?hp=97059a831f2e7ac44ccfacde3950cc2561c539ee Merge branch 'master' of stigma:platforma --- diff --git a/apps/api/forms.py b/apps/api/forms.py index d55e6291..2c9ae66a 100644 --- a/apps/api/forms.py +++ b/apps/api/forms.py @@ -4,16 +4,33 @@ __author__= "Łukasz Rekucki" __date__ = "$2009-09-20 21:34:52$" __doc__ = "Micro-forms for the API." - from django import forms +from api.models import PullRequest +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')) ) - - # which revision to update/share - target_revision = forms.RegexField('[0-9a-f]{40}') + + # + # if type == update: + # * user's revision which is the base of the merge + # if type == share: + # * revision which will be pulled to the main branch + # + # NOTE: the revision doesn't have to be owned by the user + # who requests the merge: + # a) Every user can update his branch + # b) Some users can push their changes + # -> if they can't, they leave a PRQ + # c) Some users can pull other people's changes + # d) Some users can update branches owned by special + # users associated with PRQ's + revision = forms.RegexField('[0-9a-f]{40}') # any additional comments that user wants to add to the change message = forms.CharField(required=False) @@ -39,4 +56,68 @@ class DocumentUploadForm(forms.Form): raise forms.ValidationError( "You must either provide file descriptor or raw data." ) - return clean_data \ No newline at end of file + return clean_data + +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!!! + # value = super(DocumentRetrieveForm, self).clean_user() + value = self.cleaned_data['user'] + + if value.startswith('$'): + # library user (... maybe later) + if value == '$library': + raise forms.ValidationError("Invalid user name '%s'" % value) + + m = PRQ_USER_RE.match(value) + + if m: + try: + return value + except: + raise forms.ValidationError("User doesn't exist.") + raise forms.ValidationError("Invalid user name '%s'" % value) + try: + return value + except: + raise forms.ValidationError("User doesn't exist.") + + +class TextRetrieveForm(DocumentRetrieveForm): + part = forms.CharField(required=False) + +class TextUpdateForm(DocumentRetrieveForm): + message = forms.CharField(required=False) + contents = forms.CharField(required=False) + chunks = forms.CharField(required=False) + + def clean_message(self): + value = self.cleaned_data['message'] + + if value: + return u"$USER$ " + request.POST['message'] + else: + return u"$AUTO$ XML content update." + + def clean_chunks(self): + value = self.cleaned_data['chunks'] + + try: + return json.loads(value) + except Exception, e: + forms.ValidationError("Invalid JSON: " + e.message) + + + def clean(self): + if self.cleaned_data['contents'] and self.cleaned_data['chunks']: + raise forms.ValidationError("Pass either contents or chunks - not both ") + + if not self.cleaned_data['contents'] and not self.cleaned_data['chunks']: + raise forms.ValidationError("You must pass contents or chunks.") + + return self.cleaned_data \ No newline at end of file diff --git a/apps/api/handlers/library_handlers.py b/apps/api/handlers/library_handlers.py index 488c2d40..be504e90 100644 --- a/apps/api/handlers/library_handlers.py +++ b/apps/api/handlers/library_handlers.py @@ -1,6 +1,8 @@ # -*- encoding: utf-8 -*- import os.path + import logging +log = logging.getLogger('platforma.api.library') __author__= "Łukasz Rekucki" __date__ = "$2009-09-25 15:49:50$" @@ -20,7 +22,8 @@ import librarian.html from librarian import dcparser, parser from wlrepo import * -from explorer.models import PullRequest, GalleryForDocument +from api.models import PullRequest +from explorer.models import GalleryForDocument # internal imports import api.forms as forms @@ -32,8 +35,26 @@ from api.models import PartCache import settings -log = logging.getLogger('platforma.api') +def is_prq(username): + return username.startswith('$prq-') +def check_user(request, user): + log.info("user: %r, perm: %r" % (request.user, request.user.get_all_permissions()) ) + #pull request + if is_prq(user): + if not request.user.has_perm('api.pullrequest.can_view'): + yield response.AccessDenied().django_response({ + 'reason': 'access-denied', + 'message': "You don't have enough priviliges to view pull requests." + }) + # other users + elif request.user.username != user: + if not request.user.has_perm('api.document.can_view_other'): + yield response.AccessDenied().django_response({ + 'reason': 'access-denied', + 'message': "You don't have enough priviliges to view other people's document." + }) + pass # # Document List Handlers @@ -48,10 +69,8 @@ class BasicLibraryHandler(AnonymousBaseHandler): document_list = [{ 'url': reverse('document_view', args=[docid]), 'name': docid } for docid in lib.documents() ] - return {'documents' : document_list} - # # This handler controlls the document collection # @@ -59,7 +78,6 @@ class LibraryHandler(BaseHandler): allowed_methods = ('GET', 'POST') anonymous = BasicLibraryHandler - @hglibrary def read(self, request, lib): """Return the list of documents.""" @@ -97,6 +115,7 @@ class LibraryHandler(BaseHandler): return {'documents': sorted(document_tree.itervalues(), key=natural_order(lambda d: d['name']) ) } + @validate_form(forms.DocumentUploadForm, 'POST') @hglibrary def create(self, request, form, lib): @@ -183,60 +202,105 @@ class DocumentHandler(BaseHandler): allowed_methods = ('GET', 'PUT') anonymous = BasicDocumentHandler + @validate_form(forms.DocumentRetrieveForm, 'GET') @hglibrary - def read(self, request, docid, lib): + def read(self, request, form, docid, lib): """Read document's meta data""" - log.info(u"Read %s (%s)" % (docid, type(docid)) ) - try: - doc = lib.document(docid) - udoc = doc.take(request.user.username) - except RevisionNotFound, e: - return response.EntityNotFound().django_response({ - 'exception': type(e), 'message': e.message, - 'docid': docid }) + log.info(u"User '%s' wants to %s(%s) as %s" % \ + (request.user.username, docid, form.cleaned_data['revision'], form.cleaned_data['user']) ) - # is_shared = udoc.ancestorof(doc) - # is_uptodate = is_shared or shared.ancestorof(document) + user = form.cleaned_data['user'] or request.user.username + rev = form.cleaned_data['revision'] or 'latest' - result = { - 'name': udoc.id, - 'html_url': reverse('dochtml_view', args=[udoc.id]), - 'text_url': reverse('doctext_view', args=[udoc.id]), - 'dc_url': reverse('docdc_view', args=[udoc.id]), - 'gallery_url': reverse('docgallery_view', args=[udoc.id]), - 'merge_url': reverse('docmerge_view', args=[udoc.id]), - 'user_revision': udoc.revision, - 'user_timestamp': udoc.revision.timestamp, - 'public_revision': doc.revision, - 'public_timestamp': doc.revision.timestamp, - } + for error in check_user(request, user): + return error + + try: + doc = lib.document(docid, user, rev=rev) + except RevisionMismatch, e: + # the document exists, but the revision is bad + return response.EntityNotFound().django_response({ + 'reason': 'revision-mismatch', + 'message': e.message, + 'docid': docid, + 'user': user, + }) + except RevisionNotFound, e: + # the user doesn't have this document checked out + # or some other weird error occured + # try to do the checkout + if is_prq(user) or (user == request.user.username): + try: + mdoc = lib.document(docid) + doc = mdoc.take(user) + + if is_prq(user): + # source revision, should probably change + # but there are no changes yet, so... + pass + + except RevisionNotFound, e: + return response.EntityNotFound().django_response({ + 'reason': 'document-not-found', + 'message': e.message, + 'docid': docid + }) + else: + return response.EntityNotFound().django_response({ + 'reason': 'document-not-found', + 'message': e.message, + 'docid': docid, + 'user': user, + }) - return result + return { + 'name': doc.id, + 'user': user, + 'html_url': reverse('dochtml_view', args=[doc.id]), + 'text_url': reverse('doctext_view', args=[doc.id]), + # 'dc_url': reverse('docdc_view', args=[doc.id]), + 'gallery_url': reverse('docgallery_view', args=[doc.id]), + 'merge_url': reverse('docmerge_view', args=[doc.id]), + 'user_revision': doc.revision, + 'user_timestamp': doc.revision.timestamp, + # 'public_revision': doc.revision, + # 'public_timestamp': doc.revision.timestamp, + } - @hglibrary - def update(self, request, docid, lib): - """Update information about the document, like display not""" - return + +# @hglibrary +# def update(self, request, docid, lib): +# """Update information about the document, like display not""" +# return # # # class DocumentHTMLHandler(BaseHandler): allowed_methods = ('GET') + @validate_form(forms.DocumentRetrieveForm, 'GET') @hglibrary - def read(self, request, docid, lib, stylesheet='partial'): + def read(self, request, form, docid, lib, stylesheet='partial'): """Read document as html text""" try: - revision = request.GET.get('revision', 'latest') - - if revision == 'latest': - document = lib.document(docid) - else: - document = lib.document_for_rev(revision) + revision = form.cleaned_data['revision'] + 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 refers, to document "%s", but provided "%s"' % (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 return librarian.html.transform(document.data('xml'), is_file=False, \ parse_dublincore=False, stylesheet=stylesheet,\ @@ -273,22 +337,30 @@ class DocumentGalleryHandler(BaseHandler): gallery = {'name': assoc.name, 'pages': []} - for file in os.listdir(dirpath): + for file in sorted(os.listdir(dirpath)): if not isinstance(file, unicode): - log.warn(u"File %r is gallery %r is not unicode. Ommiting."\ - % (file, dirpath) ) - continue - - name, ext = os.path.splitext(os.path.basename(file)) + try: + file = file.decode('utf-8') + except: + log.warn(u"File %r in gallery %r is not unicode. Ommiting."\ + % (file, dirpath) ) + file = None - if ext.lower() not in [u'.png', u'.jpeg', u'.jpg']: - log.info(u"Ignoring: %s %s", name, ext) - continue + if file is not None: + name, ext = os.path.splitext(os.path.basename(file)) - url = settings.MEDIA_URL + assoc.subpath + u'/' + file; + if ext.lower() not in [u'.png', u'.jpeg', u'.jpg']: + log.warn(u"Ignoring: %s %s", name, ext) + url = None + + url = settings.MEDIA_URL + assoc.subpath + u'/' + file + + if url is None: + url = settings.MEDIA_URL + u'/missing.png' + gallery['pages'].append( quote(url.encode('utf-8')) ) - gallery['pages'].sort() +# gallery['pages'].sort() galleries.append(gallery) return galleries @@ -305,41 +377,50 @@ XINCLUDE_REGEXP = r"""<(?:\w+:)?include\s+[^>]*?href=("|')wlrepo://(?P[^\1 class DocumentTextHandler(BaseHandler): allowed_methods = ('GET', 'POST') + @validate_form(forms.TextRetrieveForm, 'GET') @hglibrary - def read(self, request, docid, lib): - """Read document as raw text""" - revision = request.GET.get('revision', 'latest') - part = request.GET.get('part', False) - + def read(self, request, form, docid, lib): + """Read document as raw text""" try: - if revision == 'latest': - document = lib.document(docid) - else: - document = lib.document_for_rev(revision) - + 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'}) + 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 - # TODO: some finer-grained access control - if part is False: - # we're done :) + if not part: return document.data('xml') - else: - xdoc = parser.WLDocument.from_string(document.data('xml'),\ - parse_dublincore=False) - ptext = xdoc.part_as_text(part) + + 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({ + if ptext is None: + return response.EntityNotFound().django_response({ 'reason': 'no-part-in-document' - }) + }) - return ptext - except librarian.ParseError: + return ptext + except librarian.ParseError, e: return response.EntityNotFound().django_response({ 'reason': 'invalid-document-state', - 'exception': type(e), 'message': e.message + 'exception': type(e), + 'message': e.message }) except (EntryNotFound, RevisionNotFound), e: return response.EntityNotFound().django_response({ @@ -347,12 +428,24 @@ class DocumentTextHandler(BaseHandler): 'exception': type(e), 'message': e.message }) + @validate_form(forms.TextUpdateForm, 'POST') @hglibrary - def create(self, request, docid, lib): + def create(self, request, form, docid, lib): try: - revision = request.POST['revision'] + revision = form.cleaned_data['revision'] + msg = form.cleaned_data['message'] + user = form.cleaned_data['user'] or request.user.username - current = lib.document(docid, 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: @@ -360,25 +453,13 @@ class DocumentTextHandler(BaseHandler): "reason": "out-of-date", "provided_revision": orig.revision, "latest_revision": current.revision }) - - if request.POST.has_key('message'): - msg = u"$USER$ " + request.POST['message'] - else: - msg = u"$AUTO$ XML content update." - - if request.POST.has_key('contents'): - data = request.POST['contents'] - else: - if not request.POST.has_key('chunks'): - # bad request - return response.BadRequest().django_response({'reason': 'invalid-arguments', - 'message': 'No contents nor chunks specified.'}) - - # TODO: validate - parts = json.loads(request.POST['chunks']) + + 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(parts) + errors = xdoc.merge_chunks(chunks) if len(errors): return response.EntityConflict().django_response({ @@ -421,12 +502,13 @@ class DocumentTextHandler(BaseHandler): ndoc = None ndoc = current.invoke_and_commit(\ - xml_update_action, lambda d: (msg, current.owner) ) + 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, @@ -446,74 +528,74 @@ class DocumentTextHandler(BaseHandler): # # @requires librarian # -class DocumentDublinCoreHandler(BaseHandler): - allowed_methods = ('GET', 'POST') - - @hglibrary - def read(self, request, docid, lib): - """Read document as raw text""" - try: - revision = request.GET.get('revision', 'latest') - - if revision == 'latest': - doc = lib.document(docid) - else: - doc = 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'}) - - bookinfo = dcparser.BookInfo.from_string(doc.data('xml')) - return bookinfo.serialize() - except (EntryNotFound, RevisionNotFound), e: - return response.EntityNotFound().django_response({ - 'exception': type(e), 'message': e.message}) - - @hglibrary - def create(self, request, docid, lib): - try: - bi_json = request.POST['contents'] - revision = request.POST['revision'] - - if request.POST.has_key('message'): - msg = u"$USER$ " + request.PUT['message'] - else: - msg = u"$AUTO$ Dublin core update." - - current = lib.document(docid, request.user.username) - orig = lib.document_for_rev(revision) - - if current != orig: - return response.EntityConflict().django_response({ - "reason": "out-of-date", - "provided": orig.revision, - "latest": current.revision }) - - xmldoc = parser.WLDocument.from_string(current.data('xml')) - document.book_info = dcparser.BookInfo.from_json(bi_json) - - # zapisz - ndoc = current.quickwrite('xml', \ - document.serialize().encode('utf-8'),\ - message=msg, user=request.user.username) - - try: - # return the new revision number - return { - "document": ndoc.id, - "subview": "dc", - "previous_revision": current.revision, - "revision": ndoc.revision, - 'timestamp': ndoc.revision.timestamp, - "url": reverse("docdc_view", args=[ndoc.id]) - } - except Exception, e: - if ndoc: lib._rollback() - raise e - except RevisionNotFound: - return response.EntityNotFound().django_response() +#class DocumentDublinCoreHandler(BaseHandler): +# allowed_methods = ('GET', 'POST') +# +# @hglibrary +# def read(self, request, docid, lib): +# """Read document as raw text""" +# try: +# revision = request.GET.get('revision', 'latest') +# +# if revision == 'latest': +# doc = lib.document(docid) +# else: +# doc = 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'}) +# +# bookinfo = dcparser.BookInfo.from_string(doc.data('xml')) +# return bookinfo.serialize() +# except (EntryNotFound, RevisionNotFound), e: +# return response.EntityNotFound().django_response({ +# 'exception': type(e), 'message': e.message}) +# +# @hglibrary +# def create(self, request, docid, lib): +# try: +# bi_json = request.POST['contents'] +# revision = request.POST['revision'] +# +# if request.POST.has_key('message'): +# msg = u"$USER$ " + request.PUT['message'] +# else: +# msg = u"$AUTO$ Dublin core update." +# +# current = lib.document(docid, request.user.username) +# orig = lib.document_for_rev(revision) +# +# if current != orig: +# return response.EntityConflict().django_response({ +# "reason": "out-of-date", +# "provided": orig.revision, +# "latest": current.revision }) +# +# xmldoc = parser.WLDocument.from_string(current.data('xml')) +# document.book_info = dcparser.BookInfo.from_json(bi_json) +# +# # zapisz +# ndoc = current.quickwrite('xml', \ +# document.serialize().encode('utf-8'),\ +# message=msg, user=request.user.username) +# +# try: +# # return the new revision number +# return { +# "document": ndoc.id, +# "subview": "dc", +# "previous_revision": current.revision, +# "revision": ndoc.revision, +# 'timestamp': ndoc.revision.timestamp, +# "url": reverse("docdc_view", args=[ndoc.id]) +# } +# except Exception, e: +# if ndoc: lib._rollback() +# raise e +# except RevisionNotFound: +# return response.EntityNotFound().django_response() class MergeHandler(BaseHandler): allowed_methods = ('POST',) @@ -522,60 +604,69 @@ class MergeHandler(BaseHandler): @hglibrary def create(self, request, form, docid, lib): """Create a new document revision from the information provided by user""" + revision = form.cleaned_data['revision'] - target_rev = form.cleaned_data['target_revision'] - + # fetch the main branch document doc = lib.document(docid) - udoc = doc.take(request.user.username) - if target_rev == 'latest': - target_rev = udoc.revision + # fetch the base document + user_doc = lib.document_for_rev(revision) + base_doc = user_doc.latest() - if str(udoc.revision) != target_rev: - # user think doesn't know he has an old version - # of his own branch. - - # Updating is teorericly ok, but we need would - # have to force a refresh. Sharing may be not safe, - # 'cause it doesn't always result in update. - - # In other words, we can't lie about the resource's state - # So we should just yield and 'out-of-date' conflict - # and let the client ask again with updated info. - - # NOTE: this could result in a race condition, when there - # are 2 instances of the same user editing the same document. - # Instance "A" trying to update, and instance "B" always changing - # the document right before "A". The anwser to this problem is - # for the "A" to request a merge from 'latest' and then - # check the parent revisions in response, if he actually - # merge from where he thinks he should. If not, the client SHOULD - # update his internal state. + if base_doc != user_doc: return response.EntityConflict().django_response({ - "reason": "out-of-date", - "provided": target_rev, - "latest": udoc.revision }) + "reason": "out-of-date", + "provided": str(user_doc.revision), + "latest": str(base_doc.revision) + }) if form.cleaned_data['type'] == 'update': # update is always performed from the file branch # to the user branch - success, changed = udoc.update(request.user.username) + changed, clean = base_doc.update(request.user.username) + + # update user document + if changed: + user_doc_new = user_doc.latest() + + # shared document is the same + doc_new = doc + + if form.cleaned_data['type'] == 'share': + 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." + }) + + # check for unresolved conflicts + if base_doc.has_conflict_marks(): + return response.BadRequest().django_response({ + "reason": "unresolved-conflicts", + "message": "There are unresolved conflicts in your file. Fix them, and try again." + }) - if form.cleaned_data['type'] == 'share': - if not request.user.has_perm('explorer.document.can_share'): + if not request.user.has_perm('api.document.can_share'): # User is not permitted to make a merge, right away # So we instead create a pull request in the database try: prq, created = PullRequest.objects.get_or_create( - source_revision = str(udoc.revision), + comitter = request.user, + document = docid, + status = "N", defaults = { - 'comitter': request.user, - 'document': docid, - 'status': "N", + 'source_revision': str(base_doc.revision), 'comment': form.cleaned_data['message'] or '$AUTO$ Document shared.', } ) + # there can't be 2 pending request from same user + # for the same document + if not created: + prq.source_revision = str(base_doc.revision) + prq.comment = prq.comment + 'u\n\n' + (form.cleaned_data['message'] or u'') + prq.save() + return response.RequestAccepted().django_response(\ ticket_status=prq.status, \ ticket_uri=reverse("pullrequest_view", args=[prq.id]) ) @@ -583,23 +674,26 @@ class MergeHandler(BaseHandler): return response.EntityConflict().django_response({ 'reason': 'request-already-exist' }) - else: - success, changed = udoc.share(form.cleaned_data['message']) - if not success: - return response.EntityConflict().django_response({ - 'reason': 'merge-failure', - }) + changed = base_doc.share(form.cleaned_data['message']) - if not changed: - return response.SuccessNoContent().django_response() + # update shared version if needed + if changed: + doc_new = doc.latest() - nudoc = udoc.latest() + # the user wersion is the same + user_doc_new = base_doc + # The client can compare parent_revision to revision + # to see if he needs to update user's view + # Same goes for shared view + return response.SuccessAllOk().django_response({ - "name": nudoc.id, - "parent_user_resivion": udoc.revision, - "parent_revision": doc.revision, - "revision": nudoc.revision, - 'timestamp': nudoc.revision.timestamp, - }) + "name": user_doc_new.id, + "user": user_doc_new.owner, + "parent_revision": user_doc_new.revision, + "parent_shared_revision": doc.revision, + "revision": user_doc_new.revision, + "shared_revision": doc_new.revision, + 'timestamp': user_doc_new.revision.timestamp, + }) \ No newline at end of file diff --git a/apps/api/handlers/manage_handlers.py b/apps/api/handlers/manage_handlers.py index b3e2760b..5905724f 100644 --- a/apps/api/handlers/manage_handlers.py +++ b/apps/api/handlers/manage_handlers.py @@ -1,20 +1,24 @@ # -*- encoding: utf-8 -*- +import logging +log = logging.getLogger('platforma.api.manage') + __author__= "Łukasz Rekucki" __date__ = "$2009-09-25 15:49:50$" __doc__ = "Module documentation." -from piston.handler import BaseHandler, AnonymousBaseHandler +from piston.handler import BaseHandler from api.utils import hglibrary -from explorer.models import PullRequest +from api.models import PullRequest from api.response import * +from datetime import datetime class PullRequestListHandler(BaseHandler): allowed_methods = ('GET',) def read(self, request): - if request.user.has_perm('explorer.book.can_share'): + if request.user.has_perm('api.pullrequest.can_change'): return PullRequest.objects.all() else: return PullRequest.objects.filter(commiter=request.user) @@ -29,7 +33,7 @@ class PullRequestHandler(BaseHandler): def update(self, request, prq_id): """Change the status of request""" - if not request.user.has_perm('explorer.document.can_share'): + if not request.user.has_perm('api.pullrequest.can_change'): return AccessDenied().django_response("Insufficient priviliges") prq = PullRequest.objects.get(id=prq_id) @@ -37,36 +41,86 @@ class PullRequestHandler(BaseHandler): if not prq: return EntityNotFound().django_response() - action = request.PUT.get('action', None) - if action == 'accept' and prq.status == 'N': - return self.accept_merge(prq) + if action == 'accept': + return self.accept_merge(request.user, prq) elif action == 'reject' and prq.status in ['N', 'R']: - return self.reject_merge(prq) + return self.reject_merge(request.user, prq) else: return BadRequest().django_response() @hglibrary - def accept_merge(self, prq, lib): - doc = lib.document( prq.document ) - udoc = doc.take( prq.comitter.username ) - success, changed = udoc.share(prq.comment) - - if not success: - return EntityConflict().django_response() - - doc = doc.latest() - - prq.status = 'A' - prq.merged_revisions = unicode(doc.revision) - prq.save() - - return SuccessAllOk().django_response({ - 'status': prq.status - }) - + def accept_merge(self, user, prq, lib): + if prq.status not in ['N', 'E']: + return BadRequest().django_response({ + 'reason': 'invalid-state', + 'message': "This pull request is alredy resolved. Can't accept." + }) + + src_doc = lib.document( prq.source_revision ) + + lock = lib.lock() + try: + if not src_doc.up_to_date(): + # This revision is no longer up to date, thus + # it needs to be updated, before push: + # + # Q: where to put the updated revision ? + # A: create a special user branch named prq-#prqid + prq_doc = src_doc.take("$prq-%d" % prd.id) + + # This could be not the first time we try this, + # so the prq_doc could already be there + # and up to date + + success, changed = prq_doc.update(user.username) + prq.status = 'E' + + if not success: + prq.save() + # this can happen only if the merge program + # is misconfigured - try returning an entity conflict + # TODO: put some useful infor here + return EntityConflict().django_response() + + if changed: + prq_doc = prq_doc.latest() + + prq.source_revision = str(prq_doc.revision) + src_doc = prq_doc + + # check if there are conflicts + if prq_doc.has_conflict_marks(): + prq.status = 'E' + prq.save() + # Now, the user must resolve the conflict + return EntityConflict().django_response({ + "reason": "unresolved-conflicts", + "message": "There are conflict in the document. Resolve the conflicts retry accepting." + }) + + # So, we have an up-to-date, non-conflicting document + changed = src_doc.share(prq.comment) + + if not changed: + # this is actually very bad, but not critical + log.error("Unsynched pull request: %d" % prq.id) + + # sync state with repository + prq.status = 'A' + prq.merged_revision = str(src_doc.shared().revision) + prq.merged_timestamp = datetime() + prq.save() + + return SuccessAllOk().django_response({ + 'status': prq.status, + 'merged_into': prq.merged_revision, + 'merged_at': prq.merged_timestamp + }) + finally: + lock.release() def reject_merge(self, prq, lib): prq.status = 'R' diff --git a/apps/api/handlers/toolbar_handlers.py b/apps/api/handlers/toolbar_handlers.py index 5408db3b..09a70856 100644 --- a/apps/api/handlers/toolbar_handlers.py +++ b/apps/api/handlers/toolbar_handlers.py @@ -1,5 +1,8 @@ # -*- encoding: utf-8 -*- +import logging +log = logging.getLogger('platforma.api.toolbar') + __author__= "Łukasz Rekucki" __date__ = "$2009-09-25 15:55:33$" __doc__ = "Module documentation." diff --git a/apps/api/models.py b/apps/api/models.py index ac694880..90f962e0 100644 --- a/apps/api/models.py +++ b/apps/api/models.py @@ -1,4 +1,5 @@ from django.db import models +from django.contrib.auth.models import User # Create your models here. class PartCache(models.Model): @@ -22,12 +23,51 @@ class PartCache(models.Model): me.objects.create(user_id=userid, document_id=docid, part_id=part) - - +class PullRequest(models.Model): + REQUEST_STATUSES = { + "N": "New", + "E": "Edited/Conflicting", + "R": "Rejected", + "A": "Accepted & merged", + } - + comitter = models.ForeignKey(User) # the user who request the pull + comment = models.TextField() # addtional comments to the request - - - - \ No newline at end of file + timestamp = models.DateTimeField(auto_now_add=True) + + # document to merge + document = models.CharField(max_length=255) + + # revision to be merged into the main branch + source_revision = models.CharField(max_length=40, unique=True) + target_revision = models.CharField(max_length=40) + + # current status + status = models.CharField(max_length=1, choices=REQUEST_STATUSES.items()) + + # comment to the status change of request (if applicable) + response_comment = models.TextField(blank=True) + + # revision number in which the changes were merged (if any) + merged_revision = models.CharField(max_length=40, blank=True, null=True) + merge_timestamp = models.DateTimeField(blank=True, null=True) + + def __unicode__(self): + return unicode(self.comitter) + u':' + self.document + + + class Meta: + permissions = ( + ("pullrequest.can_view", "Can view pull request's contents."), + ) + + +# This is actually an abstract Model, but if we declare +# it as so Django ignores the permissions :( +class Document(models.Model): + class Meta: + permissions = ( + ("document.can_share", "Can share documents without pull requests."), + ("document.can_view_other", "Can view other's documents."), + ) \ No newline at end of file diff --git a/apps/api/resources.py b/apps/api/resources.py index 103933d5..a124a2fa 100644 --- a/apps/api/resources.py +++ b/apps/api/resources.py @@ -18,7 +18,7 @@ library_resource = Resource(dh.LibraryHandler, **authdata) document_resource = Resource(dh.DocumentHandler, **authdata) document_text_resource = Resource(dh.DocumentTextHandler, **authdata) document_html_resource = Resource(dh.DocumentHTMLHandler, **authdata) -document_dc_resource = Resource(dh.DocumentDublinCoreHandler, **authdata) +# document_dc_resource = Resource(dh.DocumentDublinCoreHandler, **authdata) document_gallery = Resource(dh.DocumentGalleryHandler, **authdata) document_merge = Resource(dh.MergeHandler, **authdata) @@ -41,7 +41,7 @@ __all__ = [ 'document_resource', 'document_text_resource', 'document_html_resource', - 'document_dc_resource', +# 'document_dc_resource', 'document_gallery', 'document_merge', 'toolbar_buttons', diff --git a/apps/api/response.py b/apps/api/response.py index 0d38a3aa..b5513c4e 100644 --- a/apps/api/response.py +++ b/apps/api/response.py @@ -73,10 +73,7 @@ class AccessDenied(ResponseObject): def __init__(self, **kwargs): ResponseObject.__init__(self, 403, **kwargs) - - def django_response(self, reason): - return ResponseObject.django_response(self, \ - body={'reason': reason}) + class EntityNotFound(ResponseObject): diff --git a/apps/api/urls.py b/apps/api/urls.py index 5464374f..db6ff1ef 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -68,9 +68,9 @@ urlpatterns = patterns('', # document_dc_resource, # name="docdc_view_withformat"), - url(urlpath(r'documents', DOC, 'dc', format=False), - document_dc_resource, {'emitter_format': 'json'}, - name="docdc_view"), +# url(urlpath(r'documents', DOC, 'dc', format=False), +# document_dc_resource, {'emitter_format': 'json'}, +# name="docdc_view"), # MERGE url(urlpath(r'documents', DOC, 'revision', format=False), diff --git a/apps/api/utils.py b/apps/api/utils.py index 37a89a29..19309ff6 100644 --- a/apps/api/utils.py +++ b/apps/api/utils.py @@ -35,7 +35,7 @@ def validate_form(formclass, source='GET'): def decorator(func): @wraps(func) - def decorated(self, request, * args, ** kwargs): + def decorated(self, request, *args, **kwargs): form = formclass(getattr(request, source), request.FILES) if not form.is_valid(): diff --git a/apps/explorer/admin.py b/apps/explorer/admin.py index 034eb3bb..1c91b963 100644 --- a/apps/explorer/admin.py +++ b/apps/explorer/admin.py @@ -5,5 +5,4 @@ import explorer.models admin.site.register(explorer.models.EditorSettings) admin.site.register(explorer.models.EditorPanel) -admin.site.register(explorer.models.PullRequest) admin.site.register(explorer.models.GalleryForDocument) \ No newline at end of file diff --git a/apps/explorer/models.py b/apps/explorer/models.py index 23acd7f1..73e9063a 100644 --- a/apps/explorer/models.py +++ b/apps/explorer/models.py @@ -54,42 +54,7 @@ class EditorPanel(models.Model): def __unicode__(self): return self.display_name - -class Document(models.Model): - class Meta: - permissions = ( - ("can_share", "Can share documents without pull requests."), - ) - - pass - -class PullRequest(models.Model): - REQUEST_STATUSES = ( - ("N", "Pending for resolution"), - ("R", "Rejected"), - ("A", "Accepted & merged"), - ) - - comitter = models.ForeignKey(User) # the user who request the pull - comment = models.TextField() # addtional comments to the request - - # document to merge - document = models.CharField(max_length=255) - - # revision to be merged into the main branch - source_revision = models.CharField(max_length=40, unique=True) - - # current status - status = models.CharField(max_length=1, choices=REQUEST_STATUSES) - - # comment to the status change of request (if applicable) - response_comment = models.TextField(blank=True) - - # revision number in which the changes were merged (if any) - merged_rev = models.CharField(max_length=40, blank=True, null=True) - - def __unicode__(self): - return unicode(self.comitter) + u':' + self.document + # Yes, this is intentionally unnormalized ! class GalleryForDocument(models.Model): diff --git a/apps/explorer/views.py b/apps/explorer/views.py index 124a5743..eff1b044 100644 --- a/apps/explorer/views.py +++ b/apps/explorer/views.py @@ -1,48 +1,20 @@ # -*- coding: utf-8 -*- import urllib2 -import hg, re -from datetime import date -import librarian - -from librarian import html, parser, dcparser -from librarian import ParseError, ValidationError +import logging +log = logging.getLogger('platforma.explorer.views') from django.conf import settings from django.contrib.auth.decorators import login_required, permission_required from django.core.urlresolvers import reverse -from django.http import HttpResponseRedirect, HttpResponse, HttpResponseNotFound +from django.http import HttpResponse from django.utils import simplejson as json from django.views.generic.simple import direct_to_template from django.contrib.auth.decorators import login_required -from explorer import forms, models -from toolbar import models as toolbar_models - -from django.forms.util import ErrorList - -import wlrepo - -# -# Some useful decorators - -def file_branch(fileid, user=None): - parts = fileid.split('$') - return ('personal_'+ user.username + '_' if user is not None else '') \ - + 'file_' + parts[0] - -def file_path(fileid): - return 'pub_'+fileid+'.xml' +from api.models import PullRequest -def with_repo(view): - """Open a repository for this view""" - def view_with_repo(request, *args, **kwargs): - kwargs['repo'] = wlrepo.open_library(settings.REPOSITORY_PATH, 'hg') - return view(request, *args, **kwargs) - return view_with_repo - -# def ajax_login_required(view): """Similar ro @login_required, but instead of redirect, just return some JSON stuff with error.""" @@ -54,29 +26,19 @@ def ajax_login_required(view): return view_with_auth @login_required -# @with_repo def display_editor(request, path): - # this is the only entry point where we create an autobranch for the user - # if it doesn't exists. All other views SHOULD fail. - #def ensure_branch_exists(): - # parent = repo.get_branch_tip('default') - # repo._create_branch(file_branch(path, request.user), parent) - -# try: - # repo.with_wlock(ensure_branch_exists) - + user = request.GET.get('user', request.user.username) + log.info(user) + return direct_to_template(request, 'explorer/editor.html', extra_context={ - 'fileid': path, - 'panel_list': ['lewy', 'prawy'], - 'availble_panels': models.EditorPanel.objects.all(), - # 'scriptlets': toolbar_models.Scriptlet.objects.all() + 'fileid': path, + 'euser': user }) # # View all files # -@with_repo -def file_list(request, repo): +def file_list(request): import api.forms from api.resources import library_resource @@ -90,8 +52,7 @@ def file_list(request, repo): 'filetree': doctree['documents'], 'bookform': bookform, }) -@permission_required('explorer.can_add_files') -@with_repo +@permission_required('api.document.can_add') def file_upload(request, repo): from api.resources import library_resource from api.forms import DocumentUploadForm @@ -147,9 +108,7 @@ def _get_issues_for_file(fileid): # ================= # = Pull requests = # ================= -def pull_requests(request): - from explorer.models import PullRequest - +def pull_requests(request): objects = PullRequest.objects.order_by('status') if not request.user.has_perm('explorer.book.can_share'): diff --git a/lib/hg.py b/lib/hg.py deleted file mode 100644 index 06e9f832..00000000 --- a/lib/hg.py +++ /dev/null @@ -1,235 +0,0 @@ -# -*- coding: utf-8 -*- -import os -from mercurial import localrepo, ui, encoding, util -import mercurial.merge, mercurial.error - -encoding.encoding = 'utf-8' - -X = 'g\xc5\xbceg\xc5\xbc\xc3\xb3\xc5\x82ka' - -def sanitize_string(path): - if isinstance(path, unicode): # - return path.encode('utf-8') - else: # it's a string, so we have no idea what encoding it is - return path - -class Repository(object): - """Abstrakcja repozytorium Mercurial. Działa z Mercurial w wersji 1.3.1.""" - - def __init__(self, path, create=False): - self.ui = ui.ui() - self.ui.config('ui', 'quiet', 'true') - self.ui.config('ui', 'interactive', 'false') - - self.real_path = sanitize_string(os.path.realpath(path)) - self.repo = self._open_repository(self.real_path, create) - - def _open_repository(self, path, create=False): - if os.path.isdir(path): - try: - return localrepo.localrepository(self.ui, path) - except mercurial.error.RepoError: - # dir is not an hg repo, we must init it - if create: - return localrepo.localrepository(self.ui, path, create=1) - elif create: - os.makedirs(path) - return localrepo.localrepository(self.ui, path, create=1) - raise RepositoryDoesNotExist("Repository %s does not exist." % path) - - def file_list(self, branch): - return self.in_branch(lambda: self._file_list(), branch) - - def _file_list(self): - return list(self.repo[None]) - - def get_file(self, path, branch): - return self.in_branch(lambda: self._get_file(path), branch) - - def _get_file(self, path): - path = sanitize_string(path) - if not self._file_exists(path): - raise RepositoryException("File not availble in this branch.") - - return self.repo.wread(path) - - def file_exists(self, path, branch): - return self.in_branch(lambda: self._file_exists(path), branch) - - def _file_exists(self, path): - path = sanitize_string(path) - return self.repo.dirstate[path] != "?" - - def write_file(self, path, value, branch): - return self.in_branch(lambda: self._write_file(path, value), branch) - - def _write_file(self, path, value): - path = sanitize_string(path) - return self.repo.wwrite(path, value, []) - - def add_file(self, path, value, branch): - return self.in_branch(lambda: self._add_file(path, value), branch) - - def _add_file(self, path, value): - path = sanitize_string(path) - self._write_file(path, value) - return self.repo.add( [path] ) - - def _commit(self, message, user=None): - return self.repo.commit(text=sanitize_string(message), user=sanitize_string(user)) - - def commit(self, message, branch, user=None): - return self.in_branch(lambda: self._commit(message, key=key, user=user), branch) - - def in_branch(self, action, bname): - wlock = self.repo.wlock() - try: - old = self._switch_to_branch(bname) - try: - # do some stuff - return action() - finally: - self._switch_to_branch(old) - finally: - wlock.release() - - def merge_branches(self, bnameA, bnameB, user, message): - wlock = self.repo.wlock() - try: - return self.merge_revisions(self.get_branch_tip(bnameA), - self.get_branch_tip(bnameB), user, message) - finally: - wlock.release() - - def diff(self, revA, revB): - return UpdateStatus(self.repo.status(revA, revB)) - - def merge_revisions(self, revA, revB, user, message): - wlock = self.repo.wlock() - try: - old = self.repo[None] - - self._checkout(revA) - mergestatus = self._merge(revB) - if not mergestatus.isclean(): - # revert the failed merge - self.repo.recover() - raise UncleanMerge(u'Failed to merge %d files.' % len(mergestatus.unresolved)) - - # commit the clean merge - self._commit(message, user) - - # cleanup after yourself - self._checkout(old.rev()) - except util.Abort, ae: - raise RepositoryException(u'Failed merge: ' + ae.message) - finally: - wlock.release() - - def common_ancestor(self, revA, revB): - return self.repo[revA].ancestor(self.repo[revB]) - - def _checkout(self, rev, force=True): - return MergeStatus(mercurial.merge.update(self.repo, rev, False, force, None)) - - def _merge(self, rev): - """ Merge the revision into current working directory """ - return MergeStatus(mercurial.merge.update(self.repo, rev, True, False, None)) - - def _switch_to_branch(self, bname): - bname = sanitize_string(bname) - wlock = self.repo.wlock() - try: - current = self.repo[None].branch() - if current == bname: - return current - - tip = self.get_branch_tip(bname) - status = self._checkout(tip) - - if not status.isclean(): - raise RepositoryException("Unclean branch switch. This IS REALLY bad.") - - return current - except KeyError, ke: - raise RepositoryException((u"Can't switch to branch '%s': no such branch." % bname) , ke) - except util.Abort, ae: - raise RepositoryException(u"Can't switch to branch '%s': %s" % (bname, ae.message), ae) - finally: - wlock.release() - - def with_wlock(self, action): - wlock = self.repo.wlock() - try: - action() - finally: - wlock.release() - - def _create_branch(self, name, parent_rev, msg=None, before_commit=None): - """WARNING: leaves the working directory in the new branch""" - name = sanitize_string(name) - - if self.has_branch(name): return # just exit - - self._checkout(parent_rev) - self.repo.dirstate.setbranch(name) - - if msg is None: - msg = "Initial commit for branch '%s'." % name - - if before_commit: before_commit() - self._commit(msg, user='platform') - return self.get_branch_tip(name) - - def write_lock(self): - """Returns w write lock to the repository.""" - return self.repo.wlock() - - def has_branch(self, name): - name = sanitize_string(name) - return (name in self.repo.branchmap().keys()) - - def get_branch_tip(self, name): - name = sanitize_string(name) - return self.repo.branchtags()[name] - - def getnode(self, rev): - return self.repo[rev] - -class MergeStatus(object): - - def __init__(self, mstatus): - self.updated = mstatus[0] - self.merged = mstatus[1] - self.removed = mstatus[2] - self.unresolved = mstatus[3] - - def isclean(self): - return self.unresolved == 0 - -class UpdateStatus(object): - - def __init__(self, mstatus): - self.modified = mstatus[0] - self.added = mstatus[1] - self.removed = mstatus[2] - self.deleted = mstatus[3] - self.untracked = mstatus[4] - self.ignored = mstatus[5] - self.clean = mstatus[6] - - def has_changes(self): - return bool( len(self.modified) + len(self.added) + \ - len(self.removed) + len(self.deleted) ) - -class RepositoryException(Exception): - def __init__(self, msg, cause=None): - Exception.__init__(self, msg) - self.cause = cause - -class UncleanMerge(RepositoryException): - pass - -class RepositoryDoesNotExist(RepositoryException): - pass - diff --git a/lib/wlrepo/__init__.py b/lib/wlrepo/__init__.py index 430e59f9..9de75a02 100644 --- a/lib/wlrepo/__init__.py +++ b/lib/wlrepo/__init__.py @@ -18,7 +18,7 @@ class Library(object): """Retrieve a document in the specified revision.""" pass - def document(self, docid, user=None): + def document(self, docid, user=None, rev='latest'): """Retrieve a document from a library.""" pass @@ -108,6 +108,10 @@ class LibraryException(Exception): class RevisionNotFound(LibraryException): def __init__(self, rev): LibraryException.__init__(self, "Revision %r not found." % rev) + +class RevisionMismatch(LibraryException): + def __init__(self, fdi, rev): + LibraryException.__init__(self, "No revision %r for document %r." % (rev, fdi)) class EntryNotFound(LibraryException): def __init__(self, rev, entry, guesses=[]): diff --git a/lib/wlrepo/mercurial_backend/__init__.py b/lib/wlrepo/mercurial_backend/__init__.py index 630939f7..536d08ce 100644 --- a/lib/wlrepo/mercurial_backend/__init__.py +++ b/lib/wlrepo/mercurial_backend/__init__.py @@ -1,5 +1,8 @@ # -*- encoding: utf-8 -*- +import logging +log = logging.getLogger('ral.mercurial') + __author__= "Łukasz Rekucki" __date__ = "$2009-09-25 09:20:22$" __doc__ = "Module documentation." diff --git a/lib/wlrepo/mercurial_backend/document.py b/lib/wlrepo/mercurial_backend/document.py index a8f7adc3..0883d99b 100644 --- a/lib/wlrepo/mercurial_backend/document.py +++ b/lib/wlrepo/mercurial_backend/document.py @@ -1,11 +1,18 @@ # -*- encoding: utf-8 -*- +import logging +log = logging.getLogger('ral.mercurial') + __author__ = "Łukasz Rekucki" __date__ = "$2009-09-25 09:35:06$" __doc__ = "Module documentation." import wlrepo import mercurial.error +import re + +import logging +log = logging.getLogger('wlrepo.document') class MercurialDocument(wlrepo.Document): @@ -71,6 +78,9 @@ class MercurialDocument(wlrepo.Document): def ismain(self): return self._revision.user_name is None + def islatest(self): + return (self == self.latest()) + def shared(self): if self.ismain(): return self @@ -85,13 +95,20 @@ class MercurialDocument(wlrepo.Document): def take_action(library, resolve): # branch from latest - library._create_branch(fullid, parent=self._revision) + library._set_branchname(fullid) if not self._library.has_revision(fullid): + log.info("Checking out document %s" % fullid) + self.invoke_and_commit(take_action, \ lambda d: ("$AUTO$ File checkout.", user) ) return self._library.document_for_rev(fullid) + + def up_to_date(self): + return self.ismain() or (\ + self.shared().ancestorof(self) ) + def update(self, user): """Update parts of the document.""" @@ -102,7 +119,7 @@ class MercurialDocument(wlrepo.Document): return (True, False) if self._revision.has_children(): - print 'Update failed: has children.' + log.info('Update failed: has children.') # can't update non-latest revision return (False, False) @@ -114,7 +131,6 @@ class MercurialDocument(wlrepo.Document): if sv.ancestorof(self): return (True, False) - return self._revision.merge_with(sv._revision, user=user, message="$AUTO$ Personal branch update.") finally: @@ -124,7 +140,7 @@ class MercurialDocument(wlrepo.Document): lock = self.library.lock() try: if self.ismain(): - return (True, False) # always shared + return False # always shared user = self._revision.user_name main = self.shared()._revision @@ -142,29 +158,17 @@ class MercurialDocument(wlrepo.Document): # 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 - - if main.ancestorof(local): - print "case 1" - success, changed = main.merge_with(local, user=user, message=message) - # Case 2: - # - # main * * local - # |\ | - # | \| - # | * - # | | # - # Default has no changes, to update from this branch - # since the last merge of local to default. - elif local.has_common_ancestor(main): - print "case 2" - if not local.parentof(main): - success, changed = main.merge_with(local, user=user, message=message) + # This is actually the only good case! + if main.ancestorof(local): + success, changed = main.merge_with(local, user=user, message=message) - success = True - changed = False + if not success: + raise LibraryException("Merge failed.") - # Case 3: + return changed + + # Case 2: # main * # | # * <- this case overlaps with previos one @@ -176,41 +180,27 @@ class MercurialDocument(wlrepo.Document): # There was a recent merge to the defaul branch and # no changes to local branch recently. # - # Use the fact, that user is prepared to see changes, to - # update his branch if there are any - elif local.ancestorof(main): - print "case 3" - if not local.parentof(main): - success, changed = local.merge_with(main, user=user, \ - message='$AUTO$ Local branch update during share.') - - success = True - changed = False - - else: - print "case 4" - success, changed = local.merge_with(main, user=user, \ - message='$AUTO$ Local branch update during share.') - - if not success: - return False + # Nothing to do + elif local.ancestorof(main): + return False - if changed: - local = self.latest()._revision - - success, changed = main.merge_with(local, user=user,\ - message=message) - - return success, changed + # In all other cases, the local needs an update + # and possibly conflict resolution, so fail + raise LibraryExcepton("Document not prepared for sharing.") + finally: - lock.release() + lock.release() + + + def has_conflict_marks(self): + return re.search("^(?:<<<<<<< .*|=======|>>>>>>> .*)$", self.data('xml'), re.MULTILINE) def __unicode__(self): - return u"Document(%s:%s)" % (self.name, self.owner) + return u"Document(%s:%s)" % (self.id, self.owner) def __str__(self): return self.__unicode__().encode('utf-8') def __eq__(self, other): - return (self._revision == other._revision) and (self.name == other.name) + return (self._revision == other._revision) diff --git a/lib/wlrepo/mercurial_backend/library.py b/lib/wlrepo/mercurial_backend/library.py index 4c072cb8..84e76540 100644 --- a/lib/wlrepo/mercurial_backend/library.py +++ b/lib/wlrepo/mercurial_backend/library.py @@ -1,5 +1,8 @@ # -*- encoding: utf-8 -*- +import logging +log = logging.getLogger('ral.mercurial') + __author__= "Łukasz Rekucki" __date__ = "$2009-09-25 09:33:02$" __doc__ = "Module documentation." @@ -92,13 +95,23 @@ class MercurialLibrary(wlrepo.Library): # every revision is a document return self._doccache[str(rev)] - def document(self, docid, user=None): - return self.document_for_rev(self.fulldocid(docid, user)) + def document(self, docid, user=None, rev=u'latest'): + rev = self._sanitize_string(rev) + + if rev != u'latest': + doc = self.document_for_rev(rev) + + if doc.id != docid or (doc.owner != user): + raise wlrepo.RevisionMismatch(self.fulldocid(docid, user)+u'@'+unicode(rev)) + + return doc + else: + return self.document_for_rev(self.fulldocid(docid, user)) def get_revision(self, revid): revid = self._sanitize_string(revid) - print "Looking up rev %r (%s)" %(revid, type(revid)) + log.info("Looking up rev %r (%s)" %(revid, type(revid)) ) try: ctx = self._changectx( revid ) @@ -220,7 +233,7 @@ class MercurialLibrary(wlrepo.Library): name = self._sanitize_string(name) return self._hgrepo.branchtags()[name] - def _create_branch(self, name, parent=None, before_commit=None): + def _create_branch(self, name, parent=None, before_commit=None, message=None): name = self._sanitize_string(name) if self._has_branch(name): return # just exit @@ -231,11 +244,16 @@ class MercurialLibrary(wlrepo.Library): parentrev = parent.hgrev() self._checkout(parentrev) - self._hgrepo.dirstate.setbranch(name) + self._set_branchname(name) if before_commit: before_commit(self) - self._commit("$ADMN$ Initial commit for branch '%s'." % name, user='$library') + message = message or "$ADMN$ Initial commit for branch '%s'." % name + self._commit(message, user='$library') + + def _set_branchname(self, name): + name = self._sanitize_string(name) + self._hgrepo.dirstate.setbranch(name) def _switch_to_branch(self, branchname): current = self._hgrepo[None].branch() diff --git a/project/static/css/master.css b/project/static/css/master.css index ae51e475..cebb0dc4 100644 --- a/project/static/css/master.css +++ b/project/static/css/master.css @@ -16,12 +16,17 @@ button img { /* default form style hacks */ select { border: none; - margin-left: 0.1em; + margin-left: 0.1em;f } #body-wrap { margin: 0px; - padding: 0px; + padding: 0px; + position: fixed; + left: 0px; + right: 0px; + bottom: 0px; + top: 0px; } #header { @@ -59,98 +64,6 @@ select { vertical-align: middle; } -/* ========== */ -/* = Panels = */ -/* ========== */ - -/* #panels { - height: 100%; - width: 100%; -} - -.panel-wrap { - overflow: hidden; - position: absolute; - top: 0px; - bottom: 0px; -} - -#left-panel-wrap { - left: 0px; - width: 8px; -} - -#right-panel-wrap { - right: 0px; - width: auto; - left: 8px; -} - -.panel-content { - position: absolute; - overflow: auto; - overflow-x: hidden; - top: 25px; left: 0px; bottom:0px; right: 0px; -} - -.panel-overlay { - position: absolute; - top: 0px; bottom: 0px; left: 0px; right: 0px; - z-index: 100; - background: gray; - opacity: 0.8; - text-align: center; - overflow: hidden; - display: none; - cursor: col-resize; -} - -.panel-content-overlay { -} - -.panel-wrap.last-panel .panel-content { - right: 0px; -} - -.panel-wrap.last-panel .panel-slider { - display: none; -} - -.panel-wrap .panel-toolbar { - position: absolute; - top: 0px; left:0px; right: 0px; height: 26px; - padding: 0px; - - border-bottom: 1px solid #AAA; - z-index: 80; -} -.panel-wrap .panel-slider { - position: absolute; - background-color: #DDD; - - top: 0px; bottom: 0px; right: 0px; width: 4px; - - border-left: 1px solid #999; - border-right: 1px solid #999; - border-top: none; - border-bottom: none; - - z-index: 90; - cursor: col-resize; -} - -.panel-wrap .panel-slider:hover { - background-color: #999; -} - -.panel-content-overlay.panel-wrap .panel-slider { - background-color: #DDD; -} - -*/ - -/* OLD STUFF AGAIN */ - /* Commit dialog */ #commit-dialog-error-empty-message { color: red; @@ -244,15 +157,23 @@ text#commit-dialog-message { background: #FFF; opacity: 0.8; text-align: center; - text-valign: center; + vertical-align: middle; + + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + + user-select: 'none'; + -webkit-user-select: 'none'; + -khtml-user-select: 'none'; + -moz-user-select: 'none'; + overflow: 'hidden'; } -.view-overlay p { - display: block; - position: relative; - top: auto; - bottom: auto; - height: 40px; +.view-overlay div { + position: absolute; } /* .buttontoolbarview { diff --git a/project/static/js/app.js b/project/static/js/app.js index fde15393..e8b439df 100644 --- a/project/static/js/app.js +++ b/project/static/js/app.js @@ -180,6 +180,28 @@ Editor.Object = Class.extend({ } }); +// Handle JSON error responses in uniform way +function parseXHRError(response) +{ + var message = "" + try { + var json = $.evalJSON(response.responseText); + + if(json.reason == 'xml-parse-error') { + message = json_response.message.replace(/(line\s+)(\d+)(\s+)/i, + "$1$2$3"); + + message = message.replace(/(line\s+)(\d+)(\,\s*column\s+)(\d+)/i, + "$1$2$3$4"); + } + message = json_response.message || json_response.reason || "Nieznany błąd :(("; + } catch(e) { + // not a valid JSON response + message = response.statusText; + } + return message; +} + Editor.Object._lastGuid = 0; -var panels = []; +var panels = []; \ No newline at end of file diff --git a/project/static/js/messages.js b/project/static/js/messages.js index 51a457dd..eddb46e4 100644 --- a/project/static/js/messages.js +++ b/project/static/js/messages.js @@ -1,32 +1,68 @@ /*global Editor*/ Editor.MessageCenter = Editor.Object.extend({ - init: function() { - this.messages = []; - this.flashMessages = []; - this.firstFlashMessage = null; - }, + init: function() { + this.messages = []; + this.flashMessages = []; + this.firstFlashMessage = null; + this.timeout = null; + console.log("MSC-init:", Date(), this); + }, - addMessage: function(type, text, flash) { - if (!flash) { - flash = text; - } - this.messages.push({type: type, text: text}); - this.flashMessages.push({type: type, text: flash}); - if (this.flashMessages.length == 1) { - this.set('firstFlashMessage', this.flashMessages[0]); - setTimeout(this.changeFlashMessage.bind(this), 1000 * 10); - } - }, + addMessage: function(type, tag, text, flash) + { + if (!tag) tag = '#default' + + if (!flash) { + flash = text; + } + + this.messages.push({ + type: type, + text: text + }); + + this.flashMessages.push({ + type: type, + text: flash, + tag: tag + }); + + if(this.timeout) { + if(this.flashMessages[0] && (this.flashMessages[0].tag == tag)) + { + clearTimeout(this.timeout); + this.timeout = null; + this.changeFlashMessage(); + } + } + + else { + /* queue was empty at the start */ + if (this.flashMessages.length == 1) { + console.log("MSC-added-fisrt", Date(), this); + this.set('firstFlashMessage', this.flashMessages[0]); + this.timeout = setTimeout(this.changeFlashMessage.bind(this), 3000); + } + + } + + }, - changeFlashMessage: function() { - this.flashMessages.splice(0, 1); - if (this.flashMessages.length > 0) { - this.set('firstFlashMessage', this.flashMessages[0]); - setTimeout(this.changeFlashMessage.bind(this), 1000 * 3); // 3 seconds - } else { - this.set('firstFlashMessage', null); + changeFlashMessage: function() + { + console.log("MSC-change", Date(), this); + var previous = this.flashMessages.splice(0, 1); + + if (this.flashMessages.length > 0) + { + console.log("MSC-chaning-first", Date(), this); + this.set('firstFlashMessage', this.flashMessages[0]); + this.timeout = setTimeout(this.changeFlashMessage.bind(this), 3000); + } else { + console.log("MSC-emptying", Date(), this); + this.set('firstFlashMessage', null); + } } - } }); diff --git a/project/static/js/models.js b/project/static/js/models.js index ab021f51..6399c037 100644 --- a/project/static/js/models.js +++ b/project/static/js/models.js @@ -4,7 +4,6 @@ Editor.Model = Editor.Object.extend({ data: null }); - Editor.ToolbarButtonsModel = Editor.Model.extend({ className: 'Editor.ToolbarButtonsModel', buttons: {}, @@ -16,7 +15,7 @@ Editor.ToolbarButtonsModel = Editor.Model.extend({ load: function() { if (!this.get('buttons').length) { $.ajax({ - url: toolbarUrl, + url: documentInfo.toolbarURL, dataType: 'json', success: this.loadSucceeded.bind(this) }); @@ -51,10 +50,11 @@ Editor.XMLModel = Editor.Model.extend({ data: '', state: 'empty', - init: function(serverURL, revision) { + init: function(document, serverURL) { this._super(); this.set('state', 'empty'); - this.set('revision', revision); + this.set('revision', document.get('revision')); + this.document = document; this.serverURL = serverURL; this.toolbarButtonsModel = new Editor.ToolbarButtonsModel(); this.addObserver(this, 'data', this.dataChanged.bind(this)); @@ -63,12 +63,13 @@ Editor.XMLModel = Editor.Model.extend({ load: function(force) { if (force || this.get('state') == 'empty') { this.set('state', 'loading'); - messageCenter.addMessage('info', 'Wczytuję XML...'); + messageCenter.addMessage('info', 'xmlload', 'Wczytuję XML...'); $.ajax({ url: this.serverURL, dataType: 'text', data: { - revision: this.get('revision') + revision: this.get('revision'), + user: this.document.get('user') }, success: this.loadingSucceeded.bind(this), error: this.loadingFailed.bind(this) @@ -84,26 +85,29 @@ Editor.XMLModel = Editor.Model.extend({ } this.set('data', data); this.set('state', 'synced'); - messageCenter.addMessage('success', 'Wczytałem XML :-)'); + messageCenter.addMessage('success', 'xmlload', 'Wczytałem XML :-)'); }, loadingFailed: function() { if (this.get('state') != 'loading') { alert('erroneous state:', this.get('state')); } - this.set('error', 'Nie udało się załadować panelu'); + var message = parseXHRError(response); + + this.set('error', '

Błąd przy ładowaniu XML

'+message+'

'); this.set('state', 'error'); - messageCenter.addMessage('error', 'Nie udało mi się wczytać XML. Spróbuj ponownie :-('); + messageCenter.addMessage('error', 'xmlload', 'Nie udało mi się wczytać XML. Spróbuj ponownie :-('); }, - update: function(message) { + save: function(message) { if (this.get('state') == 'dirty') { this.set('state', 'updating'); - messageCenter.addMessage('info', 'Zapisuję XML...'); + messageCenter.addMessage('info', 'xmlsave', 'Zapisuję XML...'); var payload = { contents: this.get('data'), - revision: this.get('revision') + revision: this.get('revision'), + user: this.document.get('user') }; if (message) { payload.message = message; @@ -114,28 +118,28 @@ Editor.XMLModel = Editor.Model.extend({ type: 'post', dataType: 'json', data: payload, - success: this.updatingSucceeded.bind(this), - error: this.updatingFailed.bind(this) + success: this.saveSucceeded.bind(this), + error: this.saveFailed.bind(this) }); return true; } return false; }, - updatingSucceeded: function(data) { + saveSucceeded: function(data) { if (this.get('state') != 'updating') { alert('erroneous state:', this.get('state')); } this.set('revision', data.revision); this.set('state', 'updated'); - messageCenter.addMessage('success', 'Zapisałem XML :-)'); + messageCenter.addMessage('success', 'xmlsave', 'Zapisałem XML :-)'); }, - updatingFailed: function() { + saveFailed: function() { if (this.get('state') != 'updating') { alert('erroneous state:', this.get('state')); } - messageCenter.addMessage('error', 'Nie udało mi się zapisać XML. Spróbuj ponownie :-('); + messageCenter.addMessage('error', 'xmlsave', 'Nie udało mi się zapisać XML. Spróbuj ponownie :-('); this.set('state', 'dirty'); }, @@ -169,13 +173,15 @@ Editor.HTMLModel = Editor.Model.extend({ xmlParts: {}, state: 'empty', - init: function(htmlURL, revision, dataURL) { + init: function(document, dataURL, htmlURL) { this._super(); this.set('state', 'empty'); - this.set('revision', revision); + this.set('revision', document.get('revision')); + + this.document = document; this.htmlURL = htmlURL; this.dataURL = dataURL; - this.renderURL = "http://localhost:8000/api/render"; + this.renderURL = documentInfo.renderURL; this.xmlParts = {}; }, @@ -190,13 +196,14 @@ Editor.HTMLModel = Editor.Model.extend({ url: this.htmlURL, dataType: 'text', data: { - revision: this.get('revision') + revision: this.get('revision'), + user: this.document.get('user') }, success: this.loadingSucceeded.bind(this), error: this.loadingFailed.bind(this) }); } - }, + }, loadingSucceeded: function(data) { if (this.get('state') != 'loading') { @@ -204,42 +211,17 @@ Editor.HTMLModel = Editor.Model.extend({ } this.set('data', data); this.set('state', 'synced'); - // messageCenter.addMessage('success', 'Wczytałem HTML :-)'); }, loadingFailed: function(response) { if (this.get('state') != 'loading') { alert('erroneous state:', this.get('state')); } - - var json_response = null; - var message = ""; - - try { - json_response = $.evalJSON(response.responseText); - - if(json_response.reason == 'xml-parse-error') { - - message = json_response.message.replace(/(line\s+)(\d+)(\s+)/i, - "$1$2$3"); - - message = message.replace(/(line\s+)(\d+)(\,\s*column\s+)(\d+)/i, - "$1$2$3$4"); - - - } - else { - message = json_response.message || json_response.reason || "nieznany błąd."; - } - } - catch (e) { - message = response.statusText; - } - + + var message = parseXHRError(response); + this.set('error', '

Nie udało się wczytać widoku HTML:

' + message); - - this.set('state', 'error'); - // messageCenter.addMessage('error', 'Nie udało mi się wczytać HTML. Spróbuj ponownie :-('); + this.set('state', 'error'); }, getXMLPart: function(elem, callback) @@ -261,6 +243,7 @@ Editor.HTMLModel = Editor.Model.extend({ dataType: 'text', data: { revision: this.get('revision'), + user: this.document.get('user'), part: path }, success: function(data) { @@ -299,13 +282,14 @@ Editor.HTMLModel = Editor.Model.extend({ }); }, - update: function(message) { + save: function(message) { if (this.get('state') == 'dirty') { this.set('state', 'updating'); var payload = { chunks: $.toJSON(this.xmlParts), - revision: this.get('revision') + revision: this.get('revision'), + user: this.document.get('user') }; if (message) { @@ -319,8 +303,8 @@ Editor.HTMLModel = Editor.Model.extend({ type: 'post', dataType: 'json', data: payload, - success: this.updatingSucceeded.bind(this), - error: this.updatingFailed.bind(this) + success: this.saveSucceeded.bind(this), + error: this.saveFailed.bind(this) }); return true; } @@ -328,7 +312,7 @@ Editor.HTMLModel = Editor.Model.extend({ }, - updatingSucceeded: function(data) { + saveSucceeded: function(data) { if (this.get('state') != 'updating') { alert('erroneous state:', this.get('state')); } @@ -340,11 +324,10 @@ Editor.HTMLModel = Editor.Model.extend({ this.set('state', 'updated'); }, - updatingFailed: function() { + saveFailed: function() { if (this.get('state') != 'updating') { alert('erroneous state:', this.get('state')); - } - messageCenter.addMessage('error', 'Uaktualnienie nie powiodło się', 'Uaktualnienie nie powiodło się'); + } this.set('state', 'dirty'); }, @@ -392,8 +375,7 @@ Editor.ImageGalleryModel = Editor.Model.extend({ if (data.length === 0) { this.set('data', []); - } else { - console.log('dupa'); + } else { this.set('data', data[0].pages); } @@ -414,22 +396,25 @@ Editor.DocumentModel = Editor.Model.extend({ data: null, // name, text_url, user_revision, latest_shared_rev, parts_url, dc_url, size, merge_url contentModels: {}, state: 'empty', + errors: '', + revision: '', + user: '', init: function() { this._super(); - this.set('state', 'empty'); - this.load(); + this.set('state', 'empty'); }, load: function() { if (this.get('state') == 'empty') { this.set('state', 'loading'); - messageCenter.addMessage('info', 'Ładuję dane dokumentu...'); + messageCenter.addMessage('info', 'docload', 'Ładuję dane dokumentu...'); $.ajax({ cache: false, - url: documentsUrl + fileId, + url: documentInfo.docURL, dataType: 'json', - success: this.successfulLoad.bind(this) + success: this.successfulLoad.bind(this), + error: this.failedLoad.bind(this) }); } }, @@ -437,15 +422,33 @@ Editor.DocumentModel = Editor.Model.extend({ successfulLoad: function(data) { this.set('data', data); this.set('state', 'synced'); + + this.set('revision', data.user_revision); + this.set('user', data.user); + this.contentModels = { - 'xml': new Editor.XMLModel(data.text_url, data.user_revision), - 'html': new Editor.HTMLModel(data.html_url, data.user_revision, data.text_url), - 'gallery': new Editor.ImageGalleryModel(data.gallery_url) - }; + 'xml': new Editor.XMLModel(this, data.text_url), + 'html': new Editor.HTMLModel(this, data.text_url, data.html_url), + 'gallery': new Editor.ImageGalleryModel(this, data.gallery_url) + }; + for (var key in this.contentModels) { this.contentModels[key].addObserver(this, 'state', this.contentModelStateChanged.bind(this)); } - messageCenter.addMessage('success', 'Dane dokumentu zostały załadowane :-)'); + + this.error = ''; + + messageCenter.addMessage('success', 'docload', 'Dokument załadowany poprawnie :-)'); + }, + + failedLoad: function(response) { + if (this.get('state') != 'loading') { + alert('erroneous state:', this.get('state')); + } + + var message = parseXHRError(response); + this.set('error', '

Nie udało się wczytać dokumentu

'+message+"

"); + this.set('state', 'error'); }, contentModelStateChanged: function(property, value, contentModel) { @@ -476,7 +479,7 @@ Editor.DocumentModel = Editor.Model.extend({ saveDirtyContentModel: function(message) { for (var key in this.contentModels) { if (this.contentModels[key].get('state') == 'dirty') { - this.contentModels[key].update(message); + this.contentModels[key].save(message); break; } } @@ -491,7 +494,8 @@ Editor.DocumentModel = Editor.Model.extend({ type: 'post', data: { type: 'update', - target_revision: this.data.user_revision + revision: this.revision, + user: this.user }, complete: this.updateCompleted.bind(this), success: function(data) { @@ -503,21 +507,24 @@ Editor.DocumentModel = Editor.Model.extend({ updateCompleted: function(xhr, textStatus) { console.log(xhr.status, textStatus); if (xhr.status == 200) { // Sukces - this.data.user_revision = this.get('updateData').revision; - messageCenter.addMessage('info', 'Uaktualnienie dokumentu do wersji ' + this.get('updateData').revision, + this.data = this.get('updateData'); + this.revision = this.data.user_revision; + this.user = this.data.user; + + messageCenter.addMessage('info', null, 'Uaktualnienie dokumentu do wersji ' + this.get('updateData').revision, 'Uaktualnienie dokumentu do wersji ' + this.get('updateData').revision); for (var key in this.contentModels) { this.contentModels[key].set('revision', this.data.user_revision); this.contentModels[key].set('state', 'empty'); } - messageCenter.addMessage('success', 'Uaktualniłem dokument do najnowszej wersji :-)'); + messageCenter.addMessage('success', null, 'Uaktualniłem dokument do najnowszej wersji :-)'); } else if (xhr.status == 202) { // Wygenerowano PullRequest (tutaj?) } else if (xhr.status == 204) { // Nic nie zmieniono - messageCenter.addMessage('info', 'Nic się nie zmieniło od ostatniej aktualizacji. Po co mam uaktualniać?'); + messageCenter.addMessage('info', null, 'Nic się nie zmieniło od ostatniej aktualizacji. Po co mam uaktualniać?'); } else if (xhr.status == 409) { // Konflikt podczas operacji - messageCenter.addMessage('error', 'Wystąpił konflikt podczas aktualizacji. Pędź po programistów! :-('); + messageCenter.addMessage('error', null, 'Wystąpił konflikt podczas aktualizacji. Pędź po programistów! :-('); } else if (xhr.status == 500) { - messageCenter.addMessage('critical', 'Błąd serwera. Pędź po programistów! :-('); + messageCenter.addMessage('critical', null, 'Błąd serwera. Pędź po programistów! :-('); } this.set('state', 'synced'); this.set('updateData', null); @@ -525,14 +532,15 @@ Editor.DocumentModel = Editor.Model.extend({ merge: function(message) { this.set('state', 'loading'); - messageCenter.addMessage('info', 'Scalam dokument z głównym repozytorium...'); + messageCenter.addMessage('info', null, 'Scalam dokument z głównym repozytorium...'); $.ajax({ url: this.data.merge_url, type: 'post', dataType: 'json', data: { type: 'share', - target_revision: this.data.user_revision, + revision: this.revision, + user: this.user, message: message }, complete: this.mergeCompleted.bind(this), @@ -545,20 +553,24 @@ Editor.DocumentModel = Editor.Model.extend({ mergeCompleted: function(xhr, textStatus) { console.log(xhr.status, textStatus); if (xhr.status == 200) { // Sukces - this.data.user_revision = this.get('mergeData').revision; + this.data = this.get('updateData'); + this.revision = this.data.user_revision; + this.user = this.data.user; + for (var key in this.contentModels) { - this.contentModels[key].set('revision', this.data.user_revision); + this.contentModels[key].set('revision', this.revision); this.contentModels[key].set('state', 'empty'); } - messageCenter.addMessage('success', 'Scaliłem dokument z głównym repozytorium :-)'); + + messageCenter.addMessage('success', null, 'Scaliłem dokument z głównym repozytorium :-)'); } else if (xhr.status == 202) { // Wygenerowano PullRequest - messageCenter.addMessage('success', 'Wysłałem prośbę o scalenie dokumentu z głównym repozytorium.'); + 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', 'Nic się nie zmieniło od ostatniego scalenia. Po co mam scalać?'); + 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', 'Wystąpił konflikt podczas scalania. Pędź po programistów! :-('); + messageCenter.addMessage('error', null, 'Wystąpił konflikt podczas scalania. Pędź po programistów! :-('); } else if (xhr.status == 500) { - messageCenter.addMessage('critical', 'Błąd serwera. Pędź po programistów! :-('); + messageCenter.addMessage('critical', null, 'Błąd serwera. Pędź po programistów! :-('); } this.set('state', 'synced'); this.set('mergeData', null); @@ -578,16 +590,15 @@ var leftPanelView, rightPanelContainer, doc; $(function() { - documentsUrl = $('#api-base-url').text() + '/'; - toolbarUrl = $('#api-toolbar-url').text(); - + var flashView = new FlashView('#flashview', messageCenter); + doc = new Editor.DocumentModel(); EditorView = new EditorView('#body-wrap', doc); - EditorView.freeze(); + EditorView.freeze("

Wczytuję dokument...

"); leftPanelView = new PanelContainerView('#left-panel-container', doc); rightPanelContainer = new PanelContainerView('#right-panel-container', doc); - var flashView = new FlashView('#flashview', messageCenter); + }); diff --git a/project/static/js/views/editor.js b/project/static/js/views/editor.js index 2793141f..d43caaaa 100644 --- a/project/static/js/views/editor.js +++ b/project/static/js/views/editor.js @@ -29,6 +29,7 @@ var EditorView = View.extend({ $('#commit-dialog-error-empty-message').hide(); $('#commit-dialog').jqmHide(); }); + // $('#split-dialog').jqm({ // modal: true, @@ -117,16 +118,24 @@ 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') { this.quickSaveButton.attr('disabled', 'disabled'); this.commitButton.attr('disabled', 'disabled'); this.updateButton.attr('disabled', null); this.mergeButton.attr('disabled', null); + this.unfreeze(); } else if (value == 'empty') { this.quickSaveButton.attr('disabled', 'disabled'); this.commitButton.attr('disabled', 'disabled'); this.updateButton.attr('disabled', 'disabled'); this.mergeButton.attr('disabled', 'disabled'); + } else if (value == 'error') { + this.freeze(this.model.get('error')); + this.quickSaveButton.attr('disabled', 'disabled'); + this.commitButton.attr('disabled', 'disabled'); + this.updateButton.attr('disabled', 'disabled'); + this.mergeButton.attr('disabled', 'disabled'); + } }, diff --git a/project/static/js/views/flash.js b/project/static/js/views/flash.js index b2240e43..e67b0463 100644 --- a/project/static/js/views/flash.js +++ b/project/static/js/views/flash.js @@ -23,18 +23,20 @@ var FlashView = View.extend({ render: function() { this.element.html(render_template(this.template, this)); + setTimeout(function() {}, 0); }, - modelFirstFlashMessageChanged: function(property, value) { - this.element.fadeOut('slow', function() { - this.element.css({'z-index': 0}); - this.shownMessage = value; - this.render(); + modelFirstFlashMessageChanged: function(property, value) { + this.element.fadeOut(200, (function() { + + this.element.css({'z-index': 0}); + this.shownMessage = value; + this.render(); - if(this.shownMessage) { + if(this.shownMessage) { this.element.css({'z-index': 1000}); - this.element.fadeIn('slow'); - } - }.bind(this)); + this.element.fadeIn(); + }; + }).bind(this)); } }); diff --git a/project/static/js/views/html.js b/project/static/js/views/html.js index a3db1d28..fa52bd0f 100644 --- a/project/static/js/views/html.js +++ b/project/static/js/views/html.js @@ -20,9 +20,12 @@ var HTMLView = View.extend({ modelDataChanged: function(property, value) { $('.htmlview', this.element).html(value); + this.updatePrintLink(); + }, + updatePrintLink: function() { var base = this.$printLink.attr('ui:baseref'); - this.$printLink.attr('href', base + "?revision=" + this.model.get('revision')); + this.$printLink.attr('href', base + "?user="+this.model.document.get('user')+"&revision=" + this.model.get('revision')); }, modelStateChanged: function(property, value) @@ -63,9 +66,7 @@ var HTMLView = View.extend({ if(this.$printLink) this.$printLink.unbind(); this._super(); this.$printLink = $('.html-print-link', this.element); - - var base = this.$printLink.attr('ui:baseref'); - this.$printLink.attr('href', base + "?revision=" + this.model.get('revision')); + this.updatePrintLink(); this.element.bind('click', this.itemClicked.bind(this)); }, diff --git a/project/static/js/views/view.js b/project/static/js/views/view.js index e9ff938e..d0c6d170 100644 --- a/project/static/js/views/view.js +++ b/project/static/js/views/view.js @@ -34,28 +34,22 @@ var View = Editor.Object.extend({ if (this.frozen()) { this.unfreeze(); } - this.overlay = this.overlay - || $('
' + message + '
') - .addClass(this.overlayClass) + this.overlay = this.overlay || $('
' + message + '
'); + + this.overlay.addClass(this.overlayClass) .css({ - position: 'absolute', - width: this.element.width(), - height: this.element.height(), - top: this.element.position().top, - left: this.element.position().left, - 'user-select': 'none', - '-webkit-user-select': 'none', - '-khtml-user-select': 'none', - '-moz-user-select': 'none', - overflow: 'hidden' - }) - .attr('unselectable', 'on') - .appendTo(this.element.parent()); + }).attr('unselectable', 'on') + + this.overlay.appendTo(this.element); + + var ovc = this.overlay.children('div'); + var padV = (this.overlay.height() - ovc.outerHeight())/2; + var padH = (this.overlay.width() - ovc.outerWidth())/2; + this.overlay.children('div').css({ - position: 'relative', - top: this.overlay.height() / 2 - 20 - }); + top: padV, left: padH + }); }, unfreeze: function() { @@ -66,16 +60,14 @@ var View = Editor.Object.extend({ }, resized: function(event) { - if (this.frozen()) { - this.overlay.css({ - position: 'absolute', - width: this.element.width(), - height: this.element.height(), - top: this.element.position().top, - left: this.element.position().left - }).children('div').css({ - position: 'relative', - top: this.overlay.height() / 2 - 20 + if(this.overlay) { + var ovc = this.overlay.children('div'); + var padV = (this.overlay.height() - ovc.outerHeight())/2; + var padH = (this.overlay.width() - ovc.outerWidth())/2; + + this.overlay.children('div').css({ + top: padV, + left: padH }); } }, @@ -87,4 +79,4 @@ var View = Editor.Object.extend({ this.unfreeze(); this.element.html(''); } -}); +}); \ No newline at end of file diff --git a/project/static/js/views/xml.js b/project/static/js/views/xml.js index 1681caee..5c184321 100644 --- a/project/static/js/views/xml.js +++ b/project/static/js/views/xml.js @@ -24,6 +24,9 @@ var XMLView = View.extend({ $(document).bind('xml-scroll-request', this.scrollCallback); this.parent.freeze('Ładowanie edytora...'); + + setTimeout((function(){ + this.editor = new CodeMirror($('.xmlview', this.element).get(0), { parserfile: 'parsexml.js', path: "/static/js/lib/codemirror/", @@ -37,6 +40,8 @@ var XMLView = View.extend({ onChange: this.editorDataChanged.bind(this), initCallback: this.editorDidLoad.bind(this) }); + + }).bind(this), 0); }, resized: function(event) { @@ -56,9 +61,7 @@ var XMLView = View.extend({ this.model .addObserver(this, 'data', this.modelDataChanged.bind(this)) .addObserver(this, 'state', this.modelStateChanged.bind(this)) - .load(); - - this.parent.unfreeze(); + .load(); this.editor.setCode(this.model.get('data')); this.modelStateChanged('state', this.model.get('state')); @@ -66,7 +69,9 @@ var XMLView = View.extend({ editor.grabKeys( this.hotkeyPressed.bind(this), this.isHotkey.bind(this) - ); + ); + + this.parent.unfreeze(); }, editorDataChanged: function() { @@ -85,7 +90,7 @@ var XMLView = View.extend({ } else if (value == 'unsynced') { this.freeze('Niezsynchronizowany...'); } else if (value == 'loading') { - this.freeze('Ładowanie...'); + this.freeze('Ładowanie danych...'); } else if (value == 'saving') { this.freeze('Zapisywanie...'); } else if (value == 'error') { diff --git a/project/templates/explorer/edit_dc.html b/project/templates/explorer/edit_dc.html deleted file mode 100644 index 1ad0c22d..00000000 --- a/project/templates/explorer/edit_dc.html +++ /dev/null @@ -1,4 +0,0 @@ -
-{{ form.as_p }} - -
diff --git a/project/templates/explorer/edit_text.html b/project/templates/explorer/edit_text.html deleted file mode 100644 index 1ad0c22d..00000000 --- a/project/templates/explorer/edit_text.html +++ /dev/null @@ -1,4 +0,0 @@ -
-{{ form.as_p }} - -
diff --git a/project/templates/explorer/editor.html b/project/templates/explorer/editor.html index 3c775d4e..576a7939 100644 --- a/project/templates/explorer/editor.html +++ b/project/templates/explorer/editor.html @@ -7,13 +7,21 @@ {# Libraries #} - + + {# Scriptlets #} @@ -139,7 +147,7 @@ {% endblock extrahead %} -{% block breadcrumbs %}Platforma Redakcyjna > {{ fileid }}{% endblock breadcrumbs %} +{% block breadcrumbs %}Platforma > {{euser}} > {{ fileid }}{% endblock breadcrumbs %} {% block header-toolbar %} Historia @@ -149,9 +157,8 @@ {% endblock %} -{% block maincontent %} - - +{% block maincontent %} +
diff --git a/project/templates/explorer/file_html.html b/project/templates/explorer/file_html.html deleted file mode 100644 index ab8e40b4..00000000 --- a/project/templates/explorer/file_html.html +++ /dev/null @@ -1,42 +0,0 @@ -{% extends "base.html" %} - -{% block extrahead %} - - -{% endblock extrahead %} - -{% block breadcrumbs %}Platforma Redakcyjna ❯ plik {{ hash }}{% endblock breadcrumbs %} - -{% block maincontent %} -
ŹródłoHTML
{{ image_folders_form.folders }}
 
- - -
- {{ object|safe }} -
-{% endblock maincontent %} \ No newline at end of file diff --git a/project/templates/explorer/split.html b/project/templates/explorer/split.html deleted file mode 100755 index 8d3b67bb..00000000 --- a/project/templates/explorer/split.html +++ /dev/null @@ -1,26 +0,0 @@ -
-
- Split options - {% for field in splitform %} - {% if field.is_hidden %} - {{ field}} - {% else %} - {{ field.errors }} - {% ifequal field.html_name 'splitform-autoxml' %} -

- {% else %} -

- {% endifequal %} - {% endif %} - {% endfor %} -
- - -

- - -

-
diff --git a/project/templates/explorer/split_success.html b/project/templates/explorer/split_success.html deleted file mode 100755 index 4f3a57b8..00000000 --- a/project/templates/explorer/split_success.html +++ /dev/null @@ -1,3 +0,0 @@ -

Split successful. You can edit the new part here: -{% url editor_view cfileid %}

-

\ No newline at end of file diff --git a/project/templates/manager/pull_request.html b/project/templates/manager/pull_request.html index c2964c22..3b19c3bd 100644 --- a/project/templates/manager/pull_request.html +++ b/project/templates/manager/pull_request.html @@ -54,7 +54,7 @@ - + {% if objects %} {% for pullreq in objects %} @@ -63,12 +63,16 @@ - + + {% endfor %} {% else %} - + {% endif %}
UtwórUżytkownikKomentarzStanAkcjeZgłoszonoAkcje
{{ pullreq.comitter }} {{ pullreq.comment }} {{ pullreq.status }}{{ pullreq.timestamp }} + + Zobacz +
Brak żądań
Brak żądań