The actual commit.
[redakcja.git] / apps / api / handlers / library_handlers.py
1 import os.path
2 # -*- encoding: utf-8 -*-
3
4 __author__= "Ɓukasz Rekucki"
5 __date__ = "$2009-09-25 15:49:50$"
6 __doc__ = "Module documentation."
7
8 from piston.handler import BaseHandler, AnonymousBaseHandler
9
10 import re
11 from datetime import date
12
13 from django.core.urlresolvers import reverse
14 from django.utils import simplejson as json
15
16 import librarian
17 import librarian.html
18 from librarian import dcparser
19
20 from wlrepo import *
21 from explorer.models import PullRequest, GalleryForDocument
22
23 # internal imports
24 import api.forms as forms
25 import api.response as response
26 from api.utils import validate_form, hglibrary, natural_order
27 from api.models import PartCache
28
29 #
30 import settings
31
32 #
33 # Document List Handlers
34 #
35 class BasicLibraryHandler(AnonymousBaseHandler):
36     allowed_methods = ('GET',)
37
38     @hglibrary
39     def read(self, request, lib):
40         """Return the list of documents."""       
41         document_list = [{
42             'url': reverse('document_view', args=[docid]),
43             'name': docid } for docid in lib.documents() ]
44
45         return {'documents' : document_list}
46         
47
48 class LibraryHandler(BaseHandler):
49     allowed_methods = ('GET', 'POST')
50     anonymous = BasicLibraryHandler
51
52     @hglibrary
53     def read(self, request, lib):
54         """Return the list of documents."""
55
56         documents = {}
57         
58         for docid in lib.documents():
59             docid = docid.decode('utf-8')
60             documents[docid] = {
61                 'url': reverse('document_view', args=[docid]),
62                 'name': docid,
63                 'parts': []
64             }
65
66         parts = PartCache.objects.defer('part_id')\
67             .values_list('part_id', 'document_id').distinct()
68        
69         document_tree = dict(documents)
70
71         for part, docid in parts:
72             # this way, we won't display broken links
73             if not documents.has_key(part):
74                 print "NOT FOUND:", part
75                 continue
76
77             parent = documents[docid]
78             child = documents[part]
79
80             # not top-level anymore
81             document_tree.pop(part)
82             parent['parts'].append(child)
83
84         # sort the right way
85         
86
87         for doc in documents.itervalues():
88             doc['parts'].sort(key=natural_order(lambda d: d['name']))
89             
90         return {'documents': sorted(document_tree.itervalues(),
91             key=natural_order(lambda d: d['name']) ) }
92
93     @validate_form(forms.DocumentUploadForm, 'POST')
94     @hglibrary
95     def create(self, request, form, lib):
96         """Create a new document."""       
97
98         if form.cleaned_data['ocr_data']:
99             data = form.cleaned_data['ocr_data']
100         else:            
101             data = request.FILES['ocr_file'].read().decode('utf-8')
102
103         if form.cleaned_data['generate_dc']:
104             data = librarian.wrap_text(data, unicode(date.today()))
105
106         docid = form.cleaned_data['bookname']
107
108         try:
109             lock = lib.lock()            
110             try:
111                 print "DOCID", docid                
112                 doc = lib.document_create(docid)
113                 # document created, but no content yet
114
115                 try:
116                     doc = doc.quickwrite('xml', data.encode('utf-8'),
117                         '$AUTO$ XML data uploaded.', user=request.user.username)
118                 except Exception,e:
119                     # rollback branch creation
120                     lib._rollback()
121                     raise LibraryException("Exception occured:" + repr(e))
122
123                 url = reverse('document_view', args=[doc.id])
124
125                 return response.EntityCreated().django_response(\
126                     body = {
127                         'url': url,
128                         'name': doc.id,
129                         'revision': doc.revision },
130                     url = url )            
131             finally:
132                 lock.release()
133         except LibraryException, e:
134             return response.InternalError().django_response(\
135                 {'exception': repr(e) })                
136         except DocumentAlreadyExists:
137             # Document is already there
138             return response.EntityConflict().django_response(\
139                 {"reason": "Document %s already exists." % docid})
140
141 #
142 # Document Handlers
143 #
144 class BasicDocumentHandler(AnonymousBaseHandler):
145     allowed_methods = ('GET',)
146
147     @hglibrary
148     def read(self, request, docid, lib):
149         try:    
150             doc = lib.document(docid)
151         except RevisionNotFound:
152             return rc.NOT_FOUND
153
154         result = {
155             'name': doc.id,
156             'html_url': reverse('dochtml_view', args=[doc.id,doc.revision]),
157             'text_url': reverse('doctext_view', args=[doc.id,doc.revision]),
158             'dc_url': reverse('docdc_view', args=[doc.id,doc.revision]),
159             'public_revision': doc.revision,
160         }
161
162         return result
163
164 #
165 # Document Meta Data
166 #
167 class DocumentHandler(BaseHandler):
168     allowed_methods = ('GET', 'PUT')
169     anonymous = BasicDocumentHandler
170
171     @hglibrary
172     def read(self, request, docid, lib):
173         """Read document's meta data"""       
174         try:
175             doc = lib.document(docid)
176             udoc = doc.take(request.user.username)
177         except RevisionNotFound, e:
178             return response.EntityNotFound().django_response({
179                 'exception': type(e), 'message': e.message})
180
181         # is_shared = udoc.ancestorof(doc)
182         # is_uptodate = is_shared or shared.ancestorof(document)
183
184         result = {
185             'name': udoc.id,
186             'html_url': reverse('dochtml_view', args=[udoc.id,udoc.revision]),
187             'text_url': reverse('doctext_view', args=[udoc.id,udoc.revision]),
188             'dc_url': reverse('docdc_view', args=[udoc.id,udoc.revision]),
189             'gallery_url': reverse('docgallery_view', args=[udoc.id]),
190             'merge_url': reverse('docmerge_view', args=[udoc.id]),
191             'user_revision': udoc.revision,
192             'user_timestamp': udoc.revision.timestamp,
193             'public_revision': doc.revision,
194             'public_timestamp': doc.revision.timestamp,
195         }       
196
197         return result
198
199     @hglibrary
200     def update(self, request, docid, lib):
201         """Update information about the document, like display not"""
202         return
203 #
204 #
205 #
206 class DocumentHTMLHandler(BaseHandler):
207     allowed_methods = ('GET')
208
209     @hglibrary
210     def read(self, request, docid, revision, lib):
211         """Read document as html text"""
212         try:
213             if revision == 'latest':
214                 document = lib.document(docid)
215             else:
216                 document = lib.document_for_rev(revision)
217
218             if document.id != docid:
219                 return response.BadRequest().django_response({'reason': 'name-mismatch',
220                     'message': 'Provided revision refers, to document "%s", but provided "%s"' % (document.id, docid) })
221
222             return librarian.html.transform(document.data('xml'), is_file=False)
223         except (EntryNotFound, RevisionNotFound), e:
224             return response.EntityNotFound().django_response({
225                 'exception': type(e), 'message': e.message})
226
227
228 #
229 # Image Gallery
230 #
231 from django.core.files.storage import FileSystemStorage
232
233 class DocumentGalleryHandler(BaseHandler):
234     allowed_methods = ('GET')
235     
236     def read(self, request, docid):
237         """Read meta-data about scans for gallery of this document."""
238         galleries = []
239
240         for assoc in GalleryForDocument.objects.filter(document=docid):
241             dirpath = os.path.join(settings.MEDIA_ROOT, assoc.subpath)
242
243             if not os.path.isdir(dirpath):
244                 print u"[WARNING]: missing gallery %s" % dirpath
245                 continue
246
247             gallery = {'name': assoc.name, 'pages': []}
248             
249             for file in sorted(os.listdir(dirpath), key=natural_order()):
250                 print file
251                 name, ext = os.path.splitext(os.path.basename(file))
252
253                 if ext.lower() not in ['.png', '.jpeg', '.jpg']:
254                     print "Ignoring:", name, ext
255                     continue
256
257                 url = settings.MEDIA_URL + assoc.subpath + u'/' + file.decode('utf-8');
258                 gallery['pages'].append(url)
259                 
260             galleries.append(gallery)
261
262         return galleries                      
263
264 #
265 # Document Text View
266 #
267
268 XINCLUDE_REGEXP = r"""<(?:\w+:)?include\s+[^>]*?href=("|')wlrepo://(?P<link>[^\1]+?)\1\s*[^>]*?>"""
269 #
270 #
271 #
272 class DocumentTextHandler(BaseHandler):
273     allowed_methods = ('GET', 'PUT')
274
275     @hglibrary
276     def read(self, request, docid, revision, lib):
277         """Read document as raw text"""               
278         try:
279             if revision == 'latest':
280                 document = lib.document(docid)
281             else:
282                 document = lib.document_for_rev(revision)
283
284             if document.id != docid:
285                 return response.BadRequest().django_response({'reason': 'name-mismatch',
286                     'message': 'Provided revision is not valid for this document'})
287             
288             # TODO: some finer-grained access control
289             return document.data('xml')
290         except (EntryNotFound, RevisionNotFound), e:
291             return response.EntityNotFound().django_response({
292                 'exception': type(e), 'message': e.message})
293
294     @hglibrary
295     def update(self, request, docid, revision, lib):
296         try:
297             data = request.PUT['contents']            
298
299             if request.PUT.has_key('message'):
300                 msg = u"$USER$ " + request.PUT['message']
301             else:
302                 msg = u"$AUTO$ XML content update."
303
304             current = lib.document(docid, request.user.username)
305             orig = lib.document_for_rev(revision)
306
307             if current != orig:
308                 return response.EntityConflict().django_response({
309                         "reason": "out-of-date",
310                         "provided_revision": orig.revision,
311                         "latest_revision": current.revision })
312
313             # try to find any Xinclude tags
314             includes = [m.groupdict()['link'] for m in (re.finditer(\
315                 XINCLUDE_REGEXP, data, flags=re.UNICODE) or []) ]
316
317             print "INCLUDES: ", includes
318
319             # TODO: provide useful routines to make this simpler
320             def xml_update_action(lib, resolve):
321                 try:
322                     f = lib._fileopen(resolve('parts'), 'r')
323                     stored_includes = json.loads(f.read())
324                     f.close()
325                 except:
326                     stored_includes = []
327                 
328                 if stored_includes != includes:
329                     f = lib._fileopen(resolve('parts'), 'w+')
330                     f.write(json.dumps(includes))
331                     f.close()
332
333                     lib._fileadd(resolve('parts'))
334
335                     # update the parts cache
336                     PartCache.update_cache(docid, current.owner,\
337                         stored_includes, includes)
338
339                 # now that the parts are ok, write xml
340                 f = lib._fileopen(resolve('xml'), 'w+')
341                 f.write(data.encode('utf-8'))
342                 f.close()
343
344             ndoc = None
345             ndoc = current.invoke_and_commit(\
346                 xml_update_action, lambda d: (msg, current.owner) )
347
348             try:
349                 # return the new revision number
350                 return response.SuccessAllOk().django_response({
351                     "document": ndoc.id,
352                     "subview": "xml",
353                     "previous_revision": current.revision,
354                     "revision": ndoc.revision,
355                     'timestamp': ndoc.revision.timestamp,
356                     "url": reverse("doctext_view", args=[ndoc.id, ndoc.revision])
357                 })
358             except Exception, e:
359                 if ndoc: lib._rollback()
360                 raise e        
361         except RevisionNotFound, e:
362             return response.EntityNotFound(mimetype="text/plain").\
363                 django_response(e.message)
364
365
366 #
367 # Dublin Core handlers
368 #
369 # @requires librarian
370 #
371 class DocumentDublinCoreHandler(BaseHandler):
372     allowed_methods = ('GET', 'PUT')
373
374     @hglibrary
375     def read(self, request, docid, revision, lib):
376         """Read document as raw text"""        
377         try:
378             if revision == 'latest':
379                 doc = lib.document(docid)
380             else:
381                 doc = lib.document_for_rev(revision)
382
383
384             if document.id != docid:
385                 return response.BadRequest().django_response({'reason': 'name-mismatch',
386                     'message': 'Provided revision is not valid for this document'})
387             
388             bookinfo = dcparser.BookInfo.from_string(doc.data('xml'))
389             return bookinfo.serialize()
390         except (EntryNotFound, RevisionNotFound), e:
391             return response.EntityNotFound().django_response({
392                 'exception': type(e), 'message': e.message})
393
394     @hglibrary
395     def update(self, request, docid, revision, lib):
396         try:
397             bi_json = request.PUT['contents']            
398             if request.PUT.has_key('message'):
399                 msg = u"$USER$ " + request.PUT['message']
400             else:
401                 msg = u"$AUTO$ Dublin core update."
402
403             current = lib.document(docid, request.user.username)
404             orig = lib.document_for_rev(revision)
405
406             if current != orig:
407                 return response.EntityConflict().django_response({
408                         "reason": "out-of-date",
409                         "provided": orig.revision,
410                         "latest": current.revision })
411
412             xmldoc = parser.WLDocument.from_string(current.data('xml'))
413             document.book_info = dcparser.BookInfo.from_json(bi_json)
414
415             # zapisz
416             ndoc = current.quickwrite('xml', \
417                 document.serialize().encode('utf-8'),\
418                 message=msg, user=request.user.username)
419
420             try:
421                 # return the new revision number
422                 return {
423                     "document": ndoc.id,
424                     "subview": "dc",
425                     "previous_revision": current.revision,
426                     "revision": ndoc.revision,
427                     'timestamp': ndoc.revision.timestamp,
428                     "url": reverse("docdc_view", args=[ndoc.id, ndoc.revision])
429                 }
430             except Exception, e:
431                 if ndoc: lib._rollback()
432                 raise e
433         except RevisionNotFound:
434             return response.EntityNotFound().django_response()
435
436 class MergeHandler(BaseHandler):
437     allowed_methods = ('POST',)
438
439     @validate_form(forms.MergeRequestForm, 'POST')
440     @hglibrary
441     def create(self, request, form, docid, lib):
442         """Create a new document revision from the information provided by user"""
443
444         target_rev = form.cleaned_data['target_revision']
445
446         doc = lib.document(docid)
447         udoc = doc.take(request.user.username)
448
449         if target_rev == 'latest':
450             target_rev = udoc.revision
451
452         if str(udoc.revision) != target_rev:
453             # user think doesn't know he has an old version
454             # of his own branch.
455             
456             # Updating is teorericly ok, but we need would
457             # have to force a refresh. Sharing may be not safe,
458             # 'cause it doesn't always result in update.
459
460             # In other words, we can't lie about the resource's state
461             # So we should just yield and 'out-of-date' conflict
462             # and let the client ask again with updated info.
463
464             # NOTE: this could result in a race condition, when there
465             # are 2 instances of the same user editing the same document.
466             # Instance "A" trying to update, and instance "B" always changing
467             # the document right before "A". The anwser to this problem is
468             # for the "A" to request a merge from 'latest' and then
469             # check the parent revisions in response, if he actually
470             # merge from where he thinks he should. If not, the client SHOULD
471             # update his internal state.
472             return response.EntityConflict().django_response({
473                     "reason": "out-of-date",
474                     "provided": target_rev,
475                     "latest": udoc.revision })
476
477         if not request.user.has_perm('explorer.book.can_share'):
478             # User is not permitted to make a merge, right away
479             # So we instead create a pull request in the database
480             prq = PullRequest(
481                 comitter=request.user,
482                 document=docid,
483                 source_revision = str(udoc.revision),
484                 status="N",
485                 comment = form.cleaned_data['message'] or '$AUTO$ Document shared.'
486             )
487
488             prq.save()
489             return response.RequestAccepted().django_response(\
490                 ticket_status=prq.status, \
491                 ticket_uri=reverse("pullrequest_view", args=[prq.id]) )
492
493         if form.cleaned_data['type'] == 'update':
494             # update is always performed from the file branch
495             # to the user branch
496             success, changed = udoc.update(request.user.username)
497
498         if form.cleaned_data['type'] == 'share':
499             success, changed = udoc.share(form.cleaned_data['message'])
500
501         if not success:
502             return response.EntityConflict().django_response({})
503
504         if not changed:
505             return response.SuccessNoContent().django_response()
506
507         new_udoc = udoc.latest()
508
509         return response.SuccessAllOk().django_response({
510             "name": udoc.id,
511             "parent_user_resivion": udoc.revision,
512             "parent_revision": doc.revision,
513             "revision": ndoc.revision,
514             'timestamp': ndoc.revision.timestamp,
515         })