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 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().django_response(e)
302 # Dublin Core handlers
304 # @requires librarian
306 class DocumentDublinCoreHandler(BaseHandler):
307 allowed_methods = ('GET', 'PUT')
310 def read(self, request, docid, revision, lib):
311 """Read document as raw text"""
313 if revision == 'latest':
314 doc = lib.document(docid)
316 doc = lib.document_for_rev(revision)
318 bookinfo = dcparser.BookInfo.from_string(doc.data('xml'))
319 return bookinfo.serialize()
320 except RevisionNotFound:
321 return response.EntityNotFound().django_response()
324 def update(self, request, docid, revision, lib):
326 bi_json = request.PUT['contents']
327 if request.PUT.has_key('message'):
328 msg = u"$USER$ " + request.PUT['message']
330 msg = u"$AUTO$ Dublin core update."
332 current = lib.document(docid, request.user.username)
333 orig = lib.document_for_rev(revision)
336 return response.EntityConflict().django_response({
337 "reason": "out-of-date",
338 "provided": orig.revision,
339 "latest": current.revision })
341 xmldoc = parser.WLDocument.from_string(current.data('xml'))
342 document.book_info = dcparser.BookInfo.from_json(bi_json)
345 ndoc = current.quickwrite('xml', \
346 document.serialize().encode('utf-8'),\
347 message=msg, user=request.user.username)
350 # return the new revision number
354 "previous_revision": current.revision,
355 "updated_revision": ndoc.revision
360 except RevisionNotFound:
361 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,