Work on MemberCreateResource, cleanups, README updates, misc

This commit is contained in:
nikolark committed 2013-12-08 23:51:17 +01:00
1 parent 36dd948e8e
commit 63894a93ff
9 files changed
+88 -68

No files matched your search

+17 -17
View File
@@ -18,7 +18,7 @@ You can find the documentation at [dusken.readthedocs.org](http://dusken.readthe
cp settings-sample.py settings.py cp settings-sample.py settings.py
# Edit database settings (sqlite3 is easiest to setup, but postgresql is recommended. See discussion below.) # Edit database settings (sqlite3 is easiest to setup, but postgresql is recommended. See discussion below.)
cd .. cd ..
./manage.py syncdb ./manage.py syncdb --all
./manage.py runserver ./manage.py runserver
@@ -32,29 +32,29 @@ Download latest version of Vagrant from [downloads.vagrantup.com](http://downloa
vagrant up vagrant up
vagrant ssh vagrant ssh
## Test coverage ## Development tasks
pip install coverage # Reset DB
coverage run --source='dusken' manage.py test dusken python manage.py reset_db --router=default && python manage.py syncdb --all
coverage report -m
## Generate Fixtures for Test # Generate Fixtures for Test
python manage.py dumpdata dusken.<model> -n --indent=4 > dusken/fixtures/<model>.json python manage.py dumpdata dusken.<model> -n --indent=4 > dusken/fixtures/<model>.json
## Database Diagram Generation # Database Diagram Generation
sudo apt-get install graphviz libgraphviz-dev # system requirements
pip install pygraphviz django-extensions # python requirements
python manage.py graph_models -o diagramfile.png dusken auth
sudo apt-get install graphviz libgraphviz-dev ## Development references
pip install pygraphviz django-extensions
python manage.py graph_models -o diagramfile.png dusken
## Design References
A similar data model might allready [exist](http://www.databaseanswers.org/data_models/generic_foundation/index.htm) [out](http://www.databaseanswers.org/data_models/organisations_and_people_and_transactions/index.htm) [there](http://www.databaseanswers.org/data_models/organisations_and_people/index.htm), [somewhere](http://www.databaseanswers.org/data_models/magazine_subscriptions/index.htm).
## Developer References
* [Tastypie](https://django-tastypie.readthedocs.org/) * [Tastypie](https://django-tastypie.readthedocs.org/)
## SQLite3 vs PostgreSQL ## Database
### Design references
A similar data model might allready [exist](http://www.databaseanswers.org/data_models/generic_foundation/index.htm) [out](http://www.databaseanswers.org/data_models/organisations_and_people_and_transactions/index.htm) [there](http://www.databaseanswers.org/data_models/organisations_and_people/index.htm), [somewhere](http://www.databaseanswers.org/data_models/magazine_subscriptions/index.htm).
### SQLite3 vs PostgreSQL
* SQLite runs tests much faster * SQLite runs tests much faster
* SQLite does not check foreign key constraints out of the box [[1]](http://stackoverflow.com/questions/6745763/enable-integrity-checking-with-sqlite-in-django) [FIXED]. * SQLite does not check foreign key constraints out of the box [[1]](http://stackoverflow.com/questions/6745763/enable-integrity-checking-with-sqlite-in-django) [FIXED].
* SQLite is easier to set up. * SQLite is easier to set up.
+6 -4
View File
@@ -1,20 +1,22 @@
# Service design # Service design
ApiHook-model with the following attributes: ServiceHook-model with the following attributes [DONE]:
* event * event
* callback-URL * callback-URL
* member * member
* (active) * (active)
Our own Tastypie BaseResource which triggers events based on: Our own Tastypie BaseResource which triggers events based on:
* resource_name / type of object * resource_name / type of object
* method used. * method used.
Use a queue for service tasks, f.ex rq (python library with redis backend)
Add logic to our Django Basemodel Add logic to our Django Basemodel
Use a queue for service tasks: Take a look at Django signals:
* rq (python library with redis backend)
Take a look at signals
* pre_save * pre_save
* post_save * post_save
+4 -1
View File
@@ -30,9 +30,12 @@ admin.site.unregister(django.contrib.auth.models.Group)
admin.site.register(Address) admin.site.register(Address)
admin.site.register(Country) admin.site.register(Country)
admin.site.register(Division)
admin.site.register(Group) admin.site.register(Group)
admin.site.register(Institution) admin.site.register(Institution)
admin.site.register(Member, MemberAdmin) admin.site.register(Member, MemberAdmin)
admin.site.register(MembershipType) admin.site.register(MemberMeta)
admin.site.register(Membership) admin.site.register(Membership)
admin.site.register(MembershipType)
admin.site.register(PlaceOfStudy) admin.site.register(PlaceOfStudy)
admin.site.register(ServiceHook)
+22 -40
View File
@@ -9,12 +9,14 @@ from tastypie.exceptions import ImmediateHttpResponse, BadRequest
from tastypie.http import HttpForbidden, HttpNoContent, HttpResponse, HttpAccepted from tastypie.http import HttpForbidden, HttpNoContent, HttpResponse, HttpAccepted
from tastypie import fields from tastypie import fields
from tastypie.resources import ModelResource, ALL from tastypie.resources import ModelResource, ALL
from tastypie.validation import CleanedDataFormValidation
from dusken.api.groupsbymember import GroupsByMemberResource from dusken.api.groupsbymember import GroupsByMemberResource
from dusken.authentication import MyApiKeyAuthentication, ServiceAuthentication from dusken.authentication import MyApiKeyAuthentication, ServiceAuthentication
from dusken.authorization import MyDjangoAuthorization from dusken.authorization import MyDjangoAuthorization
from dusken.models import * from dusken.models import *
from dusken.utils.api import generate_username, random_string from dusken.utils.api import generate_username, random_string
from dusken.forms import MemberCreateForm
class MemberResource(ModelResource): class MemberResource(ModelResource):
@@ -57,28 +59,6 @@ class MemberResource(ModelResource):
resource = MemberCreateResource() resource = MemberCreateResource()
return resource.dispatch_list(request, **kwargs) return resource.dispatch_list(request, **kwargs)
#def apply_filters(self, request, applicable_filters):
# # Extra filters are used for filtering by attributes in User class. Find out if there
# # are any, and if so, apply them.
# extra_filters = {}
# if 'username__exact' in applicable_filters:
# extra_filters['username'] = applicable_filters.pop('username__exact')
# if 'first_name__exact' in applicable_filters:
# extra_filters['first_name'] = applicable_filters.pop('first_name__exact')
# if 'last_name__exact' in applicable_filters:
# extra_filters['last_name'] = applicable_filters.pop('last_name__exact')
# if 'email__exact' in applicable_filters:
# extra_filters['email'] = applicable_filters.pop('email__exact')
# filtered = super(MemberResource, self).apply_filters(request, applicable_filters)
# for key,value in extra_filters.items():
# filtered = filter(lambda m: getattr(m, key) == value, filtered)
# return filtered
def hydrate(self, bundle): def hydrate(self, bundle):
""" """
Catches PATCH requests and intercepts data. Catches PATCH requests and intercepts data.
@@ -105,6 +85,8 @@ class MemberResource(ModelResource):
def _update_address(self, address, new_address): def _update_address(self, address, new_address):
if 'street_address' in new_address: if 'street_address' in new_address:
address.street_address = new_address['street_address'] address.street_address = new_address['street_address']
if 'street_address_two' in new_address:
address.street_address_two = new_address['street_address_two']
if 'city' in new_address: if 'city' in new_address:
address.city = new_address['city'] address.city = new_address['city']
if 'postal_code' in new_address: if 'postal_code' in new_address:
@@ -169,7 +151,7 @@ class MemberResource(ModelResource):
class MemberCreateResource(ModelResource): class MemberCreateResource(ModelResource):
# TODO # TODO
# - get the post request to return auth info # - get the post request to return auth info
# - do not return fields in excludes list # - Code 400 is triggered on validation error instead of 403 in tastypie.resource.ModelResource.save().
""" """
This class provides the following endpoint: This class provides the following endpoint:
(1) /api/v1/member/register/ (1) /api/v1/member/register/
@@ -189,27 +171,28 @@ class MemberCreateResource(ModelResource):
authorization = Authorization() # can do what they want with anything authorization = Authorization() # can do what they want with anything
always_return_data = True always_return_data = True
excludes = [ 'date_joined', 'password', 'is_active', 'is_staff', 'is_superuser', 'last_login' ] excludes = [ 'date_joined', 'password', 'is_active', 'is_staff', 'is_superuser', 'last_login' ]
validation = CleanedDataFormValidation(form_class=MemberCreateForm)
def dehydrate(self, bundle):
# Do not return fields in excludes
# Ref: https://groups.google.com/forum/#!topic/django-tastypie/WE_d92Fkl-I/discussion
for field in getattr(self.Meta, 'excludes', []):
if field in bundle.data:
del bundle.data[field]
return bundle
def obj_create(self, bundle, request=None, **kwargs): def obj_create(self, bundle, request=None, **kwargs):
# TODO factor out validation http://django-tastypie.readthedocs.org/en/latest/validation.html # TODO factor out validation and cleaning of fields:
# email must be unique # * password
email = bundle.data.get('email') # * username
if(len(Member.objects.filter(email__iexact=email)) > 0):
raise ImmediateHttpResponse(HttpForbidden("E-mail '{0}' already exists".format(email)))
bundle.data['email'] = Member.objects.normalize_email(email)
password = bundle.data.get('password') password = bundle.data.get('password')
if not password: if not password:
# generate a random password if it is not set # generate a random password if it is not provided
bundle.data['password'] = Member.objects.make_random_password(length=32) bundle.data['password'] = Member.objects.make_random_password(length=32)
username = bundle.data.get('username') if not bundle.data.get('username'):
if not username: # Simply use the provided email
username = bundle.data['username'] = generate_username(bundle.data) bundle.data['username'] = bundle.data['email']
# 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: # When creating a new user, check if it already exists:
try: try:
@@ -217,7 +200,6 @@ class MemberCreateResource(ModelResource):
bundle.obj.set_password(password) bundle.obj.set_password(password)
bundle.obj.save() bundle.obj.save()
except IntegrityError as e: except IntegrityError as e:
print e raise ImmediateHttpResponse(HttpForbidden("Database error: {0}".format(e)))
raise ImmediateHttpResponse(HttpForbidden('That username already exists'))
return bundle return bundle
+18
View File
@@ -0,0 +1,18 @@
from django import forms
from models import Member
class MemberCreateForm(forms.ModelForm):
# TODO
# custom validation of:
# - phonenumber (phonenumbers package, unique)
# - email (+unique)
# - legacy_id (unique)
#
# clean:
# - phonenumber
# - username
# - password
class Meta:
model = Member
+17 -4
View File
@@ -18,9 +18,9 @@ class Member(django.contrib.auth.models.AbstractUser):
username=self.username) username=self.username)
return u"{username}".format(username=self.username) return u"{username}".format(username=self.username)
phone_number = models.CharField(max_length=30, unique=True, null=True, blank=True) phone_number = models.CharField(max_length=30, null=True, blank=True)
date_of_birth = models.DateField(blank=True, null=True) date_of_birth = models.DateField(blank=True, null=True)
legacy_id = models.IntegerField(unique=True, null=True, blank=True) legacy_id = models.IntegerField(null=True, blank=True)
address = models.OneToOneField('dusken.Address', null=True, blank=True) address = models.OneToOneField('dusken.Address', null=True, blank=True)
place_of_study = models.ManyToManyField('dusken.PlaceOfStudy', null=True, blank=True) place_of_study = models.ManyToManyField('dusken.PlaceOfStudy', null=True, blank=True)
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
@@ -136,9 +136,9 @@ class Group(django.contrib.auth.models.Group):
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True) updated = models.DateTimeField(auto_now=True)
class VolunteerGroup(BaseModel): class Division(BaseModel):
""" """
Associations Associations, comittee or similar
""" """
def __unicode__(self): def __unicode__(self):
return u"{}".format(self.name) return u"{}".format(self.name)
@@ -149,6 +149,19 @@ class VolunteerGroup(BaseModel):
parent = models.ForeignKey('self', null=True, blank=True, related_name='children') parent = models.ForeignKey('self', null=True, blank=True, related_name='children')
groups = models.ManyToManyField('dusken.Group', null=True, blank=True) # permissions groups = models.ManyToManyField('dusken.Group', null=True, blank=True) # permissions
class ServiceHook(BaseModel):
"""
Events with callback_urls
"""
def __unicode__(self):
return u"{}".format(self.name)
event = models.CharField(max_length=255)
member = models.ForeignKey('dusken.Member')
is_active = models.BooleanField(default=True)
callback_url = models.TextField()
# SIGNALS # # SIGNALS #
########### ###########
models.signals.post_save.connect(create_api_key, sender=Member) models.signals.post_save.connect(create_api_key, sender=Member)
+1
View File
@@ -118,6 +118,7 @@ INSTALLED_APPS = (
'dusken', 'dusken',
'tastypie', 'tastypie',
'south', 'south',
'django_extensions',
'corsheaders', 'corsheaders',
) )
+2 -1
View File
@@ -138,7 +138,8 @@ class MemberTest(MemberTestBase):
format = 'json', format = 'json',
data = data, data = data,
authentication=self.creds) authentication=self.creds)
self.assertHttpForbidden(resp) #self.assertHttpForbidden(resp) # FIXME see MemberCreateResource in member.py
self.assertHttpBadRequest(resp)
def test_patch_change_member(self): def test_patch_change_member(self):
""" """
+1 -1
View File
@@ -6,7 +6,7 @@ if [ -f dusken.db ]; then
fi fi
echo 'Synchronizing database...' echo 'Synchronizing database...'
python manage.py syncdb --noinput > /dev/null python manage.py syncdb --noinput --all > /dev/null
echo 'Migrating database...' echo 'Migrating database...'
python manage.py migrate > /dev/null python manage.py migrate > /dev/null