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