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 '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()
214 XINCLUDE_REGEXP = r"""<(?:\w+:)?include\s+[^>]*?href=("|')wlrepo://(?P<link>[^\1]+?)\1\s*[^>]*?>"""
217 class DocumentTextHandler(BaseHandler):
218 allowed_methods = ('GET', 'PUT')
221 def read(self, request, docid, revision, lib):
222 """Read document as raw text"""
224 if revision == 'latest':
225 document = lib.document(docid)
227 document = lib.document_for_rev(revision)
229 # TODO: some finer-grained access control
230 return document.data('xml')
231 except RevisionNotFound:
232 return response.EntityNotFound().django_response()
235 def update(self, request, docid, revision, lib):
237 data = request.PUT['contents']
239 if request.PUT.has_key('message'):
240 msg = u"$USER$ " + request.PUT['message']
242 msg = u"$AUTO$ XML content update."
244 current = lib.document(docid, request.user.username)
245 orig = lib.document_for_rev(revision)
248 return response.EntityConflict().django_response({
249 "reason": "out-of-date",
250 "provided_revision": orig.revision,
251 "latest_revision": current.revision })
253 # try to find any Xinclude tags
254 includes = [m.groupdict()['link'] for m in (re.finditer(\
255 XINCLUDE_REGEXP, data, flags=re.UNICODE) or []) ]
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 # update the parts cache
272 PartCache.update_cache(docid, current.owner,\
273 stored_includes, includes)
275 # now that the parts are ok, write xml
276 f = lib._fileopen(resolve('xml'), 'w+')
280 ndoc = current.invoke_and_commit(\
281 xml_update_action, lambda d: (msg, current.owner) )
284 # return the new revision number
288 "previous_revision": current.revision,
289 "updated_revision": ndoc.revision
294 except RevisionNotFound, e:
295 return response.EntityNotFound().django_response(e)
298 # Dublin Core handlers
300 # @requires librarian
302 class DocumentDublinCoreHandler(BaseHandler):
303 allowed_methods = ('GET', 'PUT')
306 def read(self, request, docid, revision, lib):
307 """Read document as raw text"""
309 if revision == 'latest':
310 doc = lib.document(docid)
312 doc = lib.document_for_rev(revision)
314 bookinfo = dcparser.BookInfo.from_string(doc.data('xml'))
315 return bookinfo.serialize()
316 except RevisionNotFound:
317 return response.EntityNotFound().django_response()
320 def update(self, request, docid, revision, lib):
322 bi_json = request.PUT['contents']
323 if request.PUT.has_key('message'):
324 msg = u"$USER$ " + request.PUT['message']
326 msg = u"$AUTO$ Dublin core update."
328 current = lib.document(docid, request.user.username)
329 orig = lib.document_for_rev(revision)
332 return response.EntityConflict().django_response({
333 "reason": "out-of-date",
334 "provided": orig.revision,
335 "latest": current.revision })
337 xmldoc = parser.WLDocument.from_string(current.data('xml'))
338 document.book_info = dcparser.BookInfo.from_json(bi_json)
341 ndoc = current.quickwrite('xml', \
342 document.serialize().encode('utf-8'),\
343 message=msg, user=request.user.username)
346 # return the new revision number
350 "previous_revision": current.revision,
351 "updated_revision": ndoc.revision
356 except RevisionNotFound:
357 return response.EntityNotFound().django_response()
361 class MergeHandler(BaseHandler):
362 allowed_methods = ('POST',)
364 @validate_form(forms.MergeRequestForm, 'POST')
366 def create(self, request, form, docid, lib):
367 """Create a new document revision from the information provided by user"""
369 target_rev = form.cleaned_data['target_revision']
371 doc = lib.document(docid)
372 udoc = doc.take(request.user.username)
374 if target_rev == 'latest':
375 target_rev = udoc.revision
377 if str(udoc.revision) != target_rev:
378 # user think doesn't know he has an old version
381 # Updating is teorericly ok, but we need would
382 # have to force a refresh. Sharing may be not safe,
383 # 'cause it doesn't always result in update.
385 # In other words, we can't lie about the resource's state
386 # So we should just yield and 'out-of-date' conflict
387 # and let the client ask again with updated info.
389 # NOTE: this could result in a race condition, when there
390 # are 2 instances of the same user editing the same document.
391 # Instance "A" trying to update, and instance "B" always changing
392 # the document right before "A". The anwser to this problem is
393 # for the "A" to request a merge from 'latest' and then
394 # check the parent revisions in response, if he actually
395 # merge from where he thinks he should. If not, the client SHOULD
396 # update his internal state.
397 return response.EntityConflict().django_response({
398 "reason": "out-of-date",
399 "provided": target_rev,
400 "latest": udoc.revision })
402 if not request.user.has_perm('explorer.book.can_share'):
403 # User is not permitted to make a merge, right away
404 # So we instead create a pull request in the database
406 comitter=request.user,
408 source_revision = str(udoc.revision),
410 comment = form.cleaned_data['comment'] or '$AUTO$ Document shared.'
414 return response.RequestAccepted().django_response(\
415 ticket_status=prq.status, \
416 ticket_uri=reverse("pullrequest_view", args=[prq.id]) )
418 if form.cleaned_data['type'] == 'update':
419 # update is always performed from the file branch
421 success, changed = udoc.update(request.user.username)
423 if form.cleaned_data['type'] == 'share':
424 success, changed = udoc.share(form.cleaned_data['comment'])
427 return response.EntityConflict().django_response()
430 return response.SuccessNoContent().django_response()
432 new_udoc = udoc.latest()
434 return response.SuccessAllOk().django_response({
436 "parent_user_resivion": udoc.revision,
437 "parent_revision": doc.revision,
438 "revision": udoc.revision,