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