Archived
Moves create member to separate resource.
* Adds CORS support Tests are failing
This commit is contained in:
1 parent
cacc1b1b94
commit
7bba5770e8
8 files changed
+136
-36
No files matched your search
@@ -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
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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())
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
Reference in new issue
Block a user