Merge branch 'master' of stigma:platforma
authorŁukasz Rekucki <lrekucki@gmail.com>
Thu, 15 Oct 2009 16:28:32 +0000 (18:28 +0200)
committerŁukasz Rekucki <lrekucki@gmail.com>
Thu, 15 Oct 2009 16:28:32 +0000 (18:28 +0200)
33 files changed:
apps/api/forms.py
apps/api/handlers/library_handlers.py
apps/api/handlers/manage_handlers.py
apps/api/handlers/toolbar_handlers.py
apps/api/models.py
apps/api/resources.py
apps/api/response.py
apps/api/urls.py
apps/api/utils.py
apps/explorer/admin.py
apps/explorer/models.py
apps/explorer/views.py
lib/hg.py [deleted file]
lib/wlrepo/__init__.py
lib/wlrepo/mercurial_backend/__init__.py
lib/wlrepo/mercurial_backend/document.py
lib/wlrepo/mercurial_backend/library.py
project/static/css/master.css
project/static/js/app.js
project/static/js/messages.js
project/static/js/models.js
project/static/js/views/editor.js
project/static/js/views/flash.js
project/static/js/views/html.js
project/static/js/views/view.js
project/static/js/views/xml.js
project/templates/explorer/edit_dc.html [deleted file]
project/templates/explorer/edit_text.html [deleted file]
project/templates/explorer/editor.html
project/templates/explorer/file_html.html [deleted file]
project/templates/explorer/split.html [deleted file]
project/templates/explorer/split_success.html [deleted file]
project/templates/manager/pull_request.html

index d55e629..2c9ae66 100644 (file)
@@ -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
index 488c2d4..be504e9 100644 (file)
@@ -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<link>[^\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
index b3e2760..5905724 100644 (file)
@@ -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'
index 5408db3..09a7085 100644 (file)
@@ -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."
index ac69488..90f962e 100644 (file)
@@ -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
index 103933d..a124a2f 100644 (file)
@@ -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',
index 0d38a3a..b5513c4 100644 (file)
@@ -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):
 
index 5464374..db6ff1e 100644 (file)
@@ -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),
index 37a89a2..19309ff 100644 (file)
@@ -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():
index 034eb3b..1c91b96 100644 (file)
@@ -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
index 23acd7f..73e9063 100644 (file)
@@ -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):
index 124a574..eff1b04 100644 (file)
@@ -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 (file)
index 06e9f83..0000000
--- 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
-
index 430e59f..9de75a0 100644 (file)
@@ -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=[]):
index 630939f..536d08c 100644 (file)
@@ -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."
index a8f7adc..0883d99 100644 (file)
@@ -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)
 
index 4c072cb..84e7654 100644 (file)
@@ -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()
index ae51e47..cebb0dc 100644 (file)
@@ -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 {
index fde1539..e8b439d 100644 (file)
@@ -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,
+            "<a class='xml-editor-ref' href='#xml-$2-1'>$1$2$3</a>");
+
+          message = message.replace(/(line\s+)(\d+)(\,\s*column\s+)(\d+)/i,
+            "<a class='xml-editor-ref' href='#xml-$2-$4'>$1$2$3$4</a>");
+      }
+      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
index 51a457d..eddb46e 100644 (file)
@@ -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);
+        }
     }
-  }
   
 });
 
index ab021f5..6399c03 100644 (file)
@@ -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', '<h2>Błąd przy ładowaniu XML</h2><p>'+message+'</p>');
         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,
-                    "<a class='xml-editor-ref' href='#xml-$2-1'>$1$2$3</a>");
-
-                message = message.replace(/(line\s+)(\d+)(\,\s*column\s+)(\d+)/i,
-                    "<a class='xml-editor-ref' href='#xml-$2-$4'>$1$2$3$4</a>");
-
-                
-            }
-            else {
-                message = json_response.message || json_response.reason || "nieznany błąd.";
-            }
-        }
-        catch (e) {
-            message = response.statusText;
-        }
-
+        
+        var message = parseXHRError(response);
+        
         this.set('error', '<p>Nie udało się wczytać widoku HTML: </p>' + 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', '<h2>Nie udało się wczytać dokumentu</h2><p>'+message+"</p>");
+        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("<h1>Wczytuję dokument...</h1>");
 
     leftPanelView = new PanelContainerView('#left-panel-container', doc);
     rightPanelContainer = new PanelContainerView('#right-panel-container', doc);
 
-    var flashView = new FlashView('#flashview', messageCenter);   
+    
 });
index 2793141..d43caaa 100644 (file)
@@ -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');
+            
         }
     },
   
index b2240e4..e67b046 100644 (file)
@@ -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));
   }
 });
index a3db1d2..fa52bd0 100644 (file)
@@ -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));
     },
index e9ff938..d0c6d17 100644 (file)
@@ -34,28 +34,22 @@ var View = Editor.Object.extend({
         if (this.frozen()) {
             this.unfreeze();
         }
-        this.overlay = this.overlay
-        || $('<div><div>' + message + '</div></div>')
-        .addClass(this.overlayClass)
+        this.overlay = this.overlay || $('<div><div>' + message + '</div></div>');
+
+        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
index 1681cae..5c18432 100644 (file)
@@ -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 (file)
index 1ad0c22..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-<form action="" method="POST">
-{{ form.as_p }}
-<input type="submit" value="Submit" />
-</form>
diff --git a/project/templates/explorer/edit_text.html b/project/templates/explorer/edit_text.html
deleted file mode 100644 (file)
index 1ad0c22..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-<form action="" method="POST">
-{{ form.as_p }}
-<input type="submit" value="Submit" />
-</form>
index 3c775d4..576a793 100644 (file)
@@ -7,13 +7,21 @@
         <link rel="stylesheet" href="{{STATIC_URL}}css/autumn.css" type="text/css" media="screen" title="Autumn colors" charset="utf-8">
        
        <script type="text/javascript" charset="utf-8">
-               var fileId = '{{ fileid }}';
+            var documentInfo = {
+                docID: '{{ fileid }}',
+                userID: '{{ euser }}',
+                docURL: '{% url document_view fileid %}{% if euser %}?user={{ euser|urlencode }}{% endif %}',
+                toolbarURL: '{% url toolbar_buttons %}',
+                renderURL: '{% url api.views.render %}'
+            }          
+                
        </script>
        
        {# Libraries #}
-    <script src="{{STATIC_URL}}js/lib/codemirror/codemirror.js" type="text/javascript" charset="utf-8"></script>
+        <script src="{{STATIC_URL}}js/lib/codemirror/codemirror.js" type="text/javascript" charset="utf-8"></script>
        <script src="{{STATIC_URL}}js/lib/jquery.modal.js" type="text/javascript" charset="utf-8"></script>
         <script src="{{STATIC_URL}}js/lib/jquery.json.js" type="text/javascript" charset="utf-8"></script>
+        
        {# Scriptlets #}
        <script src="{{STATIC_URL}}js/button_scripts.js" type="text/javascript" charset="utf-8"></script>
        
        </script>
 {% endblock extrahead %}
 
-{% block breadcrumbs %}<a href="{% url file_list %}">Platforma Redakcyjna</a> &gt; {{ fileid }}{% endblock breadcrumbs %}
+{% block breadcrumbs %}<a href="{% url file_list %}">Platforma</a> &gt; {{euser}} &gt; {{ fileid }}{% endblock breadcrumbs %}
 
 {% block header-toolbar %}
     <a href="http://stigma.nowoczesnapolska.org.pl/platforma-hg/ksiazki/log/tip/{{ fileid }}.xml" target="_new" >Historia</a>
         <button id="action-quick-save">Quick Save</button>
 {% endblock %}
 
-{% block maincontent %}
-    <p style="display: none;" id="api-base-url">{% url document_list_view %}</p>
-    <p style="display: none;" id="api-toolbar-url">{% url toolbar_buttons %}</p>
+{% block maincontent %}    
+    
 
     <div id="splitview">
         <div id="left-panel-container" class='panel-container'></div>
diff --git a/project/templates/explorer/file_html.html b/project/templates/explorer/file_html.html
deleted file mode 100644 (file)
index ab8e40b..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-{% extends "base.html" %}
-
-{% block extrahead %}
-    <script src="/static/js/jquery.lazyload.js" type="text/javascript" charset="utf-8"></script>
-    <script type="text/javascript" charset="utf-8">        
-        $(function() {
-            $('#id_folders').change(function() {
-                $('#images').load('/images/' + $('#id_folders').val() + '/', function() {
-                    $('#images').data('lastScroll', -1000);
-                });
-            });
-            
-            function resizePanels() {
-                $('#images-wrap').height($(window).height() - 80);
-                $('#file-text').height($(window).height() - 80);
-            }
-            
-            $(window).resize(resizePanels)
-            resizePanels();
-            
-            $('#images-wrap').lazyload('.image-box', {threshold: 600});
-        });
-    </script>
-{% endblock extrahead %}
-
-{% block breadcrumbs %}<a href="/">Platforma Redakcyjna</a> ❯ plik {{ hash }}{% endblock breadcrumbs %}
-
-{% block maincontent %}
-    <div id="tabs"><a href="{% url file_xml hash %}">Źródło</a><a href="{% url file_html hash %}" class="active">HTML</a><div style="padding: 3px; margin-left: 10px">{{ image_folders_form.folders }}</div><div style="clear: both; height: 0; width: 0">&nbsp;</div></div>    
-
-    <div id="sidebar">
-        <div id="images-wrap">
-            <div id="images">
-                <p>Aby zobaczyć obrazki wybierz folder z obrazkami powyżej.</p>
-            </div>
-        </div>
-        <div id="toggle-sidebar"></div>
-    </div>
-    <div id="file-text">
-    {{ object|safe }}
-    </div>
-{% 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 (executable)
index 8d3b67b..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-<form action="{% url explorer.views.split_text fileid %}" method="POST">\r
-    <fieldset>\r
-        <legend>Split options</legend>\r
-        {% for field in splitform %}\r
-        {% if field.is_hidden %}\r
-        {{ field}}\r
-        {% else %}\r
-        {{ field.errors }}\r
-        {% ifequal field.html_name 'splitform-autoxml' %}\r
-        <p><label>{{ field }} {{ field.label }}</label></p>\r
-        {% else %}\r
-        <p><label>{{ field.label }}: {{ field }}</label></p>\r
-        {% endifequal %}\r
-        {% endif %}\r
-        {% endfor %}        \r
-    </fieldset>\r
-\r
-    <fieldset id="split-form-dc-subform" style="display: none;">\r
-    <legend>Dublin Core</legend>\r
-    {{ dcform.as_p }}\r
-    </fieldset>\r
-    <p>\r
-        <button type="submit" id="split-dialog-button-accept">Split</button>\r
-        <button type="button" id="split-dialog-button-close">Close</button>\r
-    </p>\r
-</form>\r
diff --git a/project/templates/explorer/split_success.html b/project/templates/explorer/split_success.html
deleted file mode 100755 (executable)
index 4f3a57b..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-<p>Split successful. You can edit the new part here:\r
-<a href="{% url editor_view cfileid %}" target="_new">{% url editor_view cfileid %}</a></p>\r
-<p><button type="button" id="split-dialog-button-dismiss">Close</button></p>
\ No newline at end of file
index c2964c2..3b19c3b 100644 (file)
@@ -54,7 +54,7 @@
 <table class="request-report" cellspacing="0">
     <tr>
         <th>Utwór</th><th>Użytkownik</th><th>Komentarz</th><th>Stan</th>
-        <th>Akcje</th>
+        <th>Zgłoszono</th><th>Akcje</th>
     </tr>
 {% if objects %}
     {% for pullreq in objects %}
         <td class="column-user">{{ pullreq.comitter }}</td>
         <td class="column-comment">{{ pullreq.comment }}</td>
         <td class="column-status"> {{ pullreq.status }}</td>
-        <td><button type="button" class="accept-button" title="{{pullreq.id}}">Akceptuj</button></td>
+        <td class="column-data">{{ pullreq.timestamp }}</td>
+        <td>
+            <button type="button" class="accept-button" title="{{pullreq.id}}">Akceptuj</button>
+            <a href="{% url editor_view pullreq.document %}?user=$prq-{{pullreq.id}}">Zobacz</a>
+        </td>
     </tr>
     
     {% endfor %}
 {% else %}
-    <tr><td colspan="*">Brak żądań</td></tr>
+    <tr><td colspan="6">Brak żądań</td></tr>
 {% endif %}
 </table>