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