Refactoring user resource.
parent
4b7b2727a3
commit
e13f5dfe42
|
@ -3,9 +3,10 @@
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from django.db.models.loading import get_model
|
from django.db.models.loading import get_model
|
||||||
|
from django.db.models import Q
|
||||||
from django.contrib.auth import logout, login, authenticate
|
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.response import Response
|
||||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||||
from rest_framework import status, viewsets
|
from rest_framework import status, viewsets
|
||||||
|
@ -13,10 +14,13 @@ from rest_framework import status, viewsets
|
||||||
from djmail.template_mail import MagicMailBuilder
|
from djmail.template_mail import MagicMailBuilder
|
||||||
|
|
||||||
from greenmine.base import exceptions as exc
|
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 .models import User, Role
|
||||||
from .serializers import (LoginSerializer, UserLogged,
|
from .serializers import (LoginSerializer, UserLogged,
|
||||||
UserSerializer, RoleSerializer,)
|
UserSerializer, RoleSerializer,
|
||||||
|
RecoverySerializer)
|
||||||
|
|
||||||
|
|
||||||
class RolesViewSet(viewsets.ViewSet):
|
class RolesViewSet(viewsets.ViewSet):
|
||||||
|
@ -38,55 +42,95 @@ class RolesViewSet(viewsets.ViewSet):
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
class UsersViewSet(viewsets.ViewSet):
|
class ProjectMembershipFilter(FilterBackend):
|
||||||
permission_classes = (IsAuthenticated,)
|
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")
|
project_model = get_model("projects", "Project")
|
||||||
own_projects = (project_model.objects
|
own_projects = project_model.objects.filter(members=request.user)
|
||||||
.filter(members=self.request.user))
|
|
||||||
|
|
||||||
project = self.request.QUERY_PARAMS.get('project', None)
|
project = request.QUERY_PARAMS.get('project', None)
|
||||||
if project is not None:
|
if project is not None:
|
||||||
own_projects = own_projects.filter(pk=project)
|
own_projects = own_projects.filter(pk=project)
|
||||||
|
|
||||||
queryset = (User.objects.filter(projects__in=own_projects)
|
queryset = (queryset.filter(projects__in=own_projects)
|
||||||
.order_by('username').distinct())
|
.order_by('username').distinct())
|
||||||
|
|
||||||
return queryset
|
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):
|
class UsersViewSet(ModelCrudViewSet):
|
||||||
return Response({})
|
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):
|
def password_recovery(self, request, pk=None):
|
||||||
username_or_email = request.DATA.get('username', None)
|
username_or_email = request.DATA.get('username', None)
|
||||||
|
|
||||||
if not username_or_email:
|
if not username_or_email:
|
||||||
return Response({"detail": "Invalid username or password"},
|
raise exc.WrongArguments("Invalid username or email")
|
||||||
status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all()
|
||||||
user = queryset.get(Q(username=username_or_email) |
|
user = queryset.get(Q(username=username_or_email) |
|
||||||
Q(email=username_or_email))
|
Q(email=username_or_email))
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
return Response({"detail": "Invalid username or password"},
|
raise exc.WrongArguments("Invalid username or email")
|
||||||
status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
user.token = str(uuid.uuid1())
|
user.token = str(uuid.uuid1())
|
||||||
user.save(update_fields=["token"])
|
user.save(update_fields=["token"])
|
||||||
|
|
||||||
mbuilder = MagicMailBuilder()
|
mbuilder = MagicMailBuilder()
|
||||||
email = mbuilder.password_recovery(user.email, {"user": user})
|
email = mbuilder.password_recovery(user.email, {"user": user})
|
||||||
|
email.send()
|
||||||
|
|
||||||
return Response({"detail": "Mail sended successful!"})
|
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):
|
class AuthViewSet(viewsets.ViewSet):
|
||||||
permission_classes = (AllowAny,)
|
permission_classes = (AllowAny,)
|
||||||
|
|
|
@ -18,7 +18,7 @@ class User(WatcherMixin, AbstractUser):
|
||||||
verbose_name=_('default language'))
|
verbose_name=_('default language'))
|
||||||
default_timezone = models.CharField(max_length=20, null=False, blank=True, default='',
|
default_timezone = models.CharField(max_length=20, null=False, blank=True, default='',
|
||||||
verbose_name=_('default timezone'))
|
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'))
|
verbose_name=_('token'))
|
||||||
colorize_tags = models.BooleanField(null=False, blank=True, default=False,
|
colorize_tags = models.BooleanField(null=False, blank=True, default=False,
|
||||||
verbose_name=_('colorize tags'))
|
verbose_name=_('colorize tags'))
|
||||||
|
|
|
@ -69,6 +69,20 @@ class UserSerializer(serializers.ModelSerializer):
|
||||||
return [{"id": x.id, "name": x.name} for x in obj.projects.all()]
|
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 RoleSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Role
|
model = Role
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
from django.db.models.loading import get_model
|
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")
|
model = get_model("users", "User")
|
||||||
|
|
||||||
instance = model(
|
instance = model(
|
||||||
|
@ -12,7 +12,11 @@ def create_user(id, save=True):
|
||||||
first_name="Foo{0}".format(id),
|
first_name="Foo{0}".format(id),
|
||||||
last_name="Bar{0}".format(id)
|
last_name="Bar{0}".format(id)
|
||||||
)
|
)
|
||||||
|
|
||||||
instance.set_password(instance.username)
|
instance.set_password(instance.username)
|
||||||
|
if is_superuser:
|
||||||
|
instance.is_staff = True
|
||||||
|
instance.is_superuser = True
|
||||||
|
|
||||||
if save:
|
if save:
|
||||||
instance.save()
|
instance.save()
|
||||||
|
|
|
@ -4,12 +4,153 @@ import json
|
||||||
|
|
||||||
from django import test
|
from django import test
|
||||||
from django.core.urlresolvers import reverse
|
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.base.users.tests import create_user
|
||||||
from greenmine.projects.models import Project
|
from greenmine.projects.models import Project
|
||||||
|
|
||||||
from . import create_project, add_membership
|
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):
|
class ProjectsTestCase(test.TestCase):
|
||||||
fixtures = ["initial_role.json", ]
|
fixtures = ["initial_role.json", ]
|
||||||
|
|
|
@ -59,7 +59,7 @@ SEND_BROKEN_LINK_EMAILS = True
|
||||||
IGNORABLE_404_ENDS = ('.php', '.cgi')
|
IGNORABLE_404_ENDS = ('.php', '.cgi')
|
||||||
IGNORABLE_404_STARTS = ('/phpmyadmin/',)
|
IGNORABLE_404_STARTS = ('/phpmyadmin/',)
|
||||||
|
|
||||||
ATOMIC_REQUESTS = True
|
ATOMIC_REQUESTS = False
|
||||||
|
|
||||||
TIME_ZONE = 'Europe/Madrid'
|
TIME_ZONE = 'Europe/Madrid'
|
||||||
LANGUAGE_CODE = 'en'
|
LANGUAGE_CODE = 'en'
|
||||||
|
|
Loading…
Reference in New Issue