Migrating users serializers and validators

remotes/origin/issue/4795/notification_even_they_are_disabled
Jesús Espino 2016-07-05 19:29:49 +02:00
parent 39075288ac
commit 7d2b6c34ce
5 changed files with 214 additions and 162 deletions

View File

@ -25,3 +25,7 @@ def dict_sum(*args):
assert isinstance(arg, dict)
result += collections.Counter(arg)
return result
def into_namedtuple(dictionary):
return collections.namedtuple('GenericDict', dictionary.keys())(**dictionary)

View File

@ -19,7 +19,6 @@
import uuid
from django.apps import apps
from django.db.models import Q, F
from django.utils.translation import ugettext as _
from django.core.validators import validate_email
from django.core.exceptions import ValidationError
@ -28,21 +27,21 @@ from django.conf import settings
from taiga.base import exceptions as exc
from taiga.base import filters
from taiga.base import response
from taiga.base.utils.dicts import into_namedtuple
from taiga.auth.tokens import get_user_for_token
from taiga.base.decorators import list_route
from taiga.base.decorators import detail_route
from taiga.base.api import ModelCrudViewSet
from taiga.base.api.mixins import BlockedByProjectMixin
from taiga.base.filters import PermissionBasedFilterBackend
from taiga.base.api.utils import get_object_or_404
from taiga.base.filters import MembersFilterBackend
from taiga.base.mails import mail_builder
from taiga.projects.votes import services as votes_service
from taiga.users.services import get_user_by_username_or_email
from easy_thumbnails.source_generators import pil_image
from . import models
from . import serializers
from . import validators
from . import permissions
from . import filters as user_filters
from . import services
@ -53,6 +52,8 @@ class UsersViewSet(ModelCrudViewSet):
permission_classes = (permissions.UserPermission,)
admin_serializer_class = serializers.UserAdminSerializer
serializer_class = serializers.UserSerializer
admin_validator_class = validators.UserAdminValidator
validator_class = validators.UserValidator
queryset = models.User.objects.all().prefetch_related("memberships")
filter_backends = (MembersFilterBackend,)
@ -64,6 +65,14 @@ class UsersViewSet(ModelCrudViewSet):
return self.serializer_class
def get_validator_class(self):
if self.action in ["partial_update", "update", "retrieve", "by_username"]:
user = self.object
if self.request.user == user or self.request.user.is_superuser:
return self.admin_validator_class
return self.validator_class
def create(self, *args, **kwargs):
raise exc.NotSupported()
@ -86,7 +95,7 @@ class UsersViewSet(ModelCrudViewSet):
serializer = self.get_serializer(self.object)
return response.Ok(serializer.data)
#TODO: commit_on_success
# TODO: commit_on_success
def partial_update(self, request, *args, **kwargs):
"""
We must detect if the user is trying to change his email so we can
@ -96,12 +105,10 @@ class UsersViewSet(ModelCrudViewSet):
user = self.get_object()
self.check_permissions(request, "update", user)
ret = super().partial_update(request, *args, **kwargs)
new_email = request.DATA.get('email', None)
if new_email is not None:
valid_new_email = True
duplicated_email = models.User.objects.filter(email = new_email).exists()
duplicated_email = models.User.objects.filter(email=new_email).exists()
try:
validate_email(new_email)
@ -115,14 +122,21 @@ class UsersViewSet(ModelCrudViewSet):
elif not valid_new_email:
raise exc.WrongArguments(_("Not valid email"))
#We need to generate a token for the email
# We need to generate a token for the email
request.user.email_token = str(uuid.uuid1())
request.user.new_email = new_email
request.user.save(update_fields=["email_token", "new_email"])
email = mail_builder.change_email(request.user.new_email, {"user": request.user,
"lang": request.user.lang})
email = mail_builder.change_email(
request.user.new_email,
{
"user": request.user,
"lang": request.user.lang
}
)
email.send()
ret = super().partial_update(request, *args, **kwargs)
return ret
def destroy(self, request, pk=None):
@ -165,16 +179,16 @@ class UsersViewSet(ModelCrudViewSet):
self.check_permissions(request, "change_password_from_recovery", None)
serializer = serializers.RecoverySerializer(data=request.DATA, many=False)
if not serializer.is_valid():
validator = validators.RecoveryValidator(data=request.DATA, many=False)
if not validator.is_valid():
raise exc.WrongArguments(_("Token is invalid"))
try:
user = models.User.objects.get(token=serializer.data["token"])
user = models.User.objects.get(token=validator.data["token"])
except models.User.DoesNotExist:
raise exc.WrongArguments(_("Token is invalid"))
user.set_password(serializer.data["password"])
user.set_password(validator.data["password"])
user.token = None
user.save(update_fields=["password", "token"])
@ -247,13 +261,13 @@ class UsersViewSet(ModelCrudViewSet):
"""
Verify the email change to current logged user.
"""
serializer = serializers.ChangeEmailSerializer(data=request.DATA, many=False)
if not serializer.is_valid():
validator = validators.ChangeEmailValidator(data=request.DATA, many=False)
if not validator.is_valid():
raise exc.WrongArguments(_("Invalid, are you sure the token is correct and you "
"didn't use it before?"))
try:
user = models.User.objects.get(email_token=serializer.data["email_token"])
user = models.User.objects.get(email_token=validator.data["email_token"])
except models.User.DoesNotExist:
raise exc.WrongArguments(_("Invalid, are you sure the token is correct and you "
"didn't use it before?"))
@ -280,14 +294,14 @@ class UsersViewSet(ModelCrudViewSet):
"""
Cancel an account via token
"""
serializer = serializers.CancelAccountSerializer(data=request.DATA, many=False)
if not serializer.is_valid():
validator = validators.CancelAccountValidator(data=request.DATA, many=False)
if not validator.is_valid():
raise exc.WrongArguments(_("Invalid, are you sure the token is correct?"))
try:
max_age_cancel_account = getattr(settings, "MAX_AGE_CANCEL_ACCOUNT", None)
user = get_user_for_token(serializer.data["cancel_token"], "cancel_account",
max_age=max_age_cancel_account)
user = get_user_for_token(validator.data["cancel_token"], "cancel_account",
max_age=max_age_cancel_account)
except exc.NotAuthenticated:
raise exc.WrongArguments(_("Invalid, are you sure the token is correct?"))
@ -305,7 +319,7 @@ class UsersViewSet(ModelCrudViewSet):
self.object_list = user_filters.ContactsFilterBackend().filter_queryset(
user, request, self.get_queryset(), self).extra(
select={"complete_user_name":"concat(full_name, username)"}).order_by("complete_user_name")
select={"complete_user_name": "concat(full_name, username)"}).order_by("complete_user_name")
page = self.paginate_queryset(self.object_list)
if page is not None:
@ -349,10 +363,10 @@ class UsersViewSet(ModelCrudViewSet):
for elem in elements:
if elem["type"] == "project":
# projects are liked objects
response_data.append(serializers.LikedObjectSerializer(elem, **extra_args_liked).data )
response_data.append(serializers.LikedObjectSerializer(into_namedtuple(elem), **extra_args_liked).data)
else:
# stories, tasks and issues are voted objects
response_data.append(serializers.VotedObjectSerializer(elem, **extra_args_voted).data )
response_data.append(serializers.VotedObjectSerializer(into_namedtuple(elem), **extra_args_voted).data)
return response.Ok(response_data)
@ -374,7 +388,7 @@ class UsersViewSet(ModelCrudViewSet):
"user_likes": services.get_liked_content_for_user(request.user),
}
response_data = [serializers.LikedObjectSerializer(elem, **extra_args).data for elem in elements]
response_data = [serializers.LikedObjectSerializer(into_namedtuple(elem), **extra_args).data for elem in elements]
return response.Ok(response_data)
@ -397,17 +411,18 @@ class UsersViewSet(ModelCrudViewSet):
"user_votes": services.get_voted_content_for_user(request.user),
}
response_data = [serializers.VotedObjectSerializer(elem, **extra_args).data for elem in elements]
response_data = [serializers.VotedObjectSerializer(into_namedtuple(elem), **extra_args).data for elem in elements]
return response.Ok(response_data)
######################################################
## Role
######################################################
######################################################
# Role
######################################################
class RolesViewSet(BlockedByProjectMixin, ModelCrudViewSet):
model = models.Role
serializer_class = serializers.RoleSerializer
validator_class = validators.RoleValidator
permission_classes = (permissions.RolesPermission, )
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ('project',)

View File

@ -22,7 +22,7 @@ from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from taiga.base.api import serializers
from taiga.base.fields import PgArrayField, Field, MethodField
from taiga.base.fields import PgArrayField, Field, MethodField, I18NField
from taiga.base.utils.thumbnails import get_thumbnail_url
@ -40,47 +40,28 @@ import re
# User
######################################################
class ContactProjectDetailSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = ("id", "slug", "name")
class ContactProjectDetailSerializer(serializers.LightSerializer):
id = Field()
slug = Field()
name = Field()
class UserSerializer(serializers.ModelSerializer):
full_name_display = serializers.SerializerMethodField("get_full_name_display")
photo = serializers.SerializerMethodField("get_photo")
big_photo = serializers.SerializerMethodField("get_big_photo")
gravatar_url = serializers.SerializerMethodField("get_gravatar_url")
roles = serializers.SerializerMethodField("get_roles")
projects_with_me = serializers.SerializerMethodField("get_projects_with_me")
class Meta:
model = User
# IMPORTANT: Maintain the UserAdminSerializer Meta up to date
# with this info (including there the email)
fields = ("id", "username", "full_name", "full_name_display",
"color", "bio", "lang", "theme", "timezone", "is_active",
"photo", "big_photo", "roles", "projects_with_me",
"gravatar_url")
read_only_fields = ("id",)
def validate_username(self, attrs, source):
value = attrs[source]
validator = validators.RegexValidator(re.compile('^[\w.-]+$'), _("invalid username"),
_("invalid"))
try:
validator(value)
except ValidationError:
raise serializers.ValidationError(_("Required. 255 characters or fewer. Letters, "
"numbers and /./-/_ characters'"))
if (self.object and
self.object.username != value and
User.objects.filter(username=value).exists()):
raise serializers.ValidationError(_("Invalid username. Try with a different one."))
return attrs
class UserSerializer(serializers.LightSerializer):
id = Field()
username = Field()
full_name = Field()
full_name_display = MethodField()
color = Field()
bio = Field()
lang = Field()
theme = Field()
timezone = Field()
is_active = Field()
photo = MethodField()
big_photo = MethodField()
gravatar_url = MethodField()
roles = MethodField()
projects_with_me = MethodField()
def get_full_name_display(self, obj):
return obj.get_full_name() if obj else ""
@ -113,24 +94,13 @@ class UserSerializer(serializers.ModelSerializer):
class UserAdminSerializer(UserSerializer):
total_private_projects = serializers.SerializerMethodField("get_total_private_projects")
total_public_projects = serializers.SerializerMethodField("get_total_public_projects")
class Meta:
model = User
# IMPORTANT: Maintain the UserSerializer Meta up to date
# with this info (including here the email)
fields = ("id", "username", "full_name", "full_name_display", "email",
"color", "bio", "lang", "theme", "timezone", "is_active", "photo",
"big_photo", "gravatar_url",
"max_private_projects", "max_public_projects",
"max_memberships_private_projects", "max_memberships_public_projects",
"total_private_projects", "total_public_projects")
read_only_fields = ("id", "email",
"max_private_projects", "max_public_projects",
"max_memberships_private_projects",
"max_memberships_public_projects")
total_private_projects = MethodField()
total_public_projects = MethodField()
email = Field()
max_private_projects = Field()
max_public_projects = Field()
max_memberships_private_projects = Field()
max_memberships_public_projects = Field()
def get_total_private_projects(self, user):
return user.owned_projects.filter(is_private=True).count()
@ -163,75 +133,63 @@ class UserBasicInfoSerializer(serializers.LightSerializer):
return super().to_value(instance)
class RecoverySerializer(serializers.Serializer):
token = serializers.CharField(max_length=200)
password = serializers.CharField(min_length=6)
class ChangeEmailSerializer(serializers.Serializer):
email_token = serializers.CharField(max_length=200)
class CancelAccountSerializer(serializers.Serializer):
cancel_token = serializers.CharField(max_length=200)
######################################################
# Role
######################################################
class RoleSerializer(serializers.ModelSerializer):
members_count = serializers.SerializerMethodField("get_members_count")
class RoleSerializer(serializers.LightSerializer):
id = Field()
name = Field()
computable = Field()
project = Field(attr="project_id")
order = Field()
members_count = MethodField()
permissions = PgArrayField(required=False)
class Meta:
model = Role
fields = ('id', 'name', 'permissions', 'computable', 'project', 'order', 'members_count')
i18n_fields = ("name",)
def get_members_count(self, obj):
return obj.memberships.count()
class ProjectRoleSerializer(serializers.ModelSerializer):
class Meta:
model = Role
fields = ('id', 'name', 'slug', 'order', 'computable')
i18n_fields = ("name",)
class ProjectRoleSerializer(serializers.LightSerializer):
id = Field()
name = I18NField()
slug = Field()
order = Field()
computable = Field()
######################################################
# Like
######################################################
class HighLightedContentSerializer(serializers.Serializer):
type = serializers.CharField()
id = serializers.IntegerField()
ref = serializers.IntegerField()
slug = serializers.CharField()
name = serializers.CharField()
subject = serializers.CharField()
description = serializers.SerializerMethodField("get_description")
assigned_to = serializers.IntegerField()
status = serializers.CharField()
status_color = serializers.CharField()
tags_colors = serializers.SerializerMethodField("get_tags_color")
created_date = serializers.DateTimeField()
is_private = serializers.SerializerMethodField("get_is_private")
logo_small_url = serializers.SerializerMethodField("get_logo_small_url")
class HighLightedContentSerializer(serializers.LightSerializer):
type = Field()
id = Field()
ref = Field()
slug = Field()
name = Field()
subject = Field()
description = MethodField()
assigned_to = Field()
status = Field()
status_color = Field()
tags_colors = MethodField()
created_date = Field()
is_private = MethodField()
logo_small_url = MethodField()
project = serializers.SerializerMethodField("get_project")
project_name = serializers.SerializerMethodField("get_project_name")
project_slug = serializers.SerializerMethodField("get_project_slug")
project_is_private = serializers.SerializerMethodField("get_project_is_private")
project_blocked_code = serializers.CharField()
project = MethodField()
project_name = MethodField()
project_slug = MethodField()
project_is_private = MethodField()
project_blocked_code = Field()
assigned_to_username = serializers.CharField()
assigned_to_full_name = serializers.CharField()
assigned_to_photo = serializers.SerializerMethodField("get_photo")
assigned_to_username = Field()
assigned_to_full_name = Field()
assigned_to_photo = MethodField()
is_watcher = serializers.SerializerMethodField("get_is_watcher")
total_watchers = serializers.IntegerField()
is_watcher = MethodField()
total_watchers = Field()
def __init__(self, *args, **kwargs):
# Don't pass the extra ids args up to the superclass
@ -241,18 +199,18 @@ class HighLightedContentSerializer(serializers.Serializer):
super().__init__(*args, **kwargs)
def _none_if_project(self, obj, property):
type = obj.get("type", "")
type = getattr(obj, "type", "")
if type == "project":
return None
return obj.get(property)
return getattr(obj, property)
def _none_if_not_project(self, obj, property):
type = obj.get("type", "")
type = getattr(obj, "type", "")
if type != "project":
return None
return obj.get(property)
return getattr(obj, property)
def get_project(self, obj):
return self._none_if_project(obj, "project")
@ -278,29 +236,29 @@ class HighLightedContentSerializer(serializers.Serializer):
return get_thumbnail_url(logo, settings.THN_LOGO_SMALL)
return None
def get_photo(self, obj):
type = obj.get("type", "")
def get_assigned_to_photo(self, obj):
type = getattr(obj, "type", "")
if type == "project":
return None
UserData = namedtuple("UserData", ["photo", "email"])
user_data = UserData(photo=obj["assigned_to_photo"], email=obj.get("assigned_to_email") or "")
user_data = UserData(photo=obj.assigned_to_photo, email=obj.assigned_to_email or "")
return get_photo_or_gravatar_url(user_data)
def get_tags_color(self, obj):
tags = obj.get("tags", [])
def get_tags_colors(self, obj):
tags = getattr(obj, "tags", [])
tags = tags if tags is not None else []
tags_colors = obj.get("tags_colors", [])
tags_colors = getattr(obj, "tags_colors", [])
tags_colors = tags_colors if tags_colors is not None else []
return [{"name": tc[0], "color": tc[1]} for tc in tags_colors if tc[0] in tags]
def get_is_watcher(self, obj):
return obj["id"] in self.user_watching.get(obj["type"], [])
return obj.id in self.user_watching.get(obj.type, [])
class LikedObjectSerializer(HighLightedContentSerializer):
is_fan = serializers.SerializerMethodField("get_is_fan")
total_fans = serializers.IntegerField()
is_fan = MethodField()
total_fans = Field()
def __init__(self, *args, **kwargs):
# Don't pass the extra ids args up to the superclass
@ -310,12 +268,12 @@ class LikedObjectSerializer(HighLightedContentSerializer):
super().__init__(*args, **kwargs)
def get_is_fan(self, obj):
return obj["id"] in self.user_likes.get(obj["type"], [])
return obj.id in self.user_likes.get(obj.type, [])
class VotedObjectSerializer(HighLightedContentSerializer):
is_voter = serializers.SerializerMethodField("get_is_voter")
total_voters = serializers.IntegerField()
is_voter = MethodField()
total_voters = Field()
def __init__(self, *args, **kwargs):
# Don't pass the extra ids args up to the superclass
@ -325,4 +283,4 @@ class VotedObjectSerializer(HighLightedContentSerializer):
super().__init__(*args, **kwargs)
def get_is_voter(self, obj):
return obj["id"] in self.user_votes.get(obj["type"], [])
return obj.id in self.user_votes.get(obj.type, [])

View File

@ -3,7 +3,6 @@
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# Copyright (C) 2014-2016 Anler Hernández <hello@anler.me>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
@ -17,17 +16,92 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.utils.translation import ugettext as _
from django.core import validators as core_validators
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from taiga.base.api import serializers
from taiga.base.api import validators
from taiga.base.fields import PgArrayField, Field
from . import models
from .models import User, Role
import re
class RoleExistsValidator:
def validate_role_id(self, attrs, source):
value = attrs[source]
if not models.Role.objects.filter(pk=value).exists():
if not Role.objects.filter(pk=value).exists():
msg = _("There's no role with that id")
raise serializers.ValidationError(msg)
return attrs
######################################################
# User
######################################################
class UserValidator(validators.ModelValidator):
class Meta:
model = User
fields = ("username", "full_name", "color", "bio", "lang",
"theme", "timezone", "is_active")
def validate_username(self, attrs, source):
value = attrs[source]
validator = core_validators.RegexValidator(re.compile('^[\w.-]+$'), _("invalid username"),
_("invalid"))
try:
validator(value)
except ValidationError:
raise validators.ValidationError(_("Required. 255 characters or fewer. Letters, "
"numbers and /./-/_ characters'"))
if (self.object and
self.object.username != value and
User.objects.filter(username=value).exists()):
raise validators.ValidationError(_("Invalid username. Try with a different one."))
return attrs
class UserAdminValidator(UserValidator):
class Meta:
model = User
# IMPORTANT: Maintain the UserSerializer Meta up to date
# with this info (including here the email)
fields = ("username", "full_name", "color", "bio", "lang",
"theme", "timezone", "is_active", "email")
class RecoveryValidator(validators.Validator):
token = serializers.CharField(max_length=200)
password = serializers.CharField(min_length=6)
class ChangeEmailValidator(validators.Validator):
email_token = serializers.CharField(max_length=200)
class CancelAccountValidator(validators.Validator):
cancel_token = serializers.CharField(max_length=200)
######################################################
# Role
######################################################
class RoleValidator(validators.ModelValidator):
permissions = PgArrayField(required=False)
class Meta:
model = Role
fields = ('id', 'name', 'permissions', 'computable', 'project', 'order')
i18n_fields = ("name",)
class ProjectRoleValidator(validators.ModelValidator):
class Meta:
model = Role
fields = ('id', 'name', 'slug', 'order', 'computable')

View File

@ -30,6 +30,7 @@ from ..utils import DUMMY_BMP_DATA
from taiga.base.utils import json
from taiga.base.utils.thumbnails import get_thumbnail_url
from taiga.base.utils.dicts import into_namedtuple
from taiga.users import models
from taiga.users.serializers import LikedObjectSerializer, VotedObjectSerializer
from taiga.auth.tokens import get_token_for_user
@ -505,7 +506,7 @@ def test_get_watched_list_valid_info_for_project():
raw_project_watch_info = get_watched_list(fav_user, viewer_user)[0]
project_watch_info = LikedObjectSerializer(raw_project_watch_info).data
project_watch_info = LikedObjectSerializer(into_namedtuple(raw_project_watch_info)).data
assert project_watch_info["type"] == "project"
assert project_watch_info["id"] == project.id
@ -559,7 +560,7 @@ def test_get_liked_list_valid_info():
project.refresh_totals()
raw_project_like_info = get_liked_list(fan_user, viewer_user)[0]
project_like_info = LikedObjectSerializer(raw_project_like_info).data
project_like_info = LikedObjectSerializer(into_namedtuple(raw_project_like_info)).data
assert project_like_info["type"] == "project"
assert project_like_info["id"] == project.id
@ -609,7 +610,7 @@ def test_get_watched_list_valid_info_for_not_project_types():
instance.add_watcher(fav_user)
raw_instance_watch_info = get_watched_list(fav_user, viewer_user, type=object_type)[0]
instance_watch_info = VotedObjectSerializer(raw_instance_watch_info).data
instance_watch_info = VotedObjectSerializer(into_namedtuple(raw_instance_watch_info)).data
assert instance_watch_info["type"] == object_type
assert instance_watch_info["id"] == instance.id
@ -666,7 +667,7 @@ def test_get_voted_list_valid_info():
f.VotesFactory(content_type=content_type, object_id=instance.id, count=3)
raw_instance_vote_info = get_voted_list(fav_user, viewer_user, type=object_type)[0]
instance_vote_info = VotedObjectSerializer(raw_instance_vote_info).data
instance_vote_info = VotedObjectSerializer(into_namedtuple(raw_instance_vote_info)).data
assert instance_vote_info["type"] == object_type
assert instance_vote_info["id"] == instance.id