Refactoring likes and votes
parent
9486387cc2
commit
3fc725c080
|
@ -295,6 +295,7 @@ INSTALLED_APPS = [
|
||||||
"taiga.projects.history",
|
"taiga.projects.history",
|
||||||
"taiga.projects.notifications",
|
"taiga.projects.notifications",
|
||||||
"taiga.projects.attachments",
|
"taiga.projects.attachments",
|
||||||
|
"taiga.projects.likes",
|
||||||
"taiga.projects.votes",
|
"taiga.projects.votes",
|
||||||
"taiga.projects.milestones",
|
"taiga.projects.milestones",
|
||||||
"taiga.projects.userstories",
|
"taiga.projects.userstories",
|
||||||
|
|
|
@ -44,7 +44,7 @@ from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin
|
||||||
from taiga.projects.userstories.models import UserStory, RolePoints
|
from taiga.projects.userstories.models import UserStory, RolePoints
|
||||||
from taiga.projects.tasks.models import Task
|
from taiga.projects.tasks.models import Task
|
||||||
from taiga.projects.issues.models import Issue
|
from taiga.projects.issues.models import Issue
|
||||||
from taiga.projects.votes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin
|
from taiga.projects.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin
|
||||||
from taiga.permissions import service as permissions_service
|
from taiga.permissions import service as permissions_service
|
||||||
|
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
@ -68,7 +68,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = super().get_queryset()
|
qs = super().get_queryset()
|
||||||
qs = self.attach_votes_attrs_to_queryset(qs)
|
qs = self.attach_likes_attrs_to_queryset(qs)
|
||||||
qs = attach_project_total_watchers_attrs_to_queryset(qs)
|
qs = attach_project_total_watchers_attrs_to_queryset(qs)
|
||||||
if self.request.user.is_authenticated():
|
if self.request.user.is_authenticated():
|
||||||
qs = attach_project_is_watcher_to_queryset(qs, self.request.user)
|
qs = attach_project_is_watcher_to_queryset(qs, self.request.user)
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014-2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
# 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
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# 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.contrib import admin
|
||||||
|
from django.contrib.contenttypes.admin import GenericTabularInline
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
class LikeInline(GenericTabularInline):
|
||||||
|
model = models.Like
|
||||||
|
extra = 0
|
|
@ -0,0 +1,51 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Like',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')),
|
||||||
|
('object_id', models.PositiveIntegerField()),
|
||||||
|
('created_date', models.DateTimeField(verbose_name='created date', auto_now_add=True)),
|
||||||
|
('content_type', models.ForeignKey(to='contenttypes.ContentType')),
|
||||||
|
('user', models.ForeignKey(related_name='likes', verbose_name='user', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Like',
|
||||||
|
'verbose_name_plural': 'Likes',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Likes',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')),
|
||||||
|
('object_id', models.PositiveIntegerField()),
|
||||||
|
('count', models.PositiveIntegerField(default=0, verbose_name='count')),
|
||||||
|
('content_type', models.ForeignKey(to='contenttypes.ContentType')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Likes',
|
||||||
|
'verbose_name_plural': 'Likes',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='likes',
|
||||||
|
unique_together=set([('content_type', 'object_id')]),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='like',
|
||||||
|
unique_together=set([('content_type', 'object_id', 'user')]),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014-2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
# 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
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# 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 taiga.base.api import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class FanResourceSerializerMixin(serializers.ModelSerializer):
|
||||||
|
is_fan = serializers.SerializerMethodField("get_is_fan")
|
||||||
|
total_fans = serializers.SerializerMethodField("get_total_fans")
|
||||||
|
|
||||||
|
def get_is_fan(self, obj):
|
||||||
|
# The "is_fan" attribute is attached in the get_queryset of the viewset.
|
||||||
|
return getattr(obj, "is_fan", False) or False
|
||||||
|
|
||||||
|
def get_total_fans(self, obj):
|
||||||
|
# The "total_fans" attribute is attached in the get_queryset of the viewset.
|
||||||
|
return getattr(obj, "total_fans", 0) or 0
|
|
@ -0,0 +1,92 @@
|
||||||
|
# Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014-2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
# 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
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# 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.core.exceptions import ObjectDoesNotExist
|
||||||
|
|
||||||
|
from taiga.base import response
|
||||||
|
from taiga.base.api import viewsets
|
||||||
|
from taiga.base.api.utils import get_object_or_404
|
||||||
|
from taiga.base.decorators import detail_route
|
||||||
|
|
||||||
|
from taiga.projects.likes import serializers
|
||||||
|
from taiga.projects.likes import services
|
||||||
|
from taiga.projects.likes.utils import attach_total_fans_to_queryset, attach_is_fan_to_queryset
|
||||||
|
|
||||||
|
|
||||||
|
class LikedResourceMixin:
|
||||||
|
# Note: Update get_queryset method:
|
||||||
|
# def get_queryset(self):
|
||||||
|
# qs = super().get_queryset()
|
||||||
|
# return self.attach_likes_attrs_to_queryset(qs)
|
||||||
|
|
||||||
|
def attach_likes_attrs_to_queryset(self, queryset):
|
||||||
|
qs = attach_total_fans_to_queryset(queryset)
|
||||||
|
|
||||||
|
if self.request.user.is_authenticated():
|
||||||
|
qs = attach_is_fan_to_queryset(self.request.user, qs)
|
||||||
|
|
||||||
|
return qs
|
||||||
|
|
||||||
|
@detail_route(methods=["POST"])
|
||||||
|
def like(self, request, pk=None):
|
||||||
|
obj = self.get_object()
|
||||||
|
self.check_permissions(request, "like", obj)
|
||||||
|
|
||||||
|
services.add_like(obj, user=request.user)
|
||||||
|
return response.Ok()
|
||||||
|
|
||||||
|
@detail_route(methods=["POST"])
|
||||||
|
def unlike(self, request, pk=None):
|
||||||
|
obj = self.get_object()
|
||||||
|
self.check_permissions(request, "unlike", obj)
|
||||||
|
|
||||||
|
services.remove_like(obj, user=request.user)
|
||||||
|
return response.Ok()
|
||||||
|
|
||||||
|
|
||||||
|
class FansViewSetMixin:
|
||||||
|
# Is a ModelListViewSet with two required params: permission_classes and resource_model
|
||||||
|
serializer_class = serializers.FanSerializer
|
||||||
|
list_serializer_class = serializers.FanSerializer
|
||||||
|
permission_classes = None
|
||||||
|
resource_model = None
|
||||||
|
|
||||||
|
def retrieve(self, request, *args, **kwargs):
|
||||||
|
pk = kwargs.get("pk", None)
|
||||||
|
resource_id = kwargs.get("resource_id", None)
|
||||||
|
resource = get_object_or_404(self.resource_model, pk=resource_id)
|
||||||
|
|
||||||
|
self.check_permissions(request, 'retrieve', resource)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.object = services.get_fans(resource).get(pk=pk)
|
||||||
|
except ObjectDoesNotExist: # or User.DoesNotExist
|
||||||
|
return response.NotFound()
|
||||||
|
|
||||||
|
serializer = self.get_serializer(self.object)
|
||||||
|
return response.Ok(serializer.data)
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
resource_id = kwargs.get("resource_id", None)
|
||||||
|
resource = get_object_or_404(self.resource_model, pk=resource_id)
|
||||||
|
|
||||||
|
self.check_permissions(request, 'list', resource)
|
||||||
|
|
||||||
|
return super().list(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
resource = self.resource_model.objects.get(pk=self.kwargs.get("resource_id"))
|
||||||
|
return services.get_fans(resource)
|
|
@ -0,0 +1,66 @@
|
||||||
|
# Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014-2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2015 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
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# 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.conf import settings
|
||||||
|
from django.contrib.contenttypes import generic
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class Likes(models.Model):
|
||||||
|
content_type = models.ForeignKey("contenttypes.ContentType")
|
||||||
|
object_id = models.PositiveIntegerField()
|
||||||
|
content_object = generic.GenericForeignKey("content_type", "object_id")
|
||||||
|
count = models.PositiveIntegerField(null=False, blank=False, default=0, verbose_name=_("count"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Likes")
|
||||||
|
verbose_name_plural = _("Likes")
|
||||||
|
unique_together = ("content_type", "object_id")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def project(self):
|
||||||
|
if hasattr(self.content_object, 'project'):
|
||||||
|
return self.content_object.project
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.count
|
||||||
|
|
||||||
|
|
||||||
|
class Like(models.Model):
|
||||||
|
content_type = models.ForeignKey("contenttypes.ContentType")
|
||||||
|
object_id = models.PositiveIntegerField()
|
||||||
|
content_object = generic.GenericForeignKey("content_type", "object_id")
|
||||||
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False,
|
||||||
|
related_name="likes", verbose_name=_("user"))
|
||||||
|
created_date = models.DateTimeField(null=False, blank=False, auto_now_add=True,
|
||||||
|
verbose_name=_("created date"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Like")
|
||||||
|
verbose_name_plural = _("Likes")
|
||||||
|
unique_together = ("content_type", "object_id", "user")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def project(self):
|
||||||
|
if hasattr(self.content_object, 'project'):
|
||||||
|
return self.content_object.project
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.user.get_full_name()
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014-2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2015 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
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# 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 taiga.base.api import serializers
|
||||||
|
from taiga.base.fields import TagsField
|
||||||
|
|
||||||
|
from taiga.users.models import User
|
||||||
|
from taiga.users.services import get_photo_or_gravatar_url
|
||||||
|
|
||||||
|
|
||||||
|
class FanSerializer(serializers.ModelSerializer):
|
||||||
|
full_name = serializers.CharField(source='get_full_name', required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ('id', 'username', 'full_name')
|
|
@ -0,0 +1,114 @@
|
||||||
|
# Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014-2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2015 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
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# 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.db.models import F
|
||||||
|
from django.db.transaction import atomic
|
||||||
|
from django.apps import apps
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
from .models import Likes, Like
|
||||||
|
|
||||||
|
|
||||||
|
def add_like(obj, user):
|
||||||
|
"""Add a like to an object.
|
||||||
|
|
||||||
|
If the user has already liked the object nothing happends, so this function can be considered
|
||||||
|
idempotent.
|
||||||
|
|
||||||
|
:param obj: Any Django model instance.
|
||||||
|
:param user: User adding the like. :class:`~taiga.users.models.User` instance.
|
||||||
|
"""
|
||||||
|
obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj)
|
||||||
|
with atomic():
|
||||||
|
like, created = Like.objects.get_or_create(content_type=obj_type, object_id=obj.id, user=user)
|
||||||
|
if not created:
|
||||||
|
return
|
||||||
|
|
||||||
|
likes, _ = Likes.objects.get_or_create(content_type=obj_type, object_id=obj.id)
|
||||||
|
likes.count = F('count') + 1
|
||||||
|
likes.save()
|
||||||
|
return like
|
||||||
|
|
||||||
|
|
||||||
|
def remove_like(obj, user):
|
||||||
|
"""Remove an user like from an object.
|
||||||
|
|
||||||
|
If the user has not liked the object nothing happens so this function can be considered
|
||||||
|
idempotent.
|
||||||
|
|
||||||
|
:param obj: Any Django model instance.
|
||||||
|
:param user: User removing her like. :class:`~taiga.users.models.User` instance.
|
||||||
|
"""
|
||||||
|
obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj)
|
||||||
|
with atomic():
|
||||||
|
qs = Like.objects.filter(content_type=obj_type, object_id=obj.id, user=user)
|
||||||
|
if not qs.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
qs.delete()
|
||||||
|
|
||||||
|
likes, _ = Likes.objects.get_or_create(content_type=obj_type, object_id=obj.id)
|
||||||
|
likes.count = F('count') - 1
|
||||||
|
likes.save()
|
||||||
|
|
||||||
|
|
||||||
|
def get_fans(obj):
|
||||||
|
"""Get the fans of an object.
|
||||||
|
|
||||||
|
:param obj: Any Django model instance.
|
||||||
|
|
||||||
|
:return: User queryset object representing the users that liked the object.
|
||||||
|
"""
|
||||||
|
obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj)
|
||||||
|
return get_user_model().objects.filter(likes__content_type=obj_type, likes__object_id=obj.id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_likes(obj):
|
||||||
|
"""Get the number of likes an object has.
|
||||||
|
|
||||||
|
:param obj: Any Django model instance.
|
||||||
|
|
||||||
|
:return: Number of likes or `0` if the object has no likes at all.
|
||||||
|
"""
|
||||||
|
obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return Likes.objects.get(content_type=obj_type, object_id=obj.id).count
|
||||||
|
except Likes.DoesNotExist:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def get_liked(user_or_id, model):
|
||||||
|
"""Get the objects liked by an user.
|
||||||
|
|
||||||
|
:param user_or_id: :class:`~taiga.users.models.User` instance or id.
|
||||||
|
:param model: Show only objects of this kind. Can be any Django model class.
|
||||||
|
|
||||||
|
:return: Queryset of objects representing the likes of the user.
|
||||||
|
"""
|
||||||
|
obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model)
|
||||||
|
conditions = ('likes_like.content_type_id = %s',
|
||||||
|
'%s.id = likes_like.object_id' % model._meta.db_table,
|
||||||
|
'likes_like.user_id = %s')
|
||||||
|
|
||||||
|
if isinstance(user_or_id, get_user_model()):
|
||||||
|
user_id = user_or_id.id
|
||||||
|
else:
|
||||||
|
user_id = user_or_id
|
||||||
|
|
||||||
|
return model.objects.extra(where=conditions, tables=('likes_like',),
|
||||||
|
params=(obj_type.id, user_id))
|
|
@ -0,0 +1,76 @@
|
||||||
|
# Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014-2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2015 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
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# 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.apps import apps
|
||||||
|
|
||||||
|
|
||||||
|
def attach_total_fans_to_queryset(queryset, as_field="total_fans"):
|
||||||
|
"""Attach likes count to each object of the queryset.
|
||||||
|
|
||||||
|
Because of laziness of like objects creation, this makes much simpler and more efficient to
|
||||||
|
access to liked-object number of likes.
|
||||||
|
|
||||||
|
(The other way was to do it in the serializer with some try/except blocks and additional
|
||||||
|
queries)
|
||||||
|
|
||||||
|
:param queryset: A Django queryset object.
|
||||||
|
:param as_field: Attach the likes-count as an attribute with this name.
|
||||||
|
|
||||||
|
:return: Queryset object with the additional `as_field` field.
|
||||||
|
"""
|
||||||
|
model = queryset.model
|
||||||
|
type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model)
|
||||||
|
sql = """SELECT coalesce(SUM(total_fans), 0) FROM (
|
||||||
|
SELECT coalesce(likes_likes.count, 0) total_fans
|
||||||
|
FROM likes_likes
|
||||||
|
WHERE likes_likes.content_type_id = {type_id}
|
||||||
|
AND likes_likes.object_id = {tbl}.id
|
||||||
|
) as e"""
|
||||||
|
|
||||||
|
sql = sql.format(type_id=type.id, tbl=model._meta.db_table)
|
||||||
|
qs = queryset.extra(select={as_field: sql})
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
|
def attach_is_fan_to_queryset(user, queryset, as_field="is_fan"):
|
||||||
|
"""Attach is_like boolean to each object of the queryset.
|
||||||
|
|
||||||
|
Because of laziness of like objects creation, this makes much simpler and more efficient to
|
||||||
|
access to likes-object and check if the curren user like it.
|
||||||
|
|
||||||
|
(The other way was to do it in the serializer with some try/except blocks and additional
|
||||||
|
queries)
|
||||||
|
|
||||||
|
:param user: A users.User object model
|
||||||
|
:param queryset: A Django queryset object.
|
||||||
|
:param as_field: Attach the boolean as an attribute with this name.
|
||||||
|
|
||||||
|
:return: Queryset object with the additional `as_field` field.
|
||||||
|
"""
|
||||||
|
model = queryset.model
|
||||||
|
type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model)
|
||||||
|
sql = ("""SELECT CASE WHEN (SELECT count(*)
|
||||||
|
FROM likes_like
|
||||||
|
WHERE likes_like.content_type_id = {type_id}
|
||||||
|
AND likes_like.object_id = {tbl}.id
|
||||||
|
AND likes_like.user_id = {user_id}) > 0
|
||||||
|
THEN TRUE
|
||||||
|
ELSE FALSE
|
||||||
|
END""")
|
||||||
|
sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id)
|
||||||
|
qs = queryset.extra(select={as_field: sql})
|
||||||
|
return qs
|
|
@ -37,6 +37,7 @@ from taiga.projects.attachments.models import *
|
||||||
from taiga.projects.custom_attributes.models import *
|
from taiga.projects.custom_attributes.models import *
|
||||||
from taiga.projects.custom_attributes.choices import TYPES_CHOICES, TEXT_TYPE, MULTILINE_TYPE, DATE_TYPE
|
from taiga.projects.custom_attributes.choices import TYPES_CHOICES, TEXT_TYPE, MULTILINE_TYPE, DATE_TYPE
|
||||||
from taiga.projects.history.services import take_snapshot
|
from taiga.projects.history.services import take_snapshot
|
||||||
|
from taiga.projects.likes.services import add_like
|
||||||
from taiga.projects.votes.services import add_vote
|
from taiga.projects.votes.services import add_vote
|
||||||
from taiga.events.apps import disconnect_events_signals
|
from taiga.events.apps import disconnect_events_signals
|
||||||
|
|
||||||
|
@ -98,8 +99,9 @@ NUM_TASKS = getattr(settings, "SAMPLE_DATA_NUM_TASKS", (0, 4))
|
||||||
NUM_USS_BACK = getattr(settings, "SAMPLE_DATA_NUM_USS_BACK", (8, 20))
|
NUM_USS_BACK = getattr(settings, "SAMPLE_DATA_NUM_USS_BACK", (8, 20))
|
||||||
NUM_ISSUES = getattr(settings, "SAMPLE_DATA_NUM_ISSUES", (12, 25))
|
NUM_ISSUES = getattr(settings, "SAMPLE_DATA_NUM_ISSUES", (12, 25))
|
||||||
NUM_ATTACHMENTS = getattr(settings, "SAMPLE_DATA_NUM_ATTACHMENTS", (0, 4))
|
NUM_ATTACHMENTS = getattr(settings, "SAMPLE_DATA_NUM_ATTACHMENTS", (0, 4))
|
||||||
NUM_VOTES = getattr(settings, "SAMPLE_DATA_NUM_VOTES", (0, 3))
|
NUM_LIKES = getattr(settings, "SAMPLE_DATA_NUM_LIKES", (0, 10))
|
||||||
NUM_PROJECT_WATCHERS = getattr(settings, "SAMPLE_DATA_NUM_PROJECT_WATCHERS", (0, 3))
|
NUM_VOTES = getattr(settings, "SAMPLE_DATA_NUM_VOTES", (0, 10))
|
||||||
|
NUM_WATCHERS = getattr(settings, "SAMPLE_DATA_NUM_PROJECT_WATCHERS", (0, 8))
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
sd = SampleDataHelper(seed=12345678901)
|
sd = SampleDataHelper(seed=12345678901)
|
||||||
|
@ -220,7 +222,7 @@ class Command(BaseCommand):
|
||||||
project.total_story_points = int(defined_points * self.sd.int(5,12) / 10)
|
project.total_story_points = int(defined_points * self.sd.int(5,12) / 10)
|
||||||
project.save()
|
project.save()
|
||||||
|
|
||||||
self.create_votes(project, project)
|
self.create_likes(project)
|
||||||
|
|
||||||
def create_attachment(self, obj, order):
|
def create_attachment(self, obj, order):
|
||||||
attached_file = self.sd.file_from_directory(*ATTACHMENT_SAMPLE_DATA)
|
attached_file = self.sd.file_from_directory(*ATTACHMENT_SAMPLE_DATA)
|
||||||
|
@ -301,9 +303,6 @@ class Command(BaseCommand):
|
||||||
user__isnull=False)).user
|
user__isnull=False)).user
|
||||||
bug.save()
|
bug.save()
|
||||||
|
|
||||||
watching_user = self.sd.db_object_from_queryset(project.memberships.filter(user__isnull=False)).user
|
|
||||||
bug.add_watcher(watching_user)
|
|
||||||
|
|
||||||
take_snapshot(bug,
|
take_snapshot(bug,
|
||||||
comment=self.sd.paragraph(),
|
comment=self.sd.paragraph(),
|
||||||
user=bug.owner)
|
user=bug.owner)
|
||||||
|
@ -315,7 +314,9 @@ class Command(BaseCommand):
|
||||||
comment=self.sd.paragraph(),
|
comment=self.sd.paragraph(),
|
||||||
user=bug.owner)
|
user=bug.owner)
|
||||||
|
|
||||||
self.create_votes(bug, project)
|
self.create_votes(bug)
|
||||||
|
self.create_watchers(bug)
|
||||||
|
|
||||||
return bug
|
return bug
|
||||||
|
|
||||||
def create_task(self, project, milestone, us, min_date, max_date, closed=False):
|
def create_task(self, project, milestone, us, min_date, max_date, closed=False):
|
||||||
|
@ -353,9 +354,6 @@ class Command(BaseCommand):
|
||||||
comment=self.sd.paragraph(),
|
comment=self.sd.paragraph(),
|
||||||
user=task.owner)
|
user=task.owner)
|
||||||
|
|
||||||
watching_user = self.sd.db_object_from_queryset(project.memberships.filter(user__isnull=False)).user
|
|
||||||
task.add_watcher(watching_user)
|
|
||||||
|
|
||||||
# Add history entry
|
# Add history entry
|
||||||
task.status=self.sd.db_object_from_queryset(project.task_statuses.all())
|
task.status=self.sd.db_object_from_queryset(project.task_statuses.all())
|
||||||
task.save()
|
task.save()
|
||||||
|
@ -363,7 +361,9 @@ class Command(BaseCommand):
|
||||||
comment=self.sd.paragraph(),
|
comment=self.sd.paragraph(),
|
||||||
user=task.owner)
|
user=task.owner)
|
||||||
|
|
||||||
self.create_votes(task, project)
|
self.create_votes(task)
|
||||||
|
self.create_watchers(task)
|
||||||
|
|
||||||
return task
|
return task
|
||||||
|
|
||||||
def create_us(self, project, milestone=None, computable_project_roles=[]):
|
def create_us(self, project, milestone=None, computable_project_roles=[]):
|
||||||
|
@ -404,8 +404,6 @@ class Command(BaseCommand):
|
||||||
user__isnull=False)).user
|
user__isnull=False)).user
|
||||||
us.save()
|
us.save()
|
||||||
|
|
||||||
watching_user = self.sd.db_object_from_queryset(project.memberships.filter(user__isnull=False)).user
|
|
||||||
us.add_watcher(watching_user)
|
|
||||||
|
|
||||||
take_snapshot(us,
|
take_snapshot(us,
|
||||||
comment=self.sd.paragraph(),
|
comment=self.sd.paragraph(),
|
||||||
|
@ -418,7 +416,9 @@ class Command(BaseCommand):
|
||||||
comment=self.sd.paragraph(),
|
comment=self.sd.paragraph(),
|
||||||
user=us.owner)
|
user=us.owner)
|
||||||
|
|
||||||
self.create_votes(us, project)
|
self.create_votes(us)
|
||||||
|
self.create_watchers(us)
|
||||||
|
|
||||||
return us
|
return us
|
||||||
|
|
||||||
def create_milestone(self, project, start_date, end_date):
|
def create_milestone(self, project, start_date, end_date):
|
||||||
|
@ -456,9 +456,8 @@ class Command(BaseCommand):
|
||||||
project.save()
|
project.save()
|
||||||
take_snapshot(project, user=project.owner)
|
take_snapshot(project, user=project.owner)
|
||||||
|
|
||||||
for i in range(self.sd.int(*NUM_PROJECT_WATCHERS)):
|
self.create_likes(project)
|
||||||
watching_user = self.sd.db_object_from_queryset(User.objects.all())
|
self.create_watchers(project)
|
||||||
project.add_watcher(watching_user)
|
|
||||||
|
|
||||||
return project
|
return project
|
||||||
|
|
||||||
|
@ -479,7 +478,18 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def create_votes(self, obj, project):
|
def create_votes(self, obj):
|
||||||
for i in range(self.sd.int(*NUM_VOTES)):
|
for i in range(self.sd.int(*NUM_VOTES)):
|
||||||
voting_user=self.sd.db_object_from_queryset(project.members.all())
|
user=self.sd.db_object_from_queryset(User.objects.all())
|
||||||
add_vote(obj, voting_user)
|
add_vote(obj, user)
|
||||||
|
|
||||||
|
def create_likes(self, obj):
|
||||||
|
for i in range(self.sd.int(*NUM_LIKES)):
|
||||||
|
user=self.sd.db_object_from_queryset(User.objects.all())
|
||||||
|
add_like(obj, user)
|
||||||
|
|
||||||
|
def create_watchers(self, obj):
|
||||||
|
for i in range(self.sd.int(*NUM_WATCHERS)):
|
||||||
|
user = self.sd.db_object_from_queryset(User.objects.all())
|
||||||
|
obj.add_watcher(user)
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,7 @@ from .validators import ProjectExistsValidator
|
||||||
from .custom_attributes.serializers import UserStoryCustomAttributeSerializer
|
from .custom_attributes.serializers import UserStoryCustomAttributeSerializer
|
||||||
from .custom_attributes.serializers import TaskCustomAttributeSerializer
|
from .custom_attributes.serializers import TaskCustomAttributeSerializer
|
||||||
from .custom_attributes.serializers import IssueCustomAttributeSerializer
|
from .custom_attributes.serializers import IssueCustomAttributeSerializer
|
||||||
from .votes.mixins.serializers import FanResourceSerializerMixin
|
from .likes.mixins.serializers import FanResourceSerializerMixin
|
||||||
|
|
||||||
######################################################
|
######################################################
|
||||||
## Custom values for selectors
|
## Custom values for selectors
|
||||||
|
|
|
@ -24,11 +24,11 @@ from taiga.projects.history.models import HistoryEntry
|
||||||
|
|
||||||
|
|
||||||
def _get_total_story_points(project):
|
def _get_total_story_points(project):
|
||||||
return (project.total_story_points if project.total_story_points is not None else
|
return (project.total_story_points if project.total_story_points not in [None, 0] else
|
||||||
sum(project.calculated_points["defined"].values()))
|
sum(project.calculated_points["defined"].values()))
|
||||||
|
|
||||||
def _get_total_milestones(project):
|
def _get_total_milestones(project):
|
||||||
return (project.total_milestones if project.total_milestones is not None else
|
return (project.total_milestones if project.total_milestones not in [None, 0] else
|
||||||
project.milestones.count())
|
project.milestones.count())
|
||||||
|
|
||||||
def _get_milestones_stats_for_backlog(project):
|
def _get_milestones_stats_for_backlog(project):
|
||||||
|
|
|
@ -17,19 +17,6 @@
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
|
|
||||||
class FanResourceSerializerMixin(serializers.ModelSerializer):
|
|
||||||
is_fan = serializers.SerializerMethodField("get_is_fan")
|
|
||||||
total_fans = serializers.SerializerMethodField("get_total_fans")
|
|
||||||
|
|
||||||
def get_is_fan(self, obj):
|
|
||||||
# The "is_voted" attribute is attached in the get_queryset of the viewset.
|
|
||||||
return getattr(obj, "is_voter", False) or False
|
|
||||||
|
|
||||||
def get_total_fans(self, obj):
|
|
||||||
# The "total_likes" attribute is attached in the get_queryset of the viewset.
|
|
||||||
return getattr(obj, "total_voters", 0) or 0
|
|
||||||
|
|
||||||
|
|
||||||
class VoteResourceSerializerMixin(serializers.ModelSerializer):
|
class VoteResourceSerializerMixin(serializers.ModelSerializer):
|
||||||
is_voter = serializers.SerializerMethodField("get_is_voter")
|
is_voter = serializers.SerializerMethodField("get_is_voter")
|
||||||
total_voters = serializers.SerializerMethodField("get_total_voters")
|
total_voters = serializers.SerializerMethodField("get_total_voters")
|
||||||
|
@ -39,5 +26,5 @@ class VoteResourceSerializerMixin(serializers.ModelSerializer):
|
||||||
return getattr(obj, "is_voter", False) or False
|
return getattr(obj, "is_voter", False) or False
|
||||||
|
|
||||||
def get_total_voters(self, obj):
|
def get_total_voters(self, obj):
|
||||||
# The "total_likes" attribute is attached in the get_queryset of the viewset.
|
# The "total_voters" attribute is attached in the get_queryset of the viewset.
|
||||||
return getattr(obj, "total_voters", 0) or 0
|
return getattr(obj, "total_voters", 0) or 0
|
||||||
|
|
|
@ -26,7 +26,7 @@ from taiga.projects.votes import services
|
||||||
from taiga.projects.votes.utils import attach_total_voters_to_queryset, attach_is_voter_to_queryset
|
from taiga.projects.votes.utils import attach_total_voters_to_queryset, attach_is_voter_to_queryset
|
||||||
|
|
||||||
|
|
||||||
class BaseVotedResource:
|
class VotedResourceMixin:
|
||||||
# Note: Update get_queryset method:
|
# Note: Update get_queryset method:
|
||||||
# def get_queryset(self):
|
# def get_queryset(self):
|
||||||
# qs = super().get_queryset()
|
# qs = super().get_queryset()
|
||||||
|
@ -40,46 +40,24 @@ class BaseVotedResource:
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def _add_voter(self, permission, request, pk=None):
|
@detail_route(methods=["POST"])
|
||||||
|
def upvote(self, request, pk=None):
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
self.check_permissions(request, permission, obj)
|
self.check_permissions(request, "upvote", obj)
|
||||||
|
|
||||||
services.add_vote(obj, user=request.user)
|
services.add_vote(obj, user=request.user)
|
||||||
return response.Ok()
|
return response.Ok()
|
||||||
|
|
||||||
def _remove_vote(self, permission, request, pk=None):
|
@detail_route(methods=["POST"])
|
||||||
|
def downvote(self, request, pk=None):
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
self.check_permissions(request, permission, obj)
|
self.check_permissions(request, "downvote", obj)
|
||||||
|
|
||||||
services.remove_vote(obj, user=request.user)
|
services.remove_vote(obj, user=request.user)
|
||||||
return response.Ok()
|
return response.Ok()
|
||||||
|
|
||||||
|
|
||||||
class LikedResourceMixin(BaseVotedResource):
|
class VotersViewSetMixin:
|
||||||
# Note: objects nedd 'like' and 'unlike' permissions.
|
|
||||||
|
|
||||||
@detail_route(methods=["POST"])
|
|
||||||
def like(self, request, pk=None):
|
|
||||||
return self._add_voter("like", request, pk)
|
|
||||||
|
|
||||||
@detail_route(methods=["POST"])
|
|
||||||
def unlike(self, request, pk=None):
|
|
||||||
return self._remove_vote("unlike", request, pk)
|
|
||||||
|
|
||||||
|
|
||||||
class VotedResourceMixin(BaseVotedResource):
|
|
||||||
# Note: objects nedd 'upvote' and 'downvote' permissions.
|
|
||||||
|
|
||||||
@detail_route(methods=["POST"])
|
|
||||||
def upvote(self, request, pk=None):
|
|
||||||
return self._add_voter("upvote", request, pk)
|
|
||||||
|
|
||||||
@detail_route(methods=["POST"])
|
|
||||||
def downvote(self, request, pk=None):
|
|
||||||
return self._remove_vote("downvote", request, pk)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseVotersViewSetMixin:
|
|
||||||
# Is a ModelListViewSet with two required params: permission_classes and resource_model
|
# Is a ModelListViewSet with two required params: permission_classes and resource_model
|
||||||
serializer_class = serializers.VoterSerializer
|
serializer_class = serializers.VoterSerializer
|
||||||
list_serializer_class = serializers.VoterSerializer
|
list_serializer_class = serializers.VoterSerializer
|
||||||
|
@ -112,11 +90,3 @@ class BaseVotersViewSetMixin:
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
resource = self.resource_model.objects.get(pk=self.kwargs.get("resource_id"))
|
resource = self.resource_model.objects.get(pk=self.kwargs.get("resource_id"))
|
||||||
return services.get_voters(resource)
|
return services.get_voters(resource)
|
||||||
|
|
||||||
|
|
||||||
class VotersViewSetMixin(BaseVotersViewSetMixin):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class FansViewSetMixin(BaseVotersViewSetMixin):
|
|
||||||
pass
|
|
||||||
|
|
|
@ -77,96 +77,64 @@ class UsersViewSet(ModelCrudViewSet):
|
||||||
|
|
||||||
return response.Ok(serializer.data)
|
return response.Ok(serializer.data)
|
||||||
|
|
||||||
@list_route(methods=["GET"])
|
|
||||||
def by_username(self, request, *args, **kwargs):
|
|
||||||
username = request.QUERY_PARAMS.get("username", None)
|
|
||||||
return self.retrieve(request, username=username)
|
|
||||||
|
|
||||||
def retrieve(self, request, *args, **kwargs):
|
def retrieve(self, request, *args, **kwargs):
|
||||||
self.object = get_object_or_404(models.User, **kwargs)
|
self.object = get_object_or_404(models.User, **kwargs)
|
||||||
self.check_permissions(request, 'retrieve', self.object)
|
self.check_permissions(request, 'retrieve', self.object)
|
||||||
serializer = self.get_serializer(self.object)
|
serializer = self.get_serializer(self.object)
|
||||||
return response.Ok(serializer.data)
|
return response.Ok(serializer.data)
|
||||||
|
|
||||||
@detail_route(methods=["GET"])
|
#TODO: commit_on_success
|
||||||
def contacts(self, request, *args, **kwargs):
|
def partial_update(self, request, *args, **kwargs):
|
||||||
user = get_object_or_404(models.User, **kwargs)
|
"""
|
||||||
self.check_permissions(request, 'contacts', user)
|
We must detect if the user is trying to change his email so we can
|
||||||
|
save that value and generate a token that allows him to validate it in
|
||||||
|
the new email account
|
||||||
|
"""
|
||||||
|
user = self.get_object()
|
||||||
|
self.check_permissions(request, "update", user)
|
||||||
|
|
||||||
self.object_list = user_filters.ContactsFilterBackend().filter_queryset(
|
ret = super().partial_update(request, *args, **kwargs)
|
||||||
user, request, self.get_queryset(), self).extra(
|
|
||||||
select={"complete_user_name":"concat(full_name, username)"}).order_by("complete_user_name")
|
|
||||||
|
|
||||||
page = self.paginate_queryset(self.object_list)
|
new_email = request.DATA.get('email', None)
|
||||||
if page is not None:
|
if new_email is not None:
|
||||||
serializer = self.serializer_class(page.object_list, many=True)
|
valid_new_email = True
|
||||||
else:
|
duplicated_email = models.User.objects.filter(email = new_email).exists()
|
||||||
serializer = self.serializer_class(self.object_list, many=True)
|
|
||||||
|
|
||||||
return response.Ok(serializer.data)
|
try:
|
||||||
|
validate_email(new_email)
|
||||||
|
except ValidationError:
|
||||||
|
valid_new_email = False
|
||||||
|
|
||||||
@detail_route(methods=["GET"])
|
valid_new_email = valid_new_email and new_email != request.user.email
|
||||||
def stats(self, request, *args, **kwargs):
|
|
||||||
user = get_object_or_404(models.User, **kwargs)
|
|
||||||
self.check_permissions(request, "stats", user)
|
|
||||||
return response.Ok(services.get_stats_for_user(user, request.user))
|
|
||||||
|
|
||||||
|
if duplicated_email:
|
||||||
|
raise exc.WrongArguments(_("Duplicated email"))
|
||||||
|
elif not valid_new_email:
|
||||||
|
raise exc.WrongArguments(_("Not valid email"))
|
||||||
|
|
||||||
def _serialize_liked_content(self, elem, **kwargs):
|
#We need to generate a token for the email
|
||||||
if elem.get("type") == "project":
|
request.user.email_token = str(uuid.uuid1())
|
||||||
serializer = serializers.FanSerializer
|
request.user.new_email = new_email
|
||||||
else:
|
request.user.save(update_fields=["email_token", "new_email"])
|
||||||
serializer = serializers.VotedSerializer
|
email = mail_builder.change_email(request.user.new_email, {"user": request.user,
|
||||||
|
"lang": request.user.lang})
|
||||||
|
email.send()
|
||||||
|
|
||||||
return serializer(elem, **kwargs)
|
return ret
|
||||||
|
|
||||||
|
def destroy(self, request, pk=None):
|
||||||
|
user = self.get_object()
|
||||||
|
self.check_permissions(request, "destroy", user)
|
||||||
|
stream = request.stream
|
||||||
|
request_data = stream is not None and stream.GET or None
|
||||||
|
user_cancel_account_signal.send(sender=user.__class__, user=user, request_data=request_data)
|
||||||
|
user.cancel()
|
||||||
|
return response.NoContent()
|
||||||
|
|
||||||
@detail_route(methods=["GET"])
|
@list_route(methods=["GET"])
|
||||||
def watched(self, request, *args, **kwargs):
|
def by_username(self, request, *args, **kwargs):
|
||||||
for_user = get_object_or_404(models.User, **kwargs)
|
username = request.QUERY_PARAMS.get("username", None)
|
||||||
from_user = request.user
|
return self.retrieve(request, username=username)
|
||||||
self.check_permissions(request, 'watched', for_user)
|
|
||||||
filters = {
|
|
||||||
"type": request.GET.get("type", None),
|
|
||||||
"q": request.GET.get("q", None),
|
|
||||||
}
|
|
||||||
|
|
||||||
self.object_list = services.get_watched_list(for_user, from_user, **filters)
|
|
||||||
page = self.paginate_queryset(self.object_list)
|
|
||||||
elements = page.object_list if page is not None else self.object_list
|
|
||||||
|
|
||||||
extra_args = {
|
|
||||||
"user_votes": services.get_voted_content_for_user(request.user),
|
|
||||||
"user_watching": services.get_watched_content_for_user(request.user),
|
|
||||||
}
|
|
||||||
|
|
||||||
response_data = [self._serialize_liked_content(elem, **extra_args).data for elem in elements]
|
|
||||||
return response.Ok(response_data)
|
|
||||||
|
|
||||||
|
|
||||||
@detail_route(methods=["GET"])
|
|
||||||
def liked(self, request, *args, **kwargs):
|
|
||||||
for_user = get_object_or_404(models.User, **kwargs)
|
|
||||||
from_user = request.user
|
|
||||||
self.check_permissions(request, 'liked', for_user)
|
|
||||||
filters = {
|
|
||||||
"type": request.GET.get("type", None),
|
|
||||||
"q": request.GET.get("q", None),
|
|
||||||
}
|
|
||||||
|
|
||||||
self.object_list = services.get_voted_list(for_user, from_user, **filters)
|
|
||||||
page = self.paginate_queryset(self.object_list)
|
|
||||||
elements = page.object_list if page is not None else self.object_list
|
|
||||||
|
|
||||||
extra_args = {
|
|
||||||
"user_votes": services.get_voted_content_for_user(request.user),
|
|
||||||
"user_watching": services.get_watched_content_for_user(request.user),
|
|
||||||
}
|
|
||||||
|
|
||||||
response_data = [self._serialize_liked_content(elem, **extra_args).data for elem in elements]
|
|
||||||
|
|
||||||
return response.Ok(response_data)
|
|
||||||
|
|
||||||
|
|
||||||
@list_route(methods=["POST"])
|
@list_route(methods=["POST"])
|
||||||
def password_recovery(self, request, pk=None):
|
def password_recovery(self, request, pk=None):
|
||||||
|
@ -278,45 +246,6 @@ class UsersViewSet(ModelCrudViewSet):
|
||||||
user_data = self.admin_serializer_class(request.user).data
|
user_data = self.admin_serializer_class(request.user).data
|
||||||
return response.Ok(user_data)
|
return response.Ok(user_data)
|
||||||
|
|
||||||
#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
|
|
||||||
save that value and generate a token that allows him to validate it in
|
|
||||||
the new email account
|
|
||||||
"""
|
|
||||||
user = self.get_object()
|
|
||||||
self.check_permissions(request, "update", user)
|
|
||||||
|
|
||||||
ret = super(UsersViewSet, self).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()
|
|
||||||
|
|
||||||
try:
|
|
||||||
validate_email(new_email)
|
|
||||||
except ValidationError:
|
|
||||||
valid_new_email = False
|
|
||||||
|
|
||||||
valid_new_email = valid_new_email and new_email != request.user.email
|
|
||||||
|
|
||||||
if duplicated_email:
|
|
||||||
raise exc.WrongArguments(_("Duplicated email"))
|
|
||||||
elif not valid_new_email:
|
|
||||||
raise exc.WrongArguments(_("Not valid 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.send()
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
@list_route(methods=["POST"])
|
@list_route(methods=["POST"])
|
||||||
def change_email(self, request, pk=None):
|
def change_email(self, request, pk=None):
|
||||||
"""
|
"""
|
||||||
|
@ -373,15 +302,108 @@ class UsersViewSet(ModelCrudViewSet):
|
||||||
user.cancel()
|
user.cancel()
|
||||||
return response.NoContent()
|
return response.NoContent()
|
||||||
|
|
||||||
def destroy(self, request, pk=None):
|
@detail_route(methods=["GET"])
|
||||||
user = self.get_object()
|
def contacts(self, request, *args, **kwargs):
|
||||||
self.check_permissions(request, "destroy", user)
|
user = get_object_or_404(models.User, **kwargs)
|
||||||
stream = request.stream
|
self.check_permissions(request, 'contacts', user)
|
||||||
request_data = stream is not None and stream.GET or None
|
|
||||||
user_cancel_account_signal.send(sender=user.__class__, user=user, request_data=request_data)
|
|
||||||
user.cancel()
|
|
||||||
return response.NoContent()
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
page = self.paginate_queryset(self.object_list)
|
||||||
|
if page is not None:
|
||||||
|
serializer = self.serializer_class(page.object_list, many=True)
|
||||||
|
else:
|
||||||
|
serializer = self.serializer_class(self.object_list, many=True)
|
||||||
|
|
||||||
|
return response.Ok(serializer.data)
|
||||||
|
|
||||||
|
@detail_route(methods=["GET"])
|
||||||
|
def stats(self, request, *args, **kwargs):
|
||||||
|
user = get_object_or_404(models.User, **kwargs)
|
||||||
|
self.check_permissions(request, "stats", user)
|
||||||
|
return response.Ok(services.get_stats_for_user(user, request.user))
|
||||||
|
|
||||||
|
@detail_route(methods=["GET"])
|
||||||
|
def watched(self, request, *args, **kwargs):
|
||||||
|
for_user = get_object_or_404(models.User, **kwargs)
|
||||||
|
from_user = request.user
|
||||||
|
self.check_permissions(request, 'watched', for_user)
|
||||||
|
filters = {
|
||||||
|
"type": request.GET.get("type", None),
|
||||||
|
"q": request.GET.get("q", None),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.object_list = services.get_watched_list(for_user, from_user, **filters)
|
||||||
|
page = self.paginate_queryset(self.object_list)
|
||||||
|
elements = page.object_list if page is not None else self.object_list
|
||||||
|
|
||||||
|
extra_args_liked = {
|
||||||
|
"user_watching": services.get_watched_content_for_user(request.user),
|
||||||
|
"user_likes": services.get_liked_content_for_user(request.user),
|
||||||
|
}
|
||||||
|
|
||||||
|
extra_args_voted = {
|
||||||
|
"user_watching": services.get_watched_content_for_user(request.user),
|
||||||
|
"user_votes": services.get_voted_content_for_user(request.user),
|
||||||
|
}
|
||||||
|
|
||||||
|
response_data = []
|
||||||
|
for elem in elements:
|
||||||
|
if elem["type"] == "project":
|
||||||
|
# projects are liked objects
|
||||||
|
response_data.append(serializers.LikedObjectSerializer(elem, **extra_args_liked).data )
|
||||||
|
else:
|
||||||
|
# stories, tasks and issues are voted objects
|
||||||
|
response_data.append(serializers.VotedObjectSerializer(elem, **extra_args_voted).data )
|
||||||
|
|
||||||
|
return response.Ok(response_data)
|
||||||
|
|
||||||
|
@detail_route(methods=["GET"])
|
||||||
|
def liked(self, request, *args, **kwargs):
|
||||||
|
for_user = get_object_or_404(models.User, **kwargs)
|
||||||
|
from_user = request.user
|
||||||
|
self.check_permissions(request, 'liked', for_user)
|
||||||
|
filters = {
|
||||||
|
"q": request.GET.get("q", None),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.object_list = services.get_liked_list(for_user, from_user, **filters)
|
||||||
|
page = self.paginate_queryset(self.object_list)
|
||||||
|
elements = page.object_list if page is not None else self.object_list
|
||||||
|
|
||||||
|
extra_args = {
|
||||||
|
"user_watching": services.get_watched_content_for_user(request.user),
|
||||||
|
"user_likes": services.get_liked_content_for_user(request.user),
|
||||||
|
}
|
||||||
|
|
||||||
|
response_data = [serializers.LikedObjectSerializer(elem, **extra_args).data for elem in elements]
|
||||||
|
|
||||||
|
return response.Ok(response_data)
|
||||||
|
|
||||||
|
@detail_route(methods=["GET"])
|
||||||
|
def voted(self, request, *args, **kwargs):
|
||||||
|
for_user = get_object_or_404(models.User, **kwargs)
|
||||||
|
from_user = request.user
|
||||||
|
self.check_permissions(request, 'liked', for_user)
|
||||||
|
filters = {
|
||||||
|
"type": request.GET.get("type", None),
|
||||||
|
"q": request.GET.get("q", None),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.object_list = services.get_voted_list(for_user, from_user, **filters)
|
||||||
|
page = self.paginate_queryset(self.object_list)
|
||||||
|
elements = page.object_list if page is not None else self.object_list
|
||||||
|
|
||||||
|
extra_args = {
|
||||||
|
"user_watching": services.get_watched_content_for_user(request.user),
|
||||||
|
"user_votes": services.get_voted_content_for_user(request.user),
|
||||||
|
}
|
||||||
|
|
||||||
|
response_data = [serializers.VotedObjectSerializer(elem, **extra_args).data for elem in elements]
|
||||||
|
|
||||||
|
return response.Ok(response_data)
|
||||||
|
|
||||||
######################################################
|
######################################################
|
||||||
## Role
|
## Role
|
||||||
|
|
|
@ -47,6 +47,7 @@ class UserPermission(TaigaResourcePermission):
|
||||||
change_email_perms = AllowAny()
|
change_email_perms = AllowAny()
|
||||||
contacts_perms = AllowAny()
|
contacts_perms = AllowAny()
|
||||||
liked_perms = AllowAny()
|
liked_perms = AllowAny()
|
||||||
|
voted_perms = AllowAny()
|
||||||
watched_perms = AllowAny()
|
watched_perms = AllowAny()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -159,7 +159,7 @@ class ProjectRoleSerializer(serializers.ModelSerializer):
|
||||||
######################################################
|
######################################################
|
||||||
|
|
||||||
|
|
||||||
class LikeSerializer(serializers.Serializer):
|
class HighLightedContentSerializer(serializers.Serializer):
|
||||||
type = serializers.CharField()
|
type = serializers.CharField()
|
||||||
id = serializers.IntegerField()
|
id = serializers.IntegerField()
|
||||||
ref = serializers.IntegerField()
|
ref = serializers.IntegerField()
|
||||||
|
@ -174,9 +174,6 @@ class LikeSerializer(serializers.Serializer):
|
||||||
created_date = serializers.DateTimeField()
|
created_date = serializers.DateTimeField()
|
||||||
is_private = serializers.SerializerMethodField("get_is_private")
|
is_private = serializers.SerializerMethodField("get_is_private")
|
||||||
|
|
||||||
is_watcher = serializers.SerializerMethodField("get_is_watcher")
|
|
||||||
total_watchers = serializers.IntegerField()
|
|
||||||
|
|
||||||
project = serializers.SerializerMethodField("get_project")
|
project = serializers.SerializerMethodField("get_project")
|
||||||
project_name = serializers.SerializerMethodField("get_project_name")
|
project_name = serializers.SerializerMethodField("get_project_name")
|
||||||
project_slug = serializers.SerializerMethodField("get_project_slug")
|
project_slug = serializers.SerializerMethodField("get_project_slug")
|
||||||
|
@ -186,13 +183,15 @@ class LikeSerializer(serializers.Serializer):
|
||||||
assigned_to_full_name = serializers.CharField()
|
assigned_to_full_name = serializers.CharField()
|
||||||
assigned_to_photo = serializers.SerializerMethodField("get_photo")
|
assigned_to_photo = serializers.SerializerMethodField("get_photo")
|
||||||
|
|
||||||
|
is_watcher = serializers.SerializerMethodField("get_is_watcher")
|
||||||
|
total_watchers = serializers.IntegerField()
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
# Don't pass the extra ids args up to the superclass
|
# Don't pass the extra ids args up to the superclass
|
||||||
self.user_votes = kwargs.pop("user_votes", {})
|
|
||||||
self.user_watching = kwargs.pop("user_watching", {})
|
self.user_watching = kwargs.pop("user_watching", {})
|
||||||
|
|
||||||
# Instantiate the superclass normally
|
# Instantiate the superclass normally
|
||||||
super(LikeSerializer, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def _none_if_project(self, obj, property):
|
def _none_if_project(self, obj, property):
|
||||||
type = obj.get("type", "")
|
type = obj.get("type", "")
|
||||||
|
@ -226,9 +225,6 @@ class LikeSerializer(serializers.Serializer):
|
||||||
def get_project_is_private(self, obj):
|
def get_project_is_private(self, obj):
|
||||||
return self._none_if_project(obj, "project_is_private")
|
return self._none_if_project(obj, "project_is_private")
|
||||||
|
|
||||||
def get_is_watcher(self, obj):
|
|
||||||
return obj["id"] in self.user_watching.get(obj["type"], [])
|
|
||||||
|
|
||||||
def get_photo(self, obj):
|
def get_photo(self, obj):
|
||||||
type = obj.get("type", "")
|
type = obj.get("type", "")
|
||||||
if type == "project":
|
if type == "project":
|
||||||
|
@ -242,18 +238,35 @@ class LikeSerializer(serializers.Serializer):
|
||||||
tags = obj.get("tags", [])
|
tags = obj.get("tags", [])
|
||||||
return [{"name": tc[0], "color": tc[1]} for tc in obj.get("tags_colors", []) if tc[0] in tags]
|
return [{"name": tc[0], "color": tc[1]} for tc in obj.get("tags_colors", []) if tc[0] in tags]
|
||||||
|
|
||||||
|
def get_is_watcher(self, obj):
|
||||||
|
return obj["id"] in self.user_watching.get(obj["type"], [])
|
||||||
|
|
||||||
|
|
||||||
class FanSerializer(LikeSerializer):
|
class LikedObjectSerializer(HighLightedContentSerializer):
|
||||||
is_fan = serializers.SerializerMethodField("get_is_fan")
|
is_fan = serializers.SerializerMethodField("get_is_fan")
|
||||||
total_fans = serializers.IntegerField(source="total_voters")
|
total_fans = serializers.IntegerField()
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
# Don't pass the extra ids args up to the superclass
|
||||||
|
self.user_likes = kwargs.pop("user_likes", {})
|
||||||
|
|
||||||
|
# Instantiate the superclass normally
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def get_is_fan(self, obj):
|
def get_is_fan(self, obj):
|
||||||
return obj["id"] in self.user_votes.get(obj["type"], [])
|
return obj["id"] in self.user_likes.get(obj["type"], [])
|
||||||
|
|
||||||
class VotedSerializer(LikeSerializer):
|
|
||||||
|
class VotedObjectSerializer(HighLightedContentSerializer):
|
||||||
is_voter = serializers.SerializerMethodField("get_is_voter")
|
is_voter = serializers.SerializerMethodField("get_is_voter")
|
||||||
total_voters = serializers.IntegerField()
|
total_voters = serializers.IntegerField()
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
# Don't pass the extra ids args up to the superclass
|
||||||
|
self.user_votes = kwargs.pop("user_votes", {})
|
||||||
|
|
||||||
|
# Instantiate the superclass normally
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def get_is_voter(self, obj):
|
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"], [])
|
||||||
|
|
|
@ -149,6 +149,23 @@ def get_stats_for_user(from_user, by_user):
|
||||||
return project_stats
|
return project_stats
|
||||||
|
|
||||||
|
|
||||||
|
def get_liked_content_for_user(user):
|
||||||
|
"""Returns a dict where:
|
||||||
|
- The key is the content_type model
|
||||||
|
- The values are list of id's of the different objects liked by the user
|
||||||
|
"""
|
||||||
|
if user.is_anonymous():
|
||||||
|
return {}
|
||||||
|
|
||||||
|
user_likes = {}
|
||||||
|
for (ct_model, object_id) in user.likes.values_list("content_type__model", "object_id"):
|
||||||
|
list = user_likes.get(ct_model, [])
|
||||||
|
list.append(object_id)
|
||||||
|
user_likes[ct_model] = list
|
||||||
|
|
||||||
|
return user_likes
|
||||||
|
|
||||||
|
|
||||||
def get_voted_content_for_user(user):
|
def get_voted_content_for_user(user):
|
||||||
"""Returns a dict where:
|
"""Returns a dict where:
|
||||||
- The key is the content_type model
|
- The key is the content_type model
|
||||||
|
@ -190,11 +207,12 @@ def get_watched_content_for_user(user):
|
||||||
|
|
||||||
def _build_watched_sql_for_projects(for_user):
|
def _build_watched_sql_for_projects(for_user):
|
||||||
sql = """
|
sql = """
|
||||||
SELECT projects_project.id AS id, null AS ref, 'project' AS type,
|
SELECT projects_project.id AS id, null::integer AS ref, 'project'::text AS type,
|
||||||
tags, notifications_notifypolicy.project_id AS object_id, projects_project.id AS project,
|
tags, notifications_notifypolicy.project_id AS object_id, projects_project.id AS project,
|
||||||
slug AS slug, projects_project.name AS name, null AS subject,
|
slug, projects_project.name, null::text AS subject,
|
||||||
notifications_notifypolicy.created_at as created_date, coalesce(watchers, 0) as total_watchers, coalesce(votes_votes.count, 0) total_voters, null AS assigned_to,
|
notifications_notifypolicy.created_at as created_date,
|
||||||
null as status, null as status_color
|
coalesce(watchers, 0) AS total_watchers, coalesce(likes_likes.count, 0) AS total_fans, null::integer AS total_voters,
|
||||||
|
null::integer AS assigned_to, null::text as status, null::text as status_color
|
||||||
FROM notifications_notifypolicy
|
FROM notifications_notifypolicy
|
||||||
INNER JOIN projects_project
|
INNER JOIN projects_project
|
||||||
ON (projects_project.id = notifications_notifypolicy.project_id)
|
ON (projects_project.id = notifications_notifypolicy.project_id)
|
||||||
|
@ -204,8 +222,8 @@ def _build_watched_sql_for_projects(for_user):
|
||||||
GROUP BY project_id
|
GROUP BY project_id
|
||||||
) type_watchers
|
) type_watchers
|
||||||
ON projects_project.id = type_watchers.project_id
|
ON projects_project.id = type_watchers.project_id
|
||||||
LEFT JOIN votes_votes
|
LEFT JOIN likes_likes
|
||||||
ON (projects_project.id = votes_votes.object_id AND {project_content_type_id} = votes_votes.content_type_id)
|
ON (projects_project.id = likes_likes.object_id AND {project_content_type_id} = likes_likes.content_type_id)
|
||||||
WHERE notifications_notifypolicy.user_id = {for_user_id}
|
WHERE notifications_notifypolicy.user_id = {for_user_id}
|
||||||
"""
|
"""
|
||||||
sql = sql.format(
|
sql = sql.format(
|
||||||
|
@ -215,30 +233,32 @@ def _build_watched_sql_for_projects(for_user):
|
||||||
return sql
|
return sql
|
||||||
|
|
||||||
|
|
||||||
def _build_voted_sql_for_projects(for_user):
|
def _build_liked_sql_for_projects(for_user):
|
||||||
sql = """
|
sql = """
|
||||||
SELECT projects_project.id AS id, null AS ref, 'project' AS type,
|
SELECT projects_project.id AS id, null::integer AS ref, 'project'::text AS type,
|
||||||
tags, votes_vote.object_id AS object_id, projects_project.id AS project,
|
tags, likes_like.object_id AS object_id, projects_project.id AS project,
|
||||||
slug AS slug, projects_project.name AS name, null AS subject,
|
slug, projects_project.name, null::text AS subject,
|
||||||
votes_vote.created_date, coalesce(watchers, 0) as total_watchers, coalesce(votes_votes.count, 0) total_voters, null AS assigned_to,
|
likes_like.created_date,
|
||||||
null as status, null as status_color
|
coalesce(watchers, 0) AS total_watchers, coalesce(likes_likes.count, 0) AS total_fans,
|
||||||
FROM votes_vote
|
null::integer AS assigned_to, null::text as status, null::text as status_color
|
||||||
|
FROM likes_like
|
||||||
INNER JOIN projects_project
|
INNER JOIN projects_project
|
||||||
ON (projects_project.id = votes_vote.object_id)
|
ON (projects_project.id = likes_like.object_id)
|
||||||
LEFT JOIN (SELECT project_id, count(*) watchers
|
LEFT JOIN (SELECT project_id, count(*) watchers
|
||||||
FROM notifications_notifypolicy
|
FROM notifications_notifypolicy
|
||||||
WHERE notifications_notifypolicy.notify_level != {ignore_notify_level}
|
WHERE notifications_notifypolicy.notify_level != {ignore_notify_level}
|
||||||
GROUP BY project_id
|
GROUP BY project_id
|
||||||
) type_watchers
|
) type_watchers
|
||||||
ON projects_project.id = type_watchers.project_id
|
ON projects_project.id = type_watchers.project_id
|
||||||
LEFT JOIN votes_votes
|
LEFT JOIN likes_likes
|
||||||
ON (projects_project.id = votes_votes.object_id AND {project_content_type_id} = votes_votes.content_type_id)
|
ON (projects_project.id = likes_likes.object_id AND {project_content_type_id} = likes_likes.content_type_id)
|
||||||
WHERE votes_vote.user_id = {for_user_id} AND {project_content_type_id} = votes_vote.content_type_id
|
WHERE likes_like.user_id = {for_user_id} AND {project_content_type_id} = likes_like.content_type_id
|
||||||
"""
|
"""
|
||||||
sql = sql.format(
|
sql = sql.format(
|
||||||
for_user_id=for_user.id,
|
for_user_id=for_user.id,
|
||||||
ignore_notify_level=NotifyLevel.ignore,
|
ignore_notify_level=NotifyLevel.ignore,
|
||||||
project_content_type_id=ContentType.objects.get(app_label="projects", model="project").id)
|
project_content_type_id=ContentType.objects.get(app_label="projects", model="project").id)
|
||||||
|
|
||||||
return sql
|
return sql
|
||||||
|
|
||||||
|
|
||||||
|
@ -249,8 +269,9 @@ def _build_sql_for_type(for_user, type, table_name, action_table, ref_column="re
|
||||||
SELECT {table_name}.id AS id, {ref_column} AS ref, '{type}' AS type,
|
SELECT {table_name}.id AS id, {ref_column} AS ref, '{type}' AS type,
|
||||||
tags, {action_table}.object_id AS object_id, {table_name}.{project_column} AS project,
|
tags, {action_table}.object_id AS object_id, {table_name}.{project_column} AS project,
|
||||||
{slug_column} AS slug, null AS name, {subject_column} AS subject,
|
{slug_column} AS slug, null AS name, {subject_column} AS subject,
|
||||||
{action_table}.created_date, coalesce(watchers, 0) as total_watchers, coalesce(votes_votes.count, 0) total_voters, {assigned_to_column} AS assigned_to,
|
{action_table}.created_date,
|
||||||
projects_{type}status.name as status, projects_{type}status.color as status_color
|
coalesce(watchers, 0) AS total_watchers, null::integer AS total_fans, coalesce(votes_votes.count, 0) AS total_voters,
|
||||||
|
{assigned_to_column} AS assigned_to, projects_{type}status.name as status, projects_{type}status.color as status_color
|
||||||
FROM {action_table}
|
FROM {action_table}
|
||||||
INNER JOIN django_content_type
|
INNER JOIN django_content_type
|
||||||
ON ({action_table}.content_type_id = django_content_type.id AND django_content_type.model = '{type}')
|
ON ({action_table}.content_type_id = django_content_type.id AND django_content_type.model = '{type}')
|
||||||
|
@ -272,7 +293,7 @@ def _build_sql_for_type(for_user, type, table_name, action_table, ref_column="re
|
||||||
return sql
|
return sql
|
||||||
|
|
||||||
|
|
||||||
def _get_favourites_list(for_user, from_user, action_table, project_sql_builder, type=None, q=None):
|
def get_watched_list(for_user, from_user, type=None, q=None):
|
||||||
filters_sql = ""
|
filters_sql = ""
|
||||||
and_needed = False
|
and_needed = False
|
||||||
|
|
||||||
|
@ -348,10 +369,10 @@ def _get_favourites_list(for_user, from_user, action_table, project_sql_builder,
|
||||||
for_user_id=for_user.id,
|
for_user_id=for_user.id,
|
||||||
from_user_id=from_user_id,
|
from_user_id=from_user_id,
|
||||||
filters_sql=filters_sql,
|
filters_sql=filters_sql,
|
||||||
userstories_sql=_build_sql_for_type(for_user, "userstory", "userstories_userstory", action_table, slug_column="null"),
|
userstories_sql=_build_sql_for_type(for_user, "userstory", "userstories_userstory", "notifications_watched", slug_column="null"),
|
||||||
tasks_sql=_build_sql_for_type(for_user, "task", "tasks_task", action_table, slug_column="null"),
|
tasks_sql=_build_sql_for_type(for_user, "task", "tasks_task", "notifications_watched", slug_column="null"),
|
||||||
issues_sql=_build_sql_for_type(for_user, "issue", "issues_issue", action_table, slug_column="null"),
|
issues_sql=_build_sql_for_type(for_user, "issue", "issues_issue", "notifications_watched", slug_column="null"),
|
||||||
projects_sql=project_sql_builder(for_user))
|
projects_sql=_build_watched_sql_for_projects(for_user))
|
||||||
|
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
cursor.execute(sql)
|
cursor.execute(sql)
|
||||||
|
@ -363,9 +384,167 @@ def _get_favourites_list(for_user, from_user, action_table, project_sql_builder,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_watched_list(for_user, from_user, type=None, q=None):
|
def get_liked_list(for_user, from_user, type=None, q=None):
|
||||||
return _get_favourites_list(for_user, from_user, "notifications_watched", _build_watched_sql_for_projects, type=type, q=q)
|
filters_sql = ""
|
||||||
|
and_needed = False
|
||||||
|
|
||||||
|
if type:
|
||||||
|
filters_sql += " AND type = '{type}' ".format(type=type)
|
||||||
|
|
||||||
|
if q:
|
||||||
|
filters_sql += """ AND (
|
||||||
|
to_tsvector('english_nostop', coalesce(subject,'') || ' ' ||coalesce(entities.name,'') || ' ' ||coalesce(to_char(ref, '999'),'')) @@ to_tsquery('english_nostop', '{q}')
|
||||||
|
)
|
||||||
|
""".format(q=to_tsquery(q))
|
||||||
|
|
||||||
|
sql = """
|
||||||
|
-- BEGIN Basic info: we need to mix info from different tables and denormalize it
|
||||||
|
SELECT entities.*,
|
||||||
|
projects_project.name as project_name, projects_project.description as description, projects_project.slug as project_slug, projects_project.is_private as project_is_private,
|
||||||
|
projects_project.tags_colors,
|
||||||
|
users_user.username assigned_to_username, users_user.full_name assigned_to_full_name, users_user.photo assigned_to_photo, users_user.email assigned_to_email
|
||||||
|
FROM (
|
||||||
|
{projects_sql}
|
||||||
|
) as entities
|
||||||
|
-- END Basic info
|
||||||
|
|
||||||
|
-- BEGIN Project info
|
||||||
|
LEFT JOIN projects_project
|
||||||
|
ON (entities.project = projects_project.id)
|
||||||
|
-- END Project info
|
||||||
|
|
||||||
|
-- BEGIN Assigned to user info
|
||||||
|
LEFT JOIN users_user
|
||||||
|
ON (assigned_to = users_user.id)
|
||||||
|
-- END Assigned to user info
|
||||||
|
|
||||||
|
-- BEGIN Permissions checking
|
||||||
|
LEFT JOIN projects_membership
|
||||||
|
-- Here we check the memberbships from the user requesting the info
|
||||||
|
ON (projects_membership.user_id = {from_user_id} AND projects_membership.project_id = entities.project)
|
||||||
|
|
||||||
|
LEFT JOIN users_role
|
||||||
|
ON (entities.project = users_role.project_id AND users_role.id = projects_membership.role_id)
|
||||||
|
|
||||||
|
WHERE
|
||||||
|
-- public project
|
||||||
|
(
|
||||||
|
projects_project.is_private = false
|
||||||
|
OR(
|
||||||
|
-- private project where the view_ permission is included in the user role for that project or in the anon permissions
|
||||||
|
projects_project.is_private = true
|
||||||
|
AND(
|
||||||
|
'view_project' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions))
|
||||||
|
)
|
||||||
|
))
|
||||||
|
-- END Permissions checking
|
||||||
|
{filters_sql}
|
||||||
|
|
||||||
|
ORDER BY entities.created_date DESC;
|
||||||
|
"""
|
||||||
|
|
||||||
|
from_user_id = -1
|
||||||
|
if not from_user.is_anonymous():
|
||||||
|
from_user_id = from_user.id
|
||||||
|
|
||||||
|
sql = sql.format(
|
||||||
|
for_user_id=for_user.id,
|
||||||
|
from_user_id=from_user_id,
|
||||||
|
filters_sql=filters_sql,
|
||||||
|
projects_sql=_build_liked_sql_for_projects(for_user))
|
||||||
|
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute(sql)
|
||||||
|
|
||||||
|
desc = cursor.description
|
||||||
|
return [
|
||||||
|
dict(zip([col[0] for col in desc], row))
|
||||||
|
for row in cursor.fetchall()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_voted_list(for_user, from_user, type=None, q=None):
|
def get_voted_list(for_user, from_user, type=None, q=None):
|
||||||
return _get_favourites_list(for_user, from_user, "votes_vote", _build_voted_sql_for_projects, type=type, q=q)
|
filters_sql = ""
|
||||||
|
and_needed = False
|
||||||
|
|
||||||
|
if type:
|
||||||
|
filters_sql += " AND type = '{type}' ".format(type=type)
|
||||||
|
|
||||||
|
if q:
|
||||||
|
filters_sql += """ AND (
|
||||||
|
to_tsvector('english_nostop', coalesce(subject,'') || ' ' ||coalesce(entities.name,'') || ' ' ||coalesce(to_char(ref, '999'),'')) @@ to_tsquery('english_nostop', '{q}')
|
||||||
|
)
|
||||||
|
""".format(q=to_tsquery(q))
|
||||||
|
|
||||||
|
sql = """
|
||||||
|
-- BEGIN Basic info: we need to mix info from different tables and denormalize it
|
||||||
|
SELECT entities.*,
|
||||||
|
projects_project.name as project_name, projects_project.description as description, projects_project.slug as project_slug, projects_project.is_private as project_is_private,
|
||||||
|
projects_project.tags_colors,
|
||||||
|
users_user.username assigned_to_username, users_user.full_name assigned_to_full_name, users_user.photo assigned_to_photo, users_user.email assigned_to_email
|
||||||
|
FROM (
|
||||||
|
{userstories_sql}
|
||||||
|
UNION
|
||||||
|
{tasks_sql}
|
||||||
|
UNION
|
||||||
|
{issues_sql}
|
||||||
|
) as entities
|
||||||
|
-- END Basic info
|
||||||
|
|
||||||
|
-- BEGIN Project info
|
||||||
|
LEFT JOIN projects_project
|
||||||
|
ON (entities.project = projects_project.id)
|
||||||
|
-- END Project info
|
||||||
|
|
||||||
|
-- BEGIN Assigned to user info
|
||||||
|
LEFT JOIN users_user
|
||||||
|
ON (assigned_to = users_user.id)
|
||||||
|
-- END Assigned to user info
|
||||||
|
|
||||||
|
-- BEGIN Permissions checking
|
||||||
|
LEFT JOIN projects_membership
|
||||||
|
-- Here we check the memberbships from the user requesting the info
|
||||||
|
ON (projects_membership.user_id = {from_user_id} AND projects_membership.project_id = entities.project)
|
||||||
|
|
||||||
|
LEFT JOIN users_role
|
||||||
|
ON (entities.project = users_role.project_id AND users_role.id = projects_membership.role_id)
|
||||||
|
|
||||||
|
WHERE
|
||||||
|
-- public project
|
||||||
|
(
|
||||||
|
projects_project.is_private = false
|
||||||
|
OR(
|
||||||
|
-- private project where the view_ permission is included in the user role for that project or in the anon permissions
|
||||||
|
projects_project.is_private = true
|
||||||
|
AND(
|
||||||
|
(entities.type = 'issue' AND 'view_issues' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions)))
|
||||||
|
OR (entities.type = 'task' AND 'view_tasks' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions)))
|
||||||
|
OR (entities.type = 'userstory' AND 'view_us' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions)))
|
||||||
|
)
|
||||||
|
))
|
||||||
|
-- END Permissions checking
|
||||||
|
{filters_sql}
|
||||||
|
|
||||||
|
ORDER BY entities.created_date DESC;
|
||||||
|
"""
|
||||||
|
|
||||||
|
from_user_id = -1
|
||||||
|
if not from_user.is_anonymous():
|
||||||
|
from_user_id = from_user.id
|
||||||
|
|
||||||
|
sql = sql.format(
|
||||||
|
for_user_id=for_user.id,
|
||||||
|
from_user_id=from_user_id,
|
||||||
|
filters_sql=filters_sql,
|
||||||
|
userstories_sql=_build_sql_for_type(for_user, "userstory", "userstories_userstory", "votes_vote", slug_column="null"),
|
||||||
|
tasks_sql=_build_sql_for_type(for_user, "task", "tasks_task", "votes_vote", slug_column="null"),
|
||||||
|
issues_sql=_build_sql_for_type(for_user, "issue", "issues_issue", "votes_vote", slug_column="null"))
|
||||||
|
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute(sql)
|
||||||
|
|
||||||
|
desc = cursor.description
|
||||||
|
return [
|
||||||
|
dict(zip([col[0] for col in desc], row))
|
||||||
|
for row in cursor.fetchall()
|
||||||
|
]
|
||||||
|
|
|
@ -412,14 +412,23 @@ class IssueCustomAttributesValuesFactory(Factory):
|
||||||
issue = factory.SubFactory("tests.factories.IssueFactory")
|
issue = factory.SubFactory("tests.factories.IssueFactory")
|
||||||
|
|
||||||
|
|
||||||
# class FanFactory(Factory):
|
class LikeFactory(Factory):
|
||||||
# project = factory.SubFactory("tests.factories.ProjectFactory")
|
class Meta:
|
||||||
# user = factory.SubFactory("tests.factories.UserFactory")
|
model = "likes.Like"
|
||||||
|
strategy = factory.CREATE_STRATEGY
|
||||||
|
|
||||||
|
content_type = factory.SubFactory("tests.factories.ContentTypeFactory")
|
||||||
|
object_id = factory.Sequence(lambda n: n)
|
||||||
|
user = factory.SubFactory("tests.factories.UserFactory")
|
||||||
|
|
||||||
|
|
||||||
# class StarsFactory(Factory):
|
class LikesFactory(Factory):
|
||||||
# project = factory.SubFactory("tests.factories.ProjectFactory")
|
class Meta:
|
||||||
# count = 0
|
model = "likes.Likes"
|
||||||
|
strategy = factory.CREATE_STRATEGY
|
||||||
|
|
||||||
|
content_type = factory.SubFactory("tests.factories.ContentTypeFactory")
|
||||||
|
object_id = factory.Sequence(lambda n: n)
|
||||||
|
|
||||||
|
|
||||||
class VoteFactory(Factory):
|
class VoteFactory(Factory):
|
||||||
|
|
|
@ -70,16 +70,16 @@ def data():
|
||||||
|
|
||||||
project_ct = ContentType.objects.get_for_model(Project)
|
project_ct = ContentType.objects.get_for_model(Project)
|
||||||
|
|
||||||
f.VoteFactory(content_type=project_ct, object_id=m.public_project.pk, user=m.project_member_with_perms)
|
f.LikeFactory(content_type=project_ct, object_id=m.public_project.pk, user=m.project_member_with_perms)
|
||||||
f.VoteFactory(content_type=project_ct, object_id=m.public_project.pk, user=m.project_owner)
|
f.LikeFactory(content_type=project_ct, object_id=m.public_project.pk, user=m.project_owner)
|
||||||
f.VoteFactory(content_type=project_ct, object_id=m.private_project1.pk, user=m.project_member_with_perms)
|
f.LikeFactory(content_type=project_ct, object_id=m.private_project1.pk, user=m.project_member_with_perms)
|
||||||
f.VoteFactory(content_type=project_ct, object_id=m.private_project1.pk, user=m.project_owner)
|
f.LikeFactory(content_type=project_ct, object_id=m.private_project1.pk, user=m.project_owner)
|
||||||
f.VoteFactory(content_type=project_ct, object_id=m.private_project2.pk, user=m.project_member_with_perms)
|
f.LikeFactory(content_type=project_ct, object_id=m.private_project2.pk, user=m.project_member_with_perms)
|
||||||
f.VoteFactory(content_type=project_ct, object_id=m.private_project2.pk, user=m.project_owner)
|
f.LikeFactory(content_type=project_ct, object_id=m.private_project2.pk, user=m.project_owner)
|
||||||
|
|
||||||
f.VotesFactory(content_type=project_ct, object_id=m.public_project.pk, count=2)
|
f.LikesFactory(content_type=project_ct, object_id=m.public_project.pk, count=2)
|
||||||
f.VotesFactory(content_type=project_ct, object_id=m.private_project1.pk, count=2)
|
f.LikesFactory(content_type=project_ct, object_id=m.private_project1.pk, count=2)
|
||||||
f.VotesFactory(content_type=project_ct, object_id=m.private_project2.pk, count=2)
|
f.LikesFactory(content_type=project_ct, object_id=m.private_project2.pk, count=2)
|
||||||
|
|
||||||
return m
|
return m
|
||||||
|
|
||||||
|
|
|
@ -311,3 +311,15 @@ def test_user_list_liked(client, data):
|
||||||
]
|
]
|
||||||
results = helper_test_http_method(client, 'get', url, None, users)
|
results = helper_test_http_method(client, 'get', url, None, users)
|
||||||
assert results == [200, 200, 200, 200]
|
assert results == [200, 200, 200, 200]
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_list_voted(client, data):
|
||||||
|
url = reverse('users-voted', kwargs={"pk": data.registered_user.pk})
|
||||||
|
users = [
|
||||||
|
None,
|
||||||
|
data.registered_user,
|
||||||
|
data.other_user,
|
||||||
|
data.superuser,
|
||||||
|
]
|
||||||
|
results = helper_test_http_method(client, 'get', url, None, users)
|
||||||
|
assert results == [200, 200, 200, 200]
|
||||||
|
|
|
@ -0,0 +1,123 @@
|
||||||
|
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2015 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
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
from .. import factories as f
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_like_project(client):
|
||||||
|
user = f.UserFactory.create()
|
||||||
|
project = f.create_project(owner=user)
|
||||||
|
f.MembershipFactory.create(project=project, user=user, is_owner=True)
|
||||||
|
url = reverse("projects-like", args=(project.id,))
|
||||||
|
|
||||||
|
client.login(user)
|
||||||
|
response = client.post(url)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_unlike_project(client):
|
||||||
|
user = f.UserFactory.create()
|
||||||
|
project = f.create_project(owner=user)
|
||||||
|
f.MembershipFactory.create(project=project, user=user, is_owner=True)
|
||||||
|
url = reverse("projects-unlike", args=(project.id,))
|
||||||
|
|
||||||
|
client.login(user)
|
||||||
|
response = client.post(url)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_project_fans(client):
|
||||||
|
user = f.UserFactory.create()
|
||||||
|
project = f.create_project(owner=user)
|
||||||
|
f.MembershipFactory.create(project=project, user=user, is_owner=True)
|
||||||
|
f.LikeFactory.create(content_object=project, user=user)
|
||||||
|
url = reverse("project-fans-list", args=(project.id,))
|
||||||
|
|
||||||
|
client.login(user)
|
||||||
|
response = client.get(url)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data[0]['id'] == user.id
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_project_fan(client):
|
||||||
|
user = f.UserFactory.create()
|
||||||
|
project = f.create_project(owner=user)
|
||||||
|
f.MembershipFactory.create(project=project, user=user, is_owner=True)
|
||||||
|
like = f.LikeFactory.create(content_object=project, user=user)
|
||||||
|
url = reverse("project-fans-detail", args=(project.id, like.user.id))
|
||||||
|
|
||||||
|
client.login(user)
|
||||||
|
response = client.get(url)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data['id'] == like.user.id
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_project_total_fans(client):
|
||||||
|
user = f.UserFactory.create()
|
||||||
|
project = f.create_project(owner=user)
|
||||||
|
f.MembershipFactory.create(project=project, user=user, is_owner=True)
|
||||||
|
url = reverse("projects-detail", args=(project.id,))
|
||||||
|
|
||||||
|
f.LikesFactory.create(content_object=project, count=5)
|
||||||
|
|
||||||
|
client.login(user)
|
||||||
|
response = client.get(url)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data['total_fans'] == 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_project_is_fan(client):
|
||||||
|
user = f.UserFactory.create()
|
||||||
|
project = f.create_project(owner=user)
|
||||||
|
f.MembershipFactory.create(project=project, user=user, is_owner=True)
|
||||||
|
f.LikesFactory.create(content_object=project)
|
||||||
|
url_detail = reverse("projects-detail", args=(project.id,))
|
||||||
|
url_like = reverse("projects-like", args=(project.id,))
|
||||||
|
url_unlike = reverse("projects-unlike", args=(project.id,))
|
||||||
|
|
||||||
|
client.login(user)
|
||||||
|
|
||||||
|
response = client.get(url_detail)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data['total_fans'] == 0
|
||||||
|
assert response.data['is_fan'] == False
|
||||||
|
|
||||||
|
response = client.post(url_like)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
response = client.get(url_detail)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data['total_fans'] == 1
|
||||||
|
assert response.data['is_fan'] == True
|
||||||
|
|
||||||
|
response = client.post(url_unlike)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
response = client.get(url_detail)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data['total_fans'] == 0
|
||||||
|
assert response.data['is_fan'] == False
|
|
@ -9,10 +9,10 @@ from .. import factories as f
|
||||||
|
|
||||||
from taiga.base.utils import json
|
from taiga.base.utils import json
|
||||||
from taiga.users import models
|
from taiga.users import models
|
||||||
from taiga.users.serializers import FanSerializer, VotedSerializer
|
from taiga.users.serializers import LikedObjectSerializer, VotedObjectSerializer
|
||||||
from taiga.auth.tokens import get_token_for_user
|
from taiga.auth.tokens import get_token_for_user
|
||||||
from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
|
from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
|
||||||
from taiga.users.services import get_watched_list, get_voted_list
|
from taiga.users.services import get_watched_list, get_voted_list, get_liked_list
|
||||||
|
|
||||||
from easy_thumbnails.files import generate_all_aliases, get_thumbnailer
|
from easy_thumbnails.files import generate_all_aliases, get_thumbnailer
|
||||||
|
|
||||||
|
@ -377,6 +377,25 @@ def test_get_watched_list():
|
||||||
assert len(get_watched_list(fav_user, viewer_user, q="unexisting text")) == 0
|
assert len(get_watched_list(fav_user, viewer_user, q="unexisting text")) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_liked_list():
|
||||||
|
fan_user = f.UserFactory()
|
||||||
|
viewer_user = f.UserFactory()
|
||||||
|
|
||||||
|
project = f.ProjectFactory(is_private=False, name="Testing project")
|
||||||
|
role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"])
|
||||||
|
membership = f.MembershipFactory(project=project, role=role, user=fan_user)
|
||||||
|
content_type = ContentType.objects.get_for_model(project)
|
||||||
|
f.LikeFactory(content_type=content_type, object_id=project.id, user=fan_user)
|
||||||
|
f.LikesFactory(content_type=content_type, object_id=project.id, count=1)
|
||||||
|
|
||||||
|
assert len(get_liked_list(fan_user, viewer_user)) == 1
|
||||||
|
assert len(get_liked_list(fan_user, viewer_user, type="project")) == 1
|
||||||
|
assert len(get_liked_list(fan_user, viewer_user, type="unknown")) == 0
|
||||||
|
|
||||||
|
assert len(get_liked_list(fan_user, viewer_user, q="project")) == 1
|
||||||
|
assert len(get_liked_list(fan_user, viewer_user, q="unexisting text")) == 0
|
||||||
|
|
||||||
|
|
||||||
def test_get_voted_list():
|
def test_get_voted_list():
|
||||||
fav_user = f.UserFactory()
|
fav_user = f.UserFactory()
|
||||||
viewer_user = f.UserFactory()
|
viewer_user = f.UserFactory()
|
||||||
|
@ -384,9 +403,6 @@ def test_get_voted_list():
|
||||||
project = f.ProjectFactory(is_private=False, name="Testing project")
|
project = f.ProjectFactory(is_private=False, name="Testing project")
|
||||||
role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"])
|
role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"])
|
||||||
membership = f.MembershipFactory(project=project, role=role, user=fav_user)
|
membership = f.MembershipFactory(project=project, role=role, user=fav_user)
|
||||||
content_type = ContentType.objects.get_for_model(project)
|
|
||||||
f.VoteFactory(content_type=content_type, object_id=project.id, user=fav_user)
|
|
||||||
f.VotesFactory(content_type=content_type, object_id=project.id, count=1)
|
|
||||||
|
|
||||||
user_story = f.UserStoryFactory(project=project, subject="Testing user story")
|
user_story = f.UserStoryFactory(project=project, subject="Testing user story")
|
||||||
content_type = ContentType.objects.get_for_model(user_story)
|
content_type = ContentType.objects.get_for_model(user_story)
|
||||||
|
@ -403,8 +419,7 @@ def test_get_voted_list():
|
||||||
f.VoteFactory(content_type=content_type, object_id=issue.id, user=fav_user)
|
f.VoteFactory(content_type=content_type, object_id=issue.id, user=fav_user)
|
||||||
f.VotesFactory(content_type=content_type, object_id=issue.id, count=1)
|
f.VotesFactory(content_type=content_type, object_id=issue.id, count=1)
|
||||||
|
|
||||||
assert len(get_voted_list(fav_user, viewer_user)) == 4
|
assert len(get_voted_list(fav_user, viewer_user)) == 3
|
||||||
assert len(get_voted_list(fav_user, viewer_user, type="project")) == 1
|
|
||||||
assert len(get_voted_list(fav_user, viewer_user, type="userstory")) == 1
|
assert len(get_voted_list(fav_user, viewer_user, type="userstory")) == 1
|
||||||
assert len(get_voted_list(fav_user, viewer_user, type="task")) == 1
|
assert len(get_voted_list(fav_user, viewer_user, type="task")) == 1
|
||||||
assert len(get_voted_list(fav_user, viewer_user, type="issue")) == 1
|
assert len(get_voted_list(fav_user, viewer_user, type="issue")) == 1
|
||||||
|
@ -423,7 +438,8 @@ def test_get_watched_list_valid_info_for_project():
|
||||||
project.add_watcher(fav_user)
|
project.add_watcher(fav_user)
|
||||||
|
|
||||||
raw_project_watch_info = get_watched_list(fav_user, viewer_user)[0]
|
raw_project_watch_info = get_watched_list(fav_user, viewer_user)[0]
|
||||||
project_watch_info = FanSerializer(raw_project_watch_info).data
|
|
||||||
|
project_watch_info = LikedObjectSerializer(raw_project_watch_info).data
|
||||||
|
|
||||||
assert project_watch_info["type"] == "project"
|
assert project_watch_info["type"] == "project"
|
||||||
assert project_watch_info["id"] == project.id
|
assert project_watch_info["id"] == project.id
|
||||||
|
@ -454,46 +470,46 @@ def test_get_watched_list_valid_info_for_project():
|
||||||
assert project_watch_info["assigned_to_photo"] == None
|
assert project_watch_info["assigned_to_photo"] == None
|
||||||
|
|
||||||
|
|
||||||
def test_get_voted_list_valid_info_for_project():
|
def test_get_liked_list_valid_info():
|
||||||
fav_user = f.UserFactory()
|
fan_user = f.UserFactory()
|
||||||
viewer_user = f.UserFactory()
|
viewer_user = f.UserFactory()
|
||||||
|
|
||||||
project = f.ProjectFactory(is_private=False, name="Testing project", tags=['test', 'tag'])
|
project = f.ProjectFactory(is_private=False, name="Testing project", tags=['test', 'tag'])
|
||||||
content_type = ContentType.objects.get_for_model(project)
|
content_type = ContentType.objects.get_for_model(project)
|
||||||
vote = f.VoteFactory(content_type=content_type, object_id=project.id, user=fav_user)
|
like = f.LikeFactory(content_type=content_type, object_id=project.id, user=fan_user)
|
||||||
f.VotesFactory(content_type=content_type, object_id=project.id, count=1)
|
f.LikesFactory(content_type=content_type, object_id=project.id, count=1)
|
||||||
|
|
||||||
raw_project_vote_info = get_voted_list(fav_user, viewer_user)[0]
|
raw_project_like_info = get_liked_list(fan_user, viewer_user)[0]
|
||||||
project_vote_info = FanSerializer(raw_project_vote_info).data
|
project_like_info = LikedObjectSerializer(raw_project_like_info).data
|
||||||
|
|
||||||
assert project_vote_info["type"] == "project"
|
assert project_like_info["type"] == "project"
|
||||||
assert project_vote_info["id"] == project.id
|
assert project_like_info["id"] == project.id
|
||||||
assert project_vote_info["ref"] == None
|
assert project_like_info["ref"] == None
|
||||||
assert project_vote_info["slug"] == project.slug
|
assert project_like_info["slug"] == project.slug
|
||||||
assert project_vote_info["name"] == project.name
|
assert project_like_info["name"] == project.name
|
||||||
assert project_vote_info["subject"] == None
|
assert project_like_info["subject"] == None
|
||||||
assert project_vote_info["description"] == project.description
|
assert project_like_info["description"] == project.description
|
||||||
assert project_vote_info["assigned_to"] == None
|
assert project_like_info["assigned_to"] == None
|
||||||
assert project_vote_info["status"] == None
|
assert project_like_info["status"] == None
|
||||||
assert project_vote_info["status_color"] == None
|
assert project_like_info["status_color"] == None
|
||||||
|
|
||||||
tags_colors = {tc["name"]:tc["color"] for tc in project_vote_info["tags_colors"]}
|
tags_colors = {tc["name"]:tc["color"] for tc in project_like_info["tags_colors"]}
|
||||||
assert "test" in tags_colors
|
assert "test" in tags_colors
|
||||||
assert "tag" in tags_colors
|
assert "tag" in tags_colors
|
||||||
|
|
||||||
assert project_vote_info["is_private"] == project.is_private
|
assert project_like_info["is_private"] == project.is_private
|
||||||
|
|
||||||
assert project_vote_info["is_fan"] == False
|
assert project_like_info["is_fan"] == False
|
||||||
assert project_vote_info["is_watcher"] == False
|
assert project_like_info["is_watcher"] == False
|
||||||
assert project_vote_info["total_watchers"] == 0
|
assert project_like_info["total_watchers"] == 0
|
||||||
assert project_vote_info["total_fans"] == 1
|
assert project_like_info["total_fans"] == 1
|
||||||
assert project_vote_info["project"] == None
|
assert project_like_info["project"] == None
|
||||||
assert project_vote_info["project_name"] == None
|
assert project_like_info["project_name"] == None
|
||||||
assert project_vote_info["project_slug"] == None
|
assert project_like_info["project_slug"] == None
|
||||||
assert project_vote_info["project_is_private"] == None
|
assert project_like_info["project_is_private"] == None
|
||||||
assert project_vote_info["assigned_to_username"] == None
|
assert project_like_info["assigned_to_username"] == None
|
||||||
assert project_vote_info["assigned_to_full_name"] == None
|
assert project_like_info["assigned_to_full_name"] == None
|
||||||
assert project_vote_info["assigned_to_photo"] == None
|
assert project_like_info["assigned_to_photo"] == None
|
||||||
|
|
||||||
|
|
||||||
def test_get_watched_list_valid_info_for_not_project_types():
|
def test_get_watched_list_valid_info_for_not_project_types():
|
||||||
|
@ -517,7 +533,7 @@ def test_get_watched_list_valid_info_for_not_project_types():
|
||||||
|
|
||||||
instance.add_watcher(fav_user)
|
instance.add_watcher(fav_user)
|
||||||
raw_instance_watch_info = get_watched_list(fav_user, viewer_user, type=object_type)[0]
|
raw_instance_watch_info = get_watched_list(fav_user, viewer_user, type=object_type)[0]
|
||||||
instance_watch_info = VotedSerializer(raw_instance_watch_info).data
|
instance_watch_info = VotedObjectSerializer(raw_instance_watch_info).data
|
||||||
|
|
||||||
assert instance_watch_info["type"] == object_type
|
assert instance_watch_info["type"] == object_type
|
||||||
assert instance_watch_info["id"] == instance.id
|
assert instance_watch_info["id"] == instance.id
|
||||||
|
@ -548,7 +564,7 @@ def test_get_watched_list_valid_info_for_not_project_types():
|
||||||
assert instance_watch_info["assigned_to_photo"] != ""
|
assert instance_watch_info["assigned_to_photo"] != ""
|
||||||
|
|
||||||
|
|
||||||
def test_get_voted_list_valid_info_for_not_project_types():
|
def test_get_voted_list_valid_info():
|
||||||
fav_user = f.UserFactory()
|
fav_user = f.UserFactory()
|
||||||
viewer_user = f.UserFactory()
|
viewer_user = f.UserFactory()
|
||||||
assigned_to_user = f.UserFactory()
|
assigned_to_user = f.UserFactory()
|
||||||
|
@ -572,7 +588,7 @@ def test_get_voted_list_valid_info_for_not_project_types():
|
||||||
f.VotesFactory(content_type=content_type, object_id=instance.id, count=3)
|
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]
|
raw_instance_vote_info = get_voted_list(fav_user, viewer_user, type=object_type)[0]
|
||||||
instance_vote_info = VotedSerializer(raw_instance_vote_info).data
|
instance_vote_info = VotedObjectSerializer(raw_instance_vote_info).data
|
||||||
|
|
||||||
assert instance_vote_info["type"] == object_type
|
assert instance_vote_info["type"] == object_type
|
||||||
assert instance_vote_info["id"] == instance.id
|
assert instance_vote_info["id"] == instance.id
|
||||||
|
@ -603,6 +619,87 @@ def test_get_voted_list_valid_info_for_not_project_types():
|
||||||
assert instance_vote_info["assigned_to_photo"] != ""
|
assert instance_vote_info["assigned_to_photo"] != ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_watched_list_with_liked_and_voted_objects(client):
|
||||||
|
fav_user = f.UserFactory()
|
||||||
|
|
||||||
|
project = f.ProjectFactory(is_private=False, name="Testing project")
|
||||||
|
role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"])
|
||||||
|
membership = f.MembershipFactory(project=project, role=role, user=fav_user)
|
||||||
|
project.add_watcher(fav_user)
|
||||||
|
content_type = ContentType.objects.get_for_model(project)
|
||||||
|
f.LikeFactory(content_type=content_type, object_id=project.id, user=fav_user)
|
||||||
|
|
||||||
|
voted_elements_factories = {
|
||||||
|
"userstory": f.UserStoryFactory,
|
||||||
|
"task": f.TaskFactory,
|
||||||
|
"issue": f.IssueFactory
|
||||||
|
}
|
||||||
|
|
||||||
|
for object_type in voted_elements_factories:
|
||||||
|
instance = voted_elements_factories[object_type](project=project)
|
||||||
|
content_type = ContentType.objects.get_for_model(instance)
|
||||||
|
instance.add_watcher(fav_user)
|
||||||
|
f.VoteFactory(content_type=content_type, object_id=instance.id, user=fav_user)
|
||||||
|
|
||||||
|
client.login(fav_user)
|
||||||
|
url = reverse('users-watched', kwargs={"pk": fav_user.pk})
|
||||||
|
response = client.get(url, content_type="application/json")
|
||||||
|
|
||||||
|
for element_data in response.data:
|
||||||
|
#assert element_data["is_watcher"] == True
|
||||||
|
if element_data["type"] == "project":
|
||||||
|
assert element_data["is_fan"] == True
|
||||||
|
else:
|
||||||
|
assert element_data["is_voter"] == True
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_liked_list_with_watched_objects(client):
|
||||||
|
fav_user = f.UserFactory()
|
||||||
|
|
||||||
|
project = f.ProjectFactory(is_private=False, name="Testing project")
|
||||||
|
role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"])
|
||||||
|
membership = f.MembershipFactory(project=project, role=role, user=fav_user)
|
||||||
|
project.add_watcher(fav_user)
|
||||||
|
content_type = ContentType.objects.get_for_model(project)
|
||||||
|
f.LikeFactory(content_type=content_type, object_id=project.id, user=fav_user)
|
||||||
|
|
||||||
|
client.login(fav_user)
|
||||||
|
url = reverse('users-liked', kwargs={"pk": fav_user.pk})
|
||||||
|
response = client.get(url, content_type="application/json")
|
||||||
|
|
||||||
|
element_data = response.data[0]
|
||||||
|
assert element_data["is_watcher"] == True
|
||||||
|
assert element_data["is_fan"] == True
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_voted_list_with_watched_objects(client):
|
||||||
|
fav_user = f.UserFactory()
|
||||||
|
|
||||||
|
project = f.ProjectFactory(is_private=False, name="Testing project")
|
||||||
|
role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"])
|
||||||
|
membership = f.MembershipFactory(project=project, role=role, user=fav_user)
|
||||||
|
|
||||||
|
voted_elements_factories = {
|
||||||
|
"userstory": f.UserStoryFactory,
|
||||||
|
"task": f.TaskFactory,
|
||||||
|
"issue": f.IssueFactory
|
||||||
|
}
|
||||||
|
|
||||||
|
for object_type in voted_elements_factories:
|
||||||
|
instance = voted_elements_factories[object_type](project=project)
|
||||||
|
content_type = ContentType.objects.get_for_model(instance)
|
||||||
|
instance.add_watcher(fav_user)
|
||||||
|
f.VoteFactory(content_type=content_type, object_id=instance.id, user=fav_user)
|
||||||
|
|
||||||
|
client.login(fav_user)
|
||||||
|
url = reverse('users-voted', kwargs={"pk": fav_user.pk})
|
||||||
|
response = client.get(url, content_type="application/json")
|
||||||
|
|
||||||
|
for element_data in response.data:
|
||||||
|
assert element_data["is_watcher"] == True
|
||||||
|
assert element_data["is_voter"] == True
|
||||||
|
|
||||||
|
|
||||||
def test_get_watched_list_permissions():
|
def test_get_watched_list_permissions():
|
||||||
fav_user = f.UserFactory()
|
fav_user = f.UserFactory()
|
||||||
viewer_unpriviliged_user = f.UserFactory()
|
viewer_unpriviliged_user = f.UserFactory()
|
||||||
|
@ -637,6 +734,33 @@ def test_get_watched_list_permissions():
|
||||||
assert len(get_watched_list(fav_user, viewer_unpriviliged_user)) == 4
|
assert len(get_watched_list(fav_user, viewer_unpriviliged_user)) == 4
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_liked_list_permissions():
|
||||||
|
fan_user = f.UserFactory()
|
||||||
|
viewer_unpriviliged_user = f.UserFactory()
|
||||||
|
viewer_priviliged_user = f.UserFactory()
|
||||||
|
|
||||||
|
project = f.ProjectFactory(is_private=True, name="Testing project")
|
||||||
|
role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"])
|
||||||
|
membership = f.MembershipFactory(project=project, role=role, user=viewer_priviliged_user)
|
||||||
|
content_type = ContentType.objects.get_for_model(project)
|
||||||
|
f.LikeFactory(content_type=content_type, object_id=project.id, user=fan_user)
|
||||||
|
f.LikesFactory(content_type=content_type, object_id=project.id, count=1)
|
||||||
|
|
||||||
|
#If the project is private a viewer user without any permission shouldn' see
|
||||||
|
# any vote
|
||||||
|
assert len(get_liked_list(fan_user, viewer_unpriviliged_user)) == 0
|
||||||
|
|
||||||
|
#If the project is private but the viewer user has permissions the votes should
|
||||||
|
# be accesible
|
||||||
|
assert len(get_liked_list(fan_user, viewer_priviliged_user)) == 1
|
||||||
|
|
||||||
|
#If the project is private but has the required anon permissions the votes should
|
||||||
|
# be accesible by any user too
|
||||||
|
project.anon_permissions = ["view_project", "view_us", "view_tasks", "view_issues"]
|
||||||
|
project.save()
|
||||||
|
assert len(get_liked_list(fan_user, viewer_unpriviliged_user)) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_get_voted_list_permissions():
|
def test_get_voted_list_permissions():
|
||||||
fav_user = f.UserFactory()
|
fav_user = f.UserFactory()
|
||||||
viewer_unpriviliged_user = f.UserFactory()
|
viewer_unpriviliged_user = f.UserFactory()
|
||||||
|
@ -645,9 +769,6 @@ def test_get_voted_list_permissions():
|
||||||
project = f.ProjectFactory(is_private=True, name="Testing project")
|
project = f.ProjectFactory(is_private=True, name="Testing project")
|
||||||
role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"])
|
role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"])
|
||||||
membership = f.MembershipFactory(project=project, role=role, user=viewer_priviliged_user)
|
membership = f.MembershipFactory(project=project, role=role, user=viewer_priviliged_user)
|
||||||
content_type = ContentType.objects.get_for_model(project)
|
|
||||||
f.VoteFactory(content_type=content_type, object_id=project.id, user=fav_user)
|
|
||||||
f.VotesFactory(content_type=content_type, object_id=project.id, count=1)
|
|
||||||
|
|
||||||
user_story = f.UserStoryFactory(project=project, subject="Testing user story")
|
user_story = f.UserStoryFactory(project=project, subject="Testing user story")
|
||||||
content_type = ContentType.objects.get_for_model(user_story)
|
content_type = ContentType.objects.get_for_model(user_story)
|
||||||
|
@ -670,10 +791,10 @@ def test_get_voted_list_permissions():
|
||||||
|
|
||||||
#If the project is private but the viewer user has permissions the votes should
|
#If the project is private but the viewer user has permissions the votes should
|
||||||
# be accesible
|
# be accesible
|
||||||
assert len(get_voted_list(fav_user, viewer_priviliged_user)) == 4
|
assert len(get_voted_list(fav_user, viewer_priviliged_user)) == 3
|
||||||
|
|
||||||
#If the project is private but has the required anon permissions the votes should
|
#If the project is private but has the required anon permissions the votes should
|
||||||
# be accesible by any user too
|
# be accesible by any user too
|
||||||
project.anon_permissions = ["view_project", "view_us", "view_tasks", "view_issues"]
|
project.anon_permissions = ["view_project", "view_us", "view_tasks", "view_issues"]
|
||||||
project.save()
|
project.save()
|
||||||
assert len(get_voted_list(fav_user, viewer_unpriviliged_user)) == 4
|
assert len(get_voted_list(fav_user, viewer_unpriviliged_user)) == 3
|
||||||
|
|
Loading…
Reference in New Issue