diff --git a/CHANGELOG.md b/CHANGELOG.md
index 04307eb6..bb640e86 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,13 +4,15 @@
## 1.9.0 ??? (unreleased)
### Features
-- Add a "field type" property for custom fields: 'text' and 'multiline text' right now (thanks to [@artlepool](https://github.com/artlepool))
+- Add a "field type" property for custom fields: 'text' and 'multiline text' right now (thanks to [@artlepool](https://github.com/artlepool)).
- Allow multiple actions in the commit messages.
- Now every user that coments USs, Issues or Tasks will be involved in it (add author to the watchers list).
- Fix the compatibility with BitBucket webhooks and add issues and issues comments integration.
- Add custom videoconference system.
- Add support for comments in the Gitlab webhooks integration.
- Now profile timelines only show content about the objects (US/Tasks/Issues/Wiki pages) you are involved.
+- US, tasks and Issues can be upvoted or downvoted and the voters list can be obtained.
+- Project can be starred or unstarred and the fans list can be obtained.
- i18n.
- Add polish (pl) translation.
- Add portuguese (Brazil) (pt_BR) translation.
diff --git a/taiga/permissions/permissions.py b/taiga/permissions/permissions.py
index b2e516f4..e711c77a 100644
--- a/taiga/permissions/permissions.py
+++ b/taiga/permissions/permissions.py
@@ -32,7 +32,6 @@ USER_PERMISSIONS = [
('view_milestones', _('View milestones')),
('view_us', _('View user stories')),
('view_issues', _('View issues')),
- ('vote_issues', _('Vote issues')),
('view_tasks', _('View tasks')),
('view_wiki_pages', _('View wiki pages')),
('view_wiki_links', _('View wiki links')),
@@ -41,15 +40,20 @@ USER_PERMISSIONS = [
('add_comments_to_us', _('Add comments to user stories')),
('add_comments_to_task', _('Add comments to tasks')),
('add_issue', _('Add issues')),
- ('add_comments_issue', _('Add comments to issues')),
+ ('add_comments_to_issue', _('Add comments to issues')),
('add_wiki_page', _('Add wiki page')),
('modify_wiki_page', _('Modify wiki page')),
('add_wiki_link', _('Add wiki link')),
('modify_wiki_link', _('Modify wiki link')),
+ ('star_project', _('Star project')),
+ ('vote_us', _('Vote user story')),
+ ('vote_task', _('Vote task')),
+ ('vote_issue', _('Vote issue')),
]
MEMBERS_PERMISSIONS = [
('view_project', _('View project')),
+ ('star_project', _('Star project')),
# Milestone permissions
('view_milestones', _('View milestones')),
('add_milestone', _('Add milestone')),
@@ -60,17 +64,19 @@ MEMBERS_PERMISSIONS = [
('add_us', _('Add user story')),
('modify_us', _('Modify user story')),
('delete_us', _('Delete user story')),
+ ('vote_us', _('Vote user story')),
# Task permissions
('view_tasks', _('View tasks')),
('add_task', _('Add task')),
('modify_task', _('Modify task')),
('delete_task', _('Delete task')),
+ ('vote_task', _('Vote task')),
# Issue permissions
('view_issues', _('View issues')),
- ('vote_issues', _('Vote issues')),
('add_issue', _('Add issue')),
('modify_issue', _('Modify issue')),
('delete_issue', _('Delete issue')),
+ ('vote_issue', _('Vote issue')),
# Wiki page permissions
('view_wiki_pages', _('View wiki pages')),
('add_wiki_page', _('Add wiki page')),
diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index cd70fbf3..9ff9be8c 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -44,15 +44,14 @@ from . import models
from . import permissions
from . import services
-from .votes import serializers as votes_serializers
-from .votes import services as votes_service
-from .votes.utils import attach_votescount_to_queryset
+from .votes.mixins.viewsets import StarredResourceMixin, VotersViewSetMixin
######################################################
## Project
######################################################
-class ProjectViewSet(HistoryResourceMixin, ModelCrudViewSet):
+class ProjectViewSet(StarredResourceMixin, HistoryResourceMixin, ModelCrudViewSet):
+ queryset = models.Project.objects.all()
serializer_class = serializers.ProjectDetailSerializer
admin_serializer_class = serializers.ProjectDetailAdminSerializer
list_serializer_class = serializers.ProjectSerializer
@@ -61,6 +60,10 @@ class ProjectViewSet(HistoryResourceMixin, ModelCrudViewSet):
filter_fields = (('member', 'members'),)
order_by_fields = ("memberships__user_order",)
+ def get_queryset(self):
+ qs = super().get_queryset()
+ return self.attach_votes_attrs_to_queryset(qs)
+
@list_route(methods=["POST"])
def bulk_update_order(self, request, **kwargs):
if self.request.user.is_anonymous():
@@ -74,10 +77,6 @@ class ProjectViewSet(HistoryResourceMixin, ModelCrudViewSet):
services.update_projects_order_in_bulk(data, "user_order", request.user)
return response.NoContent(data=None)
- def get_queryset(self):
- qs = models.Project.objects.all()
- return attach_votescount_to_queryset(qs, as_field="stars_count")
-
def get_serializer_class(self):
if self.action == "list":
return self.list_serializer_class
@@ -166,29 +165,6 @@ class ProjectViewSet(HistoryResourceMixin, ModelCrudViewSet):
self.check_permissions(request, "tags_colors", project)
return response.Ok(dict(project.tags_colors))
- @detail_route(methods=["POST"])
- def star(self, request, pk=None):
- project = self.get_object()
- self.check_permissions(request, "star", project)
- votes_service.add_vote(project, user=request.user)
- return response.Ok()
-
- @detail_route(methods=["POST"])
- def unstar(self, request, pk=None):
- project = self.get_object()
- self.check_permissions(request, "unstar", project)
- votes_service.remove_vote(project, user=request.user)
- return response.Ok()
-
- @detail_route(methods=["GET"])
- def fans(self, request, pk=None):
- project = self.get_object()
- self.check_permissions(request, "fans", project)
-
- voters = votes_service.get_voters(project)
- voters_data = votes_serializers.VoterSerializer(voters, many=True)
- return response.Ok(voters_data.data)
-
@detail_route(methods=["POST"])
def create_template(self, request, **kwargs):
template_name = request.DATA.get('template_name', None)
@@ -287,6 +263,10 @@ class ProjectViewSet(HistoryResourceMixin, ModelCrudViewSet):
return response.NoContent()
+class ProjectFansViewSet(VotersViewSetMixin, ModelListViewSet):
+ permission_classes = (permissions.ProjectFansPermission,)
+ resource_model = models.Project
+
######################################################
## Custom values for selectors
diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py
index ee587e8c..6fc78ce3 100644
--- a/taiga/projects/issues/api.py
+++ b/taiga/projects/issues/api.py
@@ -16,7 +16,7 @@
from django.utils.translation import ugettext as _
from django.db.models import Q
-from django.http import Http404, HttpResponse
+from django.http import HttpResponse
from taiga.base import filters
from taiga.base import exceptions as exc
@@ -33,16 +33,17 @@ from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.models import Project, IssueStatus, Severity, Priority, IssueType
from taiga.projects.milestones.models import Milestone
-from taiga.projects.votes.utils import attach_votescount_to_queryset
-from taiga.projects.votes import services as votes_service
-from taiga.projects.votes import serializers as votes_serializers
+from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
+
from . import models
from . import services
from . import permissions
from . import serializers
-class IssueViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet):
+class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
+ ModelCrudViewSet):
+ queryset = models.Issue.objects.all()
permission_classes = (permissions.IssuePermission, )
filter_backends = (filters.CanViewIssuesFilterBackend,
filters.OwnersFilter,
@@ -139,10 +140,9 @@ class IssueViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
return super().update(request, *args, **kwargs)
def get_queryset(self):
- qs = models.Issue.objects.all()
+ qs = super().get_queryset()
qs = qs.prefetch_related("attachments")
- qs = attach_votescount_to_queryset(qs, as_field="votes_count")
- return qs
+ return self.attach_votes_attrs_to_queryset(qs)
def pre_save(self, obj):
if not obj.id:
@@ -237,51 +237,7 @@ class IssueViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
return response.BadRequest(serializer.errors)
- @detail_route(methods=['post'])
- def upvote(self, request, pk=None):
- issue = get_object_or_404(models.Issue, pk=pk)
- self.check_permissions(request, 'upvote', issue)
-
- votes_service.add_vote(issue, user=request.user)
- return response.Ok()
-
- @detail_route(methods=['post'])
- def downvote(self, request, pk=None):
- issue = get_object_or_404(models.Issue, pk=pk)
-
- self.check_permissions(request, 'downvote', issue)
-
- votes_service.remove_vote(issue, user=request.user)
- return response.Ok()
-
-
-class VotersViewSet(ModelListViewSet):
- serializer_class = votes_serializers.VoterSerializer
- list_serializer_class = votes_serializers.VoterSerializer
- permission_classes = (permissions.IssueVotersPermission, )
-
- def retrieve(self, request, *args, **kwargs):
- pk = kwargs.get("pk", None)
- issue_id = kwargs.get("issue_id", None)
- issue = get_object_or_404(models.Issue, pk=issue_id)
-
- self.check_permissions(request, 'retrieve', issue)
-
- try:
- self.object = votes_service.get_voters(issue).get(pk=pk)
- except User.DoesNotExist:
- raise Http404
-
- serializer = self.get_serializer(self.object)
- return response.Ok(serializer.data)
-
- def list(self, request, *args, **kwargs):
- issue_id = kwargs.get("issue_id", None)
- issue = get_object_or_404(models.Issue, pk=issue_id)
- self.check_permissions(request, 'list', issue)
- return super().list(request, *args, **kwargs)
-
- def get_queryset(self):
- issue = models.Issue.objects.get(pk=self.kwargs.get("issue_id"))
- return votes_service.get_voters(issue)
+class IssueVotersViewSet(VotersViewSetMixin, ModelListViewSet):
+ permission_classes = (permissions.IssueVotersPermission,)
+ resource_model = models.Issue
diff --git a/taiga/projects/issues/permissions.py b/taiga/projects/issues/permissions.py
index 076b57a0..423866bc 100644
--- a/taiga/projects/issues/permissions.py
+++ b/taiga/projects/issues/permissions.py
@@ -31,10 +31,10 @@ class IssuePermission(TaigaResourcePermission):
list_perms = AllowAny()
filters_data_perms = AllowAny()
csv_perms = AllowAny()
- upvote_perms = IsAuthenticated() & HasProjectPerm('vote_issues')
- downvote_perms = IsAuthenticated() & HasProjectPerm('vote_issues')
bulk_create_perms = HasProjectPerm('add_issue')
delete_comment_perms= HasProjectPerm('modify_issue')
+ upvote_perms = IsAuthenticated() & HasProjectPerm('view_issues')
+ downvote_perms = IsAuthenticated() & HasProjectPerm('view_issues')
class HasIssueIdUrlParam(PermissionComponent):
@@ -49,8 +49,4 @@ class IssueVotersPermission(TaigaResourcePermission):
enought_perms = IsProjectOwner() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_issues')
- create_perms = HasProjectPerm('add_issue')
- update_perms = HasProjectPerm('modify_issue')
- partial_update_perms = HasProjectPerm('modify_issue')
- destroy_perms = HasProjectPerm('delete_issue')
list_perms = HasProjectPerm('view_issues')
diff --git a/taiga/projects/issues/serializers.py b/taiga/projects/issues/serializers.py
index 2b2723dd..76023cf0 100644
--- a/taiga/projects/issues/serializers.py
+++ b/taiga/projects/issues/serializers.py
@@ -19,17 +19,18 @@ from taiga.base.fields import TagsField
from taiga.base.fields import PgArrayField
from taiga.base.neighbors import NeighborsSerializerMixin
-
from taiga.mdrender.service import render as mdrender
from taiga.projects.validators import ProjectExistsValidator
from taiga.projects.notifications.validators import WatchersValidator
from taiga.projects.serializers import BasicIssueStatusSerializer
+from taiga.projects.votes.mixins.serializers import VotedResourceSerializerMixin
+
from taiga.users.serializers import UserBasicInfoSerializer
from . import models
-class IssueSerializer(WatchersValidator, serializers.ModelSerializer):
+class IssueSerializer(WatchersValidator, VotedResourceSerializerMixin, serializers.ModelSerializer):
tags = TagsField(required=False)
external_reference = PgArrayField(required=False)
is_closed = serializers.Field(source="is_closed")
@@ -37,7 +38,6 @@ class IssueSerializer(WatchersValidator, serializers.ModelSerializer):
generated_user_stories = serializers.SerializerMethodField("get_generated_user_stories")
blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html")
description_html = serializers.SerializerMethodField("get_description_html")
- votes = serializers.SerializerMethodField("get_votes_number")
status_extra_info = BasicIssueStatusSerializer(source="status", required=False, read_only=True)
assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", required=False, read_only=True)
owner_extra_info = UserBasicInfoSerializer(source="owner", required=False, read_only=True)
@@ -59,10 +59,6 @@ class IssueSerializer(WatchersValidator, serializers.ModelSerializer):
def get_description_html(self, obj):
return mdrender(obj.project, obj.description)
- def get_votes_number(self, obj):
- # The "votes_count" attribute is attached in the get_queryset of the viewset.
- return getattr(obj, "votes_count", 0)
-
class IssueListSerializer(IssueSerializer):
class Meta:
diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py
index ff152e58..236bc182 100644
--- a/taiga/projects/permissions.py
+++ b/taiga/projects/permissions.py
@@ -54,19 +54,25 @@ class ProjectPermission(TaigaResourcePermission):
list_perms = AllowAny()
stats_perms = HasProjectPerm('view_project')
member_stats_perms = HasProjectPerm('view_project')
+ issues_stats_perms = HasProjectPerm('view_project')
regenerate_userstories_csv_uuid_perms = IsProjectOwner()
regenerate_issues_csv_uuid_perms = IsProjectOwner()
regenerate_tasks_csv_uuid_perms = IsProjectOwner()
- star_perms = IsAuthenticated()
- unstar_perms = IsAuthenticated()
- issues_stats_perms = HasProjectPerm('view_project')
tags_perms = HasProjectPerm('view_project')
tags_colors_perms = HasProjectPerm('view_project')
- fans_perms = HasProjectPerm('view_project')
+ star_perms = IsAuthenticated() & HasProjectPerm('view_project')
+ unstar_perms = IsAuthenticated() & HasProjectPerm('view_project')
create_template_perms = IsSuperUser()
leave_perms = CanLeaveProject()
+class ProjectFansPermission(TaigaResourcePermission):
+ enought_perms = IsProjectOwner() | IsSuperUser()
+ global_perms = None
+ retrieve_perms = HasProjectPerm('view_project')
+ list_perms = HasProjectPerm('view_project')
+
+
class MembershipPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project')
create_perms = IsProjectOwner()
diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py
index bc1de975..a7e96834 100644
--- a/taiga/projects/serializers.py
+++ b/taiga/projects/serializers.py
@@ -40,7 +40,7 @@ from .validators import ProjectExistsValidator
from .custom_attributes.serializers import UserStoryCustomAttributeSerializer
from .custom_attributes.serializers import TaskCustomAttributeSerializer
from .custom_attributes.serializers import IssueCustomAttributeSerializer
-
+from .votes.mixins.serializers import StarredResourceSerializerMixin
######################################################
## Custom values for selectors
@@ -305,11 +305,10 @@ class ProjectMemberSerializer(serializers.ModelSerializer):
## Projects
######################################################
-class ProjectSerializer(serializers.ModelSerializer):
+class ProjectSerializer(StarredResourceSerializerMixin, serializers.ModelSerializer):
tags = TagsField(default=[], required=False)
anon_permissions = PgArrayField(required=False)
public_permissions = PgArrayField(required=False)
- stars = serializers.SerializerMethodField("get_stars_number")
my_permissions = serializers.SerializerMethodField("get_my_permissions")
i_am_owner = serializers.SerializerMethodField("get_i_am_owner")
tags_colors = TagsColorsField(required=False)
@@ -321,10 +320,6 @@ class ProjectSerializer(serializers.ModelSerializer):
exclude = ("last_us_ref", "last_task_ref", "last_issue_ref",
"issues_csv_uuid", "tasks_csv_uuid", "userstories_csv_uuid")
- def get_stars_number(self, obj):
- # The "stars_count" attribute is attached in the get_queryset of the viewset.
- return getattr(obj, "stars_count", 0)
-
def get_my_permissions(self, obj):
if "request" in self.context:
return get_user_project_permissions(self.context["request"].user, obj)
diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py
index 702d0a58..e2262632 100644
--- a/taiga/projects/tasks/api.py
+++ b/taiga/projects/tasks/api.py
@@ -20,13 +20,14 @@ from taiga.base.api.utils import get_object_or_404
from taiga.base import filters, response
from taiga.base import exceptions as exc
from taiga.base.decorators import list_route
-from taiga.base.api import ModelCrudViewSet
+from taiga.base.api import ModelCrudViewSet, ModelListViewSet
from taiga.projects.models import Project, TaskStatus
from django.http import HttpResponse
from taiga.projects.notifications.mixins import WatchedResourceMixin
from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.occ import OCCResourceMixin
+from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
from . import models
@@ -35,8 +36,9 @@ from . import serializers
from . import services
-class TaskViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet):
- model = models.Task
+class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
+ ModelCrudViewSet):
+ queryset = models.Task.objects.all()
permission_classes = (permissions.TaskPermission,)
filter_backends = (filters.CanViewTasksFilterBackend,)
filter_fields = ["user_story", "milestone", "project", "assigned_to",
@@ -82,6 +84,9 @@ class TaskViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
return super().update(request, *args, **kwargs)
+ def get_queryset(self):
+ qs = super().get_queryset()
+ return self.attach_votes_attrs_to_queryset(qs)
def pre_save(self, obj):
if obj.user_story:
@@ -165,3 +170,8 @@ class TaskViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
@list_route(methods=["POST"])
def bulk_update_us_order(self, request, **kwargs):
return self._bulk_update_order("us_order", request, **kwargs)
+
+
+class TaskVotersViewSet(VotersViewSetMixin, ModelListViewSet):
+ permission_classes = (permissions.TaskVotersPermission,)
+ resource_model = models.Task
diff --git a/taiga/projects/tasks/permissions.py b/taiga/projects/tasks/permissions.py
index 2c1fd7b0..af5fbd49 100644
--- a/taiga/projects/tasks/permissions.py
+++ b/taiga/projects/tasks/permissions.py
@@ -15,7 +15,8 @@
# along with this program. If not, see .
from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
- IsProjectOwner, AllowAny, IsSuperUser)
+ IsAuthenticated, IsProjectOwner, AllowAny,
+ IsSuperUser)
class TaskPermission(TaigaResourcePermission):
@@ -30,3 +31,12 @@ class TaskPermission(TaigaResourcePermission):
csv_perms = AllowAny()
bulk_create_perms = HasProjectPerm('add_task')
bulk_update_order_perms = HasProjectPerm('modify_task')
+ upvote_perms = IsAuthenticated() & HasProjectPerm('view_tasks')
+ downvote_perms = IsAuthenticated() & HasProjectPerm('view_tasks')
+
+
+class TaskVotersPermission(TaigaResourcePermission):
+ enought_perms = IsProjectOwner() | IsSuperUser()
+ global_perms = None
+ retrieve_perms = HasProjectPerm('view_tasks')
+ list_perms = HasProjectPerm('view_tasks')
diff --git a/taiga/projects/tasks/serializers.py b/taiga/projects/tasks/serializers.py
index 6f64e2d5..3df25a77 100644
--- a/taiga/projects/tasks/serializers.py
+++ b/taiga/projects/tasks/serializers.py
@@ -27,12 +27,14 @@ from taiga.projects.milestones.validators import SprintExistsValidator
from taiga.projects.tasks.validators import TaskExistsValidator
from taiga.projects.notifications.validators import WatchersValidator
from taiga.projects.serializers import BasicTaskStatusSerializerSerializer
+from taiga.projects.votes.mixins.serializers import VotedResourceSerializerMixin
+
from taiga.users.serializers import UserBasicInfoSerializer
from . import models
-class TaskSerializer(WatchersValidator, serializers.ModelSerializer):
+class TaskSerializer(WatchersValidator, VotedResourceSerializerMixin, serializers.ModelSerializer):
tags = TagsField(required=False, default=[])
external_reference = PgArrayField(required=False)
comment = serializers.SerializerMethodField("get_comment")
diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py
index b364a53c..8ae11d53 100644
--- a/taiga/projects/userstories/api.py
+++ b/taiga/projects/userstories/api.py
@@ -28,15 +28,15 @@ from taiga.base import exceptions as exc
from taiga.base import response
from taiga.base import status
from taiga.base.decorators import list_route
-from taiga.base.api import ModelCrudViewSet
+from taiga.base.api import ModelCrudViewSet, ModelListViewSet
from taiga.base.api.utils import get_object_or_404
from taiga.projects.notifications.mixins import WatchedResourceMixin
from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.occ import OCCResourceMixin
-
from taiga.projects.models import Project, UserStoryStatus
from taiga.projects.history.services import take_snapshot
+from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
from . import models
from . import permissions
@@ -44,8 +44,9 @@ from . import serializers
from . import services
-class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet):
- model = models.UserStory
+class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
+ ModelCrudViewSet):
+ queryset = models.UserStory.objects.all()
permission_classes = (permissions.UserStoryPermission,)
filter_backends = (filters.CanViewUsFilterBackend,
filters.OwnersFilter,
@@ -109,13 +110,13 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi
def get_queryset(self):
- qs = self.model.objects.all()
+ qs = super().get_queryset()
qs = qs.prefetch_related("role_points",
"role_points__points",
"role_points__role",
"watchers")
qs = qs.select_related("milestone", "project")
- return qs
+ return self.attach_votes_attrs_to_queryset(qs)
def pre_save(self, obj):
# This is very ugly hack, but having
@@ -264,3 +265,7 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi
self.send_notifications(self.object.generated_from_issue, history)
return response
+
+class UserStoryVotersViewSet(VotersViewSetMixin, ModelListViewSet):
+ permission_classes = (permissions.UserStoryVotersPermission,)
+ resource_model = models.UserStory
diff --git a/taiga/projects/userstories/permissions.py b/taiga/projects/userstories/permissions.py
index 3aa548db..00659141 100644
--- a/taiga/projects/userstories/permissions.py
+++ b/taiga/projects/userstories/permissions.py
@@ -30,3 +30,12 @@ class UserStoryPermission(TaigaResourcePermission):
csv_perms = AllowAny()
bulk_create_perms = IsAuthenticated() & (HasProjectPerm('add_us_to_project') | HasProjectPerm('add_us'))
bulk_update_order_perms = HasProjectPerm('modify_us')
+ upvote_perms = IsAuthenticated() & HasProjectPerm('view_us')
+ downvote_perms = IsAuthenticated() & HasProjectPerm('view_us')
+
+
+class UserStoryVotersPermission(TaigaResourcePermission):
+ enought_perms = IsProjectOwner() | IsSuperUser()
+ global_perms = None
+ retrieve_perms = HasProjectPerm('view_us')
+ list_perms = HasProjectPerm('view_us')
diff --git a/taiga/projects/userstories/serializers.py b/taiga/projects/userstories/serializers.py
index a23cd63c..6c3a76cd 100644
--- a/taiga/projects/userstories/serializers.py
+++ b/taiga/projects/userstories/serializers.py
@@ -27,6 +27,8 @@ from taiga.projects.validators import UserStoryStatusExistsValidator
from taiga.projects.userstories.validators import UserStoryExistsValidator
from taiga.projects.notifications.validators import WatchersValidator
from taiga.projects.serializers import BasicUserStoryStatusSerializer
+from taiga.projects.votes.mixins.serializers import VotedResourceSerializerMixin
+
from taiga.users.serializers import UserBasicInfoSerializer
from . import models
@@ -42,7 +44,7 @@ class RolePointsField(serializers.WritableField):
return json.loads(obj)
-class UserStorySerializer(WatchersValidator, serializers.ModelSerializer):
+class UserStorySerializer(WatchersValidator, VotedResourceSerializerMixin, serializers.ModelSerializer):
tags = TagsField(default=[], required=False)
external_reference = PgArrayField(required=False)
points = RolePointsField(source="role_points", required=False)
diff --git a/taiga/projects/votes/migrations/0002_auto_20150805_1600.py b/taiga/projects/votes/migrations/0002_auto_20150805_1600.py
new file mode 100644
index 00000000..c57f526c
--- /dev/null
+++ b/taiga/projects/votes/migrations/0002_auto_20150805_1600.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+from django.utils.timezone import utc
+from django.conf import settings
+import datetime
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('votes', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='vote',
+ name='created_date',
+ field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2015, 8, 5, 16, 0, 40, 158374, tzinfo=utc), verbose_name='created date'),
+ preserve_default=False,
+ ),
+ migrations.AlterField(
+ model_name='vote',
+ name='user',
+ field=models.ForeignKey(related_name='votes', to=settings.AUTH_USER_MODEL, verbose_name='user'),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='votes',
+ name='count',
+ field=models.PositiveIntegerField(default=0, verbose_name='count'),
+ preserve_default=True,
+ ),
+ ]
diff --git a/taiga/projects/votes/mixins/__init__.py b/taiga/projects/votes/mixins/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/taiga/projects/votes/mixins/serializers.py b/taiga/projects/votes/mixins/serializers.py
new file mode 100644
index 00000000..96028eaf
--- /dev/null
+++ b/taiga/projects/votes/mixins/serializers.py
@@ -0,0 +1,37 @@
+# Copyright (C) 2015 Andrey Antukh
+# Copyright (C) 2015 Jesús Espino
+# Copyright (C) 2015 David Barragán
+# 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 .
+
+from taiga.base.api import serializers
+
+
+class BaseVotedResourceSerializer(serializers.ModelSerializer):
+ def get_votes_counter(self, obj):
+ # The "votes_count" attribute is attached in the get_queryset of the viewset.
+ return getattr(obj, "votes_count", 0) or 0
+
+ def get_is_voted(self, obj):
+ # The "is_voted" attribute is attached in the get_queryset of the viewset.
+ return getattr(obj, "is_voted", False) or False
+
+
+class StarredResourceSerializerMixin(BaseVotedResourceSerializer):
+ stars = serializers.SerializerMethodField("get_votes_counter")
+ is_starred = serializers.SerializerMethodField("get_is_voted")
+
+
+class VotedResourceSerializerMixin(BaseVotedResourceSerializer):
+ votes = serializers.SerializerMethodField("get_votes_counter")
+ is_voted = serializers.SerializerMethodField("get_is_voted")
diff --git a/taiga/projects/votes/mixins/viewsets.py b/taiga/projects/votes/mixins/viewsets.py
new file mode 100644
index 00000000..83bf25bd
--- /dev/null
+++ b/taiga/projects/votes/mixins/viewsets.py
@@ -0,0 +1,114 @@
+# Copyright (C) 2015 Andrey Antukh
+# Copyright (C) 2015 Jesús Espino
+# Copyright (C) 2015 David Barragán
+# 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 .
+
+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.votes import serializers
+from taiga.projects.votes import services
+from taiga.projects.votes.utils import attach_votes_count_to_queryset, attach_is_vote_to_queryset
+
+
+class BaseVotedResource:
+ # Note: Update get_queryset method:
+ # def get_queryset(self):
+ # qs = super().get_queryset()
+ # return self.attach_votes_attrs_to_queryset(qs)
+
+ def attach_votes_attrs_to_queryset(self, queryset):
+ qs = attach_votes_count_to_queryset(queryset)
+
+ if self.request.user.is_authenticated():
+ qs = attach_is_vote_to_queryset(self.request.user, qs)
+
+ return qs
+
+ def _add_voter(self, permission, request, pk=None):
+ obj = self.get_object()
+ self.check_permissions(request, permission, obj)
+
+ services.add_vote(obj, user=request.user)
+ return response.Ok()
+
+ def _remove_vote(self, permission, request, pk=None):
+ obj = self.get_object()
+ self.check_permissions(request, permission, obj)
+
+ services.remove_vote(obj, user=request.user)
+ return response.Ok()
+
+
+class StarredResourceMixin(BaseVotedResource):
+ # Note: objects nedd 'star' and 'unstar' permissions.
+
+ @detail_route(methods=["POST"])
+ def star(self, request, pk=None):
+ return self._add_voter("star", request, pk)
+
+ @detail_route(methods=["POST"])
+ def unstar(self, request, pk=None):
+ return self._remove_vote("unstar", 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 VotersViewSetMixin:
+ # Is a ModelListViewSet with two required params: permission_classes and resource_model
+ serializer_class = serializers.VoterSerializer
+ list_serializer_class = serializers.VoterSerializer
+ 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_voters(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_voters(resource)
diff --git a/taiga/projects/votes/models.py b/taiga/projects/votes/models.py
index 5457c3ac..7fcf857a 100644
--- a/taiga/projects/votes/models.py
+++ b/taiga/projects/votes/models.py
@@ -16,16 +16,16 @@
# along with this program. If not, see .
from django.conf import settings
+from django.contrib.contenttypes import generic
from django.db import models
from django.utils.translation import ugettext_lazy as _
-from django.contrib.contenttypes import generic
class Votes(models.Model):
content_type = models.ForeignKey("contenttypes.ContentType")
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey("content_type", "object_id")
- count = models.PositiveIntegerField(default=0)
+ count = models.PositiveIntegerField(null=False, blank=False, default=0, verbose_name=_("count"))
class Meta:
verbose_name = _("Votes")
@@ -44,10 +44,12 @@ class Votes(models.Model):
class Vote(models.Model):
content_type = models.ForeignKey("contenttypes.ContentType")
- object_id = models.PositiveIntegerField(null=False)
+ 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="votes", verbose_name=_("votes"))
+ related_name="votes", verbose_name=_("user"))
+ created_date = models.DateTimeField(null=False, blank=False, auto_now_add=True,
+ verbose_name=_("created date"))
class Meta:
verbose_name = _("Vote")
diff --git a/taiga/projects/votes/utils.py b/taiga/projects/votes/utils.py
index 20e72b6d..bff72a6a 100644
--- a/taiga/projects/votes/utils.py
+++ b/taiga/projects/votes/utils.py
@@ -18,7 +18,7 @@
from django.apps import apps
-def attach_votescount_to_queryset(queryset, as_field="votes_count"):
+def attach_votes_count_to_queryset(queryset, as_field="votes_count"):
"""Attach votes count to each object of the queryset.
Because of laziness of vote objects creation, this makes much simpler and more efficient to
@@ -34,8 +34,40 @@ def attach_votescount_to_queryset(queryset, as_field="votes_count"):
"""
model = queryset.model
type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model)
- sql = ("SELECT coalesce(votes_votes.count, 0) FROM votes_votes "
- "WHERE votes_votes.content_type_id = {type_id} AND votes_votes.object_id = {tbl}.id")
+ sql = ("""SELECT coalesce(votes_votes.count, 0)
+ FROM votes_votes
+ WHERE votes_votes.content_type_id = {type_id}
+ AND votes_votes.object_id = {tbl}.id""")
sql = sql.format(type_id=type.id, tbl=model._meta.db_table)
qs = queryset.extra(select={as_field: sql})
return qs
+
+
+def attach_is_vote_to_queryset(user, queryset, as_field="is_voted"):
+ """Attach is_vote boolean to each object of the queryset.
+
+ Because of laziness of vote objects creation, this makes much simpler and more efficient to
+ access to votes-object and check if the curren user vote 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 votes_vote
+ WHERE votes_vote.content_type_id = {type_id}
+ AND votes_vote.object_id = {tbl}.id
+ AND votes_vote.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
diff --git a/taiga/routers.py b/taiga/routers.py
index 0f8bc675..ff7ceff0 100644
--- a/taiga/routers.py
+++ b/taiga/routers.py
@@ -1,4 +1,3 @@
-
# Copyright (C) 2014 Andrey Antukh
# Copyright (C) 2014 Jesús Espino
# Copyright (C) 2014 David Barragán
@@ -49,6 +48,7 @@ router.register(r"notify-policies", NotifyPolicyViewSet, base_name="notification
# Projects & Selectors
from taiga.projects.api import ProjectViewSet
+from taiga.projects.api import ProjectFansViewSet
from taiga.projects.api import MembershipViewSet
from taiga.projects.api import InvitationViewSet
from taiga.projects.api import UserStoryStatusViewSet
@@ -61,6 +61,7 @@ from taiga.projects.api import SeverityViewSet
from taiga.projects.api import ProjectTemplateViewSet
router.register(r"projects", ProjectViewSet, base_name="projects")
+router.register(r"projects/(?P\d+)/fans", ProjectFansViewSet, base_name="project-fans")
router.register(r"project-templates", ProjectTemplateViewSet, base_name="project-templates")
router.register(r"memberships", MembershipViewSet, base_name="memberships")
router.register(r"invitations", InvitationViewSet, base_name="invitations")
@@ -124,20 +125,26 @@ router.register(r"wiki/attachments", WikiAttachmentViewSet, base_name="wiki-atta
# Project components
from taiga.projects.milestones.api import MilestoneViewSet
from taiga.projects.userstories.api import UserStoryViewSet
+from taiga.projects.userstories.api import UserStoryVotersViewSet
from taiga.projects.tasks.api import TaskViewSet
+from taiga.projects.tasks.api import TaskVotersViewSet
from taiga.projects.issues.api import IssueViewSet
-from taiga.projects.issues.api import VotersViewSet
+from taiga.projects.issues.api import IssueVotersViewSet
from taiga.projects.wiki.api import WikiViewSet, WikiLinkViewSet
router.register(r"milestones", MilestoneViewSet, base_name="milestones")
router.register(r"userstories", UserStoryViewSet, base_name="userstories")
+router.register(r"userstories/(?P\d+)/voters", UserStoryVotersViewSet, base_name="userstory-voters")
router.register(r"tasks", TaskViewSet, base_name="tasks")
+router.register(r"tasks/(?P\d+)/voters", TaskVotersViewSet, base_name="task-voters")
router.register(r"issues", IssueViewSet, base_name="issues")
-router.register(r"issues/(?P\d+)/voters", VotersViewSet, base_name="issue-voters")
+router.register(r"issues/(?P\d+)/voters", IssueVotersViewSet, base_name="issue-voters")
router.register(r"wiki", WikiViewSet, base_name="wiki")
router.register(r"wiki-links", WikiLinkViewSet, base_name="wiki-links")
+
+
# History & Components
from taiga.projects.history.api import UserStoryHistory
from taiga.projects.history.api import TaskHistory
diff --git a/taiga/users/api.py b/taiga/users/api.py
index 33ec137c..0cd8f50c 100644
--- a/taiga/users/api.py
+++ b/taiga/users/api.py
@@ -224,15 +224,6 @@ class UsersViewSet(ModelCrudViewSet):
user_data = self.admin_serializer_class(request.user).data
return response.Ok(user_data)
- @detail_route(methods=["GET"])
- def starred(self, request, pk=None):
- user = self.get_object()
- self.check_permissions(request, 'starred', user)
-
- stars = votes_service.get_voted(user.pk, model=apps.get_model('projects', 'Project'))
- stars_data = StarredSerializer(stars, many=True)
- return response.Ok(stars_data.data)
-
#TODO: commit_on_success
def partial_update(self, request, *args, **kwargs):
"""
diff --git a/tests/integration/resources_permissions/test_issues_resources.py b/tests/integration/resources_permissions/test_issues_resources.py
index 43abdeac..f5a90e66 100644
--- a/tests/integration/resources_permissions/test_issues_resources.py
+++ b/tests/integration/resources_permissions/test_issues_resources.py
@@ -477,12 +477,10 @@ def test_issue_action_upvote(client, data):
results = helper_test_http_method(client, 'post', public_url, "", users)
assert results == [401, 200, 200, 200, 200]
-
results = helper_test_http_method(client, 'post', private_url1, "", users)
assert results == [401, 200, 200, 200, 200]
-
results = helper_test_http_method(client, 'post', private_url2, "", users)
- assert results == [401, 403, 403, 200, 200]
+ assert results == [404, 404, 404, 200, 200]
def test_issue_action_downvote(client, data):
@@ -500,18 +498,16 @@ def test_issue_action_downvote(client, data):
results = helper_test_http_method(client, 'post', public_url, "", users)
assert results == [401, 200, 200, 200, 200]
-
results = helper_test_http_method(client, 'post', private_url1, "", users)
assert results == [401, 200, 200, 200, 200]
-
results = helper_test_http_method(client, 'post', private_url2, "", users)
- assert results == [401, 403, 403, 200, 200]
+ assert results == [404, 404, 404, 200, 200]
def test_issue_voters_list(client, data):
- public_url = reverse('issue-voters-list', kwargs={"issue_id": data.public_issue.pk})
- private_url1 = reverse('issue-voters-list', kwargs={"issue_id": data.private_issue1.pk})
- private_url2 = reverse('issue-voters-list', kwargs={"issue_id": data.private_issue2.pk})
+ public_url = reverse('issue-voters-list', kwargs={"resource_id": data.public_issue.pk})
+ private_url1 = reverse('issue-voters-list', kwargs={"resource_id": data.private_issue1.pk})
+ private_url2 = reverse('issue-voters-list', kwargs={"resource_id": data.private_issue2.pk})
users = [
None,
@@ -523,21 +519,22 @@ def test_issue_voters_list(client, data):
results = helper_test_http_method(client, 'get', public_url, None, users)
assert results == [200, 200, 200, 200, 200]
-
results = helper_test_http_method(client, 'get', private_url1, None, users)
assert results == [200, 200, 200, 200, 200]
-
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]
def test_issue_voters_retrieve(client, data):
add_vote(data.public_issue, data.project_owner)
- public_url = reverse('issue-voters-detail', kwargs={"issue_id": data.public_issue.pk, "pk": data.project_owner.pk})
+ public_url = reverse('issue-voters-detail', kwargs={"resource_id": data.public_issue.pk,
+ "pk": data.project_owner.pk})
add_vote(data.private_issue1, data.project_owner)
- private_url1 = reverse('issue-voters-detail', kwargs={"issue_id": data.private_issue1.pk, "pk": data.project_owner.pk})
+ private_url1 = reverse('issue-voters-detail', kwargs={"resource_id": data.private_issue1.pk,
+ "pk": data.project_owner.pk})
add_vote(data.private_issue2, data.project_owner)
- private_url2 = reverse('issue-voters-detail', kwargs={"issue_id": data.private_issue2.pk, "pk": data.project_owner.pk})
+ private_url2 = reverse('issue-voters-detail', kwargs={"resource_id": data.private_issue2.pk,
+ "pk": data.project_owner.pk})
users = [
None,
@@ -549,10 +546,8 @@ def test_issue_voters_retrieve(client, data):
results = helper_test_http_method(client, 'get', public_url, None, users)
assert results == [200, 200, 200, 200, 200]
-
results = helper_test_http_method(client, 'get', private_url1, None, users)
assert results == [200, 200, 200, 200, 200]
-
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]
diff --git a/tests/integration/resources_permissions/test_projects_resource.py b/tests/integration/resources_permissions/test_projects_resource.py
index 575e0d62..e485f497 100644
--- a/tests/integration/resources_permissions/test_projects_resource.py
+++ b/tests/integration/resources_permissions/test_projects_resource.py
@@ -198,6 +198,25 @@ def test_project_action_stats(client, data):
assert results == [404, 404, 200, 200]
+def test_project_action_issues_stats(client, data):
+ public_url = reverse('projects-issues-stats', kwargs={"pk": data.public_project.pk})
+ private1_url = reverse('projects-issues-stats', kwargs={"pk": data.private_project1.pk})
+ private2_url = reverse('projects-issues-stats', kwargs={"pk": data.private_project2.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+ results = helper_test_http_method(client, 'get', public_url, None, users)
+ assert results == [200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private1_url, None, users)
+ assert results == [200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private2_url, None, users)
+ assert results == [404, 404, 200, 200]
+
+
def test_project_action_star(client, data):
public_url = reverse('projects-star', kwargs={"pk": data.public_project.pk})
private1_url = reverse('projects-star', kwargs={"pk": data.private_project1.pk})
@@ -236,29 +255,10 @@ def test_project_action_unstar(client, data):
assert results == [404, 404, 200, 200]
-def test_project_action_issues_stats(client, data):
- public_url = reverse('projects-issues-stats', kwargs={"pk": data.public_project.pk})
- private1_url = reverse('projects-issues-stats', kwargs={"pk": data.private_project1.pk})
- private2_url = reverse('projects-issues-stats', kwargs={"pk": data.private_project2.pk})
-
- users = [
- None,
- data.registered_user,
- data.project_member_with_perms,
- data.project_owner
- ]
- results = helper_test_http_method(client, 'get', public_url, None, users)
- assert results == [200, 200, 200, 200]
- results = helper_test_http_method(client, 'get', private1_url, None, users)
- assert results == [200, 200, 200, 200]
- results = helper_test_http_method(client, 'get', private2_url, None, users)
- assert results == [404, 404, 200, 200]
-
-
-def test_project_action_fans(client, data):
- public_url = reverse('projects-fans', kwargs={"pk": data.public_project.pk})
- private1_url = reverse('projects-fans', kwargs={"pk": data.private_project1.pk})
- private2_url = reverse('projects-fans', kwargs={"pk": data.private_project2.pk})
+def test_project_fans_list(client, data):
+ public_url = reverse('project-fans-list', kwargs={"resource_id": data.public_project.pk})
+ private1_url = reverse('project-fans-list', kwargs={"resource_id": data.private_project1.pk})
+ private2_url = reverse('project-fans-list', kwargs={"resource_id": data.private_project2.pk})
users = [
None,
@@ -273,13 +273,16 @@ def test_project_action_fans(client, data):
results = helper_test_http_method_and_count(client, 'get', private1_url, None, users)
assert results == [(200, 2), (200, 2), (200, 2), (200, 2), (200, 2)]
results = helper_test_http_method_and_count(client, 'get', private2_url, None, users)
- assert results == [(404, 1), (404, 1), (404, 1), (200, 2), (200, 2)]
+ assert results == [(401, 0), (403, 0), (403, 0), (200, 2), (200, 2)]
-def test_user_action_starred(client, data):
- url1 = reverse('users-starred', kwargs={"pk": data.project_member_without_perms.pk})
- url2 = reverse('users-starred', kwargs={"pk": data.project_member_with_perms.pk})
- url3 = reverse('users-starred', kwargs={"pk": data.project_owner.pk})
+def test_project_fans_retrieve(client, data):
+ public_url = reverse('project-fans-detail', kwargs={"resource_id": data.public_project.pk,
+ "pk": data.project_owner.pk})
+ private1_url = reverse('project-fans-detail', kwargs={"resource_id": data.private_project1.pk,
+ "pk": data.project_owner.pk})
+ private2_url = reverse('project-fans-detail', kwargs={"resource_id": data.private_project2.pk,
+ "pk": data.project_owner.pk})
users = [
None,
@@ -289,12 +292,12 @@ def test_user_action_starred(client, data):
data.project_owner
]
- results = helper_test_http_method_and_count(client, 'get', url1, None, users)
- assert results == [(200, 0), (200, 0), (200, 0), (200, 0), (200, 0)]
- results = helper_test_http_method_and_count(client, 'get', url2, None, users)
- assert results == [(200, 3), (200, 3), (200, 3), (200, 3), (200, 3)]
- results = helper_test_http_method_and_count(client, 'get', url3, None, users)
- assert results == [(200, 3), (200, 3), (200, 3), (200, 3), (200, 3)]
+ results = helper_test_http_method(client, 'get', public_url, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private1_url, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private2_url, None, users)
+ assert results == [401, 403, 403, 200, 200]
def test_project_action_create_template(client, data):
diff --git a/tests/integration/resources_permissions/test_tasks_resources.py b/tests/integration/resources_permissions/test_tasks_resources.py
index 9be43c09..b5c3c4f0 100644
--- a/tests/integration/resources_permissions/test_tasks_resources.py
+++ b/tests/integration/resources_permissions/test_tasks_resources.py
@@ -9,6 +9,7 @@ from taiga.projects.occ import OCCResourceMixin
from tests import factories as f
from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals
+from taiga.projects.votes.services import add_vote
from unittest import mock
@@ -416,6 +417,96 @@ def test_task_action_bulk_create(client, data):
assert results == [401, 403, 403, 200, 200]
+def test_task_action_upvote(client, data):
+ public_url = reverse('tasks-upvote', kwargs={"pk": data.public_task.pk})
+ private_url1 = reverse('tasks-upvote', kwargs={"pk": data.private_task1.pk})
+ private_url2 = reverse('tasks-upvote', kwargs={"pk": data.private_task2.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'post', public_url, "", users)
+ assert results == [401, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'post', private_url1, "", users)
+ assert results == [401, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'post', private_url2, "", users)
+ assert results == [404, 404, 404, 200, 200]
+
+
+def test_task_action_downvote(client, data):
+ public_url = reverse('tasks-downvote', kwargs={"pk": data.public_task.pk})
+ private_url1 = reverse('tasks-downvote', kwargs={"pk": data.private_task1.pk})
+ private_url2 = reverse('tasks-downvote', kwargs={"pk": data.private_task2.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'post', public_url, "", users)
+ assert results == [401, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'post', private_url1, "", users)
+ assert results == [401, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'post', private_url2, "", users)
+ assert results == [404, 404, 404, 200, 200]
+
+
+def test_task_voters_list(client, data):
+ public_url = reverse('task-voters-list', kwargs={"resource_id": data.public_task.pk})
+ private_url1 = reverse('task-voters-list', kwargs={"resource_id": data.private_task1.pk})
+ private_url2 = reverse('task-voters-list', kwargs={"resource_id": data.private_task2.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'get', public_url, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url1, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url2, None, users)
+ assert results == [401, 403, 403, 200, 200]
+
+
+def test_task_voters_retrieve(client, data):
+ add_vote(data.public_task, data.project_owner)
+ public_url = reverse('task-voters-detail', kwargs={"resource_id": data.public_task.pk,
+ "pk": data.project_owner.pk})
+ add_vote(data.private_task1, data.project_owner)
+ private_url1 = reverse('task-voters-detail', kwargs={"resource_id": data.private_task1.pk,
+ "pk": data.project_owner.pk})
+ add_vote(data.private_task2, data.project_owner)
+ private_url2 = reverse('task-voters-detail', kwargs={"resource_id": data.private_task2.pk,
+ "pk": data.project_owner.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'get', public_url, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url1, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url2, None, users)
+ assert results == [401, 403, 403, 200, 200]
+
+
def test_tasks_csv(client, data):
url = reverse('tasks-csv')
csv_public_uuid = data.public_project.tasks_csv_uuid
diff --git a/tests/integration/resources_permissions/test_userstories_resources.py b/tests/integration/resources_permissions/test_userstories_resources.py
index f41d84ee..c257244b 100644
--- a/tests/integration/resources_permissions/test_userstories_resources.py
+++ b/tests/integration/resources_permissions/test_userstories_resources.py
@@ -9,6 +9,7 @@ from taiga.projects.occ import OCCResourceMixin
from tests import factories as f
from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals
+from taiga.projects.votes.services import add_vote
from unittest import mock
@@ -415,6 +416,95 @@ def test_user_story_action_bulk_update_order(client, data):
results = helper_test_http_method(client, 'post', url, post_data, users)
assert results == [401, 403, 403, 204, 204]
+def test_user_story_action_upvote(client, data):
+ public_url = reverse('userstories-upvote', kwargs={"pk": data.public_user_story.pk})
+ private_url1 = reverse('userstories-upvote', kwargs={"pk": data.private_user_story1.pk})
+ private_url2 = reverse('userstories-upvote', kwargs={"pk": data.private_user_story2.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'post', public_url, "", users)
+ assert results == [401, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'post', private_url1, "", users)
+ assert results == [401, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'post', private_url2, "", users)
+ assert results == [404, 404, 404, 200, 200]
+
+
+def test_user_story_action_downvote(client, data):
+ public_url = reverse('userstories-downvote', kwargs={"pk": data.public_user_story.pk})
+ private_url1 = reverse('userstories-downvote', kwargs={"pk": data.private_user_story1.pk})
+ private_url2 = reverse('userstories-downvote', kwargs={"pk": data.private_user_story2.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'post', public_url, "", users)
+ assert results == [401, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'post', private_url1, "", users)
+ assert results == [401, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'post', private_url2, "", users)
+ assert results == [404, 404, 404, 200, 200]
+
+
+def test_user_story_voters_list(client, data):
+ public_url = reverse('userstory-voters-list', kwargs={"resource_id": data.public_user_story.pk})
+ private_url1 = reverse('userstory-voters-list', kwargs={"resource_id": data.private_user_story1.pk})
+ private_url2 = reverse('userstory-voters-list', kwargs={"resource_id": data.private_user_story2.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'get', public_url, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url1, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url2, None, users)
+ assert results == [401, 403, 403, 200, 200]
+
+
+def test_user_story_voters_retrieve(client, data):
+ add_vote(data.public_user_story, data.project_owner)
+ public_url = reverse('userstory-voters-detail', kwargs={"resource_id": data.public_user_story.pk,
+ "pk": data.project_owner.pk})
+ add_vote(data.private_user_story1, data.project_owner)
+ private_url1 = reverse('userstory-voters-detail', kwargs={"resource_id": data.private_user_story1.pk,
+ "pk": data.project_owner.pk})
+ add_vote(data.private_user_story2, data.project_owner)
+ private_url2 = reverse('userstory-voters-detail', kwargs={"resource_id": data.private_user_story2.pk,
+ "pk": data.project_owner.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'get', public_url, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url1, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url2, None, users)
+ assert results == [401, 403, 403, 200, 200]
+
def test_user_stories_csv(client, data):
url = reverse('userstories-csv')
diff --git a/tests/integration/test_star_projects.py b/tests/integration/test_star_projects.py
new file mode 100644
index 00000000..ad8d8e08
--- /dev/null
+++ b/tests/integration/test_star_projects.py
@@ -0,0 +1,123 @@
+# Copyright (C) 2015 Andrey Antukh
+# Copyright (C) 2015 Jesús Espino
+# Copyright (C) 2015 David Barragán
+# Copyright (C) 2015 Anler Hernández
+# 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 .
+
+import pytest
+from django.core.urlresolvers import reverse
+
+from .. import factories as f
+
+pytestmark = pytest.mark.django_db
+
+
+def test_star_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-star", args=(project.id,))
+
+ client.login(user)
+ response = client.post(url)
+
+ assert response.status_code == 200
+
+
+def test_unstar_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-unstar", 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.VoteFactory.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)
+ vote = f.VoteFactory.create(content_object=project, user=user)
+ url = reverse("project-fans-detail", args=(project.id, vote.user.id))
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data['id'] == vote.user.id
+
+
+def test_get_project_stars(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.VotesFactory.create(content_object=project, count=5)
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data['stars'] == 5
+
+
+def test_get_project_is_starred(client):
+ user = f.UserFactory.create()
+ project = f.create_project(owner=user)
+ f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.VotesFactory.create(content_object=project)
+ url_detail = reverse("projects-detail", args=(project.id,))
+ url_star = reverse("projects-star", args=(project.id,))
+ url_unstar = reverse("projects-unstar", args=(project.id,))
+
+ client.login(user)
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['stars'] == 0
+ assert response.data['is_starred'] == False
+
+ response = client.post(url_star)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['stars'] == 1
+ assert response.data['is_starred'] == True
+
+ response = client.post(url_unstar)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['stars'] == 0
+ assert response.data['is_starred'] == False
diff --git a/tests/integration/test_stars.py b/tests/integration/test_stars.py
deleted file mode 100644
index eddb03a5..00000000
--- a/tests/integration/test_stars.py
+++ /dev/null
@@ -1,115 +0,0 @@
-# Copyright (C) 2014 Andrey Antukh
-# Copyright (C) 2014 Jesús Espino
-# Copyright (C) 2014 David Barragán
-# Copyright (C) 2014 Anler Hernández
-# 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 .
-
-import pytest
-from django.core.urlresolvers import reverse
-
-from .. import factories as f
-
-pytestmark = pytest.mark.django_db
-
-
-def test_project_owner_star_project(client):
- user = f.UserFactory.create()
- project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory.create(project=project, is_owner=True, user=user)
- url = reverse("projects-star", args=(project.id,))
-
- client.login(user)
- response = client.post(url)
-
- assert response.status_code == 200
-
-
-def test_project_owner_unstar_project(client):
- user = f.UserFactory.create()
- project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory.create(project=project, is_owner=True, user=user)
- url = reverse("projects-unstar", args=(project.id,))
-
- client.login(user)
- response = client.post(url)
-
- assert response.status_code == 200
-
-
-def test_project_member_star_project(client):
- user = f.UserFactory.create()
- project = f.ProjectFactory.create()
- role = f.RoleFactory.create(project=project, permissions=["view_project"])
- f.MembershipFactory.create(project=project, user=user, role=role)
- url = reverse("projects-star", args=(project.id,))
-
- client.login(user)
- response = client.post(url)
-
- assert response.status_code == 200
-
-
-def test_project_member_unstar_project(client):
- user = f.UserFactory.create()
- project = f.ProjectFactory.create()
- role = f.RoleFactory.create(project=project, permissions=["view_project"])
- f.MembershipFactory.create(project=project, user=user, role=role)
- url = reverse("projects-unstar", 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.ProjectFactory.create(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
- fan = f.VoteFactory.create(content_object=project)
- url = reverse("projects-fans", args=(project.id,))
-
- client.login(user)
- response = client.get(url)
-
- assert response.status_code == 200
- assert response.data[0]['id'] == fan.user.id
-
-
-def test_list_user_starred_projects(client):
- user = f.UserFactory.create()
- project = f.ProjectFactory()
- url = reverse("users-starred", args=(user.id,))
- f.VoteFactory.create(user=user, content_object=project)
-
- client.login(user)
- response = client.get(url)
-
- assert response.status_code == 200
- assert response.data[0]['id'] == project.id
-
-
-def test_get_project_stars(client):
- user = f.UserFactory.create()
- project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
- url = reverse("projects-detail", args=(project.id,))
- f.VotesFactory.create(content_object=project, count=5)
- f.VotesFactory.create(count=3)
-
- client.login(user)
- response = client.get(url)
-
- assert response.status_code == 200
- assert response.data['stars'] == 5
diff --git a/tests/integration/test_vote_issues.py b/tests/integration/test_vote_issues.py
index 691ce432..8faca67b 100644
--- a/tests/integration/test_vote_issues.py
+++ b/tests/integration/test_vote_issues.py
@@ -1,7 +1,7 @@
-# Copyright (C) 2014 Andrey Antukh
-# Copyright (C) 2014 Jesús Espino
-# Copyright (C) 2014 David Barragán
-# Copyright (C) 2014 Anler Hernández
+# Copyright (C) 2015 Andrey Antukh
+# Copyright (C) 2015 Jesús Espino
+# Copyright (C) 2015 David Barragán
+# Copyright (C) 2015 Anler Hernández
# 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
@@ -51,8 +51,8 @@ def test_list_issue_voters(client):
user = f.UserFactory.create()
issue = f.create_issue(owner=user)
f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
- url = reverse("issue-voters-list", args=(issue.id,))
f.VoteFactory.create(content_object=issue, user=user)
+ url = reverse("issue-voters-list", args=(issue.id,))
client.login(user)
response = client.get(url)
@@ -60,7 +60,6 @@ def test_list_issue_voters(client):
assert response.status_code == 200
assert response.data[0]['id'] == user.id
-
def test_get_issue_voter(client):
user = f.UserFactory.create()
issue = f.create_issue(owner=user)
@@ -74,7 +73,6 @@ def test_get_issue_voter(client):
assert response.status_code == 200
assert response.data['id'] == vote.user.id
-
def test_get_issue_votes(client):
user = f.UserFactory.create()
issue = f.create_issue(owner=user)
@@ -88,3 +86,36 @@ def test_get_issue_votes(client):
assert response.status_code == 200
assert response.data['votes'] == 5
+
+
+def test_get_issue_is_voted(client):
+ user = f.UserFactory.create()
+ issue = f.create_issue(owner=user)
+ f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
+ f.VotesFactory.create(content_object=issue)
+ url_detail = reverse("issues-detail", args=(issue.id,))
+ url_upvote = reverse("issues-upvote", args=(issue.id,))
+ url_downvote = reverse("issues-downvote", args=(issue.id,))
+
+ client.login(user)
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['votes'] == 0
+ assert response.data['is_voted'] == False
+
+ response = client.post(url_upvote)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['votes'] == 1
+ assert response.data['is_voted'] == True
+
+ response = client.post(url_downvote)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['votes'] == 0
+ assert response.data['is_voted'] == False
diff --git a/tests/integration/test_vote_tasks.py b/tests/integration/test_vote_tasks.py
new file mode 100644
index 00000000..a5cdea56
--- /dev/null
+++ b/tests/integration/test_vote_tasks.py
@@ -0,0 +1,123 @@
+# Copyright (C) 2015 Andrey Antukh
+# Copyright (C) 2015 Jesús Espino
+# Copyright (C) 2015 David Barragán
+# Copyright (C) 2015 Anler Hernández
+# 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 .
+
+import pytest
+from django.core.urlresolvers import reverse
+
+from .. import factories as f
+
+pytestmark = pytest.mark.django_db
+
+
+def test_upvote_task(client):
+ user = f.UserFactory.create()
+ task = f.create_task(owner=user)
+ f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ url = reverse("tasks-upvote", args=(task.id,))
+
+ client.login(user)
+ response = client.post(url)
+
+ assert response.status_code == 200
+
+
+def test_downvote_task(client):
+ user = f.UserFactory.create()
+ task = f.create_task(owner=user)
+ f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ url = reverse("tasks-downvote", args=(task.id,))
+
+ client.login(user)
+ response = client.post(url)
+
+ assert response.status_code == 200
+
+
+def test_list_task_voters(client):
+ user = f.UserFactory.create()
+ task = f.create_task(owner=user)
+ f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ f.VoteFactory.create(content_object=task, user=user)
+ url = reverse("task-voters-list", args=(task.id,))
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data[0]['id'] == user.id
+
+
+def test_get_task_voter(client):
+ user = f.UserFactory.create()
+ task = f.create_task(owner=user)
+ f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ vote = f.VoteFactory.create(content_object=task, user=user)
+ url = reverse("task-voters-detail", args=(task.id, vote.user.id))
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data['id'] == vote.user.id
+
+
+def test_get_task_votes(client):
+ user = f.UserFactory.create()
+ task = f.create_task(owner=user)
+ f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ url = reverse("tasks-detail", args=(task.id,))
+
+ f.VotesFactory.create(content_object=task, count=5)
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data['votes'] == 5
+
+
+def test_get_task_is_voted(client):
+ user = f.UserFactory.create()
+ task = f.create_task(owner=user)
+ f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ f.VotesFactory.create(content_object=task)
+ url_detail = reverse("tasks-detail", args=(task.id,))
+ url_upvote = reverse("tasks-upvote", args=(task.id,))
+ url_downvote = reverse("tasks-downvote", args=(task.id,))
+
+ client.login(user)
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['votes'] == 0
+ assert response.data['is_voted'] == False
+
+ response = client.post(url_upvote)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['votes'] == 1
+ assert response.data['is_voted'] == True
+
+ response = client.post(url_downvote)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['votes'] == 0
+ assert response.data['is_voted'] == False
diff --git a/tests/integration/test_vote_userstories.py b/tests/integration/test_vote_userstories.py
new file mode 100644
index 00000000..ab863df3
--- /dev/null
+++ b/tests/integration/test_vote_userstories.py
@@ -0,0 +1,122 @@
+# Copyright (C) 2015 Andrey Antukh
+# Copyright (C) 2015 Jesús Espino
+# Copyright (C) 2015 David Barragán
+# Copyright (C) 2015 Anler Hernández
+# 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 .
+
+import pytest
+from django.core.urlresolvers import reverse
+
+from .. import factories as f
+
+pytestmark = pytest.mark.django_db
+
+
+def test_upvote_user_story(client):
+ user = f.UserFactory.create()
+ user_story = f.create_userstory(owner=user)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ url = reverse("userstories-upvote", args=(user_story.id,))
+
+ client.login(user)
+ response = client.post(url)
+
+ assert response.status_code == 200
+
+
+def test_downvote_user_story(client):
+ user = f.UserFactory.create()
+ user_story = f.create_userstory(owner=user)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ url = reverse("userstories-downvote", args=(user_story.id,))
+
+ client.login(user)
+ response = client.post(url)
+
+ assert response.status_code == 200
+
+
+def test_list_user_story_voters(client):
+ user = f.UserFactory.create()
+ user_story = f.create_userstory(owner=user)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ f.VoteFactory.create(content_object=user_story, user=user)
+ url = reverse("userstory-voters-list", args=(user_story.id,))
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data[0]['id'] == user.id
+
+def test_get_userstory_voter(client):
+ user = f.UserFactory.create()
+ user_story = f.create_userstory(owner=user)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ vote = f.VoteFactory.create(content_object=user_story, user=user)
+ url = reverse("userstory-voters-detail", args=(user_story.id, vote.user.id))
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data['id'] == vote.user.id
+
+
+def test_get_user_story_votes(client):
+ user = f.UserFactory.create()
+ user_story = f.create_userstory(owner=user)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ url = reverse("userstories-detail", args=(user_story.id,))
+
+ f.VotesFactory.create(content_object=user_story, count=5)
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data['votes'] == 5
+
+
+def test_get_user_story_is_voted(client):
+ user = f.UserFactory.create()
+ user_story = f.create_userstory(owner=user)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ f.VotesFactory.create(content_object=user_story)
+ url_detail = reverse("userstories-detail", args=(user_story.id,))
+ url_upvote = reverse("userstories-upvote", args=(user_story.id,))
+ url_downvote = reverse("userstories-downvote", args=(user_story.id,))
+
+ client.login(user)
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['votes'] == 0
+ assert response.data['is_voted'] == False
+
+ response = client.post(url_upvote)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['votes'] == 1
+ assert response.data['is_voted'] == True
+
+ response = client.post(url_downvote)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['votes'] == 0
+ assert response.data['is_voted'] == False
diff --git a/tests/utils.py b/tests/utils.py
index 10f1fd9f..96320972 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -16,7 +16,6 @@
# along with this program. If not, see .
from django.db.models import signals
-from taiga.base.utils import json
def signals_switch():
@@ -69,9 +68,9 @@ def helper_test_http_method(client, method, url, data, users, after_each_request
def helper_test_http_method_and_count(client, method, url, data, users, after_each_request=None):
responses = _helper_test_http_method_responses(client, method, url, data, users, after_each_request)
- return list(map(lambda r: (r.status_code, len(json.loads(r.content.decode('utf-8')))), responses))
+ return list(map(lambda r: (r.status_code, len(r.data) if isinstance(r.data, list) and 200 <= r.status_code < 300 else 0), responses))
def helper_test_http_method_and_keys(client, method, url, data, users, after_each_request=None):
responses = _helper_test_http_method_responses(client, method, url, data, users, after_each_request)
- return list(map(lambda r: (r.status_code, set(json.loads(r.content.decode('utf-8')).keys())), responses))
+ return list(map(lambda r: (r.status_code, set(r.data.keys() if isinstance(r.data, dict) and 200 <= r.status_code < 300 else [])), responses))