c5c52564b66d8d3f58797d1f57e6e9f8b9c7c22b
[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                 '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 os.listdir(dirpath):
259                 file = file.decode('utf-8')
260                 
261                 log.info(file)
262                 name, ext = os.path.splitext(os.path.basename(file))
263
264                 if ext.lower() not in [u'.png', u'.jpeg', u'.jpg']:
265                     log.info("Ignoring: %s %s", name, ext)
266                     continue
267
268                 url = settings.MEDIA_URL + assoc.subpath + u'/' + file;
269                 gallery['pages'].append(url)
270
271             gallery['pages'].sort()
272             galleries.append(gallery)
273
274         return galleries                      
275
276 #
277 # Document Text View
278 #
279
280 XINCLUDE_REGEXP = r"""<(?:\w+:)?include\s+[^>]*?href=("|')wlrepo://(?P<link>[^\1]+?)\1\s*[^>]*?>"""
281 #
282 #
283 #
284 class DocumentTextHandler(BaseHandler):
285     allowed_methods = ('GET', 'POST')
286
287     @hglibrary
288     def read(self, request, docid, lib):
289         """Read document as raw text"""
290         revision = request.GET.get('revision', 'latest')
291         try:
292             if revision == 'latest':
293                 document = lib.document(docid)
294             else:
295                 document = lib.document_for_rev(revision)
296
297             if document.id != docid:
298                 return response.BadRequest().django_response({'reason': 'name-mismatch',
299                     'message': 'Provided revision is not valid for this document'})
300             
301             # TODO: some finer-grained access control
302             return document.data('xml')
303         except (EntryNotFound, RevisionNotFound), e:
304             return response.EntityNotFound().django_response({
305                 'exception': type(e), 'message': e.message})
306
307     @hglibrary
308     def create(self, request, docid, lib):
309         try:
310             data = request.POST['contents']
311             revision = request.POST['revision']
312
313             if request.POST.has_key('message'):
314                 msg = u"$USER$ " + request.POST['message']
315             else:
316                 msg = u"$AUTO$ XML content update."
317
318             current = lib.document(docid, request.user.username)
319             orig = lib.document_for_rev(revision)
320
321             if current != orig:
322                 return response.EntityConflict().django_response({
323                         "reason": "out-of-date",
324                         "provided_revision": orig.revision,
325                         "latest_revision": current.revision })
326
327             # try to find any Xinclude tags
328             includes = [m.groupdict()['link'] for m in (re.finditer(\
329                 XINCLUDE_REGEXP, data, flags=re.UNICODE) or []) ]
330
331             log.info("INCLUDES: %s", includes)
332
333             # TODO: provide useful routines to make this simpler
334             def xml_update_action(lib, resolve):
335                 try:
336                     f = lib._fileopen(resolve('parts'), 'r')
337                     stored_includes = json.loads(f.read())
338                     f.close()
339                 except:
340                     stored_includes = []
341                 
342                 if stored_includes != includes:
343                     f = lib._fileopen(resolve('parts'), 'w+')
344                     f.write(json.dumps(includes))
345                     f.close()
346
347                     lib._fileadd(resolve('parts'))
348
349                     # update the parts cache
350                     PartCache.update_cache(docid, current.owner,\
351                         stored_includes, includes)
352
353                 # now that the parts are ok, write xml
354                 f = lib._fileopen(resolve('xml'), 'w+')
355                 f.write(data.encode('utf-8'))
356                 f.close()
357
358             ndoc = None
359             ndoc = current.invoke_and_commit(\
360                 xml_update_action, lambda d: (msg, current.owner) )
361
362             try:
363                 # return the new revision number
364                 return response.SuccessAllOk().django_response({
365                     "document": ndoc.id,
366                     "subview": "xml",
367                     "previous_revision": current.revision,
368                     "revision": ndoc.revision,
369                     'timestamp': ndoc.revision.timestamp,
370                     "url": reverse("doctext_view", args=[ndoc.id])
371                 })
372             except Exception, e:
373                 if ndoc: lib._rollback()
374                 raise e        
375         except RevisionNotFound, e:
376             return response.EntityNotFound(mimetype="text/plain").\
377                 django_response(e.message)
378
379
380 #
381 # Dublin Core handlers
382 #
383 # @requires librarian
384 #
385 class DocumentDublinCoreHandler(BaseHandler):
386     allowed_methods = ('GET', 'POST')
387
388     @hglibrary
389     def read(self, request, docid, lib):
390         """Read document as raw text"""        
391         try:
392             revision = request.GET.get('revision', 'latest')
393
394             if revision == 'latest':
395                 doc = lib.document(docid)
396             else:
397                 doc = lib.document_for_rev(revision)
398
399
400             if document.id != docid:
401                 return response.BadRequest().django_response({'reason': 'name-mismatch',
402                     'message': 'Provided revision is not valid for this document'})
403             
404             bookinfo = dcparser.BookInfo.from_string(doc.data('xml'))
405             return bookinfo.serialize()
406         except (EntryNotFound, RevisionNotFound), e:
407             return response.EntityNotFound().django_response({
408                 'exception': type(e), 'message': e.message})
409
410     @hglibrary
411     def create(self, request, docid, lib):
412         try:
413             bi_json = request.POST['contents']
414             revision = request.POST['revision']
415             
416             if request.POST.has_key('message'):
417                 msg = u"$USER$ " + request.PUT['message']
418             else:
419                 msg = u"$AUTO$ Dublin core update."
420
421             current = lib.document(docid, request.user.username)
422             orig = lib.document_for_rev(revision)
423
424             if current != orig:
425                 return response.EntityConflict().django_response({
426                         "reason": "out-of-date",
427                         "provided": orig.revision,
428                         "latest": current.revision })
429
430             xmldoc = parser.WLDocument.from_string(current.data('xml'))
431             document.book_info = dcparser.BookInfo.from_json(bi_json)
432
433             # zapisz
434             ndoc = current.quickwrite('xml', \
435                 document.serialize().encode('utf-8'),\
436                 message=msg, user=request.user.username)
437
438             try:
439                 # return the new revision number
440                 return {
441                     "document": ndoc.id,
442                     "subview": "dc",
443                     "previous_revision": current.revision,
444                     "revision": ndoc.revision,
445                     'timestamp': ndoc.revision.timestamp,
446                     "url": reverse("docdc_view", args=[ndoc.id])
447                 }
448             except Exception, e:
449                 if ndoc: lib._rollback()
450                 raise e
451         except RevisionNotFound:
452             return response.EntityNotFound().django_response()
453
454 class MergeHandler(BaseHandler):
455     allowed_methods = ('POST',)
456
457     @validate_form(forms.MergeRequestForm, 'POST')
458     @hglibrary
459     def create(self, request, form, docid, lib):
460         """Create a new document revision from the information provided by user"""
461
462         target_rev = form.cleaned_data['target_revision']
463
464         doc = lib.document(docid)
465         udoc = doc.take(request.user.username)
466
467         if target_rev == 'latest':
468             target_rev = udoc.revision
469
470         if str(udoc.revision) != target_rev:
471             # user think doesn't know he has an old version
472             # of his own branch.
473             
474             # Updating is teorericly ok, but we need would
475             # have to force a refresh. Sharing may be not safe,
476             # 'cause it doesn't always result in update.
477
478             # In other words, we can't lie about the resource's state
479             # So we should just yield and 'out-of-date' conflict
480             # and let the client ask again with updated info.
481
482             # NOTE: this could result in a race condition, when there
483             # are 2 instances of the same user editing the same document.
484             # Instance "A" trying to update, and instance "B" always changing
485             # the document right before "A". The anwser to this problem is
486             # for the "A" to request a merge from 'latest' and then
487             # check the parent revisions in response, if he actually
488             # merge from where he thinks he should. If not, the client SHOULD
489             # update his internal state.
490             return response.EntityConflict().django_response({
491                     "reason": "out-of-date",
492                     "provided": target_rev,
493                     "latest": udoc.revision })
494
495         if not request.user.has_perm('explorer.book.can_share'):
496             # User is not permitted to make a merge, right away
497             # So we instead create a pull request in the database
498             prq = PullRequest(
499                 comitter=request.user,
500                 document=docid,
501                 source_revision = str(udoc.revision),
502                 status="N",
503                 comment = form.cleaned_data['message'] or '$AUTO$ Document shared.'
504             )
505
506             prq.save()
507             return response.RequestAccepted().django_response(\
508                 ticket_status=prq.status, \
509                 ticket_uri=reverse("pullrequest_view", args=[prq.id]) )
510
511         if form.cleaned_data['type'] == 'update':
512             # update is always performed from the file branch
513             # to the user branch
514             success, changed = udoc.update(request.user.username)
515
516         if form.cleaned_data['type'] == 'share':
517             success, changed = udoc.share(form.cleaned_data['message'])
518
519         if not success:
520             return response.EntityConflict().django_response({
521                 'reason': 'merge-failure',
522             })
523
524         if not changed:
525             return response.SuccessNoContent().django_response()
526
527         nudoc = udoc.latest()
528
529         return response.SuccessAllOk().django_response({
530             "name": nudoc.id,
531             "parent_user_resivion": udoc.revision,
532             "parent_revision": doc.revision,
533             "revision": nudoc.revision,
534             'timestamp': nudoc.revision.timestamp,
535         })