Merge branch 'zuber-view-refactor'
[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             #'gallery_url': reverse('docdc_view', args=[udoc.id,udoc.revision]),
179             'user_revision': udoc.revision,
180             'public_revision': doc.revision,            
181         }       
182
183         return result
184
185     @hglibrary
186     def update(self, request, docid, lib):
187         """Update information about the document, like display not"""
188         return
189 #
190 #
191 #
192 class DocumentHTMLHandler(BaseHandler):
193     allowed_methods = ('GET', 'PUT')
194
195     @hglibrary
196     def read(self, request, docid, revision, lib):
197         """Read document as html text"""
198         try:
199             if revision == 'latest':
200                 document = lib.document(docid)
201             else:
202                 document = lib.document_for_rev(revision)
203
204             return librarian.html.transform(document.data('xml'), is_file=False)
205         except RevisionNotFound:
206             return response.EntityNotFound().django_response()
207
208 #
209 # Document Text View
210 #
211
212 XINCLUDE_REGEXP = r"""<(?:\w+:)?include\s+[^>]*?href=("|')wlrepo://(?P<link>[^\1]+?)\1\s*[^>]*?>"""
213 #
214 #
215 #
216 class DocumentTextHandler(BaseHandler):
217     allowed_methods = ('GET', 'PUT')
218
219     @hglibrary
220     def read(self, request, docid, revision, lib):
221         """Read document as raw text"""               
222         try:
223             if revision == 'latest':
224                 document = lib.document(docid)
225             else:
226                 document = lib.document_for_rev(revision)
227             
228             # TODO: some finer-grained access control
229             return document.data('xml')
230         except RevisionNotFound:
231             return response.EntityNotFound().django_response()
232
233     @hglibrary
234     def update(self, request, docid, revision, lib):
235         try:
236             data = request.PUT['contents']            
237
238             if request.PUT.has_key('message'):
239                 msg = u"$USER$ " + request.PUT['message']
240             else:
241                 msg = u"$AUTO$ XML content update."
242
243             current = lib.document(docid, request.user.username)
244             orig = lib.document_for_rev(revision)
245
246             if current != orig:
247                 return response.EntityConflict().django_response({
248                         "reason": "out-of-date",
249                         "provided_revision": orig.revision,
250                         "latest_revision": current.revision })
251
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 []) ]
255
256             # TODO: provide useful routines to make this simpler
257             def xml_update_action(lib, resolve):
258                 try:
259                     f = lib._fileopen(resolve('parts'), 'r')
260                     stored_includes = json.loads(f.read())
261                     f.close()
262                 except:
263                     stored_includes = []
264                 
265                 if stored_includes != includes:
266                     f = lib._fileopen(resolve('parts'), 'w+')
267                     f.write(json.dumps(includes))
268                     f.close()
269
270                     lib._fileadd(resolve('parts'))
271
272                     # update the parts cache
273                     PartCache.update_cache(docid, current.owner,\
274                         stored_includes, includes)
275
276                 # now that the parts are ok, write xml
277                 f = lib._fileopen(resolve('xml'), 'w+')
278                 f.write(data.encode('utf-8'))
279                 f.close()
280
281             ndoc = None
282             ndoc = current.invoke_and_commit(\
283                 xml_update_action, lambda d: (msg, current.owner) )
284
285             try:
286                 # return the new revision number
287                 return response.SuccessAllOk().django_response({
288                     "document": ndoc.id,
289                     "subview": "xml",
290                     "previous_revision": current.revision,
291                     "updated_revision": ndoc.revision,
292                     "url": reverse("doctext_view", args=[ndoc.id, ndoc.revision])
293                 })
294             except Exception, e:
295                 if ndoc: lib._rollback()
296                 raise e        
297         except RevisionNotFound, e:
298             return response.EntityNotFound(mimetype="text/plain").\
299                 django_response(e.message)
300
301
302 #
303 # Dublin Core handlers
304 #
305 # @requires librarian
306 #
307 class DocumentDublinCoreHandler(BaseHandler):
308     allowed_methods = ('GET', 'PUT')
309
310     @hglibrary
311     def read(self, request, docid, revision, lib):
312         """Read document as raw text"""        
313         try:
314             if revision == 'latest':
315                 doc = lib.document(docid)
316             else:
317                 doc = lib.document_for_rev(revision)
318             
319             bookinfo = dcparser.BookInfo.from_string(doc.data('xml'))
320             return bookinfo.serialize()
321         except RevisionNotFound:
322             return response.EntityNotFound().django_response()
323
324     @hglibrary
325     def update(self, request, docid, revision, lib):
326         try:
327             bi_json = request.PUT['contents']            
328             if request.PUT.has_key('message'):
329                 msg = u"$USER$ " + request.PUT['message']
330             else:
331                 msg = u"$AUTO$ Dublin core update."
332
333             current = lib.document(docid, request.user.username)
334             orig = lib.document_for_rev(revision)
335
336             if current != orig:
337                 return response.EntityConflict().django_response({
338                         "reason": "out-of-date",
339                         "provided": orig.revision,
340                         "latest": current.revision })
341
342             xmldoc = parser.WLDocument.from_string(current.data('xml'))
343             document.book_info = dcparser.BookInfo.from_json(bi_json)
344
345             # zapisz
346             ndoc = current.quickwrite('xml', \
347                 document.serialize().encode('utf-8'),\
348                 message=msg, user=request.user.username)
349
350             try:
351                 # return the new revision number
352                 return {
353                     "document": ndoc.id,
354                     "subview": "dc",
355                     "previous_revision": current.revision,
356                     "updated_revision": ndoc.revision,
357                     "url": reverse("docdc_view", args=[ndoc.id, ndoc.revision])
358                 }
359             except Exception, e:
360                 if ndoc: lib._rollback()
361                 raise e
362         except RevisionNotFound:
363             return response.EntityNotFound().django_response()
364
365 class MergeHandler(BaseHandler):
366     allowed_methods = ('POST',)
367
368     @validate_form(forms.MergeRequestForm, 'POST')
369     @hglibrary
370     def create(self, request, form, docid, lib):
371         """Create a new document revision from the information provided by user"""
372
373         target_rev = form.cleaned_data['target_revision']
374
375         doc = lib.document(docid)
376         udoc = doc.take(request.user.username)
377
378         if target_rev == 'latest':
379             target_rev = udoc.revision
380
381         if str(udoc.revision) != target_rev:
382             # user think doesn't know he has an old version
383             # of his own branch.
384             
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.
388
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.
392
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 })
405
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
409             prq = PullRequest(
410                 comitter=request.user,
411                 document=docid,
412                 source_revision = str(udoc.revision),
413                 status="N",
414                 comment = form.cleaned_data['comment'] or '$AUTO$ Document shared.'
415             )
416
417             prq.save()
418             return response.RequestAccepted().django_response(\
419                 ticket_status=prq.status, \
420                 ticket_uri=reverse("pullrequest_view", args=[prq.id]) )
421
422         if form.cleaned_data['type'] == 'update':
423             # update is always performed from the file branch
424             # to the user branch
425             success, changed = udoc.update(request.user.username)
426
427         if form.cleaned_data['type'] == 'share':
428             success, changed = udoc.share(form.cleaned_data['comment'])
429
430         if not success:
431             return response.EntityConflict().django_response()
432
433         if not changed:
434             return response.SuccessNoContent().django_response()
435
436         new_udoc = udoc.latest()
437
438         return response.SuccessAllOk().django_response({
439             "name": udoc.id,
440             "parent_user_resivion": udoc.revision,
441             "parent_revision": doc.revision,
442             "revision": udoc.revision,
443         })