diff --git a/CHANGELOG.md b/CHANGELOG.md
index 04307eb6..05daf163 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,13 +4,17 @@
## 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.
+- Now users can watch public issues, tasks and user stories.
+- Add endpoints to show the watchers list for issues, tasks and user stories.
- i18n.
- Add polish (pl) translation.
- Add portuguese (Brazil) (pt_BR) translation.
diff --git a/taiga/base/fields.py b/taiga/base/fields.py
index a1a8c56c..74b27c10 100644
--- a/taiga/base/fields.py
+++ b/taiga/base/fields.py
@@ -110,3 +110,12 @@ class TagsColorsField(serializers.WritableField):
def from_native(self, data):
return list(data.items())
+
+
+
+class WatchersField(serializers.WritableField):
+ def to_native(self, obj):
+ return obj
+
+ def from_native(self, data):
+ return data
diff --git a/taiga/base/filters.py b/taiga/base/filters.py
index cbfdbdef..e0de384c 100644
--- a/taiga/base/filters.py
+++ b/taiga/base/filters.py
@@ -18,6 +18,7 @@ from functools import reduce
import logging
from django.apps import apps
+from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.utils.translation import ugettext as _
@@ -451,6 +452,33 @@ class TagsFilter(FilterBackend):
return super().filter_queryset(request, queryset, view)
+
+class WatchersFilter(FilterBackend):
+ filter_name = 'watchers'
+
+ def __init__(self, filter_name=None):
+ if filter_name:
+ self.filter_name = filter_name
+
+ def _get_watchers_queryparams(self, params):
+ watchers = params.get(self.filter_name, None)
+ if watchers:
+ return watchers.split(",")
+
+ return None
+
+ def filter_queryset(self, request, queryset, view):
+ query_watchers = self._get_watchers_queryparams(request.QUERY_PARAMS)
+ model = queryset.model
+ if query_watchers:
+ WatchedModel = apps.get_model("notifications", "Watched")
+ watched_type = ContentType.objects.get_for_model(queryset.model)
+ watched_ids = WatchedModel.objects.filter(content_type=watched_type, user__id__in=query_watchers).values_list("object_id", flat=True)
+ queryset = queryset.filter(id__in=watched_ids)
+
+ return super().filter_queryset(request, queryset, view)
+
+
#####################################################################
# Text search filters
#####################################################################
diff --git a/taiga/export_import/serializers.py b/taiga/export_import/serializers.py
index ec0ed783..98e9d2cf 100644
--- a/taiga/export_import/serializers.py
+++ b/taiga/export_import/serializers.py
@@ -19,6 +19,7 @@ import copy
import os
from collections import OrderedDict
+from django.apps import apps
from django.core.files.base import ContentFile
from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ValidationError
@@ -43,6 +44,7 @@ from taiga.projects.attachments import models as attachments_models
from taiga.timeline import models as timeline_models
from taiga.timeline import service as timeline_service
from taiga.users import models as users_models
+from taiga.projects.notifications import services as notifications_services
from taiga.projects.votes import services as votes_service
from taiga.projects.history import services as history_service
@@ -223,6 +225,48 @@ class HistoryDiffField(JsonField):
return data
+class WatcheableObjectModelSerializer(serializers.ModelSerializer):
+ watchers = UserRelatedField(many=True, required=False)
+
+ def __init__(self, *args, **kwargs):
+ self._watchers_field = self.base_fields.pop("watchers", None)
+ super(WatcheableObjectModelSerializer, self).__init__(*args, **kwargs)
+
+ """
+ watchers is not a field from the model so we need to do some magic to make it work like a normal field
+ It's supposed to be represented as an email list but internally it's treated like notifications.Watched instances
+ """
+
+ def restore_object(self, attrs, instance=None):
+ watcher_field = self.fields.pop("watchers", None)
+ instance = super(WatcheableObjectModelSerializer, self).restore_object(attrs, instance)
+ self._watchers = self.init_data.get("watchers", [])
+ return instance
+
+ def save_watchers(self):
+ new_watcher_emails = set(self._watchers)
+ old_watcher_emails = set(notifications_services.get_watchers(self.object).values_list("email", flat=True))
+ adding_watcher_emails = list(new_watcher_emails.difference(old_watcher_emails))
+ removing_watcher_emails = list(old_watcher_emails.difference(new_watcher_emails))
+
+ User = apps.get_model("users", "User")
+ adding_users = User.objects.filter(email__in=adding_watcher_emails)
+ removing_users = User.objects.filter(email__in=removing_watcher_emails)
+
+ for user in adding_users:
+ notifications_services.add_watcher(self.object, user)
+
+ for user in removing_users:
+ notifications_services.remove_watcher(self.object, user)
+
+ self.object.watchers = notifications_services.get_watchers(self.object)
+
+ def to_native(self, obj):
+ ret = super(WatcheableObjectModelSerializer, self).to_native(obj)
+ ret["watchers"] = [user.email for user in notifications_services.get_watchers(obj)]
+ return ret
+
+
class HistoryExportSerializer(serializers.ModelSerializer):
user = HistoryUserField()
diff = HistoryDiffField(required=False)
@@ -243,7 +287,7 @@ class HistoryExportSerializerMixin(serializers.ModelSerializer):
def get_history(self, obj):
history_qs = history_service.get_history_queryset_by_model_instance(obj,
types=(history_models.HistoryType.change, history_models.HistoryType.create,))
-
+
return HistoryExportSerializer(history_qs, many=True).data
@@ -447,9 +491,8 @@ class RolePointsExportSerializer(serializers.ModelSerializer):
exclude = ('id', 'user_story')
-class MilestoneExportSerializer(serializers.ModelSerializer):
+class MilestoneExportSerializer(WatcheableObjectModelSerializer):
owner = UserRelatedField(required=False)
- watchers = UserRelatedField(many=True, required=False)
modified_date = serializers.DateTimeField(required=False)
def __init__(self, *args, **kwargs):
@@ -475,13 +518,12 @@ class MilestoneExportSerializer(serializers.ModelSerializer):
class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin,
- AttachmentExportSerializerMixin, serializers.ModelSerializer):
+ AttachmentExportSerializerMixin, WatcheableObjectModelSerializer):
owner = UserRelatedField(required=False)
status = ProjectRelatedField(slug_field="name")
user_story = ProjectRelatedField(slug_field="ref", required=False)
milestone = ProjectRelatedField(slug_field="name", required=False)
assigned_to = UserRelatedField(required=False)
- watchers = UserRelatedField(many=True, required=False)
modified_date = serializers.DateTimeField(required=False)
class Meta:
@@ -493,13 +535,12 @@ class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryE
class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin,
- AttachmentExportSerializerMixin, serializers.ModelSerializer):
+ AttachmentExportSerializerMixin, WatcheableObjectModelSerializer):
role_points = RolePointsExportSerializer(many=True, required=False)
owner = UserRelatedField(required=False)
assigned_to = UserRelatedField(required=False)
status = ProjectRelatedField(slug_field="name")
milestone = ProjectRelatedField(slug_field="name", required=False)
- watchers = UserRelatedField(many=True, required=False)
modified_date = serializers.DateTimeField(required=False)
generated_from_issue = ProjectRelatedField(slug_field="ref", required=False)
@@ -512,7 +553,7 @@ class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, His
class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin,
- AttachmentExportSerializerMixin, serializers.ModelSerializer):
+ AttachmentExportSerializerMixin, WatcheableObjectModelSerializer):
owner = UserRelatedField(required=False)
status = ProjectRelatedField(slug_field="name")
assigned_to = UserRelatedField(required=False)
@@ -520,7 +561,6 @@ class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, History
severity = ProjectRelatedField(slug_field="name")
type = ProjectRelatedField(slug_field="name")
milestone = ProjectRelatedField(slug_field="name", required=False)
- watchers = UserRelatedField(many=True, required=False)
votes = serializers.SerializerMethodField("get_votes")
modified_date = serializers.DateTimeField(required=False)
@@ -536,10 +576,9 @@ class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, History
class WikiPageExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin,
- serializers.ModelSerializer):
+ WatcheableObjectModelSerializer):
owner = UserRelatedField(required=False)
last_modifier = UserRelatedField(required=False)
- watchers = UserRelatedField(many=True, required=False)
modified_date = serializers.DateTimeField(required=False)
class Meta:
@@ -586,7 +625,7 @@ class TimelineExportSerializer(serializers.ModelSerializer):
exclude = ('id', 'project', 'namespace', 'object_id')
-class ProjectExportSerializer(serializers.ModelSerializer):
+class ProjectExportSerializer(WatcheableObjectModelSerializer):
owner = UserRelatedField(required=False)
default_points = serializers.SlugRelatedField(slug_field="name", required=False)
default_us_status = serializers.SlugRelatedField(slug_field="name", required=False)
diff --git a/taiga/export_import/service.py b/taiga/export_import/service.py
index 40578118..25b9be90 100644
--- a/taiga/export_import/service.py
+++ b/taiga/export_import/service.py
@@ -71,6 +71,7 @@ def store_project(data):
if serialized.is_valid():
serialized.object._importing = True
serialized.object.save()
+ serialized.save_watchers()
return serialized
add_errors("project", serialized.errors)
return None
@@ -217,6 +218,7 @@ def store_task(project, data):
serialized.object._not_notify = True
serialized.save()
+ serialized.save_watchers()
if serialized.object.ref:
sequence_name = refs.make_sequence_name(project)
@@ -257,6 +259,7 @@ def store_milestone(project, milestone):
serialized.object.project = project
serialized.object._importing = True
serialized.save()
+ serialized.save_watchers()
for task_without_us in milestone.get("tasks_without_us", []):
task_without_us["user_story"] = None
@@ -320,6 +323,7 @@ def store_wiki_page(project, wiki_page):
serialized.object._importing = True
serialized.object._not_notify = True
serialized.save()
+ serialized.save_watchers()
for attachment in wiki_page.get("attachments", []):
store_attachment(project, serialized.object, attachment)
@@ -382,6 +386,7 @@ def store_user_story(project, data):
serialized.object._not_notify = True
serialized.save()
+ serialized.save_watchers()
if serialized.object.ref:
sequence_name = refs.make_sequence_name(project)
@@ -442,6 +447,7 @@ def store_issue(project, data):
serialized.object._not_notify = True
serialized.save()
+ serialized.save_watchers()
if serialized.object.ref:
sequence_name = refs.make_sequence_name(project)
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/permissions/service.py b/taiga/permissions/service.py
index d9df5bd7..3a79b6ee 100644
--- a/taiga/permissions/service.py
+++ b/taiga/permissions/service.py
@@ -15,11 +15,12 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from taiga.projects.models import Membership, Project
from .permissions import OWNERS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
+from django.apps import apps
def _get_user_project_membership(user, project):
+ Membership = apps.get_model("projects", "Membership")
if user.is_anonymous():
return None
@@ -30,7 +31,7 @@ def _get_user_project_membership(user, project):
def _get_object_project(obj):
project = None
-
+ Project = apps.get_model("projects", "Project")
if isinstance(obj, Project):
project = obj
elif obj and hasattr(obj, 'project'):
diff --git a/taiga/projects/admin.py b/taiga/projects/admin.py
index f3eea353..5dcc9758 100644
--- a/taiga/projects/admin.py
+++ b/taiga/projects/admin.py
@@ -17,7 +17,10 @@
from django.contrib import admin
from taiga.projects.milestones.admin import MilestoneInline
+from taiga.projects.notifications.admin import WatchedInline
+from taiga.projects.votes.admin import VoteInline
from taiga.users.admin import RoleInline
+
from . import models
class MembershipAdmin(admin.ModelAdmin):
@@ -35,7 +38,7 @@ class ProjectAdmin(admin.ModelAdmin):
list_display = ["name", "owner", "created_date", "total_milestones",
"total_story_points"]
list_display_links = list_display
- inlines = [RoleInline, MembershipInline, MilestoneInline]
+ inlines = [RoleInline, MembershipInline, MilestoneInline, WatchedInline, VoteInline]
def get_object(self, *args, **kwargs):
self.obj = super().get_object(*args, **kwargs)
diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index cd70fbf3..43f78475 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -31,6 +31,7 @@ from taiga.base.api.utils import get_object_or_404
from taiga.base.utils.slug import slugify_uniquely
from taiga.projects.history.mixins import HistoryResourceMixin
+from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
from taiga.projects.mixins.ordering import BulkUpdateOrderMixin
from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin
@@ -44,15 +45,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, WatchedResourceMixin, ModelCrudViewSet):
+ queryset = models.Project.objects.all()
serializer_class = serializers.ProjectDetailSerializer
admin_serializer_class = serializers.ProjectDetailAdminSerializer
list_serializer_class = serializers.ProjectSerializer
@@ -61,6 +61,11 @@ class ProjectViewSet(HistoryResourceMixin, ModelCrudViewSet):
filter_fields = (('member', 'members'),)
order_by_fields = ("memberships__user_order",)
+ def get_queryset(self):
+ qs = super().get_queryset()
+ qs = self.attach_votes_attrs_to_queryset(qs)
+ return self.attach_watchers_attrs_to_queryset(qs)
+
@list_route(methods=["POST"])
def bulk_update_order(self, request, **kwargs):
if self.request.user.is_anonymous():
@@ -74,10 +79,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 +167,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 +265,14 @@ class ProjectViewSet(HistoryResourceMixin, ModelCrudViewSet):
return response.NoContent()
+class ProjectFansViewSet(VotersViewSetMixin, ModelListViewSet):
+ permission_classes = (permissions.ProjectFansPermission,)
+ resource_model = models.Project
+
+
+class ProjectWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
+ permission_classes = (permissions.ProjectWatchersPermission,)
+ resource_model = models.Project
######################################################
## Custom values for selectors
diff --git a/taiga/projects/apps.py b/taiga/projects/apps.py
index acdaa7da..06b42de8 100644
--- a/taiga/projects/apps.py
+++ b/taiga/projects/apps.py
@@ -27,12 +27,7 @@ def connect_memberships_signals():
sender=apps.get_model("projects", "Membership"),
dispatch_uid='membership_pre_delete')
- # On membership object is deleted, update watchers of all objects relation.
- signals.post_delete.connect(handlers.update_watchers_on_membership_post_delete,
- sender=apps.get_model("projects", "Membership"),
- dispatch_uid='update_watchers_on_membership_post_delete')
-
- # On membership object is deleted, update watchers of all objects relation.
+ # On membership object is deleted, update notify policies of all objects relation.
signals.post_save.connect(handlers.create_notify_policy,
sender=apps.get_model("projects", "Membership"),
dispatch_uid='create-notify-policy')
@@ -67,7 +62,6 @@ def connect_task_status_signals():
def disconnect_memberships_signals():
signals.pre_delete.disconnect(sender=apps.get_model("projects", "Membership"), dispatch_uid='membership_pre_delete')
- signals.post_delete.disconnect(sender=apps.get_model("projects", "Membership"), dispatch_uid='update_watchers_on_membership_post_delete')
signals.post_save.disconnect(sender=apps.get_model("projects", "Membership"), dispatch_uid='create-notify-policy')
diff --git a/taiga/projects/history/freeze_impl.py b/taiga/projects/history/freeze_impl.py
index a591c666..d5ecc800 100644
--- a/taiga/projects/history/freeze_impl.py
+++ b/taiga/projects/history/freeze_impl.py
@@ -288,7 +288,7 @@ def userstory_freezer(us) -> dict:
"milestone": us.milestone_id,
"client_requirement": us.client_requirement,
"team_requirement": us.team_requirement,
- "watchers": [x.id for x in us.watchers.all()],
+ "watchers": [x.id for x in us.get_watchers()],
"attachments": extract_attachments(us),
"tags": us.tags,
"points": points,
@@ -315,7 +315,7 @@ def issue_freezer(issue) -> dict:
"description": issue.description,
"description_html": mdrender(issue.project, issue.description),
"assigned_to": issue.assigned_to_id,
- "watchers": [x.pk for x in issue.watchers.all()],
+ "watchers": [x.pk for x in issue.get_watchers()],
"attachments": extract_attachments(issue),
"tags": issue.tags,
"is_blocked": issue.is_blocked,
@@ -337,7 +337,7 @@ def task_freezer(task) -> dict:
"description": task.description,
"description_html": mdrender(task.project, task.description),
"assigned_to": task.assigned_to_id,
- "watchers": [x.pk for x in task.watchers.all()],
+ "watchers": [x.pk for x in task.get_watchers()],
"attachments": extract_attachments(task),
"taskboard_order": task.taskboard_order,
"us_order": task.us_order,
@@ -359,7 +359,7 @@ def wikipage_freezer(wiki) -> dict:
"owner": wiki.owner_id,
"content": wiki.content,
"content_html": mdrender(wiki.project, wiki.content),
- "watchers": [x.pk for x in wiki.watchers.all()],
+ "watchers": [x.pk for x in wiki.get_watchers()],
"attachments": extract_attachments(wiki),
}
diff --git a/taiga/projects/issues/admin.py b/taiga/projects/issues/admin.py
index 16da297e..fe891719 100644
--- a/taiga/projects/issues/admin.py
+++ b/taiga/projects/issues/admin.py
@@ -17,13 +17,16 @@
from django.contrib import admin
from taiga.projects.attachments.admin import AttachmentInline
+from taiga.projects.notifications.admin import WatchedInline
+from taiga.projects.votes.admin import VoteInline
+
from . import models
class IssueAdmin(admin.ModelAdmin):
list_display = ["project", "milestone", "ref", "subject",]
list_display_links = ["ref", "subject",]
- # inlines = [AttachmentInline]
+ inlines = [WatchedInline, VoteInline]
def get_object(self, *args, **kwargs):
self.obj = super().get_object(*args, **kwargs)
diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py
index ee587e8c..5035418b 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
@@ -27,22 +27,23 @@ from taiga.base.api.utils import get_object_or_404
from taiga.users.models import User
-from taiga.projects.notifications.mixins import WatchedResourceMixin
+from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
from taiga.projects.occ import OCCResourceMixin
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,
@@ -52,6 +53,7 @@ class IssueViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
filters.SeveritiesFilter,
filters.PrioritiesFilter,
filters.TagsFilter,
+ filters.WatchersFilter,
filters.QFilter,
filters.OrderByFilterMixin)
retrieve_exclude_filters = (filters.OwnersFilter,
@@ -60,11 +62,11 @@ class IssueViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
filters.IssueTypesFilter,
filters.SeveritiesFilter,
filters.PrioritiesFilter,
- filters.TagsFilter,)
+ filters.TagsFilter,
+ filters.WatchersFilter,)
filter_fields = ("project",
- "status__is_closed",
- "watchers")
+ "status__is_closed")
order_by_fields = ("type",
"status",
"severity",
@@ -139,10 +141,10 @@ 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
+ qs = self.attach_votes_attrs_to_queryset(qs)
+ return self.attach_watchers_attrs_to_queryset(qs)
def pre_save(self, obj):
if not obj.id:
@@ -237,51 +239,12 @@ 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 IssueVotersViewSet(VotersViewSetMixin, ModelListViewSet):
+ permission_classes = (permissions.IssueVotersPermission,)
+ resource_model = models.Issue
-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 IssueWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
+ permission_classes = (permissions.IssueWatchersPermission,)
+ resource_model = models.Issue
diff --git a/taiga/projects/issues/migrations/0006_remove_issue_watchers.py b/taiga/projects/issues/migrations/0006_remove_issue_watchers.py
new file mode 100644
index 00000000..dd3ee037
--- /dev/null
+++ b/taiga/projects/issues/migrations/0006_remove_issue_watchers.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import connection
+from django.db import models, migrations
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes.management import update_all_contenttypes
+
+def create_notifications(apps, schema_editor):
+ update_all_contenttypes()
+ sql="""
+INSERT INTO notifications_watched (object_id, created_date, content_type_id, user_id, project_id)
+SELECT issue_id AS object_id, now() AS created_date, {content_type_id} AS content_type_id, user_id, project_id
+FROM issues_issue_watchers INNER JOIN issues_issue ON issues_issue_watchers.issue_id = issues_issue.id""".format(content_type_id=ContentType.objects.get(model='issue').id)
+ cursor = connection.cursor()
+ cursor.execute(sql)
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('notifications', '0004_watched'),
+ ('issues', '0005_auto_20150623_1923'),
+ ]
+
+ operations = [
+ migrations.RunPython(create_notifications),
+ migrations.RemoveField(
+ model_name='issue',
+ name='watchers',
+ ),
+ ]
diff --git a/taiga/projects/issues/permissions.py b/taiga/projects/issues/permissions.py
index 076b57a0..91f988ca 100644
--- a/taiga/projects/issues/permissions.py
+++ b/taiga/projects/issues/permissions.py
@@ -31,10 +31,12 @@ 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')
+ watch_perms = IsAuthenticated() & HasProjectPerm('view_issues')
+ unwatch_perms = IsAuthenticated() & HasProjectPerm('view_issues')
class HasIssueIdUrlParam(PermissionComponent):
@@ -49,8 +51,11 @@ 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')
+
+
+class IssueWatchersPermission(TaigaResourcePermission):
+ enought_perms = IsProjectOwner() | IsSuperUser()
+ global_perms = None
+ retrieve_perms = HasProjectPerm('view_issues')
list_perms = HasProjectPerm('view_issues')
diff --git a/taiga/projects/issues/serializers.py b/taiga/projects/issues/serializers.py
index 2b2723dd..64c83f17 100644
--- a/taiga/projects/issues/serializers.py
+++ b/taiga/projects/issues/serializers.py
@@ -19,17 +19,19 @@ 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.notifications.mixins import WatchedResourceModelSerializer
+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, WatchedResourceModelSerializer, serializers.ModelSerializer):
tags = TagsField(required=False)
external_reference = PgArrayField(required=False)
is_closed = serializers.Field(source="is_closed")
@@ -37,7 +39,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 +60,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/issues/services.py b/taiga/projects/issues/services.py
index 9a553cd3..d1156227 100644
--- a/taiga/projects/issues/services.py
+++ b/taiga/projects/issues/services.py
@@ -27,7 +27,7 @@ from taiga.base.utils import db, text
from taiga.projects.issues.apps import (
connect_issues_signals,
disconnect_issues_signals)
-
+from taiga.projects.votes import services as votes_services
from . import models
@@ -84,7 +84,8 @@ def issues_to_csv(project, queryset):
fieldnames = ["ref", "subject", "description", "milestone", "owner",
"owner_full_name", "assigned_to", "assigned_to_full_name",
"status", "severity", "priority", "type", "is_closed",
- "attachments", "external_reference", "tags"]
+ "attachments", "external_reference", "tags",
+ "watchers", "voters"]
for custom_attr in project.issuecustomattributes.all():
fieldnames.append(custom_attr.name)
@@ -108,6 +109,8 @@ def issues_to_csv(project, queryset):
"attachments": issue.attachments.count(),
"external_reference": issue.external_reference,
"tags": ",".join(issue.tags or []),
+ "watchers": [u.id for u in issue.get_watchers()],
+ "voters": votes_services.get_voters(issue).count(),
}
for custom_attr in project.issuecustomattributes.all():
diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py
index 0a5e583e..6c5a4bcc 100644
--- a/taiga/projects/management/commands/sample_data.py
+++ b/taiga/projects/management/commands/sample_data.py
@@ -37,6 +37,7 @@ from taiga.projects.wiki.models import *
from taiga.projects.attachments.models import *
from taiga.projects.custom_attributes.models import *
from taiga.projects.history.services import take_snapshot
+from taiga.projects.votes.services import add_vote
from taiga.events.apps import disconnect_events_signals
@@ -97,7 +98,8 @@ NUM_TASKS = getattr(settings, "SAMPLE_DATA_NUM_TASKS", (0, 4))
NUM_USS_BACK = getattr(settings, "SAMPLE_DATA_NUM_USS_BACK", (8, 20))
NUM_ISSUES = getattr(settings, "SAMPLE_DATA_NUM_ISSUES", (12, 25))
NUM_ATTACHMENTS = getattr(settings, "SAMPLE_DATA_NUM_ATTACHMENTS", (0, 4))
-
+NUM_VOTES = getattr(settings, "SAMPLE_DATA_NUM_VOTES", (0, 3))
+NUM_PROJECT_WATCHERS = getattr(settings, "SAMPLE_DATA_NUM_PROJECT_WATCHERS", (0, 3))
class Command(BaseCommand):
sd = SampleDataHelper(seed=12345678901)
@@ -215,6 +217,7 @@ class Command(BaseCommand):
project.total_story_points = int(defined_points * self.sd.int(5,12) / 10)
project.save()
+ self.create_votes(project, project)
def create_attachment(self, obj, order):
attached_file = self.sd.file_from_directory(*ATTACHMENT_SAMPLE_DATA)
@@ -287,7 +290,7 @@ class Command(BaseCommand):
bug.save()
watching_user = self.sd.db_object_from_queryset(project.memberships.filter(user__isnull=False)).user
- bug.watchers.add(watching_user)
+ bug.add_watcher(watching_user)
take_snapshot(bug,
comment=self.sd.paragraph(),
@@ -300,6 +303,7 @@ class Command(BaseCommand):
comment=self.sd.paragraph(),
user=bug.owner)
+ self.create_votes(bug, project)
return bug
def create_task(self, project, milestone, us, min_date, max_date, closed=False):
@@ -338,7 +342,7 @@ class Command(BaseCommand):
user=task.owner)
watching_user = self.sd.db_object_from_queryset(project.memberships.filter(user__isnull=False)).user
- task.watchers.add(watching_user)
+ task.add_watcher(watching_user)
# Add history entry
task.status=self.sd.db_object_from_queryset(project.task_statuses.all())
@@ -347,6 +351,7 @@ class Command(BaseCommand):
comment=self.sd.paragraph(),
user=task.owner)
+ self.create_votes(task, project)
return task
def create_us(self, project, milestone=None, computable_project_roles=[]):
@@ -387,7 +392,7 @@ class Command(BaseCommand):
us.save()
watching_user = self.sd.db_object_from_queryset(project.memberships.filter(user__isnull=False)).user
- us.watchers.add(watching_user)
+ us.add_watcher(watching_user)
take_snapshot(us,
comment=self.sd.paragraph(),
@@ -400,6 +405,7 @@ class Command(BaseCommand):
comment=self.sd.paragraph(),
user=us.owner)
+ self.create_votes(us, project)
return us
def create_milestone(self, project, start_date, end_date):
@@ -434,6 +440,11 @@ class Command(BaseCommand):
project.is_kanban_activated = True
project.save()
take_snapshot(project, user=project.owner)
+
+ for i in range(self.sd.int(*NUM_PROJECT_WATCHERS)):
+ watching_user = self.sd.db_object_from_queryset(User.objects.all())
+ project.add_watcher(watching_user)
+
return project
def create_user(self, counter=None, username=None, full_name=None, email=None):
@@ -452,3 +463,8 @@ class Command(BaseCommand):
user.save()
return user
+
+ def create_votes(self, obj, project):
+ for i in range(self.sd.int(*NUM_VOTES)):
+ voting_user=self.sd.db_object_from_queryset(project.members.all())
+ add_vote(obj, voting_user)
diff --git a/taiga/projects/migrations/0024_auto_20150810_1247.py b/taiga/projects/migrations/0024_auto_20150810_1247.py
new file mode 100644
index 00000000..f057816b
--- /dev/null
+++ b/taiga/projects/migrations/0024_auto_20150810_1247.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import djorm_pgarray.fields
+from django.conf import settings
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('projects', '0023_auto_20150721_1511'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='project',
+ name='public_permissions',
+ field=djorm_pgarray.fields.TextArrayField(default=[], dbtype='text', choices=[('view_project', 'View project'), ('star_project', 'Star project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('delete_us', 'Delete user story'), ('vote_us', 'Vote user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('delete_task', 'Delete task'), ('vote_task', 'Vote task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('delete_issue', 'Delete issue'), ('vote_issue', 'Vote issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')], verbose_name='user permissions'),
+ preserve_default=True,
+ ),
+ ]
diff --git a/taiga/projects/milestones/admin.py b/taiga/projects/milestones/admin.py
index b741a1b2..52919e87 100644
--- a/taiga/projects/milestones/admin.py
+++ b/taiga/projects/milestones/admin.py
@@ -15,6 +15,8 @@
# along with this program. If not, see .
from django.contrib import admin
+from taiga.projects.notifications.admin import WatchedInline
+from taiga.projects.votes.admin import VoteInline
from . import models
@@ -30,6 +32,7 @@ class MilestoneAdmin(admin.ModelAdmin):
list_display_links = list_display
list_filter = ["project"]
readonly_fields = ["owner"]
+ inlines = [WatchedInline, VoteInline]
admin.site.register(models.Milestone, MilestoneAdmin)
diff --git a/taiga/projects/milestones/api.py b/taiga/projects/milestones/api.py
index 132f9bf2..3194dc39 100644
--- a/taiga/projects/milestones/api.py
+++ b/taiga/projects/milestones/api.py
@@ -17,10 +17,10 @@
from taiga.base import filters
from taiga.base import response
from taiga.base.decorators import detail_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.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
from taiga.projects.history.mixins import HistoryResourceMixin
@@ -36,17 +36,17 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudView
permission_classes = (permissions.MilestonePermission,)
filter_backends = (filters.CanViewMilestonesFilterBackend,)
filter_fields = ("project", "closed")
+ queryset = models.Milestone.objects.all()
def get_queryset(self):
- qs = models.Milestone.objects.all()
+ qs = super().get_queryset()
+ qs = self.attach_watchers_attrs_to_queryset(qs)
qs = qs.prefetch_related("user_stories",
"user_stories__role_points",
"user_stories__role_points__points",
"user_stories__role_points__role",
"user_stories__generated_from_issue",
- "user_stories__project",
- "watchers",
- "user_stories__watchers")
+ "user_stories__project")
qs = qs.select_related("project")
qs = qs.order_by("-estimated_start")
return qs
@@ -93,3 +93,8 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudView
optimal_points -= optimal_points_per_day
return response.Ok(milestone_stats)
+
+
+class MilestoneWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
+ permission_classes = (permissions.MilestoneWatchersPermission,)
+ resource_model = models.Milestone
diff --git a/taiga/projects/milestones/migrations/0002_remove_milestone_watchers.py b/taiga/projects/milestones/migrations/0002_remove_milestone_watchers.py
new file mode 100644
index 00000000..69d6aacd
--- /dev/null
+++ b/taiga/projects/milestones/migrations/0002_remove_milestone_watchers.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import connection
+from django.db import models, migrations
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes.management import update_all_contenttypes
+
+def create_notifications(apps, schema_editor):
+ update_all_contenttypes()
+ sql="""
+INSERT INTO notifications_watched (object_id, created_date, content_type_id, user_id, project_id)
+SELECT milestone_id AS object_id, now() AS created_date, {content_type_id} AS content_type_id, user_id, project_id
+FROM milestones_milestone_watchers INNER JOIN milestones_milestone ON milestones_milestone_watchers.milestone_id = milestones_milestone.id""".format(content_type_id=ContentType.objects.get(model='milestone').id)
+ cursor = connection.cursor()
+ cursor.execute(sql)
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('notifications', '0004_watched'),
+ ('milestones', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.RunPython(create_notifications),
+ migrations.RemoveField(
+ model_name='milestone',
+ name='watchers',
+ ),
+ ]
diff --git a/taiga/projects/milestones/permissions.py b/taiga/projects/milestones/permissions.py
index 9823c8de..843c0c8a 100644
--- a/taiga/projects/milestones/permissions.py
+++ b/taiga/projects/milestones/permissions.py
@@ -15,8 +15,8 @@
# along with this program. If not, see .
from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
- IsProjectOwner, AllowAny,
- PermissionComponent, IsSuperUser)
+ IsAuthenticated, IsProjectOwner, AllowAny,
+ IsSuperUser)
class MilestonePermission(TaigaResourcePermission):
@@ -29,3 +29,11 @@ class MilestonePermission(TaigaResourcePermission):
destroy_perms = HasProjectPerm('delete_milestone')
list_perms = AllowAny()
stats_perms = HasProjectPerm('view_milestones')
+ watch_perms = IsAuthenticated() & HasProjectPerm('view_milestones')
+ unwatch_perms = IsAuthenticated() & HasProjectPerm('view_milestones')
+
+class MilestoneWatchersPermission(TaigaResourcePermission):
+ enought_perms = IsProjectOwner() | IsSuperUser()
+ global_perms = None
+ retrieve_perms = HasProjectPerm('view_milestones')
+ list_perms = HasProjectPerm('view_milestones')
diff --git a/taiga/projects/milestones/serializers.py b/taiga/projects/milestones/serializers.py
index 2ffd1d43..471b9546 100644
--- a/taiga/projects/milestones/serializers.py
+++ b/taiga/projects/milestones/serializers.py
@@ -17,14 +17,15 @@
from django.utils.translation import ugettext as _
from taiga.base.api import serializers
-
from taiga.base.utils import json
+from taiga.projects.notifications.mixins import WatchedResourceModelSerializer
+from taiga.projects.notifications.validators import WatchersValidator
from ..userstories.serializers import UserStorySerializer
from . import models
-class MilestoneSerializer(serializers.ModelSerializer):
+class MilestoneSerializer(WatchersValidator, WatchedResourceModelSerializer, serializers.ModelSerializer):
user_stories = UserStorySerializer(many=True, required=False, read_only=True)
total_points = serializers.SerializerMethodField("get_total_points")
closed_points = serializers.SerializerMethodField("get_closed_points")
diff --git a/taiga/projects/models.py b/taiga/projects/models.py
index 145edfb0..b2e2566f 100644
--- a/taiga/projects/models.py
+++ b/taiga/projects/models.py
@@ -40,6 +40,8 @@ from taiga.base.utils.slug import slugify_uniquely_for_queryset
from . import choices
+from . notifications.mixins import WatchedModelMixin
+
class Membership(models.Model):
# This model stores all project memberships. Also
@@ -118,7 +120,7 @@ class ProjectDefaults(models.Model):
abstract = True
-class Project(ProjectDefaults, TaggedMixin, models.Model):
+class Project(ProjectDefaults, WatchedModelMixin, TaggedMixin, models.Model):
name = models.CharField(max_length=250, null=False, blank=False,
verbose_name=_("name"))
slug = models.SlugField(max_length=250, unique=True, null=False, blank=True,
diff --git a/taiga/projects/notifications/admin.py b/taiga/projects/notifications/admin.py
new file mode 100644
index 00000000..2dc22d61
--- /dev/null
+++ b/taiga/projects/notifications/admin.py
@@ -0,0 +1,25 @@
+# Copyright (C) 2014 Andrey Antukh
+# Copyright (C) 2014 Jesús Espino
+# Copyright (C) 2014 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.contrib import admin
+from django.contrib.contenttypes.admin import GenericTabularInline
+
+from . import models
+
+
+class WatchedInline(GenericTabularInline):
+ model = models.Watched
+ extra = 0
diff --git a/taiga/projects/notifications/api.py b/taiga/projects/notifications/api.py
index 2431c7c2..b2f2d260 100644
--- a/taiga/projects/notifications/api.py
+++ b/taiga/projects/notifications/api.py
@@ -19,8 +19,9 @@ from django.db.models import Q
from taiga.base.api import ModelCrudViewSet
from taiga.projects.notifications.choices import NotifyLevel
+from taiga.projects.notifications.models import Watched
from taiga.projects.models import Project
-
+from taiga.users import services as user_services
from . import serializers
from . import models
from . import permissions
@@ -32,9 +33,13 @@ class NotifyPolicyViewSet(ModelCrudViewSet):
permission_classes = (permissions.NotifyPolicyPermission,)
def _build_needed_notify_policies(self):
+ watched_content = user_services.get_watched_content_for_user(self.request.user)
+ watched_content_project_ids = watched_content.values_list("project__id", flat=True).distinct()
+
projects = Project.objects.filter(
Q(owner=self.request.user) |
- Q(memberships__user=self.request.user)
+ Q(memberships__user=self.request.user) |
+ Q(id__in=watched_content_project_ids)
).distinct()
for project in projects:
@@ -45,5 +50,14 @@ class NotifyPolicyViewSet(ModelCrudViewSet):
return models.NotifyPolicy.objects.none()
self._build_needed_notify_policies()
- qs = models.NotifyPolicy.objects.filter(user=self.request.user)
- return qs.distinct()
+
+ # With really want to include the policies related to any content:
+ # - The user is the owner of the project
+ # - The user is member of the project
+ # - The user is watching any object from the project
+ watched_content = user_services.get_watched_content_for_user(self.request.user)
+ watched_content_project_ids = watched_content.values_list("project__id", flat=True).distinct()
+ return models.NotifyPolicy.objects.filter(Q(project__owner=self.request.user) |
+ Q(project__memberships__user=self.request.user) |
+ Q(project__id__in=watched_content_project_ids)
+ ).distinct()
diff --git a/taiga/projects/notifications/migrations/0004_watched.py b/taiga/projects/notifications/migrations/0004_watched.py
new file mode 100644
index 00000000..ab0878b7
--- /dev/null
+++ b/taiga/projects/notifications/migrations/0004_watched.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+from django.conf import settings
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('contenttypes', '0001_initial'),
+ ('notifications', '0003_auto_20141029_1143'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Watched',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('object_id', models.PositiveIntegerField()),
+ ('created_date', models.DateTimeField(verbose_name='created date', auto_now_add=True)),
+ ('content_type', models.ForeignKey(to='contenttypes.ContentType')),
+ ('user', models.ForeignKey(related_name='watched', verbose_name='user', to=settings.AUTH_USER_MODEL)),
+ ('project', models.ForeignKey(to='projects.Project', verbose_name='project', related_name='watched')),
+
+ ],
+ options={
+ 'verbose_name': 'Watched',
+ 'verbose_name_plural': 'Watched',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.AlterUniqueTogether(
+ name='watched',
+ unique_together=set([('content_type', 'object_id', 'user', 'project')]),
+ ),
+ ]
diff --git a/taiga/projects/notifications/mixins.py b/taiga/projects/notifications/mixins.py
index 362635a4..75740a3d 100644
--- a/taiga/projects/notifications/mixins.py
+++ b/taiga/projects/notifications/mixins.py
@@ -17,14 +17,26 @@
from functools import partial
from operator import is_not
-from django.conf import settings
+from django.apps import apps
+from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.utils.translation import ugettext_lazy as _
+from taiga.base import response
+from taiga.base.decorators import detail_route
+from taiga.base.api import serializers
+from taiga.base.api.utils import get_object_or_404
+from taiga.base.fields import WatchersField
from taiga.projects.notifications import services
+from taiga.projects.notifications.utils import attach_watchers_to_queryset, attach_is_watched_to_queryset
+from taiga.users.models import User
+from . import models
+from . serializers import WatcherSerializer
-class WatchedResourceMixin(object):
+
+class WatchedResourceMixin:
"""
Rest Framework resource mixin for resources susceptible
to be notifiable about their changes.
@@ -36,6 +48,27 @@ class WatchedResourceMixin(object):
_not_notify = False
+ def attach_watchers_attrs_to_queryset(self, queryset):
+ qs = attach_watchers_to_queryset(queryset)
+ if self.request.user.is_authenticated():
+ qs = attach_is_watched_to_queryset(self.request.user, qs)
+
+ return qs
+
+ @detail_route(methods=["POST"])
+ def watch(self, request, pk=None):
+ obj = self.get_object()
+ self.check_permissions(request, "watch", obj)
+ services.add_watcher(obj, request.user)
+ return response.Ok()
+
+ @detail_route(methods=["POST"])
+ def unwatch(self, request, pk=None):
+ obj = self.get_object()
+ self.check_permissions(request, "unwatch", obj)
+ services.remove_watcher(obj, request.user)
+ return response.Ok()
+
def send_notifications(self, obj, history=None):
"""
Shortcut method for resources with special save
@@ -73,7 +106,7 @@ class WatchedResourceMixin(object):
super().pre_delete(obj)
-class WatchedModelMixin(models.Model):
+class WatchedModelMixin(object):
"""
Generic model mixin that makes model compatible
with notification system.
@@ -82,11 +115,6 @@ class WatchedModelMixin(models.Model):
this mixin if you want send notifications about
your model class.
"""
- watchers = models.ManyToManyField(settings.AUTH_USER_MODEL, null=True, blank=True,
- related_name="%(app_label)s_%(class)s+",
- verbose_name=_("watchers"))
- class Meta:
- abstract = True
def get_project(self) -> object:
"""
@@ -112,7 +140,16 @@ class WatchedModelMixin(models.Model):
very inefficient way for obtain watchers but at
this momment is the simplest way.
"""
- return frozenset(self.watchers.all())
+ return frozenset(services.get_watchers(self))
+
+ def get_watched(self, user_or_id):
+ return services.get_watched(user_or_id, type(self))
+
+ def add_watcher(self, user):
+ services.add_watcher(self, user)
+
+ def remove_watcher(self, user):
+ services.remove_watcher(self, user)
def get_owner(self) -> object:
"""
@@ -140,3 +177,79 @@ class WatchedModelMixin(models.Model):
self.get_owner(),)
is_not_none = partial(is_not, None)
return frozenset(filter(is_not_none, participants))
+
+
+class WatchedResourceModelSerializer(serializers.ModelSerializer):
+ is_watched = serializers.SerializerMethodField("get_is_watched")
+ watchers = WatchersField(required=False)
+
+ def get_is_watched(self, obj):
+ # The "is_watched" attribute is attached in the get_queryset of the viewset.
+ return getattr(obj, "is_watched", False) or False
+
+ def restore_object(self, attrs, instance=None):
+ #watchers is not a field from the model but can be attached in the get_queryset of the viewset.
+ #If that's the case we need to remove it before calling the super method
+ watcher_field = self.fields.pop("watchers", None)
+ instance = super(WatchedResourceModelSerializer, self).restore_object(attrs, instance)
+ if instance is not None and self.validate_watchers(attrs, "watchers"):
+ new_watcher_ids = set(attrs.get("watchers", []))
+ old_watcher_ids = set(services.get_watchers(instance).values_list("id", flat=True))
+ adding_watcher_ids = list(new_watcher_ids.difference(old_watcher_ids))
+ removing_watcher_ids = list(old_watcher_ids.difference(new_watcher_ids))
+
+ User = apps.get_model("users", "User")
+ adding_users = User.objects.filter(id__in=adding_watcher_ids)
+ removing_users = User.objects.filter(id__in=removing_watcher_ids)
+ for user in adding_users:
+ services.add_watcher(instance, user)
+
+ for user in removing_users:
+ services.remove_watcher(instance, user)
+
+ instance.watchers = services.get_watchers(instance)
+
+ return instance
+
+
+ def to_native(self, obj):
+ #watchers is wasn't attached via the get_queryset of the viewset we need to manually add it
+ if not hasattr(obj, "watchers"):
+ obj.watchers = [user.id for user in services.get_watchers(obj)]
+
+ return super(WatchedResourceModelSerializer, self).to_native(obj)
+
+
+class WatchersViewSetMixin:
+ # Is a ModelListViewSet with two required params: permission_classes and resource_model
+ serializer_class = WatcherSerializer
+ list_serializer_class = WatcherSerializer
+ 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_watchers(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_watchers(resource)
diff --git a/taiga/projects/notifications/models.py b/taiga/projects/notifications/models.py
index 29983f90..603bdd85 100644
--- a/taiga/projects/notifications/models.py
+++ b/taiga/projects/notifications/models.py
@@ -14,13 +14,15 @@
# You should have received a copy of the GNU Affero General Public License
# 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.utils import timezone
from taiga.projects.history.choices import HISTORY_TYPE_CHOICES
-from .choices import NOTIFY_LEVEL_CHOICES
+from .choices import NOTIFY_LEVEL_CHOICES, NotifyLevel
class NotifyPolicy(models.Model):
@@ -72,3 +74,19 @@ class HistoryChangeNotification(models.Model):
class Meta:
unique_together = ("key", "owner", "project", "history_type")
+
+
+class Watched(models.Model):
+ content_type = models.ForeignKey("contenttypes.ContentType")
+ object_id = models.PositiveIntegerField()
+ content_object = generic.GenericForeignKey("content_type", "object_id")
+ user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=False, null=False,
+ related_name="watched", verbose_name=_("user"))
+ created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False,
+ verbose_name=_("created date"))
+ project = models.ForeignKey("projects.Project", null=False, blank=False,
+ verbose_name=_("project"),related_name="watched")
+ class Meta:
+ verbose_name = _("Watched")
+ verbose_name_plural = _("Watched")
+ unique_together = ("content_type", "object_id", "user", "project")
diff --git a/taiga/projects/notifications/serializers.py b/taiga/projects/notifications/serializers.py
index c60e4bc9..9b0b99cd 100644
--- a/taiga/projects/notifications/serializers.py
+++ b/taiga/projects/notifications/serializers.py
@@ -17,9 +17,10 @@
import json
from taiga.base.api import serializers
+from taiga.users.models import User
from . import models
-
+from . import choices
class NotifyPolicySerializer(serializers.ModelSerializer):
@@ -31,3 +32,11 @@ class NotifyPolicySerializer(serializers.ModelSerializer):
def get_project_name(self, obj):
return obj.project.name
+
+
+class WatcherSerializer(serializers.ModelSerializer):
+ full_name = serializers.CharField(source='get_full_name', required=False)
+
+ class Meta:
+ model = User
+ fields = ('id', 'username', 'full_name')
diff --git a/taiga/projects/notifications/services.py b/taiga/projects/notifications/services.py
index 75f7e48b..adc94a69 100644
--- a/taiga/projects/notifications/services.py
+++ b/taiga/projects/notifications/services.py
@@ -17,10 +17,11 @@
from functools import partial
from django.apps import apps
-from django.db import IntegrityError
+from django.db.transaction import atomic
+from django.db import IntegrityError, transaction
from django.contrib.contenttypes.models import ContentType
+from django.contrib.auth import get_user_model
from django.utils import timezone
-from django.db import transaction
from django.conf import settings
from django.utils.translation import ugettext as _
@@ -36,7 +37,7 @@ from taiga.projects.history.services import (make_key_from_model_object,
from taiga.permissions.service import user_has_perm
from taiga.users.models import User
-from .models import HistoryChangeNotification
+from .models import HistoryChangeNotification, Watched
def notify_policy_exists(project, user) -> bool:
@@ -121,11 +122,11 @@ def analize_object_for_watchers(obj:object, history:object):
if data["mentions"]:
for user in data["mentions"]:
- obj.watchers.add(user)
+ obj.add_watcher(user)
# Adding the person who edited the object to the watchers
if history.comment and not history.owner.is_system:
- obj.watchers.add(history.owner)
+ obj.add_watcher(history.owner)
def _filter_by_permissions(obj, user):
UserStory = apps.get_model("userstories", "UserStory")
@@ -170,15 +171,19 @@ def get_users_to_notify(obj, *, discard_users=None) -> list:
candidates = set()
candidates.update(filter(_can_notify_hard, project.members.all()))
candidates.update(filter(_can_notify_light, obj.get_watchers()))
+ candidates.update(filter(_can_notify_light, obj.project.get_watchers()))
candidates.update(filter(_can_notify_light, obj.get_participants()))
+ #TODO: coger los watchers del proyecto que quieren ser notificados por correo
+ #Filtrar los watchers según su nivel de watched y su nivel en el proyecto
+
# Remove the changer from candidates
if discard_users:
candidates = candidates - set(discard_users)
- candidates = filter(partial(_filter_by_permissions, obj), candidates)
+ candidates = set(filter(partial(_filter_by_permissions, obj), candidates))
# Filter disabled and system users
- candidates = filter(partial(_filter_notificable), candidates)
+ candidates = set(filter(partial(_filter_notificable), candidates))
return frozenset(candidates)
@@ -282,3 +287,72 @@ def send_sync_notifications(notification_id):
def process_sync_notifications():
for notification in HistoryChangeNotification.objects.all():
send_sync_notifications(notification.pk)
+
+
+def get_watchers(obj):
+ """Get the watchers of an object.
+
+ :param obj: Any Django model instance.
+
+ :return: User queryset object representing the users that voted the object.
+ """
+ obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj)
+ return get_user_model().objects.filter(watched__content_type=obj_type, watched__object_id=obj.id)
+
+
+def get_watched(user_or_id, model):
+ """Get the objects watched by an user.
+
+ :param user_or_id: :class:`~taiga.users.models.User` instance or id.
+ :param model: Show only objects of this kind. Can be any Django model class.
+
+ :return: Queryset of objects representing the votes of the user.
+ """
+ obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model)
+ conditions = ('notifications_watched.content_type_id = %s',
+ '%s.id = notifications_watched.object_id' % model._meta.db_table,
+ 'notifications_watched.user_id = %s')
+
+ if isinstance(user_or_id, get_user_model()):
+ user_id = user_or_id.id
+ else:
+ user_id = user_or_id
+
+ return model.objects.extra(where=conditions, tables=('notifications_watched',),
+ params=(obj_type.id, user_id))
+
+
+def add_watcher(obj, user):
+ """Add a watcher to an object.
+
+ If the user is already watching the object nothing happents (except if there is a level update),
+ so this function can be considered idempotent.
+
+ :param obj: Any Django model instance.
+ :param user: User adding the watch. :class:`~taiga.users.models.User` instance.
+ """
+ obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj)
+ watched, created = Watched.objects.get_or_create(content_type=obj_type,
+ object_id=obj.id, user=user, project=obj.project)
+
+ notify_policy, _ = apps.get_model("notifications", "NotifyPolicy").objects.get_or_create(
+ project=obj.project, user=user, defaults={"notify_level": NotifyLevel.watch})
+
+ return watched
+
+
+def remove_watcher(obj, user):
+ """Remove an watching user from an object.
+
+ If the user has not watched the object nothing happens so this function can be considered
+ idempotent.
+
+ :param obj: Any Django model instance.
+ :param user: User removing the watch. :class:`~taiga.users.models.User` instance.
+ """
+ obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj)
+ qs = Watched.objects.filter(content_type=obj_type, object_id=obj.id, user=user)
+ if not qs.exists():
+ return
+
+ qs.delete()
diff --git a/taiga/projects/notifications/utils.py b/taiga/projects/notifications/utils.py
new file mode 100644
index 00000000..14edd0b3
--- /dev/null
+++ b/taiga/projects/notifications/utils.py
@@ -0,0 +1,63 @@
+# 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 .
+
+from django.apps import apps
+
+
+def attach_watchers_to_queryset(queryset, as_field="watchers"):
+ """Attach watching user ids to each object of the queryset.
+
+ :param queryset: A Django queryset object.
+ :param as_field: Attach the watchers 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 array(SELECT user_id
+ FROM notifications_watched
+ WHERE notifications_watched.content_type_id = {type_id}
+ AND notifications_watched.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_watched_to_queryset(user, queryset, as_field="is_watched"):
+ """Attach is_watched boolean to each object of the queryset.
+
+ :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 notifications_watched
+ WHERE notifications_watched.content_type_id = {type_id}
+ AND notifications_watched.object_id = {tbl}.id
+ AND notifications_watched.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/projects/notifications/validators.py b/taiga/projects/notifications/validators.py
index 38c5750b..b28e0712 100644
--- a/taiga/projects/notifications/validators.py
+++ b/taiga/projects/notifications/validators.py
@@ -21,7 +21,7 @@ from taiga.base.api import serializers
class WatchersValidator:
def validate_watchers(self, attrs, source):
- users = attrs[source]
+ users = attrs.get(source, [])
# Try obtain a valid project
if self.object is None and "project" in attrs:
@@ -39,7 +39,8 @@ class WatchersValidator:
# Check if incoming watchers are contained
# in project members list
- result = set(users).difference(set(project.members.all()))
+ member_ids = project.members.values_list("id", flat=True)
+ result = set(users).difference(member_ids)
if result:
raise serializers.ValidationError(_("Watchers contains invalid users"))
diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py
index ff152e58..1ec6f984 100644
--- a/taiga/projects/permissions.py
+++ b/taiga/projects/permissions.py
@@ -54,19 +54,34 @@ 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')
+ watch_perms = IsAuthenticated() & HasProjectPerm('view_project')
+ unwatch_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 ProjectWatchersPermission(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..9a13c6e7 100644
--- a/taiga/projects/serializers.py
+++ b/taiga/projects/serializers.py
@@ -25,6 +25,8 @@ from taiga.base.fields import PgArrayField
from taiga.base.fields import TagsField
from taiga.base.fields import TagsColorsField
+from taiga.projects.notifications.validators import WatchersValidator
+
from taiga.users.services import get_photo_or_gravatar_url
from taiga.users.serializers import UserSerializer
from taiga.users.serializers import UserBasicInfoSerializer
@@ -40,7 +42,8 @@ from .validators import ProjectExistsValidator
from .custom_attributes.serializers import UserStoryCustomAttributeSerializer
from .custom_attributes.serializers import TaskCustomAttributeSerializer
from .custom_attributes.serializers import IssueCustomAttributeSerializer
-
+from .notifications.mixins import WatchedResourceModelSerializer
+from .votes.mixins.serializers import StarredResourceSerializerMixin
######################################################
## Custom values for selectors
@@ -305,11 +308,10 @@ class ProjectMemberSerializer(serializers.ModelSerializer):
## Projects
######################################################
-class ProjectSerializer(serializers.ModelSerializer):
+class ProjectSerializer(WatchersValidator, StarredResourceSerializerMixin, WatchedResourceModelSerializer, 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 +323,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/signals.py b/taiga/projects/signals.py
index f6cb0b1a..6cfb0ddf 100644
--- a/taiga/projects/signals.py
+++ b/taiga/projects/signals.py
@@ -45,24 +45,6 @@ def membership_post_delete(sender, instance, using, **kwargs):
instance.project.update_role_points()
-def update_watchers_on_membership_post_delete(sender, instance, using, **kwargs):
- models = [apps.get_model("userstories", "UserStory"),
- apps.get_model("tasks", "Task"),
- apps.get_model("issues", "Issue")]
-
- # `user_id` is used beacuse in some momments
- # instance.user can contain pointer to now
- # removed object from a database.
- for model in models:
- #filter(project=instance.project)
- filter = {
- "user_id": instance.user_id,
- "%s__project"%(model._meta.model_name): instance.project,
- }
-
- model.watchers.through.objects.filter(**filter).delete()
-
-
def create_notify_policy(sender, instance, using, **kwargs):
if instance.user:
create_notify_policy_if_not_exists(instance.project, instance.user)
diff --git a/taiga/projects/tasks/admin.py b/taiga/projects/tasks/admin.py
index 937e70a3..5295e7ca 100644
--- a/taiga/projects/tasks/admin.py
+++ b/taiga/projects/tasks/admin.py
@@ -17,6 +17,9 @@
from django.contrib import admin
from taiga.projects.attachments.admin import AttachmentInline
+from taiga.projects.notifications.admin import WatchedInline
+from taiga.projects.votes.admin import VoteInline
+
from . import models
@@ -24,7 +27,7 @@ class TaskAdmin(admin.ModelAdmin):
list_display = ["project", "milestone", "user_story", "ref", "subject",]
list_display_links = ["ref", "subject",]
list_filter = ["project"]
- # inlines = [AttachmentInline]
+ inlines = [WatchedInline, VoteInline]
def get_object(self, *args, **kwargs):
self.obj = super().get_object(*args, **kwargs)
diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py
index 702d0a58..812458f1 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.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
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,12 +36,14 @@ 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_backends = (filters.CanViewTasksFilterBackend, filters.WatchersFilter)
+ retrieve_exclude_filters = (filters.WatchersFilter,)
filter_fields = ["user_story", "milestone", "project", "assigned_to",
- "status__is_closed", "watchers"]
+ "status__is_closed"]
def get_serializer_class(self, *args, **kwargs):
if self.action in ["retrieve", "by_ref"]:
@@ -82,6 +85,10 @@ class TaskViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
return super().update(request, *args, **kwargs)
+ def get_queryset(self):
+ qs = super().get_queryset()
+ qs = self.attach_votes_attrs_to_queryset(qs)
+ return self.attach_watchers_attrs_to_queryset(qs)
def pre_save(self, obj):
if obj.user_story:
@@ -165,3 +172,13 @@ 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
+
+
+class TaskWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
+ permission_classes = (permissions.TaskWatchersPermission,)
+ resource_model = models.Task
diff --git a/taiga/projects/tasks/migrations/0008_remove_task_watchers.py b/taiga/projects/tasks/migrations/0008_remove_task_watchers.py
new file mode 100644
index 00000000..4c934957
--- /dev/null
+++ b/taiga/projects/tasks/migrations/0008_remove_task_watchers.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import connection
+from django.db import models, migrations
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes.management import update_all_contenttypes
+
+def create_notifications(apps, schema_editor):
+ update_all_contenttypes()
+ sql="""
+INSERT INTO notifications_watched (object_id, created_date, content_type_id, user_id, project_id)
+SELECT task_id AS object_id, now() AS created_date, {content_type_id} AS content_type_id, user_id, project_id
+FROM tasks_task_watchers INNER JOIN tasks_task ON tasks_task_watchers.task_id = tasks_task.id""".format(content_type_id=ContentType.objects.get(model='task').id)
+ cursor = connection.cursor()
+ cursor.execute(sql)
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('notifications', '0004_watched'),
+ ('tasks', '0007_auto_20150629_1556'),
+ ]
+
+ operations = [
+ migrations.RunPython(create_notifications),
+ migrations.RemoveField(
+ model_name='task',
+ name='watchers',
+ ),
+ ]
diff --git a/taiga/projects/tasks/permissions.py b/taiga/projects/tasks/permissions.py
index 2c1fd7b0..cf12a283 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,21 @@ 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')
+ watch_perms = IsAuthenticated() & HasProjectPerm('view_tasks')
+ unwatch_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')
+
+
+class TaskWatchersPermission(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..221a188c 100644
--- a/taiga/projects/tasks/serializers.py
+++ b/taiga/projects/tasks/serializers.py
@@ -27,12 +27,15 @@ 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.notifications.mixins import WatchedResourceModelSerializer
+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, WatchedResourceModelSerializer, serializers.ModelSerializer):
tags = TagsField(required=False, default=[])
external_reference = PgArrayField(required=False)
comment = serializers.SerializerMethodField("get_comment")
diff --git a/taiga/projects/tasks/services.py b/taiga/projects/tasks/services.py
index 61225aff..8864d893 100644
--- a/taiga/projects/tasks/services.py
+++ b/taiga/projects/tasks/services.py
@@ -23,6 +23,7 @@ from taiga.projects.tasks.apps import (
connect_tasks_signals,
disconnect_tasks_signals)
from taiga.events import events
+from taiga.projects.votes import services as votes_services
from . import models
@@ -95,7 +96,8 @@ def tasks_to_csv(project, queryset):
fieldnames = ["ref", "subject", "description", "user_story", "milestone", "owner",
"owner_full_name", "assigned_to", "assigned_to_full_name",
"status", "is_iocaine", "is_closed", "us_order",
- "taskboard_order", "attachments", "external_reference", "tags"]
+ "taskboard_order", "attachments", "external_reference", "tags",
+ "watchers", "voters"]
for custom_attr in project.taskcustomattributes.all():
fieldnames.append(custom_attr.name)
@@ -120,6 +122,8 @@ def tasks_to_csv(project, queryset):
"attachments": task.attachments.count(),
"external_reference": task.external_reference,
"tags": ",".join(task.tags or []),
+ "watchers": [u.id for u in task.get_watchers()],
+ "voters": votes_services.get_voters(task).count(),
}
for custom_attr in project.taskcustomattributes.all():
value = task.custom_attributes_values.attributes_values.get(str(custom_attr.id), None)
diff --git a/taiga/projects/userstories/admin.py b/taiga/projects/userstories/admin.py
index cd23dc35..9cb3e667 100644
--- a/taiga/projects/userstories/admin.py
+++ b/taiga/projects/userstories/admin.py
@@ -17,6 +17,8 @@
from django.contrib import admin
from taiga.projects.attachments.admin import AttachmentInline
+from taiga.projects.notifications.admin import WatchedInline
+from taiga.projects.votes.admin import VoteInline
from . import models
@@ -41,7 +43,7 @@ class UserStoryAdmin(admin.ModelAdmin):
list_display = ["project", "milestone", "ref", "subject",]
list_display_links = ["ref", "subject",]
list_filter = ["project"]
- inlines = [RolePointsInline]
+ inlines = [RolePointsInline, WatchedInline, VoteInline]
def get_object(self, *args, **kwargs):
self.obj = super().get_object(*args, **kwargs)
diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py
index b364a53c..802e3e73 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.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
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,27 +44,29 @@ 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,
filters.AssignedToFilter,
filters.StatusesFilter,
filters.TagsFilter,
+ filters.WatchersFilter,
filters.QFilter,
filters.OrderByFilterMixin)
retrieve_exclude_filters = (filters.OwnersFilter,
filters.AssignedToFilter,
filters.StatusesFilter,
- filters.TagsFilter)
+ filters.TagsFilter,
+ filters.WatchersFilter)
filter_fields = ["project",
"milestone",
"milestone__isnull",
"is_closed",
"status__is_archived",
- "status__is_closed",
- "watchers"]
+ "status__is_closed"]
order_by_fields = ["backlog_order",
"sprint_order",
"kanban_order"]
@@ -109,13 +111,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")
+ "role_points__role")
qs = qs.select_related("milestone", "project")
- return qs
+ qs = self.attach_votes_attrs_to_queryset(qs)
+ return self.attach_watchers_attrs_to_queryset(qs)
def pre_save(self, obj):
# This is very ugly hack, but having
@@ -264,3 +266,12 @@ 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
+
+
+class UserStoryWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
+ permission_classes = (permissions.UserStoryWatchersPermission,)
+ resource_model = models.UserStory
diff --git a/taiga/projects/userstories/migrations/0010_remove_userstory_watchers.py b/taiga/projects/userstories/migrations/0010_remove_userstory_watchers.py
new file mode 100644
index 00000000..0d897aca
--- /dev/null
+++ b/taiga/projects/userstories/migrations/0010_remove_userstory_watchers.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import connection
+from django.db import models, migrations
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes.management import update_all_contenttypes
+
+def create_notifications(apps, schema_editor):
+ update_all_contenttypes()
+ sql="""
+INSERT INTO notifications_watched (object_id, created_date, content_type_id, user_id, project_id)
+SELECT userstory_id AS object_id, now() AS created_date, {content_type_id} AS content_type_id, user_id, project_id
+FROM userstories_userstory_watchers INNER JOIN userstories_userstory ON userstories_userstory_watchers.userstory_id = userstories_userstory.id""".format(content_type_id=ContentType.objects.get(model='userstory').id)
+ cursor = connection.cursor()
+ cursor.execute(sql)
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('notifications', '0004_watched'),
+ ('userstories', '0009_remove_userstory_is_archived'),
+ ]
+
+ operations = [
+ migrations.RunPython(create_notifications),
+ migrations.RemoveField(
+ model_name='userstory',
+ name='watchers',
+ ),
+ ]
diff --git a/taiga/projects/userstories/permissions.py b/taiga/projects/userstories/permissions.py
index 3aa548db..fb9361ab 100644
--- a/taiga/projects/userstories/permissions.py
+++ b/taiga/projects/userstories/permissions.py
@@ -30,3 +30,21 @@ 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')
+ watch_perms = IsAuthenticated() & HasProjectPerm('view_us')
+ unwatch_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')
+
+
+class UserStoryWatchersPermission(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..a461e57b 100644
--- a/taiga/projects/userstories/serializers.py
+++ b/taiga/projects/userstories/serializers.py
@@ -27,6 +27,9 @@ 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.notifications.mixins import WatchedResourceModelSerializer
+from taiga.projects.votes.mixins.serializers import VotedResourceSerializerMixin
+
from taiga.users.serializers import UserBasicInfoSerializer
from . import models
@@ -42,7 +45,7 @@ class RolePointsField(serializers.WritableField):
return json.loads(obj)
-class UserStorySerializer(WatchersValidator, serializers.ModelSerializer):
+class UserStorySerializer(WatchersValidator, VotedResourceSerializerMixin, WatchedResourceModelSerializer, serializers.ModelSerializer):
tags = TagsField(default=[], required=False)
external_reference = PgArrayField(required=False)
points = RolePointsField(source="role_points", required=False)
diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py
index 9d913707..c06309be 100644
--- a/taiga/projects/userstories/services.py
+++ b/taiga/projects/userstories/services.py
@@ -31,6 +31,7 @@ from taiga.projects.userstories.apps import (
disconnect_userstories_signals)
from taiga.events import events
+from taiga.projects.votes import services as votes_services
from . import models
@@ -138,7 +139,8 @@ def userstories_to_csv(project,queryset):
"created_date", "modified_date", "finish_date",
"client_requirement", "team_requirement", "attachments",
"generated_from_issue", "external_reference", "tasks",
- "tags"]
+ "tags",
+ "watchers", "voters"]
for custom_attr in project.userstorycustomattributes.all():
fieldnames.append(custom_attr.name)
@@ -170,6 +172,8 @@ def userstories_to_csv(project,queryset):
"external_reference": us.external_reference,
"tasks": ",".join([str(task.ref) for task in us.tasks.all()]),
"tags": ",".join(us.tags or []),
+ "watchers": [u.id for u in us.get_watchers()],
+ "voters": votes_services.get_voters(us).count(),
}
for role in us.project.roles.filter(computable=True).order_by('name'):
diff --git a/taiga/projects/votes/admin.py b/taiga/projects/votes/admin.py
new file mode 100644
index 00000000..4e06ae8f
--- /dev/null
+++ b/taiga/projects/votes/admin.py
@@ -0,0 +1,25 @@
+# Copyright (C) 2014 Andrey Antukh
+# Copyright (C) 2014 Jesús Espino
+# Copyright (C) 2014 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.contrib import admin
+from django.contrib.contenttypes.admin import GenericTabularInline
+
+from . import models
+
+
+class VoteInline(GenericTabularInline):
+ model = models.Vote
+ extra = 0
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..a1af1ff7 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")
@@ -61,4 +63,4 @@ class Vote(models.Model):
return None
def __str__(self):
- return self.user
+ return self.user.get_full_name()
diff --git a/taiga/projects/votes/serializers.py b/taiga/projects/votes/serializers.py
index c72ae91e..b6ab72a8 100644
--- a/taiga/projects/votes/serializers.py
+++ b/taiga/projects/votes/serializers.py
@@ -16,8 +16,10 @@
# along with this program. If not, see .
from taiga.base.api import serializers
+from taiga.base.fields import TagsField
from taiga.users.models import User
+from taiga.users.services import get_photo_or_gravatar_url
class VoterSerializer(serializers.ModelSerializer):
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/projects/wiki/admin.py b/taiga/projects/wiki/admin.py
index cb846105..ca929fb3 100644
--- a/taiga/projects/wiki/admin.py
+++ b/taiga/projects/wiki/admin.py
@@ -17,6 +17,9 @@
from django.contrib import admin
from taiga.projects.attachments.admin import AttachmentInline
+from taiga.projects.notifications.admin import WatchedInline
+from taiga.projects.votes.admin import VoteInline
+
from taiga.projects.wiki.models import WikiPage
from . import models
@@ -24,7 +27,7 @@ from . import models
class WikiPageAdmin(admin.ModelAdmin):
list_display = ["project", "slug", "owner"]
list_display_links = list_display
- # inlines = [AttachmentInline]
+ inlines = [WatchedInline, VoteInline]
admin.site.register(models.WikiPage, WikiPageAdmin)
diff --git a/taiga/projects/wiki/api.py b/taiga/projects/wiki/api.py
index a2e39eda..dba3ccb0 100644
--- a/taiga/projects/wiki/api.py
+++ b/taiga/projects/wiki/api.py
@@ -21,13 +21,13 @@ from taiga.base.api.permissions import IsAuthenticated
from taiga.base import filters
from taiga.base import exceptions as exc
from taiga.base import response
-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.base.decorators import list_route
from taiga.projects.models import Project
from taiga.mdrender.service import render as mdrender
-from taiga.projects.notifications.mixins import WatchedResourceMixin
+from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.occ import OCCResourceMixin
@@ -43,6 +43,12 @@ class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
permission_classes = (permissions.WikiPagePermission,)
filter_backends = (filters.CanViewWikiPagesFilterBackend,)
filter_fields = ("project", "slug")
+ queryset = models.WikiPage.objects.all()
+
+ def get_queryset(self):
+ qs = super().get_queryset()
+ qs = self.attach_watchers_attrs_to_queryset(qs)
+ return qs
@list_route(methods=["GET"])
def by_slug(self, request):
@@ -77,6 +83,11 @@ class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
super().pre_save(obj)
+class WikiWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
+ permission_classes = (permissions.WikiPageWatchersPermission,)
+ resource_model = models.WikiPage
+
+
class WikiLinkViewSet(ModelCrudViewSet):
model = models.WikiLink
serializer_class = serializers.WikiLinkSerializer
diff --git a/taiga/projects/wiki/migrations/0002_remove_wikipage_watchers.py b/taiga/projects/wiki/migrations/0002_remove_wikipage_watchers.py
new file mode 100644
index 00000000..f2cb8159
--- /dev/null
+++ b/taiga/projects/wiki/migrations/0002_remove_wikipage_watchers.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import connection
+from django.db import models, migrations
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes.management import update_all_contenttypes
+
+def create_notifications(apps, schema_editor):
+ update_all_contenttypes()
+ sql="""
+INSERT INTO notifications_watched (object_id, created_date, content_type_id, user_id, project_id)
+SELECT wikipage_id AS object_id, now() AS created_date, {content_type_id} AS content_type_id, user_id, project_id
+FROM wiki_wikipage_watchers INNER JOIN wiki_wikipage ON wiki_wikipage_watchers.wikipage_id = wiki_wikipage.id""".format(content_type_id=ContentType.objects.get(model='wikipage').id)
+ cursor = connection.cursor()
+ cursor.execute(sql)
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('notifications', '0004_watched'),
+ ('wiki', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.RunPython(create_notifications),
+ migrations.RemoveField(
+ model_name='wikipage',
+ name='watchers',
+ ),
+ ]
diff --git a/taiga/projects/wiki/permissions.py b/taiga/projects/wiki/permissions.py
index 684880a8..c64ac985 100644
--- a/taiga/projects/wiki/permissions.py
+++ b/taiga/projects/wiki/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 WikiPagePermission(TaigaResourcePermission):
@@ -29,6 +30,16 @@ class WikiPagePermission(TaigaResourcePermission):
destroy_perms = HasProjectPerm('delete_wiki_page')
list_perms = AllowAny()
render_perms = AllowAny()
+ watch_perms = IsAuthenticated() & HasProjectPerm('view_wiki_pages')
+ unwatch_perms = IsAuthenticated() & HasProjectPerm('view_wiki_pages')
+
+
+class WikiPageWatchersPermission(TaigaResourcePermission):
+ enought_perms = IsProjectOwner() | IsSuperUser()
+ global_perms = None
+ retrieve_perms = HasProjectPerm('view_wiki_pages')
+ list_perms = HasProjectPerm('view_wiki_pages')
+
class WikiLinkPermission(TaigaResourcePermission):
enought_perms = IsProjectOwner() | IsSuperUser()
diff --git a/taiga/projects/wiki/serializers.py b/taiga/projects/wiki/serializers.py
index 45d3c99b..22c9b8bd 100644
--- a/taiga/projects/wiki/serializers.py
+++ b/taiga/projects/wiki/serializers.py
@@ -15,15 +15,15 @@
# along with this program. If not, see .
from taiga.base.api import serializers
+from taiga.projects.history import services as history_service
+from taiga.projects.notifications.mixins import WatchedResourceModelSerializer
+from taiga.projects.notifications.validators import WatchersValidator
+from taiga.mdrender.service import render as mdrender
from . import models
-from taiga.projects.history import services as history_service
-from taiga.mdrender.service import render as mdrender
-
-
-class WikiPageSerializer(serializers.ModelSerializer):
+class WikiPageSerializer(WatchersValidator, WatchedResourceModelSerializer, serializers.ModelSerializer):
html = serializers.SerializerMethodField("get_html")
editions = serializers.SerializerMethodField("get_editions")
@@ -39,6 +39,5 @@ class WikiPageSerializer(serializers.ModelSerializer):
class WikiLinkSerializer(serializers.ModelSerializer):
-
class Meta:
model = models.WikiLink
diff --git a/taiga/routers.py b/taiga/routers.py
index 0f8bc675..5a587b59 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,8 @@ 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 ProjectWatchersViewSet
from taiga.projects.api import MembershipViewSet
from taiga.projects.api import InvitationViewSet
from taiga.projects.api import UserStoryStatusViewSet
@@ -61,6 +62,8 @@ 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"projects/(?P\d+)/watchers", ProjectWatchersViewSet, base_name="project-watchers")
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")
@@ -123,21 +126,38 @@ router.register(r"wiki/attachments", WikiAttachmentViewSet, base_name="wiki-atta
# Project components
from taiga.projects.milestones.api import MilestoneViewSet
+from taiga.projects.milestones.api import MilestoneWatchersViewSet
from taiga.projects.userstories.api import UserStoryViewSet
+from taiga.projects.userstories.api import UserStoryVotersViewSet
+from taiga.projects.userstories.api import UserStoryWatchersViewSet
from taiga.projects.tasks.api import TaskViewSet
+from taiga.projects.tasks.api import TaskVotersViewSet
+from taiga.projects.tasks.api import TaskWatchersViewSet
from taiga.projects.issues.api import IssueViewSet
-from taiga.projects.issues.api import VotersViewSet
-from taiga.projects.wiki.api import WikiViewSet, WikiLinkViewSet
+from taiga.projects.issues.api import IssueVotersViewSet
+from taiga.projects.issues.api import IssueWatchersViewSet
+from taiga.projects.wiki.api import WikiViewSet
+from taiga.projects.wiki.api import WikiLinkViewSet
+from taiga.projects.wiki.api import WikiWatchersViewSet
router.register(r"milestones", MilestoneViewSet, base_name="milestones")
+router.register(r"milestones/(?P\d+)/watchers", MilestoneWatchersViewSet, base_name="milestone-watchers")
router.register(r"userstories", UserStoryViewSet, base_name="userstories")
+router.register(r"userstories/(?P\d+)/voters", UserStoryVotersViewSet, base_name="userstory-voters")
+router.register(r"userstories/(?P\d+)/watchers", UserStoryWatchersViewSet, base_name="userstory-watchers")
router.register(r"tasks", TaskViewSet, base_name="tasks")
+router.register(r"tasks/(?P\d+)/voters", TaskVotersViewSet, base_name="task-voters")
+router.register(r"tasks/(?P\d+)/watchers", TaskWatchersViewSet, base_name="task-watchers")
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"issues/(?P\d+)/watchers", IssueWatchersViewSet, base_name="issue-watchers")
router.register(r"wiki", WikiViewSet, base_name="wiki")
+router.register(r"wiki/(?P\d+)/watchers", WikiWatchersViewSet, base_name="wiki-watchers")
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/timeline/signals.py b/taiga/timeline/signals.py
index 7769817d..5673fbbb 100644
--- a/taiga/timeline/signals.py
+++ b/taiga/timeline/signals.py
@@ -22,14 +22,12 @@ from taiga.projects.history import services as history_services
from taiga.projects.models import Project
from taiga.users.models import User
from taiga.projects.history.choices import HistoryType
+from taiga.projects.notifications import services as notifications_services
from taiga.timeline.service import (push_to_timeline,
build_user_namespace,
build_project_namespace,
extract_user_info)
-# TODO: Add events to followers timeline when followers are implemented.
-# TODO: Add events to project watchers timeline when project watchers are implemented.
-
def _push_to_timeline(*args, **kwargs):
if settings.CELERY_ENABLED:
@@ -60,9 +58,9 @@ def _push_to_timelines(project, user, obj, event_type, created_datetime, extra_d
related_people |= User.objects.filter(id=obj.assigned_to_id)
## - Watchers
- watchers = getattr(obj, "watchers", None)
+ watchers = notifications_services.get_watchers(obj)
if watchers:
- related_people |= obj.watchers.all()
+ related_people |= watchers
## - Exclude inactive and system users and remove duplicate
related_people = related_people.exclude(is_active=False)
diff --git a/taiga/users/api.py b/taiga/users/api.py
index 33ec137c..ea5e5bdc 100644
--- a/taiga/users/api.py
+++ b/taiga/users/api.py
@@ -114,6 +114,33 @@ class UsersViewSet(ModelCrudViewSet):
self.check_permissions(request, "stats", user)
return response.Ok(services.get_stats_for_user(user, request.user))
+ @detail_route(methods=["GET"])
+ def favourites(self, request, *args, **kwargs):
+ for_user = get_object_or_404(models.User, **kwargs)
+ from_user = request.user
+ self.check_permissions(request, 'favourites', for_user)
+ filters = {
+ "type": request.GET.get("type", None),
+ "action": request.GET.get("action", None),
+ "q": request.GET.get("q", None),
+ }
+
+ self.object_list = services.get_favourites_list(for_user, from_user, **filters)
+ page = self.paginate_queryset(self.object_list)
+
+ extra_args = {
+ "many": True,
+ "user_votes": services.get_voted_content_for_user(request.user),
+ "user_watching": services.get_watched_content_for_user(request.user),
+ }
+
+ if page is not None:
+ serializer = serializers.FavouriteSerializer(page.object_list, **extra_args)
+ else:
+ serializer = serializers.FavouriteSerializer(self.object_list, **extra_args)
+
+ return response.Ok(serializer.data)
+
@list_route(methods=["POST"])
def password_recovery(self, request, pk=None):
username_or_email = request.DATA.get('username', None)
@@ -224,15 +251,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/taiga/users/migrations/0012_auto_20150812_1142.py b/taiga/users/migrations/0012_auto_20150812_1142.py
new file mode 100644
index 00000000..fff8c17b
--- /dev/null
+++ b/taiga/users/migrations/0012_auto_20150812_1142.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import djorm_pgarray.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0011_user_theme'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='role',
+ name='permissions',
+ field=djorm_pgarray.fields.TextArrayField(choices=[('view_project', 'View project'), ('star_project', 'Star project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('delete_us', 'Delete user story'), ('vote_us', 'Vote user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('delete_task', 'Delete task'), ('vote_task', 'Vote task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('delete_issue', 'Delete issue'), ('vote_issue', 'Vote issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')], verbose_name='permissions', default=[], dbtype='text'),
+ preserve_default=True,
+ ),
+ ]
diff --git a/taiga/users/permissions.py b/taiga/users/permissions.py
index bad16f1a..63c54751 100644
--- a/taiga/users/permissions.py
+++ b/taiga/users/permissions.py
@@ -47,6 +47,7 @@ class UserPermission(TaigaResourcePermission):
starred_perms = AllowAny()
change_email_perms = AllowAny()
contacts_perms = AllowAny()
+ favourites_perms = AllowAny()
class RolesPermission(TaigaResourcePermission):
diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py
index 115462be..3158f5f8 100644
--- a/taiga/users/serializers.py
+++ b/taiga/users/serializers.py
@@ -19,11 +19,14 @@ from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from taiga.base.api import serializers
-from taiga.base.fields import PgArrayField
+from taiga.base.fields import PgArrayField, TagsField
+
from taiga.projects.models import Project
from .models import User, Role
from .services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url
+from collections import namedtuple
+
import re
@@ -149,3 +152,55 @@ class ProjectRoleSerializer(serializers.ModelSerializer):
model = Role
fields = ('id', 'name', 'slug', 'order', 'computable')
i18n_fields = ("name",)
+
+
+######################################################
+## Favourite
+######################################################
+
+
+class FavouriteSerializer(serializers.Serializer):
+ type = serializers.CharField()
+ action = serializers.CharField()
+ id = serializers.IntegerField()
+ ref = serializers.IntegerField()
+ slug = serializers.CharField()
+ subject = serializers.CharField()
+ tags = TagsField(default=[])
+ project = serializers.IntegerField()
+ assigned_to = serializers.IntegerField()
+ total_watchers = serializers.IntegerField()
+
+ is_voted = serializers.SerializerMethodField("get_is_voted")
+ is_watched = serializers.SerializerMethodField("get_is_watched")
+
+ created_date = serializers.DateTimeField()
+
+ project_name = serializers.CharField()
+ project_slug = serializers.CharField()
+ project_is_private = serializers.CharField()
+
+ assigned_to_username = serializers.CharField()
+ assigned_to_full_name = serializers.CharField()
+ assigned_to_photo = serializers.SerializerMethodField("get_photo")
+
+ total_votes = serializers.IntegerField()
+
+ def __init__(self, *args, **kwargs):
+ # Don't pass the extra ids args up to the superclass
+ self.user_votes = kwargs.pop("user_votes", {})
+ self.user_watching = kwargs.pop("user_watching", {})
+
+ # Instantiate the superclass normally
+ super(FavouriteSerializer, self).__init__(*args, **kwargs)
+
+ def get_is_voted(self, obj):
+ return obj["id"] in self.user_votes.get(obj["type"], [])
+
+ def get_is_watched(self, obj):
+ return obj["id"] in self.user_watching.get(obj["type"], [])
+
+ def get_photo(self, obj):
+ UserData = namedtuple("UserData", ["photo", "email"])
+ user_data = UserData(photo=obj["assigned_to_photo"], email=obj.get("assigned_to_email") or "")
+ return get_photo_or_gravatar_url(user_data)
diff --git a/taiga/users/services.py b/taiga/users/services.py
index caf27226..992f4ae8 100644
--- a/taiga/users/services.py
+++ b/taiga/users/services.py
@@ -20,6 +20,7 @@ This model contains a domain logic for users application.
from django.apps import apps
from django.db.models import Q
+from django.db import connection
from django.conf import settings
from django.utils.translation import ugettext as _
@@ -142,3 +143,173 @@ def get_stats_for_user(from_user, by_user):
'total_num_closed_userstories': total_num_closed_userstories,
}
return project_stats
+
+
+def get_voted_content_for_user(user):
+ """Returns a dict where:
+ - The key is the content_type model
+ - The values are list of id's of the different objects voted by the user
+ """
+ if user.is_anonymous():
+ return {}
+
+ user_votes = {}
+ for (ct_model, object_id) in user.votes.values_list("content_type__model", "object_id"):
+ list = user_votes.get(ct_model, [])
+ list.append(object_id)
+ user_votes[ct_model] = list
+
+ return user_votes
+
+
+def get_watched_content_for_user(user):
+ """Returns a dict where:
+ - The key is the content_type model
+ - The values are list of id's of the different objects watched by the user
+ """
+ if user.is_anonymous():
+ return {}
+
+ user_watches = {}
+ for (ct_model, object_id) in user.watched.values_list("content_type__model", "object_id"):
+ list = user_watches.get(ct_model, [])
+ list.append(object_id)
+ user_watches[ct_model] = list
+
+ return user_watches
+
+
+def _build_favourites_sql_for_type(for_user, type, table_name, ref_column="ref",
+ project_column="project_id", assigned_to_column="assigned_to_id",
+ slug_column="slug", subject_column="subject"):
+ sql = """
+ SELECT {table_name}.id AS id, {ref_column} AS ref, '{type}' AS type, 'watch' AS action,
+ tags, notifications_watched.object_id AS object_id, {table_name}.{project_column} AS project,
+ {slug_column} AS slug, {subject_column} AS subject,
+ notifications_watched.created_date, coalesce(watchers, 0) as total_watchers, coalesce(votes_votes.count, 0) total_votes, {assigned_to_column} AS assigned_to
+ FROM notifications_watched
+ INNER JOIN django_content_type
+ ON (notifications_watched.content_type_id = django_content_type.id AND django_content_type.model = '{type}')
+ INNER JOIN {table_name}
+ ON ({table_name}.id = notifications_watched.object_id)
+ LEFT JOIN (SELECT object_id, content_type_id, count(*) watchers FROM notifications_watched GROUP BY object_id, content_type_id) type_watchers
+ ON {table_name}.id = type_watchers.object_id AND django_content_type.id = type_watchers.content_type_id
+ LEFT JOIN votes_votes
+ ON ({table_name}.id = votes_votes.object_id AND django_content_type.id = votes_votes.content_type_id)
+ WHERE notifications_watched.user_id = {for_user_id}
+ UNION
+ SELECT {table_name}.id AS id, {ref_column} AS ref, '{type}' AS type, 'vote' AS action,
+ tags, votes_vote.object_id AS object_id, {table_name}.{project_column} AS project,
+ {slug_column} AS slug, {subject_column} AS subject,
+ votes_vote.created_date, coalesce(watchers, 0) as total_watchers, votes_votes.count total_votes, {assigned_to_column} AS assigned_to
+ FROM votes_vote
+ INNER JOIN django_content_type
+ ON (votes_vote.content_type_id = django_content_type.id AND django_content_type.model = '{type}')
+ INNER JOIN {table_name}
+ ON ({table_name}.id = votes_vote.object_id)
+ LEFT JOIN (SELECT object_id, content_type_id, count(*) watchers FROM notifications_watched GROUP BY object_id, content_type_id) type_watchers
+ ON {table_name}.id = type_watchers.object_id AND django_content_type.id = type_watchers.content_type_id
+ LEFT JOIN votes_votes
+ ON ({table_name}.id = votes_votes.object_id AND django_content_type.id = votes_votes.content_type_id)
+ WHERE votes_vote.user_id = {for_user_id}
+ """
+ sql = sql.format(for_user_id=for_user.id, type=type, table_name=table_name,
+ ref_column = ref_column, project_column=project_column,
+ assigned_to_column=assigned_to_column, slug_column=slug_column,
+ subject_column=subject_column)
+ return sql
+
+
+def get_favourites_list(for_user, from_user, type=None, action=None, q=None):
+ filters_sql = ""
+ and_needed = False
+
+ if type:
+ filters_sql += " AND type = '{type}' ".format(type=type)
+
+ if action:
+ filters_sql += " AND action = '{action}' ".format(action=action)
+
+ if q:
+ filters_sql += " AND to_tsvector(coalesce(subject, '')) @@ plainto_tsquery('{q}') ".format(q=q)
+
+ sql = """
+ -- BEGIN Basic info: we need to mix info from different tables and denormalize it
+ SELECT entities.*,
+ projects_project.name as project_name, projects_project.slug as project_slug, projects_project.is_private as project_is_private,
+ users_user.username assigned_to_username, users_user.full_name assigned_to_full_name, users_user.photo assigned_to_photo, users_user.email assigned_to_email
+ FROM (
+ {userstories_sql}
+ UNION
+ {tasks_sql}
+ UNION
+ {issues_sql}
+ UNION
+ {projects_sql}
+ ) as entities
+ -- END Basic info
+
+ -- BEGIN Project info
+ LEFT JOIN projects_project
+ ON (entities.project = projects_project.id)
+ -- END Project info
+
+ -- BEGIN Assigned to user info
+ LEFT JOIN users_user
+ ON (assigned_to = users_user.id)
+ -- END Assigned to user info
+
+ -- BEGIN Permissions checking
+ LEFT JOIN projects_membership
+ -- Here we check the memberbships from the user requesting the info
+ ON (projects_membership.user_id = {from_user_id} AND projects_membership.project_id = entities.project)
+
+ LEFT JOIN users_role
+ ON (entities.project = users_role.project_id AND users_role.id = projects_membership.role_id)
+
+ WHERE
+ -- public project
+ (
+ projects_project.is_private = false
+ OR(
+ -- private project where the view_ permission is included in the user role for that project or in the anon permissions
+ projects_project.is_private = true
+ AND(
+ (entities.type = 'issue' AND 'view_issues' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions)))
+ OR (entities.type = 'task' AND 'view_tasks' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions)))
+ OR (entities.type = 'userstory' AND 'view_us' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions)))
+ OR (entities.type = 'project' AND 'view_project' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions)))
+ )
+ ))
+ -- END Permissions checking
+ {filters_sql}
+
+ ORDER BY entities.created_date;
+ """
+
+ from_user_id = -1
+ if not from_user.is_anonymous():
+ from_user_id = from_user.id
+
+ sql = sql.format(
+ for_user_id=for_user.id,
+ from_user_id=from_user_id,
+ filters_sql=filters_sql,
+ userstories_sql=_build_favourites_sql_for_type(for_user, "userstory", "userstories_userstory", slug_column="null"),
+ tasks_sql=_build_favourites_sql_for_type(for_user, "task", "tasks_task", slug_column="null"),
+ issues_sql=_build_favourites_sql_for_type(for_user, "issue", "issues_issue", slug_column="null"),
+ projects_sql=_build_favourites_sql_for_type(for_user, "project", "projects_project",
+ ref_column="null",
+ project_column="id",
+ assigned_to_column="null",
+ subject_column="projects_project.name")
+ )
+
+ cursor = connection.cursor()
+ cursor.execute(sql)
+
+ desc = cursor.description
+ return [
+ dict(zip([col[0] for col in desc], row))
+ for row in cursor.fetchall()
+ ]
diff --git a/taiga/webhooks/serializers.py b/taiga/webhooks/serializers.py
index e802c1b3..47d0a145 100644
--- a/taiga/webhooks/serializers.py
+++ b/taiga/webhooks/serializers.py
@@ -23,8 +23,9 @@ from taiga.projects.userstories import models as us_models
from taiga.projects.tasks import models as task_models
from taiga.projects.issues import models as issue_models
from taiga.projects.milestones import models as milestone_models
-from taiga.projects.history import models as history_models
from taiga.projects.wiki import models as wiki_models
+from taiga.projects.history import models as history_models
+from taiga.projects.notifications.mixins import WatchedResourceModelSerializer
from .models import Webhook, WebhookLog
@@ -103,7 +104,8 @@ class PointSerializer(serializers.Serializer):
return obj.value
-class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.ModelSerializer):
+class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, WatchedResourceModelSerializer,
+ serializers.ModelSerializer):
tags = TagsField(default=[], required=False)
external_reference = PgArrayField(required=False)
owner = UserSerializer()
@@ -119,7 +121,8 @@ class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, serializ
return project.userstorycustomattributes.all()
-class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.ModelSerializer):
+class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, WatchedResourceModelSerializer,
+ serializers.ModelSerializer):
tags = TagsField(default=[], required=False)
owner = UserSerializer()
assigned_to = UserSerializer()
@@ -132,7 +135,8 @@ class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.M
return project.taskcustomattributes.all()
-class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.ModelSerializer):
+class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, WatchedResourceModelSerializer,
+ serializers.ModelSerializer):
tags = TagsField(default=[], required=False)
owner = UserSerializer()
assigned_to = UserSerializer()
diff --git a/tests/factories.py b/tests/factories.py
index 8a351d49..a0950733 100644
--- a/tests/factories.py
+++ b/tests/factories.py
@@ -441,6 +441,17 @@ class VotesFactory(Factory):
object_id = factory.Sequence(lambda n: n)
+class WatchedFactory(Factory):
+ class Meta:
+ model = "notifications.Watched"
+ strategy = factory.CREATE_STRATEGY
+
+ content_type = factory.SubFactory("tests.factories.ContentTypeFactory")
+ object_id = factory.Sequence(lambda n: n)
+ user = factory.SubFactory("tests.factories.UserFactory")
+ project = factory.SubFactory("tests.factories.ProjectFactory")
+
+
class ContentTypeFactory(Factory):
class Meta:
model = "contenttypes.ContentType"
diff --git a/tests/integration/resources_permissions/test_issues_resources.py b/tests/integration/resources_permissions/test_issues_resources.py
index 43abdeac..469efacc 100644
--- a/tests/integration/resources_permissions/test_issues_resources.py
+++ b/tests/integration/resources_permissions/test_issues_resources.py
@@ -9,6 +9,7 @@ from taiga.base.utils import json
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 taiga.projects.notifications.services import add_watcher
from taiga.projects.occ import OCCResourceMixin
from unittest import mock
@@ -477,12 +478,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 +499,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 +520,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 +547,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]
@@ -579,3 +575,93 @@ def test_issues_csv(client, data):
results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users)
assert results == [200, 200, 200, 200, 200]
+
+
+def test_issue_action_watch(client, data):
+ public_url = reverse('issues-watch', kwargs={"pk": data.public_issue.pk})
+ private_url1 = reverse('issues-watch', kwargs={"pk": data.private_issue1.pk})
+ private_url2 = reverse('issues-watch', kwargs={"pk": data.private_issue2.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_issue_action_unwatch(client, data):
+ public_url = reverse('issues-unwatch', kwargs={"pk": data.public_issue.pk})
+ private_url1 = reverse('issues-unwatch', kwargs={"pk": data.private_issue1.pk})
+ private_url2 = reverse('issues-unwatch', kwargs={"pk": data.private_issue2.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_issue_watchers_list(client, data):
+ public_url = reverse('issue-watchers-list', kwargs={"resource_id": data.public_issue.pk})
+ private_url1 = reverse('issue-watchers-list', kwargs={"resource_id": data.private_issue1.pk})
+ private_url2 = reverse('issue-watchers-list', kwargs={"resource_id": data.private_issue2.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_issue_watchers_retrieve(client, data):
+ add_watcher(data.public_issue, data.project_owner)
+ public_url = reverse('issue-watchers-detail', kwargs={"resource_id": data.public_issue.pk,
+ "pk": data.project_owner.pk})
+ add_watcher(data.private_issue1, data.project_owner)
+ private_url1 = reverse('issue-watchers-detail', kwargs={"resource_id": data.private_issue1.pk,
+ "pk": data.project_owner.pk})
+ add_watcher(data.private_issue2, data.project_owner)
+ private_url2 = reverse('issue-watchers-detail', kwargs={"resource_id": data.private_issue2.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]
diff --git a/tests/integration/resources_permissions/test_milestones_resources.py b/tests/integration/resources_permissions/test_milestones_resources.py
index 955754f9..40a8c008 100644
--- a/tests/integration/resources_permissions/test_milestones_resources.py
+++ b/tests/integration/resources_permissions/test_milestones_resources.py
@@ -3,6 +3,7 @@ from django.core.urlresolvers import reverse
from taiga.base.utils import json
from taiga.projects.milestones.serializers import MilestoneSerializer
from taiga.projects.milestones.models import Milestone
+from taiga.projects.notifications.services import add_watcher
from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
from tests import factories as f
@@ -274,3 +275,93 @@ def test_milestone_action_stats(client, data):
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]
+
+
+def test_milestone_action_watch(client, data):
+ public_url = reverse('milestones-watch', kwargs={"pk": data.public_milestone.pk})
+ private_url1 = reverse('milestones-watch', kwargs={"pk": data.private_milestone1.pk})
+ private_url2 = reverse('milestones-watch', kwargs={"pk": data.private_milestone2.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_milestone_action_unwatch(client, data):
+ public_url = reverse('milestones-unwatch', kwargs={"pk": data.public_milestone.pk})
+ private_url1 = reverse('milestones-unwatch', kwargs={"pk": data.private_milestone1.pk})
+ private_url2 = reverse('milestones-unwatch', kwargs={"pk": data.private_milestone2.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_milestone_watchers_list(client, data):
+ public_url = reverse('milestone-watchers-list', kwargs={"resource_id": data.public_milestone.pk})
+ private_url1 = reverse('milestone-watchers-list', kwargs={"resource_id": data.private_milestone1.pk})
+ private_url2 = reverse('milestone-watchers-list', kwargs={"resource_id": data.private_milestone2.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_milestone_watchers_retrieve(client, data):
+ add_watcher(data.public_milestone, data.project_owner)
+ public_url = reverse('milestone-watchers-detail', kwargs={"resource_id": data.public_milestone.pk,
+ "pk": data.project_owner.pk})
+ add_watcher(data.private_milestone1, data.project_owner)
+ private_url1 = reverse('milestone-watchers-detail', kwargs={"resource_id": data.private_milestone1.pk,
+ "pk": data.project_owner.pk})
+ add_watcher(data.private_milestone2, data.project_owner)
+ private_url2 = reverse('milestone-watchers-detail', kwargs={"resource_id": data.private_milestone2.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]
diff --git a/tests/integration/resources_permissions/test_projects_resource.py b/tests/integration/resources_permissions/test_projects_resource.py
index 575e0d62..27c08d1f 100644
--- a/tests/integration/resources_permissions/test_projects_resource.py
+++ b/tests/integration/resources_permissions/test_projects_resource.py
@@ -81,6 +81,13 @@ def data():
f.VotesFactory(content_type=project_ct, object_id=m.private_project1.pk, count=2)
f.VotesFactory(content_type=project_ct, object_id=m.private_project2.pk, count=2)
+ f.WatchedFactory(content_type=project_ct, object_id=m.public_project.pk, user=m.project_member_with_perms)
+ f.WatchedFactory(content_type=project_ct, object_id=m.public_project.pk, user=m.project_owner)
+ f.WatchedFactory(content_type=project_ct, object_id=m.private_project1.pk, user=m.project_member_with_perms)
+ f.WatchedFactory(content_type=project_ct, object_id=m.private_project1.pk, user=m.project_owner)
+ f.WatchedFactory(content_type=project_ct, object_id=m.private_project2.pk, user=m.project_member_with_perms)
+ f.WatchedFactory(content_type=project_ct, object_id=m.private_project2.pk, user=m.project_owner)
+
return m
@@ -109,6 +116,7 @@ def test_project_update(client, data):
project_data = ProjectDetailSerializer(data.private_project2).data
project_data["is_private"] = False
+
project_data = json.dumps(project_data)
users = [
@@ -198,6 +206,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 +263,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 +281,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 +300,57 @@ 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_watchers_list(client, data):
+ public_url = reverse('project-watchers-list', kwargs={"resource_id": data.public_project.pk})
+ private1_url = reverse('project-watchers-list', kwargs={"resource_id": data.private_project1.pk})
+ private2_url = reverse('project-watchers-list', kwargs={"resource_id": data.private_project2.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method_and_count(client, 'get', public_url, None, users)
+ assert results == [(200, 2), (200, 2), (200, 2), (200, 2), (200, 2)]
+ 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 == [(401, 0), (403, 0), (403, 0), (200, 2), (200, 2)]
+
+
+def test_project_watchers_retrieve(client, data):
+ public_url = reverse('project-watchers-detail', kwargs={"resource_id": data.public_project.pk,
+ "pk": data.project_owner.pk})
+ private1_url = reverse('project-watchers-detail', kwargs={"resource_id": data.private_project1.pk,
+ "pk": data.project_owner.pk})
+ private2_url = reverse('project-watchers-detail', kwargs={"resource_id": data.private_project2.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', 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):
@@ -413,3 +469,41 @@ def test_regenerate_issues_csv_uuid(client, data):
results = helper_test_http_method(client, 'post', private2_url, None, users)
assert results == [404, 404, 403, 200]
+
+
+def test_project_action_watch(client, data):
+ public_url = reverse('projects-watch', kwargs={"pk": data.public_project.pk})
+ private1_url = reverse('projects-watch', kwargs={"pk": data.private_project1.pk})
+ private2_url = reverse('projects-watch', 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, 'post', public_url, None, users)
+ assert results == [401, 200, 200, 200]
+ results = helper_test_http_method(client, 'post', private1_url, None, users)
+ assert results == [401, 200, 200, 200]
+ results = helper_test_http_method(client, 'post', private2_url, None, users)
+ assert results == [404, 404, 200, 200]
+
+
+def test_project_action_unwatch(client, data):
+ public_url = reverse('projects-unwatch', kwargs={"pk": data.public_project.pk})
+ private1_url = reverse('projects-unwatch', kwargs={"pk": data.private_project1.pk})
+ private2_url = reverse('projects-unwatch', 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, 'post', public_url, None, users)
+ assert results == [401, 200, 200, 200]
+ results = helper_test_http_method(client, 'post', private1_url, None, users)
+ assert results == [401, 200, 200, 200]
+ results = helper_test_http_method(client, 'post', private2_url, None, users)
+ assert results == [404, 404, 200, 200]
diff --git a/tests/integration/resources_permissions/test_tasks_resources.py b/tests/integration/resources_permissions/test_tasks_resources.py
index 9be43c09..4a871e8e 100644
--- a/tests/integration/resources_permissions/test_tasks_resources.py
+++ b/tests/integration/resources_permissions/test_tasks_resources.py
@@ -9,6 +9,8 @@ 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 taiga.projects.notifications.services import add_watcher
from unittest import mock
@@ -416,6 +418,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
@@ -438,3 +530,93 @@ def test_tasks_csv(client, data):
results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users)
assert results == [200, 200, 200, 200, 200]
+
+
+def test_task_action_watch(client, data):
+ public_url = reverse('tasks-watch', kwargs={"pk": data.public_task.pk})
+ private_url1 = reverse('tasks-watch', kwargs={"pk": data.private_task1.pk})
+ private_url2 = reverse('tasks-watch', 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_unwatch(client, data):
+ public_url = reverse('tasks-unwatch', kwargs={"pk": data.public_task.pk})
+ private_url1 = reverse('tasks-unwatch', kwargs={"pk": data.private_task1.pk})
+ private_url2 = reverse('tasks-unwatch', 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_watchers_list(client, data):
+ public_url = reverse('task-watchers-list', kwargs={"resource_id": data.public_task.pk})
+ private_url1 = reverse('task-watchers-list', kwargs={"resource_id": data.private_task1.pk})
+ private_url2 = reverse('task-watchers-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_watchers_retrieve(client, data):
+ add_watcher(data.public_task, data.project_owner)
+ public_url = reverse('task-watchers-detail', kwargs={"resource_id": data.public_task.pk,
+ "pk": data.project_owner.pk})
+ add_watcher(data.private_task1, data.project_owner)
+ private_url1 = reverse('task-watchers-detail', kwargs={"resource_id": data.private_task1.pk,
+ "pk": data.project_owner.pk})
+ add_watcher(data.private_task2, data.project_owner)
+ private_url2 = reverse('task-watchers-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]
diff --git a/tests/integration/resources_permissions/test_users_resources.py b/tests/integration/resources_permissions/test_users_resources.py
index fada3a72..761439d1 100644
--- a/tests/integration/resources_permissions/test_users_resources.py
+++ b/tests/integration/resources_permissions/test_users_resources.py
@@ -287,3 +287,15 @@ def test_user_action_change_email(client, data):
after_each_request()
results = helper_test_http_method(client, 'post', url, patch_data, users, after_each_request=after_each_request)
assert results == [204, 204, 204]
+
+
+def test_user_list_votes(client, data):
+ url = reverse('users-favourites', kwargs={"pk": data.registered_user.pk})
+ users = [
+ None,
+ data.registered_user,
+ data.other_user,
+ data.superuser,
+ ]
+ results = helper_test_http_method(client, 'get', url, None, users)
+ assert results == [200, 200, 200, 200]
diff --git a/tests/integration/resources_permissions/test_userstories_resources.py b/tests/integration/resources_permissions/test_userstories_resources.py
index f41d84ee..20881aed 100644
--- a/tests/integration/resources_permissions/test_userstories_resources.py
+++ b/tests/integration/resources_permissions/test_userstories_resources.py
@@ -9,6 +9,8 @@ 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 taiga.projects.notifications.services import add_watcher
from unittest import mock
@@ -415,6 +417,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')
@@ -438,3 +529,93 @@ def test_user_stories_csv(client, data):
results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users)
assert results == [200, 200, 200, 200, 200]
+
+
+def test_user_story_action_watch(client, data):
+ public_url = reverse('userstories-watch', kwargs={"pk": data.public_user_story.pk})
+ private_url1 = reverse('userstories-watch', kwargs={"pk": data.private_user_story1.pk})
+ private_url2 = reverse('userstories-watch', 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_unwatch(client, data):
+ public_url = reverse('userstories-unwatch', kwargs={"pk": data.public_user_story.pk})
+ private_url1 = reverse('userstories-unwatch', kwargs={"pk": data.private_user_story1.pk})
+ private_url2 = reverse('userstories-unwatch', 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_userstory_watchers_list(client, data):
+ public_url = reverse('userstory-watchers-list', kwargs={"resource_id": data.public_user_story.pk})
+ private_url1 = reverse('userstory-watchers-list', kwargs={"resource_id": data.private_user_story1.pk})
+ private_url2 = reverse('userstory-watchers-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_userstory_watchers_retrieve(client, data):
+ add_watcher(data.public_user_story, data.project_owner)
+ public_url = reverse('userstory-watchers-detail', kwargs={"resource_id": data.public_user_story.pk,
+ "pk": data.project_owner.pk})
+ add_watcher(data.private_user_story1, data.project_owner)
+ private_url1 = reverse('userstory-watchers-detail', kwargs={"resource_id": data.private_user_story1.pk,
+ "pk": data.project_owner.pk})
+ add_watcher(data.private_user_story2, data.project_owner)
+ private_url2 = reverse('userstory-watchers-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]
diff --git a/tests/integration/resources_permissions/test_wiki_resources.py b/tests/integration/resources_permissions/test_wiki_resources.py
index cf6089b7..14f2f92b 100644
--- a/tests/integration/resources_permissions/test_wiki_resources.py
+++ b/tests/integration/resources_permissions/test_wiki_resources.py
@@ -1,10 +1,11 @@
from django.core.urlresolvers import reverse
from taiga.base.utils import json
+from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
+from taiga.projects.notifications.services import add_watcher
+from taiga.projects.occ import OCCResourceMixin
from taiga.projects.wiki.serializers import WikiPageSerializer, WikiLinkSerializer
from taiga.projects.wiki.models import WikiPage, WikiLink
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
-from taiga.projects.occ import OCCResourceMixin
from tests import factories as f
from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals
@@ -436,3 +437,93 @@ def test_wiki_link_patch(client, data):
patch_data = json.dumps({"title": "test"})
results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
assert results == [401, 403, 403, 200, 200]
+
+
+def test_wikipage_action_watch(client, data):
+ public_url = reverse('wiki-watch', kwargs={"pk": data.public_wiki_page.pk})
+ private_url1 = reverse('wiki-watch', kwargs={"pk": data.private_wiki_page1.pk})
+ private_url2 = reverse('wiki-watch', kwargs={"pk": data.private_wiki_page2.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_wikipage_action_unwatch(client, data):
+ public_url = reverse('wiki-unwatch', kwargs={"pk": data.public_wiki_page.pk})
+ private_url1 = reverse('wiki-unwatch', kwargs={"pk": data.private_wiki_page1.pk})
+ private_url2 = reverse('wiki-unwatch', kwargs={"pk": data.private_wiki_page2.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_wikipage_watchers_list(client, data):
+ public_url = reverse('wiki-watchers-list', kwargs={"resource_id": data.public_wiki_page.pk})
+ private_url1 = reverse('wiki-watchers-list', kwargs={"resource_id": data.private_wiki_page1.pk})
+ private_url2 = reverse('wiki-watchers-list', kwargs={"resource_id": data.private_wiki_page2.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_wikipage_watchers_retrieve(client, data):
+ add_watcher(data.public_wiki_page, data.project_owner)
+ public_url = reverse('wiki-watchers-detail', kwargs={"resource_id": data.public_wiki_page.pk,
+ "pk": data.project_owner.pk})
+ add_watcher(data.private_wiki_page1, data.project_owner)
+ private_url1 = reverse('wiki-watchers-detail', kwargs={"resource_id": data.private_wiki_page1.pk,
+ "pk": data.project_owner.pk})
+ add_watcher(data.private_wiki_page2, data.project_owner)
+ private_url2 = reverse('wiki-watchers-detail', kwargs={"resource_id": data.private_wiki_page2.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]
diff --git a/tests/integration/test_importer_api.py b/tests/integration/test_importer_api.py
index 96143112..073d7585 100644
--- a/tests/integration/test_importer_api.py
+++ b/tests/integration/test_importer_api.py
@@ -47,13 +47,15 @@ def test_invalid_project_import(client):
def test_valid_project_import_without_extra_data(client):
user = f.UserFactory.create()
+ user_watching = f.UserFactory.create(email="testing@taiga.io")
client.login(user)
url = reverse("importer-list")
data = {
"name": "Imported project",
"description": "Imported project",
- "roles": [{"name": "Role"}]
+ "roles": [{"name": "Role"}],
+ "watchers": ["testing@taiga.io"]
}
response = client.post(url, json.dumps(data), content_type="application/json")
@@ -66,6 +68,7 @@ def test_valid_project_import_without_extra_data(client):
]
assert all(map(lambda x: len(response_data[x]) == 0, must_empty_children))
assert response_data["owner"] == user.email
+ assert response_data["watchers"] == [user_watching.email]
def test_valid_project_import_with_not_existing_memberships(client):
@@ -383,6 +386,7 @@ def test_valid_issue_import_with_custom_attributes_values(client):
def test_valid_issue_import_with_extra_data(client):
user = f.UserFactory.create()
+ user_watching = f.UserFactory.create(email="testing@taiga.io")
project = f.ProjectFactory.create(owner=user)
f.MembershipFactory(project=project, user=user, is_owner=True)
project.default_issue_type = f.IssueTypeFactory.create(project=project)
@@ -403,7 +407,8 @@ def test_valid_issue_import_with_extra_data(client):
"name": "imported attachment",
"data": base64.b64encode(b"TEST").decode("utf-8")
}
- }]
+ }],
+ "watchers": ["testing@taiga.io"]
}
response = client.post(url, json.dumps(data), content_type="application/json")
@@ -413,6 +418,7 @@ def test_valid_issue_import_with_extra_data(client):
assert response_data["owner"] == user.email
assert response_data["ref"] is not None
assert response_data["finished_date"] == "2014-10-24T00:00:00+0000"
+ assert response_data["watchers"] == [user_watching.email]
def test_invalid_issue_import_with_extra_data(client):
@@ -535,6 +541,7 @@ def test_valid_us_import_without_extra_data(client):
def test_valid_us_import_with_extra_data(client):
user = f.UserFactory.create()
+ user_watching = f.UserFactory.create(email="testing@taiga.io")
project = f.ProjectFactory.create(owner=user)
f.MembershipFactory(project=project, user=user, is_owner=True)
project.default_us_status = f.UserStoryStatusFactory.create(project=project)
@@ -551,7 +558,8 @@ def test_valid_us_import_with_extra_data(client):
"name": "imported attachment",
"data": base64.b64encode(b"TEST").decode("utf-8")
}
- }]
+ }],
+ "watchers": ["testing@taiga.io"]
}
response = client.post(url, json.dumps(data), content_type="application/json")
@@ -560,6 +568,7 @@ def test_valid_us_import_with_extra_data(client):
assert len(response_data["attachments"]) == 1
assert response_data["owner"] == user.email
assert response_data["ref"] is not None
+ assert response_data["watchers"] == [user_watching.email]
def test_invalid_us_import_with_extra_data(client):
@@ -664,6 +673,7 @@ def test_valid_task_import_with_custom_attributes_values(client):
def test_valid_task_import_with_extra_data(client):
user = f.UserFactory.create()
+ user_watching = f.UserFactory.create(email="testing@taiga.io")
project = f.ProjectFactory.create(owner=user)
f.MembershipFactory(project=project, user=user, is_owner=True)
project.default_task_status = f.TaskStatusFactory.create(project=project)
@@ -680,7 +690,8 @@ def test_valid_task_import_with_extra_data(client):
"name": "imported attachment",
"data": base64.b64encode(b"TEST").decode("utf-8")
}
- }]
+ }],
+ "watchers": ["testing@taiga.io"]
}
response = client.post(url, json.dumps(data), content_type="application/json")
@@ -689,6 +700,7 @@ def test_valid_task_import_with_extra_data(client):
assert len(response_data["attachments"]) == 1
assert response_data["owner"] == user.email
assert response_data["ref"] is not None
+ assert response_data["watchers"] == [user_watching.email]
def test_invalid_task_import_with_extra_data(client):
@@ -787,6 +799,7 @@ def test_valid_wiki_page_import_without_extra_data(client):
def test_valid_wiki_page_import_with_extra_data(client):
user = f.UserFactory.create()
+ user_watching = f.UserFactory.create(email="testing@taiga.io")
project = f.ProjectFactory.create(owner=user)
f.MembershipFactory(project=project, user=user, is_owner=True)
client.login(user)
@@ -801,7 +814,8 @@ def test_valid_wiki_page_import_with_extra_data(client):
"name": "imported attachment",
"data": base64.b64encode(b"TEST").decode("utf-8")
}
- }]
+ }],
+ "watchers": ["testing@taiga.io"]
}
response = client.post(url, json.dumps(data), content_type="application/json")
@@ -809,6 +823,7 @@ def test_valid_wiki_page_import_with_extra_data(client):
response_data = response.data
assert len(response_data["attachments"]) == 1
assert response_data["owner"] == user.email
+ assert response_data["watchers"] == [user_watching.email]
def test_invalid_wiki_page_import_with_extra_data(client):
@@ -877,6 +892,7 @@ def test_invalid_milestone_import(client):
def test_valid_milestone_import(client):
user = f.UserFactory.create()
+ user_watching = f.UserFactory.create(email="testing@taiga.io")
project = f.ProjectFactory.create(owner=user)
f.MembershipFactory(project=project, user=user, is_owner=True)
client.login(user)
@@ -886,11 +902,12 @@ def test_valid_milestone_import(client):
"name": "Imported milestone",
"estimated_start": "2014-10-10",
"estimated_finish": "2014-10-20",
+ "watchers": ["testing@taiga.io"]
}
response = client.post(url, json.dumps(data), content_type="application/json")
assert response.status_code == 201
- response.data
+ assert response.data["watchers"] == [user_watching.email]
diff --git a/tests/integration/test_issues.py b/tests/integration/test_issues.py
index 84727427..54722c93 100644
--- a/tests/integration/test_issues.py
+++ b/tests/integration/test_issues.py
@@ -412,6 +412,6 @@ def test_custom_fields_csv_generation():
data.seek(0)
reader = csv.reader(data)
row = next(reader)
- assert row[16] == attr.name
+ assert row[18] == attr.name
row = next(reader)
- assert row[16] == "val1"
+ assert row[18] == "val1"
diff --git a/tests/integration/test_notifications.py b/tests/integration/test_notifications.py
index a5a9847c..87ea400d 100644
--- a/tests/integration/test_notifications.py
+++ b/tests/integration/test_notifications.py
@@ -97,7 +97,7 @@ def test_analize_object_for_watchers():
history.comment = ""
services.analize_object_for_watchers(issue, history)
- assert issue.watchers.add.call_count == 2
+ assert issue.add_watcher.call_count == 2
def test_analize_object_for_watchers_adding_owner_non_empty_comment():
@@ -112,7 +112,7 @@ def test_analize_object_for_watchers_adding_owner_non_empty_comment():
history.owner = user1
services.analize_object_for_watchers(issue, history)
- assert issue.watchers.add.call_count == 1
+ assert issue.add_watcher.call_count == 1
def test_analize_object_for_watchers_no_adding_owner_empty_comment():
@@ -127,7 +127,7 @@ def test_analize_object_for_watchers_no_adding_owner_empty_comment():
history.owner = user1
services.analize_object_for_watchers(issue, history)
- assert issue.watchers.add.call_count == 0
+ assert issue.add_watcher.call_count == 0
def test_users_to_notify():
@@ -180,7 +180,7 @@ def test_users_to_notify():
assert users == {member1.user, issue.get_owner()}
# Test with watchers
- issue.watchers.add(member3.user)
+ issue.add_watcher(member3.user)
users = services.get_users_to_notify(issue)
assert len(users) == 3
assert users == {member1.user, member3.user, issue.get_owner()}
@@ -189,28 +189,146 @@ def test_users_to_notify():
policy2.notify_level = NotifyLevel.ignore
policy2.save()
- issue.watchers.add(member3.user)
+ issue.add_watcher(member3.user)
users = services.get_users_to_notify(issue)
assert len(users) == 2
assert users == {member1.user, issue.get_owner()}
# Test with watchers without permissions
- issue.watchers.add(member5.user)
+ issue.add_watcher(member5.user)
users = services.get_users_to_notify(issue)
assert len(users) == 2
assert users == {member1.user, issue.get_owner()}
# Test with inactive user
- issue.watchers.add(inactive_member1.user)
+ issue.add_watcher(inactive_member1.user)
assert len(users) == 2
assert users == {member1.user, issue.get_owner()}
# Test with system user
- issue.watchers.add(system_member1.user)
+ issue.add_watcher(system_member1.user)
assert len(users) == 2
assert users == {member1.user, issue.get_owner()}
+def test_watching_users_to_notify_on_issue_modification_1():
+ # If:
+ # - the user is watching the issue
+ # - the user is not watching the project
+ # - the notify policy is watch
+ # Then:
+ # - email is sent
+ project = f.ProjectFactory.create(anon_permissions= ["view_issues"])
+ issue = f.IssueFactory.create(project=project)
+ watching_user = f.UserFactory()
+ issue.add_watcher(watching_user)
+ watching_user_policy = services.get_notify_policy(project, watching_user)
+ issue.description = "test1"
+ issue.save()
+ watching_user_policy.notify_level = NotifyLevel.watch
+ users = services.get_users_to_notify(issue)
+ assert users == {watching_user, issue.owner}
+
+
+def test_watching_users_to_notify_on_issue_modification_2():
+ # If:
+ # - the user is watching the issue
+ # - the user is not watching the project
+ # - the notify policy is notwatch
+ # Then:
+ # - email is sent
+ project = f.ProjectFactory.create(anon_permissions= ["view_issues"])
+ issue = f.IssueFactory.create(project=project)
+ watching_user = f.UserFactory()
+ issue.add_watcher(watching_user)
+ watching_user_policy = services.get_notify_policy(project, watching_user)
+ issue.description = "test1"
+ issue.save()
+ watching_user_policy.notify_level = NotifyLevel.notwatch
+ users = services.get_users_to_notify(issue)
+ assert users == {watching_user, issue.owner}
+
+
+def test_watching_users_to_notify_on_issue_modification_3():
+ # If:
+ # - the user is watching the issue
+ # - the user is not watching the project
+ # - the notify policy is ignore
+ # Then:
+ # - email is not sent
+ project = f.ProjectFactory.create(anon_permissions= ["view_issues"])
+ issue = f.IssueFactory.create(project=project)
+ watching_user = f.UserFactory()
+ issue.add_watcher(watching_user)
+ watching_user_policy = services.get_notify_policy(project, watching_user)
+ issue.description = "test1"
+ issue.save()
+ watching_user_policy.notify_level = NotifyLevel.ignore
+ watching_user_policy.save()
+ users = services.get_users_to_notify(issue)
+ assert users == {issue.owner}
+
+
+def test_watching_users_to_notify_on_issue_modification_4():
+ # If:
+ # - the user is not watching the issue
+ # - the user is watching the project
+ # - the notify policy is ignore
+ # Then:
+ # - email is not sent
+ project = f.ProjectFactory.create(anon_permissions= ["view_issues"])
+ issue = f.IssueFactory.create(project=project)
+ watching_user = f.UserFactory()
+ project.add_watcher(watching_user)
+ watching_user_policy = services.get_notify_policy(project, watching_user)
+ issue.description = "test1"
+ issue.save()
+ watching_user_policy.notify_level = NotifyLevel.ignore
+ watching_user_policy.save()
+ users = services.get_users_to_notify(issue)
+ assert users == {issue.owner}
+
+
+def test_watching_users_to_notify_on_issue_modification_5():
+ # If:
+ # - the user is not watching the issue
+ # - the user is watching the project
+ # - the notify policy is watch
+ # Then:
+ # - email is sent
+ project = f.ProjectFactory.create(anon_permissions= ["view_issues"])
+ issue = f.IssueFactory.create(project=project)
+ watching_user = f.UserFactory()
+ project.add_watcher(watching_user)
+ watching_user_policy = services.get_notify_policy(project, watching_user)
+ issue.description = "test1"
+ issue.save()
+ watching_user_policy.notify_level = NotifyLevel.watch
+ watching_user_policy.save()
+ users = services.get_users_to_notify(issue)
+ assert users == {watching_user, issue.owner}
+
+
+def test_watching_users_to_notify_on_issue_modification_6():
+ # If:
+ # - the user is not watching the issue
+ # - the user is watching the project
+ # - the notify policy is notwatch
+ # Then:
+ # - email is sent
+ project = f.ProjectFactory.create(anon_permissions= ["view_issues"])
+ issue = f.IssueFactory.create(project=project)
+ watching_user = f.UserFactory()
+ project.add_watcher(watching_user)
+ watching_user_policy = services.get_notify_policy(project, watching_user)
+ issue.description = "test1"
+ issue.save()
+ watching_user_policy.notify_level = NotifyLevel.notwatch
+ watching_user_policy.save()
+ users = services.get_users_to_notify(issue)
+ assert users == {watching_user, issue.owner}
+
+
def test_send_notifications_using_services_method(settings, mail):
settings.CHANGE_NOTIFICATIONS_MIN_INTERVAL = 1
@@ -344,7 +462,7 @@ def test_watchers_assignation_for_issue(client):
issue = f.create_issue(project=project1, owner=user1)
data = {"version": issue.version,
- "watchers": [user1.pk]}
+ "watchersa": [user1.pk]}
url = reverse("issues-detail", args=[issue.pk])
response = client.json.patch(url, json.dumps(data))
diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py
index 45b7c96e..2a4f8e5c 100644
--- a/tests/integration/test_projects.py
+++ b/tests/integration/test_projects.py
@@ -265,7 +265,7 @@ def test_leave_project_respect_watching_items(client):
url = reverse("projects-leave", args=(project.id,))
response = client.post(url)
assert response.status_code == 200
- assert list(issue.watchers.all()) == [user]
+ assert issue.watchers == [user]
def test_delete_membership_only_owner(client):
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_tasks.py b/tests/integration/test_tasks.py
index 382bfc6f..08825955 100644
--- a/tests/integration/test_tasks.py
+++ b/tests/integration/test_tasks.py
@@ -163,6 +163,6 @@ def test_custom_fields_csv_generation():
data.seek(0)
reader = csv.reader(data)
row = next(reader)
- assert row[17] == attr.name
+ assert row[19] == attr.name
row = next(reader)
- assert row[17] == "val1"
+ assert row[19] == "val1"
diff --git a/tests/integration/test_timeline.py b/tests/integration/test_timeline.py
index 957c5c91..4620e881 100644
--- a/tests/integration/test_timeline.py
+++ b/tests/integration/test_timeline.py
@@ -196,8 +196,10 @@ def test_create_membership_timeline():
def test_update_project_timeline():
+ user_watcher= factories.UserFactory()
project = factories.ProjectFactory.create(name="test project timeline")
history_services.take_snapshot(project, user=project.owner)
+ project.add_watcher(user_watcher)
project.name = "test project timeline updated"
project.save()
history_services.take_snapshot(project, user=project.owner)
@@ -206,11 +208,18 @@ def test_update_project_timeline():
assert project_timeline[0].data["project"]["name"] == "test project timeline updated"
assert project_timeline[0].data["values_diff"]["name"][0] == "test project timeline"
assert project_timeline[0].data["values_diff"]["name"][1] == "test project timeline updated"
+ user_watcher_timeline = service.get_profile_timeline(user_watcher)
+ assert user_watcher_timeline[0].event_type == "projects.project.change"
+ assert user_watcher_timeline[0].data["project"]["name"] == "test project timeline updated"
+ assert user_watcher_timeline[0].data["values_diff"]["name"][0] == "test project timeline"
+ assert user_watcher_timeline[0].data["values_diff"]["name"][1] == "test project timeline updated"
def test_update_milestone_timeline():
+ user_watcher= factories.UserFactory()
milestone = factories.MilestoneFactory.create(name="test milestone timeline")
history_services.take_snapshot(milestone, user=milestone.owner)
+ milestone.add_watcher(user_watcher)
milestone.name = "test milestone timeline updated"
milestone.save()
history_services.take_snapshot(milestone, user=milestone.owner)
@@ -219,11 +228,18 @@ def test_update_milestone_timeline():
assert project_timeline[0].data["milestone"]["name"] == "test milestone timeline updated"
assert project_timeline[0].data["values_diff"]["name"][0] == "test milestone timeline"
assert project_timeline[0].data["values_diff"]["name"][1] == "test milestone timeline updated"
+ user_watcher_timeline = service.get_profile_timeline(user_watcher)
+ assert user_watcher_timeline[0].event_type == "milestones.milestone.change"
+ assert user_watcher_timeline[0].data["milestone"]["name"] == "test milestone timeline updated"
+ assert user_watcher_timeline[0].data["values_diff"]["name"][0] == "test milestone timeline"
+ assert user_watcher_timeline[0].data["values_diff"]["name"][1] == "test milestone timeline updated"
def test_update_user_story_timeline():
+ user_watcher= factories.UserFactory()
user_story = factories.UserStoryFactory.create(subject="test us timeline")
history_services.take_snapshot(user_story, user=user_story.owner)
+ user_story.add_watcher(user_watcher)
user_story.subject = "test us timeline updated"
user_story.save()
history_services.take_snapshot(user_story, user=user_story.owner)
@@ -232,11 +248,18 @@ def test_update_user_story_timeline():
assert project_timeline[0].data["userstory"]["subject"] == "test us timeline updated"
assert project_timeline[0].data["values_diff"]["subject"][0] == "test us timeline"
assert project_timeline[0].data["values_diff"]["subject"][1] == "test us timeline updated"
+ user_watcher_timeline = service.get_profile_timeline(user_watcher)
+ assert user_watcher_timeline[0].event_type == "userstories.userstory.change"
+ assert user_watcher_timeline[0].data["userstory"]["subject"] == "test us timeline updated"
+ assert user_watcher_timeline[0].data["values_diff"]["subject"][0] == "test us timeline"
+ assert user_watcher_timeline[0].data["values_diff"]["subject"][1] == "test us timeline updated"
def test_update_issue_timeline():
+ user_watcher= factories.UserFactory()
issue = factories.IssueFactory.create(subject="test issue timeline")
history_services.take_snapshot(issue, user=issue.owner)
+ issue.add_watcher(user_watcher)
issue.subject = "test issue timeline updated"
issue.save()
history_services.take_snapshot(issue, user=issue.owner)
@@ -245,11 +268,18 @@ def test_update_issue_timeline():
assert project_timeline[0].data["issue"]["subject"] == "test issue timeline updated"
assert project_timeline[0].data["values_diff"]["subject"][0] == "test issue timeline"
assert project_timeline[0].data["values_diff"]["subject"][1] == "test issue timeline updated"
+ user_watcher_timeline = service.get_profile_timeline(user_watcher)
+ assert user_watcher_timeline[0].event_type == "issues.issue.change"
+ assert user_watcher_timeline[0].data["issue"]["subject"] == "test issue timeline updated"
+ assert user_watcher_timeline[0].data["values_diff"]["subject"][0] == "test issue timeline"
+ assert user_watcher_timeline[0].data["values_diff"]["subject"][1] == "test issue timeline updated"
def test_update_task_timeline():
+ user_watcher= factories.UserFactory()
task = factories.TaskFactory.create(subject="test task timeline")
history_services.take_snapshot(task, user=task.owner)
+ task.add_watcher(user_watcher)
task.subject = "test task timeline updated"
task.save()
history_services.take_snapshot(task, user=task.owner)
@@ -258,11 +288,18 @@ def test_update_task_timeline():
assert project_timeline[0].data["task"]["subject"] == "test task timeline updated"
assert project_timeline[0].data["values_diff"]["subject"][0] == "test task timeline"
assert project_timeline[0].data["values_diff"]["subject"][1] == "test task timeline updated"
+ user_watcher_timeline = service.get_profile_timeline(user_watcher)
+ assert user_watcher_timeline[0].event_type == "tasks.task.change"
+ assert user_watcher_timeline[0].data["task"]["subject"] == "test task timeline updated"
+ assert user_watcher_timeline[0].data["values_diff"]["subject"][0] == "test task timeline"
+ assert user_watcher_timeline[0].data["values_diff"]["subject"][1] == "test task timeline updated"
def test_update_wiki_page_timeline():
+ user_watcher= factories.UserFactory()
page = factories.WikiPageFactory.create(slug="test wiki page timeline")
history_services.take_snapshot(page, user=page.owner)
+ page.add_watcher(user_watcher)
page.slug = "test wiki page timeline updated"
page.save()
history_services.take_snapshot(page, user=page.owner)
@@ -271,6 +308,11 @@ def test_update_wiki_page_timeline():
assert project_timeline[0].data["wikipage"]["slug"] == "test wiki page timeline updated"
assert project_timeline[0].data["values_diff"]["slug"][0] == "test wiki page timeline"
assert project_timeline[0].data["values_diff"]["slug"][1] == "test wiki page timeline updated"
+ user_watcher_timeline = service.get_profile_timeline(user_watcher)
+ assert user_watcher_timeline[0].event_type == "wiki.wikipage.change"
+ assert user_watcher_timeline[0].data["wikipage"]["slug"] == "test wiki page timeline updated"
+ assert user_watcher_timeline[0].data["values_diff"]["slug"][0] == "test wiki page timeline"
+ assert user_watcher_timeline[0].data["values_diff"]["slug"][1] == "test wiki page timeline updated"
def test_update_membership_timeline():
@@ -298,50 +340,80 @@ def test_update_membership_timeline():
def test_delete_project_timeline():
project = factories.ProjectFactory.create(name="test project timeline")
+ user_watcher= factories.UserFactory()
+ project.add_watcher(user_watcher)
history_services.take_snapshot(project, user=project.owner, delete=True)
user_timeline = service.get_project_timeline(project)
assert user_timeline[0].event_type == "projects.project.delete"
assert user_timeline[0].data["project"]["id"] == project.id
+ user_watcher_timeline = service.get_profile_timeline(user_watcher)
+ assert user_watcher_timeline[0].event_type == "projects.project.delete"
+ assert user_watcher_timeline[0].data["project"]["id"] == project.id
def test_delete_milestone_timeline():
milestone = factories.MilestoneFactory.create(name="test milestone timeline")
+ user_watcher= factories.UserFactory()
+ milestone.add_watcher(user_watcher)
history_services.take_snapshot(milestone, user=milestone.owner, delete=True)
project_timeline = service.get_project_timeline(milestone.project)
assert project_timeline[0].event_type == "milestones.milestone.delete"
assert project_timeline[0].data["milestone"]["name"] == "test milestone timeline"
+ user_watcher_timeline = service.get_profile_timeline(user_watcher)
+ assert user_watcher_timeline[0].event_type == "milestones.milestone.delete"
+ assert user_watcher_timeline[0].data["milestone"]["name"] == "test milestone timeline"
def test_delete_user_story_timeline():
user_story = factories.UserStoryFactory.create(subject="test us timeline")
+ user_watcher= factories.UserFactory()
+ user_story.add_watcher(user_watcher)
history_services.take_snapshot(user_story, user=user_story.owner, delete=True)
project_timeline = service.get_project_timeline(user_story.project)
assert project_timeline[0].event_type == "userstories.userstory.delete"
assert project_timeline[0].data["userstory"]["subject"] == "test us timeline"
+ user_watcher_timeline = service.get_profile_timeline(user_watcher)
+ assert user_watcher_timeline[0].event_type == "userstories.userstory.delete"
+ assert user_watcher_timeline[0].data["userstory"]["subject"] == "test us timeline"
def test_delete_issue_timeline():
issue = factories.IssueFactory.create(subject="test issue timeline")
+ user_watcher= factories.UserFactory()
+ issue.add_watcher(user_watcher)
history_services.take_snapshot(issue, user=issue.owner, delete=True)
project_timeline = service.get_project_timeline(issue.project)
assert project_timeline[0].event_type == "issues.issue.delete"
assert project_timeline[0].data["issue"]["subject"] == "test issue timeline"
+ user_watcher_timeline = service.get_profile_timeline(user_watcher)
+ assert user_watcher_timeline[0].event_type == "issues.issue.delete"
+ assert user_watcher_timeline[0].data["issue"]["subject"] == "test issue timeline"
def test_delete_task_timeline():
task = factories.TaskFactory.create(subject="test task timeline")
+ user_watcher= factories.UserFactory()
+ task.add_watcher(user_watcher)
history_services.take_snapshot(task, user=task.owner, delete=True)
project_timeline = service.get_project_timeline(task.project)
assert project_timeline[0].event_type == "tasks.task.delete"
assert project_timeline[0].data["task"]["subject"] == "test task timeline"
+ user_watcher_timeline = service.get_profile_timeline(user_watcher)
+ assert user_watcher_timeline[0].event_type == "tasks.task.delete"
+ assert user_watcher_timeline[0].data["task"]["subject"] == "test task timeline"
def test_delete_wiki_page_timeline():
page = factories.WikiPageFactory.create(slug="test wiki page timeline")
+ user_watcher= factories.UserFactory()
+ page.add_watcher(user_watcher)
history_services.take_snapshot(page, user=page.owner, delete=True)
project_timeline = service.get_project_timeline(page.project)
assert project_timeline[0].event_type == "wiki.wikipage.delete"
assert project_timeline[0].data["wikipage"]["slug"] == "test wiki page timeline"
+ user_watcher_timeline = service.get_profile_timeline(user_watcher)
+ assert user_watcher_timeline[0].event_type == "wiki.wikipage.delete"
+ assert user_watcher_timeline[0].data["wikipage"]["slug"] == "test wiki page timeline"
def test_delete_membership_timeline():
@@ -384,16 +456,6 @@ def test_assigned_to_user_story_timeline():
assert user_timeline[0].data["userstory"]["subject"] == "test us timeline"
-def test_watchers_to_user_story_timeline():
- membership = factories.MembershipFactory.create()
- user_story = factories.UserStoryFactory.create(subject="test us timeline", project=membership.project)
- user_story.watchers.add(membership.user)
- history_services.take_snapshot(user_story, user=user_story.owner)
- user_timeline = service.get_profile_timeline(membership.user)
- assert user_timeline[0].event_type == "userstories.userstory.create"
- assert user_timeline[0].data["userstory"]["subject"] == "test us timeline"
-
-
def test_user_data_for_non_system_users():
user_story = factories.UserStoryFactory.create(subject="test us timeline")
history_services.take_snapshot(user_story, user=user_story.owner)
diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py
index 2d3f1664..b658045f 100644
--- a/tests/integration/test_users.py
+++ b/tests/integration/test_users.py
@@ -1,6 +1,7 @@
import pytest
from tempfile import NamedTemporaryFile
+from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse
from .. import factories as f
@@ -9,6 +10,7 @@ from taiga.base.utils import json
from taiga.users import models
from taiga.auth.tokens import get_token_for_user
from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
+from taiga.users.services import get_favourites_list
pytestmark = pytest.mark.django_db
@@ -249,3 +251,166 @@ def test_list_contacts_public_projects(client):
response_content = response.data
assert len(response_content) == 1
assert response_content[0]["id"] == user_2.id
+
+
+def test_get_favourites_list():
+ fav_user = f.UserFactory()
+ viewer_user = f.UserFactory()
+
+ project = f.ProjectFactory(is_private=False, name="Testing project")
+ role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"])
+ membership = f.MembershipFactory(project=project, role=role, user=fav_user)
+ project.add_watcher(fav_user)
+ content_type = ContentType.objects.get_for_model(project)
+ f.VoteFactory(content_type=content_type, object_id=project.id, user=fav_user)
+ f.VotesFactory(content_type=content_type, object_id=project.id, count=1)
+
+ user_story = f.UserStoryFactory(project=project, subject="Testing user story")
+ user_story.add_watcher(fav_user)
+ content_type = ContentType.objects.get_for_model(user_story)
+ f.VoteFactory(content_type=content_type, object_id=user_story.id, user=fav_user)
+ f.VotesFactory(content_type=content_type, object_id=user_story.id, count=1)
+
+ task = f.TaskFactory(project=project, subject="Testing task")
+ task.add_watcher(fav_user)
+ content_type = ContentType.objects.get_for_model(task)
+ f.VoteFactory(content_type=content_type, object_id=task.id, user=fav_user)
+ f.VotesFactory(content_type=content_type, object_id=task.id, count=1)
+
+ issue = f.IssueFactory(project=project, subject="Testing issue")
+ issue.add_watcher(fav_user)
+ content_type = ContentType.objects.get_for_model(issue)
+ f.VoteFactory(content_type=content_type, object_id=issue.id, user=fav_user)
+ f.VotesFactory(content_type=content_type, object_id=issue.id, count=1)
+
+ assert len(get_favourites_list(fav_user, viewer_user)) == 8
+ assert len(get_favourites_list(fav_user, viewer_user, type="project")) == 2
+ assert len(get_favourites_list(fav_user, viewer_user, type="userstory")) == 2
+ assert len(get_favourites_list(fav_user, viewer_user, type="task")) == 2
+ assert len(get_favourites_list(fav_user, viewer_user, type="issue")) == 2
+ assert len(get_favourites_list(fav_user, viewer_user, type="unknown")) == 0
+
+ assert len(get_favourites_list(fav_user, viewer_user, action="watch")) == 4
+ assert len(get_favourites_list(fav_user, viewer_user, action="vote")) == 4
+
+ assert len(get_favourites_list(fav_user, viewer_user, q="issue")) == 2
+ assert len(get_favourites_list(fav_user, viewer_user, q="unexisting text")) == 0
+
+
+def test_get_favourites_list_valid_info_for_project():
+ fav_user = f.UserFactory()
+ viewer_user = f.UserFactory()
+ watcher_user = f.UserFactory()
+
+ project = f.ProjectFactory(is_private=False, name="Testing project")
+ project.add_watcher(watcher_user)
+ content_type = ContentType.objects.get_for_model(project)
+ vote = f.VoteFactory(content_type=content_type, object_id=project.id, user=fav_user)
+ f.VotesFactory(content_type=content_type, object_id=project.id, count=1)
+
+ project_vote_info = get_favourites_list(fav_user, viewer_user)[0]
+ assert project_vote_info["type"] == "project"
+ assert project_vote_info["action"] == "vote"
+ assert project_vote_info["id"] == project.id
+ assert project_vote_info["ref"] == None
+ assert project_vote_info["slug"] == project.slug
+ assert project_vote_info["subject"] == project.name
+ assert project_vote_info["tags"] == project.tags
+ assert project_vote_info["project"] == project.id
+ assert project_vote_info["assigned_to"] == None
+ assert project_vote_info["total_watchers"] == 1
+ assert project_vote_info["created_date"] == vote.created_date
+ assert project_vote_info["project_name"] == project.name
+ assert project_vote_info["project_slug"] == project.slug
+ assert project_vote_info["project_is_private"] == project.is_private
+ assert project_vote_info["assigned_to_username"] == None
+ assert project_vote_info["assigned_to_full_name"] == None
+ assert project_vote_info["assigned_to_photo"] == None
+ assert project_vote_info["assigned_to_email"] == None
+ assert project_vote_info["total_votes"] == 1
+
+
+def test_get_favourites_list_valid_info_for_not_project_types():
+ fav_user = f.UserFactory()
+ viewer_user = f.UserFactory()
+ watcher_user = f.UserFactory()
+ assigned_to_user = f.UserFactory()
+
+ project = f.ProjectFactory(is_private=False, name="Testing project")
+
+ factories = {
+ "userstory": f.UserStoryFactory,
+ "task": f.TaskFactory,
+ "issue": f.IssueFactory
+ }
+
+ for object_type in factories:
+ instance = factories[object_type](project=project,
+ subject="Testing",
+ tags=["test1", "test2"],
+ assigned_to=assigned_to_user)
+
+ instance.add_watcher(watcher_user)
+ content_type = ContentType.objects.get_for_model(instance)
+ vote = f.VoteFactory(content_type=content_type, object_id=instance.id, user=fav_user)
+ f.VotesFactory(content_type=content_type, object_id=instance.id, count=3)
+
+ instance_vote_info = get_favourites_list(fav_user, viewer_user, type=object_type)[0]
+ assert instance_vote_info["type"] == object_type
+ assert instance_vote_info["action"] == "vote"
+ assert instance_vote_info["id"] == instance.id
+ assert instance_vote_info["ref"] == instance.ref
+ assert instance_vote_info["slug"] == None
+ assert instance_vote_info["subject"] == instance.subject
+ assert instance_vote_info["tags"] == instance.tags
+ assert instance_vote_info["project"] == instance.project.id
+ assert instance_vote_info["assigned_to"] == assigned_to_user.id
+ assert instance_vote_info["total_watchers"] == 1
+ assert instance_vote_info["created_date"] == vote.created_date
+ assert instance_vote_info["project_name"] == instance.project.name
+ assert instance_vote_info["project_slug"] == instance.project.slug
+ assert instance_vote_info["project_is_private"] == instance.project.is_private
+ assert instance_vote_info["assigned_to_username"] == assigned_to_user.username
+ assert instance_vote_info["assigned_to_full_name"] == assigned_to_user.full_name
+ assert instance_vote_info["assigned_to_photo"] == ''
+ assert instance_vote_info["assigned_to_email"] == assigned_to_user.email
+ assert instance_vote_info["total_votes"] == 3
+
+
+def test_get_favourites_list_permissions():
+ fav_user = f.UserFactory()
+ viewer_unpriviliged_user = f.UserFactory()
+ viewer_priviliged_user = f.UserFactory()
+
+ project = f.ProjectFactory(is_private=True, name="Testing project")
+ role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"])
+ membership = f.MembershipFactory(project=project, role=role, user=viewer_priviliged_user)
+ content_type = ContentType.objects.get_for_model(project)
+ f.VoteFactory(content_type=content_type, object_id=project.id, user=fav_user)
+ f.VotesFactory(content_type=content_type, object_id=project.id, count=1)
+
+ user_story = f.UserStoryFactory(project=project, subject="Testing user story")
+ content_type = ContentType.objects.get_for_model(user_story)
+ f.VoteFactory(content_type=content_type, object_id=user_story.id, user=fav_user)
+ f.VotesFactory(content_type=content_type, object_id=user_story.id, count=1)
+
+ task = f.TaskFactory(project=project, subject="Testing task")
+ content_type = ContentType.objects.get_for_model(task)
+ f.VoteFactory(content_type=content_type, object_id=task.id, user=fav_user)
+ f.VotesFactory(content_type=content_type, object_id=task.id, count=1)
+
+ issue = f.IssueFactory(project=project, subject="Testing issue")
+ content_type = ContentType.objects.get_for_model(issue)
+ f.VoteFactory(content_type=content_type, object_id=issue.id, user=fav_user)
+ f.VotesFactory(content_type=content_type, object_id=issue.id, count=1)
+
+ #If the project is private a viewer user without any permission shouldn' see any vote
+ assert len(get_favourites_list(fav_user, viewer_unpriviliged_user)) == 0
+
+ #If the project is private but the viewer user has permissions the votes should be accesible
+ assert len(get_favourites_list(fav_user, viewer_priviliged_user)) == 4
+
+ #If the project is private but has the required anon permissions the votes should be accesible by any user too
+ project.anon_permissions = ["view_project", "view_us", "view_tasks", "view_issues"]
+ project.save()
+ assert len(get_favourites_list(fav_user, viewer_unpriviliged_user)) == 4
diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py
index d8972da2..6b49568a 100644
--- a/tests/integration/test_userstories.py
+++ b/tests/integration/test_userstories.py
@@ -483,6 +483,6 @@ def test_custom_fields_csv_generation():
data.seek(0)
reader = csv.reader(data)
row = next(reader)
- assert row[24] == attr.name
+ assert row[26] == attr.name
row = next(reader)
- assert row[24] == "val1"
+ assert row[26] == "val1"
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/integration/test_watch_issues.py b/tests/integration/test_watch_issues.py
new file mode 100644
index 00000000..09ba4f7b
--- /dev/null
+++ b/tests/integration/test_watch_issues.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
+import json
+from django.core.urlresolvers import reverse
+
+from .. import factories as f
+
+pytestmark = pytest.mark.django_db
+
+
+def test_watch_issue(client):
+ user = f.UserFactory.create()
+ issue = f.create_issue(owner=user)
+ f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
+ url = reverse("issues-watch", args=(issue.id,))
+
+ client.login(user)
+ response = client.post(url)
+
+ assert response.status_code == 200
+
+
+def test_unwatch_issue(client):
+ user = f.UserFactory.create()
+ issue = f.create_issue(owner=user)
+ f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
+ url = reverse("issues-watch", args=(issue.id,))
+
+ client.login(user)
+ response = client.post(url)
+
+ assert response.status_code == 200
+
+
+def test_list_issue_watchers(client):
+ user = f.UserFactory.create()
+ issue = f.IssueFactory(owner=user)
+ f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
+ f.WatchedFactory.create(content_object=issue, user=user)
+ url = reverse("issue-watchers-list", args=(issue.id,))
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data[0]['id'] == user.id
+
+
+def test_get_issue_watcher(client):
+ user = f.UserFactory.create()
+ issue = f.IssueFactory(owner=user)
+ f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
+ watch = f.WatchedFactory.create(content_object=issue, user=user)
+ url = reverse("issue-watchers-detail", args=(issue.id, watch.user.id))
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data['id'] == watch.user.id
+
+
+def test_get_issue_watchers(client):
+ user = f.UserFactory.create()
+ issue = f.IssueFactory(owner=user)
+ f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
+ url = reverse("issues-detail", args=(issue.id,))
+
+ f.WatchedFactory.create(content_object=issue, user=user)
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data['watchers'] == [user.id]
+
+
+def test_get_issue_is_watched(client):
+ user = f.UserFactory.create()
+ issue = f.IssueFactory(owner=user)
+ f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
+ url_detail = reverse("issues-detail", args=(issue.id,))
+ url_watch = reverse("issues-watch", args=(issue.id,))
+ url_unwatch = reverse("issues-unwatch", args=(issue.id,))
+
+ client.login(user)
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == []
+ assert response.data['is_watched'] == False
+
+ response = client.post(url_watch)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == [user.id]
+ assert response.data['is_watched'] == True
+
+ response = client.post(url_unwatch)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == []
+ assert response.data['is_watched'] == False
diff --git a/tests/integration/test_watch_milestones.py b/tests/integration/test_watch_milestones.py
new file mode 100644
index 00000000..72fea24d
--- /dev/null
+++ b/tests/integration/test_watch_milestones.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
+import json
+from django.core.urlresolvers import reverse
+
+from .. import factories as f
+
+pytestmark = pytest.mark.django_db
+
+
+def test_watch_milestone(client):
+ user = f.UserFactory.create()
+ milestone = f.MilestoneFactory(owner=user)
+ f.MembershipFactory.create(project=milestone.project, user=user, is_owner=True)
+ url = reverse("milestones-watch", args=(milestone.id,))
+
+ client.login(user)
+ response = client.post(url)
+
+ assert response.status_code == 200
+
+
+def test_unwatch_milestone(client):
+ user = f.UserFactory.create()
+ milestone = f.MilestoneFactory(owner=user)
+ f.MembershipFactory.create(project=milestone.project, user=user, is_owner=True)
+ url = reverse("milestones-watch", args=(milestone.id,))
+
+ client.login(user)
+ response = client.post(url)
+
+ assert response.status_code == 200
+
+
+def test_list_milestone_watchers(client):
+ user = f.UserFactory.create()
+ milestone = f.MilestoneFactory(owner=user)
+ f.MembershipFactory.create(project=milestone.project, user=user, is_owner=True)
+ f.WatchedFactory.create(content_object=milestone, user=user)
+ url = reverse("milestone-watchers-list", args=(milestone.id,))
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data[0]['id'] == user.id
+
+
+def test_get_milestone_watcher(client):
+ user = f.UserFactory.create()
+ milestone = f.MilestoneFactory(owner=user)
+ f.MembershipFactory.create(project=milestone.project, user=user, is_owner=True)
+ watch = f.WatchedFactory.create(content_object=milestone, user=user)
+ url = reverse("milestone-watchers-detail", args=(milestone.id, watch.user.id))
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data['id'] == watch.user.id
+
+
+def test_get_milestone_watchers(client):
+ user = f.UserFactory.create()
+ milestone = f.MilestoneFactory(owner=user)
+ f.MembershipFactory.create(project=milestone.project, user=user, is_owner=True)
+ url = reverse("milestones-detail", args=(milestone.id,))
+
+ f.WatchedFactory.create(content_object=milestone, user=user)
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data['watchers'] == [user.id]
+
+
+def test_get_milestone_is_watched(client):
+ user = f.UserFactory.create()
+ milestone = f.MilestoneFactory(owner=user)
+ f.MembershipFactory.create(project=milestone.project, user=user, is_owner=True)
+ url_detail = reverse("milestones-detail", args=(milestone.id,))
+ url_watch = reverse("milestones-watch", args=(milestone.id,))
+ url_unwatch = reverse("milestones-unwatch", args=(milestone.id,))
+
+ client.login(user)
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == []
+ assert response.data['is_watched'] == False
+
+ response = client.post(url_watch)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == [user.id]
+ assert response.data['is_watched'] == True
+
+ response = client.post(url_unwatch)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == []
+ assert response.data['is_watched'] == False
diff --git a/tests/integration/test_watch_projects.py b/tests/integration/test_watch_projects.py
new file mode 100644
index 00000000..358c15f2
--- /dev/null
+++ b/tests/integration/test_watch_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
+import json
+from django.core.urlresolvers import reverse
+
+from .. import factories as f
+
+pytestmark = pytest.mark.django_db
+
+
+def test_watch_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-watch", args=(project.id,))
+
+ client.login(user)
+ response = client.post(url)
+
+ assert response.status_code == 200
+
+
+def test_unwacth_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-unwatch", args=(project.id,))
+
+ client.login(user)
+ response = client.post(url)
+
+ assert response.status_code == 200
+
+
+def test_list_project_watchers(client):
+ user = f.UserFactory.create()
+ project = f.create_project(owner=user)
+ f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.WatchedFactory.create(content_object=project, user=user)
+ url = reverse("project-watchers-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_watcher(client):
+ user = f.UserFactory.create()
+ project = f.create_project(owner=user)
+ f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ watch = f.WatchedFactory.create(content_object=project, user=user)
+ url = reverse("project-watchers-detail", args=(project.id, watch.user.id))
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data['id'] == watch.user.id
+
+
+def test_get_project_watchers(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.WatchedFactory.create(content_object=project, user=user)
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data['watchers'] == [user.id]
+
+
+def test_get_project_is_watched(client):
+ user = f.UserFactory.create()
+ project = f.create_project(owner=user)
+ f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ url_detail = reverse("projects-detail", args=(project.id,))
+ url_watch = reverse("projects-watch", args=(project.id,))
+ url_unwatch = reverse("projects-unwatch", args=(project.id,))
+
+ client.login(user)
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == []
+ assert response.data['is_watched'] == False
+
+ response = client.post(url_watch)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == [user.id]
+ assert response.data['is_watched'] == True
+
+ response = client.post(url_unwatch)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == []
+ assert response.data['is_watched'] == False
diff --git a/tests/integration/test_watch_tasks.py b/tests/integration/test_watch_tasks.py
new file mode 100644
index 00000000..7444a948
--- /dev/null
+++ b/tests/integration/test_watch_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
+import json
+from django.core.urlresolvers import reverse
+
+from .. import factories as f
+
+pytestmark = pytest.mark.django_db
+
+
+def test_watch_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-watch", args=(task.id,))
+
+ client.login(user)
+ response = client.post(url)
+
+ assert response.status_code == 200
+
+
+def test_unwatch_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-watch", args=(task.id,))
+
+ client.login(user)
+ response = client.post(url)
+
+ assert response.status_code == 200
+
+
+def test_list_task_watchers(client):
+ user = f.UserFactory.create()
+ task = f.TaskFactory(owner=user)
+ f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ f.WatchedFactory.create(content_object=task, user=user)
+ url = reverse("task-watchers-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_watcher(client):
+ user = f.UserFactory.create()
+ task = f.TaskFactory(owner=user)
+ f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ watch = f.WatchedFactory.create(content_object=task, user=user)
+ url = reverse("task-watchers-detail", args=(task.id, watch.user.id))
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data['id'] == watch.user.id
+
+
+def test_get_task_watchers(client):
+ user = f.UserFactory.create()
+ task = f.TaskFactory(owner=user)
+ f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ url = reverse("tasks-detail", args=(task.id,))
+
+ f.WatchedFactory.create(content_object=task, user=user)
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data['watchers'] == [user.id]
+
+
+def test_get_task_is_watched(client):
+ user = f.UserFactory.create()
+ task = f.TaskFactory(owner=user)
+ f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ url_detail = reverse("tasks-detail", args=(task.id,))
+ url_watch = reverse("tasks-watch", args=(task.id,))
+ url_unwatch = reverse("tasks-unwatch", args=(task.id,))
+
+ client.login(user)
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == []
+ assert response.data['is_watched'] == False
+
+ response = client.post(url_watch)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == [user.id]
+ assert response.data['is_watched'] == True
+
+ response = client.post(url_unwatch)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == []
+ assert response.data['is_watched'] == False
diff --git a/tests/integration/test_watch_userstories.py b/tests/integration/test_watch_userstories.py
new file mode 100644
index 00000000..cad86151
--- /dev/null
+++ b/tests/integration/test_watch_userstories.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
+import json
+from django.core.urlresolvers import reverse
+
+from .. import factories as f
+
+pytestmark = pytest.mark.django_db
+
+
+def test_watch_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-watch", args=(user_story.id,))
+
+ client.login(user)
+ response = client.post(url)
+
+ assert response.status_code == 200
+
+
+def test_unwatch_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-unwatch", args=(user_story.id,))
+
+ client.login(user)
+ response = client.post(url)
+
+ assert response.status_code == 200
+
+
+def test_list_user_story_watchers(client):
+ user = f.UserFactory.create()
+ user_story = f.UserStoryFactory(owner=user)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ f.WatchedFactory.create(content_object=user_story, user=user)
+ url = reverse("userstory-watchers-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_user_story_watcher(client):
+ user = f.UserFactory.create()
+ user_story = f.UserStoryFactory(owner=user)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ watch = f.WatchedFactory.create(content_object=user_story, user=user)
+ url = reverse("userstory-watchers-detail", args=(user_story.id, watch.user.id))
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data['id'] == watch.user.id
+
+
+def test_get_user_story_watchers(client):
+ user = f.UserFactory.create()
+ user_story = f.UserStoryFactory(owner=user)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ url = reverse("userstories-detail", args=(user_story.id,))
+
+ f.WatchedFactory.create(content_object=user_story, user=user)
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data['watchers'] == [user.id]
+
+
+def test_get_user_story_is_watched(client):
+ user = f.UserFactory.create()
+ user_story = f.UserStoryFactory(owner=user)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ url_detail = reverse("userstories-detail", args=(user_story.id,))
+ url_watch = reverse("userstories-watch", args=(user_story.id,))
+ url_unwatch = reverse("userstories-unwatch", args=(user_story.id,))
+
+ client.login(user)
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == []
+ assert response.data['is_watched'] == False
+
+ response = client.post(url_watch)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == [user.id]
+ assert response.data['is_watched'] == True
+
+ response = client.post(url_unwatch)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == []
+ assert response.data['is_watched'] == False
diff --git a/tests/integration/test_watch_wikipages.py b/tests/integration/test_watch_wikipages.py
new file mode 100644
index 00000000..c4f96bb6
--- /dev/null
+++ b/tests/integration/test_watch_wikipages.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
+import json
+from django.core.urlresolvers import reverse
+
+from .. import factories as f
+
+pytestmark = pytest.mark.django_db
+
+
+def test_watch_wikipage(client):
+ user = f.UserFactory.create()
+ wikipage = f.WikiPageFactory(owner=user)
+ f.MembershipFactory.create(project=wikipage.project, user=user, is_owner=True)
+ url = reverse("wiki-watch", args=(wikipage.id,))
+
+ client.login(user)
+ response = client.post(url)
+
+ assert response.status_code == 200
+
+
+def test_unwatch_wikipage(client):
+ user = f.UserFactory.create()
+ wikipage = f.WikiPageFactory(owner=user)
+ f.MembershipFactory.create(project=wikipage.project, user=user, is_owner=True)
+ url = reverse("wiki-watch", args=(wikipage.id,))
+
+ client.login(user)
+ response = client.post(url)
+
+ assert response.status_code == 200
+
+
+def test_list_wikipage_watchers(client):
+ user = f.UserFactory.create()
+ wikipage = f.WikiPageFactory(owner=user)
+ f.MembershipFactory.create(project=wikipage.project, user=user, is_owner=True)
+ f.WatchedFactory.create(content_object=wikipage, user=user)
+ url = reverse("wiki-watchers-list", args=(wikipage.id,))
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data[0]['id'] == user.id
+
+
+def test_get_wikipage_watcher(client):
+ user = f.UserFactory.create()
+ wikipage = f.WikiPageFactory(owner=user)
+ f.MembershipFactory.create(project=wikipage.project, user=user, is_owner=True)
+ watch = f.WatchedFactory.create(content_object=wikipage, user=user)
+ url = reverse("wiki-watchers-detail", args=(wikipage.id, watch.user.id))
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data['id'] == watch.user.id
+
+
+def test_get_wikipage_watchers(client):
+ user = f.UserFactory.create()
+ wikipage = f.WikiPageFactory(owner=user)
+ f.MembershipFactory.create(project=wikipage.project, user=user, is_owner=True)
+ url = reverse("wiki-detail", args=(wikipage.id,))
+
+ f.WatchedFactory.create(content_object=wikipage, user=user)
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data['watchers'] == [user.id]
+
+
+def test_get_wikipage_is_watched(client):
+ user = f.UserFactory.create()
+ wikipage = f.WikiPageFactory(owner=user)
+ f.MembershipFactory.create(project=wikipage.project, user=user, is_owner=True)
+ url_detail = reverse("wiki-detail", args=(wikipage.id,))
+ url_watch = reverse("wiki-watch", args=(wikipage.id,))
+ url_unwatch = reverse("wiki-unwatch", args=(wikipage.id,))
+
+ client.login(user)
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == []
+ assert response.data['is_watched'] == False
+
+ response = client.post(url_watch)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == [user.id]
+ assert response.data['is_watched'] == True
+
+ response = client.post(url_unwatch)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == []
+ assert response.data['is_watched'] == False
diff --git a/tests/integration/test_webhooks.py b/tests/integration/test_webhooks.py
index 0b3b32f0..9f4ecd71 100644
--- a/tests/integration/test_webhooks.py
+++ b/tests/integration/test_webhooks.py
@@ -90,3 +90,26 @@ def test_new_object_with_two_webhook(settings):
with patch('taiga.webhooks.tasks.delete_webhook') as delete_webhook_mock:
services.take_snapshot(obj, user=obj.owner, comment="test", delete=True)
assert delete_webhook_mock.call_count == 2
+
+
+def test_send_request_one_webhook(settings):
+ settings.WEBHOOKS_ENABLED = True
+ project = f.ProjectFactory()
+ f.WebhookFactory.create(project=project)
+
+ objects = [
+ f.IssueFactory.create(project=project),
+ f.TaskFactory.create(project=project),
+ f.UserStoryFactory.create(project=project),
+ f.WikiPageFactory.create(project=project)
+ ]
+
+ for obj in objects:
+ with patch('taiga.webhooks.tasks._send_request') as _send_request_mock:
+ services.take_snapshot(obj, user=obj.owner, comment="test")
+ assert _send_request_mock.call_count == 1
+
+ for obj in objects:
+ with patch('taiga.webhooks.tasks._send_request') as _send_request_mock:
+ services.take_snapshot(obj, user=obj.owner, comment="test", delete=True)
+ assert _send_request_mock.call_count == 1
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))