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