From e13f5dfe428c80de28b19b558a6ef5bc3d7c082d Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 16 Oct 2013 17:14:25 +0200 Subject: [PATCH] Refactoring user resource. --- greenmine/base/users/api.py | 88 +++++++++++---- greenmine/base/users/models.py | 2 +- greenmine/base/users/serializers.py | 14 +++ greenmine/base/users/tests/__init__.py | 6 +- greenmine/projects/tests/tests_api.py | 141 +++++++++++++++++++++++++ greenmine/settings/common.py | 2 +- 6 files changed, 228 insertions(+), 25 deletions(-) diff --git a/greenmine/base/users/api.py b/greenmine/base/users/api.py index 2b87e36f..d99174e0 100644 --- a/greenmine/base/users/api.py +++ b/greenmine/base/users/api.py @@ -3,9 +3,10 @@ import uuid from django.db.models.loading import get_model +from django.db.models import Q from django.contrib.auth import logout, login, authenticate -from rest_framework.decorators import action +from rest_framework.decorators import list_route, action from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework import status, viewsets @@ -13,10 +14,13 @@ from rest_framework import status, viewsets from djmail.template_mail import MagicMailBuilder from greenmine.base import exceptions as exc +from greenmine.base.filters import FilterBackend +from greenmine.base.api import ModelCrudViewSet from .models import User, Role from .serializers import (LoginSerializer, UserLogged, - UserSerializer, RoleSerializer,) + UserSerializer, RoleSerializer, + RecoverySerializer) class RolesViewSet(viewsets.ViewSet): @@ -38,55 +42,95 @@ class RolesViewSet(viewsets.ViewSet): return Response(serializer.data) -class UsersViewSet(viewsets.ViewSet): - permission_classes = (IsAuthenticated,) +class ProjectMembershipFilter(FilterBackend): + def filter_queryset(self, request, queryset, view): + queryset = super().filter_queryset(request, queryset, view) + + if request.user.is_superuser: + return queryset - def get_list_queryset(self): project_model = get_model("projects", "Project") - own_projects = (project_model.objects - .filter(members=self.request.user)) + own_projects = project_model.objects.filter(members=request.user) - project = self.request.QUERY_PARAMS.get('project', None) + project = request.QUERY_PARAMS.get('project', None) if project is not None: own_projects = own_projects.filter(pk=project) - queryset = (User.objects.filter(projects__in=own_projects) - .order_by('username').distinct()) - + queryset = (queryset.filter(projects__in=own_projects) + .order_by('username').distinct()) return queryset - def list(self, request, pk=None): - queryset = self.get_list_queryset() - serializer = UserSerializer(queryset, many=True) - return Response(serializer.data) - def retrieve(self, request, pk=None): - return Response({}) +class UsersViewSet(ModelCrudViewSet): + permission_classes = (IsAuthenticated,) + serializer_class = UserSerializer + queryset = User.objects.all() + filter_backends = (ProjectMembershipFilter,) - @action(methods=["POST"], permission_classes=[]) + def pre_conditions_on_save(self, obj): + if not self.request.user.is_superuser and obj.id != self.request.user.id: + raise exc.PreconditionError() + + def pre_conditions_on_delete(self, obj): + if not self.request.user.is_superuser and obj.id != self.request.user.id: + raise exc.PreconditionError() + + @list_route(permission_classes=[AllowAny], methods=["POST"]) def password_recovery(self, request, pk=None): username_or_email = request.DATA.get('username', None) if not username_or_email: - return Response({"detail": "Invalid username or password"}, - status.HTTP_400_BAD_REQUEST) + raise exc.WrongArguments("Invalid username or email") try: queryset = User.objects.all() user = queryset.get(Q(username=username_or_email) | Q(email=username_or_email)) except User.DoesNotExist: - return Response({"detail": "Invalid username or password"}, - status.HTTP_400_BAD_REQUEST) + raise exc.WrongArguments("Invalid username or email") user.token = str(uuid.uuid1()) user.save(update_fields=["token"]) mbuilder = MagicMailBuilder() email = mbuilder.password_recovery(user.email, {"user": user}) + email.send() return Response({"detail": "Mail sended successful!"}) + @list_route(permission_classes=[AllowAny], methods=["POST"]) + def change_password_from_recovery(self, request, pk=None): + """ + Change password with token (from password recovery step). + """ + serializer = RecoverySerializer(data=request.DATA, many=False) + if not serializer.is_valid(): + raise exc.WrongArguments("Token is invalid") + + user = User.objects.get(token=serializer.data["token"]) + user.set_password(serializer.data["password"]) + user.token = None + user.save(update_fields=["password", "token"]) + + return Response(status=status.HTTP_204_NO_CONTENT) + + @list_route(permission_classes=[IsAuthenticated], methods=["POST"]) + def change_password(self, request, pk=None): + """ + Change password to current logged user. + """ + password = request.DATA.get("password") + + if not password: + raise exc.WrongArguments("incomplete argiments") + + if len(password) < 6: + raise exc.WrongArguments("invalid password length") + + request.user.set_password(password) + request.user.save(update_fields=["password"]) + return Response(status=status.HTTP_204_NO_CONTENT) + class AuthViewSet(viewsets.ViewSet): permission_classes = (AllowAny,) diff --git a/greenmine/base/users/models.py b/greenmine/base/users/models.py index 017817d4..8f458b1b 100644 --- a/greenmine/base/users/models.py +++ b/greenmine/base/users/models.py @@ -18,7 +18,7 @@ class User(WatcherMixin, AbstractUser): verbose_name=_('default language')) default_timezone = models.CharField(max_length=20, null=False, blank=True, default='', verbose_name=_('default timezone')) - token = models.CharField(max_length=200, null=False, blank=True, default='', + token = models.CharField(max_length=200, null=True, blank=True, default=None, verbose_name=_('token')) colorize_tags = models.BooleanField(null=False, blank=True, default=False, verbose_name=_('colorize tags')) diff --git a/greenmine/base/users/serializers.py b/greenmine/base/users/serializers.py index a0e924b9..51ddb7c2 100644 --- a/greenmine/base/users/serializers.py +++ b/greenmine/base/users/serializers.py @@ -69,6 +69,20 @@ class UserSerializer(serializers.ModelSerializer): return [{"id": x.id, "name": x.name} for x in obj.projects.all()] +class RecoverySerializer(serializers.Serializer): + token = serializers.CharField(max_length=200) + password = serializers.CharField(min_length=6) + + def validate_token(self, attrs, source): + token = attrs[source] + try: + user = User.objects.get(token=token) + except User.DoesNotExist: + raise serializers.ValidationError("invalid token") + + return attrs + + class RoleSerializer(serializers.ModelSerializer): class Meta: model = Role diff --git a/greenmine/base/users/tests/__init__.py b/greenmine/base/users/tests/__init__.py index fbf49cd4..97ad1ab2 100644 --- a/greenmine/base/users/tests/__init__.py +++ b/greenmine/base/users/tests/__init__.py @@ -3,7 +3,7 @@ from django.db.models.loading import get_model -def create_user(id, save=True): +def create_user(id, save=True, is_superuser=False): model = get_model("users", "User") instance = model( @@ -12,7 +12,11 @@ def create_user(id, save=True): first_name="Foo{0}".format(id), last_name="Bar{0}".format(id) ) + instance.set_password(instance.username) + if is_superuser: + instance.is_staff = True + instance.is_superuser = True if save: instance.save() diff --git a/greenmine/projects/tests/tests_api.py b/greenmine/projects/tests/tests_api.py index bd399d95..fc10000d 100644 --- a/greenmine/projects/tests/tests_api.py +++ b/greenmine/projects/tests/tests_api.py @@ -4,12 +4,153 @@ import json from django import test from django.core.urlresolvers import reverse +from django.core import mail +from django.db.models import get_model from greenmine.base.users.tests import create_user from greenmine.projects.models import Project from . import create_project, add_membership +class ProfileTestCase(test.TestCase): + fixtures = ["initial_role.json", ] + + def setUp(self): + self.user1 = create_user(1, is_superuser=True) + self.user2 = create_user(2) + self.user3 = create_user(3) + + self.project1 = create_project(1, self.user1) + self.project2 = create_project(2, self.user1) + self.project3 = create_project(3, self.user2) + + add_membership(self.project1, self.user3, "dev") + add_membership(self.project3, self.user3, "dev") + add_membership(self.project3, self.user2, "dev") + + def test_list_users(self): + response = self.client.login(username=self.user3.username, + password=self.user3.username) + self.assertTrue(response) + + response = self.client.get(reverse("users-list")) + self.assertEqual(response.status_code, 200) + + users_list = response.data + self.assertEqual(len(users_list), 2) + + + def test_update_users(self): + response = self.client.login(username=self.user3.username, + password=self.user3.username) + self.assertTrue(response) + + data = {"first_name": "Foo Bar"} + + response = self.client.patch( + reverse("users-detail", args=[self.user2.pk]), + content_type="application/json", + data=json.dumps(data)) + self.assertEqual(response.status_code, 400) + + def test_update_users_self(self): + response = self.client.login(username=self.user3.username, + password=self.user3.username) + self.assertTrue(response) + + data = {"first_name": "Foo Bar"} + response = self.client.patch( + reverse("users-detail", args=[self.user3.pk]), + content_type="application/json", + data=json.dumps(data)) + + self.assertEqual(response.status_code, 200) + + def test_update_users_superuser(self): + response = self.client.login(username=self.user1.username, + password=self.user1.username) + self.assertTrue(response) + + data = {"first_name": "Foo Bar"} + response = self.client.patch( + reverse("users-detail", args=[self.user3.pk]), + content_type="application/json", + data=json.dumps(data)) + + self.assertEqual(response.status_code, 200) + + def test_delete_users(self): + response = self.client.login(username=self.user3.username, + password=self.user3.username) + self.assertTrue(response) + + data = {"first_name": "Foo Bar"} + response = self.client.delete( + reverse("users-detail", args=[self.user2.pk])) + self.assertEqual(response.status_code, 400) + + def test_delete_users_self(self): + response = self.client.login(username=self.user3.username, + password=self.user3.username) + self.assertTrue(response) + + data = {"first_name": "Foo Bar"} + response = self.client.delete( + reverse("users-detail", args=[self.user3.pk])) + + self.assertEqual(response.status_code, 204) + + + def test_delete_users_superuser(self): + response = self.client.login(username=self.user1.username, + password=self.user1.username) + self.assertTrue(response) + + data = {"first_name": "Foo Bar"} + response = self.client.delete( + reverse("users-detail", args=[self.user3.pk])) + + self.assertEqual(response.status_code, 204) + + def test_password_recovery(self): + url = reverse("users-password-recovery") + data = {"username": self.user1.username} + + response = self.client.post(url, data=json.dumps(data), + content_type="application/json") + self.assertEqual(response.status_code, 200) + self.assertEqual(len(mail.outbox), 1) + self.assertNotEqual(len(mail.outbox[0].body), 0) + + def test_users_change_password_from_recovery(self): + self.user1.token = "1111-1111-1111-1111" + self.user1.save() + + url = reverse("users-change-password-from-recovery") + data = {"token": self.user1.token, "password": "111111"} + + response = self.client.post(url, data=json.dumps(data), + content_type="application/json") + self.assertEqual(response.status_code, 204) + + user = get_model("users", "User").objects.get(pk=self.user1.pk) + self.assertTrue(user.check_password("111111")) + + def test_users_change_password(self): + response = self.client.login(username=self.user1.username, + password=self.user1.username) + self.assertTrue(response) + + url = reverse("users-change-password") + data = {"password": "111111"} + + response = self.client.post(url, data=json.dumps(data), + content_type="application/json") + self.assertEqual(response.status_code, 204) + + user = get_model("users", "User").objects.get(pk=self.user1.pk) + self.assertTrue(user.check_password("111111")) + class ProjectsTestCase(test.TestCase): fixtures = ["initial_role.json", ] diff --git a/greenmine/settings/common.py b/greenmine/settings/common.py index adfcedd0..69b98a13 100644 --- a/greenmine/settings/common.py +++ b/greenmine/settings/common.py @@ -59,7 +59,7 @@ SEND_BROKEN_LINK_EMAILS = True IGNORABLE_404_ENDS = ('.php', '.cgi') IGNORABLE_404_STARTS = ('/phpmyadmin/',) -ATOMIC_REQUESTS = True +ATOMIC_REQUESTS = False TIME_ZONE = 'Europe/Madrid' LANGUAGE_CODE = 'en'