1 # -*- encoding: utf-8 -*-
5 __author__= "Ćukasz Rekucki"
6 __date__ = "$2009-09-25 15:49:50$"
7 __doc__ = "Module documentation."
9 from piston.handler import BaseHandler, AnonymousBaseHandler
12 from datetime import date
14 from django.core.urlresolvers import reverse
15 from django.utils import simplejson as json
19 from librarian import dcparser
22 from explorer.models import PullRequest, GalleryForDocument
25 import api.forms as forms
26 import api.response as response
27 from api.utils import validate_form, hglibrary, natural_order
28 from api.models import PartCache
34 log = logging.getLogger('platforma.api')
38 # Document List Handlers
40 class BasicLibraryHandler(AnonymousBaseHandler):
41 allowed_methods = ('GET',)
44 def read(self, request, lib):
45 """Return the list of documents."""
47 'url': reverse('document_view', args=[docid]),
48 'name': docid } for docid in lib.documents() ]
50 return {'documents' : document_list}
53 class LibraryHandler(BaseHandler):
54 allowed_methods = ('GET', 'POST')
55 anonymous = BasicLibraryHandler
58 def read(self, request, lib):
59 """Return the list of documents."""
63 for docid in lib.documents():
64 docid = docid.decode('utf-8')
66 'url': reverse('document_view', args=[docid]),
71 parts = PartCache.objects.defer('part_id')\
72 .values_list('part_id', 'document_id').distinct()
74 document_tree = dict(documents)
76 for part, docid in parts:
77 # this way, we won't display broken links
78 if not documents.has_key(part):
79 log.info("NOT FOUND: %s", part)
82 parent = documents[docid]
83 child = documents[part]
85 # not top-level anymore
86 document_tree.pop(part)
87 parent['parts'].append(child)
89 for doc in documents.itervalues():
90 doc['parts'].sort(key=natural_order(lambda d: d['name']))
92 return {'documents': sorted(document_tree.itervalues(),
93 key=natural_order(lambda d: d['name']) ) }
95 @validate_form(forms.DocumentUploadForm, 'POST')
97 def create(self, request, form, lib):
98 """Create a new document."""
100 if form.cleaned_data['ocr_data']:
101 data = form.cleaned_data['ocr_data']
103 data = request.FILES['ocr_file'].read().decode('utf-8')
106 return response.BadRequest().django_response('You must pass ocr_data or ocr_file.')
108 if form.cleaned_data['generate_dc']:
109 data = librarian.wrap_text(data, unicode(date.today()))
111 docid = form.cleaned_data['bookname']
116 log.info("DOCID %s", docid)
117 doc = lib.document_create(docid)
118 # document created, but no content yet
121 doc = doc.quickwrite('xml', data.encode('utf-8'),
122 '$AUTO$ XML data uploaded.', user=request.user.username)
125 # rollback branch creation
127 raise LibraryException(traceback.format_exc())
129 url = reverse('document_view', args=[doc.id])
131 return response.EntityCreated().django_response(\
135 'revision': doc.revision },
139 except LibraryException, e:
141 return response.InternalError().django_response(\
142 {'exception': traceback.format_exc()} )
143 except DocumentAlreadyExists:
144 # Document is already there
145 return response.EntityConflict().django_response(\
146 {"reason": "Document %s already exists." % docid})
151 class BasicDocumentHandler(AnonymousBaseHandler):
152 allowed_methods = ('GET',)
155 def read(self, request, docid, lib):
157 doc = lib.document(docid)
158 except RevisionNotFound:
163 'html_url': reverse('dochtml_view', args=[doc.id]),
164 'text_url': reverse('doctext_view', args=[doc.id]),
165 'dc_url': reverse('docdc_view', args=[doc.id]),
166 'public_revision': doc.revision,
174 class DocumentHandler(BaseHandler):
175 allowed_methods = ('GET', 'PUT')
176 anonymous = BasicDocumentHandler
179 def read(self, request, docid, lib):
180 """Read document's meta data"""
181 log.info("Read %s", docid)
183 doc = lib.document(docid)
184 udoc = doc.take(request.user.username)
185 except RevisionNotFound, e:
186 return response.EntityNotFound().django_response({
187 'exception': type(e), 'message': e.message})
189 # is_shared = udoc.ancestorof(doc)
190 # is_uptodate = is_shared or shared.ancestorof(document)
194 'html_url': reverse('dochtml_view', args=[udoc.id]),
195 'text_url': reverse('doctext_view', args=[udoc.id]),
196 'dc_url': reverse('docdc_view', args=[udoc.id]),
197 'gallery_url': reverse('docgallery_view', args=[udoc.id]),
198 'merge_url': reverse('docmerge_view', args=[udoc.id]),
199 'user_revision': udoc.revision,
200 'user_timestamp': udoc.revision.timestamp,
201 'public_revision': doc.revision,
202 'public_timestamp': doc.revision.timestamp,
208 def update(self, request, docid, lib):
209 """Update information about the document, like display not"""
214 class DocumentHTMLHandler(BaseHandler):
215 allowed_methods = ('GET')
218 def read(self, request, docid, lib):
219 """Read document as html text"""
221 revision = request.GET.get('revision', 'latest')
223 if revision == 'latest':
224 document = lib.document(docid)
226 document = lib.document_for_rev(revision)
228 if document.id != docid:
229 return response.BadRequest().django_response({'reason': 'name-mismatch',
230 'message': 'Provided revision refers, to document "%s", but provided "%s"' % (document.id, docid) })
232 return librarian.html.transform(document.data('xml'), is_file=False, parse_dublincore=False)
233 except (EntryNotFound, RevisionNotFound), e:
234 return response.EntityNotFound().django_response({
235 'exception': type(e), 'message': e.message})
242 class DocumentGalleryHandler(BaseHandler):
243 allowed_methods = ('GET')
245 def read(self, request, docid):
246 """Read meta-data about scans for gallery of this document."""
249 for assoc in GalleryForDocument.objects.filter(document=docid):
250 dirpath = os.path.join(settings.MEDIA_ROOT, assoc.subpath)
252 if not os.path.isdir(dirpath):
253 log.info(u"[WARNING]: missing gallery %s", dirpath)
256 gallery = {'name': assoc.name, 'pages': []}
258 for file in sorted(os.listdir(dirpath)):
260 name, ext = os.path.splitext(os.path.basename(file))
262 if ext.lower() not in ['.png', '.jpeg', '.jpg']:
263 log.info("Ignoring: %s %s", name, ext)
266 url = settings.MEDIA_URL + assoc.subpath + u'/' + file.decode('utf-8');
267 gallery['pages'].append(url)
269 galleries.append(gallery)
277 XINCLUDE_REGEXP = r"""<(?:\w+:)?include\s+[^>]*?href=("|')wlrepo://(?P<link>[^\1]+?)\1\s*[^>]*?>"""
281 class DocumentTextHandler(BaseHandler):
282 allowed_methods = ('GET', 'POST')
285 def read(self, request, docid, lib):
286 """Read document as raw text"""
287 revision = request.GET.get('revision', 'latest')
289 if revision == 'latest':
290 document = lib.document(docid)
292 document = lib.document_for_rev(revision)
294 if document.id != docid:
295 return response.BadRequest().django_response({'reason': 'name-mismatch',
296 'message': 'Provided revision is not valid for this document'})
298 # TODO: some finer-grained access control
299 return document.data('xml')
300 except (EntryNotFound, RevisionNotFound), e:
301 return response.EntityNotFound().django_response({
302 'exception': type(e), 'message': e.message})
305 def create(self, request, docid, lib):
307 data = request.POST['contents']
308 revision = request.POST['revision']
310 if request.POST.has_key('message'):
311 msg = u"$USER$ " + request.POST['message']
313 msg = u"$AUTO$ XML content update."
315 current = lib.document(docid, request.user.username)
316 orig = lib.document_for_rev(revision)
319 return response.EntityConflict().django_response({
320 "reason": "out-of-date",
321 "provided_revision": orig.revision,
322 "latest_revision": current.revision })
324 # try to find any Xinclude tags
325 includes = [m.groupdict()['link'] for m in (re.finditer(\
326 XINCLUDE_REGEXP, data, flags=re.UNICODE) or []) ]
328 log.info("INCLUDES: %s", includes)
330 # TODO: provide useful routines to make this simpler
331 def xml_update_action(lib, resolve):
333 f = lib._fileopen(resolve('parts'), 'r')
334 stored_includes = json.loads(f.read())
339 if stored_includes != includes:
340 f = lib._fileopen(resolve('parts'), 'w+')
341 f.write(json.dumps(includes))
344 lib._fileadd(resolve('parts'))
346 # update the parts cache
347 PartCache.update_cache(docid, current.owner,\
348 stored_includes, includes)
350 # now that the parts are ok, write xml
351 f = lib._fileopen(resolve('xml'), 'w+')
352 f.write(data.encode('utf-8'))
356 ndoc = current.invoke_and_commit(\
357 xml_update_action, lambda d: (msg, current.owner) )
360 # return the new revision number
361 return response.SuccessAllOk().django_response({
364 "previous_revision": current.revision,
365 "revision": ndoc.revision,
366 'timestamp': ndoc.revision.timestamp,
367 "url": reverse("doctext_view", args=[ndoc.id])
370 if ndoc: lib._rollback()
372 except RevisionNotFound, e:
373 return response.EntityNotFound(mimetype="text/plain").\
374 django_response(e.message)
378 # Dublin Core handlers
380 # @requires librarian
382 class DocumentDublinCoreHandler(BaseHandler):
383 allowed_methods = ('GET', 'POST')
386 def read(self, request, docid, lib):
387 """Read document as raw text"""
389 revision = request.GET.get('revision', 'latest')
391 if revision == 'latest':
392 doc = lib.document(docid)
394 doc = lib.document_for_rev(revision)
397 if document.id != docid:
398 return response.BadRequest().django_response({'reason': 'name-mismatch',
399 'message': 'Provided revision is not valid for this document'})
401 bookinfo = dcparser.BookInfo.from_string(doc.data('xml'))
402 return bookinfo.serialize()
403 except (EntryNotFound, RevisionNotFound), e:
404 return response.EntityNotFound().django_response({
405 'exception': type(e), 'message': e.message})
408 def create(self, request, docid, lib):
410 bi_json = request.POST['contents']
411 revision = request.POST['revision']
413 if request.POST.has_key('message'):
414 msg = u"$USER$ " + request.PUT['message']
416 msg = u"$AUTO$ Dublin core update."
418 current = lib.document(docid, request.user.username)
419 orig = lib.document_for_rev(revision)
422 return response.EntityConflict().django_response({
423 "reason": "out-of-date",
424 "provided": orig.revision,
425 "latest": current.revision })
427 xmldoc = parser.WLDocument.from_string(current.data('xml'))
428 document.book_info = dcparser.BookInfo.from_json(bi_json)
431 ndoc = current.quickwrite('xml', \
432 document.serialize().encode('utf-8'),\
433 message=msg, user=request.user.username)
436 # return the new revision number
440 "previous_revision": current.revision,
441 "revision": ndoc.revision,
442 'timestamp': ndoc.revision.timestamp,
443 "url": reverse("docdc_view", args=[ndoc.id])
446 if ndoc: lib._rollback()
448 except RevisionNotFound:
449 return response.EntityNotFound().django_response()
451 class MergeHandler(BaseHandler):
452 allowed_methods = ('POST',)
454 @validate_form(forms.MergeRequestForm, 'POST')
456 def create(self, request, form, docid, lib):
457 """Create a new document revision from the information provided by user"""
459 target_rev = form.cleaned_data['target_revision']
461 doc = lib.document(docid)
462 udoc = doc.take(request.user.username)
464 if target_rev == 'latest':
465 target_rev = udoc.revision
467 if str(udoc.revision) != target_rev:
468 # user think doesn't know he has an old version
471 # Updating is teorericly ok, but we need would
472 # have to force a refresh. Sharing may be not safe,
473 # 'cause it doesn't always result in update.
475 # In other words, we can't lie about the resource's state
476 # So we should just yield and 'out-of-date' conflict
477 # and let the client ask again with updated info.
479 # NOTE: this could result in a race condition, when there
480 # are 2 instances of the same user editing the same document.
481 # Instance "A" trying to update, and instance "B" always changing
482 # the document right before "A". The anwser to this problem is
483 # for the "A" to request a merge from 'latest' and then
484 # check the parent revisions in response, if he actually
485 # merge from where he thinks he should. If not, the client SHOULD
486 # update his internal state.
487 return response.EntityConflict().django_response({
488 "reason": "out-of-date",
489 "provided": target_rev,
490 "latest": udoc.revision })
492 if not request.user.has_perm('explorer.book.can_share'):
493 # User is not permitted to make a merge, right away
494 # So we instead create a pull request in the database
496 comitter=request.user,
498 source_revision = str(udoc.revision),
500 comment = form.cleaned_data['message'] or '$AUTO$ Document shared.'
504 return response.RequestAccepted().django_response(\
505 ticket_status=prq.status, \
506 ticket_uri=reverse("pullrequest_view", args=[prq.id]) )
508 if form.cleaned_data['type'] == 'update':
509 # update is always performed from the file branch
511 success, changed = udoc.update(request.user.username)
513 if form.cleaned_data['type'] == 'share':
514 success, changed = udoc.share(form.cleaned_data['message'])
517 return response.EntityConflict().django_response({
518 'reason': 'merge-failure',
522 return response.SuccessNoContent().django_response()
524 nudoc = udoc.latest()
526 return response.SuccessAllOk().django_response({
528 "parent_user_resivion": udoc.revision,
529 "parent_revision": doc.revision,
530 "revision": nudoc.revision,
531 'timestamp': nudoc.revision.timestamp,