Łatwiejsze wybieranie galerii dla dokumentów w interfejsie administracyjnym.
[redakcja.git] / apps / api / handlers / library_handlers.py
1 # -*- encoding: utf-8 -*-
2 import os.path
3
4 import logging
5 log = logging.getLogger('platforma.api.library')
6
7 from piston.handler import BaseHandler, AnonymousBaseHandler
8
9 from datetime import date
10
11 from django.core.urlresolvers import reverse
12 from django.db import IntegrityError
13
14 import librarian
15 import librarian.html
16 import difflib
17
18 from wlrepo import *
19 from explorer.models import GalleryForDocument
20
21 # internal imports
22 import api.forms as forms
23 import api.response as response
24 from api.utils import validate_form, hglibrary, natural_order
25 from api.models import PartCache, PullRequest
26
27 from pygments import highlight
28 from pygments.lexers import DiffLexer
29 from pygments.formatters import HtmlFormatter
30
31 #
32 import settings
33
34
35 def is_prq(username):
36     return username.startswith('$prq-')
37
38 def prq_for_user(username):
39     try:
40         return PullRequest.objects.get(id=int(username[5:]))
41     except:
42         return None
43
44 def check_user(request, user):
45     log.info("user: %r, perm: %r" % (request.user, request.user.get_all_permissions()) )
46     #pull request
47     if is_prq(user):
48         if not request.user.has_perm('api.view_prq'):
49             yield response.AccessDenied().django_response({
50                 'reason': 'access-denied',
51                 'message': "You don't have enough priviliges to view pull requests."
52             })
53     # other users
54     elif request.user.username != user:
55         if not request.user.has_perm('api.view_other_document'):
56             yield response.AccessDenied().django_response({
57                 'reason': 'access-denied',
58                 'message': "You don't have enough priviliges to view other people's document."
59             })
60     pass
61
62 #
63 # Document List Handlers
64 #
65 # TODO: security check
66 class BasicLibraryHandler(AnonymousBaseHandler):
67     allowed_methods = ('GET',)
68
69     @hglibrary
70     def read(self, request, lib):
71         """Return the list of documents."""       
72         document_list = [{
73             'url': reverse('document_view', args=[docid]),
74             'name': docid } for docid in lib.documents() ]
75         return {'documents' : document_list}
76         
77 #
78 # This handler controlls the document collection
79 #
80 class LibraryHandler(BaseHandler):
81     allowed_methods = ('GET', 'POST')
82     anonymous = BasicLibraryHandler
83
84     @hglibrary
85     def read(self, request, lib):
86         """Return the list of documents."""
87
88         documents = {}
89         
90         for docid in lib.documents():            
91             documents[docid] = {
92                 'url': reverse('document_view', args=[docid]),
93                 'name': docid,
94                 'parts': []
95             }
96
97         parts = PartCache.objects.defer('part_id')\
98             .values_list('part_id', 'document_id').distinct()
99        
100         document_tree = dict(documents)
101
102         for part, docid in parts:
103             # this way, we won't display broken links
104             if not documents.has_key(part):
105                 log.info("NOT FOUND: %s", part)
106                 continue
107
108             parent = documents[docid]
109             child = documents[part]
110
111             # not top-level anymore
112             document_tree.pop(part)
113             parent['parts'].append(child)
114         
115         for doc in documents.itervalues():
116             doc['parts'].sort(key=natural_order(lambda d: d['name']))
117             
118         return {'documents': sorted(document_tree.itervalues(),
119             key=natural_order(lambda d: d['name']) ) }
120
121
122     @validate_form(forms.DocumentUploadForm, 'POST')
123     @hglibrary
124     def create(self, request, form, lib):
125         """Create a new document."""       
126
127         if form.cleaned_data['ocr_data']:
128             data = form.cleaned_data['ocr_data']
129         else:            
130             data = request.FILES['ocr_file'].read().decode('utf-8')
131
132         if data is None:
133             return response.BadRequest().django_response('You must pass ocr_data or ocr_file.')
134
135         if form.cleaned_data['generate_dc']:
136             data = librarian.wrap_text(data, unicode(date.today()))
137
138         docid = form.cleaned_data['bookname']
139
140         try:
141             lock = lib.lock()            
142             try:
143                 log.info("DOCID %s", docid)
144                 doc = lib.document_create(docid)
145                 # document created, but no content yet
146                 try:
147                     doc = doc.quickwrite('xml', data.encode('utf-8'),
148                         '$AUTO$ XML data uploaded.', user=request.user.username)
149                 except Exception,e:
150                     import traceback
151                     # rollback branch creation
152                     lib._rollback()
153                     raise LibraryException(traceback.format_exc())
154
155                 url = reverse('document_view', args=[doc.id])
156
157                 return response.EntityCreated().django_response(\
158                     body = {
159                         'url': url,
160                         'name': doc.id,
161                         'revision': doc.revision },
162                     url = url )            
163             finally:
164                 lock.release()
165         except LibraryException, e:
166             import traceback
167             return response.InternalError().django_response({
168                 "reason": traceback.format_exc()
169             })
170         except DocumentAlreadyExists:
171             # Document is already there
172             return response.EntityConflict().django_response({
173                 "reason": "already-exists",
174                 "message": "Document already exists." % docid
175             })
176
177 #
178 # Document Handlers
179 #
180 class DiffHandler(BaseHandler):
181     allowed_methods = ('GET',)
182     
183     @hglibrary
184     def read(self, request, docid, lib):
185         '''Return diff between source_revision and target_revision)'''        
186         revision = request.GET.get('revision')
187         if not revision:
188             return ''
189         source_document = lib.document(docid)
190         target_document = lib.document_for_revision(revision)
191         print source_document, target_document
192         
193         diff = difflib.unified_diff(
194             source_document.data('xml').splitlines(True),
195             target_document.data('xml').splitlines(True),
196             'source',
197             'target')
198         
199         s =  ''.join(list(diff))
200         return highlight(s, DiffLexer(), HtmlFormatter(cssclass="pastie"))
201
202
203 #
204 # Document Meta Data
205 #
206 class DocumentHandler(BaseHandler):
207     allowed_methods = ('GET', 'PUT')
208
209     @validate_form(forms.DocumentRetrieveForm, 'GET')
210     @hglibrary
211     def read(self, request, form, docid, lib):
212         """Read document's meta data"""       
213         log.info(u"User '%s' wants to edit %s(%s) as %s" % \
214             (request.user.username, docid, form.cleaned_data['revision'], form.cleaned_data['user']) )
215
216         user = form.cleaned_data['user'] or request.user.username
217         rev = form.cleaned_data['revision'] or 'latest'
218
219         for error in check_user(request, user):
220             return error
221             
222         try:
223             doc = lib.document(docid, user, rev=rev)
224         except RevisionMismatch, e:
225             # the document exists, but the revision is bad
226             return response.EntityNotFound().django_response({
227                 'reason': 'revision-mismatch',
228                 'message': e.message,
229                 'docid': docid,
230                 'user': user,
231             })
232         except RevisionNotFound, e:
233             # the user doesn't have this document checked out
234             # or some other weird error occured
235             # try to do the checkout
236             try:
237                 if user == request.user.username:
238                     mdoc = lib.document(docid)
239                     doc = mdoc.take(user)
240                 elif is_prq(user):
241                     prq = prq_for_user(user)
242                     # commiter's document
243                     prq_doc = lib.document_for_revision(prq.source_revision)
244                     doc = prq_doc.take(user)
245                 else:
246                     return response.EntityNotFound().django_response({
247                         'reason': 'document-not-found',
248                         'message': e.message,
249                         'docid': docid,
250                         'user': user,
251                     })
252             except RevisionNotFound, e:
253                 return response.EntityNotFound().django_response({
254                     'reason': 'document-not-found',
255                     'message': e.message,
256                     'docid': docid,
257                     'user': user
258                 })
259
260         return {
261             'name': doc.id,
262             'user': user,
263             'html_url': reverse('dochtml_view', args=[doc.id]),
264             'text_url': reverse('doctext_view', args=[doc.id]),
265             # 'dc_url': reverse('docdc_view', args=[doc.id]),
266             'gallery_url': reverse('docgallery_view', args=[doc.id]),
267             'merge_url': reverse('docmerge_view', args=[doc.id]),
268             'revision': doc.revision,
269             'timestamp': doc.revision.timestamp,
270             # 'public_revision': doc.revision,
271             # 'public_timestamp': doc.revision.timestamp,
272         }   
273
274     
275 #    @hglibrary
276 #    def update(self, request, docid, lib):
277 #        """Update information about the document, like display not"""
278 #        return
279 #
280 #
281 #
282 class DocumentHTMLHandler(BaseHandler):
283     allowed_methods = ('GET')
284
285     @validate_form(forms.DocumentRetrieveForm, 'GET')
286     @hglibrary
287     def read(self, request, form, docid, lib, stylesheet='full'):
288         """Read document as html text"""
289         try:
290             revision = form.cleaned_data['revision']
291             user = form.cleaned_data['user'] or request.user.username
292             document = lib.document_for_revision(revision)
293
294             if document.id != docid:
295                 return response.BadRequest().django_response({
296                     'reason': 'name-mismatch',
297                     'message': 'Provided revision is not valid for this document'
298                 })
299
300             if document.owner != user:
301                 return response.BadRequest().django_response({
302                     'reason': 'user-mismatch',
303                     'message': "Provided revision doesn't belong to user %s" % user
304                 })
305
306             for error in check_user(request, user):
307                 return error
308
309             return librarian.html.transform(document.data('xml'), is_file=False, \
310                 parse_dublincore=False, stylesheet=stylesheet,\
311                 options={
312                     "with-paths": 'boolean(1)',                    
313                 })
314                 
315         except (EntryNotFound, RevisionNotFound), e:
316             return response.EntityNotFound().django_response({
317                 'reason': 'not-found', 'message': e.message})
318         except librarian.ValidationError, e:
319             return response.InternalError().django_response({
320                 'reason': 'xml-non-valid', 'message': e.message or u''})
321         except librarian.ParseError, e:
322             return response.InternalError().django_response({
323                 'reason': 'xml-parse-error', 'message': e.message or u'' })
324
325 #
326 # Image Gallery
327 #
328
329 class DocumentGalleryHandler(BaseHandler):
330     allowed_methods = ('GET')
331     
332     
333     def read(self, request, docid):
334         """Read meta-data about scans for gallery of this document."""
335         galleries = []
336         from urllib import quote
337
338         for assoc in GalleryForDocument.objects.filter(document=docid):
339             dirpath = os.path.join(settings.MEDIA_ROOT, assoc.subpath)
340
341             if not os.path.isdir(dirpath):
342                 log.warn(u"[WARNING]: missing gallery %s", dirpath)
343                 continue
344
345             gallery = {'name': assoc.name, 'pages': []}
346             
347             for file in os.listdir(dirpath):
348                 if not isinstance(file, unicode):
349                     try:
350                         file = file.decode('utf-8')
351                     except:
352                         log.warn(u"File %r in gallery %r is not unicode. Ommiting."\
353                             % (file, dirpath) )
354                         file = None
355
356                 if file is not None:
357                     name, ext = os.path.splitext(os.path.basename(file))
358
359                     if ext.lower() not in [u'.png', u'.jpeg', u'.jpg']:
360                         log.warn(u"Ignoring: %s %s", name, ext)
361                         url = None
362
363                     url = settings.MEDIA_URL + assoc.subpath + u'/' + file
364                 
365                 if url is None:
366                     url = settings.MEDIA_URL + u'/missing.png'
367                     
368                 gallery['pages'].append( quote(url.encode('utf-8')) )
369
370             gallery['pages'].sort()
371             galleries.append(gallery)
372
373         return galleries
374
375
376
377 #
378 # Dublin Core handlers
379 #
380 # @requires librarian
381 #
382 #class DocumentDublinCoreHandler(BaseHandler):
383 #    allowed_methods = ('GET', 'POST')
384 #
385 #    @hglibrary
386 #    def read(self, request, docid, lib):
387 #        """Read document as raw text"""
388 #        try:
389 #            revision = request.GET.get('revision', 'latest')
390 #
391 #            if revision == 'latest':
392 #                doc = lib.document(docid)
393 #            else:
394 #                doc = lib.document_for_revision(revision)
395 #
396 #
397 #            if document.id != docid:
398 #                return response.BadRequest().django_response({'reason': 'name-mismatch',
399 #                    'message': 'Provided revision is not valid for this document'})
400 #
401 #            bookinfo = dcparser.BookInfo.from_string(doc.data('xml'))
402 #            return bookinfo.serialize()
403 #        except (EntryNotFound, RevisionNotFound), e:
404 #            return response.EntityNotFound().django_response({
405 #                'exception': type(e), 'message': e.message})
406 #
407 #    @hglibrary
408 #    def create(self, request, docid, lib):
409 #        try:
410 #            bi_json = request.POST['contents']
411 #            revision = request.POST['revision']
412 #
413 #            if request.POST.has_key('message'):
414 #                msg = u"$USER$ " + request.PUT['message']
415 #            else:
416 #                msg = u"$AUTO$ Dublin core update."
417 #
418 #            current = lib.document(docid, request.user.username)
419 #            orig = lib.document_for_revision(revision)
420 #
421 #            if current != orig:
422 #                return response.EntityConflict().django_response({
423 #                        "reason": "out-of-date",
424 #                        "provided": orig.revision,
425 #                        "latest": current.revision })
426 #
427 #            xmldoc = parser.WLDocument.from_string(current.data('xml'))
428 #            document.book_info = dcparser.BookInfo.from_json(bi_json)
429 #
430 #            # zapisz
431 #            ndoc = current.quickwrite('xml', \
432 #                document.serialize().encode('utf-8'),\
433 #                message=msg, user=request.user.username)
434 #
435 #            try:
436 #                # return the new revision number
437 #                return {
438 #                    "document": ndoc.id,
439 #                    "subview": "dc",
440 #                    "previous_revision": current.revision,
441 #                    "revision": ndoc.revision,
442 #                    'timestamp': ndoc.revision.timestamp,
443 #                    "url": reverse("docdc_view", args=[ndoc.id])
444 #                }
445 #            except Exception, e:
446 #                if ndoc: lib._rollback()
447 #                raise e
448 #        except RevisionNotFound:
449 #            return response.EntityNotFound().django_response()
450
451 class MergeHandler(BaseHandler):
452     allowed_methods = ('POST',)
453
454     @validate_form(forms.MergeRequestForm, 'POST')
455     @hglibrary
456     def create(self, request, form, docid, lib):
457         """Create a new document revision from the information provided by user"""
458         try:
459             revision = form.cleaned_data['revision']
460
461             # fetch the main branch document
462             doc = lib.document(docid)
463
464             # fetch the base document
465             user_doc = lib.document_for_revision(revision)
466             base_doc = user_doc.latest()
467
468             if base_doc != user_doc:
469                 return response.EntityConflict().django_response({
470                     "reason": "out-of-date",
471                     "provided": str(user_doc.revision),
472                     "latest": str(base_doc.revision)
473                 })
474
475             if form.cleaned_data['type'] == 'update':
476                 # update is always performed from the file branch
477                 # to the user branch
478                 user_doc_new = base_doc.update(request.user.username)
479
480                 if user_doc_new == user_doc:
481                     return response.SuccessAllOk().django_response({
482                         "result": "no-op"
483                     })
484
485                 # shared document is the same
486                 doc_new = doc
487
488             if form.cleaned_data['type'] == 'share':
489                 if not base_doc.up_to_date():
490                     return response.BadRequest().django_response({
491                         "reason": "not-fast-forward",
492                         "message": "You must first update your branch to the latest version."
493                     })
494
495                 anwser, info = base_doc.would_share()
496
497                 if not anwser:
498                     return response.SuccessAllOk().django_response({
499                         "result": "no-op", "message": info
500                     })
501
502                 # check for unresolved conflicts
503                 if base_doc.has_conflict_marks():
504                     return response.BadRequest().django_response({
505                         "reason": "unresolved-conflicts",
506                         "message": "There are unresolved conflicts in your file. Fix them, and try again."
507                     })
508
509                 if not request.user.has_perm('api.share_document'):
510                     # User is not permitted to make a merge, right away
511                     # So we instead create a pull request in the database
512                     try:
513                         prq, created = PullRequest.objects.get_or_create(
514                             comitter = request.user,
515                             document = docid,
516                             status = "N",
517                             defaults = {
518                                 'source_revision': str(base_doc.revision),
519                                 'comment': form.cleaned_data['message'] or '$AUTO$ Document shared.',
520                             }
521                         )
522
523                         # there can't be 2 pending request from same user
524                         # for the same document
525                         if not created:
526                             prq.source_revision = str(base_doc.revision)
527                             prq.comment = prq.comment + 'u\n\n' + (form.cleaned_data['message'] or u'')
528                             prq.save()
529
530                         return response.RequestAccepted().django_response(\
531                             ticket_status=prq.status, \
532                             ticket_uri=reverse("pullrequest_view", args=[prq.id]) )
533                     except IntegrityError:
534                         return response.EntityConflict().django_response({
535                             'reason': 'request-already-exist'
536                         })
537
538                 changed = base_doc.share(form.cleaned_data['message'])
539
540                 # update shared version if needed
541                 if changed:
542                     doc_new = doc.latest()
543                 else:
544                     doc_new = doc
545
546                 # the user wersion is the same
547                 user_doc_new = base_doc
548
549             # The client can compare parent_revision to revision
550             # to see if he needs to update user's view
551             # Same goes for shared view
552
553             return response.SuccessAllOk().django_response({
554                 "result": "success",
555                 "name": user_doc_new.id,
556                 "user": user_doc_new.owner,
557
558                 "revision": user_doc_new.revision,
559                 'timestamp': user_doc_new.revision.timestamp,
560
561                 "parent_revision": user_doc.revision,
562                 "parent_timestamp": user_doc.revision.timestamp,
563
564                 "shared_revision": doc_new.revision,
565                 "shared_timestamp": doc_new.revision.timestamp,
566
567                 "shared_parent_revision": doc.revision,
568                 "shared_parent_timestamp": doc.revision.timestamp,
569             })
570         except wlrepo.OutdatedException, e:
571             return response.BadRequest().django_response({
572                         "reason": "not-fast-forward",
573                         "message": e.message
574                     })
575         except wlrepo.LibraryException, e:
576             return response.InternalError().django_response({
577                         "reason": "merge-error",
578                         "message": e.message
579                     })