diff --git a/README.md b/README.md index 4768fec..50f450b 100644 --- a/README.md +++ b/README.md @@ -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. -n --indent=4 > dusken/fixtures/.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. diff --git a/README.services.md b/README.services.md index bc25ed0..87d675a 100644 --- a/README.services.md +++ b/README.services.md @@ -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 diff --git a/dusken/admin.py b/dusken/admin.py index 58e07da..1877611 100644 --- a/dusken/admin.py +++ b/dusken/admin.py @@ -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) diff --git a/dusken/api/member.py b/dusken/api/member.py index 610c634..89ef5c9 100644 --- a/dusken/api/member.py +++ b/dusken/api/member.py @@ -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 diff --git a/dusken/forms.py b/dusken/forms.py new file mode 100644 index 0000000..a022a24 --- /dev/null +++ b/dusken/forms.py @@ -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 diff --git a/dusken/models.py b/dusken/models.py index 3841862..ea24a19 100644 --- a/dusken/models.py +++ b/dusken/models.py @@ -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) diff --git a/dusken/settings-sample.py b/dusken/settings-sample.py index 67998bf..68758b4 100644 --- a/dusken/settings-sample.py +++ b/dusken/settings-sample.py @@ -118,6 +118,7 @@ INSTALLED_APPS = ( 'dusken', 'tastypie', 'south', + 'django_extensions', 'corsheaders', ) diff --git a/dusken/tests/member.py b/dusken/tests/member.py index b6109c5..010e7ca 100644 --- a/dusken/tests/member.py +++ b/dusken/tests/member.py @@ -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): """ diff --git a/recreatedb.sh b/recreatedb.sh index a6ccf77..0e00abd 100755 --- a/recreatedb.sh +++ b/recreatedb.sh @@ -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