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]),
157 'text_url': reverse('doctext_view', args=[doc.id]),
158 'dc_url': reverse('docdc_view', args=[doc.id]),
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]),
187 'text_url': reverse('doctext_view', args=[udoc.id]),
188 'dc_url': reverse('docdc_view', args=[udoc.id]),
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, lib):
211 """Read document as html text"""
213 revision = request.GET.get('revision', 'latest')
215 if revision == 'latest':
216 document = lib.document(docid)
218 document = lib.document_for_rev(revision)
220 if document.id != docid:
221 return response.BadRequest().django_response({'reason': 'name-mismatch',
222 'message': 'Provided revision refers, to document "%s", but provided "%s"' % (document.id, docid) })
224 return librarian.html.transform(document.data('xml'), is_file=False)
225 except (EntryNotFound, RevisionNotFound), e:
226 return response.EntityNotFound().django_response({
227 'exception': type(e), 'message': e.message})
233 from django.core.files.storage import FileSystemStorage
235 class DocumentGalleryHandler(BaseHandler):
236 allowed_methods = ('GET')
238 def read(self, request, docid):
239 """Read meta-data about scans for gallery of this document."""
242 for assoc in GalleryForDocument.objects.filter(document=docid):
243 dirpath = os.path.join(settings.MEDIA_ROOT, assoc.subpath)
245 if not os.path.isdir(dirpath):
246 print u"[WARNING]: missing gallery %s" % dirpath
249 gallery = {'name': assoc.name, 'pages': []}
251 for file in sorted(os.listdir(dirpath), key=natural_order()):
253 name, ext = os.path.splitext(os.path.basename(file))
255 if ext.lower() not in ['.png', '.jpeg', '.jpg']:
256 print "Ignoring:", name, ext
259 url = settings.MEDIA_URL + assoc.subpath + u'/' + file.decode('utf-8');
260 gallery['pages'].append(url)
262 galleries.append(gallery)
270 XINCLUDE_REGEXP = r"""<(?:\w+:)?include\s+[^>]*?href=("|')wlrepo://(?P<link>[^\1]+?)\1\s*[^>]*?>"""
274 class DocumentTextHandler(BaseHandler):
275 allowed_methods = ('GET', 'POST')
278 def read(self, request, docid, lib):
279 """Read document as raw text"""
280 revision = request.GET.get('revision', 'latest')
282 if revision == 'latest':
283 document = lib.document(docid)
285 document = lib.document_for_rev(revision)
287 if document.id != docid:
288 return response.BadRequest().django_response({'reason': 'name-mismatch',
289 'message': 'Provided revision is not valid for this document'})
291 # TODO: some finer-grained access control
292 return document.data('xml')
293 except (EntryNotFound, RevisionNotFound), e:
294 return response.EntityNotFound().django_response({
295 'exception': type(e), 'message': e.message})
298 def create(self, request, docid, lib):
300 data = request.POST['contents']
301 revision = request.POST['revision']
303 if request.POST.has_key('message'):
304 msg = u"$USER$ " + request.POST['message']
306 msg = u"$AUTO$ XML content update."
308 current = lib.document(docid, request.user.username)
309 orig = lib.document_for_rev(revision)
312 return response.EntityConflict().django_response({
313 "reason": "out-of-date",
314 "provided_revision": orig.revision,
315 "latest_revision": current.revision })
317 # try to find any Xinclude tags
318 includes = [m.groupdict()['link'] for m in (re.finditer(\
319 XINCLUDE_REGEXP, data, flags=re.UNICODE) or []) ]
321 print "INCLUDES: ", includes
323 # TODO: provide useful routines to make this simpler
324 def xml_update_action(lib, resolve):
326 f = lib._fileopen(resolve('parts'), 'r')
327 stored_includes = json.loads(f.read())
332 if stored_includes != includes:
333 f = lib._fileopen(resolve('parts'), 'w+')
334 f.write(json.dumps(includes))
337 lib._fileadd(resolve('parts'))
339 # update the parts cache
340 PartCache.update_cache(docid, current.owner,\
341 stored_includes, includes)
343 # now that the parts are ok, write xml
344 f = lib._fileopen(resolve('xml'), 'w+')
345 f.write(data.encode('utf-8'))
349 ndoc = current.invoke_and_commit(\
350 xml_update_action, lambda d: (msg, current.owner) )
353 # return the new revision number
354 return response.SuccessAllOk().django_response({
357 "previous_revision": current.revision,
358 "revision": ndoc.revision,
359 'timestamp': ndoc.revision.timestamp,
360 "url": reverse("doctext_view", args=[ndoc.id])
363 if ndoc: lib._rollback()
365 except RevisionNotFound, e:
366 return response.EntityNotFound(mimetype="text/plain").\
367 django_response(e.message)
371 # Dublin Core handlers
373 # @requires librarian
375 class DocumentDublinCoreHandler(BaseHandler):
376 allowed_methods = ('GET', 'POST')
379 def read(self, request, docid, lib):
380 """Read document as raw text"""
382 revision = request.GET.get('revision', 'latest')
384 if revision == 'latest':
385 doc = lib.document(docid)
387 doc = lib.document_for_rev(revision)
390 if document.id != docid:
391 return response.BadRequest().django_response({'reason': 'name-mismatch',
392 'message': 'Provided revision is not valid for this document'})
394 bookinfo = dcparser.BookInfo.from_string(doc.data('xml'))
395 return bookinfo.serialize()
396 except (EntryNotFound, RevisionNotFound), e:
397 return response.EntityNotFound().django_response({
398 'exception': type(e), 'message': e.message})
401 def create(self, request, docid, lib):
403 bi_json = request.POST['contents']
404 revision = request.POST['revision']
406 if request.POST.has_key('message'):
407 msg = u"$USER$ " + request.PUT['message']
409 msg = u"$AUTO$ Dublin core update."
411 current = lib.document(docid, request.user.username)
412 orig = lib.document_for_rev(revision)
415 return response.EntityConflict().django_response({
416 "reason": "out-of-date",
417 "provided": orig.revision,
418 "latest": current.revision })
420 xmldoc = parser.WLDocument.from_string(current.data('xml'))
421 document.book_info = dcparser.BookInfo.from_json(bi_json)
424 ndoc = current.quickwrite('xml', \
425 document.serialize().encode('utf-8'),\
426 message=msg, user=request.user.username)
429 # return the new revision number
433 "previous_revision": current.revision,
434 "revision": ndoc.revision,
435 'timestamp': ndoc.revision.timestamp,
436 "url": reverse("docdc_view", args=[ndoc.id])
439 if ndoc: lib._rollback()
441 except RevisionNotFound:
442 return response.EntityNotFound().django_response()
444 class MergeHandler(BaseHandler):
445 allowed_methods = ('POST',)
447 @validate_form(forms.MergeRequestForm, 'POST')
449 def create(self, request, form, docid, lib):
450 """Create a new document revision from the information provided by user"""
452 target_rev = form.cleaned_data['target_revision']
454 doc = lib.document(docid)
455 udoc = doc.take(request.user.username)
457 if target_rev == 'latest':
458 target_rev = udoc.revision
460 if str(udoc.revision) != target_rev:
461 # user think doesn't know he has an old version
464 # Updating is teorericly ok, but we need would
465 # have to force a refresh. Sharing may be not safe,
466 # 'cause it doesn't always result in update.
468 # In other words, we can't lie about the resource's state
469 # So we should just yield and 'out-of-date' conflict
470 # and let the client ask again with updated info.
472 # NOTE: this could result in a race condition, when there
473 # are 2 instances of the same user editing the same document.
474 # Instance "A" trying to update, and instance "B" always changing
475 # the document right before "A". The anwser to this problem is
476 # for the "A" to request a merge from 'latest' and then
477 # check the parent revisions in response, if he actually
478 # merge from where he thinks he should. If not, the client SHOULD
479 # update his internal state.
480 return response.EntityConflict().django_response({
481 "reason": "out-of-date",
482 "provided": target_rev,
483 "latest": udoc.revision })
485 if not request.user.has_perm('explorer.book.can_share'):
486 # User is not permitted to make a merge, right away
487 # So we instead create a pull request in the database
489 comitter=request.user,
491 source_revision = str(udoc.revision),
493 comment = form.cleaned_data['message'] or '$AUTO$ Document shared.'
497 return response.RequestAccepted().django_response(\
498 ticket_status=prq.status, \
499 ticket_uri=reverse("pullrequest_view", args=[prq.id]) )
501 if form.cleaned_data['type'] == 'update':
502 # update is always performed from the file branch
504 success, changed = udoc.update(request.user.username)
506 if form.cleaned_data['type'] == 'share':
507 success, changed = udoc.share(form.cleaned_data['message'])
510 return response.EntityConflict().django_response({
511 'reason': 'merge-failure',
515 return response.SuccessNoContent().django_response()
517 new_udoc = udoc.latest()
519 return response.SuccessAllOk().django_response({
521 "parent_user_resivion": udoc.revision,
522 "parent_revision": doc.revision,
523 "revision": ndoc.revision,
524 'timestamp': ndoc.revision.timestamp,