From 085dd288093e1fd5455cc4db9f82998f05656a14 Mon Sep 17 00:00:00 2001 From: =?utf8?q?=C5=81ukasz=20Rekucki?= Date: Sat, 26 Sep 2009 19:01:53 +0200 Subject: [PATCH] Added upload_document managment command. --- apps/api/handlers/library_handlers.py | 118 +++++++++++++++--- apps/api/management/__init__.py | 4 + apps/api/management/commands/__init__.py | 4 + .../management/commands/upload_document.py | 42 +++++++ apps/api/response.py | 7 +- apps/api/tests/__init__.py | 1 + apps/explorer/admin.py | 3 +- apps/explorer/models.py | 30 +++-- lib/wlrepo/mercurial_backend/__init__.py | 10 +- lib/wlrepo/mercurial_backend/document.py | 67 +++++----- lib/wlrepo/mercurial_backend/library.py | 7 +- 11 files changed, 225 insertions(+), 68 deletions(-) create mode 100644 apps/api/management/__init__.py create mode 100644 apps/api/management/commands/__init__.py create mode 100644 apps/api/management/commands/upload_document.py diff --git a/apps/api/handlers/library_handlers.py b/apps/api/handlers/library_handlers.py index 8862de0c..552eb6f6 100644 --- a/apps/api/handlers/library_handlers.py +++ b/apps/api/handlers/library_handlers.py @@ -14,12 +14,14 @@ from datetime import date from django.core.urlresolvers import reverse -from wlrepo import 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(\ @@ -278,8 +294,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/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/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/lib/wlrepo/mercurial_backend/__init__.py b/lib/wlrepo/mercurial_backend/__init__.py index 10a4cf8a..2815881b 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): @@ -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..c7f2f9f7 100644 --- a/lib/wlrepo/mercurial_backend/document.py +++ b/lib/wlrepo/mercurial_backend/document.py @@ -22,12 +22,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 +38,10 @@ 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) + return self._library.document(docid=self.id, user=self.owner) 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 +52,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 +73,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 +117,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 +131,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 +150,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.') - print "no_changes: ", no_changes - return no_changes + if not success: + return False + + 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..82b5263f 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 # -- 2.20.1