add api: basic auth / unit tests / online doc (based on django-piston)
authorraph
Fri, 09 Jul 2010 10:05:29 +0200
changeset 287 fc5ed157ebfe
parent 282 b5deb8e32219
child 288 c6fe4822a243
add api: basic auth / unit tests / online doc (based on django-piston)
buildout.cfg
src/cm/api/__init__.py
src/cm/api/handlers.py
src/cm/api/urls.py
src/cm/fixtures/test_content.yaml
src/cm/security.py
src/cm/templates/api_doc.html
src/cm/tests/__init__.py
src/cm/tests/test_api.py
src/cm/tests/test_security.py
src/cm/urls.py
--- a/buildout.cfg	Fri Jun 11 11:04:23 2010 +0200
+++ b/buildout.cfg	Fri Jul 09 10:05:29 2010 +0200
@@ -4,6 +4,8 @@
 	django
 	python
 	django-extensions
+	django-piston
+	omelette
 develop = .
 
 [python]
@@ -23,11 +25,13 @@
 pythonpath = src
 	src/cm
 	${django-extensions:location}
+	${django-piston:location}
 eggs = 
 	django-flash
 	django-tagging
+#	django-piston
 #	django-css	
-	chardet
+#	chardet
 	feedparser
 	PIL
 	BeautifulSoup
@@ -43,4 +47,12 @@
 [django-extensions]
 recipe=zerokspot.recipe.git
 repository=git://github.com/django-extensions/django-extensions.git
-#rev=7c73978b55fcadbe2cd6f2abbefbedb5a85c2c8c
\ No newline at end of file
+#rev=7c73978b55fcadbe2cd6f2abbefbedb5a85c2c8c
+
+[django-piston]
+recipe = mercurialrecipe
+repository = http://bitbucket.org/jespern/django-piston
+
+[omelette]
+recipe = collective.recipe.omelette
+eggs = ${django:eggs}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/cm/api/handlers.py	Fri Jul 09 10:05:29 2010 +0200
@@ -0,0 +1,251 @@
+from piston.handler import AnonymousBaseHandler, BaseHandler
+from piston.utils import rc
+
+from cm.models import Text,TextVersion, Role, UserRole
+from cm.views import get_keys_from_dict, get_textversion_by_keys_or_404, get_text_by_keys_or_404, get_textversion_by_keys_or_404, redirect
+from cm.security import get_texts_with_perm, has_perm, get_viewable_comments, \
+    has_perm_on_text_api
+from cm.security import get_viewable_comments
+from cm.utils.embed import embed_html
+from cm.views.create import CreateTextContentForm, create_text
+from piston.utils import validate
+from settings import SITE_URL
+
+URL_PREFIX = SITE_URL + '/api'
+ 
+class AnonymousTextHandler(AnonymousBaseHandler):
+    type = "Text methods"
+    title = "Read text info"
+    fields = ('key', 'title', 'format', 'content', 'created', 'modified', 'nb_comments', 'nb_versions', 'embed_html', ('last_text_version', ('created','modified', 'format', 'title', 'content')))   
+    allowed_methods = ('GET', )   
+    model = Text
+    desc = """ Read text identified by `key`."""
+    args = None
+
+    @staticmethod
+    def endpoint():
+        return URL_PREFIX + '/text/{key}/'
+
+
+    @has_perm_on_text_api('can_view_text')
+    def read(self, request, key):
+        
+        text = get_text_by_keys_or_404(key)
+        setattr(text,'nb_comments',len(get_viewable_comments(request, text.last_text_version.comment_set.all(), text)))
+        setattr(text,'nb_versions',text.get_versions_number())
+        setattr(text,'embed_html',embed_html(text.key))
+
+        return text
+
+class TextHandler(BaseHandler):
+    type = "Text methods"    
+    anonymous = AnonymousTextHandler
+    allowed_methods = ('GET',)  
+    no_display = True 
+
+class AnonymousTextVersionHandler(AnonymousBaseHandler):
+    type = "Text methods"
+    title = "Read text version info"
+    fields = ('key', 'title', 'format', 'content', 'created', 'modified', 'nb_comments',)   
+    allowed_methods = ('GET', )   
+    model = Text
+    desc = """ Read text version identified by `version_key` inside text identified by `key`."""
+    args = None
+
+    @staticmethod
+    def endpoint():
+        return URL_PREFIX + '/text/{key}/{version_key}/'
+
+
+    @has_perm_on_text_api('can_view_text')
+    def read(self, request, key, version_key):
+        text_version = get_textversion_by_keys_or_404(version_key, key=key)
+        setattr(text_version,'nb_comments',len(get_viewable_comments(request, text_version.comment_set.all(), text_version.text)))
+
+        return text_version
+
+class TextVersionHandler(BaseHandler):
+    type = "Text methods"    
+    anonymous = AnonymousTextVersionHandler
+    allowed_methods = ('GET',)  
+    no_display = True 
+
+class AnonymousTextListHandler(AnonymousBaseHandler):
+    title = "List texts"    
+    type = "Text methods"    
+    fields = ('key', 'title', 'created', 'modified', 'nb_comments', 'nb_versions',)   
+    allowed_methods = ('GET',)   
+    model = Text
+    desc = """Lists texts on workspace."""        
+
+    @staticmethod
+    def endpoint():
+        return URL_PREFIX + '/text/'
+
+    def read(self, request):
+        order_by = '-id'
+        texts = get_texts_with_perm(request, 'can_view_text').order_by(order_by)        
+        return texts
+
+class TextListHandler(BaseHandler):
+    title = "Create text"    
+    type = "Text methods"    
+    allowed_methods = ('POST', )    
+    anonymous = AnonymousTextListHandler
+    desc = "Create a text with the provided parameters."
+    args = """<br/>
+`title`: title of the text<br/>
+`format`: format content ('markdown', 'html')<br/>
+`content`: content (in specified format)<br/>
+`anon_role`: role to give to anon users: null, 4: commentator, 5: observer<br/>
+        """
+     
+    @staticmethod
+    def endpoint():
+        return URL_PREFIX + '/text/'
+
+    def create(self, request):
+        form = CreateTextContentForm(request.POST)
+        if form.is_valid():
+            text = create_text(request.user, form.cleaned_data)
+            anon_role = request.POST.get('anon_role', None)
+            if anon_role:
+                userrole = UserRole.objects.create(user=None, role=Role.objects.get(id=anon_role), text=text)         
+            return {'key' : text.key , 'version_key' : text.last_text_version.key, 'created': text.created}
+        else:
+            resp = rc.BAD_REQUEST
+        return resp
+
+from cm.exception import UnauthorizedException
+from cm.views.texts import text_delete
+
+class TextDeleteHandler(BaseHandler):
+    type = "Text methods"
+    allowed_methods = ('POST', )    
+    title = "Delete text"    
+    desc = "Delete the text identified by `key`."
+
+    @staticmethod
+    def endpoint():
+        return URL_PREFIX + '/text/{key}/delete/'
+
+    def create(self, request):
+        """
+        Delete text identified by `key`.
+        """
+        try:
+            key = request.POST.get('key')
+            text_delete(request, key=key)
+        except UnauthorizedException:
+            return rc.FORBIDDEN
+        except KeyError:
+            return rc.BAD_REQUEST
+        return rc.DELETED
+
+from cm.views.texts import text_pre_edit
+ 
+class TextPreEditHandler(BaseHandler):
+    type = "Text methods"
+    allowed_methods = ('POST', )    
+    title = "Ask for edit impact"    
+    desc = "Returns the number of impacted comments."
+    args = """<br />
+`new_format`: new format content ('markdown', 'html')<br />        
+`new_content`: new content (in specified format)<br />    
+    """ 
+
+    @staticmethod
+    def endpoint():
+        return URL_PREFIX + '/text/{key}/pre_edit/'
+    
+    def create(self, request, key):
+        return text_pre_edit(request, key=key)
+
+from cm.views.texts import text_edit
+    
+class TextEditHandler(BaseHandler):
+    allowed_methods = ('POST', )    
+    type = "Text methods"
+    title = "Edit text"    
+    desc = "Update text identified by `key`."
+    args = """<br />
+`title`: new title of the text<br />
+`format`: new format content ('markdown', 'html')<br />
+`content`: new content (in specified format)<br />
+`note`: note to add to edit<br />
+`new_version`: boolean: should a new version of the text be created?<br />
+`keep_comments`: boolean: should existing comments be keep (if possible)?<br />
+    """ 
+    
+    @staticmethod
+    def endpoint():
+        return URL_PREFIX + '/text/{key}/edit/'
+    
+    
+    def create(self, request, key):
+        res = text_edit(request, key=key)
+        text = get_text_by_keys_or_404(key)
+        text_version = text.last_text_version
+        return {'version_key' : text_version.key , 'created': text_version.created}
+
+from django.contrib.auth import authenticate
+    
+class SetUserHandler(AnonymousBaseHandler):
+    allowed_methods = ('POST',)    
+    type = "User methods"
+    title = "Set username and email"    
+    desc = "Set username and email to use when commenting."
+    args = """<br />
+`user_name`: user's name<br />
+`user_email`: user's email<br />
+    """ 
+    
+    @staticmethod
+    def endpoint():
+        return URL_PREFIX + '/setuser/'
+    
+    def create(self, request):
+        user_name = request.POST.get('user_name', None)
+        user_email = request.POST.get('user_email', None)
+        if user_name and user_email: 
+            response = rc.ALL_OK
+            response.set_cookie('user_name', user_name)
+            response.set_cookie('user_email', user_email)
+            return response
+        else:
+            return rc.BAD_REQUEST    
+                         
+
+
+from piston.doc import documentation_view
+
+from piston.handler import handler_tracker
+from django.template import RequestContext
+from piston.doc import generate_doc
+from django.shortcuts import render_to_response
+
+def documentation(request):
+    """
+    Generic documentation view. Generates documentation
+    from the handlers you've defined.
+    """
+    docs = [ ]
+
+    for handler in handler_tracker:
+        doc = generate_doc(handler)
+        setattr(doc,'type', handler.type)
+        docs.append(doc)
+
+    def _compare(doc1, doc2): 
+       #handlers and their anonymous counterparts are put next to each other.
+       name1 = doc1.name.replace("Anonymous", "")
+       name2 = doc2.name.replace("Anonymous", "")
+       return cmp(name1, name2)    
+ 
+    #docs.sort(_compare)
+       
+    return render_to_response('api_doc.html', 
+        { 'docs': docs }, RequestContext(request))
+
+from piston.doc import generate_doc
+DocHandler = generate_doc(TextPreEditHandler)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/cm/api/urls.py	Fri Jul 09 10:05:29 2010 +0200
@@ -0,0 +1,28 @@
+from django.conf.urls.defaults import *
+
+from piston.resource import Resource
+from piston.authentication import HttpBasicAuthentication
+
+from cm.api.handlers import * 
+auth = HttpBasicAuthentication(realm='Comt API')
+
+text_handler = Resource(handler=TextHandler, authentication=auth)
+textversion_handler = Resource(handler=TextVersionHandler, authentication=auth)
+text_list_handler = Resource(handler=TextListHandler, authentication=auth)
+text_delete_handler = Resource(handler=TextDeleteHandler, authentication=auth)
+text_pre_edit_handler = Resource(handler=TextPreEditHandler, authentication=auth)
+text_edit_handler = Resource(handler=TextEditHandler, authentication=auth)
+setuser_handler = Resource(handler=SetUserHandler, authentication=None)
+
+#doc_handler = Resource(handler=DocHandler)
+
+urlpatterns = patterns('',
+   url(r'^text/(?P<key>\w*)/$', text_handler),
+   url(r'^text/$', text_list_handler),
+   url(r'^text/(?P<key>\w*)/delete/$', text_delete_handler),
+   url(r'^text/(?P<key>\w*)/pre_edit/$', text_pre_edit_handler),
+   url(r'^text/(?P<key>\w*)/edit/$', text_edit_handler),
+   url(r'^text/(?P<key>\w*)/(?P<version_key>\w*)/$', textversion_handler),
+   url(r'^setuser/$', setuser_handler),
+   url(r'^doc/$', documentation),
+)
--- a/src/cm/fixtures/test_content.yaml	Fri Jun 11 11:04:23 2010 +0200
+++ b/src/cm/fixtures/test_content.yaml	Fri Jul 09 10:05:29 2010 +0200
@@ -360,6 +360,58 @@
     adminkey: "text_adminkey_3"
     user: 2    
 
+# text 4
+- model : cm.textversion
+  pk: 4
+  fields:
+    created: "2009-02-13 04:01:12"
+    modified: "2009-02-13 04:01:12"
+    title: 'title 4, public text'
+    format: 'markdown'
+    content: 'aaa bbb ccc ddd eee fff ggg'
+    text: 4
+    mod_posteriori: True
+    key: "textversion_key_4" 
+    adminkey: "tv_adminkey_4" 
+    
+- model : cm.text
+  pk: 4
+  fields:
+    created: "2009-02-13 04:01:12"
+    modified: "2009-02-13 04:01:12"
+    last_text_version: 4
+    title: 'title 4, public text'
+    state: "approved"
+    key: "text_key_4" 
+    adminkey: "text_adminkey_4" 
+    user: 1
+
+# text 5
+- model : cm.textversion
+  pk: 5
+  fields:
+    created: "2009-02-13 04:01:12"
+    modified: "2009-02-13 04:01:12"
+    title: 'title 5, public text'
+    format: 'markdown'
+    content: 'aaa bbb ccc ddd eee fff ggg'
+    text: 5
+    mod_posteriori: True
+    key: "textversion_key_5" 
+    adminkey: "tv_adminkey_5" 
+    
+- model : cm.text
+  pk: 5
+  fields:
+    created: "2009-02-13 04:01:12"
+    modified: "2009-02-13 04:01:12"
+    last_text_version: 5
+    title: 'title 5, public text'
+    state: "approved"
+    key: "text_key_5" 
+    adminkey: "text_adminkey_5" 
+    user: 1    
+    
 ############### userrole ############### 
 
 # user 1 is global Manager
@@ -426,6 +478,24 @@
     user: 4
     text: 2
 
+# user null (anon is Commentator on text 4)
+# userrole 8
+- model : cm.userrole
+  pk: 8
+  fields:
+    role: 4
+    user: null
+    text: 4
+    
+# user null (anon is Commentator on text 5)
+# userrole 9
+- model : cm.userrole
+  pk: 9
+  fields:
+    role: 4
+    user: null
+    text: 5
+    
 ############### comment ###############
 
 # comment 1 (visible on text 2)
--- a/src/cm/security.py	Fri Jun 11 11:04:23 2010 +0200
+++ b/src/cm/security.py	Fri Jul 09 10:05:29 2010 +0200
@@ -7,7 +7,7 @@
 from django.http import HttpResponseRedirect
 from django.utils.http import urlquote
 from django.db.models import Q
-
+from piston.utils import rc
 import logging
 
 from cm.models import *
@@ -210,8 +210,14 @@
 
         return _check_global_perm
     return _dec    
+
+def has_perm_on_text_api(perm_name, must_be_logged_in=False, redirect_field_name=REDIRECT_FIELD_NAME):    
+    return _has_perm_on_text(perm_name, must_be_logged_in, redirect_field_name, api=True)
     
-def has_perm_on_text(perm_name, must_be_logged_in=False, redirect_field_name=REDIRECT_FIELD_NAME):    
+def has_perm_on_text(perm_name, must_be_logged_in=False, redirect_field_name=REDIRECT_FIELD_NAME, api=False):
+    return _has_perm_on_text(perm_name, must_be_logged_in, redirect_field_name, api)
+
+def _has_perm_on_text(perm_name, must_be_logged_in=False, redirect_field_name=REDIRECT_FIELD_NAME, api=False):    
     """
     decorator protection checking for perm for logged in user
     force logged in (i.e. redirect to connection screen if not if must_be_logged_in 
@@ -222,15 +228,24 @@
                 return view_func(request, *args, **kwargs)
 
             if must_be_logged_in and not is_authenticated(request):
-                login_url = reverse('login')
-                return HttpResponseRedirect('%s?%s=%s' % (login_url, redirect_field_name, urlquote(request.get_full_path())))
+                if not api:
+                    login_url = reverse('login')
+                    return HttpResponseRedirect('%s?%s=%s' % (login_url, redirect_field_name, urlquote(request.get_full_path())))
+                else:
+                    return rc.FORBIDDEN
+
             
             if 'key' in kwargs: 
                 text = get_object_or_404(Text, key=kwargs['key'])                
             else:
                 raise Exception('no security check possible')
-                                    
-            if has_perm(request, perm_name, text=text): 
+                
+            # in api, the view has an object as first parameter, request is args[0]
+            if not api:                
+                req = request
+            else:                    
+                req = args[0]     
+            if has_perm(req, perm_name, text=text): 
                 return view_func(request, *args, **kwargs)
             #else:
                 # TODO: (? useful ?) if some user have the perm and not logged-in : redirect to login
@@ -238,7 +253,11 @@
                 #    return HttpResponseRedirect('%s?%s=%s' % (login_url, redirect_field_name, urlquote(request.get_full_path())))                    
             # else : unauthorized
             
-            raise UnauthorizedException('No perm %s' % perm_name)
+            if not api:
+                raise UnauthorizedException('No perm %s' % perm_name)
+            else:
+                return rc.FORBIDDEN
+
         _check_local_perm.__doc__ = view_func.__doc__
         _check_local_perm.__dict__ = view_func.__dict__
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/cm/templates/api_doc.html	Fri Jul 09 10:05:29 2010 +0200
@@ -0,0 +1,75 @@
+{% extends "site/layout/base_workspace.html" %}
+{% load com %}
+{% load i18n %}
+
+{% block title %}
+{% blocktrans %}API Documentation{% endblocktrans %}
+{% endblock %}
+
+{% block head %}
+{% endblock %}
+
+{% block content %}
+
+
+
+  <style type="text/css" media="screen">
+a.reflink {
+color:#C60F0F;
+font-size:0.8em;
+padding:0 4px;
+text-decoration:none;
+visibility:hidden;
+}
+
+:hover > a.reflink {
+visibility:visible;
+}
+  </style>
+
+
+<h1>API Documentation</h1>
+
+<h2>Presentation</h2>
+
+The API exposes method for external application to deal with content store in COMT.
+
+The authentification is done using <a href="http://fr.wikipedia.org/wiki/HTTP_Authentification">HTTP Authentification</a>.
+
+The default return format is 'json', add '?format=other_format' where other_format is 'json', 'xml', 'yaml' to change the results' format. 
+{% load markup %}
+
+		{% regroup docs by type as grouped_docs %}
+
+		
+		{% for dd in grouped_docs %}
+			<h2>{{ dd.grouper }}</h2>
+		    {% for doc in dd.list %}
+			{% if not doc.handler.no_display %}
+			<h3>{{ doc.handler.title }}&nbsp;<a href="#{{ doc.handler.title|iriencode }}" class="reflink" title="Permalink" name="{{ doc.handler.title|iriencode }}">ΒΆ</a></h3>
+			
+			<p>{{ doc.handler.desc }}</p>
+			<p>
+				{{ doc.doc|default:""|restructuredtext }}
+			</p>
+			
+			<p>
+				Endpoint: <b>{{ doc.handler.endpoint }}</b>
+			</p>
+			
+			<p>
+				Method: {% for meth in doc.allowed_methods %}<b>{{ meth }}</b>{% if not forloop.last %}, {% endif %}{% endfor %}
+			</p>
+
+			<p>
+				Args: {% if doc.handler.args %}{{ doc.handler.args|safe }}{% else %}None{% endif %}
+			</p>
+			
+					
+			<br />		
+			{% endif %}		
+			{% endfor %}
+		{% endfor %}
+	</body>
+</html>
+{% endblock %}
--- a/src/cm/tests/__init__.py	Fri Jun 11 11:04:23 2010 +0200
+++ b/src/cm/tests/__init__.py	Fri Jul 09 10:05:29 2010 +0200
@@ -6,3 +6,4 @@
 from cm.tests.test_history import *
 from cm.tests.test_security import *
 from cm.tests.test_activity import *
+from cm.tests.test_api import *
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/cm/tests/test_api.py	Fri Jul 09 10:05:29 2010 +0200
@@ -0,0 +1,207 @@
+from django.test import TestCase
+from django.test.client import Client
+from django.core import management
+from datetime import datetime
+from cm.activity import *
+from cm.models import *
+from cm.security import *
+
+from django.http import HttpRequest, HttpResponse
+from django.utils import simplejson
+
+from cm.api.handlers import *
+
+#from piston.test import TestCase
+from piston.models import Consumer
+from piston.handler import BaseHandler
+from piston.utils import rc
+from piston.resource import Resource
+
+class FalseRequest(object):
+    def __init__(self, user):
+        self.user = user
+
+class APITest(TestCase):
+    fixtures = ['roles_generic', 'test_content', ]
+    
+    def test_text_get(self):
+        """
+        Anonymous api call
+        """
+        
+        resource = Resource(AnonymousTextHandler)
+        request = HttpRequest()
+        setattr(request, 'user' , None)
+        request.method = 'GET'
+        
+        # get public text
+        response = resource(request, key='text_key_4', emitter_format='json')
+        self.assertEquals(200, response.status_code) # 401: forbidden
+        response_data = simplejson.loads(response.content)
+        self.assertEquals(response_data.get('created'), '2009-02-13 04:01:12')
+        
+        # error: private text
+        response = resource(request, key='text_key_3', emitter_format='json')
+        self.assertEquals(401, response.status_code)
+
+
+    def test_text_get_logged_in(self):
+        """
+        Logged in as manager api call
+        """
+
+        resource = Resource(AnonymousTextHandler)
+        request = HttpRequest()
+        user = User.objects.get(pk=1)
+        setattr(request, 'user' , user)
+        request.method = 'GET'
+  
+        response = resource(request, key='text_key_3', emitter_format='json')
+        self.assertEquals(200, response.status_code)
+
+
+    def test_text_create(self):
+        request = FalseRequest(None) 
+        nb_anon_texts = get_texts_with_perm(request, 'can_view_text').count()
+        nb_texts = Text.objects.count()
+        
+        resource = Resource(TextListHandler)
+        
+        # create one private text 
+        request = HttpRequest()
+        user = User.objects.get(pk=1)
+        setattr(request, 'user' , user)
+        request.method = 'POST'
+        setattr(request, 'POST' , {'content':'test content', 'format':"markdown", 'title': 'my title'})
+        response = resource(request,)
+
+        self.assertEquals(200, response.status_code)
+        self.assertTrue('key' in simplejson.loads(response.content).keys())
+
+        request = FalseRequest(None) 
+        self.assertEqual(get_texts_with_perm(request, 'can_view_text').count(), nb_anon_texts) # NO more anon text
+        
+        # create one text with anon observer
+        request = HttpRequest()
+        user = User.objects.get(pk=1)
+        setattr(request, 'user' , user)
+        request.method = 'POST'
+        setattr(request, 'POST' , {'content':'test content', 'format':"markdown", 'title': 'my title', 'anon_role' : 4})
+        response = resource(request,)
+
+        self.assertEquals(200, response.status_code)
+        self.assertTrue('key' in simplejson.loads(response.content).keys())
+        
+        self.assertEquals(nb_texts + 2, Text.objects.count()) # 2 more texts should have been created
+
+        request = FalseRequest(None) 
+        self.assertEqual(get_texts_with_perm(request, 'can_view_text').count(), nb_anon_texts + 1) # one more anon accessible text available
+        
+    def test_list_text_get(self):
+        """
+        List texts anon
+        """
+        resource = Resource(AnonymousTextListHandler)
+        request = HttpRequest()
+        setattr(request, 'user' , None)
+        request.method = 'GET'
+  
+        response = resource(request, emitter_format='json')
+        self.assertEquals(200, response.status_code)
+        self.assertEquals(2, len(simplejson.loads(response.content)))
+
+    def test_list_text_logged_in(self):
+        """
+        List texts manager
+        """
+        resource = Resource(AnonymousTextListHandler)
+        request = HttpRequest()
+        user = User.objects.get(pk=1)
+        setattr(request, 'user' , user)
+        request.method = 'GET'
+  
+        response = resource(request, emitter_format='json')
+        self.assertEquals(200, response.status_code)
+        self.assertEquals(5, len(simplejson.loads(response.content)))
+
+    def test_delete_text_logged_in_works(self):
+        """
+        Delete text
+        """
+        nb_texts = Text.objects.count()
+        
+        resource = Resource(TextDeleteHandler)
+        request = HttpRequest()
+        user = User.objects.get(pk=1)
+        setattr(request, 'user' , user)
+        request.method = 'POST'
+        setattr(request, 'POST' , {'key':'text_key_3'})
+        setattr(request, 'flash' , {})
+  
+        response = resource(request, emitter_format='json')
+        self.assertEquals(204, response.status_code)
+
+        # one text deleted
+        self.assertEquals(nb_texts - 1, Text.objects.count())
+        
+    def test_delete_text_logged_in_fail(self):
+        """
+        Delete text (and fail: insufficient rights)
+        """
+        nb_texts = Text.objects.count()
+
+        resource = Resource(TextDeleteHandler)
+        request = HttpRequest()
+        user = User.objects.get(pk=3)
+        setattr(request, 'user' , user)
+        request.method = 'POST'
+        setattr(request, 'POST' , {'key':'text_key_3'})
+        setattr(request, 'flash' , {})
+  
+        response = resource(request, emitter_format='json')
+        self.assertEquals(401, response.status_code)
+        
+        # no text deleted
+        self.assertEquals(nb_texts, Text.objects.count())
+
+
+    def test_pre_edit(self):
+        """
+        Pre edit text: should return number of comments to remove
+        """
+        resource = Resource(TextPreEditHandler)
+        request = HttpRequest()
+        user = User.objects.get(pk=1) 
+        setattr(request, 'user' , user)
+        request.method = 'POST'
+        setattr(request, 'POST' , {'new_format' : 'markdown', 'new_content' : u'ggg'})
+        setattr(request, 'flash' , {})
+    
+        response = resource(request, key='text_key_2', emitter_format='json')
+        self.assertEquals(response.content, '{"nb_removed": 3}')
+
+    def xtest_edit(self):
+        """
+        Edit text
+        """
+        resource = Resource(TextEditHandler)
+        request = HttpRequest()
+        setattr(request,'session',None)
+        user = User.objects.get(pk=1) 
+        setattr(request, 'user' , user)
+        request.method = 'POST'
+        setattr(request, 'POST' , {'new_format' : 'markdown', 'new_content' : u'ggg'})
+        setattr(request, 'flash' , {})
+    
+        response = resource(request, key='text_key_2', emitter_format='json')
+
+        self.assertEquals(Text.objects.get(pk=2).last_text_version.content , u'ggg')
+
+    
+    def test_setuser(self):
+        """
+        Set username/email for commenting
+        """
+        from django.test.client import Client
+        c = Client()
+        response = c.post('/setuser/', {'username': 'my_username', 'email': 'my_email'})
--- a/src/cm/tests/test_security.py	Fri Jun 11 11:04:23 2010 +0200
+++ b/src/cm/tests/test_security.py	Fri Jul 09 10:05:29 2010 +0200
@@ -17,19 +17,19 @@
     def test_access_rights(self):
         # anon user sees no text
         request = FalseRequest(None)                
-        self.assertEqual(get_texts_with_perm(request, 'can_view_text').count(), 0)
+        self.assertEqual(get_texts_with_perm(request, 'can_view_text').count(), 2)
 
         # user 1 sees all texts
         user1 = UserProfile.objects.get(id=1).user        
         request = FalseRequest(user1)       
-        self.assertEqual(get_texts_with_perm(request, 'can_view_text').count(), 3)
+        self.assertEqual(get_texts_with_perm(request, 'can_view_text').count(), 5)
         
-        # user 2 sees only 2 texts
+        # user 2 sees only 4 texts
         user2 = UserProfile.objects.get(id=2).user
         request = FalseRequest(user2)        
-        self.assertEqual(get_texts_with_perm(request, 'can_view_text').count(), 2)
+        self.assertEqual(get_texts_with_perm(request, 'can_view_text').count(), 4)
 
-        # user 4 sees only 2 texts (global manager but commentator on text 4
+        # user 4 manages only 2 texts (global manager but commentator on text 4
         user4 = UserProfile.objects.get(id=4).user
         request = FalseRequest(user4)
         self.assertEqual(get_texts_with_perm(request, 'can_manage_text').count(), 2)
--- a/src/cm/urls.py	Fri Jun 11 11:04:23 2010 +0200
+++ b/src/cm/urls.py	Fri Jul 09 10:05:29 2010 +0200
@@ -157,3 +157,7 @@
 urlpatterns += patterns('',
     (r'^jsi18n/$', 'django.views.i18n.javascript_catalog', js_info_dict),
 )
+
+urlpatterns += patterns('',
+   (r'^api/', include('cm.api.urls')),
+)