Document index displayed as a tree, not list.
[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 = current.invoke_and_commit(\
281                 xml_update_action, lambda d: (msg, current.owner) )
282
283             try:
284                 # return the new revision number
285                 return {
286                     "document": ndoc.id,
287                     "subview": "xml",
288                     "previous_revision": current.revision,
289                     "updated_revision": ndoc.revision
290                 }
291             except Exception, e:
292                 lib._rollback()
293                 raise e        
294         except RevisionNotFound, e:
295             return response.EntityNotFound().django_response(e)
296
297 #
298 # Dublin Core handlers
299 #
300 # @requires librarian
301 #
302 class DocumentDublinCoreHandler(BaseHandler):
303     allowed_methods = ('GET', 'PUT')
304
305     @hglibrary
306     def read(self, request, docid, revision, lib):
307         """Read document as raw text"""        
308         try:
309             if revision == 'latest':
310                 doc = lib.document(docid)
311             else:
312                 doc = lib.document_for_rev(revision)
313             
314             bookinfo = dcparser.BookInfo.from_string(doc.data('xml'))
315             return bookinfo.serialize()
316         except RevisionNotFound:
317             return response.EntityNotFound().django_response()
318
319     @hglibrary
320     def update(self, request, docid, revision, lib):
321         try:
322             bi_json = request.PUT['contents']            
323             if request.PUT.has_key('message'):
324                 msg = u"$USER$ " + request.PUT['message']
325             else:
326                 msg = u"$AUTO$ Dublin core update."
327
328             current = lib.document(docid, request.user.username)
329             orig = lib.document_for_rev(revision)
330
331             if current != orig:
332                 return response.EntityConflict().django_response({
333                         "reason": "out-of-date",
334                         "provided": orig.revision,
335                         "latest": current.revision })
336
337             xmldoc = parser.WLDocument.from_string(current.data('xml'))
338             document.book_info = dcparser.BookInfo.from_json(bi_json)
339
340             # zapisz
341             ndoc = current.quickwrite('xml', \
342                 document.serialize().encode('utf-8'),\
343                 message=msg, user=request.user.username)
344
345             try:
346                 # return the new revision number
347                 return {
348                     "document": ndoc.id,
349                     "subview": "dc",
350                     "previous_revision": current.revision,
351                     "updated_revision": ndoc.revision
352                 }
353             except Exception, e:
354                 lib._rollback()
355                 raise e
356         except RevisionNotFound:
357             return response.EntityNotFound().django_response()
358
359
360
361 class MergeHandler(BaseHandler):
362     allowed_methods = ('POST',)
363
364     @validate_form(forms.MergeRequestForm, 'POST')
365     @hglibrary
366     def create(self, request, form, docid, lib):
367         """Create a new document revision from the information provided by user"""
368
369         target_rev = form.cleaned_data['target_revision']
370
371         doc = lib.document(docid)
372         udoc = doc.take(request.user.username)
373
374         if target_rev == 'latest':
375             target_rev = udoc.revision
376
377         if str(udoc.revision) != target_rev:
378             # user think doesn't know he has an old version
379             # of his own branch.
380             
381             # Updating is teorericly ok, but we need would
382             # have to force a refresh. Sharing may be not safe,
383             # 'cause it doesn't always result in update.
384
385             # In other words, we can't lie about the resource's state
386             # So we should just yield and 'out-of-date' conflict
387             # and let the client ask again with updated info.
388
389             # NOTE: this could result in a race condition, when there
390             # are 2 instances of the same user editing the same document.
391             # Instance "A" trying to update, and instance "B" always changing
392             # the document right before "A". The anwser to this problem is
393             # for the "A" to request a merge from 'latest' and then
394             # check the parent revisions in response, if he actually
395             # merge from where he thinks he should. If not, the client SHOULD
396             # update his internal state.
397             return response.EntityConflict().django_response({
398                     "reason": "out-of-date",
399                     "provided": target_rev,
400                     "latest": udoc.revision })
401
402         if not request.user.has_perm('explorer.book.can_share'):
403             # User is not permitted to make a merge, right away
404             # So we instead create a pull request in the database
405             prq = PullRequest(
406                 comitter=request.user,
407                 document=docid,
408                 source_revision = str(udoc.revision),
409                 status="N",
410                 comment = form.cleaned_data['comment'] or '$AUTO$ Document shared.'
411             )
412
413             prq.save()
414             return response.RequestAccepted().django_response(\
415                 ticket_status=prq.status, \
416                 ticket_uri=reverse("pullrequest_view", args=[prq.id]) )
417
418         if form.cleaned_data['type'] == 'update':
419             # update is always performed from the file branch
420             # to the user branch
421             success, changed = udoc.update(request.user.username)
422
423         if form.cleaned_data['type'] == 'share':
424             success, changed = udoc.share(form.cleaned_data['comment'])
425
426         if not success:
427             return response.EntityConflict().django_response()
428
429         if not changed:
430             return response.SuccessNoContent().django_response()
431
432         new_udoc = udoc.latest()
433
434         return response.SuccessAllOk().django_response({
435             "name": udoc.id,
436             "parent_user_resivion": udoc.revision,
437             "parent_revision": doc.revision,
438             "revision": udoc.revision,
439         })