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