Moves create member to separate resource.

* Adds CORS support

Tests are failing
This commit is contained in:
nikolark committed 2013-11-24 01:52:29 +01:00
1 parent cacc1b1b94
commit 7bba5770e8
8 files changed
+136 -36

No files matched your search

+6 -2
View File
@@ -16,7 +16,7 @@ You can find the documentation at [dusken.readthedocs.org](http://dusken.readthe
pip install -r requirements.txt
cd dusken
cp settings-sample.py settings.py
# Edit database settings (sqlite3 is easiest to setup, but postgresql is recommended)
# Edit database settings (sqlite3 is easiest to setup, but postgresql is recommended. See discussion below.)
cd ..
./manage.py syncdb
./manage.py runserver
@@ -32,7 +32,6 @@ Download latest version of Vagrant from [downloads.vagrantup.com](http://downloa
vagrant up
vagrant ssh
## Generate Fixtures for Test
python manage.py dumpdata dusken.<model> -n --indent=4 > dusken/fixtures/<model>.json
@@ -50,3 +49,8 @@ A similar data model might allready [exist](http://www.databaseanswers.org/data_
* [Tastypie](https://django-tastypie.readthedocs.org/)
## SQLite3 vs PostgreSQL
Tests run much faster with SQLite, but it does not check foreign key constraints out of the box [[1]](http://stackoverflow.com/questions/6745763/enable-integrity-checking-with-sqlite-in-django).
SQLite is much easier to set up, but does not provide environment parity between development and production [[2]](http://12factor.net/dev-prod-parity).
+74 -25
View File
@@ -1,15 +1,20 @@
from django.conf.urls import url
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import User
from django.conf.urls import url
from django.db import IntegrityError
from django.shortcuts import get_object_or_404
from tastypie import fields
from tastypie.exceptions import ImmediateHttpResponse
from tastypie.authorization import Authorization
from tastypie.exceptions import ImmediateHttpResponse, BadRequest
from tastypie.http import HttpForbidden, HttpNoContent, HttpResponse, HttpAccepted
from tastypie import fields
from tastypie.resources import ModelResource, ALL
from dusken.models import *
from dusken.authorization import MyDjangoAuthorization
from dusken.authentication import MyApiKeyAuthentication
from dusken.api.groupsbymember import GroupsByMemberResource
from dusken.authentication import MyApiKeyAuthentication, ServiceAuthentication
from dusken.authorization import MyDjangoAuthorization
from dusken.models import *
from dusken.utils.api import generate_username, random_string
class MemberResource(ModelResource):
@@ -21,10 +26,10 @@ class MemberResource(ModelResource):
class Meta:
queryset = Member.objects.all()
resource_name = 'member'
list_allowed_methods = [ 'get', 'post' ]
list_allowed_methods = [ 'get' ]
detail_allowed_methods = [ 'get', 'patch', 'delete' ]
authentication = MyApiKeyAuthentication()
authorization = MyDjangoAuthorization()
authentication = MyApiKeyAuthentication() # Who are you?
authorization = MyDjangoAuthorization() # What are you allowed to do?
excludes = [ 'date_joined', 'password', 'is_active', 'is_staff', 'is_superuser', 'last_login' ]
filtering = {
'first_name' : [ 'exact' ],
@@ -70,25 +75,13 @@ class MemberResource(ModelResource):
def hydrate(self, bundle):
"""
Catches POST and PATCH requests and intercepts data.
Catches PATCH requests and intercepts data.
"""
update_existing_user = bundle.obj.id is not None
if update_existing_user:
# If the user already exists, return an error when attempting to change the username.
if bundle.data['username'] != bundle.obj.username:
raise ImmediateHttpResponse(HttpForbidden("You can't change your username."))
else:
# If creating new user, check if it already exists:
member_count = len(Member.objects.filter(username=bundle.data['username'])) \
+ len(Member.objects.filter(email=bundle.data['email'])) \
+ len(Member.objects.filter(phone_number=bundle.data['phone_number']))
if member_count > 0:
# TODO Specify what data is missing
raise ImmediateHttpResponse(HttpForbidden("User with given data already exists"))
# If the user already exists, return an error when attempting to change the username.
if bundle.data['username'] != bundle.obj.username:
raise ImmediateHttpResponse(HttpForbidden("You can't change your username."))
# Quick hack to allow new users to be created anonymously (unauthenticated)
bundle._anonymous_request_allowed = True
return bundle
def obj_update(self, bundle, **kwargs):
@@ -114,6 +107,7 @@ class MemberResource(ModelResource):
try:
country = Country.objects.get(name=new_address['country'])
except Country.DoesNotExist, e:
# FIXME change to badrequest?
raise ImmediateHttpResponse(HttpForbidden(
'The country "{}" does not exist.'.format(new_address['country']))
)
@@ -165,3 +159,58 @@ class MemberResource(ModelResource):
member.is_active = False
member.save()
return HttpNoContent()
class MemberCreateResource(ModelResource):
# TODO
# - get the post request to return auth info
# - do not return fields in excludes list
"""
This class provides the following endpoint:
(1) /api/v1/register/
The create Member resource is needed because:
- You need a user account to authenticate to our API
- and you need to to be authenticated to create a user account.
Ref: http://psjinx.com/programming/2013/06/07/so-you-want-to-create-users-using-djangotastypie/
"""
class Meta:
object_class = Member
resource_name = "register"
allowed_methods = ['post']
default_format = "application/json"
always_return_data = True
queryset = Member.objects.all()
authentication = ServiceAuthentication() # anyone
authorization = Authorization() # can do what they want with anything
excludes = [ 'date_joined', 'password', 'is_active', 'is_staff', 'is_superuser', 'last_login' ]
def obj_create(self, bundle, request=None, **kwargs):
# TODO factor out validation http://django-tastypie.readthedocs.org/en/latest/validation.html
# email must be unique
email = bundle.data.get('email')
if(len(Member.objects.filter(email=email)) > 0):
raise BadRequest("E-mail '{0}' already exists".format(email))
password = bundle.data.get('password')
if not password:
# generate a random password if it is not set
bundle.data['password'] = random_string(32);
username = bundle.data.get('username')
if not username:
username = bundle.data['username'] = generate_username(bundle.data)
# FIXME unsafe
while(len(Member.objects.filter(username=username)) > 0):
username = bundle.data['username'] = generate_username(bundle.data)
# When creating a new user, check if it already exists:
try:
bundle = super(MemberCreateResource, self).obj_create(bundle, **kwargs)
bundle.obj.set_password(password)
bundle.obj.save()
except IntegrityError as e:
print e
raise BadRequest('That username already exists')
return bundle
+14 -1
View File
@@ -1,6 +1,8 @@
from tastypie.authentication import ApiKeyAuthentication
from tastypie.authentication import ApiKeyAuthentication, Authentication
from tastypie.http import HttpUnauthorized
# TODO replace with Oauth2 http://django-tastypie.readthedocs.org/en/latest/cookbook.html#creating-a-full-oauth-2-0-api
class MyApiKeyAuthentication(ApiKeyAuthentication):
"""
Override method below to use our custom User model
@@ -40,3 +42,14 @@ class MyApiKeyAuthentication(ApiKeyAuthentication):
request.user = user
return key_auth_check
class ServiceAuthentication(Authentication):
pass
#
# TODO auth for services
#
#def is_authenticated(self, request, **kwargs):
# return True
# return False
+13 -2
View File
@@ -95,6 +95,7 @@ MIDDLEWARE_CLASSES = (
'django.contrib.messages.middleware.MessageMiddleware',
# Uncomment the next line for simple clickjacking protection:
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'corsheaders.middleware.CorsMiddleware',
)
ROOT_URLCONF = 'dusken.urls'
@@ -119,7 +120,8 @@ INSTALLED_APPS = (
'django.contrib.admindocs',
'dusken',
'tastypie',
'south'
'south',
'corsheaders',
)
@@ -152,7 +154,16 @@ LOGGING = {
}
}
TASTYPIE_DEFAULT_FORMATS = ['json']
# Our own User class used for authentication.
AUTH_USER_MODEL = 'dusken.Member'
TASTYPIE_DEFAULT_FORMATS = ['json']
CORS_ORIGIN_ALLOW_ALL = False
# Allow Ajax calls only from tuple of domain:port
CORS_ORIGIN_WHITELIST = (
'127.0.0.1:3030',
'dusken.neuf.no:3030',
'dusken.neuf.no',
)
+2 -1
View File
@@ -2,7 +2,7 @@ from django.conf.urls import patterns, include, url
from django.contrib import admin
from tastypie.api import Api
from dusken.api.member import MemberResource
from dusken.api.member import MemberResource, MemberCreateResource
from dusken.api.membership import MembershipResource
from dusken.api.group import GroupResource
from dusken.api.institution import InstitutionResource
@@ -11,6 +11,7 @@ admin.autodiscover()
v1_api = Api(api_name='v1')
v1_api.register(MemberResource())
v1_api.register(MemberCreateResource())
v1_api.register(MembershipResource())
v1_api.register(GroupResource())
v1_api.register(InstitutionResource())
+19
View File
@@ -0,0 +1,19 @@
import string
import random
from django.utils.text import slugify
def random_string(max_length=15):
return u''.join(random.choice(string.ascii_lowercase + string.digits) for x in range(max_length))
def generate_username(data, max_length=15):
username = u""
# 1. join first and last name
username = u"{}{}".format(
slugify(data.get('first_name', u'')),
slugify(data.get('last_name', u'')))
if not username:
# 2. if no first and last, generate random
# TODO set flag so that unspecified username can be cheched later
username = random_string(max_length)
return username
+3 -1
View File
@@ -12,7 +12,9 @@ def authenticate(request):
# get apikey and return it
user = form.get_user()
api_key = user.api_key.key
return HttpResponse(json.dumps({'api_key': api_key}), content_type='application/javascript; charset=utf8')
response = HttpResponse(json.dumps({'api_key': api_key}), content_type='application/javascript; charset=utf8', )
response['Access-Control-Allow-Origin'] = '*'
return response
else:
return HttpResponse(json.dumps({'error': 'unauthorized'}), content_type='application/javascript; charset=utf8', status=401)
else:
+5 -4
View File
@@ -1,8 +1,9 @@
Django==1.5.4
South==0.8.2
django-tastypie==0.10.0
psycopg2==2.5.1
django-cors-headers==0.12
django-extensions==1.2.2
django-sphinxdoc==1.2.0
sphinxcontrib-httpdomain==1.1.9
django-tastypie==0.10.0
psycopg2==2.5.1
python-mimeparse==0.1.4
South==0.8.2
sphinxcontrib-httpdomain==1.1.9