From 945f3ac6075911a85f3fc40ec97b7132056a0511 Mon Sep 17 00:00:00 2001 From: Nikolai Kristiansen Date: Thu, 26 Dec 2013 02:14:55 +0100 Subject: [PATCH] Starts MeResource, groupprofile, misc --- dusken/admin.py | 3 -- dusken/api/abstract_groupmember.py | 3 +- dusken/api/address.py | 13 ++++++ dusken/api/division.py | 13 ++++++ dusken/api/group.py | 2 +- dusken/api/groupsbymember.py | 3 +- dusken/api/me.py | 70 ++++++++++++++++++++++++++++++ dusken/api/membersbygroup.py | 3 +- dusken/authorization.py | 7 ++- dusken/fixtures/group.json | 10 +++-- dusken/models.py | 47 ++++++++++++-------- dusken/urls.py | 2 + 12 files changed, 145 insertions(+), 31 deletions(-) create mode 100644 dusken/api/address.py create mode 100644 dusken/api/division.py create mode 100644 dusken/api/me.py diff --git a/dusken/admin.py b/dusken/admin.py index 1877611..bb56d31 100644 --- a/dusken/admin.py +++ b/dusken/admin.py @@ -26,12 +26,9 @@ class MemberAdmin(admin.ModelAdmin): return self.readonly_fields + ('username', ) return self.readonly_fields -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(MemberMeta) diff --git a/dusken/api/abstract_groupmember.py b/dusken/api/abstract_groupmember.py index 11b03b5..ae347e5 100644 --- a/dusken/api/abstract_groupmember.py +++ b/dusken/api/abstract_groupmember.py @@ -5,7 +5,8 @@ from tastypie.bundle import Bundle from tastypie.exceptions import ImmediateHttpResponse from tastypie.http import HttpNotFound, HttpAccepted, HttpCreated, HttpResponse, HttpBadRequest, HttpNoContent from tastypie.resources import Resource -from dusken.models import Member, Group +from dusken.models import Member +from django.contrib.auth.models import Group import logging diff --git a/dusken/api/address.py b/dusken/api/address.py new file mode 100644 index 0000000..1e60138 --- /dev/null +++ b/dusken/api/address.py @@ -0,0 +1,13 @@ +from tastypie.resources import ModelResource +from tastypie import fields + +from dusken.models import Address +from dusken.authorization import MyDjangoAuthorization +from dusken.authentication import OAuth20Authentication + +class AddressResource(ModelResource): + class Meta: + queryset = Address.objects.all() + resource_name = 'address' + authorization = MyDjangoAuthorization() + authentication = OAuth20Authentication() diff --git a/dusken/api/division.py b/dusken/api/division.py new file mode 100644 index 0000000..9d6ba1c --- /dev/null +++ b/dusken/api/division.py @@ -0,0 +1,13 @@ +from tastypie.resources import ModelResource +from tastypie import fields + +from dusken.models import Division +from dusken.authorization import MyDjangoAuthorization +from dusken.authentication import OAuth20Authentication + +class DivisionResource(ModelResource): + class Meta: + queryset = Division.objects.all() + resource_name = 'division' + authorization = MyDjangoAuthorization() + authentication = OAuth20Authentication() diff --git a/dusken/api/group.py b/dusken/api/group.py index d5c7ae2..bd252f9 100644 --- a/dusken/api/group.py +++ b/dusken/api/group.py @@ -2,7 +2,7 @@ from django.conf.urls import url from tastypie.authorization import Authorization from tastypie.resources import ModelResource, ALL -from dusken.models import Group +from django.contrib.auth.models import Group from dusken.api.membersbygroup import MembersByGroupResource class GroupResource(ModelResource): diff --git a/dusken/api/groupsbymember.py b/dusken/api/groupsbymember.py index 5288767..3fd8346 100644 --- a/dusken/api/groupsbymember.py +++ b/dusken/api/groupsbymember.py @@ -1,6 +1,7 @@ from django.shortcuts import get_object_or_404 from dusken.api.abstract_groupmember import GroupMemberObject, AbstractGroupMemberResource -from dusken.models import Member, Group +from dusken.models import Member +from django.contrib.auth.models import Group import logging diff --git a/dusken/api/me.py b/dusken/api/me.py new file mode 100644 index 0000000..cba549d --- /dev/null +++ b/dusken/api/me.py @@ -0,0 +1,70 @@ +from django.conf.urls import url +from django.contrib.auth.hashers import make_password +from django.contrib.auth.models import User +from django.db import IntegrityError +from django.shortcuts import get_object_or_404 + +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 tastypie.validation import CleanedDataFormValidation + +from dusken.authentication import ServiceAuthentication, OAuth20Authentication +from dusken.authorization import MyDjangoAuthorization +from dusken.models import * +from dusken.utils.api import generate_username, random_string +from dusken.forms import MemberCreateForm + +from dusken.api.division import DivisionResource +from dusken.api.membership import MembershipResource +from dusken.api.address import AddressResource +from dusken.api.groupsbymember import GroupsByMemberResource + +class MeResource(ModelResource): + """ + This class provides following endpoints: + (1) /api/v1/me/ + """ + groups = fields.ToManyField(GroupsByMemberResource, 'groups') + divisions = fields.ToManyField(DivisionResource, 'division_set') + memberships = fields.ToManyField(MembershipResource, 'membership_set') + #address = fields.ForeignKey(AddressResource, 'address') + + class Meta: + queryset = Member.objects.all() + resource_name = 'me' + list_allowed_methods = [ 'get' ] + detail_allowed_methods = [ 'get'] + authentication = OAuth20Authentication() # Who are you? + authorization = MyDjangoAuthorization() # What are you allowed to do? + excludes = [ 'date_joined', 'password', 'is_active', 'is_staff', 'is_superuser', 'last_login' ] + + def get_object_list(self, request): + # Only return the authenticated user's member object + return super(MeResource, self).get_object_list(request).filter(pk=request.user.id) + + def detail_groups(self, request, **kwargs): + resource = GroupsByMemberResource() + return resource.dispatch_detail(request, **kwargs) + + def dehydrate(self, bundle): + """ + Catches GET requests and adds more data. + """ + bundle.data['has_valid_membership'] = bundle.obj.has_valid_membership() + # Add address: + #address = bundle.obj.address + #if address is None: + # bundle.data['address'] = None + #else: + # # TODO: There has to be a better way... + # bundle.data['address'] = { + # 'city' : address.city, + # 'country' : address.country.name, + # 'postal_code' : address.postal_code, + # 'street_address' : address.street_address, + # } + + return bundle diff --git a/dusken/api/membersbygroup.py b/dusken/api/membersbygroup.py index bed7315..24cb121 100644 --- a/dusken/api/membersbygroup.py +++ b/dusken/api/membersbygroup.py @@ -1,6 +1,7 @@ from django.shortcuts import get_object_or_404 from dusken.api.abstract_groupmember import GroupMemberObject, AbstractGroupMemberResource -from dusken.models import Member, Group +from dusken.models import Member +from django.contrib.auth.models import Group import logging diff --git a/dusken/authorization.py b/dusken/authorization.py index f288418..87f9acd 100644 --- a/dusken/authorization.py +++ b/dusken/authorization.py @@ -3,7 +3,7 @@ from tastypie.authentication import Authentication from tastypie.authorization import DjangoAuthorization from tastypie.exceptions import Unauthorized -# TODO add django-guardian for object level/row level permissions. +# TODO add django-guardian for object level/row level permissions (apply_limit). # Ref: https://gist.github.com/7wonders/6557760 # Usage example: User in group x is admin of group y # Literal usage example: Runar in group KAK-styret is admin of group KAK. @@ -74,3 +74,8 @@ class MyDjangoAuthorization(DjangoAuthorization): raise Unauthorized("You are not allowed to access that resource.") return True + + #def apply_limit(self): + # pass + # TODO limit objects which a user are allowed to do crud operations on with django-guardian. + # apply_limit is called by apply_authorization_limits diff --git a/dusken/fixtures/group.json b/dusken/fixtures/group.json index b2a3123..9fc9a30 100644 --- a/dusken/fixtures/group.json +++ b/dusken/fixtures/group.json @@ -8,11 +8,12 @@ }, { "pk": 1, - "model": "dusken.group", + "model": "dusken.groupprofile", "fields": { "updated": "2013-09-18T18:31:27.377Z", "created": "2013-09-18T18:14:49.159Z", - "posix_name": "EDB" + "posix_name": "EDB", + "group": 1 } }, { @@ -24,11 +25,12 @@ }, { "pk": 2, - "model": "dusken.group", + "model": "dusken.groupprofile", "fields": { "updated": "2013-09-18T18:32:02.861Z", "created": "2013-09-18T18:32:02.861Z", - "posix_name": "KAK" + "posix_name": "KAK", + "group": 2 } } ] diff --git a/dusken/models.py b/dusken/models.py index ea24a19..f4da27b 100644 --- a/dusken/models.py +++ b/dusken/models.py @@ -1,8 +1,10 @@ import django +import datetime + from django.db import models from tastypie.models import create_api_key -class BaseModel(models.Model): +class AbstractBaseModel(models.Model): created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) @@ -28,9 +30,14 @@ class Member(django.contrib.auth.models.AbstractUser): def owner(self): return self + + def has_valid_membership(self): + # FIXME more than one membership? + # FIXME membership_type? + membership = self.membership_set.filter(end_date__gt=datetime.datetime.now()) + return len(membership) == 1 and membership.is_valid() - -class Membership(BaseModel): +class Membership(AbstractBaseModel): def __unicode__(self): return u"{0}: {1} - {2}".format( self.member, @@ -46,10 +53,13 @@ class Membership(BaseModel): def expires(self): return self.end_date + def is_valid(self): + return self.payment is not None + def owner(self): return self.member -class MembershipType(BaseModel): +class MembershipType(AbstractBaseModel): def __unicode__(self): return u"{}".format(self.name) @@ -57,7 +67,7 @@ class MembershipType(BaseModel): is_active = models.BooleanField(default=True) does_not_expire = models.BooleanField(default=False) -class PaymentType(models.Model): +class PaymentType(AbstractBaseModel): def __unicode__(self): return u"{}".format(self.name) @@ -65,7 +75,7 @@ class PaymentType(models.Model): is_active = models.BooleanField(default=True) -class Payment(BaseModel): +class Payment(AbstractBaseModel): def __unicode__(self): return self.payment_type #TODO @@ -74,7 +84,7 @@ class Payment(BaseModel): value = models.IntegerField() transaction_id = models.IntegerField(unique=True, null=True, blank=True) -class Address(BaseModel): +class Address(AbstractBaseModel): class Meta: verbose_name_plural = "Addresses" @@ -86,13 +96,13 @@ class Address(BaseModel): country=self.country) street_address = models.CharField(max_length=255) - street_address_two = models.CharField(max_length=255) + street_address_two = models.CharField(max_length=255, null=True, blank=True) postal_code = models.CharField(max_length=10) city = models.CharField(max_length=100) country = models.ForeignKey('dusken.Country', null=True, blank=True) -class Country(BaseModel): +class Country(AbstractBaseModel): class Meta: verbose_name_plural = "Countries" @@ -103,7 +113,7 @@ class Country(BaseModel): code = models.CharField(max_length=3, unique=True) #ISO 3166-1 alpha 2 -class PlaceOfStudy(BaseModel): +class PlaceOfStudy(AbstractBaseModel): def __unicode__(self): return u"{institution}, {year}".format( institution=self.institution, @@ -113,30 +123,29 @@ class PlaceOfStudy(BaseModel): institution = models.ForeignKey('dusken.Institution') -class Institution(BaseModel): +class Institution(AbstractBaseModel): def __unicode__(self): return u'%s - %s' % (self.short_name, self.name) name = models.CharField(max_length=255) short_name = models.CharField(max_length=16) -class MemberMeta(BaseModel): +class MemberMeta(AbstractBaseModel): key = models.CharField(max_length=255) value = models.TextField(blank=True) member = models.ForeignKey('dusken.Member') -class Group(django.contrib.auth.models.Group): +class GroupProfile(AbstractBaseModel): """ django.contrib.auth.model.Group extended with additional fields. """ def __unicode__(self): - return u"{} ({})".format(self.name, self.posix_name) + return u"{}".format(self.posix_name) posix_name = models.CharField(max_length=255, unique=True) - created = models.DateTimeField(auto_now_add=True) - updated = models.DateTimeField(auto_now=True) + group = models.OneToOneField(django.contrib.auth.models.Group) -class Division(BaseModel): +class Division(AbstractBaseModel): """ Associations, comittee or similar """ @@ -147,9 +156,9 @@ class Division(BaseModel): is_active = models.BooleanField(default=True) members = models.ManyToManyField('dusken.Member', null=True, blank=True) parent = models.ForeignKey('self', null=True, blank=True, related_name='children') - groups = models.ManyToManyField('dusken.Group', null=True, blank=True) # permissions + groups = models.ManyToManyField(django.contrib.auth.models.Group, null=True, blank=True) # permissions -class ServiceHook(BaseModel): +class ServiceHook(AbstractBaseModel): """ Events with callback_urls """ diff --git a/dusken/urls.py b/dusken/urls.py index abf4f1c..e808d3f 100644 --- a/dusken/urls.py +++ b/dusken/urls.py @@ -2,6 +2,7 @@ from django.conf.urls import patterns, include, url from django.contrib import admin from tastypie.api import Api +from dusken.api.me import MeResource from dusken.api.member import MemberResource, MemberCreateResource from dusken.api.membership import MembershipResource from dusken.api.group import GroupResource @@ -10,6 +11,7 @@ from dusken.api.institution import InstitutionResource admin.autodiscover() v1_api = Api(api_name='v1') +v1_api.register(MeResource()) v1_api.register(MemberResource()) v1_api.register(MembershipResource()) v1_api.register(GroupResource())