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