2 # -*- encoding: utf-8 -*-
4 __author__= "Łukasz Rekucki"
5 __date__ = "$2009-09-25 15:49:50$"
6 __doc__ = "Module documentation."
8 from piston.handler import BaseHandler, AnonymousBaseHandler
11 from datetime import date
13 from django.core.urlresolvers import reverse
14 from django.utils import simplejson as json
18 from librarian import dcparser
21 from explorer.models import PullRequest, GalleryForDocument
24 import api.forms as forms
25 import api.response as response
26 from api.utils import validate_form, hglibrary, natural_order
27 from api.models import PartCache
33 # Document List Handlers
35 class BasicLibraryHandler(AnonymousBaseHandler):
36 allowed_methods = ('GET',)
39 def read(self, request, lib):
40 """Return the list of documents."""
42 'url': reverse('document_view', args=[docid]),
43 'name': docid } for docid in lib.documents() ]
45 return {'documents' : document_list}
48 class LibraryHandler(BaseHandler):
49 allowed_methods = ('GET', 'POST')
50 anonymous = BasicLibraryHandler
53 def read(self, request, lib):
54 """Return the list of documents."""
58 for docid in lib.documents():
59 docid = docid.decode('utf-8')
61 'url': reverse('document_view', args=[docid]),
66 parts = PartCache.objects.defer('part_id')\
67 .values_list('part_id', 'document_id').distinct()
69 document_tree = dict(documents)
71 for part, docid in parts:
72 # this way, we won't display broken links
73 if not documents.has_key(part):
74 print "NOT FOUND:", part
77 parent = documents[docid]
78 child = documents[part]
80 # not top-level anymore
81 document_tree.pop(part)
82 parent['parts'].append(child)
84 for doc in documents.itervalues():
85 doc['parts'].sort(key=natural_order(lambda d: d['name']))
87 return {'documents': sorted(document_tree.itervalues(),
88 key=natural_order(lambda d: d['name']) ) }
90 @validate_form(forms.DocumentUploadForm, 'POST')
92 def create(self, request, form, lib):
93 """Create a new document."""
95 if form.cleaned_data['ocr_data']:
96 data = form.cleaned_data['ocr_data']
98 data = request.FILES['ocr_file'].read().decode('utf-8')
100 if form.cleaned_data['generate_dc']:
101 data = librarian.wrap_text(data, unicode(date.today()))
103 docid = form.cleaned_data['bookname']
109 doc = lib.document_create(docid)
110 # document created, but no content yet
113 doc = doc.quickwrite('xml', data.encode('utf-8'),
114 '$AUTO$ XML data uploaded.', user=request.user.username)
116 # rollback branch creation
118 raise LibraryException("Exception occured:" + repr(e))
120 url = reverse('document_view', args=[doc.id])
122 return response.EntityCreated().django_response(\
126 'revision': doc.revision },
130 except LibraryException, e:
131 return response.InternalError().django_response(\
132 {'exception': repr(e) })
133 except DocumentAlreadyExists:
134 # Document is already there
135 return response.EntityConflict().django_response(\
136 {"reason": "Document %s already exists." % docid})
141 class BasicDocumentHandler(AnonymousBaseHandler):
142 allowed_methods = ('GET',)
145 def read(self, request, docid, lib):
147 doc = lib.document(docid)
148 except RevisionNotFound:
153 'html_url': reverse('dochtml_view', args=[doc.id]),
154 'text_url': reverse('doctext_view', args=[doc.id]),
155 'dc_url': reverse('docdc_view', args=[doc.id]),
156 'public_revision': doc.revision,
164 class DocumentHandler(BaseHandler):
165 allowed_methods = ('GET', 'PUT')
166 anonymous = BasicDocumentHandler
169 def read(self, request, docid, lib):
170 """Read document's meta data"""
172 doc = lib.document(docid)
173 udoc = doc.take(request.user.username)
174 except RevisionNotFound, e:
175 return response.EntityNotFound().django_response({
176 'exception': type(e), 'message': e.message})
178 # is_shared = udoc.ancestorof(doc)
179 # is_uptodate = is_shared or shared.ancestorof(document)
183 'html_url': reverse('dochtml_view', args=[udoc.id]),
184 'text_url': reverse('doctext_view', args=[udoc.id]),
185 'dc_url': reverse('docdc_view', args=[udoc.id]),
186 'gallery_url': reverse('docgallery_view', args=[udoc.id]),
187 'merge_url': reverse('docmerge_view', args=[udoc.id]),
188 'user_revision': udoc.revision,
189 'user_timestamp': udoc.revision.timestamp,
190 'public_revision': doc.revision,
191 'public_timestamp': doc.revision.timestamp,
197 def update(self, request, docid, lib):
198 """Update information about the document, like display not"""
203 class DocumentHTMLHandler(BaseHandler):
204 allowed_methods = ('GET')
207 def read(self, request, docid, lib):
208 """Read document as html text"""
210 revision = request.GET.get('revision', 'latest')
212 if revision == 'latest':
213 document = lib.document(docid)
215 document = lib.document_for_rev(revision)
217 if document.id != docid:
218 return response.BadRequest().django_response({'reason': 'name-mismatch',
219 'message': 'Provided revision refers, to document "%s", but provided "%s"' % (document.id, docid) })
221 return librarian.html.transform(document.data('xml'), is_file=False)
222 except (EntryNotFound, RevisionNotFound), e:
223 return response.EntityNotFound().django_response({
224 'exception': type(e), 'message': e.message})
230 from django.core.files.storage import FileSystemStorage
232 class DocumentGalleryHandler(BaseHandler):
233 allowed_methods = ('GET')
235 def read(self, request, docid):
236 """Read meta-data about scans for gallery of this document."""
239 for assoc in GalleryForDocument.objects.filter(document=docid):
240 dirpath = os.path.join(settings.MEDIA_ROOT, assoc.subpath)
242 if not os.path.isdir(dirpath):
243 print u"[WARNING]: missing gallery %s" % dirpath
246 gallery = {'name': assoc.name, 'pages': []}
248 for file in sorted(os.listdir(dirpath), key=natural_order()):
250 name, ext = os.path.splitext(os.path.basename(file))
252 if ext.lower() not in ['.png', '.jpeg', '.jpg']:
253 print "Ignoring:", name, ext
256 url = settings.MEDIA_URL + assoc.subpath + u'/' + file.decode('utf-8');
257 gallery['pages'].append(url)
259 galleries.append(gallery)
267 XINCLUDE_REGEXP = r"""<(?:\w+:)?include\s+[^>]*?href=("|')wlrepo://(?P<link>[^\1]+?)\1\s*[^>]*?>"""
271 class DocumentTextHandler(BaseHandler):
272 allowed_methods = ('GET', 'POST')
275 def read(self, request, docid, lib):
276 """Read document as raw text"""
277 revision = request.GET.get('revision', 'latest')
279 if revision == 'latest':
280 document = lib.document(docid)
282 document = lib.document_for_rev(revision)
284 if document.id != docid:
285 return response.BadRequest().django_response({'reason': 'name-mismatch',
286 'message': 'Provided revision is not valid for this document'})
288 # TODO: some finer-grained access control
289 return document.data('xml')
290 except (EntryNotFound, RevisionNotFound), e:
291 return response.EntityNotFound().django_response({
292 'exception': type(e), 'message': e.message})
295 def create(self, request, docid, lib):
297 data = request.POST['contents']
298 revision = request.POST['revision']
300 if request.POST.has_key('message'):
301 msg = u"$USER$ " + request.POST['message']
303 msg = u"$AUTO$ XML content update."
305 current = lib.document(docid, request.user.username)
306 orig = lib.document_for_rev(revision)
309 return response.EntityConflict().django_response({
310 "reason": "out-of-date",
311 "provided_revision": orig.revision,
312 "latest_revision": current.revision })
314 # try to find any Xinclude tags
315 includes = [m.groupdict()['link'] for m in (re.finditer(\
316 XINCLUDE_REGEXP, data, flags=re.UNICODE) or []) ]
318 print "INCLUDES: ", includes
320 # TODO: provide useful routines to make this simpler
321 def xml_update_action(lib, resolve):
323 f = lib._fileopen(resolve('parts'), 'r')
324 stored_includes = json.loads(f.read())
329 if stored_includes != includes:
330 f = lib._fileopen(resolve('parts'), 'w+')
331 f.write(json.dumps(includes))
334 lib._fileadd(resolve('parts'))
336 # update the parts cache
337 PartCache.update_cache(docid, current.owner,\
338 stored_includes, includes)
340 # now that the parts are ok, write xml
341 f = lib._fileopen(resolve('xml'), 'w+')
342 f.write(data.encode('utf-8'))
346 ndoc = current.invoke_and_commit(\
347 xml_update_action, lambda d: (msg, current.owner) )
350 # return the new revision number
351 return response.SuccessAllOk().django_response({
354 "previous_revision": current.revision,
355 "revision": ndoc.revision,
356 'timestamp': ndoc.revision.timestamp,
357 "url": reverse("doctext_view", args=[ndoc.id])
360 if ndoc: lib._rollback()
362 except RevisionNotFound, e:
363 return response.EntityNotFound(mimetype="text/plain").\
364 django_response(e.message)
368 # Dublin Core handlers
370 # @requires librarian
372 class DocumentDublinCoreHandler(BaseHandler):
373 allowed_methods = ('GET', 'POST')
376 def read(self, request, docid, lib):
377 """Read document as raw text"""
379 revision = request.GET.get('revision', 'latest')
381 if revision == 'latest':
382 doc = lib.document(docid)
384 doc = lib.document_for_rev(revision)
387 if document.id != docid:
388 return response.BadRequest().django_response({'reason': 'name-mismatch',
389 'message': 'Provided revision is not valid for this document'})
391 bookinfo = dcparser.BookInfo.from_string(doc.data('xml'))
392 return bookinfo.serialize()
393 except (EntryNotFound, RevisionNotFound), e:
394 return response.EntityNotFound().django_response({
395 'exception': type(e), 'message': e.message})
398 def create(self, request, docid, lib):
400 bi_json = request.POST['contents']
401 revision = request.POST['revision']
403 if request.POST.has_key('message'):
404 msg = u"$USER$ " + request.PUT['message']
406 msg = u"$AUTO$ Dublin core update."
408 current = lib.document(docid, request.user.username)
409 orig = lib.document_for_rev(revision)
412 return response.EntityConflict().django_response({
413 "reason": "out-of-date",
414 "provided": orig.revision,
415 "latest": current.revision })
417 xmldoc = parser.WLDocument.from_string(current.data('xml'))
418 document.book_info = dcparser.BookInfo.from_json(bi_json)
421 ndoc = current.quickwrite('xml', \
422 document.serialize().encode('utf-8'),\
423 message=msg, user=request.user.username)
426 # return the new revision number
430 "previous_revision": current.revision,
431 "revision": ndoc.revision,
432 'timestamp': ndoc.revision.timestamp,
433 "url": reverse("docdc_view", args=[ndoc.id])
436 if ndoc: lib._rollback()
438 except RevisionNotFound:
439 return response.EntityNotFound().django_response()
441 class MergeHandler(BaseHandler):
442 allowed_methods = ('POST',)
444 @validate_form(forms.MergeRequestForm, 'POST')
446 def create(self, request, form, docid, lib):
447 """Create a new document revision from the information provided by user"""
449 target_rev = form.cleaned_data['target_revision']
451 doc = lib.document(docid)
452 udoc = doc.take(request.user.username)
454 if target_rev == 'latest':
455 target_rev = udoc.revision
457 if str(udoc.revision) != target_rev:
458 # user think doesn't know he has an old version
461 # Updating is teorericly ok, but we need would
462 # have to force a refresh. Sharing may be not safe,
463 # 'cause it doesn't always result in update.
465 # In other words, we can't lie about the resource's state
466 # So we should just yield and 'out-of-date' conflict
467 # and let the client ask again with updated info.
469 # NOTE: this could result in a race condition, when there
470 # are 2 instances of the same user editing the same document.
471 # Instance "A" trying to update, and instance "B" always changing
472 # the document right before "A". The anwser to this problem is
473 # for the "A" to request a merge from 'latest' and then
474 # check the parent revisions in response, if he actually
475 # merge from where he thinks he should. If not, the client SHOULD
476 # update his internal state.
477 return response.EntityConflict().django_response({
478 "reason": "out-of-date",
479 "provided": target_rev,
480 "latest": udoc.revision })
482 if not request.user.has_perm('explorer.book.can_share'):
483 # User is not permitted to make a merge, right away
484 # So we instead create a pull request in the database
486 comitter=request.user,
488 source_revision = str(udoc.revision),
490 comment = form.cleaned_data['message'] or '$AUTO$ Document shared.'
494 return response.RequestAccepted().django_response(\
495 ticket_status=prq.status, \
496 ticket_uri=reverse("pullrequest_view", args=[prq.id]) )
498 if form.cleaned_data['type'] == 'update':
499 # update is always performed from the file branch
501 success, changed = udoc.update(request.user.username)
503 if form.cleaned_data['type'] == 'share':
504 success, changed = udoc.share(form.cleaned_data['message'])
507 return response.EntityConflict().django_response({
508 'reason': 'merge-failure',
512 return response.SuccessNoContent().django_response()
514 new_udoc = udoc.latest()
516 return response.SuccessAllOk().django_response({
518 "parent_user_resivion": udoc.revision,
519 "parent_revision": doc.revision,
520 "revision": ndoc.revision,
521 'timestamp': ndoc.revision.timestamp,