Merge pull request #492 from taigaio/refactoring-likes

Refactoring likes and votes
remotes/origin/logger
David Barragán Merino 2015-10-23 10:51:59 +02:00
commit 0fc99d3d6f
27 changed files with 1231 additions and 299 deletions

View File

@ -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",

View File

@ -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)

View File

View File

@ -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

View File

@ -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')]),
),
]

View File

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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')

View File

@ -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))

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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"], [])

View File

@ -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()
]

View File

@ -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):

View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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