diff --git a/src/codeschool/core/users/api.py b/src/codeschool/core/users/api.py index b283a7f..56996e1 100644 --- a/src/codeschool/core/users/api.py +++ b/src/codeschool/core/users/api.py @@ -1,5 +1,8 @@ from codeschool.api import router +from django.conf.urls import url + from . import views router.register(r'users', views.UserViewSet,base_name="users") -router.register(r'profile', views.ProfileViewSet) +router.register(r'detail', views.UserDetailViewSet) +router.register(r'change_password',views.ChangePasswordViewSet,base_name="change-password") diff --git a/src/codeschool/core/users/models.py b/src/codeschool/core/users/models.py index a06d491..c9ea96c 100644 --- a/src/codeschool/core/users/models.py +++ b/src/codeschool/core/users/models.py @@ -201,7 +201,7 @@ class Profile(models.TimeStampedModel): _('Phone'), max_length=20, blank=True, - null=True, + default="", ) gender = models.SmallIntegerField( _('gender'), @@ -217,7 +217,7 @@ class Profile(models.TimeStampedModel): website = models.URLField( _('Website'), blank=True, - null=True, + default="", help_text=_( 'A website that is shown publicly in your profile.' ) diff --git a/src/codeschool/core/users/permissions.py b/src/codeschool/core/users/permissions.py index 097ec80..90534ce 100644 --- a/src/codeschool/core/users/permissions.py +++ b/src/codeschool/core/users/permissions.py @@ -4,22 +4,30 @@ from .views import * import re -# https://github.com/HazyResearch/elementary/blob/master/django/resources/views.py - - class UserPermissions(BasePermission): """ Custom user permissions. Admin can do everything. POSTS are allowed from anyone. """ def has_permission(self, request, view): - single_user_regex = r"/api/users/[0-9]+" + single_user_regex = r"/api/detail/$" user_regex = r"/api/users" - profile_regex = r"/api/profile/" + profile_regex = r"/api/detail/[0-9]+/" + prof_regex = r"/api/prof" + p_regex = r"/api/prof/[0-9]+/" + change_password_regex = r"/api/users/[0-9]+/change-password" + if request.user.is_authenticated() and not request.user.is_staff and re.match(prof_regex,request.path): + all_info = view.queryset + owner_user_id = request.user.id + owner_user_info = Profile.objects.filter(user_id=owner_user_id) + view.queryset = owner_user_info + return request.method in ['GET', 'HEAD', 'OPTIONS', 'PUT'] + if re.match(change_password_regex,request.path): + return request.user.is_staff if request.user.is_authenticated() and not request.user.is_staff and re.match(profile_regex,request.path): all_info = view.queryset owner_user_id = request.user.id - owner_user_info = Profile.objects.filter(id=owner_user_id) + owner_user_info = User.objects.filter(id=owner_user_id) view.queryset = owner_user_info return request.method in ['GET', 'HEAD', 'OPTIONS', 'PUT'] if request.user.is_authenticated() and not request.user.is_staff: @@ -28,17 +36,13 @@ def has_permission(self, request, view): owner_user_info = User.objects.filter(id=owner_user_id) view.queryset = owner_user_info return request.method in ['GET', 'HEAD', 'OPTIONS', 'PUT'] - if re.match(single_user_regex, request.path): - url_user_search = re.search(r"[0-9]+", request.path) - url_user_id = int(url_user_search.group(0)) - return request.user.id == url_user_id or request.user.is_staff if not request.user or not request.user.is_authenticated(): return request.method == 'POST' - if re.match(user_regex, request.path) or re.match(profile_regex,request.path): + if re.match(single_user_regex, request.path): return request.user.is_staff return False def has_object_permission(self, request, view, obj): if request.user.is_staff: return True - return request.user == obj + return request.user.id == obj.__dict__['id'] diff --git a/src/codeschool/core/users/serializers.py b/src/codeschool/core/users/serializers.py index a2092b4..2b0b032 100644 --- a/src/codeschool/core/users/serializers.py +++ b/src/codeschool/core/users/serializers.py @@ -1,52 +1,70 @@ from rest_framework import serializers from rest_framework.decorators import detail_route, list_route -from django.contrib.auth.hashers import make_password +from django.contrib.auth.hashers import make_password,check_password from . import models + +class ChangePasswordSerializer(serializers.ModelSerializer): + + url = serializers.HyperlinkedIdentityField(view_name="change-password-detail") + password_confirmation = serializers.CharField(write_only=True) + old_password = serializers.CharField(write_only=True) + + class Meta: + model = models.User + fields = ('url','old_password','password','password_confirmation') + + write_only = {'write_only':True} + extra_kwargs = {'password':write_only, + 'password_confirmation':write_only, + } + + def update(self, instance, validated_data): + + old_password_from_db = models.User.objects.values().get(id=instance.__dict__['id'])['password'] + old_password_from_input = validated_data.pop('old_password') + new_password = validated_data.pop('password') + if check_password(old_password_from_input,old_password_from_db) and check_password(new_password,make_password(validated_data['password_confirmation'])): + instance.password = make_password(new_password) + instance.save() + else: + raise serializers.ValidationError("I guess you didn't type the old password correctly") + return instance + class ProfileSerializer(serializers.ModelSerializer): - """ - Serialize user profiles - """ - # TODO: hyperlinks as sub-resource under each user - # TODO: add extra user fields? - # TODO: nullify fields that user is not allowed to see? (this may be - # expensive in querysets) class Meta: model = models.Profile - fields = ( - 'gender','phone','date_of_birth' - ,'website','about_me', 'visibility', 'user' - ) - read_only = {'read_only': True} - extra_kwargs = { - 'user':read_only - } + fields = ('gender','phone','website','date_of_birth', + 'website','about_me', 'visibility','user') + read_only = {'read_only':True} + extra_kwargs = {"user":read_only} -class UserSerializer(serializers.ModelSerializer): +class CreateUserSerializer(serializers.ModelSerializer): """ Serialize User objects. """ role = serializers.SerializerMethodField() + profile = ProfileSerializer() password_confirmation = serializers.CharField(write_only=True) class Meta: model = models.User - fields = ('alias', 'role','email', 'name', 'school_id', 'password', 'password_confirmation') + fields = ('username', 'alias', 'role','email', 'name', 'school_id','password','password_confirmation', 'profile') write_only = {'write_only': True} + read_only = {'read_only':True} + + + extra_kwargs = { - 'email': write_only, - 'role': write_only, - 'name': write_only, - 'school_id': write_only, - 'password': write_only, - 'password_confirmation': write_only - } + 'password':write_only, + 'profile':write_only, + } def get_role(self, obj): if(obj.role == models.User.ROLE_STUDENT): @@ -58,14 +76,98 @@ def get_role(self, obj): elif(obj.role == models.User.ROLE_ADMIN): return 'admin' - def create(self, validated_data): + validated_data['password'] = validated_data['password'] password_confirmation = validated_data.pop('password_confirmation', None) if(password_confirmation == validated_data['password']): validated_data['password'] = make_password(validated_data['password']) - return super(UserSerializer, self).create(validated_data) else: - raise Exception() + raise serializers.ValidationError("I guess you didn't type the password confirmation just like the password") + profile_data = validated_data.pop('profile') + user = super(CreateUserSerializer,self).create(validated_data) + self.update_or_create_profile(user,profile_data) + return user + + def update_or_create_profile(self, user, profile_data): + # This always creates a Profile if the User is missing one; + # change the logic here if that's not right for your app + models.Profile.objects.update_or_create(user=user, defaults=profile_data) + + + +class UserSerializer(serializers.HyperlinkedModelSerializer): + + class Meta: + model = models.User + fields = ('url',) + + + +class UserDetailSerializer(serializers.HyperlinkedModelSerializer): + """ + Serialize User objects. + """ + + role = serializers.SerializerMethodField() + profile = ProfileSerializer() + + class Meta: + model = models.User + fields = ('username', 'alias', 'role','email', 'name', 'school_id', 'profile') + write_only = {'write_only': True} + read_only = {'read_only':True} + + + + extra_kwargs = { + 'profile':write_only, + 'school_id':read_only, + } + + def get_role(self, obj): + if(obj.role == models.User.ROLE_STUDENT): + return 'student' + elif(obj.role == models.User.ROLE_TEACHER): + return 'teacher' + elif(obj.role == models.User.ROLE_STAFF): + return 'staff' + elif(obj.role == models.User.ROLE_ADMIN): + return 'admin' + + + def create(self, validated_data): + validated_data['password'] = make_password(validated_data['password']) + profile_data = validated_data.pop('profile') + user = super(UserDetailSerializer,self).create(validated_data) + self.update_or_create_profile(user,profile_data) + return user + + def update_or_create_profile(self, user, profile_data): + # This always creates a Profile if the User is missing one; + # change the logic here if that's not right for your app + models.Profile.objects.update_or_create(user=user, defaults=profile_data) + + def update(self, instance, validated_data): + + + profile_data = validated_data.pop('profile') + profile = instance.profile + + instance.alias = validated_data.get('alias') + instance.email = validated_data.get('email') + instance.name = validated_data.get('name') + + instance.profile.gender = profile_data.get('gender') + instance.profile.phone = profile_data.get('phone') + instance.profile.date_of_birth = profile_data.get('date_of_birth') + instance.profile.about_me = profile_data.get('about_me') + instance.profile.website = profile_data.get('website') + + + instance.save() + profile.save() + + return instance class FullUserSerializer(serializers.ModelSerializer): diff --git a/src/codeschool/core/users/views.py b/src/codeschool/core/users/views.py index 0dcc93f..1f8fc82 100644 --- a/src/codeschool/core/users/views.py +++ b/src/codeschool/core/users/views.py @@ -14,7 +14,6 @@ from rest_framework.response import Response from rest_framework.permissions import IsAdminUser from .permissions import UserPermissions - authentication_backend = get_config('AUTHENTICATION_BACKENDS')[-1] @@ -22,12 +21,27 @@ # REST endpoints # +class ChangePasswordViewSet(viewsets.ModelViewSet): + permission_classes = (UserPermissions,) + queryset = models.User.objects.all() + serializer_class = serializers.ChangePasswordSerializer class ProfileViewSet(viewsets.ModelViewSet): permission_classes = (UserPermissions,) - method = 'put' queryset = models.Profile.objects.all() serializer_class = serializers.ProfileSerializer +class UserDetailViewSet(viewsets.ModelViewSet): + permission_classes = (UserPermissions,) + queryset = models.User.objects.all() + serializer_class = serializers.UserDetailSerializer + + def get_serializer_class(self): + serializer_class = self.serializer_class + if self.request.method == 'POST': + serializer_class = serializers.CreateUserSerializer + return serializer_class + + class UserViewSet(viewsets.ModelViewSet): """ Active users in the Codeschool platform. @@ -48,8 +62,18 @@ def set_profile(self, request, pk=None): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - + @detail_route(methods=['put'],url_path='change-password') + def set_profile(self, request, pk=None): + print("Llll") + profile = self.get_object() + serializer = serializers.ProfileSerializer(data=request.data) + if serializer.is_valid(): + profile.set_profile(serializer.data['phone']) + profile.save() + return Response({'status': 'gender set'}) + else: + return Response(serializer.errors, + status=status.HTTP_400_BAD_REQUEST) # # Auth views #