1 # -*- encoding: utf-8 -*-
5 log = logging.getLogger('platforma.api.library')
7 __author__= "Ćukasz Rekucki"
8 __date__ = "$2009-09-25 15:49:50$"
9 __doc__ = "Module documentation."
11 from piston.handler import BaseHandler, AnonymousBaseHandler
12 from django.http import HttpResponse
14 from datetime import date
16 from django.core.urlresolvers import reverse
17 from django.db import IntegrityError
22 from librarian import dcparser, parser
25 from api.models import PullRequest
26 from explorer.models import GalleryForDocument
29 import api.forms as forms
30 import api.response as response
31 from api.utils import validate_form, hglibrary, natural_order
32 from api.models import PartCache, PullRequest
39 return username.startswith('$prq-')
41 def prq_for_user(username):
43 return PullRequest.objects.get(id=int(username[5:]))
47 def check_user(request, user):
48 log.info("user: %r, perm: %r" % (request.user, request.user.get_all_permissions()) )
51 if not request.user.has_perm('api.view_prq'):
52 yield response.AccessDenied().django_response({
53 'reason': 'access-denied',
54 'message': "You don't have enough priviliges to view pull requests."
57 elif request.user.username != user:
58 if not request.user.has_perm('api.view_other_document'):
59 yield response.AccessDenied().django_response({
60 'reason': 'access-denied',
61 'message': "You don't have enough priviliges to view other people's document."
66 # Document List Handlers
68 # TODO: security check
69 class BasicLibraryHandler(AnonymousBaseHandler):
70 allowed_methods = ('GET',)
73 def read(self, request, lib):
74 """Return the list of documents."""
76 'url': reverse('document_view', args=[docid]),
77 'name': docid } for docid in lib.documents() ]
78 return {'documents' : document_list}
81 # This handler controlls the document collection
83 class LibraryHandler(BaseHandler):
84 allowed_methods = ('GET', 'POST')
85 anonymous = BasicLibraryHandler
88 def read(self, request, lib):
89 """Return the list of documents."""
93 for docid in lib.documents():
95 'url': reverse('document_view', args=[docid]),
100 parts = PartCache.objects.defer('part_id')\
101 .values_list('part_id', 'document_id').distinct()
103 document_tree = dict(documents)
105 for part, docid in parts:
106 # this way, we won't display broken links
107 if not documents.has_key(part):
108 log.info("NOT FOUND: %s", part)
111 parent = documents[docid]
112 child = documents[part]
114 # not top-level anymore
115 document_tree.pop(part)
116 parent['parts'].append(child)
118 for doc in documents.itervalues():
119 doc['parts'].sort(key=natural_order(lambda d: d['name']))
121 return {'documents': sorted(document_tree.itervalues(),
122 key=natural_order(lambda d: d['name']) ) }
125 @validate_form(forms.DocumentUploadForm, 'POST')
127 def create(self, request, form, lib):
128 """Create a new document."""
130 if form.cleaned_data['ocr_data']:
131 data = form.cleaned_data['ocr_data']
133 data = request.FILES['ocr_file'].read().decode('utf-8')
136 return response.BadRequest().django_response('You must pass ocr_data or ocr_file.')
138 if form.cleaned_data['generate_dc']:
139 data = librarian.wrap_text(data, unicode(date.today()))
141 docid = form.cleaned_data['bookname']
146 log.info("DOCID %s", docid)
147 doc = lib.document_create(docid)
148 # document created, but no content yet
150 doc = doc.quickwrite('xml', data.encode('utf-8'),
151 '$AUTO$ XML data uploaded.', user=request.user.username)
154 # rollback branch creation
156 raise LibraryException(traceback.format_exc())
158 url = reverse('document_view', args=[doc.id])
160 return response.EntityCreated().django_response(\
164 'revision': doc.revision },
168 except LibraryException, e:
170 return response.InternalError().django_response({
171 "reason": traceback.format_exc()
173 except DocumentAlreadyExists:
174 # Document is already there
175 return response.EntityConflict().django_response({
176 "reason": "already-exists",
177 "message": "Document already exists." % docid
183 class BasicDocumentHandler(AnonymousBaseHandler):
184 allowed_methods = ('GET',)
187 def read(self, request, docid, lib):
189 doc = lib.document(docid)
190 except RevisionNotFound:
195 'html_url': reverse('dochtml_view', args=[doc.id]),
196 'text_url': reverse('doctext_view', args=[doc.id]),
197 'dc_url': reverse('docdc_view', args=[doc.id]),
198 'public_revision': doc.revision,
204 class DiffHandler(BaseHandler):
205 allowed_methods = ('GET',)
208 def read(self, request, source_revision, target_revision, lib):
209 '''Return diff between source_revision and target_revision)'''
210 source_document = lib.document_for_revision(source_revision)
211 target_document = lib.document_for_revision(target_revision)
212 print source_document,
213 print target_document
214 diff = difflib.unified_diff(
215 source_document.data('xml').splitlines(True),
216 target_document.data('xml').splitlines(True),
220 return ''.join(list(diff))
226 class DocumentHandler(BaseHandler):
227 allowed_methods = ('GET', 'PUT')
228 anonymous = BasicDocumentHandler
230 @validate_form(forms.DocumentRetrieveForm, 'GET')
232 def read(self, request, form, docid, lib):
233 """Read document's meta data"""
234 log.info(u"User '%s' wants to edit %s(%s) as %s" % \
235 (request.user.username, docid, form.cleaned_data['revision'], form.cleaned_data['user']) )
237 user = form.cleaned_data['user'] or request.user.username
238 rev = form.cleaned_data['revision'] or 'latest'
240 for error in check_user(request, user):
244 doc = lib.document(docid, user, rev=rev)
245 except RevisionMismatch, e:
246 # the document exists, but the revision is bad
247 return response.EntityNotFound().django_response({
248 'reason': 'revision-mismatch',
249 'message': e.message,
253 except RevisionNotFound, e:
254 # the user doesn't have this document checked out
255 # or some other weird error occured
256 # try to do the checkout
258 if user == request.user.username:
259 mdoc = lib.document(docid)
260 doc = mdoc.take(user)
262 prq = prq_for_user(user)
263 # commiter's document
264 prq_doc = lib.document_for_revision(prq.source_revision)
265 doc = prq_doc.take(user)
267 return response.EntityNotFound().django_response({
268 'reason': 'document-not-found',
269 'message': e.message,
273 except RevisionNotFound, e:
274 return response.EntityNotFound().django_response({
275 'reason': 'document-not-found',
276 'message': e.message,
284 'html_url': reverse('dochtml_view', args=[doc.id]),
285 'text_url': reverse('doctext_view', args=[doc.id]),
286 # 'dc_url': reverse('docdc_view', args=[doc.id]),
287 'gallery_url': reverse('docgallery_view', args=[doc.id]),
288 'merge_url': reverse('docmerge_view', args=[doc.id]),
289 'revision': doc.revision,
290 'timestamp': doc.revision.timestamp,
291 # 'public_revision': doc.revision,
292 # 'public_timestamp': doc.revision.timestamp,
297 # def update(self, request, docid, lib):
298 # """Update information about the document, like display not"""
303 class DocumentHTMLHandler(BaseHandler):
304 allowed_methods = ('GET')
306 @validate_form(forms.DocumentRetrieveForm, 'GET')
308 def read(self, request, form, docid, lib, stylesheet='partial'):
309 """Read document as html text"""
311 revision = form.cleaned_data['revision']
312 user = form.cleaned_data['user'] or request.user.username
313 document = lib.document_for_revision(revision)
315 if document.id != docid:
316 return response.BadRequest().django_response({
317 'reason': 'name-mismatch',
318 'message': 'Provided revision is not valid for this document'
321 if document.owner != user:
322 return response.BadRequest().django_response({
323 'reason': 'user-mismatch',
324 'message': "Provided revision doesn't belong to user %s" % user
327 for error in check_user(request, user):
330 return librarian.html.transform(document.data('xml'), is_file=False, \
331 parse_dublincore=False, stylesheet='full',\
333 "with-paths": 'boolean(1)',
336 except (EntryNotFound, RevisionNotFound), e:
337 return response.EntityNotFound().django_response({
338 'reason': 'not-found', 'message': e.message})
339 except librarian.ValidationError, e:
340 return response.InternalError().django_response({
341 'reason': 'xml-non-valid', 'message': e.message or u''})
342 except librarian.ParseError, e:
343 return response.InternalError().django_response({
344 'reason': 'xml-parse-error', 'message': e.message or u'' })
350 class DocumentGalleryHandler(BaseHandler):
351 allowed_methods = ('GET')
354 def read(self, request, docid):
355 """Read meta-data about scans for gallery of this document."""
357 from urllib import quote
359 for assoc in GalleryForDocument.objects.filter(document=docid):
360 dirpath = os.path.join(settings.MEDIA_ROOT, assoc.subpath)
362 if not os.path.isdir(dirpath):
363 log.warn(u"[WARNING]: missing gallery %s", dirpath)
366 gallery = {'name': assoc.name, 'pages': []}
368 for file in os.listdir(dirpath):
369 if not isinstance(file, unicode):
371 file = file.decode('utf-8')
373 log.warn(u"File %r in gallery %r is not unicode. Ommiting."\
378 name, ext = os.path.splitext(os.path.basename(file))
380 if ext.lower() not in [u'.png', u'.jpeg', u'.jpg']:
381 log.warn(u"Ignoring: %s %s", name, ext)
384 url = settings.MEDIA_URL + assoc.subpath + u'/' + file
387 url = settings.MEDIA_URL + u'/missing.png'
389 gallery['pages'].append( quote(url.encode('utf-8')) )
391 gallery['pages'].sort()
392 galleries.append(gallery)
399 # Dublin Core handlers
401 # @requires librarian
403 #class DocumentDublinCoreHandler(BaseHandler):
404 # allowed_methods = ('GET', 'POST')
407 # def read(self, request, docid, lib):
408 # """Read document as raw text"""
410 # revision = request.GET.get('revision', 'latest')
412 # if revision == 'latest':
413 # doc = lib.document(docid)
415 # doc = lib.document_for_revision(revision)
418 # if document.id != docid:
419 # return response.BadRequest().django_response({'reason': 'name-mismatch',
420 # 'message': 'Provided revision is not valid for this document'})
422 # bookinfo = dcparser.BookInfo.from_string(doc.data('xml'))
423 # return bookinfo.serialize()
424 # except (EntryNotFound, RevisionNotFound), e:
425 # return response.EntityNotFound().django_response({
426 # 'exception': type(e), 'message': e.message})
429 # def create(self, request, docid, lib):
431 # bi_json = request.POST['contents']
432 # revision = request.POST['revision']
434 # if request.POST.has_key('message'):
435 # msg = u"$USER$ " + request.PUT['message']
437 # msg = u"$AUTO$ Dublin core update."
439 # current = lib.document(docid, request.user.username)
440 # orig = lib.document_for_revision(revision)
442 # if current != orig:
443 # return response.EntityConflict().django_response({
444 # "reason": "out-of-date",
445 # "provided": orig.revision,
446 # "latest": current.revision })
448 # xmldoc = parser.WLDocument.from_string(current.data('xml'))
449 # document.book_info = dcparser.BookInfo.from_json(bi_json)
452 # ndoc = current.quickwrite('xml', \
453 # document.serialize().encode('utf-8'),\
454 # message=msg, user=request.user.username)
457 # # return the new revision number
459 # "document": ndoc.id,
461 # "previous_revision": current.revision,
462 # "revision": ndoc.revision,
463 # 'timestamp': ndoc.revision.timestamp,
464 # "url": reverse("docdc_view", args=[ndoc.id])
466 # except Exception, e:
467 # if ndoc: lib._rollback()
469 # except RevisionNotFound:
470 # return response.EntityNotFound().django_response()
472 class MergeHandler(BaseHandler):
473 allowed_methods = ('POST',)
475 @validate_form(forms.MergeRequestForm, 'POST')
477 def create(self, request, form, docid, lib):
478 """Create a new document revision from the information provided by user"""
479 revision = form.cleaned_data['revision']
481 # fetch the main branch document
482 doc = lib.document(docid)
484 # fetch the base document
485 user_doc = lib.document_for_revision(revision)
486 base_doc = user_doc.latest()
488 if base_doc != user_doc:
489 return response.EntityConflict().django_response({
490 "reason": "out-of-date",
491 "provided": str(user_doc.revision),
492 "latest": str(base_doc.revision)
495 if form.cleaned_data['type'] == 'update':
496 # update is always performed from the file branch
498 user_doc_new = base_doc.update(request.user.username)
500 if user_doc_new == user_doc:
501 return response.SuccessAllOk().django_response({
505 # shared document is the same
508 if form.cleaned_data['type'] == 'share':
509 if not base_doc.up_to_date():
510 return response.BadRequest().django_response({
511 "reason": "not-fast-forward",
512 "message": "You must first update your branch to the latest version."
515 anwser, info = base_doc.would_share()
518 return response.SuccessAllOk().django_response({
519 "result": "no-op", "message": info
522 # check for unresolved conflicts
523 if base_doc.has_conflict_marks():
524 return response.BadRequest().django_response({
525 "reason": "unresolved-conflicts",
526 "message": "There are unresolved conflicts in your file. Fix them, and try again."
529 if not request.user.has_perm('api.share_document'):
530 # User is not permitted to make a merge, right away
531 # So we instead create a pull request in the database
533 prq, created = PullRequest.objects.get_or_create(
534 comitter = request.user,
538 'source_revision': str(base_doc.revision),
539 'comment': form.cleaned_data['message'] or '$AUTO$ Document shared.',
543 # there can't be 2 pending request from same user
544 # for the same document
546 prq.source_revision = str(base_doc.revision)
547 prq.comment = prq.comment + 'u\n\n' + (form.cleaned_data['message'] or u'')
550 return response.RequestAccepted().django_response(\
551 ticket_status=prq.status, \
552 ticket_uri=reverse("pullrequest_view", args=[prq.id]) )
553 except IntegrityError:
554 return response.EntityConflict().django_response({
555 'reason': 'request-already-exist'
558 changed = base_doc.share(form.cleaned_data['message'])
560 # update shared version if needed
562 doc_new = doc.latest()
566 # the user wersion is the same
567 user_doc_new = base_doc
569 # The client can compare parent_revision to revision
570 # to see if he needs to update user's view
571 # Same goes for shared view
573 return response.SuccessAllOk().django_response({
575 "name": user_doc_new.id,
576 "user": user_doc_new.owner,
578 "revision": user_doc_new.revision,
579 'timestamp': user_doc_new.revision.timestamp,
581 "parent_revision": user_doc.revision,
582 "parent_timestamp": user_doc.revision.timestamp,
584 "shared_revision": doc_new.revision,
585 "shared_timestamp": doc_new.revision.timestamp,
587 "shared_parent_revision": doc.revision,
588 "shared_parent_timestamp": doc.revision.timestamp,