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)
92 for doc in documents.itervalues():
93 doc['parts'].sort(key=natural_order(lambda d: d['name']))
95 return {'documents': sorted(document_tree.itervalues(),
96 key=natural_order(lambda d: d['name']) ) }
98 @validate_form(forms.DocumentUploadForm, 'POST')
100 def create(self, request, form, lib):
101 """Create a new document."""
103 if form.cleaned_data['ocr_data']:
104 data = form.cleaned_data['ocr_data']
106 data = request.FILES['ocr_file'].read().decode('utf-8')
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)
124 # rollback branch creation
126 raise LibraryException("Exception occured:" + repr(e))
128 url = reverse('document_view', args=[doc.id])
130 return response.EntityCreated().django_response(\
134 'revision': doc.revision },
138 except LibraryException, e:
139 return response.InternalError().django_response(\
140 {'exception': repr(e) })
141 except DocumentAlreadyExists:
142 # Document is already there
143 return response.EntityConflict().django_response(\
144 {"reason": "Document %s already exists." % docid})
149 class BasicDocumentHandler(AnonymousBaseHandler):
150 allowed_methods = ('GET',)
153 def read(self, request, docid, lib):
155 doc = lib.document(docid)
156 except RevisionNotFound:
161 'html_url': reverse('dochtml_view', args=[doc.id]),
162 'text_url': reverse('doctext_view', args=[doc.id]),
163 'dc_url': reverse('docdc_view', args=[doc.id]),
164 'public_revision': doc.revision,
172 class DocumentHandler(BaseHandler):
173 allowed_methods = ('GET', 'PUT')
174 anonymous = BasicDocumentHandler
177 def read(self, request, docid, lib):
178 """Read document's meta data"""
179 log.info("Read %s", docid)
181 doc = lib.document(docid)
182 udoc = doc.take(request.user.username)
183 except RevisionNotFound, e:
184 return response.EntityNotFound().django_response({
185 'exception': type(e), 'message': e.message})
187 # is_shared = udoc.ancestorof(doc)
188 # is_uptodate = is_shared or shared.ancestorof(document)
192 'html_url': reverse('dochtml_view', args=[udoc.id]),
193 'text_url': reverse('doctext_view', args=[udoc.id]),
194 'dc_url': reverse('docdc_view', args=[udoc.id]),
195 'gallery_url': reverse('docgallery_view', args=[udoc.id]),
196 'merge_url': reverse('docmerge_view', args=[udoc.id]),
197 'user_revision': udoc.revision,
198 'user_timestamp': udoc.revision.timestamp,
199 'public_revision': doc.revision,
200 'public_timestamp': doc.revision.timestamp,
206 def update(self, request, docid, lib):
207 """Update information about the document, like display not"""
212 class DocumentHTMLHandler(BaseHandler):
213 allowed_methods = ('GET')
216 def read(self, request, docid, lib):
217 """Read document as html text"""
219 revision = request.GET.get('revision', 'latest')
221 if revision == 'latest':
222 document = lib.document(docid)
224 document = lib.document_for_rev(revision)
226 if document.id != docid:
227 return response.BadRequest().django_response({'reason': 'name-mismatch',
228 'message': 'Provided revision refers, to document "%s", but provided "%s"' % (document.id, docid) })
230 return librarian.html.transform(document.data('xml'), is_file=False)
231 except (EntryNotFound, RevisionNotFound), e:
232 return response.EntityNotFound().django_response({
233 'exception': type(e), 'message': e.message})
239 from django.core.files.storage import FileSystemStorage
241 class DocumentGalleryHandler(BaseHandler):
242 allowed_methods = ('GET')
244 def read(self, request, docid):
245 """Read meta-data about scans for gallery of this document."""
248 for assoc in GalleryForDocument.objects.filter(document=docid):
249 dirpath = os.path.join(settings.MEDIA_ROOT, assoc.subpath)
251 if not os.path.isdir(dirpath):
252 log.info(u"[WARNING]: missing gallery %s", dirpath)
255 gallery = {'name': assoc.name, 'pages': []}
257 for file in sorted(os.listdir(dirpath), key=natural_order()):
259 name, ext = os.path.splitext(os.path.basename(file))
261 if ext.lower() not in ['.png', '.jpeg', '.jpg']:
262 log.info("Ignoring: %s %s", name, ext)
265 url = settings.MEDIA_URL + assoc.subpath + u'/' + file.decode('utf-8');
266 gallery['pages'].append(url)
268 galleries.append(gallery)
276 XINCLUDE_REGEXP = r"""<(?:\w+:)?include\s+[^>]*?href=("|')wlrepo://(?P<link>[^\1]+?)\1\s*[^>]*?>"""
280 class DocumentTextHandler(BaseHandler):
281 allowed_methods = ('GET', 'POST')
284 def read(self, request, docid, lib):
285 """Read document as raw text"""
286 revision = request.GET.get('revision', 'latest')
288 if revision == 'latest':
289 document = lib.document(docid)
291 document = lib.document_for_rev(revision)
293 if document.id != docid:
294 return response.BadRequest().django_response({'reason': 'name-mismatch',
295 'message': 'Provided revision is not valid for this document'})
297 # TODO: some finer-grained access control
298 return document.data('xml')
299 except (EntryNotFound, RevisionNotFound), e:
300 return response.EntityNotFound().django_response({
301 'exception': type(e), 'message': e.message})
304 def create(self, request, docid, lib):
306 data = request.POST['contents']
307 revision = request.POST['revision']
309 if request.POST.has_key('message'):
310 msg = u"$USER$ " + request.POST['message']
312 msg = u"$AUTO$ XML content update."
314 current = lib.document(docid, request.user.username)
315 orig = lib.document_for_rev(revision)
318 return response.EntityConflict().django_response({
319 "reason": "out-of-date",
320 "provided_revision": orig.revision,
321 "latest_revision": current.revision })
323 # try to find any Xinclude tags
324 includes = [m.groupdict()['link'] for m in (re.finditer(\
325 XINCLUDE_REGEXP, data, flags=re.UNICODE) or []) ]
327 log.info("INCLUDES: %s", includes)
329 # TODO: provide useful routines to make this simpler
330 def xml_update_action(lib, resolve):
332 f = lib._fileopen(resolve('parts'), 'r')
333 stored_includes = json.loads(f.read())
338 if stored_includes != includes:
339 f = lib._fileopen(resolve('parts'), 'w+')
340 f.write(json.dumps(includes))
343 lib._fileadd(resolve('parts'))
345 # update the parts cache
346 PartCache.update_cache(docid, current.owner,\
347 stored_includes, includes)
349 # now that the parts are ok, write xml
350 f = lib._fileopen(resolve('xml'), 'w+')
351 f.write(data.encode('utf-8'))
355 ndoc = current.invoke_and_commit(\
356 xml_update_action, lambda d: (msg, current.owner) )
359 # return the new revision number
360 return response.SuccessAllOk().django_response({
363 "previous_revision": current.revision,
364 "revision": ndoc.revision,
365 'timestamp': ndoc.revision.timestamp,
366 "url": reverse("doctext_view", args=[ndoc.id])
369 if ndoc: lib._rollback()
371 except RevisionNotFound, e:
372 return response.EntityNotFound(mimetype="text/plain").\
373 django_response(e.message)
377 # Dublin Core handlers
379 # @requires librarian
381 class DocumentDublinCoreHandler(BaseHandler):
382 allowed_methods = ('GET', 'POST')
385 def read(self, request, docid, lib):
386 """Read document as raw text"""
388 revision = request.GET.get('revision', 'latest')
390 if revision == 'latest':
391 doc = lib.document(docid)
393 doc = lib.document_for_rev(revision)
396 if document.id != docid:
397 return response.BadRequest().django_response({'reason': 'name-mismatch',
398 'message': 'Provided revision is not valid for this document'})
400 bookinfo = dcparser.BookInfo.from_string(doc.data('xml'))
401 return bookinfo.serialize()
402 except (EntryNotFound, RevisionNotFound), e:
403 return response.EntityNotFound().django_response({
404 'exception': type(e), 'message': e.message})
407 def create(self, request, docid, lib):
409 bi_json = request.POST['contents']
410 revision = request.POST['revision']
412 if request.POST.has_key('message'):
413 msg = u"$USER$ " + request.PUT['message']
415 msg = u"$AUTO$ Dublin core update."
417 current = lib.document(docid, request.user.username)
418 orig = lib.document_for_rev(revision)
421 return response.EntityConflict().django_response({
422 "reason": "out-of-date",
423 "provided": orig.revision,
424 "latest": current.revision })
426 xmldoc = parser.WLDocument.from_string(current.data('xml'))
427 document.book_info = dcparser.BookInfo.from_json(bi_json)
430 ndoc = current.quickwrite('xml', \
431 document.serialize().encode('utf-8'),\
432 message=msg, user=request.user.username)
435 # return the new revision number
439 "previous_revision": current.revision,
440 "revision": ndoc.revision,
441 'timestamp': ndoc.revision.timestamp,
442 "url": reverse("docdc_view", args=[ndoc.id])
445 if ndoc: lib._rollback()
447 except RevisionNotFound:
448 return response.EntityNotFound().django_response()
450 class MergeHandler(BaseHandler):
451 allowed_methods = ('POST',)
453 @validate_form(forms.MergeRequestForm, 'POST')
455 def create(self, request, form, docid, lib):
456 """Create a new document revision from the information provided by user"""
458 target_rev = form.cleaned_data['target_revision']
460 doc = lib.document(docid)
461 udoc = doc.take(request.user.username)
463 if target_rev == 'latest':
464 target_rev = udoc.revision
466 if str(udoc.revision) != target_rev:
467 # user think doesn't know he has an old version
470 # Updating is teorericly ok, but we need would
471 # have to force a refresh. Sharing may be not safe,
472 # 'cause it doesn't always result in update.
474 # In other words, we can't lie about the resource's state
475 # So we should just yield and 'out-of-date' conflict
476 # and let the client ask again with updated info.
478 # NOTE: this could result in a race condition, when there
479 # are 2 instances of the same user editing the same document.
480 # Instance "A" trying to update, and instance "B" always changing
481 # the document right before "A". The anwser to this problem is
482 # for the "A" to request a merge from 'latest' and then
483 # check the parent revisions in response, if he actually
484 # merge from where he thinks he should. If not, the client SHOULD
485 # update his internal state.
486 return response.EntityConflict().django_response({
487 "reason": "out-of-date",
488 "provided": target_rev,
489 "latest": udoc.revision })
491 if not request.user.has_perm('explorer.book.can_share'):
492 # User is not permitted to make a merge, right away
493 # So we instead create a pull request in the database
495 comitter=request.user,
497 source_revision = str(udoc.revision),
499 comment = form.cleaned_data['message'] or '$AUTO$ Document shared.'
503 return response.RequestAccepted().django_response(\
504 ticket_status=prq.status, \
505 ticket_uri=reverse("pullrequest_view", args=[prq.id]) )
507 if form.cleaned_data['type'] == 'update':
508 # update is always performed from the file branch
510 success, changed = udoc.update(request.user.username)
512 if form.cleaned_data['type'] == 'share':
513 success, changed = udoc.share(form.cleaned_data['message'])
516 return response.EntityConflict().django_response({
517 'reason': 'merge-failure',
521 return response.SuccessNoContent().django_response()
523 new_udoc = udoc.latest()
525 return response.SuccessAllOk().django_response({
527 "parent_user_resivion": udoc.revision,
528 "parent_revision": doc.revision,
529 "revision": ndoc.revision,
530 'timestamp': ndoc.revision.timestamp,