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