Improve votes module

remotes/origin/enhancement/email-actions
David Barragán Merino 2015-08-05 22:22:05 +02:00
parent cf63031844
commit 44eee5212a
32 changed files with 975 additions and 320 deletions

View File

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

View File

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

View File

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

View File

@ -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
class IssueVotersViewSet(VotersViewSetMixin, ModelListViewSet):
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)
resource_model = models.Issue

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

View File

@ -0,0 +1,37 @@
# 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>
# 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 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")

View File

@ -0,0 +1,114 @@
# 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>
# 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.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)

View File

@ -16,16 +16,16 @@
# 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 _
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")

View File

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

View File

@ -1,4 +1,3 @@
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
@ -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<resource_id>\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<resource_id>\d+)/voters", UserStoryVotersViewSet, base_name="userstory-voters")
router.register(r"tasks", TaskViewSet, base_name="tasks")
router.register(r"tasks/(?P<resource_id>\d+)/voters", TaskVotersViewSet, base_name="task-voters")
router.register(r"issues", IssueViewSet, base_name="issues")
router.register(r"issues/(?P<issue_id>\d+)/voters", VotersViewSet, base_name="issue-voters")
router.register(r"issues/(?P<resource_id>\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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -1,115 +0,0 @@
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014 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_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

View File

@ -1,7 +1,7 @@
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014 Anler Hernández <hello@anler.me>
# 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
@ -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

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

View File

@ -0,0 +1,122 @@
# 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_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

View File

@ -16,7 +16,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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))