Archived
Work on MemberCreateResource, cleanups, README updates, misc
This commit is contained in:
1 parent
36dd948e8e
commit
63894a93ff
9 files changed
+89
-69
No files matched your search
@@ -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
|
||||||
|
|
||||||
|
|
||||||
@@ -31,30 +31,30 @@ Download latest version of Vagrant from [downloads.vagrantup.com](http://downloa
|
|||||||
vagrant plugin install vagrant-salt
|
vagrant plugin install vagrant-salt
|
||||||
vagrant up
|
vagrant up
|
||||||
vagrant ssh
|
vagrant ssh
|
||||||
|
|
||||||
|
## Development tasks
|
||||||
|
# Reset DB
|
||||||
|
python manage.py reset_db --router=default && python manage.py syncdb --all
|
||||||
|
|
||||||
## Test coverage
|
# Generate Fixtures for Test
|
||||||
pip install coverage
|
|
||||||
coverage run --source='dusken' manage.py test dusken
|
|
||||||
coverage report -m
|
|
||||||
|
|
||||||
## 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
@@ -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
@@ -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
@@ -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
|
||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ INSTALLED_APPS = (
|
|||||||
'dusken',
|
'dusken',
|
||||||
'tastypie',
|
'tastypie',
|
||||||
'south',
|
'south',
|
||||||
|
'django_extensions',
|
||||||
'corsheaders',
|
'corsheaders',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user