Fixed text update.
[redakcja.git] / apps / api / handlers / library_handlers.py
1 # -*- encoding: utf-8 -*-
2
3 __author__= "Ɓukasz Rekucki"
4 __date__ = "$2009-09-25 15:49:50$"
5 __doc__ = "Module documentation."
6
7 from piston.handler import BaseHandler, AnonymousBaseHandler
8
9 import re
10 from datetime import date
11
12 from django.core.urlresolvers import reverse
13 from django.utils import simplejson as json
14
15 import librarian
16 import librarian.html
17 from librarian import dcparser
18
19 from wlrepo import RevisionNotFound, LibraryException, DocumentAlreadyExists
20 from explorer.models import PullRequest
21
22 # internal imports
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
27
28 #
29 # Document List Handlers
30 #
31 class BasicLibraryHandler(AnonymousBaseHandler):
32     allowed_methods = ('GET',)
33
34     @hglibrary
35     def read(self, request, lib):
36         """Return the list of documents."""       
37         document_list = [{
38             'url': reverse('document_view', args=[docid]),
39             'name': docid } for docid in lib.documents() ]
40
41         return {'documents' : document_list}
42         
43
44 class LibraryHandler(BaseHandler):
45     allowed_methods = ('GET', 'POST')
46     anonymous = BasicLibraryHandler
47
48     @hglibrary
49     def read(self, request, lib):
50         """Return the list of documents."""
51
52         documents = {}
53         
54         for docid in lib.documents():
55             documents[docid] = {
56                 'url': reverse('document_view', args=[docid]),
57                 'name': docid,
58                 'parts': []
59             }
60
61         related = PartCache.objects.defer('part_id')\
62             .values_list('part_id', 'document_id').distinct()
63
64         for part, docid in related:
65             # this way, we won't display broken links
66             if not documents.has_key(part):
67                 continue
68
69             child = documents[part]
70             parent = documents[docid]
71             
72             if isinstance(parent, dict): # the parent is top-level
73                 documents.pop(part)                
74                 parent['parts'].append(child)
75                 documents[part] = child['parts']
76             else: # not top-level
77                 parent.append(child)
78             
79         return {
80             'documents': [d for d in documents.itervalues() if isinstance(d, dict)]
81         }
82
83     @validate_form(forms.DocumentUploadForm, 'POST')
84     @hglibrary
85     def create(self, request, form, lib):
86         """Create a new document."""       
87
88         if form.cleaned_data['ocr_data']:
89             data = form.cleaned_data['ocr_data']
90         else:            
91             data = request.FILES['ocr_file'].read().decode('utf-8')
92
93         if form.cleaned_data['generate_dc']:
94             data = librarian.wrap_text(data, unicode(date.today()))
95
96         docid = form.cleaned_data['bookname']
97
98         try:
99             lock = lib.lock()            
100             try:
101                 print "DOCID", docid                
102                 doc = lib.document_create(docid)
103                 # document created, but no content yet
104
105                 try:
106                     doc = doc.quickwrite('xml', data.encode('utf-8'),
107                         '$AUTO$ XML data uploaded.', user=request.user.username)
108                 except Exception,e:
109                     # rollback branch creation
110                     lib._rollback()
111                     raise LibraryException("Exception occured:" + repr(e))
112
113                 url = reverse('document_view', args=[doc.id])
114
115                 return response.EntityCreated().django_response(\
116                     body = {
117                         'url': url,
118                         'name': doc.id,
119                         'revision': doc.revision },
120                     url = url )            
121             finally:
122                 lock.release()
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})
130
131 #
132 # Document Handlers
133 #
134 class BasicDocumentHandler(AnonymousBaseHandler):
135     allowed_methods = ('GET',)
136
137     @hglibrary
138     def read(self, request, docid, lib):
139         try:    
140             doc = lib.document(docid)
141         except RevisionNotFound:
142             return rc.NOT_FOUND
143
144         result = {
145             'name': doc.id,
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,
150         }
151
152         return result
153
154 #
155 # Document Meta Data
156 #
157 class DocumentHandler(BaseHandler):
158     allowed_methods = ('GET', 'PUT')
159     anonymous = BasicDocumentHandler
160
161     @hglibrary
162     def read(self, request, docid, lib):
163         """Read document's meta data"""       
164         try:
165             doc = lib.document(docid)
166             udoc = doc.take(request.user.username)
167         except RevisionNotFound:
168             return request.EnityNotFound().django_response()
169
170         # is_shared = udoc.ancestorof(doc)
171         # is_uptodate = is_shared or shared.ancestorof(document)
172
173         result = {
174             'name': udoc.id,
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,            
180         }       
181
182         return result
183
184     @hglibrary
185     def update(self, request, docid, lib):
186         """Update information about the document, like display not"""
187         return
188 #
189 #
190 #
191 class DocumentHTMLHandler(BaseHandler):
192     allowed_methods = ('GET', 'PUT')
193
194     @hglibrary
195     def read(self, request, docid, revision, lib):
196         """Read document as html text"""
197         try:
198             if revision == 'latest':
199                 document = lib.document(docid)
200             else:
201                 document = lib.document_for_rev(revision)
202
203             return librarian.html.transform(document.data('xml'), is_file=False)
204         except RevisionNotFound:
205             return response.EntityNotFound().django_response()
206
207
208
209
210 #
211 # Document Text View
212 #
213
214 XINCLUDE_REGEXP = r"""<(?:\w+:)?include\s+[^>]*?href=("|')wlrepo://(?P<link>[^\1]+?)\1\s*[^>]*?>"""
215 #
216 #
217 class DocumentTextHandler(BaseHandler):
218     allowed_methods = ('GET', 'PUT')
219
220     @hglibrary
221     def read(self, request, docid, revision, lib):
222         """Read document as raw text"""               
223         try:
224             if revision == 'latest':
225                 document = lib.document(docid)
226             else:
227                 document = lib.document_for_rev(revision)
228             
229             # TODO: some finer-grained access control
230             return document.data('xml')
231         except RevisionNotFound:
232             return response.EntityNotFound().django_response()
233
234     @hglibrary
235     def update(self, request, docid, revision, lib):
236         try:
237             data = request.PUT['contents']            
238
239             if request.PUT.has_key('message'):
240                 msg = u"$USER$ " + request.PUT['message']
241             else:
242                 msg = u"$AUTO$ XML content update."
243
244             current = lib.document(docid, request.user.username)
245             orig = lib.document_for_rev(revision)
246
247             if current != orig:
248                 return response.EntityConflict().django_response({
249                         "reason": "out-of-date",
250                         "provided_revision": orig.revision,
251                         "latest_revision": current.revision })
252
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 []) ]
256
257             # TODO: provide useful routines to make this simpler
258             def xml_update_action(lib, resolve):
259                 try:
260                     f = lib._fileopen(resolve('parts'), 'r')
261                     stored_includes = json.loads(f.read())
262                     f.close()
263                 except:
264                     stored_includes = []
265                 
266                 if stored_includes != includes:
267                     f = lib._fileopen(resolve('parts'), 'w+')
268                     f.write(json.dumps(includes))
269                     f.close()
270
271                     # update the parts cache
272                     PartCache.update_cache(docid, current.owner,\
273                         stored_includes, includes)
274
275                 # now that the parts are ok, write xml
276                 f = lib._fileopen(resolve('xml'), 'w+')
277                 f.write(data)
278                 f.close()
279
280             ndoc = None
281             ndoc = current.invoke_and_commit(\
282                 xml_update_action, lambda d: (msg, current.owner) )
283
284             try:
285                 # return the new revision number
286                 return response.SuccessAllOk().django_response({
287                     "document": ndoc.id,
288                     "subview": "xml",
289                     "previous_revision": current.revision,
290                     "updated_revision": ndoc.revision,
291                     "url": reverse("doctext_view", args=[ndoc.id, ndoc.revision])
292                 })
293             except Exception, e:
294                 if ndoc: lib._rollback()
295                 raise e        
296         except RevisionNotFound, e:
297             return response.EntityNotFound().django_response(e)
298
299 #
300 # Dublin Core handlers
301 #
302 # @requires librarian
303 #
304 class DocumentDublinCoreHandler(BaseHandler):
305     allowed_methods = ('GET', 'PUT')
306
307     @hglibrary
308     def read(self, request, docid, revision, lib):
309         """Read document as raw text"""        
310         try:
311             if revision == 'latest':
312                 doc = lib.document(docid)
313             else:
314                 doc = lib.document_for_rev(revision)
315             
316             bookinfo = dcparser.BookInfo.from_string(doc.data('xml'))
317             return bookinfo.serialize()
318         except RevisionNotFound:
319             return response.EntityNotFound().django_response()
320
321     @hglibrary
322     def update(self, request, docid, revision, lib):
323         try:
324             bi_json = request.PUT['contents']            
325             if request.PUT.has_key('message'):
326                 msg = u"$USER$ " + request.PUT['message']
327             else:
328                 msg = u"$AUTO$ Dublin core update."
329
330             current = lib.document(docid, request.user.username)
331             orig = lib.document_for_rev(revision)
332
333             if current != orig:
334                 return response.EntityConflict().django_response({
335                         "reason": "out-of-date",
336                         "provided": orig.revision,
337                         "latest": current.revision })
338
339             xmldoc = parser.WLDocument.from_string(current.data('xml'))
340             document.book_info = dcparser.BookInfo.from_json(bi_json)
341
342             # zapisz
343             ndoc = current.quickwrite('xml', \
344                 document.serialize().encode('utf-8'),\
345                 message=msg, user=request.user.username)
346
347             try:
348                 # return the new revision number
349                 return {
350                     "document": ndoc.id,
351                     "subview": "dc",
352                     "previous_revision": current.revision,
353                     "updated_revision": ndoc.revision
354                 }
355             except Exception, e:
356                 lib._rollback()
357                 raise e
358         except RevisionNotFound:
359             return response.EntityNotFound().django_response()
360
361
362
363 class MergeHandler(BaseHandler):
364     allowed_methods = ('POST',)
365
366     @validate_form(forms.MergeRequestForm, 'POST')
367     @hglibrary
368     def create(self, request, form, docid, lib):
369         """Create a new document revision from the information provided by user"""
370
371         target_rev = form.cleaned_data['target_revision']
372
373         doc = lib.document(docid)
374         udoc = doc.take(request.user.username)
375
376         if target_rev == 'latest':
377             target_rev = udoc.revision
378
379         if str(udoc.revision) != target_rev:
380             # user think doesn't know he has an old version
381             # of his own branch.
382             
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.
386
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.
390
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 })
403
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
407             prq = PullRequest(
408                 comitter=request.user,
409                 document=docid,
410                 source_revision = str(udoc.revision),
411                 status="N",
412                 comment = form.cleaned_data['comment'] or '$AUTO$ Document shared.'
413             )
414
415             prq.save()
416             return response.RequestAccepted().django_response(\
417                 ticket_status=prq.status, \
418                 ticket_uri=reverse("pullrequest_view", args=[prq.id]) )
419
420         if form.cleaned_data['type'] == 'update':
421             # update is always performed from the file branch
422             # to the user branch
423             success, changed = udoc.update(request.user.username)
424
425         if form.cleaned_data['type'] == 'share':
426             success, changed = udoc.share(form.cleaned_data['comment'])
427
428         if not success:
429             return response.EntityConflict().django_response()
430
431         if not changed:
432             return response.SuccessNoContent().django_response()
433
434         new_udoc = udoc.latest()
435
436         return response.SuccessAllOk().django_response({
437             "name": udoc.id,
438             "parent_user_resivion": udoc.revision,
439             "parent_revision": doc.revision,
440             "revision": udoc.revision,
441         })