From: zuber Date: Sat, 26 Sep 2009 18:28:35 +0000 (+0200) Subject: Merge branch 'master' into view-refactor X-Git-Url: https://git.mdrn.pl/redakcja.git/commitdiff_plain/eb77cf373a37d609fcf39626c36d2878d4a60e6c?hp=80a2c67a898b06c9c28bda89d7aa012676c2a23a Merge branch 'master' into view-refactor --- diff --git a/apps/api/handlers/library_handlers.py b/apps/api/handlers/library_handlers.py index 06513522..5f844dba 100644 --- a/apps/api/handlers/library_handlers.py +++ b/apps/api/handlers/library_handlers.py @@ -7,19 +7,21 @@ __doc__ = "Module documentation." from piston.handler import BaseHandler, AnonymousBaseHandler -import settings import librarian +import librarian.html import api.forms as forms from datetime import date from django.core.urlresolvers import reverse -from wlrepo import MercurialLibrary, RevisionNotFound, DocumentAlreadyExists +from wlrepo import RevisionNotFound, LibraryException, DocumentAlreadyExists from librarian import dcparser import api.response as response from api.utils import validate_form, hglibrary +from explorer.models import PullRequest + # # Document List Handlers # @@ -56,7 +58,7 @@ class LibraryHandler(BaseHandler): """Create a new document.""" if form.cleaned_data['ocr_data']: - data = form.cleaned_data['ocr_data'].encode('utf-8') + data = form.cleaned_data['ocr_data'] else: data = request.FILES['ocr_file'].read().decode('utf-8') @@ -64,20 +66,34 @@ class LibraryHandler(BaseHandler): data = librarian.wrap_text(data, unicode(date.today())) docid = form.cleaned_data['bookname'] + try: - doc = lib.document_create(docid) - doc = doc.quickwrite('xml', data, '$AUTO$ XML data uploaded.', - user=request.user.username) - - url = reverse('document_view', args=[doc.id]) - - return response.EntityCreated().django_response(\ - body = { - 'url': url, - 'name': doc.id, - 'revision': doc.revision }, - url = url ) - + lock = lib.lock() + try: + doc = lib.document_create(docid) + # document created, but no content yet + + try: + doc = doc.quickwrite('xml', data.encode('utf-8'), + '$AUTO$ XML data uploaded.', user=request.user.username) + except Exception,e: + # rollback branch creation + lib._rollback() + raise LibraryException("Exception occured:" + repr(e)) + + url = reverse('document_view', args=[doc.id]) + + return response.EntityCreated().django_response(\ + body = { + 'url': url, + 'name': doc.id, + 'revision': doc.revision }, + url = url ) + finally: + lock.release() + except LibraryException, e: + return response.InternalError().django_response(\ + {'exception': repr(e) }) except DocumentAlreadyExists: # Document is already there return response.EntityConflict().django_response(\ @@ -98,6 +114,7 @@ class BasicDocumentHandler(AnonymousBaseHandler): result = { 'name': doc.id, + 'html_url': reverse('dochtml_view', args=[doc.id,doc.revision]), 'text_url': reverse('doctext_view', args=[doc.id,doc.revision]), 'dc_url': reverse('docdc_view', args=[doc.id,doc.revision]), 'public_revision': doc.revision, @@ -126,8 +143,9 @@ class DocumentHandler(BaseHandler): result = { 'name': udoc.id, - 'text_url': reverse('doctext_view', args=[doc.id,doc.revision]), - 'dc_url': reverse('docdc_view', args=[doc.id,doc.revision]), + 'html_url': reverse('dochtml_view', args=[udoc.id,udoc.revision]), + 'text_url': reverse('doctext_view', args=[udoc.id,udoc.revision]), + 'dc_url': reverse('docdc_view', args=[udoc.id,udoc.revision]), 'user_revision': udoc.revision, 'public_revision': doc.revision, } @@ -138,6 +156,25 @@ class DocumentHandler(BaseHandler): def update(self, request, docid, lib): """Update information about the document, like display not""" return +# +# +# + +class DocumentHTMLHandler(BaseHandler): + allowed_methods = ('GET', 'PUT') + + @hglibrary + def read(self, request, docid, revision, lib): + """Read document as html text""" + try: + if revision == 'latest': + document = lib.document(docid) + else: + document = lib.document_for_rev(revision) + + return librarian.html.transform(document.data('xml')) + except RevisionNotFound: + return response.EntityNotFound().django_response() # # Document Text View @@ -178,15 +215,19 @@ class DocumentTextHandler(BaseHandler): "provided_revision": orig.revision, "latest_revision": current.revision }) - ndoc = doc.quickwrite('xml', data, msg) - - # return the new revision number - return { - "document": ndoc.id, - "subview": "xml", - "previous_revision": prev, - "updated_revision": ndoc.revision - } + ndoc = current.quickwrite('xml', data, msg) + + try: + # return the new revision number + return { + "document": ndoc.id, + "subview": "xml", + "previous_revision": current.revision, + "updated_revision": ndoc.revision + } + except Exception, e: + lib.rollback() + raise e except (RevisionNotFound, KeyError): return response.EntityNotFound().django_response() @@ -257,8 +298,72 @@ class MergeHandler(BaseHandler): def create(self, request, form, docid, lib): """Create a new document revision from the information provided by user""" - pass + target_rev = form.cleaned_data['target_revision'] - - - + doc = lib.document(docid) + udoc = doc.take(request.user.username) + + if target_rev == 'latest': + target_rev = udoc.revision + + if 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. + return response.EntityConflict().django_response({ + "reason": "out-of-date", + "provided": target_revision, + "latest": udoc.revision }) + + if not request.user.has_permission('explorer.pull_request.can_add'): + # User is not permitted to make a merge, right away + # So we instead create a pull request in the database + prq = PullRequest( + commiter=request.uset.username, + document=docid, + source_revision = udoc.revision, + status="N", + comment = form.cleaned_data['comment'] + ) + + prq.save() + return response.RequestAccepted() + + if form.cleanded_data['type'] == 'update': + # update is always performed from the file branch + # to the user branch + success, changed = udoc.update(request.user.username) + + if form.cleanded_data['type'] == 'share': + success, changed = udoc.share(form.cleaned_data['comment']) + + if not success: + return response.EntityConflict().django_response() + + if not changed: + return response.SuccessNoContent().django_response() + + new_udoc = udoc.latest() + + return response.SuccessAllOk().django_response({ + "name": udoc.id, + "parent_user_resivion": udoc.revision, + "parent_revision": doc.revision, + "revision": udoc.revision, + }) \ No newline at end of file diff --git a/apps/api/management/__init__.py b/apps/api/management/__init__.py new file mode 100644 index 00000000..5ff26b2a --- /dev/null +++ b/apps/api/management/__init__.py @@ -0,0 +1,4 @@ +# To change this template, choose Tools | Templates +# and open the template in the editor. + + diff --git a/apps/api/management/commands/__init__.py b/apps/api/management/commands/__init__.py new file mode 100644 index 00000000..5ff26b2a --- /dev/null +++ b/apps/api/management/commands/__init__.py @@ -0,0 +1,4 @@ +# To change this template, choose Tools | Templates +# and open the template in the editor. + + diff --git a/apps/api/management/commands/upload_document.py b/apps/api/management/commands/upload_document.py new file mode 100644 index 00000000..66ef9243 --- /dev/null +++ b/apps/api/management/commands/upload_document.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# -*- conding: utf-8 -*- +__author__="lreqc" +__date__ ="$2009-09-08 14:31:26$" + +from django.core.management.base import BaseCommand +from django.utils import simplejson as json +from django.test.client import Client +from django.core.urlresolvers import reverse + +from optparse import make_option + +class Command(BaseCommand): + + option_list = BaseCommand.option_list + ( + make_option('-u', '--user', action='store', dest='username'), + make_option('-p', '--password', action='store', dest='password'), + make_option('-d', '--dublin-core', action='store_true', dest='dc'), + ) + + def handle(self, *args, **options): + client = Client() + if not options['username'] or not options['password']: + raise CommandError("You must provide login data") + + client.login(username=options['username'], \ + password=options['password']) + + print options['username'], options['password'] + + filename = args[0] + bookname = args[1] + + print "Uploading '%s' as document '%s'" % (filename, bookname) + print "Wth DC template" if options['dc'] else "" + + print client.post( reverse("document_list_view"),\ + { + 'bookname': bookname, + 'ocr_file': open(filename), + 'generate_dc': options['dc'] } ) + diff --git a/apps/api/resources.py b/apps/api/resources.py index 7681436b..113d4b6d 100644 --- a/apps/api/resources.py +++ b/apps/api/resources.py @@ -20,6 +20,7 @@ import api.handlers.library_handlers as dh 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_merge = Resource(dh.MergeHandler, **authdata) diff --git a/apps/api/response.py b/apps/api/response.py index c1401635..a094f9c1 100644 --- a/apps/api/response.py +++ b/apps/api/response.py @@ -98,9 +98,12 @@ class EntityConflict(ResponseObject): # # Server side errors # -class NotImplemented(ResponseObject): +class InternalError(ResponseObject): def __init__(self, **kwargs): - ResponseObject.__init__(self, 501, **kwargs) + ResponseObject.__init__(self, 500, **kwargs) +class NotImplemented(ResponseObject): + def __init__(self, **kwargs): + ResponseObject.__init__(self, 501, **kwargs) \ No newline at end of file diff --git a/apps/api/tests/__init__.py b/apps/api/tests/__init__.py index acf92f5c..72b41326 100644 --- a/apps/api/tests/__init__.py +++ b/apps/api/tests/__init__.py @@ -19,6 +19,7 @@ def temprepo(name): def decorator(func): + @wraps(func) def decorated(self, *args, **kwargs): clean = False diff --git a/apps/api/urls.py b/apps/api/urls.py index 8c10158a..b36cbe30 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -41,6 +41,10 @@ urlpatterns = patterns('', document_text_resource, {'emitter_format': 'rawxml'}, name="doctext_view"), + url(urlpath(r'documents', DOC, 'html', REVISION, format=False), + document_text_resource, {'emitter_format': 'rawhtml'}, + name="dochtml_view"), + url(urlpath(r'documents', DOC, 'dc', REVISION), document_dc_resource, name="docdc_view_withformat"), diff --git a/apps/api/utils.py b/apps/api/utils.py index 0e0468a0..d8177ab0 100644 --- a/apps/api/utils.py +++ b/apps/api/utils.py @@ -18,8 +18,9 @@ class TextEmitter(Emitter): def render(self, request): return unicode(self.construct()) -Emitter.register('text', TextEmitter, 'text/plain; charset=utf-8') -Emitter.register('rawxml', TextEmitter, 'application/xml; charset=UTF-8') +Emitter.register('raw', TextEmitter, 'text/plain; charset=utf-8') +Emitter.register('rawhtml', TextEmitter, 'text/html; charset=utf-8') +Emitter.register('rawxml', TextEmitter, 'application/xml; charset=utf-8') class DjangoAuth(object): diff --git a/apps/explorer/admin.py b/apps/explorer/admin.py index 7932f4eb..b496893f 100644 --- a/apps/explorer/admin.py +++ b/apps/explorer/admin.py @@ -4,4 +4,5 @@ from django.utils.translation import ugettext_lazy as _ import explorer.models admin.site.register(explorer.models.EditorSettings) -admin.site.register(explorer.models.EditorPanel) \ No newline at end of file +admin.site.register(explorer.models.EditorPanel) +admin.site.register(explorer.models.PullRequest) \ No newline at end of file diff --git a/apps/explorer/models.py b/apps/explorer/models.py index a1cb56cf..0a3a252d 100644 --- a/apps/explorer/models.py +++ b/apps/explorer/models.py @@ -64,20 +64,30 @@ class Book(models.Model): class PullRequest(models.Model): - comitter = models.ForeignKey(User) # the user who request the pull - file = models.CharField(max_length=256) # the file to request - source_rev = models.CharField(max_length=40) # revision number of the commiter + 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 - # revision number in which the changes were merged (if any) - merged_rev = models.CharField(max_length=40, null=True) - - def __unicode__(self): - return u"Pull request from %s, source: %s %s, status: %s." % \ - (self.commiter, self.file, self.source_rev, \ - (("merged into "+self.merged_rev) if self.merged_rev else "pending") ) + # document to merge + document = models.CharField(max_length=255) + # revision to be merged into the main branch + source_revision = models.CharField(max_length=40) + + # 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 get_image_folders(): return sorted(fn for fn in os.listdir(os.path.join(settings.MEDIA_ROOT, settings.IMAGE_DIR)) if not fn.startswith('.')) diff --git a/apps/explorer/views.py b/apps/explorer/views.py index e3ca63a7..c10e33ba 100644 --- a/apps/explorer/views.py +++ b/apps/explorer/views.py @@ -348,27 +348,26 @@ def file_dc(request, path, repo): # Display the main editor view @login_required -@with_repo -def display_editor(request, path, repo): - +# @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) + #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) +# try: + # repo.with_wlock(ensure_branch_exists) - 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() - }) - except KeyError: - return direct_to_template(request, 'explorer/nofile.html', \ - extra_context = { 'fileid': path }) + 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() + }) +# except KeyError: +# return direct_to_template(request, 'explorer/nofile.html', \ +# extra_context = { 'fileid': path }) # =============== # = Panel views = diff --git a/lib/wlrepo/mercurial_backend/__init__.py b/lib/wlrepo/mercurial_backend/__init__.py index 10a4cf8a..c1d3d30f 100644 --- a/lib/wlrepo/mercurial_backend/__init__.py +++ b/lib/wlrepo/mercurial_backend/__init__.py @@ -6,8 +6,6 @@ __doc__ = "Module documentation." import wlrepo - - class MercurialRevision(wlrepo.Revision): def __init__(self, lib, changectx): @@ -22,7 +20,7 @@ class MercurialRevision(wlrepo.Revision): idx = branchname.find("$doc:") if(idx < 0): raise ValueError("Revision %s is not a valid document revision." % changectx.hex()); - self._username = branchname[0:idx] + self._username = branchname[6:idx] self._docname = branchname[idx+5:] else: raise ValueError("Revision %s is not a valid document revision." % changectx.hex()); @@ -70,8 +68,12 @@ class MercurialRevision(wlrepo.Revision): lock = self._library._lock(True) try: self._library._checkout(self._changectx.node()) - self._library._merge(other._changectx.node()) - self._library._commit(user=user, message=message) + status = self._library._merge(other._changectx.node()) + if status.is_clean(): + self._library._commit(user=user, message=message) + return (True, True) + else: + return (False, False) finally: lock.release() diff --git a/lib/wlrepo/mercurial_backend/document.py b/lib/wlrepo/mercurial_backend/document.py index 657fa6e3..6cf8a5bf 100644 --- a/lib/wlrepo/mercurial_backend/document.py +++ b/lib/wlrepo/mercurial_backend/document.py @@ -15,6 +15,14 @@ class MercurialDocument(wlrepo.Document): def quickwrite(self, entry, data, msg, user=None): user = user or self.owner + + if isinstance(data, unicode): + data = data.encode('utf-8') + + user = self._library._sanitize_string(user) + msg = self._library._sanitize_string(msg) + entry = self._library._sanitize_string(entry) + if user is None: raise ValueError("Can't determine user.") @@ -22,12 +30,13 @@ class MercurialDocument(wlrepo.Document): f = l._fileopen(r(entry), "w+") f.write(data) f.close() - l._fileadd(r(entry)) + l._fileadd(r(entry)) return self.invoke_and_commit(write, lambda d: (msg, user)) - def invoke_and_commit(self, ops, before_commit): - lock = self._library._lock() + def invoke_and_commit(self, ops, + before_commit, rollback=False): + lock = self._library.lock() try: self._library._checkout(self._revision.hgrev()) @@ -37,11 +46,15 @@ class MercurialDocument(wlrepo.Document): ops(self._library, entry_path) message, user = before_commit(self) self._library._commit(message, user) - - return self._library.document(docid=self.id, user=self.owner) + try: + return self._library.document(docid=self.id, user=user) + except Exception, e: + # rollback the last commit + self._library.rollback() + raise e finally: - lock.release() - + lock.release() + # def commit(self, message, user): # """Make a new commit.""" # self.invoke_and_commit(message, user, lambda *a: True) @@ -52,7 +65,11 @@ class MercurialDocument(wlrepo.Document): def shared(self): if self.ismain(): return self - return self._library.document(docid=self._revision.document_name()) + + return self._library.document(docid=self.id) + + def latest(self): + return self._library.document(docid=self.id, user=self.owner) def take(self, user): fullid = self._library.fulldocid(self.id, user) @@ -69,37 +86,34 @@ class MercurialDocument(wlrepo.Document): def update(self, user): """Update parts of the document.""" - lock = self.library._lock() + lock = self.library.lock() try: if self.ismain(): # main revision of the document - return True + return (True, False) if self._revision.has_children(): # can't update non-latest revision - return False + return (False, False) sv = self.shared() if not sv.ancestorof(self) and not self.parentof(sv): - self._revision.merge_with(sv._revision, user=user) + return self._revision.merge_with(sv._revision, user=user) - return True + return (False, False) finally: lock.release() def share(self, message): - lock = self.library._lock() + lock = self.library.lock() try: if self.ismain(): - return True # always shared + return (True, False) # always shared user = self._revision.user_name() - main = self.shared()._revision - local = self._revision - - no_changes = True + local = self._revision # Case 1: # * local @@ -116,8 +130,7 @@ class MercurialDocument(wlrepo.Document): if main.ancestorof(local): print "case 1" - main.merge_with(local, user=user, message=message) - no_changes = False + success, changed = main.merge_with(local, user=user, message=message) # Case 2: # # main * * local @@ -131,8 +144,7 @@ class MercurialDocument(wlrepo.Document): elif local.has_common_ancestor(main): print "case 2" if not local.parentof(main): - main.merge_with(local, user=user, message=message) - no_changes = False + success, changed = main.merge_with(local, user=user, message=message) # Case 3: # main * @@ -151,16 +163,24 @@ class MercurialDocument(wlrepo.Document): elif local.ancestorof(main): print "case 3" if not local.parentof(main): - local.merge_with(main, user=user, message='Local branch update.') - no_changes = False + success, changed = local.merge_with(main, user=user, \ + message='$AUTO$ Local branch update during share.') + else: print "case 4" - local.merge_with(main, user=user, message='Local branch update.') - local = self.shelf() - main.merge_with(local, user=user, message=message) + success, changed = local.merge_with(main, user=user, \ + message='$AUTO$ Local branch update during share.') + + if not success: + return False - print "no_changes: ", no_changes - return no_changes + if changed: + local = local.latest() + + success, changed = main.merge_with(local, user=user,\ + message=message) + + return success, changed finally: lock.release() diff --git a/lib/wlrepo/mercurial_backend/library.py b/lib/wlrepo/mercurial_backend/library.py index e9861f4c..7a33bf38 100644 --- a/lib/wlrepo/mercurial_backend/library.py +++ b/lib/wlrepo/mercurial_backend/library.py @@ -147,11 +147,11 @@ class MercurialLibrary(wlrepo.Library): # Locking # - def _lock(self, write_mode=False): + def lock(self, write_mode=False): return self._hgrepo.wlock() # no support for read/write mode yet def _transaction(self, write_mode, action): - lock = self._lock(write_mode) + lock = self.lock(write_mode) try: return action(self) finally: @@ -199,6 +199,9 @@ class MercurialLibrary(wlrepo.Library): def _changectx(self, nodeid): return self._hgrepo.changectx(nodeid) + def _rollback(self): + return self._hgrepo.rollback() + # # BASIC BRANCH routines # @@ -252,9 +255,6 @@ class MercurialLibrary(wlrepo.Library): return None if isinstance(s, unicode): - s = s.encode('utf-8') - - if ' ' in s: - raise ValueError('Whitespace is forbidden!') + s = s.encode('utf-8') return s \ No newline at end of file diff --git a/project/templates/explorer/file_list.html b/project/templates/explorer/file_list.html index bf6758f2..9628f042 100644 --- a/project/templates/explorer/file_list.html +++ b/project/templates/explorer/file_list.html @@ -81,10 +81,12 @@ $(function() { {% if perms.explorer.can_add_files %}

Dodaj nowy utwór

+
{{ bookform }}

+
{% endif %}