Merge pull request #424 from taigaio/us/2117/fa_proj/2122/upvote_content

Star/Unstar project, Upvote/Downvote issues, tasks and userstories and watch public issues, tasks and user stories
remotes/origin/enhancement/email-actions
David Barragán Merino 2015-08-26 15:48:34 +02:00
commit ec29efa606
99 changed files with 3888 additions and 477 deletions

View File

@ -4,13 +4,17 @@
## 1.9.0 ??? (unreleased) ## 1.9.0 ??? (unreleased)
### Features ### 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. - 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). - 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. - Fix the compatibility with BitBucket webhooks and add issues and issues comments integration.
- Add custom videoconference system. - Add custom videoconference system.
- Add support for comments in the Gitlab webhooks integration. - 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. - 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. - i18n.
- Add polish (pl) translation. - Add polish (pl) translation.
- Add portuguese (Brazil) (pt_BR) translation. - Add portuguese (Brazil) (pt_BR) translation.

View File

@ -110,3 +110,12 @@ class TagsColorsField(serializers.WritableField):
def from_native(self, data): def from_native(self, data):
return list(data.items()) return list(data.items())
class WatchersField(serializers.WritableField):
def to_native(self, obj):
return obj
def from_native(self, data):
return data

View File

@ -18,6 +18,7 @@ from functools import reduce
import logging import logging
from django.apps import apps from django.apps import apps
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q from django.db.models import Q
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@ -451,6 +452,33 @@ class TagsFilter(FilterBackend):
return super().filter_queryset(request, queryset, view) 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 # Text search filters
##################################################################### #####################################################################

View File

@ -19,6 +19,7 @@ import copy
import os import os
from collections import OrderedDict from collections import OrderedDict
from django.apps import apps
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ValidationError 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 models as timeline_models
from taiga.timeline import service as timeline_service from taiga.timeline import service as timeline_service
from taiga.users import models as users_models 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.votes import services as votes_service
from taiga.projects.history import services as history_service from taiga.projects.history import services as history_service
@ -223,6 +225,48 @@ class HistoryDiffField(JsonField):
return data 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): class HistoryExportSerializer(serializers.ModelSerializer):
user = HistoryUserField() user = HistoryUserField()
diff = HistoryDiffField(required=False) diff = HistoryDiffField(required=False)
@ -243,7 +287,7 @@ class HistoryExportSerializerMixin(serializers.ModelSerializer):
def get_history(self, obj): def get_history(self, obj):
history_qs = history_service.get_history_queryset_by_model_instance(obj, history_qs = history_service.get_history_queryset_by_model_instance(obj,
types=(history_models.HistoryType.change, history_models.HistoryType.create,)) types=(history_models.HistoryType.change, history_models.HistoryType.create,))
return HistoryExportSerializer(history_qs, many=True).data return HistoryExportSerializer(history_qs, many=True).data
@ -447,9 +491,8 @@ class RolePointsExportSerializer(serializers.ModelSerializer):
exclude = ('id', 'user_story') exclude = ('id', 'user_story')
class MilestoneExportSerializer(serializers.ModelSerializer): class MilestoneExportSerializer(WatcheableObjectModelSerializer):
owner = UserRelatedField(required=False) owner = UserRelatedField(required=False)
watchers = UserRelatedField(many=True, required=False)
modified_date = serializers.DateTimeField(required=False) modified_date = serializers.DateTimeField(required=False)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -475,13 +518,12 @@ class MilestoneExportSerializer(serializers.ModelSerializer):
class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin,
AttachmentExportSerializerMixin, serializers.ModelSerializer): AttachmentExportSerializerMixin, WatcheableObjectModelSerializer):
owner = UserRelatedField(required=False) owner = UserRelatedField(required=False)
status = ProjectRelatedField(slug_field="name") status = ProjectRelatedField(slug_field="name")
user_story = ProjectRelatedField(slug_field="ref", required=False) user_story = ProjectRelatedField(slug_field="ref", required=False)
milestone = ProjectRelatedField(slug_field="name", required=False) milestone = ProjectRelatedField(slug_field="name", required=False)
assigned_to = UserRelatedField(required=False) assigned_to = UserRelatedField(required=False)
watchers = UserRelatedField(many=True, required=False)
modified_date = serializers.DateTimeField(required=False) modified_date = serializers.DateTimeField(required=False)
class Meta: class Meta:
@ -493,13 +535,12 @@ class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryE
class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin,
AttachmentExportSerializerMixin, serializers.ModelSerializer): AttachmentExportSerializerMixin, WatcheableObjectModelSerializer):
role_points = RolePointsExportSerializer(many=True, required=False) role_points = RolePointsExportSerializer(many=True, required=False)
owner = UserRelatedField(required=False) owner = UserRelatedField(required=False)
assigned_to = UserRelatedField(required=False) assigned_to = UserRelatedField(required=False)
status = ProjectRelatedField(slug_field="name") status = ProjectRelatedField(slug_field="name")
milestone = ProjectRelatedField(slug_field="name", required=False) milestone = ProjectRelatedField(slug_field="name", required=False)
watchers = UserRelatedField(many=True, required=False)
modified_date = serializers.DateTimeField(required=False) modified_date = serializers.DateTimeField(required=False)
generated_from_issue = ProjectRelatedField(slug_field="ref", required=False) generated_from_issue = ProjectRelatedField(slug_field="ref", required=False)
@ -512,7 +553,7 @@ class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, His
class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin,
AttachmentExportSerializerMixin, serializers.ModelSerializer): AttachmentExportSerializerMixin, WatcheableObjectModelSerializer):
owner = UserRelatedField(required=False) owner = UserRelatedField(required=False)
status = ProjectRelatedField(slug_field="name") status = ProjectRelatedField(slug_field="name")
assigned_to = UserRelatedField(required=False) assigned_to = UserRelatedField(required=False)
@ -520,7 +561,6 @@ class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, History
severity = ProjectRelatedField(slug_field="name") severity = ProjectRelatedField(slug_field="name")
type = ProjectRelatedField(slug_field="name") type = ProjectRelatedField(slug_field="name")
milestone = ProjectRelatedField(slug_field="name", required=False) milestone = ProjectRelatedField(slug_field="name", required=False)
watchers = UserRelatedField(many=True, required=False)
votes = serializers.SerializerMethodField("get_votes") votes = serializers.SerializerMethodField("get_votes")
modified_date = serializers.DateTimeField(required=False) modified_date = serializers.DateTimeField(required=False)
@ -536,10 +576,9 @@ class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, History
class WikiPageExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, class WikiPageExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin,
serializers.ModelSerializer): WatcheableObjectModelSerializer):
owner = UserRelatedField(required=False) owner = UserRelatedField(required=False)
last_modifier = UserRelatedField(required=False) last_modifier = UserRelatedField(required=False)
watchers = UserRelatedField(many=True, required=False)
modified_date = serializers.DateTimeField(required=False) modified_date = serializers.DateTimeField(required=False)
class Meta: class Meta:
@ -586,7 +625,7 @@ class TimelineExportSerializer(serializers.ModelSerializer):
exclude = ('id', 'project', 'namespace', 'object_id') exclude = ('id', 'project', 'namespace', 'object_id')
class ProjectExportSerializer(serializers.ModelSerializer): class ProjectExportSerializer(WatcheableObjectModelSerializer):
owner = UserRelatedField(required=False) owner = UserRelatedField(required=False)
default_points = serializers.SlugRelatedField(slug_field="name", required=False) default_points = serializers.SlugRelatedField(slug_field="name", required=False)
default_us_status = serializers.SlugRelatedField(slug_field="name", required=False) default_us_status = serializers.SlugRelatedField(slug_field="name", required=False)

View File

@ -71,6 +71,7 @@ def store_project(data):
if serialized.is_valid(): if serialized.is_valid():
serialized.object._importing = True serialized.object._importing = True
serialized.object.save() serialized.object.save()
serialized.save_watchers()
return serialized return serialized
add_errors("project", serialized.errors) add_errors("project", serialized.errors)
return None return None
@ -217,6 +218,7 @@ def store_task(project, data):
serialized.object._not_notify = True serialized.object._not_notify = True
serialized.save() serialized.save()
serialized.save_watchers()
if serialized.object.ref: if serialized.object.ref:
sequence_name = refs.make_sequence_name(project) sequence_name = refs.make_sequence_name(project)
@ -257,6 +259,7 @@ def store_milestone(project, milestone):
serialized.object.project = project serialized.object.project = project
serialized.object._importing = True serialized.object._importing = True
serialized.save() serialized.save()
serialized.save_watchers()
for task_without_us in milestone.get("tasks_without_us", []): for task_without_us in milestone.get("tasks_without_us", []):
task_without_us["user_story"] = None task_without_us["user_story"] = None
@ -320,6 +323,7 @@ def store_wiki_page(project, wiki_page):
serialized.object._importing = True serialized.object._importing = True
serialized.object._not_notify = True serialized.object._not_notify = True
serialized.save() serialized.save()
serialized.save_watchers()
for attachment in wiki_page.get("attachments", []): for attachment in wiki_page.get("attachments", []):
store_attachment(project, serialized.object, attachment) store_attachment(project, serialized.object, attachment)
@ -382,6 +386,7 @@ def store_user_story(project, data):
serialized.object._not_notify = True serialized.object._not_notify = True
serialized.save() serialized.save()
serialized.save_watchers()
if serialized.object.ref: if serialized.object.ref:
sequence_name = refs.make_sequence_name(project) sequence_name = refs.make_sequence_name(project)
@ -442,6 +447,7 @@ def store_issue(project, data):
serialized.object._not_notify = True serialized.object._not_notify = True
serialized.save() serialized.save()
serialized.save_watchers()
if serialized.object.ref: if serialized.object.ref:
sequence_name = refs.make_sequence_name(project) sequence_name = refs.make_sequence_name(project)

View File

@ -32,7 +32,6 @@ USER_PERMISSIONS = [
('view_milestones', _('View milestones')), ('view_milestones', _('View milestones')),
('view_us', _('View user stories')), ('view_us', _('View user stories')),
('view_issues', _('View issues')), ('view_issues', _('View issues')),
('vote_issues', _('Vote issues')),
('view_tasks', _('View tasks')), ('view_tasks', _('View tasks')),
('view_wiki_pages', _('View wiki pages')), ('view_wiki_pages', _('View wiki pages')),
('view_wiki_links', _('View wiki links')), ('view_wiki_links', _('View wiki links')),
@ -41,15 +40,20 @@ USER_PERMISSIONS = [
('add_comments_to_us', _('Add comments to user stories')), ('add_comments_to_us', _('Add comments to user stories')),
('add_comments_to_task', _('Add comments to tasks')), ('add_comments_to_task', _('Add comments to tasks')),
('add_issue', _('Add issues')), ('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')), ('add_wiki_page', _('Add wiki page')),
('modify_wiki_page', _('Modify wiki page')), ('modify_wiki_page', _('Modify wiki page')),
('add_wiki_link', _('Add wiki link')), ('add_wiki_link', _('Add wiki link')),
('modify_wiki_link', _('Modify 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 = [ MEMBERS_PERMISSIONS = [
('view_project', _('View project')), ('view_project', _('View project')),
('star_project', _('Star project')),
# Milestone permissions # Milestone permissions
('view_milestones', _('View milestones')), ('view_milestones', _('View milestones')),
('add_milestone', _('Add milestone')), ('add_milestone', _('Add milestone')),
@ -60,17 +64,19 @@ MEMBERS_PERMISSIONS = [
('add_us', _('Add user story')), ('add_us', _('Add user story')),
('modify_us', _('Modify user story')), ('modify_us', _('Modify user story')),
('delete_us', _('Delete user story')), ('delete_us', _('Delete user story')),
('vote_us', _('Vote user story')),
# Task permissions # Task permissions
('view_tasks', _('View tasks')), ('view_tasks', _('View tasks')),
('add_task', _('Add task')), ('add_task', _('Add task')),
('modify_task', _('Modify task')), ('modify_task', _('Modify task')),
('delete_task', _('Delete task')), ('delete_task', _('Delete task')),
('vote_task', _('Vote task')),
# Issue permissions # Issue permissions
('view_issues', _('View issues')), ('view_issues', _('View issues')),
('vote_issues', _('Vote issues')),
('add_issue', _('Add issue')), ('add_issue', _('Add issue')),
('modify_issue', _('Modify issue')), ('modify_issue', _('Modify issue')),
('delete_issue', _('Delete issue')), ('delete_issue', _('Delete issue')),
('vote_issue', _('Vote issue')),
# Wiki page permissions # Wiki page permissions
('view_wiki_pages', _('View wiki pages')), ('view_wiki_pages', _('View wiki pages')),
('add_wiki_page', _('Add wiki page')), ('add_wiki_page', _('Add wiki page')),

View File

@ -15,11 +15,12 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.projects.models import Membership, Project
from .permissions import OWNERS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS from .permissions import OWNERS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
from django.apps import apps
def _get_user_project_membership(user, project): def _get_user_project_membership(user, project):
Membership = apps.get_model("projects", "Membership")
if user.is_anonymous(): if user.is_anonymous():
return None return None
@ -30,7 +31,7 @@ def _get_user_project_membership(user, project):
def _get_object_project(obj): def _get_object_project(obj):
project = None project = None
Project = apps.get_model("projects", "Project")
if isinstance(obj, Project): if isinstance(obj, Project):
project = obj project = obj
elif obj and hasattr(obj, 'project'): elif obj and hasattr(obj, 'project'):

View File

@ -17,7 +17,10 @@
from django.contrib import admin from django.contrib import admin
from taiga.projects.milestones.admin import MilestoneInline 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 taiga.users.admin import RoleInline
from . import models from . import models
class MembershipAdmin(admin.ModelAdmin): class MembershipAdmin(admin.ModelAdmin):
@ -35,7 +38,7 @@ class ProjectAdmin(admin.ModelAdmin):
list_display = ["name", "owner", "created_date", "total_milestones", list_display = ["name", "owner", "created_date", "total_milestones",
"total_story_points"] "total_story_points"]
list_display_links = list_display list_display_links = list_display
inlines = [RoleInline, MembershipInline, MilestoneInline] inlines = [RoleInline, MembershipInline, MilestoneInline, WatchedInline, VoteInline]
def get_object(self, *args, **kwargs): def get_object(self, *args, **kwargs):
self.obj = super().get_object(*args, **kwargs) self.obj = super().get_object(*args, **kwargs)

View File

@ -31,6 +31,7 @@ from taiga.base.api.utils import get_object_or_404
from taiga.base.utils.slug import slugify_uniquely from taiga.base.utils.slug import slugify_uniquely
from taiga.projects.history.mixins import HistoryResourceMixin 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.ordering import BulkUpdateOrderMixin
from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin
@ -44,15 +45,14 @@ from . import models
from . import permissions from . import permissions
from . import services from . import services
from .votes import serializers as votes_serializers from .votes.mixins.viewsets import StarredResourceMixin, VotersViewSetMixin
from .votes import services as votes_service
from .votes.utils import attach_votescount_to_queryset
###################################################### ######################################################
## Project ## Project
###################################################### ######################################################
class ProjectViewSet(HistoryResourceMixin, ModelCrudViewSet): class ProjectViewSet(StarredResourceMixin, HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet):
queryset = models.Project.objects.all()
serializer_class = serializers.ProjectDetailSerializer serializer_class = serializers.ProjectDetailSerializer
admin_serializer_class = serializers.ProjectDetailAdminSerializer admin_serializer_class = serializers.ProjectDetailAdminSerializer
list_serializer_class = serializers.ProjectSerializer list_serializer_class = serializers.ProjectSerializer
@ -61,6 +61,11 @@ class ProjectViewSet(HistoryResourceMixin, ModelCrudViewSet):
filter_fields = (('member', 'members'),) filter_fields = (('member', 'members'),)
order_by_fields = ("memberships__user_order",) 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"]) @list_route(methods=["POST"])
def bulk_update_order(self, request, **kwargs): def bulk_update_order(self, request, **kwargs):
if self.request.user.is_anonymous(): 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) services.update_projects_order_in_bulk(data, "user_order", request.user)
return response.NoContent(data=None) 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): def get_serializer_class(self):
if self.action == "list": if self.action == "list":
return self.list_serializer_class return self.list_serializer_class
@ -166,29 +167,6 @@ class ProjectViewSet(HistoryResourceMixin, ModelCrudViewSet):
self.check_permissions(request, "tags_colors", project) self.check_permissions(request, "tags_colors", project)
return response.Ok(dict(project.tags_colors)) 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"]) @detail_route(methods=["POST"])
def create_template(self, request, **kwargs): def create_template(self, request, **kwargs):
template_name = request.DATA.get('template_name', None) template_name = request.DATA.get('template_name', None)
@ -287,6 +265,14 @@ class ProjectViewSet(HistoryResourceMixin, ModelCrudViewSet):
return response.NoContent() 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 ## Custom values for selectors

View File

@ -27,12 +27,7 @@ def connect_memberships_signals():
sender=apps.get_model("projects", "Membership"), sender=apps.get_model("projects", "Membership"),
dispatch_uid='membership_pre_delete') dispatch_uid='membership_pre_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_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.
signals.post_save.connect(handlers.create_notify_policy, signals.post_save.connect(handlers.create_notify_policy,
sender=apps.get_model("projects", "Membership"), sender=apps.get_model("projects", "Membership"),
dispatch_uid='create-notify-policy') dispatch_uid='create-notify-policy')
@ -67,7 +62,6 @@ def connect_task_status_signals():
def disconnect_memberships_signals(): def disconnect_memberships_signals():
signals.pre_delete.disconnect(sender=apps.get_model("projects", "Membership"), dispatch_uid='membership_pre_delete') 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') signals.post_save.disconnect(sender=apps.get_model("projects", "Membership"), dispatch_uid='create-notify-policy')

View File

@ -288,7 +288,7 @@ def userstory_freezer(us) -> dict:
"milestone": us.milestone_id, "milestone": us.milestone_id,
"client_requirement": us.client_requirement, "client_requirement": us.client_requirement,
"team_requirement": us.team_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), "attachments": extract_attachments(us),
"tags": us.tags, "tags": us.tags,
"points": points, "points": points,
@ -315,7 +315,7 @@ def issue_freezer(issue) -> dict:
"description": issue.description, "description": issue.description,
"description_html": mdrender(issue.project, issue.description), "description_html": mdrender(issue.project, issue.description),
"assigned_to": issue.assigned_to_id, "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), "attachments": extract_attachments(issue),
"tags": issue.tags, "tags": issue.tags,
"is_blocked": issue.is_blocked, "is_blocked": issue.is_blocked,
@ -337,7 +337,7 @@ def task_freezer(task) -> dict:
"description": task.description, "description": task.description,
"description_html": mdrender(task.project, task.description), "description_html": mdrender(task.project, task.description),
"assigned_to": task.assigned_to_id, "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), "attachments": extract_attachments(task),
"taskboard_order": task.taskboard_order, "taskboard_order": task.taskboard_order,
"us_order": task.us_order, "us_order": task.us_order,
@ -359,7 +359,7 @@ def wikipage_freezer(wiki) -> dict:
"owner": wiki.owner_id, "owner": wiki.owner_id,
"content": wiki.content, "content": wiki.content,
"content_html": mdrender(wiki.project, 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), "attachments": extract_attachments(wiki),
} }

View File

@ -17,13 +17,16 @@
from django.contrib import admin from django.contrib import admin
from taiga.projects.attachments.admin import AttachmentInline from taiga.projects.attachments.admin import AttachmentInline
from taiga.projects.notifications.admin import WatchedInline
from taiga.projects.votes.admin import VoteInline
from . import models from . import models
class IssueAdmin(admin.ModelAdmin): class IssueAdmin(admin.ModelAdmin):
list_display = ["project", "milestone", "ref", "subject",] list_display = ["project", "milestone", "ref", "subject",]
list_display_links = ["ref", "subject",] list_display_links = ["ref", "subject",]
# inlines = [AttachmentInline] inlines = [WatchedInline, VoteInline]
def get_object(self, *args, **kwargs): def get_object(self, *args, **kwargs):
self.obj = super().get_object(*args, **kwargs) self.obj = super().get_object(*args, **kwargs)

View File

@ -16,7 +16,7 @@
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.db.models import Q 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 filters
from taiga.base import exceptions as exc 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.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.occ import OCCResourceMixin
from taiga.projects.history.mixins import HistoryResourceMixin from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.models import Project, IssueStatus, Severity, Priority, IssueType from taiga.projects.models import Project, IssueStatus, Severity, Priority, IssueType
from taiga.projects.milestones.models import Milestone from taiga.projects.milestones.models import Milestone
from taiga.projects.votes.utils import attach_votescount_to_queryset from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
from taiga.projects.votes import services as votes_service
from taiga.projects.votes import serializers as votes_serializers
from . import models from . import models
from . import services from . import services
from . import permissions from . import permissions
from . import serializers 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, ) permission_classes = (permissions.IssuePermission, )
filter_backends = (filters.CanViewIssuesFilterBackend, filter_backends = (filters.CanViewIssuesFilterBackend,
filters.OwnersFilter, filters.OwnersFilter,
@ -52,6 +53,7 @@ class IssueViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
filters.SeveritiesFilter, filters.SeveritiesFilter,
filters.PrioritiesFilter, filters.PrioritiesFilter,
filters.TagsFilter, filters.TagsFilter,
filters.WatchersFilter,
filters.QFilter, filters.QFilter,
filters.OrderByFilterMixin) filters.OrderByFilterMixin)
retrieve_exclude_filters = (filters.OwnersFilter, retrieve_exclude_filters = (filters.OwnersFilter,
@ -60,11 +62,11 @@ class IssueViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
filters.IssueTypesFilter, filters.IssueTypesFilter,
filters.SeveritiesFilter, filters.SeveritiesFilter,
filters.PrioritiesFilter, filters.PrioritiesFilter,
filters.TagsFilter,) filters.TagsFilter,
filters.WatchersFilter,)
filter_fields = ("project", filter_fields = ("project",
"status__is_closed", "status__is_closed")
"watchers")
order_by_fields = ("type", order_by_fields = ("type",
"status", "status",
"severity", "severity",
@ -139,10 +141,10 @@ class IssueViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
return super().update(request, *args, **kwargs) return super().update(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
qs = models.Issue.objects.all() qs = super().get_queryset()
qs = qs.prefetch_related("attachments") qs = qs.prefetch_related("attachments")
qs = attach_votescount_to_queryset(qs, as_field="votes_count") qs = self.attach_votes_attrs_to_queryset(qs)
return qs return self.attach_watchers_attrs_to_queryset(qs)
def pre_save(self, obj): def pre_save(self, obj):
if not obj.id: if not obj.id:
@ -237,51 +239,12 @@ class IssueViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
return response.BadRequest(serializer.errors) 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) class IssueVotersViewSet(VotersViewSetMixin, ModelListViewSet):
permission_classes = (permissions.IssueVotersPermission,)
votes_service.add_vote(issue, user=request.user) resource_model = models.Issue
return response.Ok()
@detail_route(methods=['post'])
def downvote(self, request, pk=None):
issue = get_object_or_404(models.Issue, pk=pk)
self.check_permissions(request, 'downvote', issue)
votes_service.remove_vote(issue, user=request.user)
return response.Ok()
class VotersViewSet(ModelListViewSet): class IssueWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
serializer_class = votes_serializers.VoterSerializer permission_classes = (permissions.IssueWatchersPermission,)
list_serializer_class = votes_serializers.VoterSerializer resource_model = models.Issue
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)

View File

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

View File

@ -31,10 +31,12 @@ class IssuePermission(TaigaResourcePermission):
list_perms = AllowAny() list_perms = AllowAny()
filters_data_perms = AllowAny() filters_data_perms = AllowAny()
csv_perms = AllowAny() csv_perms = AllowAny()
upvote_perms = IsAuthenticated() & HasProjectPerm('vote_issues')
downvote_perms = IsAuthenticated() & HasProjectPerm('vote_issues')
bulk_create_perms = HasProjectPerm('add_issue') bulk_create_perms = HasProjectPerm('add_issue')
delete_comment_perms= HasProjectPerm('modify_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): class HasIssueIdUrlParam(PermissionComponent):
@ -49,8 +51,11 @@ class IssueVotersPermission(TaigaResourcePermission):
enought_perms = IsProjectOwner() | IsSuperUser() enought_perms = IsProjectOwner() | IsSuperUser()
global_perms = None global_perms = None
retrieve_perms = HasProjectPerm('view_issues') retrieve_perms = HasProjectPerm('view_issues')
create_perms = HasProjectPerm('add_issue') list_perms = HasProjectPerm('view_issues')
update_perms = HasProjectPerm('modify_issue')
partial_update_perms = HasProjectPerm('modify_issue')
destroy_perms = HasProjectPerm('delete_issue') class IssueWatchersPermission(TaigaResourcePermission):
enought_perms = IsProjectOwner() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_issues')
list_perms = HasProjectPerm('view_issues') list_perms = HasProjectPerm('view_issues')

View File

@ -19,17 +19,19 @@ from taiga.base.fields import TagsField
from taiga.base.fields import PgArrayField from taiga.base.fields import PgArrayField
from taiga.base.neighbors import NeighborsSerializerMixin from taiga.base.neighbors import NeighborsSerializerMixin
from taiga.mdrender.service import render as mdrender from taiga.mdrender.service import render as mdrender
from taiga.projects.validators import ProjectExistsValidator from taiga.projects.validators import ProjectExistsValidator
from taiga.projects.notifications.validators import WatchersValidator from taiga.projects.notifications.validators import WatchersValidator
from taiga.projects.serializers import BasicIssueStatusSerializer 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 taiga.users.serializers import UserBasicInfoSerializer
from . import models from . import models
class IssueSerializer(WatchersValidator, serializers.ModelSerializer): class IssueSerializer(WatchersValidator, VotedResourceSerializerMixin, WatchedResourceModelSerializer, serializers.ModelSerializer):
tags = TagsField(required=False) tags = TagsField(required=False)
external_reference = PgArrayField(required=False) external_reference = PgArrayField(required=False)
is_closed = serializers.Field(source="is_closed") 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") generated_user_stories = serializers.SerializerMethodField("get_generated_user_stories")
blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html") blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html")
description_html = serializers.SerializerMethodField("get_description_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) status_extra_info = BasicIssueStatusSerializer(source="status", required=False, read_only=True)
assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", 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) 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): def get_description_html(self, obj):
return mdrender(obj.project, obj.description) 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 IssueListSerializer(IssueSerializer):
class Meta: class Meta:

View File

@ -27,7 +27,7 @@ from taiga.base.utils import db, text
from taiga.projects.issues.apps import ( from taiga.projects.issues.apps import (
connect_issues_signals, connect_issues_signals,
disconnect_issues_signals) disconnect_issues_signals)
from taiga.projects.votes import services as votes_services
from . import models from . import models
@ -84,7 +84,8 @@ def issues_to_csv(project, queryset):
fieldnames = ["ref", "subject", "description", "milestone", "owner", fieldnames = ["ref", "subject", "description", "milestone", "owner",
"owner_full_name", "assigned_to", "assigned_to_full_name", "owner_full_name", "assigned_to", "assigned_to_full_name",
"status", "severity", "priority", "type", "is_closed", "status", "severity", "priority", "type", "is_closed",
"attachments", "external_reference", "tags"] "attachments", "external_reference", "tags",
"watchers", "voters"]
for custom_attr in project.issuecustomattributes.all(): for custom_attr in project.issuecustomattributes.all():
fieldnames.append(custom_attr.name) fieldnames.append(custom_attr.name)
@ -108,6 +109,8 @@ def issues_to_csv(project, queryset):
"attachments": issue.attachments.count(), "attachments": issue.attachments.count(),
"external_reference": issue.external_reference, "external_reference": issue.external_reference,
"tags": ",".join(issue.tags or []), "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(): for custom_attr in project.issuecustomattributes.all():

View File

@ -37,6 +37,7 @@ from taiga.projects.wiki.models import *
from taiga.projects.attachments.models import * from taiga.projects.attachments.models import *
from taiga.projects.custom_attributes.models import * from taiga.projects.custom_attributes.models import *
from taiga.projects.history.services import take_snapshot from taiga.projects.history.services import take_snapshot
from taiga.projects.votes.services import add_vote
from taiga.events.apps import disconnect_events_signals 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_USS_BACK = getattr(settings, "SAMPLE_DATA_NUM_USS_BACK", (8, 20))
NUM_ISSUES = getattr(settings, "SAMPLE_DATA_NUM_ISSUES", (12, 25)) NUM_ISSUES = getattr(settings, "SAMPLE_DATA_NUM_ISSUES", (12, 25))
NUM_ATTACHMENTS = getattr(settings, "SAMPLE_DATA_NUM_ATTACHMENTS", (0, 4)) NUM_ATTACHMENTS = getattr(settings, "SAMPLE_DATA_NUM_ATTACHMENTS", (0, 4))
NUM_VOTES = getattr(settings, "SAMPLE_DATA_NUM_VOTES", (0, 3))
NUM_PROJECT_WATCHERS = getattr(settings, "SAMPLE_DATA_NUM_PROJECT_WATCHERS", (0, 3))
class Command(BaseCommand): class Command(BaseCommand):
sd = SampleDataHelper(seed=12345678901) 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.total_story_points = int(defined_points * self.sd.int(5,12) / 10)
project.save() project.save()
self.create_votes(project, project)
def create_attachment(self, obj, order): def create_attachment(self, obj, order):
attached_file = self.sd.file_from_directory(*ATTACHMENT_SAMPLE_DATA) attached_file = self.sd.file_from_directory(*ATTACHMENT_SAMPLE_DATA)
@ -287,7 +290,7 @@ class Command(BaseCommand):
bug.save() bug.save()
watching_user = self.sd.db_object_from_queryset(project.memberships.filter(user__isnull=False)).user 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, take_snapshot(bug,
comment=self.sd.paragraph(), comment=self.sd.paragraph(),
@ -300,6 +303,7 @@ class Command(BaseCommand):
comment=self.sd.paragraph(), comment=self.sd.paragraph(),
user=bug.owner) user=bug.owner)
self.create_votes(bug, project)
return bug return bug
def create_task(self, project, milestone, us, min_date, max_date, closed=False): def create_task(self, project, milestone, us, min_date, max_date, closed=False):
@ -338,7 +342,7 @@ class Command(BaseCommand):
user=task.owner) user=task.owner)
watching_user = self.sd.db_object_from_queryset(project.memberships.filter(user__isnull=False)).user 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 # Add history entry
task.status=self.sd.db_object_from_queryset(project.task_statuses.all()) task.status=self.sd.db_object_from_queryset(project.task_statuses.all())
@ -347,6 +351,7 @@ class Command(BaseCommand):
comment=self.sd.paragraph(), comment=self.sd.paragraph(),
user=task.owner) user=task.owner)
self.create_votes(task, project)
return task return task
def create_us(self, project, milestone=None, computable_project_roles=[]): def create_us(self, project, milestone=None, computable_project_roles=[]):
@ -387,7 +392,7 @@ class Command(BaseCommand):
us.save() us.save()
watching_user = self.sd.db_object_from_queryset(project.memberships.filter(user__isnull=False)).user 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, take_snapshot(us,
comment=self.sd.paragraph(), comment=self.sd.paragraph(),
@ -400,6 +405,7 @@ class Command(BaseCommand):
comment=self.sd.paragraph(), comment=self.sd.paragraph(),
user=us.owner) user=us.owner)
self.create_votes(us, project)
return us return us
def create_milestone(self, project, start_date, end_date): def create_milestone(self, project, start_date, end_date):
@ -434,6 +440,11 @@ class Command(BaseCommand):
project.is_kanban_activated = True project.is_kanban_activated = True
project.save() project.save()
take_snapshot(project, user=project.owner) take_snapshot(project, user=project.owner)
for i in range(self.sd.int(*NUM_PROJECT_WATCHERS)):
watching_user = self.sd.db_object_from_queryset(User.objects.all())
project.add_watcher(watching_user)
return project return project
def create_user(self, counter=None, username=None, full_name=None, email=None): def create_user(self, counter=None, username=None, full_name=None, email=None):
@ -452,3 +463,8 @@ class Command(BaseCommand):
user.save() user.save()
return user 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)

View File

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

View File

@ -15,6 +15,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.contrib import admin from django.contrib import admin
from taiga.projects.notifications.admin import WatchedInline
from taiga.projects.votes.admin import VoteInline
from . import models from . import models
@ -30,6 +32,7 @@ class MilestoneAdmin(admin.ModelAdmin):
list_display_links = list_display list_display_links = list_display
list_filter = ["project"] list_filter = ["project"]
readonly_fields = ["owner"] readonly_fields = ["owner"]
inlines = [WatchedInline, VoteInline]
admin.site.register(models.Milestone, MilestoneAdmin) admin.site.register(models.Milestone, MilestoneAdmin)

View File

@ -17,10 +17,10 @@
from taiga.base import filters from taiga.base import filters
from taiga.base import response from taiga.base import response
from taiga.base.decorators import detail_route 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.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.history.mixins import HistoryResourceMixin
@ -36,17 +36,17 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudView
permission_classes = (permissions.MilestonePermission,) permission_classes = (permissions.MilestonePermission,)
filter_backends = (filters.CanViewMilestonesFilterBackend,) filter_backends = (filters.CanViewMilestonesFilterBackend,)
filter_fields = ("project", "closed") filter_fields = ("project", "closed")
queryset = models.Milestone.objects.all()
def get_queryset(self): 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", qs = qs.prefetch_related("user_stories",
"user_stories__role_points", "user_stories__role_points",
"user_stories__role_points__points", "user_stories__role_points__points",
"user_stories__role_points__role", "user_stories__role_points__role",
"user_stories__generated_from_issue", "user_stories__generated_from_issue",
"user_stories__project", "user_stories__project")
"watchers",
"user_stories__watchers")
qs = qs.select_related("project") qs = qs.select_related("project")
qs = qs.order_by("-estimated_start") qs = qs.order_by("-estimated_start")
return qs return qs
@ -93,3 +93,8 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudView
optimal_points -= optimal_points_per_day optimal_points -= optimal_points_per_day
return response.Ok(milestone_stats) return response.Ok(milestone_stats)
class MilestoneWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
permission_classes = (permissions.MilestoneWatchersPermission,)
resource_model = models.Milestone

View File

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

View File

@ -15,8 +15,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm, from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
IsProjectOwner, AllowAny, IsAuthenticated, IsProjectOwner, AllowAny,
PermissionComponent, IsSuperUser) IsSuperUser)
class MilestonePermission(TaigaResourcePermission): class MilestonePermission(TaigaResourcePermission):
@ -29,3 +29,11 @@ class MilestonePermission(TaigaResourcePermission):
destroy_perms = HasProjectPerm('delete_milestone') destroy_perms = HasProjectPerm('delete_milestone')
list_perms = AllowAny() list_perms = AllowAny()
stats_perms = HasProjectPerm('view_milestones') 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')

View File

@ -17,14 +17,15 @@
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.utils import json 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 ..userstories.serializers import UserStorySerializer
from . import models from . import models
class MilestoneSerializer(serializers.ModelSerializer): class MilestoneSerializer(WatchersValidator, WatchedResourceModelSerializer, serializers.ModelSerializer):
user_stories = UserStorySerializer(many=True, required=False, read_only=True) user_stories = UserStorySerializer(many=True, required=False, read_only=True)
total_points = serializers.SerializerMethodField("get_total_points") total_points = serializers.SerializerMethodField("get_total_points")
closed_points = serializers.SerializerMethodField("get_closed_points") closed_points = serializers.SerializerMethodField("get_closed_points")

View File

@ -40,6 +40,8 @@ from taiga.base.utils.slug import slugify_uniquely_for_queryset
from . import choices from . import choices
from . notifications.mixins import WatchedModelMixin
class Membership(models.Model): class Membership(models.Model):
# This model stores all project memberships. Also # This model stores all project memberships. Also
@ -118,7 +120,7 @@ class ProjectDefaults(models.Model):
abstract = True 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, name = models.CharField(max_length=250, null=False, blank=False,
verbose_name=_("name")) verbose_name=_("name"))
slug = models.SlugField(max_length=250, unique=True, null=False, blank=True, slug = models.SlugField(max_length=250, unique=True, null=False, blank=True,

View File

@ -0,0 +1,25 @@
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.contrib import admin
from django.contrib.contenttypes.admin import GenericTabularInline
from . import models
class WatchedInline(GenericTabularInline):
model = models.Watched
extra = 0

View File

@ -19,8 +19,9 @@ from django.db.models import Q
from taiga.base.api import ModelCrudViewSet from taiga.base.api import ModelCrudViewSet
from taiga.projects.notifications.choices import NotifyLevel from taiga.projects.notifications.choices import NotifyLevel
from taiga.projects.notifications.models import Watched
from taiga.projects.models import Project from taiga.projects.models import Project
from taiga.users import services as user_services
from . import serializers from . import serializers
from . import models from . import models
from . import permissions from . import permissions
@ -32,9 +33,13 @@ class NotifyPolicyViewSet(ModelCrudViewSet):
permission_classes = (permissions.NotifyPolicyPermission,) permission_classes = (permissions.NotifyPolicyPermission,)
def _build_needed_notify_policies(self): 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( projects = Project.objects.filter(
Q(owner=self.request.user) | 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() ).distinct()
for project in projects: for project in projects:
@ -45,5 +50,14 @@ class NotifyPolicyViewSet(ModelCrudViewSet):
return models.NotifyPolicy.objects.none() return models.NotifyPolicy.objects.none()
self._build_needed_notify_policies() 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()

View File

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

View File

@ -17,14 +17,26 @@
from functools import partial from functools import partial
from operator import is_not 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.db import models
from django.utils.translation import ugettext_lazy as _ 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 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 Rest Framework resource mixin for resources susceptible
to be notifiable about their changes. to be notifiable about their changes.
@ -36,6 +48,27 @@ class WatchedResourceMixin(object):
_not_notify = False _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): def send_notifications(self, obj, history=None):
""" """
Shortcut method for resources with special save Shortcut method for resources with special save
@ -73,7 +106,7 @@ class WatchedResourceMixin(object):
super().pre_delete(obj) super().pre_delete(obj)
class WatchedModelMixin(models.Model): class WatchedModelMixin(object):
""" """
Generic model mixin that makes model compatible Generic model mixin that makes model compatible
with notification system. with notification system.
@ -82,11 +115,6 @@ class WatchedModelMixin(models.Model):
this mixin if you want send notifications about this mixin if you want send notifications about
your model class. 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: def get_project(self) -> object:
""" """
@ -112,7 +140,16 @@ class WatchedModelMixin(models.Model):
very inefficient way for obtain watchers but at very inefficient way for obtain watchers but at
this momment is the simplest way. 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: def get_owner(self) -> object:
""" """
@ -140,3 +177,79 @@ class WatchedModelMixin(models.Model):
self.get_owner(),) self.get_owner(),)
is_not_none = partial(is_not, None) is_not_none = partial(is_not, None)
return frozenset(filter(is_not_none, participants)) 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)

View File

@ -14,13 +14,15 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf import settings
from django.contrib.contenttypes import generic
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils import timezone from django.utils import timezone
from taiga.projects.history.choices import HISTORY_TYPE_CHOICES 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): class NotifyPolicy(models.Model):
@ -72,3 +74,19 @@ class HistoryChangeNotification(models.Model):
class Meta: class Meta:
unique_together = ("key", "owner", "project", "history_type") 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")

View File

@ -17,9 +17,10 @@
import json import json
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.users.models import User
from . import models from . import models
from . import choices
class NotifyPolicySerializer(serializers.ModelSerializer): class NotifyPolicySerializer(serializers.ModelSerializer):
@ -31,3 +32,11 @@ class NotifyPolicySerializer(serializers.ModelSerializer):
def get_project_name(self, obj): def get_project_name(self, obj):
return obj.project.name 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')

View File

@ -17,10 +17,11 @@
from functools import partial from functools import partial
from django.apps import apps 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.contenttypes.models import ContentType
from django.contrib.auth import get_user_model
from django.utils import timezone from django.utils import timezone
from django.db import transaction
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext as _ 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.permissions.service import user_has_perm
from taiga.users.models import User from taiga.users.models import User
from .models import HistoryChangeNotification from .models import HistoryChangeNotification, Watched
def notify_policy_exists(project, user) -> bool: def notify_policy_exists(project, user) -> bool:
@ -121,11 +122,11 @@ def analize_object_for_watchers(obj:object, history:object):
if data["mentions"]: if data["mentions"]:
for user in 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 # Adding the person who edited the object to the watchers
if history.comment and not history.owner.is_system: 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): def _filter_by_permissions(obj, user):
UserStory = apps.get_model("userstories", "UserStory") UserStory = apps.get_model("userstories", "UserStory")
@ -170,15 +171,19 @@ def get_users_to_notify(obj, *, discard_users=None) -> list:
candidates = set() candidates = set()
candidates.update(filter(_can_notify_hard, project.members.all())) 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.get_watchers()))
candidates.update(filter(_can_notify_light, obj.project.get_watchers()))
candidates.update(filter(_can_notify_light, obj.get_participants())) 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 # Remove the changer from candidates
if discard_users: if discard_users:
candidates = candidates - set(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 # Filter disabled and system users
candidates = filter(partial(_filter_notificable), candidates) candidates = set(filter(partial(_filter_notificable), candidates))
return frozenset(candidates) return frozenset(candidates)
@ -282,3 +287,72 @@ def send_sync_notifications(notification_id):
def process_sync_notifications(): def process_sync_notifications():
for notification in HistoryChangeNotification.objects.all(): for notification in HistoryChangeNotification.objects.all():
send_sync_notifications(notification.pk) 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()

View File

@ -0,0 +1,63 @@
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014 Anler Hernández <hello@anler.me>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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

View File

@ -21,7 +21,7 @@ from taiga.base.api import serializers
class WatchersValidator: class WatchersValidator:
def validate_watchers(self, attrs, source): def validate_watchers(self, attrs, source):
users = attrs[source] users = attrs.get(source, [])
# Try obtain a valid project # Try obtain a valid project
if self.object is None and "project" in attrs: if self.object is None and "project" in attrs:
@ -39,7 +39,8 @@ class WatchersValidator:
# Check if incoming watchers are contained # Check if incoming watchers are contained
# in project members list # 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: if result:
raise serializers.ValidationError(_("Watchers contains invalid users")) raise serializers.ValidationError(_("Watchers contains invalid users"))

View File

@ -54,19 +54,34 @@ class ProjectPermission(TaigaResourcePermission):
list_perms = AllowAny() list_perms = AllowAny()
stats_perms = HasProjectPerm('view_project') stats_perms = HasProjectPerm('view_project')
member_stats_perms = HasProjectPerm('view_project') member_stats_perms = HasProjectPerm('view_project')
issues_stats_perms = HasProjectPerm('view_project')
regenerate_userstories_csv_uuid_perms = IsProjectOwner() regenerate_userstories_csv_uuid_perms = IsProjectOwner()
regenerate_issues_csv_uuid_perms = IsProjectOwner() regenerate_issues_csv_uuid_perms = IsProjectOwner()
regenerate_tasks_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_perms = HasProjectPerm('view_project')
tags_colors_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() create_template_perms = IsSuperUser()
leave_perms = CanLeaveProject() 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): class MembershipPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project') retrieve_perms = HasProjectPerm('view_project')
create_perms = IsProjectOwner() create_perms = IsProjectOwner()

View File

@ -25,6 +25,8 @@ from taiga.base.fields import PgArrayField
from taiga.base.fields import TagsField from taiga.base.fields import TagsField
from taiga.base.fields import TagsColorsField 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.services import get_photo_or_gravatar_url
from taiga.users.serializers import UserSerializer from taiga.users.serializers import UserSerializer
from taiga.users.serializers import UserBasicInfoSerializer 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 UserStoryCustomAttributeSerializer
from .custom_attributes.serializers import TaskCustomAttributeSerializer from .custom_attributes.serializers import TaskCustomAttributeSerializer
from .custom_attributes.serializers import IssueCustomAttributeSerializer from .custom_attributes.serializers import IssueCustomAttributeSerializer
from .notifications.mixins import WatchedResourceModelSerializer
from .votes.mixins.serializers import StarredResourceSerializerMixin
###################################################### ######################################################
## Custom values for selectors ## Custom values for selectors
@ -305,11 +308,10 @@ class ProjectMemberSerializer(serializers.ModelSerializer):
## Projects ## Projects
###################################################### ######################################################
class ProjectSerializer(serializers.ModelSerializer): class ProjectSerializer(WatchersValidator, StarredResourceSerializerMixin, WatchedResourceModelSerializer, serializers.ModelSerializer):
tags = TagsField(default=[], required=False) tags = TagsField(default=[], required=False)
anon_permissions = PgArrayField(required=False) anon_permissions = PgArrayField(required=False)
public_permissions = PgArrayField(required=False) public_permissions = PgArrayField(required=False)
stars = serializers.SerializerMethodField("get_stars_number")
my_permissions = serializers.SerializerMethodField("get_my_permissions") my_permissions = serializers.SerializerMethodField("get_my_permissions")
i_am_owner = serializers.SerializerMethodField("get_i_am_owner") i_am_owner = serializers.SerializerMethodField("get_i_am_owner")
tags_colors = TagsColorsField(required=False) tags_colors = TagsColorsField(required=False)
@ -321,10 +323,6 @@ class ProjectSerializer(serializers.ModelSerializer):
exclude = ("last_us_ref", "last_task_ref", "last_issue_ref", exclude = ("last_us_ref", "last_task_ref", "last_issue_ref",
"issues_csv_uuid", "tasks_csv_uuid", "userstories_csv_uuid") "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): def get_my_permissions(self, obj):
if "request" in self.context: if "request" in self.context:
return get_user_project_permissions(self.context["request"].user, obj) return get_user_project_permissions(self.context["request"].user, obj)

View File

@ -45,24 +45,6 @@ def membership_post_delete(sender, instance, using, **kwargs):
instance.project.update_role_points() 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): def create_notify_policy(sender, instance, using, **kwargs):
if instance.user: if instance.user:
create_notify_policy_if_not_exists(instance.project, instance.user) create_notify_policy_if_not_exists(instance.project, instance.user)

View File

@ -17,6 +17,9 @@
from django.contrib import admin from django.contrib import admin
from taiga.projects.attachments.admin import AttachmentInline from taiga.projects.attachments.admin import AttachmentInline
from taiga.projects.notifications.admin import WatchedInline
from taiga.projects.votes.admin import VoteInline
from . import models from . import models
@ -24,7 +27,7 @@ class TaskAdmin(admin.ModelAdmin):
list_display = ["project", "milestone", "user_story", "ref", "subject",] list_display = ["project", "milestone", "user_story", "ref", "subject",]
list_display_links = ["ref", "subject",] list_display_links = ["ref", "subject",]
list_filter = ["project"] list_filter = ["project"]
# inlines = [AttachmentInline] inlines = [WatchedInline, VoteInline]
def get_object(self, *args, **kwargs): def get_object(self, *args, **kwargs):
self.obj = super().get_object(*args, **kwargs) self.obj = super().get_object(*args, **kwargs)

View File

@ -20,13 +20,14 @@ from taiga.base.api.utils import get_object_or_404
from taiga.base import filters, response from taiga.base import filters, response
from taiga.base import exceptions as exc from taiga.base import exceptions as exc
from taiga.base.decorators import list_route 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 taiga.projects.models import Project, TaskStatus
from django.http import HttpResponse 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.history.mixins import HistoryResourceMixin
from taiga.projects.occ import OCCResourceMixin from taiga.projects.occ import OCCResourceMixin
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
from . import models from . import models
@ -35,12 +36,14 @@ from . import serializers
from . import services from . import services
class TaskViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet): class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
model = models.Task ModelCrudViewSet):
queryset = models.Task.objects.all()
permission_classes = (permissions.TaskPermission,) 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", filter_fields = ["user_story", "milestone", "project", "assigned_to",
"status__is_closed", "watchers"] "status__is_closed"]
def get_serializer_class(self, *args, **kwargs): def get_serializer_class(self, *args, **kwargs):
if self.action in ["retrieve", "by_ref"]: if self.action in ["retrieve", "by_ref"]:
@ -82,6 +85,10 @@ class TaskViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
return super().update(request, *args, **kwargs) 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): def pre_save(self, obj):
if obj.user_story: if obj.user_story:
@ -165,3 +172,13 @@ class TaskViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
@list_route(methods=["POST"]) @list_route(methods=["POST"])
def bulk_update_us_order(self, request, **kwargs): def bulk_update_us_order(self, request, **kwargs):
return self._bulk_update_order("us_order", 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

View File

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

View File

@ -15,7 +15,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm, from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
IsProjectOwner, AllowAny, IsSuperUser) IsAuthenticated, IsProjectOwner, AllowAny,
IsSuperUser)
class TaskPermission(TaigaResourcePermission): class TaskPermission(TaigaResourcePermission):
@ -30,3 +31,21 @@ class TaskPermission(TaigaResourcePermission):
csv_perms = AllowAny() csv_perms = AllowAny()
bulk_create_perms = HasProjectPerm('add_task') bulk_create_perms = HasProjectPerm('add_task')
bulk_update_order_perms = HasProjectPerm('modify_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')

View File

@ -27,12 +27,15 @@ from taiga.projects.milestones.validators import SprintExistsValidator
from taiga.projects.tasks.validators import TaskExistsValidator from taiga.projects.tasks.validators import TaskExistsValidator
from taiga.projects.notifications.validators import WatchersValidator from taiga.projects.notifications.validators import WatchersValidator
from taiga.projects.serializers import BasicTaskStatusSerializerSerializer 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 taiga.users.serializers import UserBasicInfoSerializer
from . import models from . import models
class TaskSerializer(WatchersValidator, serializers.ModelSerializer): class TaskSerializer(WatchersValidator, VotedResourceSerializerMixin, WatchedResourceModelSerializer, serializers.ModelSerializer):
tags = TagsField(required=False, default=[]) tags = TagsField(required=False, default=[])
external_reference = PgArrayField(required=False) external_reference = PgArrayField(required=False)
comment = serializers.SerializerMethodField("get_comment") comment = serializers.SerializerMethodField("get_comment")

View File

@ -23,6 +23,7 @@ from taiga.projects.tasks.apps import (
connect_tasks_signals, connect_tasks_signals,
disconnect_tasks_signals) disconnect_tasks_signals)
from taiga.events import events from taiga.events import events
from taiga.projects.votes import services as votes_services
from . import models from . import models
@ -95,7 +96,8 @@ def tasks_to_csv(project, queryset):
fieldnames = ["ref", "subject", "description", "user_story", "milestone", "owner", fieldnames = ["ref", "subject", "description", "user_story", "milestone", "owner",
"owner_full_name", "assigned_to", "assigned_to_full_name", "owner_full_name", "assigned_to", "assigned_to_full_name",
"status", "is_iocaine", "is_closed", "us_order", "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(): for custom_attr in project.taskcustomattributes.all():
fieldnames.append(custom_attr.name) fieldnames.append(custom_attr.name)
@ -120,6 +122,8 @@ def tasks_to_csv(project, queryset):
"attachments": task.attachments.count(), "attachments": task.attachments.count(),
"external_reference": task.external_reference, "external_reference": task.external_reference,
"tags": ",".join(task.tags or []), "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(): for custom_attr in project.taskcustomattributes.all():
value = task.custom_attributes_values.attributes_values.get(str(custom_attr.id), None) value = task.custom_attributes_values.attributes_values.get(str(custom_attr.id), None)

View File

@ -17,6 +17,8 @@
from django.contrib import admin from django.contrib import admin
from taiga.projects.attachments.admin import AttachmentInline from taiga.projects.attachments.admin import AttachmentInline
from taiga.projects.notifications.admin import WatchedInline
from taiga.projects.votes.admin import VoteInline
from . import models from . import models
@ -41,7 +43,7 @@ class UserStoryAdmin(admin.ModelAdmin):
list_display = ["project", "milestone", "ref", "subject",] list_display = ["project", "milestone", "ref", "subject",]
list_display_links = ["ref", "subject",] list_display_links = ["ref", "subject",]
list_filter = ["project"] list_filter = ["project"]
inlines = [RolePointsInline] inlines = [RolePointsInline, WatchedInline, VoteInline]
def get_object(self, *args, **kwargs): def get_object(self, *args, **kwargs):
self.obj = super().get_object(*args, **kwargs) self.obj = super().get_object(*args, **kwargs)

View File

@ -28,15 +28,15 @@ from taiga.base import exceptions as exc
from taiga.base import response from taiga.base import response
from taiga.base import status from taiga.base import status
from taiga.base.decorators import list_route 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.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.history.mixins import HistoryResourceMixin
from taiga.projects.occ import OCCResourceMixin from taiga.projects.occ import OCCResourceMixin
from taiga.projects.models import Project, UserStoryStatus from taiga.projects.models import Project, UserStoryStatus
from taiga.projects.history.services import take_snapshot from taiga.projects.history.services import take_snapshot
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
from . import models from . import models
from . import permissions from . import permissions
@ -44,27 +44,29 @@ from . import serializers
from . import services from . import services
class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet): class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
model = models.UserStory ModelCrudViewSet):
queryset = models.UserStory.objects.all()
permission_classes = (permissions.UserStoryPermission,) permission_classes = (permissions.UserStoryPermission,)
filter_backends = (filters.CanViewUsFilterBackend, filter_backends = (filters.CanViewUsFilterBackend,
filters.OwnersFilter, filters.OwnersFilter,
filters.AssignedToFilter, filters.AssignedToFilter,
filters.StatusesFilter, filters.StatusesFilter,
filters.TagsFilter, filters.TagsFilter,
filters.WatchersFilter,
filters.QFilter, filters.QFilter,
filters.OrderByFilterMixin) filters.OrderByFilterMixin)
retrieve_exclude_filters = (filters.OwnersFilter, retrieve_exclude_filters = (filters.OwnersFilter,
filters.AssignedToFilter, filters.AssignedToFilter,
filters.StatusesFilter, filters.StatusesFilter,
filters.TagsFilter) filters.TagsFilter,
filters.WatchersFilter)
filter_fields = ["project", filter_fields = ["project",
"milestone", "milestone",
"milestone__isnull", "milestone__isnull",
"is_closed", "is_closed",
"status__is_archived", "status__is_archived",
"status__is_closed", "status__is_closed"]
"watchers"]
order_by_fields = ["backlog_order", order_by_fields = ["backlog_order",
"sprint_order", "sprint_order",
"kanban_order"] "kanban_order"]
@ -109,13 +111,13 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi
def get_queryset(self): def get_queryset(self):
qs = self.model.objects.all() qs = super().get_queryset()
qs = qs.prefetch_related("role_points", qs = qs.prefetch_related("role_points",
"role_points__points", "role_points__points",
"role_points__role", "role_points__role")
"watchers")
qs = qs.select_related("milestone", "project") 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): def pre_save(self, obj):
# This is very ugly hack, but having # 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) self.send_notifications(self.object.generated_from_issue, history)
return response 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

View File

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

View File

@ -30,3 +30,21 @@ class UserStoryPermission(TaigaResourcePermission):
csv_perms = AllowAny() csv_perms = AllowAny()
bulk_create_perms = IsAuthenticated() & (HasProjectPerm('add_us_to_project') | HasProjectPerm('add_us')) bulk_create_perms = IsAuthenticated() & (HasProjectPerm('add_us_to_project') | HasProjectPerm('add_us'))
bulk_update_order_perms = HasProjectPerm('modify_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')

View File

@ -27,6 +27,9 @@ from taiga.projects.validators import UserStoryStatusExistsValidator
from taiga.projects.userstories.validators import UserStoryExistsValidator from taiga.projects.userstories.validators import UserStoryExistsValidator
from taiga.projects.notifications.validators import WatchersValidator from taiga.projects.notifications.validators import WatchersValidator
from taiga.projects.serializers import BasicUserStoryStatusSerializer 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 taiga.users.serializers import UserBasicInfoSerializer
from . import models from . import models
@ -42,7 +45,7 @@ class RolePointsField(serializers.WritableField):
return json.loads(obj) return json.loads(obj)
class UserStorySerializer(WatchersValidator, serializers.ModelSerializer): class UserStorySerializer(WatchersValidator, VotedResourceSerializerMixin, WatchedResourceModelSerializer, serializers.ModelSerializer):
tags = TagsField(default=[], required=False) tags = TagsField(default=[], required=False)
external_reference = PgArrayField(required=False) external_reference = PgArrayField(required=False)
points = RolePointsField(source="role_points", required=False) points = RolePointsField(source="role_points", required=False)

View File

@ -31,6 +31,7 @@ from taiga.projects.userstories.apps import (
disconnect_userstories_signals) disconnect_userstories_signals)
from taiga.events import events from taiga.events import events
from taiga.projects.votes import services as votes_services
from . import models from . import models
@ -138,7 +139,8 @@ def userstories_to_csv(project,queryset):
"created_date", "modified_date", "finish_date", "created_date", "modified_date", "finish_date",
"client_requirement", "team_requirement", "attachments", "client_requirement", "team_requirement", "attachments",
"generated_from_issue", "external_reference", "tasks", "generated_from_issue", "external_reference", "tasks",
"tags"] "tags",
"watchers", "voters"]
for custom_attr in project.userstorycustomattributes.all(): for custom_attr in project.userstorycustomattributes.all():
fieldnames.append(custom_attr.name) fieldnames.append(custom_attr.name)
@ -170,6 +172,8 @@ def userstories_to_csv(project,queryset):
"external_reference": us.external_reference, "external_reference": us.external_reference,
"tasks": ",".join([str(task.ref) for task in us.tasks.all()]), "tasks": ",".join([str(task.ref) for task in us.tasks.all()]),
"tags": ",".join(us.tags or []), "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'): for role in us.project.roles.filter(computable=True).order_by('name'):

View File

@ -0,0 +1,25 @@
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.contrib import admin
from django.contrib.contenttypes.admin import GenericTabularInline
from . import models
class VoteInline(GenericTabularInline):
model = models.Vote
extra = 0

View File

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.utils.timezone import utc
from django.conf import settings
import datetime
class Migration(migrations.Migration):
dependencies = [
('votes', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='vote',
name='created_date',
field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2015, 8, 5, 16, 0, 40, 158374, tzinfo=utc), verbose_name='created date'),
preserve_default=False,
),
migrations.AlterField(
model_name='vote',
name='user',
field=models.ForeignKey(related_name='votes', to=settings.AUTH_USER_MODEL, verbose_name='user'),
preserve_default=True,
),
migrations.AlterField(
model_name='votes',
name='count',
field=models.PositiveIntegerField(default=0, verbose_name='count'),
preserve_default=True,
),
]

View File

View File

@ -0,0 +1,37 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.base.api import serializers
class BaseVotedResourceSerializer(serializers.ModelSerializer):
def get_votes_counter(self, obj):
# The "votes_count" attribute is attached in the get_queryset of the viewset.
return getattr(obj, "votes_count", 0) or 0
def get_is_voted(self, obj):
# The "is_voted" attribute is attached in the get_queryset of the viewset.
return getattr(obj, "is_voted", False) or False
class StarredResourceSerializerMixin(BaseVotedResourceSerializer):
stars = serializers.SerializerMethodField("get_votes_counter")
is_starred = serializers.SerializerMethodField("get_is_voted")
class VotedResourceSerializerMixin(BaseVotedResourceSerializer):
votes = serializers.SerializerMethodField("get_votes_counter")
is_voted = serializers.SerializerMethodField("get_is_voted")

View File

@ -0,0 +1,114 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.core.exceptions import ObjectDoesNotExist
from taiga.base import response
from taiga.base.api import viewsets
from taiga.base.api.utils import get_object_or_404
from taiga.base.decorators import detail_route
from taiga.projects.votes import serializers
from taiga.projects.votes import services
from taiga.projects.votes.utils import attach_votes_count_to_queryset, attach_is_vote_to_queryset
class BaseVotedResource:
# Note: Update get_queryset method:
# def get_queryset(self):
# qs = super().get_queryset()
# return self.attach_votes_attrs_to_queryset(qs)
def attach_votes_attrs_to_queryset(self, queryset):
qs = attach_votes_count_to_queryset(queryset)
if self.request.user.is_authenticated():
qs = attach_is_vote_to_queryset(self.request.user, qs)
return qs
def _add_voter(self, permission, request, pk=None):
obj = self.get_object()
self.check_permissions(request, permission, obj)
services.add_vote(obj, user=request.user)
return response.Ok()
def _remove_vote(self, permission, request, pk=None):
obj = self.get_object()
self.check_permissions(request, permission, obj)
services.remove_vote(obj, user=request.user)
return response.Ok()
class StarredResourceMixin(BaseVotedResource):
# Note: objects nedd 'star' and 'unstar' permissions.
@detail_route(methods=["POST"])
def star(self, request, pk=None):
return self._add_voter("star", request, pk)
@detail_route(methods=["POST"])
def unstar(self, request, pk=None):
return self._remove_vote("unstar", request, pk)
class VotedResourceMixin(BaseVotedResource):
# Note: objects nedd 'upvote' and 'downvote' permissions.
@detail_route(methods=["POST"])
def upvote(self, request, pk=None):
return self._add_voter("upvote", request, pk)
@detail_route(methods=["POST"])
def downvote(self, request, pk=None):
return self._remove_vote("downvote", request, pk)
class VotersViewSetMixin:
# Is a ModelListViewSet with two required params: permission_classes and resource_model
serializer_class = serializers.VoterSerializer
list_serializer_class = serializers.VoterSerializer
permission_classes = None
resource_model = None
def retrieve(self, request, *args, **kwargs):
pk = kwargs.get("pk", None)
resource_id = kwargs.get("resource_id", None)
resource = get_object_or_404(self.resource_model, pk=resource_id)
self.check_permissions(request, 'retrieve', resource)
try:
self.object = services.get_voters(resource).get(pk=pk)
except ObjectDoesNotExist: # or User.DoesNotExist
return response.NotFound()
serializer = self.get_serializer(self.object)
return response.Ok(serializer.data)
def list(self, request, *args, **kwargs):
resource_id = kwargs.get("resource_id", None)
resource = get_object_or_404(self.resource_model, pk=resource_id)
self.check_permissions(request, 'list', resource)
return super().list(request, *args, **kwargs)
def get_queryset(self):
resource = self.resource_model.objects.get(pk=self.kwargs.get("resource_id"))
return services.get_voters(resource)

View File

@ -16,16 +16,16 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes import generic
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.contrib.contenttypes import generic
class Votes(models.Model): class Votes(models.Model):
content_type = models.ForeignKey("contenttypes.ContentType") content_type = models.ForeignKey("contenttypes.ContentType")
object_id = models.PositiveIntegerField() object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey("content_type", "object_id") 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: class Meta:
verbose_name = _("Votes") verbose_name = _("Votes")
@ -44,10 +44,12 @@ class Votes(models.Model):
class Vote(models.Model): class Vote(models.Model):
content_type = models.ForeignKey("contenttypes.ContentType") content_type = models.ForeignKey("contenttypes.ContentType")
object_id = models.PositiveIntegerField(null=False) object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey("content_type", "object_id") content_object = generic.GenericForeignKey("content_type", "object_id")
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False, 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: class Meta:
verbose_name = _("Vote") verbose_name = _("Vote")
@ -61,4 +63,4 @@ class Vote(models.Model):
return None return None
def __str__(self): def __str__(self):
return self.user return self.user.get_full_name()

View File

@ -16,8 +16,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.fields import TagsField
from taiga.users.models import User from taiga.users.models import User
from taiga.users.services import get_photo_or_gravatar_url
class VoterSerializer(serializers.ModelSerializer): class VoterSerializer(serializers.ModelSerializer):

View File

@ -18,7 +18,7 @@
from django.apps import apps 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. """Attach votes count to each object of the queryset.
Because of laziness of vote objects creation, this makes much simpler and more efficient to 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 model = queryset.model
type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model) type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model)
sql = ("SELECT coalesce(votes_votes.count, 0) FROM votes_votes " sql = ("""SELECT coalesce(votes_votes.count, 0)
"WHERE votes_votes.content_type_id = {type_id} AND votes_votes.object_id = {tbl}.id") 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) sql = sql.format(type_id=type.id, tbl=model._meta.db_table)
qs = queryset.extra(select={as_field: sql}) qs = queryset.extra(select={as_field: sql})
return qs return qs
def attach_is_vote_to_queryset(user, queryset, as_field="is_voted"):
"""Attach is_vote boolean to each object of the queryset.
Because of laziness of vote objects creation, this makes much simpler and more efficient to
access to votes-object and check if the curren user vote it.
(The other way was to do it in the serializer with some try/except blocks and additional
queries)
:param user: A users.User object model
:param queryset: A Django queryset object.
:param as_field: Attach the boolean as an attribute with this name.
:return: Queryset object with the additional `as_field` field.
"""
model = queryset.model
type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model)
sql = ("""SELECT CASE WHEN (SELECT count(*)
FROM votes_vote
WHERE votes_vote.content_type_id = {type_id}
AND votes_vote.object_id = {tbl}.id
AND votes_vote.user_id = {user_id}) > 0
THEN TRUE
ELSE FALSE
END""")
sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id)
qs = queryset.extra(select={as_field: sql})
return qs

View File

@ -17,6 +17,9 @@
from django.contrib import admin from django.contrib import admin
from taiga.projects.attachments.admin import AttachmentInline 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 taiga.projects.wiki.models import WikiPage
from . import models from . import models
@ -24,7 +27,7 @@ from . import models
class WikiPageAdmin(admin.ModelAdmin): class WikiPageAdmin(admin.ModelAdmin):
list_display = ["project", "slug", "owner"] list_display = ["project", "slug", "owner"]
list_display_links = list_display list_display_links = list_display
# inlines = [AttachmentInline] inlines = [WatchedInline, VoteInline]
admin.site.register(models.WikiPage, WikiPageAdmin) admin.site.register(models.WikiPage, WikiPageAdmin)

View File

@ -21,13 +21,13 @@ from taiga.base.api.permissions import IsAuthenticated
from taiga.base import filters from taiga.base import filters
from taiga.base import exceptions as exc from taiga.base import exceptions as exc
from taiga.base import response 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.api.utils import get_object_or_404
from taiga.base.decorators import list_route from taiga.base.decorators import list_route
from taiga.projects.models import Project from taiga.projects.models import Project
from taiga.mdrender.service import render as mdrender 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.history.mixins import HistoryResourceMixin
from taiga.projects.occ import OCCResourceMixin from taiga.projects.occ import OCCResourceMixin
@ -43,6 +43,12 @@ class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
permission_classes = (permissions.WikiPagePermission,) permission_classes = (permissions.WikiPagePermission,)
filter_backends = (filters.CanViewWikiPagesFilterBackend,) filter_backends = (filters.CanViewWikiPagesFilterBackend,)
filter_fields = ("project", "slug") 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"]) @list_route(methods=["GET"])
def by_slug(self, request): def by_slug(self, request):
@ -77,6 +83,11 @@ class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
super().pre_save(obj) super().pre_save(obj)
class WikiWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
permission_classes = (permissions.WikiPageWatchersPermission,)
resource_model = models.WikiPage
class WikiLinkViewSet(ModelCrudViewSet): class WikiLinkViewSet(ModelCrudViewSet):
model = models.WikiLink model = models.WikiLink
serializer_class = serializers.WikiLinkSerializer serializer_class = serializers.WikiLinkSerializer

View File

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

View File

@ -15,7 +15,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm, from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
IsProjectOwner, AllowAny, IsSuperUser) IsAuthenticated, IsProjectOwner, AllowAny,
IsSuperUser)
class WikiPagePermission(TaigaResourcePermission): class WikiPagePermission(TaigaResourcePermission):
@ -29,6 +30,16 @@ class WikiPagePermission(TaigaResourcePermission):
destroy_perms = HasProjectPerm('delete_wiki_page') destroy_perms = HasProjectPerm('delete_wiki_page')
list_perms = AllowAny() list_perms = AllowAny()
render_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): class WikiLinkPermission(TaigaResourcePermission):
enought_perms = IsProjectOwner() | IsSuperUser() enought_perms = IsProjectOwner() | IsSuperUser()

View File

@ -15,15 +15,15 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.base.api import serializers 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 . import models
from taiga.projects.history import services as history_service
from taiga.mdrender.service import render as mdrender class WikiPageSerializer(WatchersValidator, WatchedResourceModelSerializer, serializers.ModelSerializer):
class WikiPageSerializer(serializers.ModelSerializer):
html = serializers.SerializerMethodField("get_html") html = serializers.SerializerMethodField("get_html")
editions = serializers.SerializerMethodField("get_editions") editions = serializers.SerializerMethodField("get_editions")
@ -39,6 +39,5 @@ class WikiPageSerializer(serializers.ModelSerializer):
class WikiLinkSerializer(serializers.ModelSerializer): class WikiLinkSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.WikiLink model = models.WikiLink

View File

@ -1,4 +1,3 @@
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com> # Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com> # Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
@ -49,6 +48,8 @@ router.register(r"notify-policies", NotifyPolicyViewSet, base_name="notification
# Projects & Selectors # Projects & Selectors
from taiga.projects.api import ProjectViewSet 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 MembershipViewSet
from taiga.projects.api import InvitationViewSet from taiga.projects.api import InvitationViewSet
from taiga.projects.api import UserStoryStatusViewSet from taiga.projects.api import UserStoryStatusViewSet
@ -61,6 +62,8 @@ from taiga.projects.api import SeverityViewSet
from taiga.projects.api import ProjectTemplateViewSet from taiga.projects.api import ProjectTemplateViewSet
router.register(r"projects", ProjectViewSet, base_name="projects") router.register(r"projects", ProjectViewSet, base_name="projects")
router.register(r"projects/(?P<resource_id>\d+)/fans", ProjectFansViewSet, base_name="project-fans")
router.register(r"projects/(?P<resource_id>\d+)/watchers", ProjectWatchersViewSet, base_name="project-watchers")
router.register(r"project-templates", ProjectTemplateViewSet, base_name="project-templates") router.register(r"project-templates", ProjectTemplateViewSet, base_name="project-templates")
router.register(r"memberships", MembershipViewSet, base_name="memberships") router.register(r"memberships", MembershipViewSet, base_name="memberships")
router.register(r"invitations", InvitationViewSet, base_name="invitations") router.register(r"invitations", InvitationViewSet, base_name="invitations")
@ -123,21 +126,38 @@ router.register(r"wiki/attachments", WikiAttachmentViewSet, base_name="wiki-atta
# Project components # Project components
from taiga.projects.milestones.api import MilestoneViewSet 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 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 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 IssueViewSet
from taiga.projects.issues.api import VotersViewSet from taiga.projects.issues.api import IssueVotersViewSet
from taiga.projects.wiki.api import WikiViewSet, WikiLinkViewSet 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", MilestoneViewSet, base_name="milestones")
router.register(r"milestones/(?P<resource_id>\d+)/watchers", MilestoneWatchersViewSet, base_name="milestone-watchers")
router.register(r"userstories", UserStoryViewSet, base_name="userstories") router.register(r"userstories", UserStoryViewSet, base_name="userstories")
router.register(r"userstories/(?P<resource_id>\d+)/voters", UserStoryVotersViewSet, base_name="userstory-voters")
router.register(r"userstories/(?P<resource_id>\d+)/watchers", UserStoryWatchersViewSet, base_name="userstory-watchers")
router.register(r"tasks", TaskViewSet, base_name="tasks") router.register(r"tasks", TaskViewSet, base_name="tasks")
router.register(r"tasks/(?P<resource_id>\d+)/voters", TaskVotersViewSet, base_name="task-voters")
router.register(r"tasks/(?P<resource_id>\d+)/watchers", TaskWatchersViewSet, base_name="task-watchers")
router.register(r"issues", IssueViewSet, base_name="issues") router.register(r"issues", IssueViewSet, base_name="issues")
router.register(r"issues/(?P<issue_id>\d+)/voters", VotersViewSet, base_name="issue-voters") router.register(r"issues/(?P<resource_id>\d+)/voters", IssueVotersViewSet, base_name="issue-voters")
router.register(r"issues/(?P<resource_id>\d+)/watchers", IssueWatchersViewSet, base_name="issue-watchers")
router.register(r"wiki", WikiViewSet, base_name="wiki") router.register(r"wiki", WikiViewSet, base_name="wiki")
router.register(r"wiki/(?P<resource_id>\d+)/watchers", WikiWatchersViewSet, base_name="wiki-watchers")
router.register(r"wiki-links", WikiLinkViewSet, base_name="wiki-links") router.register(r"wiki-links", WikiLinkViewSet, base_name="wiki-links")
# History & Components # History & Components
from taiga.projects.history.api import UserStoryHistory from taiga.projects.history.api import UserStoryHistory
from taiga.projects.history.api import TaskHistory from taiga.projects.history.api import TaskHistory

View File

@ -22,14 +22,12 @@ from taiga.projects.history import services as history_services
from taiga.projects.models import Project from taiga.projects.models import Project
from taiga.users.models import User from taiga.users.models import User
from taiga.projects.history.choices import HistoryType from taiga.projects.history.choices import HistoryType
from taiga.projects.notifications import services as notifications_services
from taiga.timeline.service import (push_to_timeline, from taiga.timeline.service import (push_to_timeline,
build_user_namespace, build_user_namespace,
build_project_namespace, build_project_namespace,
extract_user_info) 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): def _push_to_timeline(*args, **kwargs):
if settings.CELERY_ENABLED: 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) related_people |= User.objects.filter(id=obj.assigned_to_id)
## - Watchers ## - Watchers
watchers = getattr(obj, "watchers", None) watchers = notifications_services.get_watchers(obj)
if watchers: if watchers:
related_people |= obj.watchers.all() related_people |= watchers
## - Exclude inactive and system users and remove duplicate ## - Exclude inactive and system users and remove duplicate
related_people = related_people.exclude(is_active=False) related_people = related_people.exclude(is_active=False)

View File

@ -114,6 +114,33 @@ class UsersViewSet(ModelCrudViewSet):
self.check_permissions(request, "stats", user) self.check_permissions(request, "stats", user)
return response.Ok(services.get_stats_for_user(user, request.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"]) @list_route(methods=["POST"])
def password_recovery(self, request, pk=None): def password_recovery(self, request, pk=None):
username_or_email = request.DATA.get('username', 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 user_data = self.admin_serializer_class(request.user).data
return response.Ok(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 #TODO: commit_on_success
def partial_update(self, request, *args, **kwargs): def partial_update(self, request, *args, **kwargs):
""" """

View File

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

View File

@ -47,6 +47,7 @@ class UserPermission(TaigaResourcePermission):
starred_perms = AllowAny() starred_perms = AllowAny()
change_email_perms = AllowAny() change_email_perms = AllowAny()
contacts_perms = AllowAny() contacts_perms = AllowAny()
favourites_perms = AllowAny()
class RolesPermission(TaigaResourcePermission): class RolesPermission(TaigaResourcePermission):

View File

@ -19,11 +19,14 @@ from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from taiga.base.api import serializers 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 taiga.projects.models import Project
from .models import User, Role from .models import User, Role
from .services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url from .services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url
from collections import namedtuple
import re import re
@ -149,3 +152,55 @@ class ProjectRoleSerializer(serializers.ModelSerializer):
model = Role model = Role
fields = ('id', 'name', 'slug', 'order', 'computable') fields = ('id', 'name', 'slug', 'order', 'computable')
i18n_fields = ("name",) 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)

View File

@ -20,6 +20,7 @@ This model contains a domain logic for users application.
from django.apps import apps from django.apps import apps
from django.db.models import Q from django.db.models import Q
from django.db import connection
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext as _ 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, 'total_num_closed_userstories': total_num_closed_userstories,
} }
return project_stats 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()
]

View File

@ -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.tasks import models as task_models
from taiga.projects.issues import models as issue_models from taiga.projects.issues import models as issue_models
from taiga.projects.milestones import models as milestone_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.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 from .models import Webhook, WebhookLog
@ -103,7 +104,8 @@ class PointSerializer(serializers.Serializer):
return obj.value return obj.value
class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.ModelSerializer): class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, WatchedResourceModelSerializer,
serializers.ModelSerializer):
tags = TagsField(default=[], required=False) tags = TagsField(default=[], required=False)
external_reference = PgArrayField(required=False) external_reference = PgArrayField(required=False)
owner = UserSerializer() owner = UserSerializer()
@ -119,7 +121,8 @@ class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, serializ
return project.userstorycustomattributes.all() return project.userstorycustomattributes.all()
class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.ModelSerializer): class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, WatchedResourceModelSerializer,
serializers.ModelSerializer):
tags = TagsField(default=[], required=False) tags = TagsField(default=[], required=False)
owner = UserSerializer() owner = UserSerializer()
assigned_to = UserSerializer() assigned_to = UserSerializer()
@ -132,7 +135,8 @@ class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.M
return project.taskcustomattributes.all() return project.taskcustomattributes.all()
class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.ModelSerializer): class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, WatchedResourceModelSerializer,
serializers.ModelSerializer):
tags = TagsField(default=[], required=False) tags = TagsField(default=[], required=False)
owner = UserSerializer() owner = UserSerializer()
assigned_to = UserSerializer() assigned_to = UserSerializer()

View File

@ -441,6 +441,17 @@ class VotesFactory(Factory):
object_id = factory.Sequence(lambda n: n) 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 ContentTypeFactory(Factory):
class Meta: class Meta:
model = "contenttypes.ContentType" model = "contenttypes.ContentType"

View File

@ -9,6 +9,7 @@ from taiga.base.utils import json
from tests import factories as f from tests import factories as f
from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals
from taiga.projects.votes.services import add_vote from taiga.projects.votes.services import add_vote
from taiga.projects.notifications.services import add_watcher
from taiga.projects.occ import OCCResourceMixin from taiga.projects.occ import OCCResourceMixin
from unittest import mock 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) results = helper_test_http_method(client, 'post', public_url, "", users)
assert results == [401, 200, 200, 200, 200] assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url1, "", users) results = helper_test_http_method(client, 'post', private_url1, "", users)
assert results == [401, 200, 200, 200, 200] assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users) 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): 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) results = helper_test_http_method(client, 'post', public_url, "", users)
assert results == [401, 200, 200, 200, 200] assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url1, "", users) results = helper_test_http_method(client, 'post', private_url1, "", users)
assert results == [401, 200, 200, 200, 200] assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users) 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): def test_issue_voters_list(client, data):
public_url = reverse('issue-voters-list', kwargs={"issue_id": data.public_issue.pk}) public_url = reverse('issue-voters-list', kwargs={"resource_id": data.public_issue.pk})
private_url1 = reverse('issue-voters-list', kwargs={"issue_id": data.private_issue1.pk}) private_url1 = reverse('issue-voters-list', kwargs={"resource_id": data.private_issue1.pk})
private_url2 = reverse('issue-voters-list', kwargs={"issue_id": data.private_issue2.pk}) private_url2 = reverse('issue-voters-list', kwargs={"resource_id": data.private_issue2.pk})
users = [ users = [
None, None,
@ -523,21 +520,22 @@ def test_issue_voters_list(client, data):
results = helper_test_http_method(client, 'get', public_url, None, users) results = helper_test_http_method(client, 'get', public_url, None, users)
assert results == [200, 200, 200, 200, 200] assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url1, None, users) results = helper_test_http_method(client, 'get', private_url1, None, users)
assert results == [200, 200, 200, 200, 200] assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url2, None, users) results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200] assert results == [401, 403, 403, 200, 200]
def test_issue_voters_retrieve(client, data): def test_issue_voters_retrieve(client, data):
add_vote(data.public_issue, data.project_owner) 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) 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) 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 = [ users = [
None, None,
@ -549,10 +547,8 @@ def test_issue_voters_retrieve(client, data):
results = helper_test_http_method(client, 'get', public_url, None, users) results = helper_test_http_method(client, 'get', public_url, None, users)
assert results == [200, 200, 200, 200, 200] assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url1, None, users) results = helper_test_http_method(client, 'get', private_url1, None, users)
assert results == [200, 200, 200, 200, 200] assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url2, None, users) results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200] 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) results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users)
assert results == [200, 200, 200, 200, 200] 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]

View File

@ -3,6 +3,7 @@ from django.core.urlresolvers import reverse
from taiga.base.utils import json from taiga.base.utils import json
from taiga.projects.milestones.serializers import MilestoneSerializer from taiga.projects.milestones.serializers import MilestoneSerializer
from taiga.projects.milestones.models import Milestone 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 taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
from tests import factories as f 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) results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200] 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]

View File

@ -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_project1.pk, count=2)
f.VotesFactory(content_type=project_ct, object_id=m.private_project2.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 return m
@ -109,6 +116,7 @@ def test_project_update(client, data):
project_data = ProjectDetailSerializer(data.private_project2).data project_data = ProjectDetailSerializer(data.private_project2).data
project_data["is_private"] = False project_data["is_private"] = False
project_data = json.dumps(project_data) project_data = json.dumps(project_data)
users = [ users = [
@ -198,6 +206,25 @@ def test_project_action_stats(client, data):
assert results == [404, 404, 200, 200] 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): def test_project_action_star(client, data):
public_url = reverse('projects-star', kwargs={"pk": data.public_project.pk}) public_url = reverse('projects-star', kwargs={"pk": data.public_project.pk})
private1_url = reverse('projects-star', kwargs={"pk": data.private_project1.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] assert results == [404, 404, 200, 200]
def test_project_action_issues_stats(client, data): def test_project_fans_list(client, data):
public_url = reverse('projects-issues-stats', kwargs={"pk": data.public_project.pk}) public_url = reverse('project-fans-list', kwargs={"resource_id": data.public_project.pk})
private1_url = reverse('projects-issues-stats', kwargs={"pk": data.private_project1.pk}) private1_url = reverse('project-fans-list', kwargs={"resource_id": data.private_project1.pk})
private2_url = reverse('projects-issues-stats', kwargs={"pk": data.private_project2.pk}) private2_url = reverse('project-fans-list', kwargs={"resource_id": 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})
users = [ users = [
None, 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) 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)] 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) 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): def test_project_fans_retrieve(client, data):
url1 = reverse('users-starred', kwargs={"pk": data.project_member_without_perms.pk}) public_url = reverse('project-fans-detail', kwargs={"resource_id": data.public_project.pk,
url2 = reverse('users-starred', kwargs={"pk": data.project_member_with_perms.pk}) "pk": data.project_owner.pk})
url3 = reverse('users-starred', kwargs={"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 = [ users = [
None, None,
@ -289,12 +300,57 @@ def test_user_action_starred(client, data):
data.project_owner data.project_owner
] ]
results = helper_test_http_method_and_count(client, 'get', url1, None, users) results = helper_test_http_method(client, 'get', public_url, None, users)
assert results == [(200, 0), (200, 0), (200, 0), (200, 0), (200, 0)] assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method_and_count(client, 'get', url2, None, users) results = helper_test_http_method(client, 'get', private1_url, None, users)
assert results == [(200, 3), (200, 3), (200, 3), (200, 3), (200, 3)] assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method_and_count(client, 'get', url3, None, users) results = helper_test_http_method(client, 'get', private2_url, None, users)
assert results == [(200, 3), (200, 3), (200, 3), (200, 3), (200, 3)] 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): 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) results = helper_test_http_method(client, 'post', private2_url, None, users)
assert results == [404, 404, 403, 200] 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]

View File

@ -9,6 +9,8 @@ from taiga.projects.occ import OCCResourceMixin
from tests import factories as f from tests import factories as f
from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals 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 from unittest import mock
@ -416,6 +418,96 @@ def test_task_action_bulk_create(client, data):
assert results == [401, 403, 403, 200, 200] 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): def test_tasks_csv(client, data):
url = reverse('tasks-csv') url = reverse('tasks-csv')
csv_public_uuid = data.public_project.tasks_csv_uuid 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) results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users)
assert results == [200, 200, 200, 200, 200] 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]

View File

@ -287,3 +287,15 @@ def test_user_action_change_email(client, data):
after_each_request() after_each_request()
results = helper_test_http_method(client, 'post', url, patch_data, users, after_each_request=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] 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]

View File

@ -9,6 +9,8 @@ from taiga.projects.occ import OCCResourceMixin
from tests import factories as f from tests import factories as f
from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals 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 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) results = helper_test_http_method(client, 'post', url, post_data, users)
assert results == [401, 403, 403, 204, 204] 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): def test_user_stories_csv(client, data):
url = reverse('userstories-csv') 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) results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users)
assert results == [200, 200, 200, 200, 200] 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]

View File

@ -1,10 +1,11 @@
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from taiga.base.utils import json 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.serializers import WikiPageSerializer, WikiLinkSerializer
from taiga.projects.wiki.models import WikiPage, WikiLink 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 import factories as f
from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals 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"}) patch_data = json.dumps({"title": "test"})
results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
assert results == [401, 403, 403, 200, 200] 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]

View File

@ -47,13 +47,15 @@ def test_invalid_project_import(client):
def test_valid_project_import_without_extra_data(client): def test_valid_project_import_without_extra_data(client):
user = f.UserFactory.create() user = f.UserFactory.create()
user_watching = f.UserFactory.create(email="testing@taiga.io")
client.login(user) client.login(user)
url = reverse("importer-list") url = reverse("importer-list")
data = { data = {
"name": "Imported project", "name": "Imported project",
"description": "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") 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 all(map(lambda x: len(response_data[x]) == 0, must_empty_children))
assert response_data["owner"] == user.email assert response_data["owner"] == user.email
assert response_data["watchers"] == [user_watching.email]
def test_valid_project_import_with_not_existing_memberships(client): 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): def test_valid_issue_import_with_extra_data(client):
user = f.UserFactory.create() user = f.UserFactory.create()
user_watching = f.UserFactory.create(email="testing@taiga.io")
project = f.ProjectFactory.create(owner=user) project = f.ProjectFactory.create(owner=user)
f.MembershipFactory(project=project, user=user, is_owner=True) f.MembershipFactory(project=project, user=user, is_owner=True)
project.default_issue_type = f.IssueTypeFactory.create(project=project) 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", "name": "imported attachment",
"data": base64.b64encode(b"TEST").decode("utf-8") "data": base64.b64encode(b"TEST").decode("utf-8")
} }
}] }],
"watchers": ["testing@taiga.io"]
} }
response = client.post(url, json.dumps(data), content_type="application/json") 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["owner"] == user.email
assert response_data["ref"] is not None assert response_data["ref"] is not None
assert response_data["finished_date"] == "2014-10-24T00:00:00+0000" 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): 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): def test_valid_us_import_with_extra_data(client):
user = f.UserFactory.create() user = f.UserFactory.create()
user_watching = f.UserFactory.create(email="testing@taiga.io")
project = f.ProjectFactory.create(owner=user) project = f.ProjectFactory.create(owner=user)
f.MembershipFactory(project=project, user=user, is_owner=True) f.MembershipFactory(project=project, user=user, is_owner=True)
project.default_us_status = f.UserStoryStatusFactory.create(project=project) 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", "name": "imported attachment",
"data": base64.b64encode(b"TEST").decode("utf-8") "data": base64.b64encode(b"TEST").decode("utf-8")
} }
}] }],
"watchers": ["testing@taiga.io"]
} }
response = client.post(url, json.dumps(data), content_type="application/json") 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 len(response_data["attachments"]) == 1
assert response_data["owner"] == user.email assert response_data["owner"] == user.email
assert response_data["ref"] is not None assert response_data["ref"] is not None
assert response_data["watchers"] == [user_watching.email]
def test_invalid_us_import_with_extra_data(client): 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): def test_valid_task_import_with_extra_data(client):
user = f.UserFactory.create() user = f.UserFactory.create()
user_watching = f.UserFactory.create(email="testing@taiga.io")
project = f.ProjectFactory.create(owner=user) project = f.ProjectFactory.create(owner=user)
f.MembershipFactory(project=project, user=user, is_owner=True) f.MembershipFactory(project=project, user=user, is_owner=True)
project.default_task_status = f.TaskStatusFactory.create(project=project) 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", "name": "imported attachment",
"data": base64.b64encode(b"TEST").decode("utf-8") "data": base64.b64encode(b"TEST").decode("utf-8")
} }
}] }],
"watchers": ["testing@taiga.io"]
} }
response = client.post(url, json.dumps(data), content_type="application/json") 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 len(response_data["attachments"]) == 1
assert response_data["owner"] == user.email assert response_data["owner"] == user.email
assert response_data["ref"] is not None assert response_data["ref"] is not None
assert response_data["watchers"] == [user_watching.email]
def test_invalid_task_import_with_extra_data(client): 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): def test_valid_wiki_page_import_with_extra_data(client):
user = f.UserFactory.create() user = f.UserFactory.create()
user_watching = f.UserFactory.create(email="testing@taiga.io")
project = f.ProjectFactory.create(owner=user) project = f.ProjectFactory.create(owner=user)
f.MembershipFactory(project=project, user=user, is_owner=True) f.MembershipFactory(project=project, user=user, is_owner=True)
client.login(user) client.login(user)
@ -801,7 +814,8 @@ def test_valid_wiki_page_import_with_extra_data(client):
"name": "imported attachment", "name": "imported attachment",
"data": base64.b64encode(b"TEST").decode("utf-8") "data": base64.b64encode(b"TEST").decode("utf-8")
} }
}] }],
"watchers": ["testing@taiga.io"]
} }
response = client.post(url, json.dumps(data), content_type="application/json") 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 response_data = response.data
assert len(response_data["attachments"]) == 1 assert len(response_data["attachments"]) == 1
assert response_data["owner"] == user.email assert response_data["owner"] == user.email
assert response_data["watchers"] == [user_watching.email]
def test_invalid_wiki_page_import_with_extra_data(client): 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): def test_valid_milestone_import(client):
user = f.UserFactory.create() user = f.UserFactory.create()
user_watching = f.UserFactory.create(email="testing@taiga.io")
project = f.ProjectFactory.create(owner=user) project = f.ProjectFactory.create(owner=user)
f.MembershipFactory(project=project, user=user, is_owner=True) f.MembershipFactory(project=project, user=user, is_owner=True)
client.login(user) client.login(user)
@ -886,11 +902,12 @@ def test_valid_milestone_import(client):
"name": "Imported milestone", "name": "Imported milestone",
"estimated_start": "2014-10-10", "estimated_start": "2014-10-10",
"estimated_finish": "2014-10-20", "estimated_finish": "2014-10-20",
"watchers": ["testing@taiga.io"]
} }
response = client.post(url, json.dumps(data), content_type="application/json") response = client.post(url, json.dumps(data), content_type="application/json")
assert response.status_code == 201 assert response.status_code == 201
response.data assert response.data["watchers"] == [user_watching.email]

View File

@ -412,6 +412,6 @@ def test_custom_fields_csv_generation():
data.seek(0) data.seek(0)
reader = csv.reader(data) reader = csv.reader(data)
row = next(reader) row = next(reader)
assert row[16] == attr.name assert row[18] == attr.name
row = next(reader) row = next(reader)
assert row[16] == "val1" assert row[18] == "val1"

View File

@ -97,7 +97,7 @@ def test_analize_object_for_watchers():
history.comment = "" history.comment = ""
services.analize_object_for_watchers(issue, history) 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(): 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 history.owner = user1
services.analize_object_for_watchers(issue, history) 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(): 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 history.owner = user1
services.analize_object_for_watchers(issue, history) 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(): def test_users_to_notify():
@ -180,7 +180,7 @@ def test_users_to_notify():
assert users == {member1.user, issue.get_owner()} assert users == {member1.user, issue.get_owner()}
# Test with watchers # Test with watchers
issue.watchers.add(member3.user) issue.add_watcher(member3.user)
users = services.get_users_to_notify(issue) users = services.get_users_to_notify(issue)
assert len(users) == 3 assert len(users) == 3
assert users == {member1.user, member3.user, issue.get_owner()} assert users == {member1.user, member3.user, issue.get_owner()}
@ -189,28 +189,146 @@ def test_users_to_notify():
policy2.notify_level = NotifyLevel.ignore policy2.notify_level = NotifyLevel.ignore
policy2.save() policy2.save()
issue.watchers.add(member3.user) issue.add_watcher(member3.user)
users = services.get_users_to_notify(issue) users = services.get_users_to_notify(issue)
assert len(users) == 2 assert len(users) == 2
assert users == {member1.user, issue.get_owner()} assert users == {member1.user, issue.get_owner()}
# Test with watchers without permissions # Test with watchers without permissions
issue.watchers.add(member5.user) issue.add_watcher(member5.user)
users = services.get_users_to_notify(issue) users = services.get_users_to_notify(issue)
assert len(users) == 2 assert len(users) == 2
assert users == {member1.user, issue.get_owner()} assert users == {member1.user, issue.get_owner()}
# Test with inactive user # Test with inactive user
issue.watchers.add(inactive_member1.user) issue.add_watcher(inactive_member1.user)
assert len(users) == 2 assert len(users) == 2
assert users == {member1.user, issue.get_owner()} assert users == {member1.user, issue.get_owner()}
# Test with system user # Test with system user
issue.watchers.add(system_member1.user) issue.add_watcher(system_member1.user)
assert len(users) == 2 assert len(users) == 2
assert users == {member1.user, issue.get_owner()} 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): def test_send_notifications_using_services_method(settings, mail):
settings.CHANGE_NOTIFICATIONS_MIN_INTERVAL = 1 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) issue = f.create_issue(project=project1, owner=user1)
data = {"version": issue.version, data = {"version": issue.version,
"watchers": [user1.pk]} "watchersa": [user1.pk]}
url = reverse("issues-detail", args=[issue.pk]) url = reverse("issues-detail", args=[issue.pk])
response = client.json.patch(url, json.dumps(data)) response = client.json.patch(url, json.dumps(data))

View File

@ -265,7 +265,7 @@ def test_leave_project_respect_watching_items(client):
url = reverse("projects-leave", args=(project.id,)) url = reverse("projects-leave", args=(project.id,))
response = client.post(url) response = client.post(url)
assert response.status_code == 200 assert response.status_code == 200
assert list(issue.watchers.all()) == [user] assert issue.watchers == [user]
def test_delete_membership_only_owner(client): def test_delete_membership_only_owner(client):

View File

@ -0,0 +1,123 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2015 Anler Hernández <hello@anler.me>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import pytest
from django.core.urlresolvers import reverse
from .. import factories as f
pytestmark = pytest.mark.django_db
def test_star_project(client):
user = f.UserFactory.create()
project = f.create_project(owner=user)
f.MembershipFactory.create(project=project, user=user, is_owner=True)
url = reverse("projects-star", args=(project.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200
def test_unstar_project(client):
user = f.UserFactory.create()
project = f.create_project(owner=user)
f.MembershipFactory.create(project=project, user=user, is_owner=True)
url = reverse("projects-unstar", args=(project.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200
def test_list_project_fans(client):
user = f.UserFactory.create()
project = f.create_project(owner=user)
f.MembershipFactory.create(project=project, user=user, is_owner=True)
f.VoteFactory.create(content_object=project, user=user)
url = reverse("project-fans-list", args=(project.id,))
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data[0]['id'] == user.id
def test_get_project_fan(client):
user = f.UserFactory.create()
project = f.create_project(owner=user)
f.MembershipFactory.create(project=project, user=user, is_owner=True)
vote = f.VoteFactory.create(content_object=project, user=user)
url = reverse("project-fans-detail", args=(project.id, vote.user.id))
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data['id'] == vote.user.id
def test_get_project_stars(client):
user = f.UserFactory.create()
project = f.create_project(owner=user)
f.MembershipFactory.create(project=project, user=user, is_owner=True)
url = reverse("projects-detail", args=(project.id,))
f.VotesFactory.create(content_object=project, count=5)
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data['stars'] == 5
def test_get_project_is_starred(client):
user = f.UserFactory.create()
project = f.create_project(owner=user)
f.MembershipFactory.create(project=project, user=user, is_owner=True)
f.VotesFactory.create(content_object=project)
url_detail = reverse("projects-detail", args=(project.id,))
url_star = reverse("projects-star", args=(project.id,))
url_unstar = reverse("projects-unstar", args=(project.id,))
client.login(user)
response = client.get(url_detail)
assert response.status_code == 200
assert response.data['stars'] == 0
assert response.data['is_starred'] == False
response = client.post(url_star)
assert response.status_code == 200
response = client.get(url_detail)
assert response.status_code == 200
assert response.data['stars'] == 1
assert response.data['is_starred'] == True
response = client.post(url_unstar)
assert response.status_code == 200
response = client.get(url_detail)
assert response.status_code == 200
assert response.data['stars'] == 0
assert response.data['is_starred'] == False

View File

@ -1,115 +0,0 @@
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014 Anler Hernández <hello@anler.me>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import pytest
from django.core.urlresolvers import reverse
from .. import factories as f
pytestmark = pytest.mark.django_db
def test_project_owner_star_project(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
f.MembershipFactory.create(project=project, is_owner=True, user=user)
url = reverse("projects-star", args=(project.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200
def test_project_owner_unstar_project(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
f.MembershipFactory.create(project=project, is_owner=True, user=user)
url = reverse("projects-unstar", args=(project.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200
def test_project_member_star_project(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create()
role = f.RoleFactory.create(project=project, permissions=["view_project"])
f.MembershipFactory.create(project=project, user=user, role=role)
url = reverse("projects-star", args=(project.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200
def test_project_member_unstar_project(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create()
role = f.RoleFactory.create(project=project, permissions=["view_project"])
f.MembershipFactory.create(project=project, user=user, role=role)
url = reverse("projects-unstar", args=(project.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200
def test_list_project_fans(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
f.MembershipFactory.create(project=project, user=user, is_owner=True)
fan = f.VoteFactory.create(content_object=project)
url = reverse("projects-fans", args=(project.id,))
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data[0]['id'] == fan.user.id
def test_list_user_starred_projects(client):
user = f.UserFactory.create()
project = f.ProjectFactory()
url = reverse("users-starred", args=(user.id,))
f.VoteFactory.create(user=user, content_object=project)
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data[0]['id'] == project.id
def test_get_project_stars(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
f.MembershipFactory.create(project=project, user=user, is_owner=True)
url = reverse("projects-detail", args=(project.id,))
f.VotesFactory.create(content_object=project, count=5)
f.VotesFactory.create(count=3)
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data['stars'] == 5

View File

@ -163,6 +163,6 @@ def test_custom_fields_csv_generation():
data.seek(0) data.seek(0)
reader = csv.reader(data) reader = csv.reader(data)
row = next(reader) row = next(reader)
assert row[17] == attr.name assert row[19] == attr.name
row = next(reader) row = next(reader)
assert row[17] == "val1" assert row[19] == "val1"

View File

@ -196,8 +196,10 @@ def test_create_membership_timeline():
def test_update_project_timeline(): def test_update_project_timeline():
user_watcher= factories.UserFactory()
project = factories.ProjectFactory.create(name="test project timeline") project = factories.ProjectFactory.create(name="test project timeline")
history_services.take_snapshot(project, user=project.owner) history_services.take_snapshot(project, user=project.owner)
project.add_watcher(user_watcher)
project.name = "test project timeline updated" project.name = "test project timeline updated"
project.save() project.save()
history_services.take_snapshot(project, user=project.owner) 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["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"][0] == "test project timeline"
assert project_timeline[0].data["values_diff"]["name"][1] == "test project timeline updated" 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(): def test_update_milestone_timeline():
user_watcher= factories.UserFactory()
milestone = factories.MilestoneFactory.create(name="test milestone timeline") milestone = factories.MilestoneFactory.create(name="test milestone timeline")
history_services.take_snapshot(milestone, user=milestone.owner) history_services.take_snapshot(milestone, user=milestone.owner)
milestone.add_watcher(user_watcher)
milestone.name = "test milestone timeline updated" milestone.name = "test milestone timeline updated"
milestone.save() milestone.save()
history_services.take_snapshot(milestone, user=milestone.owner) 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["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"][0] == "test milestone timeline"
assert project_timeline[0].data["values_diff"]["name"][1] == "test milestone timeline updated" 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(): def test_update_user_story_timeline():
user_watcher= factories.UserFactory()
user_story = factories.UserStoryFactory.create(subject="test us timeline") user_story = factories.UserStoryFactory.create(subject="test us timeline")
history_services.take_snapshot(user_story, user=user_story.owner) 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.subject = "test us timeline updated"
user_story.save() user_story.save()
history_services.take_snapshot(user_story, user=user_story.owner) 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["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"][0] == "test us timeline"
assert project_timeline[0].data["values_diff"]["subject"][1] == "test us timeline updated" 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(): def test_update_issue_timeline():
user_watcher= factories.UserFactory()
issue = factories.IssueFactory.create(subject="test issue timeline") issue = factories.IssueFactory.create(subject="test issue timeline")
history_services.take_snapshot(issue, user=issue.owner) history_services.take_snapshot(issue, user=issue.owner)
issue.add_watcher(user_watcher)
issue.subject = "test issue timeline updated" issue.subject = "test issue timeline updated"
issue.save() issue.save()
history_services.take_snapshot(issue, user=issue.owner) 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["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"][0] == "test issue timeline"
assert project_timeline[0].data["values_diff"]["subject"][1] == "test issue timeline updated" 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(): def test_update_task_timeline():
user_watcher= factories.UserFactory()
task = factories.TaskFactory.create(subject="test task timeline") task = factories.TaskFactory.create(subject="test task timeline")
history_services.take_snapshot(task, user=task.owner) history_services.take_snapshot(task, user=task.owner)
task.add_watcher(user_watcher)
task.subject = "test task timeline updated" task.subject = "test task timeline updated"
task.save() task.save()
history_services.take_snapshot(task, user=task.owner) 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["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"][0] == "test task timeline"
assert project_timeline[0].data["values_diff"]["subject"][1] == "test task timeline updated" 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(): def test_update_wiki_page_timeline():
user_watcher= factories.UserFactory()
page = factories.WikiPageFactory.create(slug="test wiki page timeline") page = factories.WikiPageFactory.create(slug="test wiki page timeline")
history_services.take_snapshot(page, user=page.owner) history_services.take_snapshot(page, user=page.owner)
page.add_watcher(user_watcher)
page.slug = "test wiki page timeline updated" page.slug = "test wiki page timeline updated"
page.save() page.save()
history_services.take_snapshot(page, user=page.owner) 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["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"][0] == "test wiki page timeline"
assert project_timeline[0].data["values_diff"]["slug"][1] == "test wiki page timeline updated" 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(): def test_update_membership_timeline():
@ -298,50 +340,80 @@ def test_update_membership_timeline():
def test_delete_project_timeline(): def test_delete_project_timeline():
project = factories.ProjectFactory.create(name="test 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) history_services.take_snapshot(project, user=project.owner, delete=True)
user_timeline = service.get_project_timeline(project) user_timeline = service.get_project_timeline(project)
assert user_timeline[0].event_type == "projects.project.delete" assert user_timeline[0].event_type == "projects.project.delete"
assert user_timeline[0].data["project"]["id"] == project.id 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(): def test_delete_milestone_timeline():
milestone = factories.MilestoneFactory.create(name="test 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) history_services.take_snapshot(milestone, user=milestone.owner, delete=True)
project_timeline = service.get_project_timeline(milestone.project) project_timeline = service.get_project_timeline(milestone.project)
assert project_timeline[0].event_type == "milestones.milestone.delete" assert project_timeline[0].event_type == "milestones.milestone.delete"
assert project_timeline[0].data["milestone"]["name"] == "test milestone timeline" 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(): def test_delete_user_story_timeline():
user_story = factories.UserStoryFactory.create(subject="test us 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) history_services.take_snapshot(user_story, user=user_story.owner, delete=True)
project_timeline = service.get_project_timeline(user_story.project) project_timeline = service.get_project_timeline(user_story.project)
assert project_timeline[0].event_type == "userstories.userstory.delete" assert project_timeline[0].event_type == "userstories.userstory.delete"
assert project_timeline[0].data["userstory"]["subject"] == "test us timeline" 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(): def test_delete_issue_timeline():
issue = factories.IssueFactory.create(subject="test 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) history_services.take_snapshot(issue, user=issue.owner, delete=True)
project_timeline = service.get_project_timeline(issue.project) project_timeline = service.get_project_timeline(issue.project)
assert project_timeline[0].event_type == "issues.issue.delete" assert project_timeline[0].event_type == "issues.issue.delete"
assert project_timeline[0].data["issue"]["subject"] == "test issue timeline" 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(): def test_delete_task_timeline():
task = factories.TaskFactory.create(subject="test 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) history_services.take_snapshot(task, user=task.owner, delete=True)
project_timeline = service.get_project_timeline(task.project) project_timeline = service.get_project_timeline(task.project)
assert project_timeline[0].event_type == "tasks.task.delete" assert project_timeline[0].event_type == "tasks.task.delete"
assert project_timeline[0].data["task"]["subject"] == "test task timeline" 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(): def test_delete_wiki_page_timeline():
page = factories.WikiPageFactory.create(slug="test 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) history_services.take_snapshot(page, user=page.owner, delete=True)
project_timeline = service.get_project_timeline(page.project) project_timeline = service.get_project_timeline(page.project)
assert project_timeline[0].event_type == "wiki.wikipage.delete" assert project_timeline[0].event_type == "wiki.wikipage.delete"
assert project_timeline[0].data["wikipage"]["slug"] == "test wiki page timeline" 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(): 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" 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(): def test_user_data_for_non_system_users():
user_story = factories.UserStoryFactory.create(subject="test us timeline") user_story = factories.UserStoryFactory.create(subject="test us timeline")
history_services.take_snapshot(user_story, user=user_story.owner) history_services.take_snapshot(user_story, user=user_story.owner)

View File

@ -1,6 +1,7 @@
import pytest import pytest
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from .. import factories as f from .. import factories as f
@ -9,6 +10,7 @@ from taiga.base.utils import json
from taiga.users import models from taiga.users import models
from taiga.auth.tokens import get_token_for_user from taiga.auth.tokens import get_token_for_user
from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
from taiga.users.services import get_favourites_list
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
@ -249,3 +251,166 @@ def test_list_contacts_public_projects(client):
response_content = response.data response_content = response.data
assert len(response_content) == 1 assert len(response_content) == 1
assert response_content[0]["id"] == user_2.id 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

View File

@ -483,6 +483,6 @@ def test_custom_fields_csv_generation():
data.seek(0) data.seek(0)
reader = csv.reader(data) reader = csv.reader(data)
row = next(reader) row = next(reader)
assert row[24] == attr.name assert row[26] == attr.name
row = next(reader) row = next(reader)
assert row[24] == "val1" assert row[26] == "val1"

View File

@ -1,7 +1,7 @@
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com> # Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com> # Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014 Anler Hernández <hello@anler.me> # Copyright (C) 2015 Anler Hernández <hello@anler.me>
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the # 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() user = f.UserFactory.create()
issue = f.create_issue(owner=user) issue = f.create_issue(owner=user)
f.MembershipFactory.create(project=issue.project, user=user, is_owner=True) 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) f.VoteFactory.create(content_object=issue, user=user)
url = reverse("issue-voters-list", args=(issue.id,))
client.login(user) client.login(user)
response = client.get(url) response = client.get(url)
@ -60,7 +60,6 @@ def test_list_issue_voters(client):
assert response.status_code == 200 assert response.status_code == 200
assert response.data[0]['id'] == user.id assert response.data[0]['id'] == user.id
def test_get_issue_voter(client): def test_get_issue_voter(client):
user = f.UserFactory.create() user = f.UserFactory.create()
issue = f.create_issue(owner=user) issue = f.create_issue(owner=user)
@ -74,7 +73,6 @@ def test_get_issue_voter(client):
assert response.status_code == 200 assert response.status_code == 200
assert response.data['id'] == vote.user.id assert response.data['id'] == vote.user.id
def test_get_issue_votes(client): def test_get_issue_votes(client):
user = f.UserFactory.create() user = f.UserFactory.create()
issue = f.create_issue(owner=user) issue = f.create_issue(owner=user)
@ -88,3 +86,36 @@ def test_get_issue_votes(client):
assert response.status_code == 200 assert response.status_code == 200
assert response.data['votes'] == 5 assert response.data['votes'] == 5
def test_get_issue_is_voted(client):
user = f.UserFactory.create()
issue = f.create_issue(owner=user)
f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
f.VotesFactory.create(content_object=issue)
url_detail = reverse("issues-detail", args=(issue.id,))
url_upvote = reverse("issues-upvote", args=(issue.id,))
url_downvote = reverse("issues-downvote", args=(issue.id,))
client.login(user)
response = client.get(url_detail)
assert response.status_code == 200
assert response.data['votes'] == 0
assert response.data['is_voted'] == False
response = client.post(url_upvote)
assert response.status_code == 200
response = client.get(url_detail)
assert response.status_code == 200
assert response.data['votes'] == 1
assert response.data['is_voted'] == True
response = client.post(url_downvote)
assert response.status_code == 200
response = client.get(url_detail)
assert response.status_code == 200
assert response.data['votes'] == 0
assert response.data['is_voted'] == False

View File

@ -0,0 +1,123 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2015 Anler Hernández <hello@anler.me>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import pytest
from django.core.urlresolvers import reverse
from .. import factories as f
pytestmark = pytest.mark.django_db
def test_upvote_task(client):
user = f.UserFactory.create()
task = f.create_task(owner=user)
f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
url = reverse("tasks-upvote", args=(task.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200
def test_downvote_task(client):
user = f.UserFactory.create()
task = f.create_task(owner=user)
f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
url = reverse("tasks-downvote", args=(task.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200
def test_list_task_voters(client):
user = f.UserFactory.create()
task = f.create_task(owner=user)
f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
f.VoteFactory.create(content_object=task, user=user)
url = reverse("task-voters-list", args=(task.id,))
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data[0]['id'] == user.id
def test_get_task_voter(client):
user = f.UserFactory.create()
task = f.create_task(owner=user)
f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
vote = f.VoteFactory.create(content_object=task, user=user)
url = reverse("task-voters-detail", args=(task.id, vote.user.id))
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data['id'] == vote.user.id
def test_get_task_votes(client):
user = f.UserFactory.create()
task = f.create_task(owner=user)
f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
url = reverse("tasks-detail", args=(task.id,))
f.VotesFactory.create(content_object=task, count=5)
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data['votes'] == 5
def test_get_task_is_voted(client):
user = f.UserFactory.create()
task = f.create_task(owner=user)
f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
f.VotesFactory.create(content_object=task)
url_detail = reverse("tasks-detail", args=(task.id,))
url_upvote = reverse("tasks-upvote", args=(task.id,))
url_downvote = reverse("tasks-downvote", args=(task.id,))
client.login(user)
response = client.get(url_detail)
assert response.status_code == 200
assert response.data['votes'] == 0
assert response.data['is_voted'] == False
response = client.post(url_upvote)
assert response.status_code == 200
response = client.get(url_detail)
assert response.status_code == 200
assert response.data['votes'] == 1
assert response.data['is_voted'] == True
response = client.post(url_downvote)
assert response.status_code == 200
response = client.get(url_detail)
assert response.status_code == 200
assert response.data['votes'] == 0
assert response.data['is_voted'] == False

View File

@ -0,0 +1,122 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2015 Anler Hernández <hello@anler.me>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import pytest
from django.core.urlresolvers import reverse
from .. import factories as f
pytestmark = pytest.mark.django_db
def test_upvote_user_story(client):
user = f.UserFactory.create()
user_story = f.create_userstory(owner=user)
f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
url = reverse("userstories-upvote", args=(user_story.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200
def test_downvote_user_story(client):
user = f.UserFactory.create()
user_story = f.create_userstory(owner=user)
f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
url = reverse("userstories-downvote", args=(user_story.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200
def test_list_user_story_voters(client):
user = f.UserFactory.create()
user_story = f.create_userstory(owner=user)
f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
f.VoteFactory.create(content_object=user_story, user=user)
url = reverse("userstory-voters-list", args=(user_story.id,))
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data[0]['id'] == user.id
def test_get_userstory_voter(client):
user = f.UserFactory.create()
user_story = f.create_userstory(owner=user)
f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
vote = f.VoteFactory.create(content_object=user_story, user=user)
url = reverse("userstory-voters-detail", args=(user_story.id, vote.user.id))
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data['id'] == vote.user.id
def test_get_user_story_votes(client):
user = f.UserFactory.create()
user_story = f.create_userstory(owner=user)
f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
url = reverse("userstories-detail", args=(user_story.id,))
f.VotesFactory.create(content_object=user_story, count=5)
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data['votes'] == 5
def test_get_user_story_is_voted(client):
user = f.UserFactory.create()
user_story = f.create_userstory(owner=user)
f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
f.VotesFactory.create(content_object=user_story)
url_detail = reverse("userstories-detail", args=(user_story.id,))
url_upvote = reverse("userstories-upvote", args=(user_story.id,))
url_downvote = reverse("userstories-downvote", args=(user_story.id,))
client.login(user)
response = client.get(url_detail)
assert response.status_code == 200
assert response.data['votes'] == 0
assert response.data['is_voted'] == False
response = client.post(url_upvote)
assert response.status_code == 200
response = client.get(url_detail)
assert response.status_code == 200
assert response.data['votes'] == 1
assert response.data['is_voted'] == True
response = client.post(url_downvote)
assert response.status_code == 200
response = client.get(url_detail)
assert response.status_code == 200
assert response.data['votes'] == 0
assert response.data['is_voted'] == False

View File

@ -0,0 +1,123 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2015 Anler Hernández <hello@anler.me>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import pytest
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

View File

@ -0,0 +1,123 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2015 Anler Hernández <hello@anler.me>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import pytest
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

View File

@ -0,0 +1,123 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2015 Anler Hernández <hello@anler.me>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import pytest
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

View File

@ -0,0 +1,123 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2015 Anler Hernández <hello@anler.me>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import pytest
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

View File

@ -0,0 +1,123 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2015 Anler Hernández <hello@anler.me>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import pytest
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

View File

@ -0,0 +1,123 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2015 Anler Hernández <hello@anler.me>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import pytest
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

View File

@ -90,3 +90,26 @@ def test_new_object_with_two_webhook(settings):
with patch('taiga.webhooks.tasks.delete_webhook') as delete_webhook_mock: with patch('taiga.webhooks.tasks.delete_webhook') as delete_webhook_mock:
services.take_snapshot(obj, user=obj.owner, comment="test", delete=True) services.take_snapshot(obj, user=obj.owner, comment="test", delete=True)
assert delete_webhook_mock.call_count == 2 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

View File

@ -16,7 +16,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.db.models import signals from django.db.models import signals
from taiga.base.utils import json
def signals_switch(): 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): 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) 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): 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) 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))