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
#
"""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')
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(\
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,
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,
}
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
"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()
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
--- /dev/null
+# To change this template, choose Tools | Templates
+# and open the template in the editor.
+
+
--- /dev/null
+# To change this template, choose Tools | Templates
+# and open the template in the editor.
+
+
--- /dev/null
+#!/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'] } )
+
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)
#
# 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
def decorator(func):
+
@wraps(func)
def decorated(self, *args, **kwargs):
clean = False
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"),
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):
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
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('.'))
# 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 =
import wlrepo
-
-
class MercurialRevision(wlrepo.Revision):
def __init__(self, lib, changectx):
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());
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()
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.")
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())
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)
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)
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
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
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 *
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()
# 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:
def _changectx(self, nodeid):
return self._hgrepo.changectx(nodeid)
+ def _rollback(self):
+ return self._hgrepo.rollback()
+
#
# BASIC BRANCH routines
#
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
{% if perms.explorer.can_add_files %}
<div class="upload-file-widget">
<h2>Dodaj nowy utwór</h2>
+
<form action="/api/documents" method="POST" enctype="multipart/form-data">
{{ bookform }}
<p><button type="submit">Dodaj książkę</button></p>
</form>
+
</div>
{% endif %}