1 # -*- encoding: utf-8 -*-
3 __author__= "Ćukasz Rekucki"
4 __date__ = "$2009-09-25 15:49:50$"
5 __doc__ = "Module documentation."
7 from piston.handler import BaseHandler, AnonymousBaseHandler
10 from datetime import date
12 from django.core.urlresolvers import reverse
13 from django.utils import simplejson as json
17 from librarian import dcparser
19 from wlrepo import RevisionNotFound, LibraryException, DocumentAlreadyExists
20 from explorer.models import PullRequest
23 import api.forms as forms
24 import api.response as response
25 from api.utils import validate_form, hglibrary
26 from api.models import PartCache
29 # Document List Handlers
31 class BasicLibraryHandler(AnonymousBaseHandler):
32 allowed_methods = ('GET',)
35 def read(self, request, lib):
36 """Return the list of documents."""
38 'url': reverse('document_view', args=[docid]),
39 'name': docid } for docid in lib.documents() ]
41 return {'documents' : document_list}
44 class LibraryHandler(BaseHandler):
45 allowed_methods = ('GET', 'POST')
46 anonymous = BasicLibraryHandler
49 def read(self, request, lib):
50 """Return the list of documents."""
54 for docid in lib.documents():
55 docid = docid.decode('utf-8')
57 'url': reverse('document_view', args=[docid]),
62 parts = PartCache.objects.defer('part_id')\
63 .values_list('part_id', 'document_id').distinct()
65 document_tree = dict(documents)
67 for part, docid in parts:
68 # this way, we won't display broken links
69 if not documents.has_key(part):
70 print "NOT FOUND:", part
73 parent = documents[docid]
74 child = documents[part]
76 # not top-level anymore
77 document_tree.pop(part)
78 parent['parts'].append(child)
80 return {'documents': sorted(document_tree.values()) }
82 @validate_form(forms.DocumentUploadForm, 'POST')
84 def create(self, request, form, lib):
85 """Create a new document."""
87 if form.cleaned_data['ocr_data']:
88 data = form.cleaned_data['ocr_data']
90 data = request.FILES['ocr_file'].read().decode('utf-8')
92 if form.cleaned_data['generate_dc']:
93 data = librarian.wrap_text(data, unicode(date.today()))
95 docid = form.cleaned_data['bookname']
101 doc = lib.document_create(docid)
102 # document created, but no content yet
105 doc = doc.quickwrite('xml', data.encode('utf-8'),
106 '$AUTO$ XML data uploaded.', user=request.user.username)
108 # rollback branch creation
110 raise LibraryException("Exception occured:" + repr(e))
112 url = reverse('document_view', args=[doc.id])
114 return response.EntityCreated().django_response(\
118 'revision': doc.revision },
122 except LibraryException, e:
123 return response.InternalError().django_response(\
124 {'exception': repr(e) })
125 except DocumentAlreadyExists:
126 # Document is already there
127 return response.EntityConflict().django_response(\
128 {"reason": "Document %s already exists." % docid})
133 class BasicDocumentHandler(AnonymousBaseHandler):
134 allowed_methods = ('GET',)
137 def read(self, request, docid, lib):
139 doc = lib.document(docid)
140 except RevisionNotFound:
145 'html_url': reverse('dochtml_view', args=[doc.id,doc.revision]),
146 'text_url': reverse('doctext_view', args=[doc.id,doc.revision]),
147 'dc_url': reverse('docdc_view', args=[doc.id,doc.revision]),
148 'public_revision': doc.revision,
156 class DocumentHandler(BaseHandler):
157 allowed_methods = ('GET', 'PUT')
158 anonymous = BasicDocumentHandler
161 def read(self, request, docid, lib):
162 """Read document's meta data"""
164 doc = lib.document(docid)
165 udoc = doc.take(request.user.username)
166 except RevisionNotFound:
167 return request.EnityNotFound().django_response()
169 # is_shared = udoc.ancestorof(doc)
170 # is_uptodate = is_shared or shared.ancestorof(document)
174 'html_url': reverse('dochtml_view', args=[udoc.id,udoc.revision]),
175 'text_url': reverse('doctext_view', args=[udoc.id,udoc.revision]),
176 'dc_url': reverse('docdc_view', args=[udoc.id,udoc.revision]),
177 #'gallery_url': reverse('docdc_view', args=[udoc.id,udoc.revision]),
178 'user_revision': udoc.revision,
179 'public_revision': doc.revision,
185 def update(self, request, docid, lib):
186 """Update information about the document, like display not"""
191 class DocumentHTMLHandler(BaseHandler):
192 allowed_methods = ('GET', 'PUT')
195 def read(self, request, docid, revision, lib):
196 """Read document as html text"""
198 if revision == 'latest':
199 document = lib.document(docid)
201 document = lib.document_for_rev(revision)
203 return librarian.html.transform(document.data('xml'), is_file=False)
204 except RevisionNotFound:
205 return response.EntityNotFound().django_response()
211 XINCLUDE_REGEXP = r"""<(?:\w+:)?include\s+[^>]*?href=("|')wlrepo://(?P<link>[^\1]+?)\1\s*[^>]*?>"""
215 class DocumentTextHandler(BaseHandler):
216 allowed_methods = ('GET', 'PUT')
219 def read(self, request, docid, revision, lib):
220 """Read document as raw text"""
222 if revision == 'latest':
223 document = lib.document(docid)
225 document = lib.document_for_rev(revision)
227 # TODO: some finer-grained access control
228 return document.data('xml')
229 except RevisionNotFound:
230 return response.EntityNotFound().django_response()
233 def update(self, request, docid, revision, lib):
235 data = request.PUT['contents']
237 if request.PUT.has_key('message'):
238 msg = u"$USER$ " + request.PUT['message']
240 msg = u"$AUTO$ XML content update."
242 current = lib.document(docid, request.user.username)
243 orig = lib.document_for_rev(revision)
246 return response.EntityConflict().django_response({
247 "reason": "out-of-date",
248 "provided_revision": orig.revision,
249 "latest_revision": current.revision })
251 # try to find any Xinclude tags
252 includes = [m.groupdict()['link'] for m in (re.finditer(\
253 XINCLUDE_REGEXP, data, flags=re.UNICODE) or []) ]
255 print "INCLUDES: ", includes
257 # TODO: provide useful routines to make this simpler
258 def xml_update_action(lib, resolve):
260 f = lib._fileopen(resolve('parts'), 'r')
261 stored_includes = json.loads(f.read())
266 if stored_includes != includes:
267 f = lib._fileopen(resolve('parts'), 'w+')
268 f.write(json.dumps(includes))
271 lib._fileadd(resolve('parts'))
273 # update the parts cache
274 PartCache.update_cache(docid, current.owner,\
275 stored_includes, includes)
277 # now that the parts are ok, write xml
278 f = lib._fileopen(resolve('xml'), 'w+')
279 f.write(data.encode('utf-8'))
283 ndoc = current.invoke_and_commit(\
284 xml_update_action, lambda d: (msg, current.owner) )
287 # return the new revision number
288 return response.SuccessAllOk().django_response({
291 "previous_revision": current.revision,
292 "updated_revision": ndoc.revision,
293 "url": reverse("doctext_view", args=[ndoc.id, ndoc.revision])
296 if ndoc: lib._rollback()
298 except RevisionNotFound, e:
299 return response.EntityNotFound(mimetype="text/plain").\
300 django_response(e.message)
304 # Dublin Core handlers
306 # @requires librarian
308 class DocumentDublinCoreHandler(BaseHandler):
309 allowed_methods = ('GET', 'PUT')
312 def read(self, request, docid, revision, lib):
313 """Read document as raw text"""
315 if revision == 'latest':
316 doc = lib.document(docid)
318 doc = lib.document_for_rev(revision)
320 bookinfo = dcparser.BookInfo.from_string(doc.data('xml'))
321 return bookinfo.serialize()
322 except RevisionNotFound:
323 return response.EntityNotFound().django_response()
326 def update(self, request, docid, revision, lib):
328 bi_json = request.PUT['contents']
329 if request.PUT.has_key('message'):
330 msg = u"$USER$ " + request.PUT['message']
332 msg = u"$AUTO$ Dublin core update."
334 current = lib.document(docid, request.user.username)
335 orig = lib.document_for_rev(revision)
338 return response.EntityConflict().django_response({
339 "reason": "out-of-date",
340 "provided": orig.revision,
341 "latest": current.revision })
343 xmldoc = parser.WLDocument.from_string(current.data('xml'))
344 document.book_info = dcparser.BookInfo.from_json(bi_json)
347 ndoc = current.quickwrite('xml', \
348 document.serialize().encode('utf-8'),\
349 message=msg, user=request.user.username)
352 # return the new revision number
356 "previous_revision": current.revision,
357 "updated_revision": ndoc.revision,
358 "url": reverse("docdc_view", args=[ndoc.id, ndoc.revision])
361 if ndoc: lib._rollback()
363 except RevisionNotFound:
364 return response.EntityNotFound().django_response()
366 class MergeHandler(BaseHandler):
367 allowed_methods = ('POST',)
369 @validate_form(forms.MergeRequestForm, 'POST')
371 def create(self, request, form, docid, lib):
372 """Create a new document revision from the information provided by user"""
374 target_rev = form.cleaned_data['target_revision']
376 doc = lib.document(docid)
377 udoc = doc.take(request.user.username)
379 if target_rev == 'latest':
380 target_rev = udoc.revision
382 if str(udoc.revision) != target_rev:
383 # user think doesn't know he has an old version
386 # Updating is teorericly ok, but we need would
387 # have to force a refresh. Sharing may be not safe,
388 # 'cause it doesn't always result in update.
390 # In other words, we can't lie about the resource's state
391 # So we should just yield and 'out-of-date' conflict
392 # and let the client ask again with updated info.
394 # NOTE: this could result in a race condition, when there
395 # are 2 instances of the same user editing the same document.
396 # Instance "A" trying to update, and instance "B" always changing
397 # the document right before "A". The anwser to this problem is
398 # for the "A" to request a merge from 'latest' and then
399 # check the parent revisions in response, if he actually
400 # merge from where he thinks he should. If not, the client SHOULD
401 # update his internal state.
402 return response.EntityConflict().django_response({
403 "reason": "out-of-date",
404 "provided": target_rev,
405 "latest": udoc.revision })
407 if not request.user.has_perm('explorer.book.can_share'):
408 # User is not permitted to make a merge, right away
409 # So we instead create a pull request in the database
411 comitter=request.user,
413 source_revision = str(udoc.revision),
415 comment = form.cleaned_data['comment'] or '$AUTO$ Document shared.'
419 return response.RequestAccepted().django_response(\
420 ticket_status=prq.status, \
421 ticket_uri=reverse("pullrequest_view", args=[prq.id]) )
423 if form.cleaned_data['type'] == 'update':
424 # update is always performed from the file branch
426 success, changed = udoc.update(request.user.username)
428 if form.cleaned_data['type'] == 'share':
429 success, changed = udoc.share(form.cleaned_data['comment'])
432 return response.EntityConflict().django_response()
435 return response.SuccessNoContent().django_response()
437 new_udoc = udoc.latest()
439 return response.SuccessAllOk().django_response({
441 "parent_user_resivion": udoc.revision,
442 "parent_revision": doc.revision,
443 "revision": udoc.revision,