diff --git a/taiga/export_import/service.py b/taiga/export_import/service.py index df1cf666..d2ed7dec 100644 --- a/taiga/export_import/service.py +++ b/taiga/export_import/service.py @@ -130,7 +130,8 @@ def store_membership(project, membership): serialized.object._importing = True if not serialized.object.token: serialized.object.token = str(uuid.uuid1()) - serialized.object.user = find_invited_user(serialized.object, default=serialized.object.user) + serialized.object.user = find_invited_user(serialized.object.email, + default=serialized.object.user) serialized.save() return serialized diff --git a/taiga/projects/api.py b/taiga/projects/api.py index 94129988..1b18e4c3 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -185,52 +185,29 @@ class MembershipViewSet(ModelCrudViewSet): filter_backends = (filters.CanViewProjectFilterBackend,) filter_fields = ("project", "role") - def create(self, request, *args, **kwargs): - data = request.DATA.copy() - data.update({"invited_by_id": request.user.id}) - serializer = self.get_serializer(data=data, files=request.FILES) - - if serializer.is_valid(): - project_id = serializer.data["project"] - project = get_object_or_404(models.Project, id=project_id) - - self.check_permissions(request, 'create', project) - - qs = self.model.objects.filter(Q(project_id=project_id, - user__email=serializer.data["email"]) | - Q(project_id=project_id, - email=serializer.data["email"])) - if qs.count() > 0: - raise exc.WrongArguments(_("Email address is already taken.")) - - self.pre_save(serializer.object) - self.object = serializer.save(force_insert=True) - self.post_save(self.object, created=True) - headers = self.get_success_headers(serializer.data) - return Response(serializer.data, status=status.HTTP_201_CREATED, - headers=headers) - - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - @list_route(methods=["POST"]) def bulk_create(self, request, **kwargs): serializer = serializers.MembersBulkSerializer(data=request.DATA) - if serializer.is_valid(): - data = serializer.data - project = models.Project.objects.get(id=data["project_id"]) - self.check_permissions(request, 'bulk_create', project) - try: - members = services.create_members_in_bulk( - data["bulk_memberships"], project=project, callback=self.post_save, - precall=self.pre_save) - except ValidationError as err: - return Response(err.message_dict, status=status.HTTP_400_BAD_REQUEST) + if not serializer.is_valid(): + return response.BadRequest(serializer.errors) - members_serialized = self.serializer_class(members, many=True) + data = serializer.data + project = models.Project.objects.get(id=data["project_id"]) + self.check_permissions(request, 'bulk_create', project) - return response.Ok(data=members_serialized.data) + # TODO: this should be moved to main exception handler instead + # of handling explicit exception catchin here. - return response.BadRequest(serializer.errors) + try: + members = services.create_members_in_bulk(data["bulk_memberships"], + project=project, + callback=self.post_save, + precall=self.pre_save) + except ValidationError as err: + return response.BadRequest(err.message_dict) + + members_serialized = self.serializer_class(members, many=True) + return response.Ok(data=members_serialized.data) @detail_route(methods=["POST"]) def resend_invitation(self, request, **kwargs): @@ -241,14 +218,13 @@ class MembershipViewSet(ModelCrudViewSet): services.send_invitation(invitation=invitation) return Response(status=status.HTTP_204_NO_CONTENT) - def pre_save(self, object): - # Only assign new token if a current token value is empty. - if not object.token: - object.token = str(uuid.uuid1()) + def pre_save(self, obj): + if not obj.token: + obj.token = str(uuid.uuid1()) - object.user = services.find_invited_user(object, default=object.user) - - super().pre_save(object) + obj.invited_by = self.request.user + obj.user = services.find_invited_user(obj.email, default=obj.user) + super().pre_save(obj) def post_save(self, object, created=False): super().post_save(object, created=created) diff --git a/taiga/projects/migrations/0003_auto_20140913_1710.py b/taiga/projects/migrations/0003_auto_20140913_1710.py new file mode 100644 index 00000000..efc706a5 --- /dev/null +++ b/taiga/projects/migrations/0003_auto_20140913_1710.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('projects', '0002_auto_20140903_0920'), + ] + + operations = [ + migrations.RenameField( + model_name='membership', + old_name='invited_by_id', + new_name='invited_by_id_old', + ), + + migrations.AddField( + model_name='membership', + name='invited_by', + field=models.ForeignKey(null=True, to=settings.AUTH_USER_MODEL, blank=True, related_name='ihaveinvited+'), + preserve_default=True, + ), + + migrations.RunSQL("UPDATE projects_membership SET invited_by_id = invited_by_id_old"), + + migrations.RemoveField( + model_name='membership', + name='invited_by_id_old', + ), + + ] diff --git a/taiga/projects/models.py b/taiga/projects/models.py index 03416998..ea1f4e12 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -66,7 +66,9 @@ class Membership(models.Model): verbose_name=_("creado el")) token = models.CharField(max_length=60, blank=True, null=True, default=None, verbose_name=_("token")) - invited_by_id = models.IntegerField(null=True, blank=True) + + invited_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="ihaveinvited+", + null=True, blank=True) def clean(self): # TODO: Review and do it more robust @@ -130,7 +132,8 @@ class Project(ProjectDefaults, TaggedMixin, models.Model): owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False, related_name="owned_projects", verbose_name=_("owner")) members = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="projects", - through="Membership", verbose_name=_("members")) + through="Membership", verbose_name=_("members"), + through_fields=("project", "user")) total_milestones = models.IntegerField(default=0, null=True, blank=True, verbose_name=_("total of milestones")) total_story_points = models.FloatField(default=0, verbose_name=_("total story points")) diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index 6a27a709..fae3f7f1 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -15,8 +15,11 @@ # along with this program. If not, see . from os import path -from rest_framework import serializers + from django.utils.translation import ugettext_lazy as _ +from django.db.models import Q + +from rest_framework import serializers from taiga.base.serializers import JsonField, PgArrayField, ModelSerializer, TagsColorsField from taiga.users.models import Role, User @@ -97,9 +100,9 @@ class MembershipSerializer(ModelSerializer): email = serializers.EmailField(required=True) color = serializers.CharField(source='user.color', required=False, read_only=True) photo = serializers.SerializerMethodField("get_photo") - invited_by = serializers.SerializerMethodField("get_invited_by") project_name = serializers.SerializerMethodField("get_project_name") project_slug = serializers.SerializerMethodField("get_project_slug") + invited_by = UserSerializer(read_only=True) class Meta: model = models.Membership @@ -115,13 +118,24 @@ class MembershipSerializer(ModelSerializer): def get_project_slug(self, obj): return obj.project.slug if obj and obj.project else "" - def get_invited_by(self, membership): - try: - queryset = User.objects.get(pk=membership.invited_by_id) - except User.DoesNotExist: - return None - else: - return UserSerializer(queryset).data + def validate_email(self, attrs, source): + project = attrs["project"] + email = attrs[source] + + qs = models.Membership.objects.all() + + # If self.object is not None, the serializer is in update + # mode, and for it, it should exclude self. + if self.object: + qs = qs.exclude(pk=self.object.pk) + + qs = qs.filter(Q(project_id=project.id, user__email=email) | + Q(project_id=project.id, email=email)) + + if qs.count() > 0: + raise serializers.ValidationError(_("Email address is already taken")) + + return attrs class ProjectMembershipSerializer(ModelSerializer): diff --git a/taiga/projects/services/invitations.py b/taiga/projects/services/invitations.py index 13ca2743..e7cf7339 100644 --- a/taiga/projects/services/invitations.py +++ b/taiga/projects/services/invitations.py @@ -1,3 +1,6 @@ +from django.apps import apps +from django.conf import settings + from djmail.template_mail import MagicMailBuilder @@ -12,15 +15,20 @@ def send_invitation(invitation): email.send() -def find_invited_user(invitation, default=None): +def find_invited_user(email, default=None): """Check if the invited user is already a registered. :param invitation: Invitation object. :param default: Default object to return if user is not found. + TODO: only used by importer/exporter and should be moved here + :return: The user if it's found, othwerwise return `default`. """ + + User = apps.get_model(settings.AUTH_USER_MODEL) + try: - return type(invitation).user.get_queryset().filter(email=invitation.email).all()[0] - except IndexError: + return User.objects.get(email=email) + except User.DoesNotExist: return default