Fixed the index tree display.
[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             docid = docid.decode('utf-8')
56             documents[docid] = {
57                 'url': reverse('document_view', args=[docid]),
58                 'name': docid,
59                 'parts': []
60             }
61
62         parts = PartCache.objects.defer('part_id')\
63             .values_list('part_id', 'document_id').distinct()
64        
65         document_tree = dict(documents)
66
67         for part, docid in parts:
68             # this way, we won't display broken links
69             if not documents.has_key(part):
70                 print "NOT FOUND:", part
71                 continue
72
73             parent = documents[docid]
74             child = documents[part]
75
76             # not top-level anymore
77             document_tree.pop(part)
78             parent['parts'].append(child)
79             
80         return {'documents': sorted(document_tree.values()) }
81
82     @validate_form(forms.DocumentUploadForm, 'POST')
83     @hglibrary
84     def create(self, request, form, lib):
85         """Create a new document."""       
86
87         if form.cleaned_data['ocr_data']:
88             data = form.cleaned_data['ocr_data']
89         else:            
90             data = request.FILES['ocr_file'].read().decode('utf-8')
91
92         if form.cleaned_data['generate_dc']:
93             data = librarian.wrap_text(data, unicode(date.today()))
94
95         docid = form.cleaned_data['bookname']
96
97         try:
98             lock = lib.lock()            
99             try:
100                 print "DOCID", docid                
101                 doc = lib.document_create(docid)
102                 # document created, but no content yet
103
104                 try:
105                     doc = doc.quickwrite('xml', data.encode('utf-8'),
106                         '$AUTO$ XML data uploaded.', user=request.user.username)
107                 except Exception,e:
108                     # rollback branch creation
109                     lib._rollback()
110                     raise LibraryException("Exception occured:" + repr(e))
111
112                 url = reverse('document_view', args=[doc.id])
113
114                 return response.EntityCreated().django_response(\
115                     body = {
116                         'url': url,
117                         'name': doc.id,
118                         'revision': doc.revision },
119                     url = url )            
120             finally:
121                 lock.release()
122         except LibraryException, e:
123             return response.InternalError().django_response(\
124                 {'exception': repr(e) })                
125         except DocumentAlreadyExists:
126             # Document is already there
127             return response.EntityConflict().django_response(\
128                 {"reason": "Document %s already exists." % docid})
129
130 #
131 # Document Handlers
132 #
133 class BasicDocumentHandler(AnonymousBaseHandler):
134     allowed_methods = ('GET',)
135
136     @hglibrary
137     def read(self, request, docid, lib):
138         try:    
139             doc = lib.document(docid)
140         except RevisionNotFound:
141             return rc.NOT_FOUND
142
143         result = {
144             'name': doc.id,
145             'html_url': reverse('dochtml_view', args=[doc.id,doc.revision]),
146             'text_url': reverse('doctext_view', args=[doc.id,doc.revision]),
147             'dc_url': reverse('docdc_view', args=[doc.id,doc.revision]),
148             'public_revision': doc.revision,
149         }
150
151         return result
152
153 #
154 # Document Meta Data
155 #
156 class DocumentHandler(BaseHandler):
157     allowed_methods = ('GET', 'PUT')
158     anonymous = BasicDocumentHandler
159
160     @hglibrary
161     def read(self, request, docid, lib):
162         """Read document's meta data"""       
163         try:
164             doc = lib.document(docid)
165             udoc = doc.take(request.user.username)
166         except RevisionNotFound:
167             return request.EnityNotFound().django_response()
168
169         # is_shared = udoc.ancestorof(doc)
170         # is_uptodate = is_shared or shared.ancestorof(document)
171
172         result = {
173             'name': udoc.id,
174             'html_url': reverse('dochtml_view', args=[udoc.id,udoc.revision]),
175             'text_url': reverse('doctext_view', args=[udoc.id,udoc.revision]),
176             'dc_url': reverse('docdc_view', args=[udoc.id,udoc.revision]),
177             #'gallery_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 # Document Text View
209 #
210
211 XINCLUDE_REGEXP = r"""<(?:\w+:)?include\s+[^>]*?href=("|')wlrepo://(?P<link>[^\1]+?)\1\s*[^>]*?>"""
212 #
213 #
214 #
215 class DocumentTextHandler(BaseHandler):
216     allowed_methods = ('GET', 'PUT')
217
218     @hglibrary
219     def read(self, request, docid, revision, lib):
220         """Read document as raw text"""               
221         try:
222             if revision == 'latest':
223                 document = lib.document(docid)
224             else:
225                 document = lib.document_for_rev(revision)
226             
227             # TODO: some finer-grained access control
228             return document.data('xml')
229         except RevisionNotFound:
230             return response.EntityNotFound().django_response()
231
232     @hglibrary
233     def update(self, request, docid, revision, lib):
234         try:
235             data = request.PUT['contents']            
236
237             if request.PUT.has_key('message'):
238                 msg = u"$USER$ " + request.PUT['message']
239             else:
240                 msg = u"$AUTO$ XML content update."
241
242             current = lib.document(docid, request.user.username)
243             orig = lib.document_for_rev(revision)
244
245             if current != orig:
246                 return response.EntityConflict().django_response({
247                         "reason": "out-of-date",
248                         "provided_revision": orig.revision,
249                         "latest_revision": current.revision })
250
251             # try to find any Xinclude tags
252             includes = [m.groupdict()['link'] for m in (re.finditer(\
253                 XINCLUDE_REGEXP, data, flags=re.UNICODE) or []) ]
254
255             print "INCLUDES: ", includes
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                     lib._fileadd(resolve('parts'))
272
273                     # update the parts cache
274                     PartCache.update_cache(docid, current.owner,\
275                         stored_includes, includes)
276
277                 # now that the parts are ok, write xml
278                 f = lib._fileopen(resolve('xml'), 'w+')
279                 f.write(data.encode('utf-8'))
280                 f.close()
281
282             ndoc = None
283             ndoc = current.invoke_and_commit(\
284                 xml_update_action, lambda d: (msg, current.owner) )
285
286             try:
287                 # return the new revision number
288                 return response.SuccessAllOk().django_response({
289                     "document": ndoc.id,
290                     "subview": "xml",
291                     "previous_revision": current.revision,
292                     "updated_revision": ndoc.revision,
293                     "url": reverse("doctext_view", args=[ndoc.id, ndoc.revision])
294                 })
295             except Exception, e:
296                 if ndoc: lib._rollback()
297                 raise e        
298         except RevisionNotFound, e:
299             return response.EntityNotFound(mimetype="text/plain").\
300                 django_response(e.message)
301
302
303 #
304 # Dublin Core handlers
305 #
306 # @requires librarian
307 #
308 class DocumentDublinCoreHandler(BaseHandler):
309     allowed_methods = ('GET', 'PUT')
310
311     @hglibrary
312     def read(self, request, docid, revision, lib):
313         """Read document as raw text"""        
314         try:
315             if revision == 'latest':
316                 doc = lib.document(docid)
317             else:
318                 doc = lib.document_for_rev(revision)
319             
320             bookinfo = dcparser.BookInfo.from_string(doc.data('xml'))
321             return bookinfo.serialize()
322         except RevisionNotFound:
323             return response.EntityNotFound().django_response()
324
325     @hglibrary
326     def update(self, request, docid, revision, lib):
327         try:
328             bi_json = request.PUT['contents']            
329             if request.PUT.has_key('message'):
330                 msg = u"$USER$ " + request.PUT['message']
331             else:
332                 msg = u"$AUTO$ Dublin core update."
333
334             current = lib.document(docid, request.user.username)
335             orig = lib.document_for_rev(revision)
336
337             if current != orig:
338                 return response.EntityConflict().django_response({
339                         "reason": "out-of-date",
340                         "provided": orig.revision,
341                         "latest": current.revision })
342
343             xmldoc = parser.WLDocument.from_string(current.data('xml'))
344             document.book_info = dcparser.BookInfo.from_json(bi_json)
345
346             # zapisz
347             ndoc = current.quickwrite('xml', \
348                 document.serialize().encode('utf-8'),\
349                 message=msg, user=request.user.username)
350
351             try:
352                 # return the new revision number
353                 return {
354                     "document": ndoc.id,
355                     "subview": "dc",
356                     "previous_revision": current.revision,
357                     "updated_revision": ndoc.revision,
358                     "url": reverse("docdc_view", args=[ndoc.id, ndoc.revision])
359                 }
360             except Exception, e:
361                 if ndoc: lib._rollback()
362                 raise e
363         except RevisionNotFound:
364             return response.EntityNotFound().django_response()
365
366 class MergeHandler(BaseHandler):
367     allowed_methods = ('POST',)
368
369     @validate_form(forms.MergeRequestForm, 'POST')
370     @hglibrary
371     def create(self, request, form, docid, lib):
372         """Create a new document revision from the information provided by user"""
373
374         target_rev = form.cleaned_data['target_revision']
375
376         doc = lib.document(docid)
377         udoc = doc.take(request.user.username)
378
379         if target_rev == 'latest':
380             target_rev = udoc.revision
381
382         if str(udoc.revision) != target_rev:
383             # user think doesn't know he has an old version
384             # of his own branch.
385             
386             # Updating is teorericly ok, but we need would
387             # have to force a refresh. Sharing may be not safe,
388             # 'cause it doesn't always result in update.
389
390             # In other words, we can't lie about the resource's state
391             # So we should just yield and 'out-of-date' conflict
392             # and let the client ask again with updated info.
393
394             # NOTE: this could result in a race condition, when there
395             # are 2 instances of the same user editing the same document.
396             # Instance "A" trying to update, and instance "B" always changing
397             # the document right before "A". The anwser to this problem is
398             # for the "A" to request a merge from 'latest' and then
399             # check the parent revisions in response, if he actually
400             # merge from where he thinks he should. If not, the client SHOULD
401             # update his internal state.
402             return response.EntityConflict().django_response({
403                     "reason": "out-of-date",
404                     "provided": target_rev,
405                     "latest": udoc.revision })
406
407         if not request.user.has_perm('explorer.book.can_share'):
408             # User is not permitted to make a merge, right away
409             # So we instead create a pull request in the database
410             prq = PullRequest(
411                 comitter=request.user,
412                 document=docid,
413                 source_revision = str(udoc.revision),
414                 status="N",
415                 comment = form.cleaned_data['comment'] or '$AUTO$ Document shared.'
416             )
417
418             prq.save()
419             return response.RequestAccepted().django_response(\
420                 ticket_status=prq.status, \
421                 ticket_uri=reverse("pullrequest_view", args=[prq.id]) )
422
423         if form.cleaned_data['type'] == 'update':
424             # update is always performed from the file branch
425             # to the user branch
426             success, changed = udoc.update(request.user.username)
427
428         if form.cleaned_data['type'] == 'share':
429             success, changed = udoc.share(form.cleaned_data['comment'])
430
431         if not success:
432             return response.EntityConflict().django_response()
433
434         if not changed:
435             return response.SuccessNoContent().django_response()
436
437         new_udoc = udoc.latest()
438
439         return response.SuccessAllOk().django_response({
440             "name": udoc.id,
441             "parent_user_resivion": udoc.revision,
442             "parent_revision": doc.revision,
443             "revision": udoc.revision,
444         })