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
|
||||
# Edit database settings (sqlite3 is easiest to setup, but postgresql is recommended. See discussion below.)
|
||||
cd ..
|
||||
./manage.py syncdb
|
||||
./manage.py syncdb --all
|
||||
./manage.py runserver
|
||||
|
||||
|
||||
@@ -31,30 +31,30 @@ Download latest version of Vagrant from [downloads.vagrantup.com](http://downloa
|
||||
vagrant plugin install vagrant-salt
|
||||
vagrant up
|
||||
vagrant ssh
|
||||
|
||||
## Development tasks
|
||||
# Reset DB
|
||||
python manage.py reset_db --router=default && python manage.py syncdb --all
|
||||
|
||||
## Test coverage
|
||||
pip install coverage
|
||||
coverage run --source='dusken' manage.py test dusken
|
||||
coverage report -m
|
||||
|
||||
## Generate Fixtures for Test
|
||||
# Generate Fixtures for Test
|
||||
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
|
||||
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
|
||||
## Development references
|
||||
|
||||
* [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 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.
|
||||
|
||||
+6
-4
@@ -1,20 +1,22 @@
|
||||
# Service design
|
||||
|
||||
ApiHook-model with the following attributes:
|
||||
ServiceHook-model with the following attributes [DONE]:
|
||||
|
||||
* event
|
||||
* callback-URL
|
||||
* member
|
||||
* (active)
|
||||
|
||||
Our own Tastypie BaseResource which triggers events based on:
|
||||
|
||||
* resource_name / type of object
|
||||
* method used.
|
||||
|
||||
Use a queue for service tasks, f.ex rq (python library with redis backend)
|
||||
|
||||
Add logic to our Django Basemodel
|
||||
|
||||
Use a queue for service tasks:
|
||||
* rq (python library with redis backend)
|
||||
Take a look at Django signals:
|
||||
|
||||
Take a look at signals
|
||||
* pre_save
|
||||
* post_save
|
||||
+4
-1
@@ -30,9 +30,12 @@ admin.site.unregister(django.contrib.auth.models.Group)
|
||||
|
||||
admin.site.register(Address)
|
||||
admin.site.register(Country)
|
||||
admin.site.register(Division)
|
||||
admin.site.register(Group)
|
||||
admin.site.register(Institution)
|
||||
admin.site.register(Member, MemberAdmin)
|
||||
admin.site.register(MembershipType)
|
||||
admin.site.register(MemberMeta)
|
||||
admin.site.register(Membership)
|
||||
admin.site.register(MembershipType)
|
||||
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 import fields
|
||||
from tastypie.resources import ModelResource, ALL
|
||||
from tastypie.validation import CleanedDataFormValidation
|
||||
|
||||
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
|
||||
from dusken.forms import MemberCreateForm
|
||||
|
||||
|
||||
class MemberResource(ModelResource):
|
||||
@@ -57,28 +59,6 @@ class MemberResource(ModelResource):
|
||||
resource = MemberCreateResource()
|
||||
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):
|
||||
"""
|
||||
Catches PATCH requests and intercepts data.
|
||||
@@ -105,6 +85,8 @@ class MemberResource(ModelResource):
|
||||
def _update_address(self, address, new_address):
|
||||
if 'street_address' in new_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:
|
||||
address.city = new_address['city']
|
||||
if 'postal_code' in new_address:
|
||||
@@ -169,7 +151,7 @@ class MemberResource(ModelResource):
|
||||
class MemberCreateResource(ModelResource):
|
||||
# TODO
|
||||
# - 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:
|
||||
(1) /api/v1/member/register/
|
||||
@@ -189,27 +171,28 @@ class MemberCreateResource(ModelResource):
|
||||
authorization = Authorization() # can do what they want with anything
|
||||
always_return_data = True
|
||||
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):
|
||||
# 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__iexact=email)) > 0):
|
||||
raise ImmediateHttpResponse(HttpForbidden("E-mail '{0}' already exists".format(email)))
|
||||
|
||||
bundle.data['email'] = Member.objects.normalize_email(email)
|
||||
|
||||
# TODO factor out validation and cleaning of fields:
|
||||
# * password
|
||||
# * username
|
||||
password = bundle.data.get('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)
|
||||
|
||||
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)
|
||||
if not bundle.data.get('username'):
|
||||
# Simply use the provided email
|
||||
bundle.data['username'] = bundle.data['email']
|
||||
|
||||
# When creating a new user, check if it already exists:
|
||||
try:
|
||||
@@ -217,7 +200,6 @@ class MemberCreateResource(ModelResource):
|
||||
bundle.obj.set_password(password)
|
||||
bundle.obj.save()
|
||||
except IntegrityError as e:
|
||||
print e
|
||||
raise ImmediateHttpResponse(HttpForbidden('That username already exists'))
|
||||
raise ImmediateHttpResponse(HttpForbidden("Database error: {0}".format(e)))
|
||||
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
place_of_study = models.ManyToManyField('dusken.PlaceOfStudy', null=True, blank=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)
|
||||
updated = models.DateTimeField(auto_now=True)
|
||||
|
||||
class VolunteerGroup(BaseModel):
|
||||
class Division(BaseModel):
|
||||
"""
|
||||
Associations
|
||||
Associations, comittee or similar
|
||||
"""
|
||||
def __unicode__(self):
|
||||
return u"{}".format(self.name)
|
||||
@@ -149,6 +149,19 @@ class VolunteerGroup(BaseModel):
|
||||
parent = models.ForeignKey('self', null=True, blank=True, related_name='children')
|
||||
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 #
|
||||
###########
|
||||
models.signals.post_save.connect(create_api_key, sender=Member)
|
||||
|
||||
@@ -118,6 +118,7 @@ INSTALLED_APPS = (
|
||||
'dusken',
|
||||
'tastypie',
|
||||
'south',
|
||||
'django_extensions',
|
||||
'corsheaders',
|
||||
)
|
||||
|
||||
|
||||
@@ -138,7 +138,8 @@ class MemberTest(MemberTestBase):
|
||||
format = 'json',
|
||||
data = data,
|
||||
authentication=self.creds)
|
||||
self.assertHttpForbidden(resp)
|
||||
#self.assertHttpForbidden(resp) # FIXME see MemberCreateResource in member.py
|
||||
self.assertHttpBadRequest(resp)
|
||||
|
||||
def test_patch_change_member(self):
|
||||
"""
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ if [ -f dusken.db ]; then
|
||||
fi
|
||||
|
||||
echo 'Synchronizing database...'
|
||||
python manage.py syncdb --noinput > /dev/null
|
||||
python manage.py syncdb --noinput --all > /dev/null
|
||||
echo 'Migrating database...'
|
||||
python manage.py migrate > /dev/null
|
||||
|
||||
|
||||
Reference in new issue
Block a user