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():
56 'url': reverse('document_view', args=[docid]),
61 related = PartCache.objects.defer('part_id')\
62 .values_list('part_id', 'document_id').distinct()
64 for part, docid in related:
65 # this way, we won't display broken links
66 if not documents.has_key(part):
69 child = documents[part]
70 parent = documents[docid]
72 if isinstance(parent, dict): # the parent is top-level
74 parent['parts'].append(child)
75 documents[part] = child['parts']
80 'documents': [d for d in documents.itervalues() if isinstance(d, dict)]
83 @validate_form(forms.DocumentUploadForm, 'POST')
85 def create(self, request, form, lib):
86 """Create a new document."""
88 if form.cleaned_data['ocr_data']:
89 data = form.cleaned_data['ocr_data']
91 data = request.FILES['ocr_file'].read().decode('utf-8')
93 if form.cleaned_data['generate_dc']:
94 data = librarian.wrap_text(data, unicode(date.today()))
96 docid = form.cleaned_data['bookname']
102 doc = lib.document_create(docid)
103 # document created, but no content yet
106 doc = doc.quickwrite('xml', data.encode('utf-8'),
107 '$AUTO$ XML data uploaded.', user=request.user.username)
109 # rollback branch creation
111 raise LibraryException("Exception occured:" + repr(e))
113 url = reverse('document_view', args=[doc.id])
115 return response.EntityCreated().django_response(\
119 'revision': doc.revision },
123 except LibraryException, e:
124 return response.InternalError().django_response(\
125 {'exception': repr(e) })
126 except DocumentAlreadyExists:
127 # Document is already there
128 return response.EntityConflict().django_response(\
129 {"reason": "Document %s already exists." % docid})
134 class BasicDocumentHandler(AnonymousBaseHandler):
135 allowed_methods = ('GET',)
138 def read(self, request, docid, lib):
140 doc = lib.document(docid)
141 except RevisionNotFound:
146 'html_url': reverse('dochtml_view', args=[doc.id,doc.revision]),
147 'text_url': reverse('doctext_view', args=[doc.id,doc.revision]),
148 'dc_url': reverse('docdc_view', args=[doc.id,doc.revision]),
149 'public_revision': doc.revision,
157 class DocumentHandler(BaseHandler):
158 allowed_methods = ('GET', 'PUT')
159 anonymous = BasicDocumentHandler
162 def read(self, request, docid, lib):
163 """Read document's meta data"""
165 doc = lib.document(docid)
166 udoc = doc.take(request.user.username)
167 except RevisionNotFound:
168 return request.EnityNotFound().django_response()
170 # is_shared = udoc.ancestorof(doc)
171 # is_uptodate = is_shared or shared.ancestorof(document)
175 'html_url': reverse('dochtml_view', args=[udoc.id,udoc.revision]),
176 'text_url': reverse('doctext_view', args=[udoc.id,udoc.revision]),
177 'dc_url': reverse('docdc_view', args=[udoc.id,udoc.revision]),
178 #'gallery_url': reverse('docdc_view', args=[udoc.id,udoc.revision]),
179 'user_revision': udoc.revision,
180 'public_revision': doc.revision,
186 def update(self, request, docid, lib):
187 """Update information about the document, like display not"""
192 class DocumentHTMLHandler(BaseHandler):
193 allowed_methods = ('GET', 'PUT')
196 def read(self, request, docid, revision, lib):
197 """Read document as html text"""
199 if revision == 'latest':
200 document = lib.document(docid)
202 document = lib.document_for_rev(revision)
204 return librarian.html.transform(document.data('xml'), is_file=False)
205 except RevisionNotFound:
206 return response.EntityNotFound().django_response()
212 XINCLUDE_REGEXP = r"""<(?:\w+:)?include\s+[^>]*?href=("|')wlrepo://(?P<link>[^\1]+?)\1\s*[^>]*?>"""
216 class DocumentTextHandler(BaseHandler):
217 allowed_methods = ('GET', 'PUT')
220 def read(self, request, docid, revision, lib):
221 """Read document as raw text"""
223 if revision == 'latest':
224 document = lib.document(docid)
226 document = lib.document_for_rev(revision)
228 # TODO: some finer-grained access control
229 return document.data('xml')
230 except RevisionNotFound:
231 return response.EntityNotFound().django_response()
234 def update(self, request, docid, revision, lib):
236 data = request.PUT['contents']
238 if request.PUT.has_key('message'):
239 msg = u"$USER$ " + request.PUT['message']
241 msg = u"$AUTO$ XML content update."
243 current = lib.document(docid, request.user.username)
244 orig = lib.document_for_rev(revision)
247 return response.EntityConflict().django_response({
248 "reason": "out-of-date",
249 "provided_revision": orig.revision,
250 "latest_revision": current.revision })
252 # try to find any Xinclude tags
253 includes = [m.groupdict()['link'] for m in (re.finditer(\
254 XINCLUDE_REGEXP, data, flags=re.UNICODE) or []) ]
256 # TODO: provide useful routines to make this simpler
257 def xml_update_action(lib, resolve):
259 f = lib._fileopen(resolve('parts'), 'r')
260 stored_includes = json.loads(f.read())
265 if stored_includes != includes:
266 f = lib._fileopen(resolve('parts'), 'w+')
267 f.write(json.dumps(includes))
270 lib._fileadd(resolve('parts'))
272 # update the parts cache
273 PartCache.update_cache(docid, current.owner,\
274 stored_includes, includes)
276 # now that the parts are ok, write xml
277 f = lib._fileopen(resolve('xml'), 'w+')
278 f.write(data.encode('utf-8'))
282 ndoc = current.invoke_and_commit(\
283 xml_update_action, lambda d: (msg, current.owner) )
286 # return the new revision number
287 return response.SuccessAllOk().django_response({
290 "previous_revision": current.revision,
291 "updated_revision": ndoc.revision,
292 "url": reverse("doctext_view", args=[ndoc.id, ndoc.revision])
295 if ndoc: lib._rollback()
297 except RevisionNotFound, e:
298 return response.EntityNotFound(mimetype="text/plain").\
299 django_response(e.message)
303 # Dublin Core handlers
305 # @requires librarian
307 class DocumentDublinCoreHandler(BaseHandler):
308 allowed_methods = ('GET', 'PUT')
311 def read(self, request, docid, revision, lib):
312 """Read document as raw text"""
314 if revision == 'latest':
315 doc = lib.document(docid)
317 doc = lib.document_for_rev(revision)
319 bookinfo = dcparser.BookInfo.from_string(doc.data('xml'))
320 return bookinfo.serialize()
321 except RevisionNotFound:
322 return response.EntityNotFound().django_response()
325 def update(self, request, docid, revision, lib):
327 bi_json = request.PUT['contents']
328 if request.PUT.has_key('message'):
329 msg = u"$USER$ " + request.PUT['message']
331 msg = u"$AUTO$ Dublin core update."
333 current = lib.document(docid, request.user.username)
334 orig = lib.document_for_rev(revision)
337 return response.EntityConflict().django_response({
338 "reason": "out-of-date",
339 "provided": orig.revision,
340 "latest": current.revision })
342 xmldoc = parser.WLDocument.from_string(current.data('xml'))
343 document.book_info = dcparser.BookInfo.from_json(bi_json)
346 ndoc = current.quickwrite('xml', \
347 document.serialize().encode('utf-8'),\
348 message=msg, user=request.user.username)
351 # return the new revision number
355 "previous_revision": current.revision,
356 "updated_revision": ndoc.revision,
357 "url": reverse("docdc_view", args=[ndoc.id, ndoc.revision])
360 if ndoc: lib._rollback()
362 except RevisionNotFound:
363 return response.EntityNotFound().django_response()
365 class MergeHandler(BaseHandler):
366 allowed_methods = ('POST',)
368 @validate_form(forms.MergeRequestForm, 'POST')
370 def create(self, request, form, docid, lib):
371 """Create a new document revision from the information provided by user"""
373 target_rev = form.cleaned_data['target_revision']
375 doc = lib.document(docid)
376 udoc = doc.take(request.user.username)
378 if target_rev == 'latest':
379 target_rev = udoc.revision
381 if str(udoc.revision) != target_rev:
382 # user think doesn't know he has an old version
385 # Updating is teorericly ok, but we need would
386 # have to force a refresh. Sharing may be not safe,
387 # 'cause it doesn't always result in update.
389 # In other words, we can't lie about the resource's state
390 # So we should just yield and 'out-of-date' conflict
391 # and let the client ask again with updated info.
393 # NOTE: this could result in a race condition, when there
394 # are 2 instances of the same user editing the same document.
395 # Instance "A" trying to update, and instance "B" always changing
396 # the document right before "A". The anwser to this problem is
397 # for the "A" to request a merge from 'latest' and then
398 # check the parent revisions in response, if he actually
399 # merge from where he thinks he should. If not, the client SHOULD
400 # update his internal state.
401 return response.EntityConflict().django_response({
402 "reason": "out-of-date",
403 "provided": target_rev,
404 "latest": udoc.revision })
406 if not request.user.has_perm('explorer.book.can_share'):
407 # User is not permitted to make a merge, right away
408 # So we instead create a pull request in the database
410 comitter=request.user,
412 source_revision = str(udoc.revision),
414 comment = form.cleaned_data['comment'] or '$AUTO$ Document shared.'
418 return response.RequestAccepted().django_response(\
419 ticket_status=prq.status, \
420 ticket_uri=reverse("pullrequest_view", args=[prq.id]) )
422 if form.cleaned_data['type'] == 'update':
423 # update is always performed from the file branch
425 success, changed = udoc.update(request.user.username)
427 if form.cleaned_data['type'] == 'share':
428 success, changed = udoc.share(form.cleaned_data['comment'])
431 return response.EntityConflict().django_response()
434 return response.SuccessNoContent().django_response()
436 new_udoc = udoc.latest()
438 return response.SuccessAllOk().django_response({
440 "parent_user_resivion": udoc.revision,
441 "parent_revision": doc.revision,
442 "revision": udoc.revision,