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