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+')
282 ndoc = current.invoke_and_commit(\
283 xml_update_action, lambda d: (msg, current.owner) )
286 # return the new revision number
290 "previous_revision": current.revision,
291 "updated_revision": ndoc.revision
296 except RevisionNotFound, e:
297 return response.EntityNotFound().django_response(e)
300 # Dublin Core handlers
302 # @requires librarian
304 class DocumentDublinCoreHandler(BaseHandler):
305 allowed_methods = ('GET', 'PUT')
308 def read(self, request, docid, revision, lib):
309 """Read document as raw text"""
311 if revision == 'latest':
312 doc = lib.document(docid)
314 doc = lib.document_for_rev(revision)
316 bookinfo = dcparser.BookInfo.from_string(doc.data('xml'))
317 return bookinfo.serialize()
318 except RevisionNotFound:
319 return response.EntityNotFound().django_response()
322 def update(self, request, docid, revision, lib):
324 bi_json = request.PUT['contents']
325 if request.PUT.has_key('message'):
326 msg = u"$USER$ " + request.PUT['message']
328 msg = u"$AUTO$ Dublin core update."
330 current = lib.document(docid, request.user.username)
331 orig = lib.document_for_rev(revision)
334 return response.EntityConflict().django_response({
335 "reason": "out-of-date",
336 "provided": orig.revision,
337 "latest": current.revision })
339 xmldoc = parser.WLDocument.from_string(current.data('xml'))
340 document.book_info = dcparser.BookInfo.from_json(bi_json)
343 ndoc = current.quickwrite('xml', \
344 document.serialize().encode('utf-8'),\
345 message=msg, user=request.user.username)
348 # return the new revision number
352 "previous_revision": current.revision,
353 "updated_revision": ndoc.revision
358 except RevisionNotFound:
359 return response.EntityNotFound().django_response()
363 class MergeHandler(BaseHandler):
364 allowed_methods = ('POST',)
366 @validate_form(forms.MergeRequestForm, 'POST')
368 def create(self, request, form, docid, lib):
369 """Create a new document revision from the information provided by user"""
371 target_rev = form.cleaned_data['target_revision']
373 doc = lib.document(docid)
374 udoc = doc.take(request.user.username)
376 if target_rev == 'latest':
377 target_rev = udoc.revision
379 if str(udoc.revision) != target_rev:
380 # user think doesn't know he has an old version
383 # Updating is teorericly ok, but we need would
384 # have to force a refresh. Sharing may be not safe,
385 # 'cause it doesn't always result in update.
387 # In other words, we can't lie about the resource's state
388 # So we should just yield and 'out-of-date' conflict
389 # and let the client ask again with updated info.
391 # NOTE: this could result in a race condition, when there
392 # are 2 instances of the same user editing the same document.
393 # Instance "A" trying to update, and instance "B" always changing
394 # the document right before "A". The anwser to this problem is
395 # for the "A" to request a merge from 'latest' and then
396 # check the parent revisions in response, if he actually
397 # merge from where he thinks he should. If not, the client SHOULD
398 # update his internal state.
399 return response.EntityConflict().django_response({
400 "reason": "out-of-date",
401 "provided": target_rev,
402 "latest": udoc.revision })
404 if not request.user.has_perm('explorer.book.can_share'):
405 # User is not permitted to make a merge, right away
406 # So we instead create a pull request in the database
408 comitter=request.user,
410 source_revision = str(udoc.revision),
412 comment = form.cleaned_data['comment'] or '$AUTO$ Document shared.'
416 return response.RequestAccepted().django_response(\
417 ticket_status=prq.status, \
418 ticket_uri=reverse("pullrequest_view", args=[prq.id]) )
420 if form.cleaned_data['type'] == 'update':
421 # update is always performed from the file branch
423 success, changed = udoc.update(request.user.username)
425 if form.cleaned_data['type'] == 'share':
426 success, changed = udoc.share(form.cleaned_data['comment'])
429 return response.EntityConflict().django_response()
432 return response.SuccessNoContent().django_response()
434 new_udoc = udoc.latest()
436 return response.SuccessAllOk().django_response({
438 "parent_user_resivion": udoc.revision,
439 "parent_revision": doc.revision,
440 "revision": udoc.revision,