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