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)
87 for doc in documents.itervalues():
88 doc['parts'].sort(key=natural_order(lambda d: d['name']))
90 return {'documents': sorted(document_tree.itervalues(),
91 key=natural_order(lambda d: d['name']) ) }
93 @validate_form(forms.DocumentUploadForm, 'POST')
95 def create(self, request, form, lib):
96 """Create a new document."""
98 if form.cleaned_data['ocr_data']:
99 data = form.cleaned_data['ocr_data']
101 data = request.FILES['ocr_file'].read().decode('utf-8')
103 if form.cleaned_data['generate_dc']:
104 data = librarian.wrap_text(data, unicode(date.today()))
106 docid = form.cleaned_data['bookname']
112 doc = lib.document_create(docid)
113 # document created, but no content yet
116 doc = doc.quickwrite('xml', data.encode('utf-8'),
117 '$AUTO$ XML data uploaded.', user=request.user.username)
119 # rollback branch creation
121 raise LibraryException("Exception occured:" + repr(e))
123 url = reverse('document_view', args=[doc.id])
125 return response.EntityCreated().django_response(\
129 'revision': doc.revision },
133 except LibraryException, e:
134 return response.InternalError().django_response(\
135 {'exception': repr(e) })
136 except DocumentAlreadyExists:
137 # Document is already there
138 return response.EntityConflict().django_response(\
139 {"reason": "Document %s already exists." % docid})
144 class BasicDocumentHandler(AnonymousBaseHandler):
145 allowed_methods = ('GET',)
148 def read(self, request, docid, lib):
150 doc = lib.document(docid)
151 except RevisionNotFound:
156 'html_url': reverse('dochtml_view', args=[doc.id,doc.revision]),
157 'text_url': reverse('doctext_view', args=[doc.id,doc.revision]),
158 'dc_url': reverse('docdc_view', args=[doc.id,doc.revision]),
159 'public_revision': doc.revision,
167 class DocumentHandler(BaseHandler):
168 allowed_methods = ('GET', 'PUT')
169 anonymous = BasicDocumentHandler
172 def read(self, request, docid, lib):
173 """Read document's meta data"""
175 doc = lib.document(docid)
176 udoc = doc.take(request.user.username)
177 except RevisionNotFound, e:
178 return response.EntityNotFound().django_response({
179 'exception': type(e), 'message': e.message})
181 # is_shared = udoc.ancestorof(doc)
182 # is_uptodate = is_shared or shared.ancestorof(document)
186 'html_url': reverse('dochtml_view', args=[udoc.id,udoc.revision]),
187 'text_url': reverse('doctext_view', args=[udoc.id,udoc.revision]),
188 'dc_url': reverse('docdc_view', args=[udoc.id,udoc.revision]),
189 'gallery_url': reverse('docgallery_view', args=[udoc.id]),
190 'merge_url': reverse('docmerge_view', args=[udoc.id]),
191 'user_revision': udoc.revision,
192 'user_timestamp': udoc.revision.timestamp,
193 'public_revision': doc.revision,
194 'public_timestamp': doc.revision.timestamp,
200 def update(self, request, docid, lib):
201 """Update information about the document, like display not"""
206 class DocumentHTMLHandler(BaseHandler):
207 allowed_methods = ('GET')
210 def read(self, request, docid, revision, lib):
211 """Read document as html text"""
213 if revision == 'latest':
214 document = lib.document(docid)
216 document = lib.document_for_rev(revision)
218 if document.id != docid:
219 return response.BadRequest().django_response({'reason': 'name-mismatch',
220 'message': 'Provided revision refers, to document "%s", but provided "%s"' % (document.id, docid) })
222 return librarian.html.transform(document.data('xml'), is_file=False)
223 except (EntryNotFound, RevisionNotFound), e:
224 return response.EntityNotFound().django_response({
225 'exception': type(e), 'message': e.message})
231 from django.core.files.storage import FileSystemStorage
233 class DocumentGalleryHandler(BaseHandler):
234 allowed_methods = ('GET')
236 def read(self, request, docid):
237 """Read meta-data about scans for gallery of this document."""
240 for assoc in GalleryForDocument.objects.filter(document=docid):
241 dirpath = os.path.join(settings.MEDIA_ROOT, assoc.subpath)
243 if not os.path.isdir(dirpath):
244 print u"[WARNING]: missing gallery %s" % dirpath
247 gallery = {'name': assoc.name, 'pages': []}
249 for file in sorted(os.listdir(dirpath), key=natural_order()):
251 name, ext = os.path.splitext(os.path.basename(file))
253 if ext.lower() not in ['.png', '.jpeg', '.jpg']:
254 print "Ignoring:", name, ext
257 url = settings.MEDIA_URL + assoc.subpath + u'/' + file.decode('utf-8');
258 gallery['pages'].append(url)
260 galleries.append(gallery)
268 XINCLUDE_REGEXP = r"""<(?:\w+:)?include\s+[^>]*?href=("|')wlrepo://(?P<link>[^\1]+?)\1\s*[^>]*?>"""
272 class DocumentTextHandler(BaseHandler):
273 allowed_methods = ('GET', 'PUT')
276 def read(self, request, docid, revision, lib):
277 """Read document as raw text"""
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 update(self, request, docid, revision, lib):
297 data = request.PUT['contents']
299 if request.PUT.has_key('message'):
300 msg = u"$USER$ " + request.PUT['message']
302 msg = u"$AUTO$ XML content update."
304 current = lib.document(docid, request.user.username)
305 orig = lib.document_for_rev(revision)
308 return response.EntityConflict().django_response({
309 "reason": "out-of-date",
310 "provided_revision": orig.revision,
311 "latest_revision": current.revision })
313 # try to find any Xinclude tags
314 includes = [m.groupdict()['link'] for m in (re.finditer(\
315 XINCLUDE_REGEXP, data, flags=re.UNICODE) or []) ]
317 print "INCLUDES: ", includes
319 # TODO: provide useful routines to make this simpler
320 def xml_update_action(lib, resolve):
322 f = lib._fileopen(resolve('parts'), 'r')
323 stored_includes = json.loads(f.read())
328 if stored_includes != includes:
329 f = lib._fileopen(resolve('parts'), 'w+')
330 f.write(json.dumps(includes))
333 lib._fileadd(resolve('parts'))
335 # update the parts cache
336 PartCache.update_cache(docid, current.owner,\
337 stored_includes, includes)
339 # now that the parts are ok, write xml
340 f = lib._fileopen(resolve('xml'), 'w+')
341 f.write(data.encode('utf-8'))
345 ndoc = current.invoke_and_commit(\
346 xml_update_action, lambda d: (msg, current.owner) )
349 # return the new revision number
350 return response.SuccessAllOk().django_response({
353 "previous_revision": current.revision,
354 "revision": ndoc.revision,
355 'timestamp': ndoc.revision.timestamp,
356 "url": reverse("doctext_view", args=[ndoc.id, ndoc.revision])
359 if ndoc: lib._rollback()
361 except RevisionNotFound, e:
362 return response.EntityNotFound(mimetype="text/plain").\
363 django_response(e.message)
367 # Dublin Core handlers
369 # @requires librarian
371 class DocumentDublinCoreHandler(BaseHandler):
372 allowed_methods = ('GET', 'PUT')
375 def read(self, request, docid, revision, lib):
376 """Read document as raw text"""
378 if revision == 'latest':
379 doc = lib.document(docid)
381 doc = lib.document_for_rev(revision)
384 if document.id != docid:
385 return response.BadRequest().django_response({'reason': 'name-mismatch',
386 'message': 'Provided revision is not valid for this document'})
388 bookinfo = dcparser.BookInfo.from_string(doc.data('xml'))
389 return bookinfo.serialize()
390 except (EntryNotFound, RevisionNotFound), e:
391 return response.EntityNotFound().django_response({
392 'exception': type(e), 'message': e.message})
395 def update(self, request, docid, revision, lib):
397 bi_json = request.PUT['contents']
398 if request.PUT.has_key('message'):
399 msg = u"$USER$ " + request.PUT['message']
401 msg = u"$AUTO$ Dublin core update."
403 current = lib.document(docid, request.user.username)
404 orig = lib.document_for_rev(revision)
407 return response.EntityConflict().django_response({
408 "reason": "out-of-date",
409 "provided": orig.revision,
410 "latest": current.revision })
412 xmldoc = parser.WLDocument.from_string(current.data('xml'))
413 document.book_info = dcparser.BookInfo.from_json(bi_json)
416 ndoc = current.quickwrite('xml', \
417 document.serialize().encode('utf-8'),\
418 message=msg, user=request.user.username)
421 # return the new revision number
425 "previous_revision": current.revision,
426 "revision": ndoc.revision,
427 'timestamp': ndoc.revision.timestamp,
428 "url": reverse("docdc_view", args=[ndoc.id, ndoc.revision])
431 if ndoc: lib._rollback()
433 except RevisionNotFound:
434 return response.EntityNotFound().django_response()
436 class MergeHandler(BaseHandler):
437 allowed_methods = ('POST',)
439 @validate_form(forms.MergeRequestForm, 'POST')
441 def create(self, request, form, docid, lib):
442 """Create a new document revision from the information provided by user"""
444 target_rev = form.cleaned_data['target_revision']
446 doc = lib.document(docid)
447 udoc = doc.take(request.user.username)
449 if target_rev == 'latest':
450 target_rev = udoc.revision
452 if str(udoc.revision) != target_rev:
453 # user think doesn't know he has an old version
456 # Updating is teorericly ok, but we need would
457 # have to force a refresh. Sharing may be not safe,
458 # 'cause it doesn't always result in update.
460 # In other words, we can't lie about the resource's state
461 # So we should just yield and 'out-of-date' conflict
462 # and let the client ask again with updated info.
464 # NOTE: this could result in a race condition, when there
465 # are 2 instances of the same user editing the same document.
466 # Instance "A" trying to update, and instance "B" always changing
467 # the document right before "A". The anwser to this problem is
468 # for the "A" to request a merge from 'latest' and then
469 # check the parent revisions in response, if he actually
470 # merge from where he thinks he should. If not, the client SHOULD
471 # update his internal state.
472 return response.EntityConflict().django_response({
473 "reason": "out-of-date",
474 "provided": target_rev,
475 "latest": udoc.revision })
477 if not request.user.has_perm('explorer.book.can_share'):
478 # User is not permitted to make a merge, right away
479 # So we instead create a pull request in the database
481 comitter=request.user,
483 source_revision = str(udoc.revision),
485 comment = form.cleaned_data['message'] or '$AUTO$ Document shared.'
489 return response.RequestAccepted().django_response(\
490 ticket_status=prq.status, \
491 ticket_uri=reverse("pullrequest_view", args=[prq.id]) )
493 if form.cleaned_data['type'] == 'update':
494 # update is always performed from the file branch
496 success, changed = udoc.update(request.user.username)
498 if form.cleaned_data['type'] == 'share':
499 success, changed = udoc.share(form.cleaned_data['message'])
502 return response.EntityConflict().django_response({})
505 return response.SuccessNoContent().django_response()
507 new_udoc = udoc.latest()
509 return response.SuccessAllOk().django_response({
511 "parent_user_resivion": udoc.revision,
512 "parent_revision": doc.revision,
513 "revision": ndoc.revision,
514 'timestamp': ndoc.revision.timestamp,