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)
### Features
- Add a "field type" property for custom fields: 'text' and 'multiline text' right now (thanks to [@artlepool](https://github.com/artlepool))
- Add a "field type" property for custom fields: 'text' and 'multiline text' right now (thanks to [@artlepool](https://github.com/artlepool)).
- Allow multiple actions in the commit messages.
- Now every user that coments USs, Issues or Tasks will be involved in it (add author to the watchers list).
- Fix the compatibility with BitBucket webhooks and add issues and issues comments integration.
- Add custom videoconference system.
- Add support for comments in the Gitlab webhooks integration.
- Now profile timelines only show content about the objects (US/Tasks/Issues/Wiki pages) you are involved.
- US, tasks and Issues can be upvoted or downvoted and the voters list can be obtained.
- Project can be starred or unstarred and the fans list can be obtained.
- Now users can watch public issues, tasks and user stories.
- Add endpoints to show the watchers list for issues, tasks and user stories.
- i18n.
- Add polish (pl) translation.
- Add portuguese (Brazil) (pt_BR) translation.

View File

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

View File

@ -18,6 +18,7 @@ from functools import reduce
import logging
from django.apps import apps
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.utils.translation import ugettext as _
@ -451,6 +452,33 @@ class TagsFilter(FilterBackend):
return super().filter_queryset(request, queryset, view)
class WatchersFilter(FilterBackend):
filter_name = 'watchers'
def __init__(self, filter_name=None):
if filter_name:
self.filter_name = filter_name
def _get_watchers_queryparams(self, params):
watchers = params.get(self.filter_name, None)
if watchers:
return watchers.split(",")
return None
def filter_queryset(self, request, queryset, view):
query_watchers = self._get_watchers_queryparams(request.QUERY_PARAMS)
model = queryset.model
if query_watchers:
WatchedModel = apps.get_model("notifications", "Watched")
watched_type = ContentType.objects.get_for_model(queryset.model)
watched_ids = WatchedModel.objects.filter(content_type=watched_type, user__id__in=query_watchers).values_list("object_id", flat=True)
queryset = queryset.filter(id__in=watched_ids)
return super().filter_queryset(request, queryset, view)
#####################################################################
# Text search filters
#####################################################################

View File

@ -19,6 +19,7 @@ import copy
import os
from collections import OrderedDict
from django.apps import apps
from django.core.files.base import ContentFile
from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ValidationError
@ -43,6 +44,7 @@ from taiga.projects.attachments import models as attachments_models
from taiga.timeline import models as timeline_models
from taiga.timeline import service as timeline_service
from taiga.users import models as users_models
from taiga.projects.notifications import services as notifications_services
from taiga.projects.votes import services as votes_service
from taiga.projects.history import services as history_service
@ -223,6 +225,48 @@ class HistoryDiffField(JsonField):
return data
class WatcheableObjectModelSerializer(serializers.ModelSerializer):
watchers = UserRelatedField(many=True, required=False)
def __init__(self, *args, **kwargs):
self._watchers_field = self.base_fields.pop("watchers", None)
super(WatcheableObjectModelSerializer, self).__init__(*args, **kwargs)
"""
watchers is not a field from the model so we need to do some magic to make it work like a normal field
It's supposed to be represented as an email list but internally it's treated like notifications.Watched instances
"""
def restore_object(self, attrs, instance=None):
watcher_field = self.fields.pop("watchers", None)
instance = super(WatcheableObjectModelSerializer, self).restore_object(attrs, instance)
self._watchers = self.init_data.get("watchers", [])
return instance
def save_watchers(self):
new_watcher_emails = set(self._watchers)
old_watcher_emails = set(notifications_services.get_watchers(self.object).values_list("email", flat=True))
adding_watcher_emails = list(new_watcher_emails.difference(old_watcher_emails))
removing_watcher_emails = list(old_watcher_emails.difference(new_watcher_emails))
User = apps.get_model("users", "User")
adding_users = User.objects.filter(email__in=adding_watcher_emails)
removing_users = User.objects.filter(email__in=removing_watcher_emails)
for user in adding_users:
notifications_services.add_watcher(self.object, user)
for user in removing_users:
notifications_services.remove_watcher(self.object, user)
self.object.watchers = notifications_services.get_watchers(self.object)
def to_native(self, obj):
ret = super(WatcheableObjectModelSerializer, self).to_native(obj)
ret["watchers"] = [user.email for user in notifications_services.get_watchers(obj)]
return ret
class HistoryExportSerializer(serializers.ModelSerializer):
user = HistoryUserField()
diff = HistoryDiffField(required=False)
@ -243,7 +287,7 @@ class HistoryExportSerializerMixin(serializers.ModelSerializer):
def get_history(self, obj):
history_qs = history_service.get_history_queryset_by_model_instance(obj,
types=(history_models.HistoryType.change, history_models.HistoryType.create,))
return HistoryExportSerializer(history_qs, many=True).data
@ -447,9 +491,8 @@ class RolePointsExportSerializer(serializers.ModelSerializer):
exclude = ('id', 'user_story')
class MilestoneExportSerializer(serializers.ModelSerializer):
class MilestoneExportSerializer(WatcheableObjectModelSerializer):
owner = UserRelatedField(required=False)
watchers = UserRelatedField(many=True, required=False)
modified_date = serializers.DateTimeField(required=False)
def __init__(self, *args, **kwargs):
@ -475,13 +518,12 @@ class MilestoneExportSerializer(serializers.ModelSerializer):
class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin,
AttachmentExportSerializerMixin, serializers.ModelSerializer):
AttachmentExportSerializerMixin, WatcheableObjectModelSerializer):
owner = UserRelatedField(required=False)
status = ProjectRelatedField(slug_field="name")
user_story = ProjectRelatedField(slug_field="ref", required=False)
milestone = ProjectRelatedField(slug_field="name", required=False)
assigned_to = UserRelatedField(required=False)
watchers = UserRelatedField(many=True, required=False)
modified_date = serializers.DateTimeField(required=False)
class Meta:
@ -493,13 +535,12 @@ class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryE
class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin,
AttachmentExportSerializerMixin, serializers.ModelSerializer):
AttachmentExportSerializerMixin, WatcheableObjectModelSerializer):
role_points = RolePointsExportSerializer(many=True, required=False)
owner = UserRelatedField(required=False)
assigned_to = UserRelatedField(required=False)
status = ProjectRelatedField(slug_field="name")
milestone = ProjectRelatedField(slug_field="name", required=False)
watchers = UserRelatedField(many=True, required=False)
modified_date = serializers.DateTimeField(required=False)
generated_from_issue = ProjectRelatedField(slug_field="ref", required=False)
@ -512,7 +553,7 @@ class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, His
class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin,
AttachmentExportSerializerMixin, serializers.ModelSerializer):
AttachmentExportSerializerMixin, WatcheableObjectModelSerializer):
owner = UserRelatedField(required=False)
status = ProjectRelatedField(slug_field="name")
assigned_to = UserRelatedField(required=False)
@ -520,7 +561,6 @@ class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, History
severity = ProjectRelatedField(slug_field="name")
type = ProjectRelatedField(slug_field="name")
milestone = ProjectRelatedField(slug_field="name", required=False)
watchers = UserRelatedField(many=True, required=False)
votes = serializers.SerializerMethodField("get_votes")
modified_date = serializers.DateTimeField(required=False)
@ -536,10 +576,9 @@ class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, History
class WikiPageExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin,
serializers.ModelSerializer):
WatcheableObjectModelSerializer):
owner = UserRelatedField(required=False)
last_modifier = UserRelatedField(required=False)
watchers = UserRelatedField(many=True, required=False)
modified_date = serializers.DateTimeField(required=False)
class Meta:
@ -586,7 +625,7 @@ class TimelineExportSerializer(serializers.ModelSerializer):
exclude = ('id', 'project', 'namespace', 'object_id')
class ProjectExportSerializer(serializers.ModelSerializer):
class ProjectExportSerializer(WatcheableObjectModelSerializer):
owner = UserRelatedField(required=False)
default_points = serializers.SlugRelatedField(slug_field="name", required=False)
default_us_status = serializers.SlugRelatedField(slug_field="name", required=False)

View File

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

View File

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

View File

@ -15,11 +15,12 @@
# 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.projects.models import Membership, Project
from .permissions import OWNERS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
from django.apps import apps
def _get_user_project_membership(user, project):
Membership = apps.get_model("projects", "Membership")
if user.is_anonymous():
return None
@ -30,7 +31,7 @@ def _get_user_project_membership(user, project):
def _get_object_project(obj):
project = None
Project = apps.get_model("projects", "Project")
if isinstance(obj, Project):
project = obj
elif obj and hasattr(obj, 'project'):

View File

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

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.projects.history.mixins import HistoryResourceMixin
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
from taiga.projects.mixins.ordering import BulkUpdateOrderMixin
from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin
@ -44,15 +45,14 @@ from . import models
from . import permissions
from . import services
from .votes import serializers as votes_serializers
from .votes import services as votes_service
from .votes.utils import attach_votescount_to_queryset
from .votes.mixins.viewsets import StarredResourceMixin, VotersViewSetMixin
######################################################
## Project
######################################################
class ProjectViewSet(HistoryResourceMixin, ModelCrudViewSet):
class ProjectViewSet(StarredResourceMixin, HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet):
queryset = models.Project.objects.all()
serializer_class = serializers.ProjectDetailSerializer
admin_serializer_class = serializers.ProjectDetailAdminSerializer
list_serializer_class = serializers.ProjectSerializer
@ -61,6 +61,11 @@ class ProjectViewSet(HistoryResourceMixin, ModelCrudViewSet):
filter_fields = (('member', 'members'),)
order_by_fields = ("memberships__user_order",)
def get_queryset(self):
qs = super().get_queryset()
qs = self.attach_votes_attrs_to_queryset(qs)
return self.attach_watchers_attrs_to_queryset(qs)
@list_route(methods=["POST"])
def bulk_update_order(self, request, **kwargs):
if self.request.user.is_anonymous():
@ -74,10 +79,6 @@ class ProjectViewSet(HistoryResourceMixin, ModelCrudViewSet):
services.update_projects_order_in_bulk(data, "user_order", request.user)
return response.NoContent(data=None)
def get_queryset(self):
qs = models.Project.objects.all()
return attach_votescount_to_queryset(qs, as_field="stars_count")
def get_serializer_class(self):
if self.action == "list":
return self.list_serializer_class
@ -166,29 +167,6 @@ class ProjectViewSet(HistoryResourceMixin, ModelCrudViewSet):
self.check_permissions(request, "tags_colors", project)
return response.Ok(dict(project.tags_colors))
@detail_route(methods=["POST"])
def star(self, request, pk=None):
project = self.get_object()
self.check_permissions(request, "star", project)
votes_service.add_vote(project, user=request.user)
return response.Ok()
@detail_route(methods=["POST"])
def unstar(self, request, pk=None):
project = self.get_object()
self.check_permissions(request, "unstar", project)
votes_service.remove_vote(project, user=request.user)
return response.Ok()
@detail_route(methods=["GET"])
def fans(self, request, pk=None):
project = self.get_object()
self.check_permissions(request, "fans", project)
voters = votes_service.get_voters(project)
voters_data = votes_serializers.VoterSerializer(voters, many=True)
return response.Ok(voters_data.data)
@detail_route(methods=["POST"])
def create_template(self, request, **kwargs):
template_name = request.DATA.get('template_name', None)
@ -287,6 +265,14 @@ class ProjectViewSet(HistoryResourceMixin, ModelCrudViewSet):
return response.NoContent()
class ProjectFansViewSet(VotersViewSetMixin, ModelListViewSet):
permission_classes = (permissions.ProjectFansPermission,)
resource_model = models.Project
class ProjectWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
permission_classes = (permissions.ProjectWatchersPermission,)
resource_model = models.Project
######################################################
## Custom values for selectors

View File

@ -27,12 +27,7 @@ def connect_memberships_signals():
sender=apps.get_model("projects", "Membership"),
dispatch_uid='membership_pre_delete')
# On membership object is deleted, update watchers of all objects relation.
signals.post_delete.connect(handlers.update_watchers_on_membership_post_delete,
sender=apps.get_model("projects", "Membership"),
dispatch_uid='update_watchers_on_membership_post_delete')
# On membership object is deleted, update watchers of all objects relation.
# On membership object is deleted, update notify policies of all objects relation.
signals.post_save.connect(handlers.create_notify_policy,
sender=apps.get_model("projects", "Membership"),
dispatch_uid='create-notify-policy')
@ -67,7 +62,6 @@ def connect_task_status_signals():
def disconnect_memberships_signals():
signals.pre_delete.disconnect(sender=apps.get_model("projects", "Membership"), dispatch_uid='membership_pre_delete')
signals.post_delete.disconnect(sender=apps.get_model("projects", "Membership"), dispatch_uid='update_watchers_on_membership_post_delete')
signals.post_save.disconnect(sender=apps.get_model("projects", "Membership"), dispatch_uid='create-notify-policy')

View File

@ -288,7 +288,7 @@ def userstory_freezer(us) -> dict:
"milestone": us.milestone_id,
"client_requirement": us.client_requirement,
"team_requirement": us.team_requirement,
"watchers": [x.id for x in us.watchers.all()],
"watchers": [x.id for x in us.get_watchers()],
"attachments": extract_attachments(us),
"tags": us.tags,
"points": points,
@ -315,7 +315,7 @@ def issue_freezer(issue) -> dict:
"description": issue.description,
"description_html": mdrender(issue.project, issue.description),
"assigned_to": issue.assigned_to_id,
"watchers": [x.pk for x in issue.watchers.all()],
"watchers": [x.pk for x in issue.get_watchers()],
"attachments": extract_attachments(issue),
"tags": issue.tags,
"is_blocked": issue.is_blocked,
@ -337,7 +337,7 @@ def task_freezer(task) -> dict:
"description": task.description,
"description_html": mdrender(task.project, task.description),
"assigned_to": task.assigned_to_id,
"watchers": [x.pk for x in task.watchers.all()],
"watchers": [x.pk for x in task.get_watchers()],
"attachments": extract_attachments(task),
"taskboard_order": task.taskboard_order,
"us_order": task.us_order,
@ -359,7 +359,7 @@ def wikipage_freezer(wiki) -> dict:
"owner": wiki.owner_id,
"content": wiki.content,
"content_html": mdrender(wiki.project, wiki.content),
"watchers": [x.pk for x in wiki.watchers.all()],
"watchers": [x.pk for x in wiki.get_watchers()],
"attachments": extract_attachments(wiki),
}

View File

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

View File

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

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

View File

@ -19,17 +19,19 @@ from taiga.base.fields import TagsField
from taiga.base.fields import PgArrayField
from taiga.base.neighbors import NeighborsSerializerMixin
from taiga.mdrender.service import render as mdrender
from taiga.projects.validators import ProjectExistsValidator
from taiga.projects.notifications.validators import WatchersValidator
from taiga.projects.serializers import BasicIssueStatusSerializer
from taiga.projects.notifications.mixins import WatchedResourceModelSerializer
from taiga.projects.votes.mixins.serializers import VotedResourceSerializerMixin
from taiga.users.serializers import UserBasicInfoSerializer
from . import models
class IssueSerializer(WatchersValidator, serializers.ModelSerializer):
class IssueSerializer(WatchersValidator, VotedResourceSerializerMixin, WatchedResourceModelSerializer, serializers.ModelSerializer):
tags = TagsField(required=False)
external_reference = PgArrayField(required=False)
is_closed = serializers.Field(source="is_closed")
@ -37,7 +39,6 @@ class IssueSerializer(WatchersValidator, serializers.ModelSerializer):
generated_user_stories = serializers.SerializerMethodField("get_generated_user_stories")
blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html")
description_html = serializers.SerializerMethodField("get_description_html")
votes = serializers.SerializerMethodField("get_votes_number")
status_extra_info = BasicIssueStatusSerializer(source="status", required=False, read_only=True)
assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", required=False, read_only=True)
owner_extra_info = UserBasicInfoSerializer(source="owner", required=False, read_only=True)
@ -59,10 +60,6 @@ class IssueSerializer(WatchersValidator, serializers.ModelSerializer):
def get_description_html(self, obj):
return mdrender(obj.project, obj.description)
def get_votes_number(self, obj):
# The "votes_count" attribute is attached in the get_queryset of the viewset.
return getattr(obj, "votes_count", 0)
class IssueListSerializer(IssueSerializer):
class Meta:

View File

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

View File

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

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/>.
from django.contrib import admin
from taiga.projects.notifications.admin import WatchedInline
from taiga.projects.votes.admin import VoteInline
from . import models
@ -30,6 +32,7 @@ class MilestoneAdmin(admin.ModelAdmin):
list_display_links = list_display
list_filter = ["project"]
readonly_fields = ["owner"]
inlines = [WatchedInline, VoteInline]
admin.site.register(models.Milestone, MilestoneAdmin)

View File

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

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/>.
from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
IsProjectOwner, AllowAny,
PermissionComponent, IsSuperUser)
IsAuthenticated, IsProjectOwner, AllowAny,
IsSuperUser)
class MilestonePermission(TaigaResourcePermission):
@ -29,3 +29,11 @@ class MilestonePermission(TaigaResourcePermission):
destroy_perms = HasProjectPerm('delete_milestone')
list_perms = AllowAny()
stats_perms = HasProjectPerm('view_milestones')
watch_perms = IsAuthenticated() & HasProjectPerm('view_milestones')
unwatch_perms = IsAuthenticated() & HasProjectPerm('view_milestones')
class MilestoneWatchersPermission(TaigaResourcePermission):
enought_perms = IsProjectOwner() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_milestones')
list_perms = HasProjectPerm('view_milestones')

View File

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

View File

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

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.projects.notifications.choices import NotifyLevel
from taiga.projects.notifications.models import Watched
from taiga.projects.models import Project
from taiga.users import services as user_services
from . import serializers
from . import models
from . import permissions
@ -32,9 +33,13 @@ class NotifyPolicyViewSet(ModelCrudViewSet):
permission_classes = (permissions.NotifyPolicyPermission,)
def _build_needed_notify_policies(self):
watched_content = user_services.get_watched_content_for_user(self.request.user)
watched_content_project_ids = watched_content.values_list("project__id", flat=True).distinct()
projects = Project.objects.filter(
Q(owner=self.request.user) |
Q(memberships__user=self.request.user)
Q(memberships__user=self.request.user) |
Q(id__in=watched_content_project_ids)
).distinct()
for project in projects:
@ -45,5 +50,14 @@ class NotifyPolicyViewSet(ModelCrudViewSet):
return models.NotifyPolicy.objects.none()
self._build_needed_notify_policies()
qs = models.NotifyPolicy.objects.filter(user=self.request.user)
return qs.distinct()
# With really want to include the policies related to any content:
# - The user is the owner of the project
# - The user is member of the project
# - The user is watching any object from the project
watched_content = user_services.get_watched_content_for_user(self.request.user)
watched_content_project_ids = watched_content.values_list("project__id", flat=True).distinct()
return models.NotifyPolicy.objects.filter(Q(project__owner=self.request.user) |
Q(project__memberships__user=self.request.user) |
Q(project__id__in=watched_content_project_ids)
).distinct()

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 operator import is_not
from django.conf import settings
from django.apps import apps
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.utils.translation import ugettext_lazy as _
from taiga.base import response
from taiga.base.decorators import detail_route
from taiga.base.api import serializers
from taiga.base.api.utils import get_object_or_404
from taiga.base.fields import WatchersField
from taiga.projects.notifications import services
from taiga.projects.notifications.utils import attach_watchers_to_queryset, attach_is_watched_to_queryset
from taiga.users.models import User
from . import models
from . serializers import WatcherSerializer
class WatchedResourceMixin(object):
class WatchedResourceMixin:
"""
Rest Framework resource mixin for resources susceptible
to be notifiable about their changes.
@ -36,6 +48,27 @@ class WatchedResourceMixin(object):
_not_notify = False
def attach_watchers_attrs_to_queryset(self, queryset):
qs = attach_watchers_to_queryset(queryset)
if self.request.user.is_authenticated():
qs = attach_is_watched_to_queryset(self.request.user, qs)
return qs
@detail_route(methods=["POST"])
def watch(self, request, pk=None):
obj = self.get_object()
self.check_permissions(request, "watch", obj)
services.add_watcher(obj, request.user)
return response.Ok()
@detail_route(methods=["POST"])
def unwatch(self, request, pk=None):
obj = self.get_object()
self.check_permissions(request, "unwatch", obj)
services.remove_watcher(obj, request.user)
return response.Ok()
def send_notifications(self, obj, history=None):
"""
Shortcut method for resources with special save
@ -73,7 +106,7 @@ class WatchedResourceMixin(object):
super().pre_delete(obj)
class WatchedModelMixin(models.Model):
class WatchedModelMixin(object):
"""
Generic model mixin that makes model compatible
with notification system.
@ -82,11 +115,6 @@ class WatchedModelMixin(models.Model):
this mixin if you want send notifications about
your model class.
"""
watchers = models.ManyToManyField(settings.AUTH_USER_MODEL, null=True, blank=True,
related_name="%(app_label)s_%(class)s+",
verbose_name=_("watchers"))
class Meta:
abstract = True
def get_project(self) -> object:
"""
@ -112,7 +140,16 @@ class WatchedModelMixin(models.Model):
very inefficient way for obtain watchers but at
this momment is the simplest way.
"""
return frozenset(self.watchers.all())
return frozenset(services.get_watchers(self))
def get_watched(self, user_or_id):
return services.get_watched(user_or_id, type(self))
def add_watcher(self, user):
services.add_watcher(self, user)
def remove_watcher(self, user):
services.remove_watcher(self, user)
def get_owner(self) -> object:
"""
@ -140,3 +177,79 @@ class WatchedModelMixin(models.Model):
self.get_owner(),)
is_not_none = partial(is_not, None)
return frozenset(filter(is_not_none, participants))
class WatchedResourceModelSerializer(serializers.ModelSerializer):
is_watched = serializers.SerializerMethodField("get_is_watched")
watchers = WatchersField(required=False)
def get_is_watched(self, obj):
# The "is_watched" attribute is attached in the get_queryset of the viewset.
return getattr(obj, "is_watched", False) or False
def restore_object(self, attrs, instance=None):
#watchers is not a field from the model but can be attached in the get_queryset of the viewset.
#If that's the case we need to remove it before calling the super method
watcher_field = self.fields.pop("watchers", None)
instance = super(WatchedResourceModelSerializer, self).restore_object(attrs, instance)
if instance is not None and self.validate_watchers(attrs, "watchers"):
new_watcher_ids = set(attrs.get("watchers", []))
old_watcher_ids = set(services.get_watchers(instance).values_list("id", flat=True))
adding_watcher_ids = list(new_watcher_ids.difference(old_watcher_ids))
removing_watcher_ids = list(old_watcher_ids.difference(new_watcher_ids))
User = apps.get_model("users", "User")
adding_users = User.objects.filter(id__in=adding_watcher_ids)
removing_users = User.objects.filter(id__in=removing_watcher_ids)
for user in adding_users:
services.add_watcher(instance, user)
for user in removing_users:
services.remove_watcher(instance, user)
instance.watchers = services.get_watchers(instance)
return instance
def to_native(self, obj):
#watchers is wasn't attached via the get_queryset of the viewset we need to manually add it
if not hasattr(obj, "watchers"):
obj.watchers = [user.id for user in services.get_watchers(obj)]
return super(WatchedResourceModelSerializer, self).to_native(obj)
class WatchersViewSetMixin:
# Is a ModelListViewSet with two required params: permission_classes and resource_model
serializer_class = WatcherSerializer
list_serializer_class = WatcherSerializer
permission_classes = None
resource_model = None
def retrieve(self, request, *args, **kwargs):
pk = kwargs.get("pk", None)
resource_id = kwargs.get("resource_id", None)
resource = get_object_or_404(self.resource_model, pk=resource_id)
self.check_permissions(request, 'retrieve', resource)
try:
self.object = services.get_watchers(resource).get(pk=pk)
except ObjectDoesNotExist: # or User.DoesNotExist
return response.NotFound()
serializer = self.get_serializer(self.object)
return response.Ok(serializer.data)
def list(self, request, *args, **kwargs):
resource_id = kwargs.get("resource_id", None)
resource = get_object_or_404(self.resource_model, pk=resource_id)
self.check_permissions(request, 'list', resource)
return super().list(request, *args, **kwargs)
def get_queryset(self):
resource = self.resource_model.objects.get(pk=self.kwargs.get("resource_id"))
return services.get_watchers(resource)

View File

@ -14,13 +14,15 @@
# 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.conf import settings
from django.contrib.contenttypes import generic
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from taiga.projects.history.choices import HISTORY_TYPE_CHOICES
from .choices import NOTIFY_LEVEL_CHOICES
from .choices import NOTIFY_LEVEL_CHOICES, NotifyLevel
class NotifyPolicy(models.Model):
@ -72,3 +74,19 @@ class HistoryChangeNotification(models.Model):
class Meta:
unique_together = ("key", "owner", "project", "history_type")
class Watched(models.Model):
content_type = models.ForeignKey("contenttypes.ContentType")
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey("content_type", "object_id")
user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=False, null=False,
related_name="watched", verbose_name=_("user"))
created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False,
verbose_name=_("created date"))
project = models.ForeignKey("projects.Project", null=False, blank=False,
verbose_name=_("project"),related_name="watched")
class Meta:
verbose_name = _("Watched")
verbose_name_plural = _("Watched")
unique_together = ("content_type", "object_id", "user", "project")

View File

@ -17,9 +17,10 @@
import json
from taiga.base.api import serializers
from taiga.users.models import User
from . import models
from . import choices
class NotifyPolicySerializer(serializers.ModelSerializer):
@ -31,3 +32,11 @@ class NotifyPolicySerializer(serializers.ModelSerializer):
def get_project_name(self, obj):
return obj.project.name
class WatcherSerializer(serializers.ModelSerializer):
full_name = serializers.CharField(source='get_full_name', required=False)
class Meta:
model = User
fields = ('id', 'username', 'full_name')

View File

@ -17,10 +17,11 @@
from functools import partial
from django.apps import apps
from django.db import IntegrityError
from django.db.transaction import atomic
from django.db import IntegrityError, transaction
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth import get_user_model
from django.utils import timezone
from django.db import transaction
from django.conf import settings
from django.utils.translation import ugettext as _
@ -36,7 +37,7 @@ from taiga.projects.history.services import (make_key_from_model_object,
from taiga.permissions.service import user_has_perm
from taiga.users.models import User
from .models import HistoryChangeNotification
from .models import HistoryChangeNotification, Watched
def notify_policy_exists(project, user) -> bool:
@ -121,11 +122,11 @@ def analize_object_for_watchers(obj:object, history:object):
if data["mentions"]:
for user in data["mentions"]:
obj.watchers.add(user)
obj.add_watcher(user)
# Adding the person who edited the object to the watchers
if history.comment and not history.owner.is_system:
obj.watchers.add(history.owner)
obj.add_watcher(history.owner)
def _filter_by_permissions(obj, user):
UserStory = apps.get_model("userstories", "UserStory")
@ -170,15 +171,19 @@ def get_users_to_notify(obj, *, discard_users=None) -> list:
candidates = set()
candidates.update(filter(_can_notify_hard, project.members.all()))
candidates.update(filter(_can_notify_light, obj.get_watchers()))
candidates.update(filter(_can_notify_light, obj.project.get_watchers()))
candidates.update(filter(_can_notify_light, obj.get_participants()))
#TODO: coger los watchers del proyecto que quieren ser notificados por correo
#Filtrar los watchers según su nivel de watched y su nivel en el proyecto
# Remove the changer from candidates
if discard_users:
candidates = candidates - set(discard_users)
candidates = filter(partial(_filter_by_permissions, obj), candidates)
candidates = set(filter(partial(_filter_by_permissions, obj), candidates))
# Filter disabled and system users
candidates = filter(partial(_filter_notificable), candidates)
candidates = set(filter(partial(_filter_notificable), candidates))
return frozenset(candidates)
@ -282,3 +287,72 @@ def send_sync_notifications(notification_id):
def process_sync_notifications():
for notification in HistoryChangeNotification.objects.all():
send_sync_notifications(notification.pk)
def get_watchers(obj):
"""Get the watchers of an object.
:param obj: Any Django model instance.
:return: User queryset object representing the users that voted the object.
"""
obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj)
return get_user_model().objects.filter(watched__content_type=obj_type, watched__object_id=obj.id)
def get_watched(user_or_id, model):
"""Get the objects watched by an user.
:param user_or_id: :class:`~taiga.users.models.User` instance or id.
:param model: Show only objects of this kind. Can be any Django model class.
:return: Queryset of objects representing the votes of the user.
"""
obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model)
conditions = ('notifications_watched.content_type_id = %s',
'%s.id = notifications_watched.object_id' % model._meta.db_table,
'notifications_watched.user_id = %s')
if isinstance(user_or_id, get_user_model()):
user_id = user_or_id.id
else:
user_id = user_or_id
return model.objects.extra(where=conditions, tables=('notifications_watched',),
params=(obj_type.id, user_id))
def add_watcher(obj, user):
"""Add a watcher to an object.
If the user is already watching the object nothing happents (except if there is a level update),
so this function can be considered idempotent.
:param obj: Any Django model instance.
:param user: User adding the watch. :class:`~taiga.users.models.User` instance.
"""
obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj)
watched, created = Watched.objects.get_or_create(content_type=obj_type,
object_id=obj.id, user=user, project=obj.project)
notify_policy, _ = apps.get_model("notifications", "NotifyPolicy").objects.get_or_create(
project=obj.project, user=user, defaults={"notify_level": NotifyLevel.watch})
return watched
def remove_watcher(obj, user):
"""Remove an watching user from an object.
If the user has not watched the object nothing happens so this function can be considered
idempotent.
:param obj: Any Django model instance.
:param user: User removing the watch. :class:`~taiga.users.models.User` instance.
"""
obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj)
qs = Watched.objects.filter(content_type=obj_type, object_id=obj.id, user=user)
if not qs.exists():
return
qs.delete()

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

View File

@ -54,19 +54,34 @@ class ProjectPermission(TaigaResourcePermission):
list_perms = AllowAny()
stats_perms = HasProjectPerm('view_project')
member_stats_perms = HasProjectPerm('view_project')
issues_stats_perms = HasProjectPerm('view_project')
regenerate_userstories_csv_uuid_perms = IsProjectOwner()
regenerate_issues_csv_uuid_perms = IsProjectOwner()
regenerate_tasks_csv_uuid_perms = IsProjectOwner()
star_perms = IsAuthenticated()
unstar_perms = IsAuthenticated()
issues_stats_perms = HasProjectPerm('view_project')
tags_perms = HasProjectPerm('view_project')
tags_colors_perms = HasProjectPerm('view_project')
fans_perms = HasProjectPerm('view_project')
star_perms = IsAuthenticated() & HasProjectPerm('view_project')
unstar_perms = IsAuthenticated() & HasProjectPerm('view_project')
watch_perms = IsAuthenticated() & HasProjectPerm('view_project')
unwatch_perms = IsAuthenticated() & HasProjectPerm('view_project')
create_template_perms = IsSuperUser()
leave_perms = CanLeaveProject()
class ProjectFansPermission(TaigaResourcePermission):
enought_perms = IsProjectOwner() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_project')
list_perms = HasProjectPerm('view_project')
class ProjectWatchersPermission(TaigaResourcePermission):
enought_perms = IsProjectOwner() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_project')
list_perms = HasProjectPerm('view_project')
class MembershipPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project')
create_perms = IsProjectOwner()

View File

@ -25,6 +25,8 @@ from taiga.base.fields import PgArrayField
from taiga.base.fields import TagsField
from taiga.base.fields import TagsColorsField
from taiga.projects.notifications.validators import WatchersValidator
from taiga.users.services import get_photo_or_gravatar_url
from taiga.users.serializers import UserSerializer
from taiga.users.serializers import UserBasicInfoSerializer
@ -40,7 +42,8 @@ from .validators import ProjectExistsValidator
from .custom_attributes.serializers import UserStoryCustomAttributeSerializer
from .custom_attributes.serializers import TaskCustomAttributeSerializer
from .custom_attributes.serializers import IssueCustomAttributeSerializer
from .notifications.mixins import WatchedResourceModelSerializer
from .votes.mixins.serializers import StarredResourceSerializerMixin
######################################################
## Custom values for selectors
@ -305,11 +308,10 @@ class ProjectMemberSerializer(serializers.ModelSerializer):
## Projects
######################################################
class ProjectSerializer(serializers.ModelSerializer):
class ProjectSerializer(WatchersValidator, StarredResourceSerializerMixin, WatchedResourceModelSerializer, serializers.ModelSerializer):
tags = TagsField(default=[], required=False)
anon_permissions = PgArrayField(required=False)
public_permissions = PgArrayField(required=False)
stars = serializers.SerializerMethodField("get_stars_number")
my_permissions = serializers.SerializerMethodField("get_my_permissions")
i_am_owner = serializers.SerializerMethodField("get_i_am_owner")
tags_colors = TagsColorsField(required=False)
@ -321,10 +323,6 @@ class ProjectSerializer(serializers.ModelSerializer):
exclude = ("last_us_ref", "last_task_ref", "last_issue_ref",
"issues_csv_uuid", "tasks_csv_uuid", "userstories_csv_uuid")
def get_stars_number(self, obj):
# The "stars_count" attribute is attached in the get_queryset of the viewset.
return getattr(obj, "stars_count", 0)
def get_my_permissions(self, obj):
if "request" in self.context:
return get_user_project_permissions(self.context["request"].user, obj)

View File

@ -45,24 +45,6 @@ def membership_post_delete(sender, instance, using, **kwargs):
instance.project.update_role_points()
def update_watchers_on_membership_post_delete(sender, instance, using, **kwargs):
models = [apps.get_model("userstories", "UserStory"),
apps.get_model("tasks", "Task"),
apps.get_model("issues", "Issue")]
# `user_id` is used beacuse in some momments
# instance.user can contain pointer to now
# removed object from a database.
for model in models:
#filter(project=instance.project)
filter = {
"user_id": instance.user_id,
"%s__project"%(model._meta.model_name): instance.project,
}
model.watchers.through.objects.filter(**filter).delete()
def create_notify_policy(sender, instance, using, **kwargs):
if instance.user:
create_notify_policy_if_not_exists(instance.project, instance.user)

View File

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

View File

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

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/>.
from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
IsProjectOwner, AllowAny, IsSuperUser)
IsAuthenticated, IsProjectOwner, AllowAny,
IsSuperUser)
class TaskPermission(TaigaResourcePermission):
@ -30,3 +31,21 @@ class TaskPermission(TaigaResourcePermission):
csv_perms = AllowAny()
bulk_create_perms = HasProjectPerm('add_task')
bulk_update_order_perms = HasProjectPerm('modify_task')
upvote_perms = IsAuthenticated() & HasProjectPerm('view_tasks')
downvote_perms = IsAuthenticated() & HasProjectPerm('view_tasks')
watch_perms = IsAuthenticated() & HasProjectPerm('view_tasks')
unwatch_perms = IsAuthenticated() & HasProjectPerm('view_tasks')
class TaskVotersPermission(TaigaResourcePermission):
enought_perms = IsProjectOwner() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_tasks')
list_perms = HasProjectPerm('view_tasks')
class TaskWatchersPermission(TaigaResourcePermission):
enought_perms = IsProjectOwner() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_tasks')
list_perms = HasProjectPerm('view_tasks')

View File

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

View File

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

View File

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

View File

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

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()
bulk_create_perms = IsAuthenticated() & (HasProjectPerm('add_us_to_project') | HasProjectPerm('add_us'))
bulk_update_order_perms = HasProjectPerm('modify_us')
upvote_perms = IsAuthenticated() & HasProjectPerm('view_us')
downvote_perms = IsAuthenticated() & HasProjectPerm('view_us')
watch_perms = IsAuthenticated() & HasProjectPerm('view_us')
unwatch_perms = IsAuthenticated() & HasProjectPerm('view_us')
class UserStoryVotersPermission(TaigaResourcePermission):
enought_perms = IsProjectOwner() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_us')
list_perms = HasProjectPerm('view_us')
class UserStoryWatchersPermission(TaigaResourcePermission):
enought_perms = IsProjectOwner() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_us')
list_perms = HasProjectPerm('view_us')

View File

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

View File

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

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/>.
from django.conf import settings
from django.contrib.contenttypes import generic
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.contrib.contenttypes import generic
class Votes(models.Model):
content_type = models.ForeignKey("contenttypes.ContentType")
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey("content_type", "object_id")
count = models.PositiveIntegerField(default=0)
count = models.PositiveIntegerField(null=False, blank=False, default=0, verbose_name=_("count"))
class Meta:
verbose_name = _("Votes")
@ -44,10 +44,12 @@ class Votes(models.Model):
class Vote(models.Model):
content_type = models.ForeignKey("contenttypes.ContentType")
object_id = models.PositiveIntegerField(null=False)
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey("content_type", "object_id")
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False,
related_name="votes", verbose_name=_("votes"))
related_name="votes", verbose_name=_("user"))
created_date = models.DateTimeField(null=False, blank=False, auto_now_add=True,
verbose_name=_("created date"))
class Meta:
verbose_name = _("Vote")
@ -61,4 +63,4 @@ class Vote(models.Model):
return None
def __str__(self):
return self.user
return self.user.get_full_name()

View File

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

View File

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

View File

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

View File

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

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/>.
from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
IsProjectOwner, AllowAny, IsSuperUser)
IsAuthenticated, IsProjectOwner, AllowAny,
IsSuperUser)
class WikiPagePermission(TaigaResourcePermission):
@ -29,6 +30,16 @@ class WikiPagePermission(TaigaResourcePermission):
destroy_perms = HasProjectPerm('delete_wiki_page')
list_perms = AllowAny()
render_perms = AllowAny()
watch_perms = IsAuthenticated() & HasProjectPerm('view_wiki_pages')
unwatch_perms = IsAuthenticated() & HasProjectPerm('view_wiki_pages')
class WikiPageWatchersPermission(TaigaResourcePermission):
enought_perms = IsProjectOwner() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_wiki_pages')
list_perms = HasProjectPerm('view_wiki_pages')
class WikiLinkPermission(TaigaResourcePermission):
enought_perms = IsProjectOwner() | IsSuperUser()

View File

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

View File

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

View File

@ -22,14 +22,12 @@ from taiga.projects.history import services as history_services
from taiga.projects.models import Project
from taiga.users.models import User
from taiga.projects.history.choices import HistoryType
from taiga.projects.notifications import services as notifications_services
from taiga.timeline.service import (push_to_timeline,
build_user_namespace,
build_project_namespace,
extract_user_info)
# TODO: Add events to followers timeline when followers are implemented.
# TODO: Add events to project watchers timeline when project watchers are implemented.
def _push_to_timeline(*args, **kwargs):
if settings.CELERY_ENABLED:
@ -60,9 +58,9 @@ def _push_to_timelines(project, user, obj, event_type, created_datetime, extra_d
related_people |= User.objects.filter(id=obj.assigned_to_id)
## - Watchers
watchers = getattr(obj, "watchers", None)
watchers = notifications_services.get_watchers(obj)
if watchers:
related_people |= obj.watchers.all()
related_people |= watchers
## - Exclude inactive and system users and remove duplicate
related_people = related_people.exclude(is_active=False)

View File

@ -114,6 +114,33 @@ class UsersViewSet(ModelCrudViewSet):
self.check_permissions(request, "stats", user)
return response.Ok(services.get_stats_for_user(user, request.user))
@detail_route(methods=["GET"])
def favourites(self, request, *args, **kwargs):
for_user = get_object_or_404(models.User, **kwargs)
from_user = request.user
self.check_permissions(request, 'favourites', for_user)
filters = {
"type": request.GET.get("type", None),
"action": request.GET.get("action", None),
"q": request.GET.get("q", None),
}
self.object_list = services.get_favourites_list(for_user, from_user, **filters)
page = self.paginate_queryset(self.object_list)
extra_args = {
"many": True,
"user_votes": services.get_voted_content_for_user(request.user),
"user_watching": services.get_watched_content_for_user(request.user),
}
if page is not None:
serializer = serializers.FavouriteSerializer(page.object_list, **extra_args)
else:
serializer = serializers.FavouriteSerializer(self.object_list, **extra_args)
return response.Ok(serializer.data)
@list_route(methods=["POST"])
def password_recovery(self, request, pk=None):
username_or_email = request.DATA.get('username', None)
@ -224,15 +251,6 @@ class UsersViewSet(ModelCrudViewSet):
user_data = self.admin_serializer_class(request.user).data
return response.Ok(user_data)
@detail_route(methods=["GET"])
def starred(self, request, pk=None):
user = self.get_object()
self.check_permissions(request, 'starred', user)
stars = votes_service.get_voted(user.pk, model=apps.get_model('projects', 'Project'))
stars_data = StarredSerializer(stars, many=True)
return response.Ok(stars_data.data)
#TODO: commit_on_success
def partial_update(self, request, *args, **kwargs):
"""

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()
change_email_perms = AllowAny()
contacts_perms = AllowAny()
favourites_perms = AllowAny()
class RolesPermission(TaigaResourcePermission):

View File

@ -19,11 +19,14 @@ from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from taiga.base.api import serializers
from taiga.base.fields import PgArrayField
from taiga.base.fields import PgArrayField, TagsField
from taiga.projects.models import Project
from .models import User, Role
from .services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url
from collections import namedtuple
import re
@ -149,3 +152,55 @@ class ProjectRoleSerializer(serializers.ModelSerializer):
model = Role
fields = ('id', 'name', 'slug', 'order', 'computable')
i18n_fields = ("name",)
######################################################
## Favourite
######################################################
class FavouriteSerializer(serializers.Serializer):
type = serializers.CharField()
action = serializers.CharField()
id = serializers.IntegerField()
ref = serializers.IntegerField()
slug = serializers.CharField()
subject = serializers.CharField()
tags = TagsField(default=[])
project = serializers.IntegerField()
assigned_to = serializers.IntegerField()
total_watchers = serializers.IntegerField()
is_voted = serializers.SerializerMethodField("get_is_voted")
is_watched = serializers.SerializerMethodField("get_is_watched")
created_date = serializers.DateTimeField()
project_name = serializers.CharField()
project_slug = serializers.CharField()
project_is_private = serializers.CharField()
assigned_to_username = serializers.CharField()
assigned_to_full_name = serializers.CharField()
assigned_to_photo = serializers.SerializerMethodField("get_photo")
total_votes = serializers.IntegerField()
def __init__(self, *args, **kwargs):
# Don't pass the extra ids args up to the superclass
self.user_votes = kwargs.pop("user_votes", {})
self.user_watching = kwargs.pop("user_watching", {})
# Instantiate the superclass normally
super(FavouriteSerializer, self).__init__(*args, **kwargs)
def get_is_voted(self, obj):
return obj["id"] in self.user_votes.get(obj["type"], [])
def get_is_watched(self, obj):
return obj["id"] in self.user_watching.get(obj["type"], [])
def get_photo(self, obj):
UserData = namedtuple("UserData", ["photo", "email"])
user_data = UserData(photo=obj["assigned_to_photo"], email=obj.get("assigned_to_email") or "")
return get_photo_or_gravatar_url(user_data)

View File

@ -20,6 +20,7 @@ This model contains a domain logic for users application.
from django.apps import apps
from django.db.models import Q
from django.db import connection
from django.conf import settings
from django.utils.translation import ugettext as _
@ -142,3 +143,173 @@ def get_stats_for_user(from_user, by_user):
'total_num_closed_userstories': total_num_closed_userstories,
}
return project_stats
def get_voted_content_for_user(user):
"""Returns a dict where:
- The key is the content_type model
- The values are list of id's of the different objects voted by the user
"""
if user.is_anonymous():
return {}
user_votes = {}
for (ct_model, object_id) in user.votes.values_list("content_type__model", "object_id"):
list = user_votes.get(ct_model, [])
list.append(object_id)
user_votes[ct_model] = list
return user_votes
def get_watched_content_for_user(user):
"""Returns a dict where:
- The key is the content_type model
- The values are list of id's of the different objects watched by the user
"""
if user.is_anonymous():
return {}
user_watches = {}
for (ct_model, object_id) in user.watched.values_list("content_type__model", "object_id"):
list = user_watches.get(ct_model, [])
list.append(object_id)
user_watches[ct_model] = list
return user_watches
def _build_favourites_sql_for_type(for_user, type, table_name, ref_column="ref",
project_column="project_id", assigned_to_column="assigned_to_id",
slug_column="slug", subject_column="subject"):
sql = """
SELECT {table_name}.id AS id, {ref_column} AS ref, '{type}' AS type, 'watch' AS action,
tags, notifications_watched.object_id AS object_id, {table_name}.{project_column} AS project,
{slug_column} AS slug, {subject_column} AS subject,
notifications_watched.created_date, coalesce(watchers, 0) as total_watchers, coalesce(votes_votes.count, 0) total_votes, {assigned_to_column} AS assigned_to
FROM notifications_watched
INNER JOIN django_content_type
ON (notifications_watched.content_type_id = django_content_type.id AND django_content_type.model = '{type}')
INNER JOIN {table_name}
ON ({table_name}.id = notifications_watched.object_id)
LEFT JOIN (SELECT object_id, content_type_id, count(*) watchers FROM notifications_watched GROUP BY object_id, content_type_id) type_watchers
ON {table_name}.id = type_watchers.object_id AND django_content_type.id = type_watchers.content_type_id
LEFT JOIN votes_votes
ON ({table_name}.id = votes_votes.object_id AND django_content_type.id = votes_votes.content_type_id)
WHERE notifications_watched.user_id = {for_user_id}
UNION
SELECT {table_name}.id AS id, {ref_column} AS ref, '{type}' AS type, 'vote' AS action,
tags, votes_vote.object_id AS object_id, {table_name}.{project_column} AS project,
{slug_column} AS slug, {subject_column} AS subject,
votes_vote.created_date, coalesce(watchers, 0) as total_watchers, votes_votes.count total_votes, {assigned_to_column} AS assigned_to
FROM votes_vote
INNER JOIN django_content_type
ON (votes_vote.content_type_id = django_content_type.id AND django_content_type.model = '{type}')
INNER JOIN {table_name}
ON ({table_name}.id = votes_vote.object_id)
LEFT JOIN (SELECT object_id, content_type_id, count(*) watchers FROM notifications_watched GROUP BY object_id, content_type_id) type_watchers
ON {table_name}.id = type_watchers.object_id AND django_content_type.id = type_watchers.content_type_id
LEFT JOIN votes_votes
ON ({table_name}.id = votes_votes.object_id AND django_content_type.id = votes_votes.content_type_id)
WHERE votes_vote.user_id = {for_user_id}
"""
sql = sql.format(for_user_id=for_user.id, type=type, table_name=table_name,
ref_column = ref_column, project_column=project_column,
assigned_to_column=assigned_to_column, slug_column=slug_column,
subject_column=subject_column)
return sql
def get_favourites_list(for_user, from_user, type=None, action=None, q=None):
filters_sql = ""
and_needed = False
if type:
filters_sql += " AND type = '{type}' ".format(type=type)
if action:
filters_sql += " AND action = '{action}' ".format(action=action)
if q:
filters_sql += " AND to_tsvector(coalesce(subject, '')) @@ plainto_tsquery('{q}') ".format(q=q)
sql = """
-- BEGIN Basic info: we need to mix info from different tables and denormalize it
SELECT entities.*,
projects_project.name as project_name, projects_project.slug as project_slug, projects_project.is_private as project_is_private,
users_user.username assigned_to_username, users_user.full_name assigned_to_full_name, users_user.photo assigned_to_photo, users_user.email assigned_to_email
FROM (
{userstories_sql}
UNION
{tasks_sql}
UNION
{issues_sql}
UNION
{projects_sql}
) as entities
-- END Basic info
-- BEGIN Project info
LEFT JOIN projects_project
ON (entities.project = projects_project.id)
-- END Project info
-- BEGIN Assigned to user info
LEFT JOIN users_user
ON (assigned_to = users_user.id)
-- END Assigned to user info
-- BEGIN Permissions checking
LEFT JOIN projects_membership
-- Here we check the memberbships from the user requesting the info
ON (projects_membership.user_id = {from_user_id} AND projects_membership.project_id = entities.project)
LEFT JOIN users_role
ON (entities.project = users_role.project_id AND users_role.id = projects_membership.role_id)
WHERE
-- public project
(
projects_project.is_private = false
OR(
-- private project where the view_ permission is included in the user role for that project or in the anon permissions
projects_project.is_private = true
AND(
(entities.type = 'issue' AND 'view_issues' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions)))
OR (entities.type = 'task' AND 'view_tasks' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions)))
OR (entities.type = 'userstory' AND 'view_us' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions)))
OR (entities.type = 'project' AND 'view_project' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions)))
)
))
-- END Permissions checking
{filters_sql}
ORDER BY entities.created_date;
"""
from_user_id = -1
if not from_user.is_anonymous():
from_user_id = from_user.id
sql = sql.format(
for_user_id=for_user.id,
from_user_id=from_user_id,
filters_sql=filters_sql,
userstories_sql=_build_favourites_sql_for_type(for_user, "userstory", "userstories_userstory", slug_column="null"),
tasks_sql=_build_favourites_sql_for_type(for_user, "task", "tasks_task", slug_column="null"),
issues_sql=_build_favourites_sql_for_type(for_user, "issue", "issues_issue", slug_column="null"),
projects_sql=_build_favourites_sql_for_type(for_user, "project", "projects_project",
ref_column="null",
project_column="id",
assigned_to_column="null",
subject_column="projects_project.name")
)
cursor = connection.cursor()
cursor.execute(sql)
desc = cursor.description
return [
dict(zip([col[0] for col in desc], row))
for row in cursor.fetchall()
]

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

View File

@ -441,6 +441,17 @@ class VotesFactory(Factory):
object_id = factory.Sequence(lambda n: n)
class WatchedFactory(Factory):
class Meta:
model = "notifications.Watched"
strategy = factory.CREATE_STRATEGY
content_type = factory.SubFactory("tests.factories.ContentTypeFactory")
object_id = factory.Sequence(lambda n: n)
user = factory.SubFactory("tests.factories.UserFactory")
project = factory.SubFactory("tests.factories.ProjectFactory")
class ContentTypeFactory(Factory):
class Meta:
model = "contenttypes.ContentType"

View File

@ -9,6 +9,7 @@ from taiga.base.utils import json
from tests import factories as f
from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals
from taiga.projects.votes.services import add_vote
from taiga.projects.notifications.services import add_watcher
from taiga.projects.occ import OCCResourceMixin
from unittest import mock
@ -477,12 +478,10 @@ def test_issue_action_upvote(client, data):
results = helper_test_http_method(client, 'post', public_url, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url1, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [401, 403, 403, 200, 200]
assert results == [404, 404, 404, 200, 200]
def test_issue_action_downvote(client, data):
@ -500,18 +499,16 @@ def test_issue_action_downvote(client, data):
results = helper_test_http_method(client, 'post', public_url, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url1, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [401, 403, 403, 200, 200]
assert results == [404, 404, 404, 200, 200]
def test_issue_voters_list(client, data):
public_url = reverse('issue-voters-list', kwargs={"issue_id": data.public_issue.pk})
private_url1 = reverse('issue-voters-list', kwargs={"issue_id": data.private_issue1.pk})
private_url2 = reverse('issue-voters-list', kwargs={"issue_id": data.private_issue2.pk})
public_url = reverse('issue-voters-list', kwargs={"resource_id": data.public_issue.pk})
private_url1 = reverse('issue-voters-list', kwargs={"resource_id": data.private_issue1.pk})
private_url2 = reverse('issue-voters-list', kwargs={"resource_id": data.private_issue2.pk})
users = [
None,
@ -523,21 +520,22 @@ def test_issue_voters_list(client, data):
results = helper_test_http_method(client, 'get', public_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url1, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]
def test_issue_voters_retrieve(client, data):
add_vote(data.public_issue, data.project_owner)
public_url = reverse('issue-voters-detail', kwargs={"issue_id": data.public_issue.pk, "pk": data.project_owner.pk})
public_url = reverse('issue-voters-detail', kwargs={"resource_id": data.public_issue.pk,
"pk": data.project_owner.pk})
add_vote(data.private_issue1, data.project_owner)
private_url1 = reverse('issue-voters-detail', kwargs={"issue_id": data.private_issue1.pk, "pk": data.project_owner.pk})
private_url1 = reverse('issue-voters-detail', kwargs={"resource_id": data.private_issue1.pk,
"pk": data.project_owner.pk})
add_vote(data.private_issue2, data.project_owner)
private_url2 = reverse('issue-voters-detail', kwargs={"issue_id": data.private_issue2.pk, "pk": data.project_owner.pk})
private_url2 = reverse('issue-voters-detail', kwargs={"resource_id": data.private_issue2.pk,
"pk": data.project_owner.pk})
users = [
None,
@ -549,10 +547,8 @@ def test_issue_voters_retrieve(client, data):
results = helper_test_http_method(client, 'get', public_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url1, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]
@ -579,3 +575,93 @@ def test_issues_csv(client, data):
results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users)
assert results == [200, 200, 200, 200, 200]
def test_issue_action_watch(client, data):
public_url = reverse('issues-watch', kwargs={"pk": data.public_issue.pk})
private_url1 = reverse('issues-watch', kwargs={"pk": data.private_issue1.pk})
private_url2 = reverse('issues-watch', kwargs={"pk": data.private_issue2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'post', public_url, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url1, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
def test_issue_action_unwatch(client, data):
public_url = reverse('issues-unwatch', kwargs={"pk": data.public_issue.pk})
private_url1 = reverse('issues-unwatch', kwargs={"pk": data.private_issue1.pk})
private_url2 = reverse('issues-unwatch', kwargs={"pk": data.private_issue2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'post', public_url, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url1, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
def test_issue_watchers_list(client, data):
public_url = reverse('issue-watchers-list', kwargs={"resource_id": data.public_issue.pk})
private_url1 = reverse('issue-watchers-list', kwargs={"resource_id": data.private_issue1.pk})
private_url2 = reverse('issue-watchers-list', kwargs={"resource_id": data.private_issue2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'get', public_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url1, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]
def test_issue_watchers_retrieve(client, data):
add_watcher(data.public_issue, data.project_owner)
public_url = reverse('issue-watchers-detail', kwargs={"resource_id": data.public_issue.pk,
"pk": data.project_owner.pk})
add_watcher(data.private_issue1, data.project_owner)
private_url1 = reverse('issue-watchers-detail', kwargs={"resource_id": data.private_issue1.pk,
"pk": data.project_owner.pk})
add_watcher(data.private_issue2, data.project_owner)
private_url2 = reverse('issue-watchers-detail', kwargs={"resource_id": data.private_issue2.pk,
"pk": data.project_owner.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'get', public_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url1, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]

View File

@ -3,6 +3,7 @@ from django.core.urlresolvers import reverse
from taiga.base.utils import json
from taiga.projects.milestones.serializers import MilestoneSerializer
from taiga.projects.milestones.models import Milestone
from taiga.projects.notifications.services import add_watcher
from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
from tests import factories as f
@ -274,3 +275,93 @@ def test_milestone_action_stats(client, data):
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]
def test_milestone_action_watch(client, data):
public_url = reverse('milestones-watch', kwargs={"pk": data.public_milestone.pk})
private_url1 = reverse('milestones-watch', kwargs={"pk": data.private_milestone1.pk})
private_url2 = reverse('milestones-watch', kwargs={"pk": data.private_milestone2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'post', public_url, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url1, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
def test_milestone_action_unwatch(client, data):
public_url = reverse('milestones-unwatch', kwargs={"pk": data.public_milestone.pk})
private_url1 = reverse('milestones-unwatch', kwargs={"pk": data.private_milestone1.pk})
private_url2 = reverse('milestones-unwatch', kwargs={"pk": data.private_milestone2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'post', public_url, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url1, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
def test_milestone_watchers_list(client, data):
public_url = reverse('milestone-watchers-list', kwargs={"resource_id": data.public_milestone.pk})
private_url1 = reverse('milestone-watchers-list', kwargs={"resource_id": data.private_milestone1.pk})
private_url2 = reverse('milestone-watchers-list', kwargs={"resource_id": data.private_milestone2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'get', public_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url1, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]
def test_milestone_watchers_retrieve(client, data):
add_watcher(data.public_milestone, data.project_owner)
public_url = reverse('milestone-watchers-detail', kwargs={"resource_id": data.public_milestone.pk,
"pk": data.project_owner.pk})
add_watcher(data.private_milestone1, data.project_owner)
private_url1 = reverse('milestone-watchers-detail', kwargs={"resource_id": data.private_milestone1.pk,
"pk": data.project_owner.pk})
add_watcher(data.private_milestone2, data.project_owner)
private_url2 = reverse('milestone-watchers-detail', kwargs={"resource_id": data.private_milestone2.pk,
"pk": data.project_owner.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'get', public_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url1, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]

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_project2.pk, count=2)
f.WatchedFactory(content_type=project_ct, object_id=m.public_project.pk, user=m.project_member_with_perms)
f.WatchedFactory(content_type=project_ct, object_id=m.public_project.pk, user=m.project_owner)
f.WatchedFactory(content_type=project_ct, object_id=m.private_project1.pk, user=m.project_member_with_perms)
f.WatchedFactory(content_type=project_ct, object_id=m.private_project1.pk, user=m.project_owner)
f.WatchedFactory(content_type=project_ct, object_id=m.private_project2.pk, user=m.project_member_with_perms)
f.WatchedFactory(content_type=project_ct, object_id=m.private_project2.pk, user=m.project_owner)
return m
@ -109,6 +116,7 @@ def test_project_update(client, data):
project_data = ProjectDetailSerializer(data.private_project2).data
project_data["is_private"] = False
project_data = json.dumps(project_data)
users = [
@ -198,6 +206,25 @@ def test_project_action_stats(client, data):
assert results == [404, 404, 200, 200]
def test_project_action_issues_stats(client, data):
public_url = reverse('projects-issues-stats', kwargs={"pk": data.public_project.pk})
private1_url = reverse('projects-issues-stats', kwargs={"pk": data.private_project1.pk})
private2_url = reverse('projects-issues-stats', kwargs={"pk": data.private_project2.pk})
users = [
None,
data.registered_user,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'get', public_url, None, users)
assert results == [200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private1_url, None, users)
assert results == [200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private2_url, None, users)
assert results == [404, 404, 200, 200]
def test_project_action_star(client, data):
public_url = reverse('projects-star', kwargs={"pk": data.public_project.pk})
private1_url = reverse('projects-star', kwargs={"pk": data.private_project1.pk})
@ -236,29 +263,10 @@ def test_project_action_unstar(client, data):
assert results == [404, 404, 200, 200]
def test_project_action_issues_stats(client, data):
public_url = reverse('projects-issues-stats', kwargs={"pk": data.public_project.pk})
private1_url = reverse('projects-issues-stats', kwargs={"pk": data.private_project1.pk})
private2_url = reverse('projects-issues-stats', kwargs={"pk": data.private_project2.pk})
users = [
None,
data.registered_user,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'get', public_url, None, users)
assert results == [200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private1_url, None, users)
assert results == [200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private2_url, None, users)
assert results == [404, 404, 200, 200]
def test_project_action_fans(client, data):
public_url = reverse('projects-fans', kwargs={"pk": data.public_project.pk})
private1_url = reverse('projects-fans', kwargs={"pk": data.private_project1.pk})
private2_url = reverse('projects-fans', kwargs={"pk": data.private_project2.pk})
def test_project_fans_list(client, data):
public_url = reverse('project-fans-list', kwargs={"resource_id": data.public_project.pk})
private1_url = reverse('project-fans-list', kwargs={"resource_id": data.private_project1.pk})
private2_url = reverse('project-fans-list', kwargs={"resource_id": data.private_project2.pk})
users = [
None,
@ -273,13 +281,16 @@ def test_project_action_fans(client, data):
results = helper_test_http_method_and_count(client, 'get', private1_url, None, users)
assert results == [(200, 2), (200, 2), (200, 2), (200, 2), (200, 2)]
results = helper_test_http_method_and_count(client, 'get', private2_url, None, users)
assert results == [(404, 1), (404, 1), (404, 1), (200, 2), (200, 2)]
assert results == [(401, 0), (403, 0), (403, 0), (200, 2), (200, 2)]
def test_user_action_starred(client, data):
url1 = reverse('users-starred', kwargs={"pk": data.project_member_without_perms.pk})
url2 = reverse('users-starred', kwargs={"pk": data.project_member_with_perms.pk})
url3 = reverse('users-starred', kwargs={"pk": data.project_owner.pk})
def test_project_fans_retrieve(client, data):
public_url = reverse('project-fans-detail', kwargs={"resource_id": data.public_project.pk,
"pk": data.project_owner.pk})
private1_url = reverse('project-fans-detail', kwargs={"resource_id": data.private_project1.pk,
"pk": data.project_owner.pk})
private2_url = reverse('project-fans-detail', kwargs={"resource_id": data.private_project2.pk,
"pk": data.project_owner.pk})
users = [
None,
@ -289,12 +300,57 @@ def test_user_action_starred(client, data):
data.project_owner
]
results = helper_test_http_method_and_count(client, 'get', url1, None, users)
assert results == [(200, 0), (200, 0), (200, 0), (200, 0), (200, 0)]
results = helper_test_http_method_and_count(client, 'get', url2, None, users)
assert results == [(200, 3), (200, 3), (200, 3), (200, 3), (200, 3)]
results = helper_test_http_method_and_count(client, 'get', url3, None, users)
assert results == [(200, 3), (200, 3), (200, 3), (200, 3), (200, 3)]
results = helper_test_http_method(client, 'get', public_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private1_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private2_url, None, users)
assert results == [401, 403, 403, 200, 200]
def test_project_watchers_list(client, data):
public_url = reverse('project-watchers-list', kwargs={"resource_id": data.public_project.pk})
private1_url = reverse('project-watchers-list', kwargs={"resource_id": data.private_project1.pk})
private2_url = reverse('project-watchers-list', kwargs={"resource_id": data.private_project2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method_and_count(client, 'get', public_url, None, users)
assert results == [(200, 2), (200, 2), (200, 2), (200, 2), (200, 2)]
results = helper_test_http_method_and_count(client, 'get', private1_url, None, users)
assert results == [(200, 2), (200, 2), (200, 2), (200, 2), (200, 2)]
results = helper_test_http_method_and_count(client, 'get', private2_url, None, users)
assert results == [(401, 0), (403, 0), (403, 0), (200, 2), (200, 2)]
def test_project_watchers_retrieve(client, data):
public_url = reverse('project-watchers-detail', kwargs={"resource_id": data.public_project.pk,
"pk": data.project_owner.pk})
private1_url = reverse('project-watchers-detail', kwargs={"resource_id": data.private_project1.pk,
"pk": data.project_owner.pk})
private2_url = reverse('project-watchers-detail', kwargs={"resource_id": data.private_project2.pk,
"pk": data.project_owner.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'get', public_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private1_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private2_url, None, users)
assert results == [401, 403, 403, 200, 200]
def test_project_action_create_template(client, data):
@ -413,3 +469,41 @@ def test_regenerate_issues_csv_uuid(client, data):
results = helper_test_http_method(client, 'post', private2_url, None, users)
assert results == [404, 404, 403, 200]
def test_project_action_watch(client, data):
public_url = reverse('projects-watch', kwargs={"pk": data.public_project.pk})
private1_url = reverse('projects-watch', kwargs={"pk": data.private_project1.pk})
private2_url = reverse('projects-watch', kwargs={"pk": data.private_project2.pk})
users = [
None,
data.registered_user,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'post', public_url, None, users)
assert results == [401, 200, 200, 200]
results = helper_test_http_method(client, 'post', private1_url, None, users)
assert results == [401, 200, 200, 200]
results = helper_test_http_method(client, 'post', private2_url, None, users)
assert results == [404, 404, 200, 200]
def test_project_action_unwatch(client, data):
public_url = reverse('projects-unwatch', kwargs={"pk": data.public_project.pk})
private1_url = reverse('projects-unwatch', kwargs={"pk": data.private_project1.pk})
private2_url = reverse('projects-unwatch', kwargs={"pk": data.private_project2.pk})
users = [
None,
data.registered_user,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'post', public_url, None, users)
assert results == [401, 200, 200, 200]
results = helper_test_http_method(client, 'post', private1_url, None, users)
assert results == [401, 200, 200, 200]
results = helper_test_http_method(client, 'post', private2_url, None, users)
assert results == [404, 404, 200, 200]

View File

@ -9,6 +9,8 @@ from taiga.projects.occ import OCCResourceMixin
from tests import factories as f
from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals
from taiga.projects.votes.services import add_vote
from taiga.projects.notifications.services import add_watcher
from unittest import mock
@ -416,6 +418,96 @@ def test_task_action_bulk_create(client, data):
assert results == [401, 403, 403, 200, 200]
def test_task_action_upvote(client, data):
public_url = reverse('tasks-upvote', kwargs={"pk": data.public_task.pk})
private_url1 = reverse('tasks-upvote', kwargs={"pk": data.private_task1.pk})
private_url2 = reverse('tasks-upvote', kwargs={"pk": data.private_task2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'post', public_url, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url1, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
def test_task_action_downvote(client, data):
public_url = reverse('tasks-downvote', kwargs={"pk": data.public_task.pk})
private_url1 = reverse('tasks-downvote', kwargs={"pk": data.private_task1.pk})
private_url2 = reverse('tasks-downvote', kwargs={"pk": data.private_task2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'post', public_url, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url1, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
def test_task_voters_list(client, data):
public_url = reverse('task-voters-list', kwargs={"resource_id": data.public_task.pk})
private_url1 = reverse('task-voters-list', kwargs={"resource_id": data.private_task1.pk})
private_url2 = reverse('task-voters-list', kwargs={"resource_id": data.private_task2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'get', public_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url1, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]
def test_task_voters_retrieve(client, data):
add_vote(data.public_task, data.project_owner)
public_url = reverse('task-voters-detail', kwargs={"resource_id": data.public_task.pk,
"pk": data.project_owner.pk})
add_vote(data.private_task1, data.project_owner)
private_url1 = reverse('task-voters-detail', kwargs={"resource_id": data.private_task1.pk,
"pk": data.project_owner.pk})
add_vote(data.private_task2, data.project_owner)
private_url2 = reverse('task-voters-detail', kwargs={"resource_id": data.private_task2.pk,
"pk": data.project_owner.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'get', public_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url1, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]
def test_tasks_csv(client, data):
url = reverse('tasks-csv')
csv_public_uuid = data.public_project.tasks_csv_uuid
@ -438,3 +530,93 @@ def test_tasks_csv(client, data):
results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users)
assert results == [200, 200, 200, 200, 200]
def test_task_action_watch(client, data):
public_url = reverse('tasks-watch', kwargs={"pk": data.public_task.pk})
private_url1 = reverse('tasks-watch', kwargs={"pk": data.private_task1.pk})
private_url2 = reverse('tasks-watch', kwargs={"pk": data.private_task2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'post', public_url, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url1, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
def test_task_action_unwatch(client, data):
public_url = reverse('tasks-unwatch', kwargs={"pk": data.public_task.pk})
private_url1 = reverse('tasks-unwatch', kwargs={"pk": data.private_task1.pk})
private_url2 = reverse('tasks-unwatch', kwargs={"pk": data.private_task2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'post', public_url, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url1, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
def test_task_watchers_list(client, data):
public_url = reverse('task-watchers-list', kwargs={"resource_id": data.public_task.pk})
private_url1 = reverse('task-watchers-list', kwargs={"resource_id": data.private_task1.pk})
private_url2 = reverse('task-watchers-list', kwargs={"resource_id": data.private_task2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'get', public_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url1, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]
def test_task_watchers_retrieve(client, data):
add_watcher(data.public_task, data.project_owner)
public_url = reverse('task-watchers-detail', kwargs={"resource_id": data.public_task.pk,
"pk": data.project_owner.pk})
add_watcher(data.private_task1, data.project_owner)
private_url1 = reverse('task-watchers-detail', kwargs={"resource_id": data.private_task1.pk,
"pk": data.project_owner.pk})
add_watcher(data.private_task2, data.project_owner)
private_url2 = reverse('task-watchers-detail', kwargs={"resource_id": data.private_task2.pk,
"pk": data.project_owner.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'get', public_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url1, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]

View File

@ -287,3 +287,15 @@ def test_user_action_change_email(client, data):
after_each_request()
results = helper_test_http_method(client, 'post', url, patch_data, users, after_each_request=after_each_request)
assert results == [204, 204, 204]
def test_user_list_votes(client, data):
url = reverse('users-favourites', kwargs={"pk": data.registered_user.pk})
users = [
None,
data.registered_user,
data.other_user,
data.superuser,
]
results = helper_test_http_method(client, 'get', url, None, users)
assert results == [200, 200, 200, 200]

View File

@ -9,6 +9,8 @@ from taiga.projects.occ import OCCResourceMixin
from tests import factories as f
from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals
from taiga.projects.votes.services import add_vote
from taiga.projects.notifications.services import add_watcher
from unittest import mock
@ -415,6 +417,95 @@ def test_user_story_action_bulk_update_order(client, data):
results = helper_test_http_method(client, 'post', url, post_data, users)
assert results == [401, 403, 403, 204, 204]
def test_user_story_action_upvote(client, data):
public_url = reverse('userstories-upvote', kwargs={"pk": data.public_user_story.pk})
private_url1 = reverse('userstories-upvote', kwargs={"pk": data.private_user_story1.pk})
private_url2 = reverse('userstories-upvote', kwargs={"pk": data.private_user_story2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'post', public_url, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url1, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
def test_user_story_action_downvote(client, data):
public_url = reverse('userstories-downvote', kwargs={"pk": data.public_user_story.pk})
private_url1 = reverse('userstories-downvote', kwargs={"pk": data.private_user_story1.pk})
private_url2 = reverse('userstories-downvote', kwargs={"pk": data.private_user_story2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'post', public_url, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url1, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
def test_user_story_voters_list(client, data):
public_url = reverse('userstory-voters-list', kwargs={"resource_id": data.public_user_story.pk})
private_url1 = reverse('userstory-voters-list', kwargs={"resource_id": data.private_user_story1.pk})
private_url2 = reverse('userstory-voters-list', kwargs={"resource_id": data.private_user_story2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'get', public_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url1, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]
def test_user_story_voters_retrieve(client, data):
add_vote(data.public_user_story, data.project_owner)
public_url = reverse('userstory-voters-detail', kwargs={"resource_id": data.public_user_story.pk,
"pk": data.project_owner.pk})
add_vote(data.private_user_story1, data.project_owner)
private_url1 = reverse('userstory-voters-detail', kwargs={"resource_id": data.private_user_story1.pk,
"pk": data.project_owner.pk})
add_vote(data.private_user_story2, data.project_owner)
private_url2 = reverse('userstory-voters-detail', kwargs={"resource_id": data.private_user_story2.pk,
"pk": data.project_owner.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'get', public_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url1, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]
def test_user_stories_csv(client, data):
url = reverse('userstories-csv')
@ -438,3 +529,93 @@ def test_user_stories_csv(client, data):
results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users)
assert results == [200, 200, 200, 200, 200]
def test_user_story_action_watch(client, data):
public_url = reverse('userstories-watch', kwargs={"pk": data.public_user_story.pk})
private_url1 = reverse('userstories-watch', kwargs={"pk": data.private_user_story1.pk})
private_url2 = reverse('userstories-watch', kwargs={"pk": data.private_user_story2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'post', public_url, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url1, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
def test_user_story_action_unwatch(client, data):
public_url = reverse('userstories-unwatch', kwargs={"pk": data.public_user_story.pk})
private_url1 = reverse('userstories-unwatch', kwargs={"pk": data.private_user_story1.pk})
private_url2 = reverse('userstories-unwatch', kwargs={"pk": data.private_user_story2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'post', public_url, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url1, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
def test_userstory_watchers_list(client, data):
public_url = reverse('userstory-watchers-list', kwargs={"resource_id": data.public_user_story.pk})
private_url1 = reverse('userstory-watchers-list', kwargs={"resource_id": data.private_user_story1.pk})
private_url2 = reverse('userstory-watchers-list', kwargs={"resource_id": data.private_user_story2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'get', public_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url1, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]
def test_userstory_watchers_retrieve(client, data):
add_watcher(data.public_user_story, data.project_owner)
public_url = reverse('userstory-watchers-detail', kwargs={"resource_id": data.public_user_story.pk,
"pk": data.project_owner.pk})
add_watcher(data.private_user_story1, data.project_owner)
private_url1 = reverse('userstory-watchers-detail', kwargs={"resource_id": data.private_user_story1.pk,
"pk": data.project_owner.pk})
add_watcher(data.private_user_story2, data.project_owner)
private_url2 = reverse('userstory-watchers-detail', kwargs={"resource_id": data.private_user_story2.pk,
"pk": data.project_owner.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'get', public_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url1, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]

View File

@ -1,10 +1,11 @@
from django.core.urlresolvers import reverse
from taiga.base.utils import json
from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
from taiga.projects.notifications.services import add_watcher
from taiga.projects.occ import OCCResourceMixin
from taiga.projects.wiki.serializers import WikiPageSerializer, WikiLinkSerializer
from taiga.projects.wiki.models import WikiPage, WikiLink
from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
from taiga.projects.occ import OCCResourceMixin
from tests import factories as f
from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals
@ -436,3 +437,93 @@ def test_wiki_link_patch(client, data):
patch_data = json.dumps({"title": "test"})
results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
assert results == [401, 403, 403, 200, 200]
def test_wikipage_action_watch(client, data):
public_url = reverse('wiki-watch', kwargs={"pk": data.public_wiki_page.pk})
private_url1 = reverse('wiki-watch', kwargs={"pk": data.private_wiki_page1.pk})
private_url2 = reverse('wiki-watch', kwargs={"pk": data.private_wiki_page2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'post', public_url, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url1, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
def test_wikipage_action_unwatch(client, data):
public_url = reverse('wiki-unwatch', kwargs={"pk": data.public_wiki_page.pk})
private_url1 = reverse('wiki-unwatch', kwargs={"pk": data.private_wiki_page1.pk})
private_url2 = reverse('wiki-unwatch', kwargs={"pk": data.private_wiki_page2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'post', public_url, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url1, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
def test_wikipage_watchers_list(client, data):
public_url = reverse('wiki-watchers-list', kwargs={"resource_id": data.public_wiki_page.pk})
private_url1 = reverse('wiki-watchers-list', kwargs={"resource_id": data.private_wiki_page1.pk})
private_url2 = reverse('wiki-watchers-list', kwargs={"resource_id": data.private_wiki_page2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'get', public_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url1, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]
def test_wikipage_watchers_retrieve(client, data):
add_watcher(data.public_wiki_page, data.project_owner)
public_url = reverse('wiki-watchers-detail', kwargs={"resource_id": data.public_wiki_page.pk,
"pk": data.project_owner.pk})
add_watcher(data.private_wiki_page1, data.project_owner)
private_url1 = reverse('wiki-watchers-detail', kwargs={"resource_id": data.private_wiki_page1.pk,
"pk": data.project_owner.pk})
add_watcher(data.private_wiki_page2, data.project_owner)
private_url2 = reverse('wiki-watchers-detail', kwargs={"resource_id": data.private_wiki_page2.pk,
"pk": data.project_owner.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'get', public_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url1, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]

View File

@ -47,13 +47,15 @@ def test_invalid_project_import(client):
def test_valid_project_import_without_extra_data(client):
user = f.UserFactory.create()
user_watching = f.UserFactory.create(email="testing@taiga.io")
client.login(user)
url = reverse("importer-list")
data = {
"name": "Imported project",
"description": "Imported project",
"roles": [{"name": "Role"}]
"roles": [{"name": "Role"}],
"watchers": ["testing@taiga.io"]
}
response = client.post(url, json.dumps(data), content_type="application/json")
@ -66,6 +68,7 @@ def test_valid_project_import_without_extra_data(client):
]
assert all(map(lambda x: len(response_data[x]) == 0, must_empty_children))
assert response_data["owner"] == user.email
assert response_data["watchers"] == [user_watching.email]
def test_valid_project_import_with_not_existing_memberships(client):
@ -383,6 +386,7 @@ def test_valid_issue_import_with_custom_attributes_values(client):
def test_valid_issue_import_with_extra_data(client):
user = f.UserFactory.create()
user_watching = f.UserFactory.create(email="testing@taiga.io")
project = f.ProjectFactory.create(owner=user)
f.MembershipFactory(project=project, user=user, is_owner=True)
project.default_issue_type = f.IssueTypeFactory.create(project=project)
@ -403,7 +407,8 @@ def test_valid_issue_import_with_extra_data(client):
"name": "imported attachment",
"data": base64.b64encode(b"TEST").decode("utf-8")
}
}]
}],
"watchers": ["testing@taiga.io"]
}
response = client.post(url, json.dumps(data), content_type="application/json")
@ -413,6 +418,7 @@ def test_valid_issue_import_with_extra_data(client):
assert response_data["owner"] == user.email
assert response_data["ref"] is not None
assert response_data["finished_date"] == "2014-10-24T00:00:00+0000"
assert response_data["watchers"] == [user_watching.email]
def test_invalid_issue_import_with_extra_data(client):
@ -535,6 +541,7 @@ def test_valid_us_import_without_extra_data(client):
def test_valid_us_import_with_extra_data(client):
user = f.UserFactory.create()
user_watching = f.UserFactory.create(email="testing@taiga.io")
project = f.ProjectFactory.create(owner=user)
f.MembershipFactory(project=project, user=user, is_owner=True)
project.default_us_status = f.UserStoryStatusFactory.create(project=project)
@ -551,7 +558,8 @@ def test_valid_us_import_with_extra_data(client):
"name": "imported attachment",
"data": base64.b64encode(b"TEST").decode("utf-8")
}
}]
}],
"watchers": ["testing@taiga.io"]
}
response = client.post(url, json.dumps(data), content_type="application/json")
@ -560,6 +568,7 @@ def test_valid_us_import_with_extra_data(client):
assert len(response_data["attachments"]) == 1
assert response_data["owner"] == user.email
assert response_data["ref"] is not None
assert response_data["watchers"] == [user_watching.email]
def test_invalid_us_import_with_extra_data(client):
@ -664,6 +673,7 @@ def test_valid_task_import_with_custom_attributes_values(client):
def test_valid_task_import_with_extra_data(client):
user = f.UserFactory.create()
user_watching = f.UserFactory.create(email="testing@taiga.io")
project = f.ProjectFactory.create(owner=user)
f.MembershipFactory(project=project, user=user, is_owner=True)
project.default_task_status = f.TaskStatusFactory.create(project=project)
@ -680,7 +690,8 @@ def test_valid_task_import_with_extra_data(client):
"name": "imported attachment",
"data": base64.b64encode(b"TEST").decode("utf-8")
}
}]
}],
"watchers": ["testing@taiga.io"]
}
response = client.post(url, json.dumps(data), content_type="application/json")
@ -689,6 +700,7 @@ def test_valid_task_import_with_extra_data(client):
assert len(response_data["attachments"]) == 1
assert response_data["owner"] == user.email
assert response_data["ref"] is not None
assert response_data["watchers"] == [user_watching.email]
def test_invalid_task_import_with_extra_data(client):
@ -787,6 +799,7 @@ def test_valid_wiki_page_import_without_extra_data(client):
def test_valid_wiki_page_import_with_extra_data(client):
user = f.UserFactory.create()
user_watching = f.UserFactory.create(email="testing@taiga.io")
project = f.ProjectFactory.create(owner=user)
f.MembershipFactory(project=project, user=user, is_owner=True)
client.login(user)
@ -801,7 +814,8 @@ def test_valid_wiki_page_import_with_extra_data(client):
"name": "imported attachment",
"data": base64.b64encode(b"TEST").decode("utf-8")
}
}]
}],
"watchers": ["testing@taiga.io"]
}
response = client.post(url, json.dumps(data), content_type="application/json")
@ -809,6 +823,7 @@ def test_valid_wiki_page_import_with_extra_data(client):
response_data = response.data
assert len(response_data["attachments"]) == 1
assert response_data["owner"] == user.email
assert response_data["watchers"] == [user_watching.email]
def test_invalid_wiki_page_import_with_extra_data(client):
@ -877,6 +892,7 @@ def test_invalid_milestone_import(client):
def test_valid_milestone_import(client):
user = f.UserFactory.create()
user_watching = f.UserFactory.create(email="testing@taiga.io")
project = f.ProjectFactory.create(owner=user)
f.MembershipFactory(project=project, user=user, is_owner=True)
client.login(user)
@ -886,11 +902,12 @@ def test_valid_milestone_import(client):
"name": "Imported milestone",
"estimated_start": "2014-10-10",
"estimated_finish": "2014-10-20",
"watchers": ["testing@taiga.io"]
}
response = client.post(url, json.dumps(data), content_type="application/json")
assert response.status_code == 201
response.data
assert response.data["watchers"] == [user_watching.email]

View File

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

View File

@ -97,7 +97,7 @@ def test_analize_object_for_watchers():
history.comment = ""
services.analize_object_for_watchers(issue, history)
assert issue.watchers.add.call_count == 2
assert issue.add_watcher.call_count == 2
def test_analize_object_for_watchers_adding_owner_non_empty_comment():
@ -112,7 +112,7 @@ def test_analize_object_for_watchers_adding_owner_non_empty_comment():
history.owner = user1
services.analize_object_for_watchers(issue, history)
assert issue.watchers.add.call_count == 1
assert issue.add_watcher.call_count == 1
def test_analize_object_for_watchers_no_adding_owner_empty_comment():
@ -127,7 +127,7 @@ def test_analize_object_for_watchers_no_adding_owner_empty_comment():
history.owner = user1
services.analize_object_for_watchers(issue, history)
assert issue.watchers.add.call_count == 0
assert issue.add_watcher.call_count == 0
def test_users_to_notify():
@ -180,7 +180,7 @@ def test_users_to_notify():
assert users == {member1.user, issue.get_owner()}
# Test with watchers
issue.watchers.add(member3.user)
issue.add_watcher(member3.user)
users = services.get_users_to_notify(issue)
assert len(users) == 3
assert users == {member1.user, member3.user, issue.get_owner()}
@ -189,28 +189,146 @@ def test_users_to_notify():
policy2.notify_level = NotifyLevel.ignore
policy2.save()
issue.watchers.add(member3.user)
issue.add_watcher(member3.user)
users = services.get_users_to_notify(issue)
assert len(users) == 2
assert users == {member1.user, issue.get_owner()}
# Test with watchers without permissions
issue.watchers.add(member5.user)
issue.add_watcher(member5.user)
users = services.get_users_to_notify(issue)
assert len(users) == 2
assert users == {member1.user, issue.get_owner()}
# Test with inactive user
issue.watchers.add(inactive_member1.user)
issue.add_watcher(inactive_member1.user)
assert len(users) == 2
assert users == {member1.user, issue.get_owner()}
# Test with system user
issue.watchers.add(system_member1.user)
issue.add_watcher(system_member1.user)
assert len(users) == 2
assert users == {member1.user, issue.get_owner()}
def test_watching_users_to_notify_on_issue_modification_1():
# If:
# - the user is watching the issue
# - the user is not watching the project
# - the notify policy is watch
# Then:
# - email is sent
project = f.ProjectFactory.create(anon_permissions= ["view_issues"])
issue = f.IssueFactory.create(project=project)
watching_user = f.UserFactory()
issue.add_watcher(watching_user)
watching_user_policy = services.get_notify_policy(project, watching_user)
issue.description = "test1"
issue.save()
watching_user_policy.notify_level = NotifyLevel.watch
users = services.get_users_to_notify(issue)
assert users == {watching_user, issue.owner}
def test_watching_users_to_notify_on_issue_modification_2():
# If:
# - the user is watching the issue
# - the user is not watching the project
# - the notify policy is notwatch
# Then:
# - email is sent
project = f.ProjectFactory.create(anon_permissions= ["view_issues"])
issue = f.IssueFactory.create(project=project)
watching_user = f.UserFactory()
issue.add_watcher(watching_user)
watching_user_policy = services.get_notify_policy(project, watching_user)
issue.description = "test1"
issue.save()
watching_user_policy.notify_level = NotifyLevel.notwatch
users = services.get_users_to_notify(issue)
assert users == {watching_user, issue.owner}
def test_watching_users_to_notify_on_issue_modification_3():
# If:
# - the user is watching the issue
# - the user is not watching the project
# - the notify policy is ignore
# Then:
# - email is not sent
project = f.ProjectFactory.create(anon_permissions= ["view_issues"])
issue = f.IssueFactory.create(project=project)
watching_user = f.UserFactory()
issue.add_watcher(watching_user)
watching_user_policy = services.get_notify_policy(project, watching_user)
issue.description = "test1"
issue.save()
watching_user_policy.notify_level = NotifyLevel.ignore
watching_user_policy.save()
users = services.get_users_to_notify(issue)
assert users == {issue.owner}
def test_watching_users_to_notify_on_issue_modification_4():
# If:
# - the user is not watching the issue
# - the user is watching the project
# - the notify policy is ignore
# Then:
# - email is not sent
project = f.ProjectFactory.create(anon_permissions= ["view_issues"])
issue = f.IssueFactory.create(project=project)
watching_user = f.UserFactory()
project.add_watcher(watching_user)
watching_user_policy = services.get_notify_policy(project, watching_user)
issue.description = "test1"
issue.save()
watching_user_policy.notify_level = NotifyLevel.ignore
watching_user_policy.save()
users = services.get_users_to_notify(issue)
assert users == {issue.owner}
def test_watching_users_to_notify_on_issue_modification_5():
# If:
# - the user is not watching the issue
# - the user is watching the project
# - the notify policy is watch
# Then:
# - email is sent
project = f.ProjectFactory.create(anon_permissions= ["view_issues"])
issue = f.IssueFactory.create(project=project)
watching_user = f.UserFactory()
project.add_watcher(watching_user)
watching_user_policy = services.get_notify_policy(project, watching_user)
issue.description = "test1"
issue.save()
watching_user_policy.notify_level = NotifyLevel.watch
watching_user_policy.save()
users = services.get_users_to_notify(issue)
assert users == {watching_user, issue.owner}
def test_watching_users_to_notify_on_issue_modification_6():
# If:
# - the user is not watching the issue
# - the user is watching the project
# - the notify policy is notwatch
# Then:
# - email is sent
project = f.ProjectFactory.create(anon_permissions= ["view_issues"])
issue = f.IssueFactory.create(project=project)
watching_user = f.UserFactory()
project.add_watcher(watching_user)
watching_user_policy = services.get_notify_policy(project, watching_user)
issue.description = "test1"
issue.save()
watching_user_policy.notify_level = NotifyLevel.notwatch
watching_user_policy.save()
users = services.get_users_to_notify(issue)
assert users == {watching_user, issue.owner}
def test_send_notifications_using_services_method(settings, mail):
settings.CHANGE_NOTIFICATIONS_MIN_INTERVAL = 1
@ -344,7 +462,7 @@ def test_watchers_assignation_for_issue(client):
issue = f.create_issue(project=project1, owner=user1)
data = {"version": issue.version,
"watchers": [user1.pk]}
"watchersa": [user1.pk]}
url = reverse("issues-detail", args=[issue.pk])
response = client.json.patch(url, json.dumps(data))

View File

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

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)
reader = csv.reader(data)
row = next(reader)
assert row[17] == attr.name
assert row[19] == attr.name
row = next(reader)
assert row[17] == "val1"
assert row[19] == "val1"

View File

@ -196,8 +196,10 @@ def test_create_membership_timeline():
def test_update_project_timeline():
user_watcher= factories.UserFactory()
project = factories.ProjectFactory.create(name="test project timeline")
history_services.take_snapshot(project, user=project.owner)
project.add_watcher(user_watcher)
project.name = "test project timeline updated"
project.save()
history_services.take_snapshot(project, user=project.owner)
@ -206,11 +208,18 @@ def test_update_project_timeline():
assert project_timeline[0].data["project"]["name"] == "test project timeline updated"
assert project_timeline[0].data["values_diff"]["name"][0] == "test project timeline"
assert project_timeline[0].data["values_diff"]["name"][1] == "test project timeline updated"
user_watcher_timeline = service.get_profile_timeline(user_watcher)
assert user_watcher_timeline[0].event_type == "projects.project.change"
assert user_watcher_timeline[0].data["project"]["name"] == "test project timeline updated"
assert user_watcher_timeline[0].data["values_diff"]["name"][0] == "test project timeline"
assert user_watcher_timeline[0].data["values_diff"]["name"][1] == "test project timeline updated"
def test_update_milestone_timeline():
user_watcher= factories.UserFactory()
milestone = factories.MilestoneFactory.create(name="test milestone timeline")
history_services.take_snapshot(milestone, user=milestone.owner)
milestone.add_watcher(user_watcher)
milestone.name = "test milestone timeline updated"
milestone.save()
history_services.take_snapshot(milestone, user=milestone.owner)
@ -219,11 +228,18 @@ def test_update_milestone_timeline():
assert project_timeline[0].data["milestone"]["name"] == "test milestone timeline updated"
assert project_timeline[0].data["values_diff"]["name"][0] == "test milestone timeline"
assert project_timeline[0].data["values_diff"]["name"][1] == "test milestone timeline updated"
user_watcher_timeline = service.get_profile_timeline(user_watcher)
assert user_watcher_timeline[0].event_type == "milestones.milestone.change"
assert user_watcher_timeline[0].data["milestone"]["name"] == "test milestone timeline updated"
assert user_watcher_timeline[0].data["values_diff"]["name"][0] == "test milestone timeline"
assert user_watcher_timeline[0].data["values_diff"]["name"][1] == "test milestone timeline updated"
def test_update_user_story_timeline():
user_watcher= factories.UserFactory()
user_story = factories.UserStoryFactory.create(subject="test us timeline")
history_services.take_snapshot(user_story, user=user_story.owner)
user_story.add_watcher(user_watcher)
user_story.subject = "test us timeline updated"
user_story.save()
history_services.take_snapshot(user_story, user=user_story.owner)
@ -232,11 +248,18 @@ def test_update_user_story_timeline():
assert project_timeline[0].data["userstory"]["subject"] == "test us timeline updated"
assert project_timeline[0].data["values_diff"]["subject"][0] == "test us timeline"
assert project_timeline[0].data["values_diff"]["subject"][1] == "test us timeline updated"
user_watcher_timeline = service.get_profile_timeline(user_watcher)
assert user_watcher_timeline[0].event_type == "userstories.userstory.change"
assert user_watcher_timeline[0].data["userstory"]["subject"] == "test us timeline updated"
assert user_watcher_timeline[0].data["values_diff"]["subject"][0] == "test us timeline"
assert user_watcher_timeline[0].data["values_diff"]["subject"][1] == "test us timeline updated"
def test_update_issue_timeline():
user_watcher= factories.UserFactory()
issue = factories.IssueFactory.create(subject="test issue timeline")
history_services.take_snapshot(issue, user=issue.owner)
issue.add_watcher(user_watcher)
issue.subject = "test issue timeline updated"
issue.save()
history_services.take_snapshot(issue, user=issue.owner)
@ -245,11 +268,18 @@ def test_update_issue_timeline():
assert project_timeline[0].data["issue"]["subject"] == "test issue timeline updated"
assert project_timeline[0].data["values_diff"]["subject"][0] == "test issue timeline"
assert project_timeline[0].data["values_diff"]["subject"][1] == "test issue timeline updated"
user_watcher_timeline = service.get_profile_timeline(user_watcher)
assert user_watcher_timeline[0].event_type == "issues.issue.change"
assert user_watcher_timeline[0].data["issue"]["subject"] == "test issue timeline updated"
assert user_watcher_timeline[0].data["values_diff"]["subject"][0] == "test issue timeline"
assert user_watcher_timeline[0].data["values_diff"]["subject"][1] == "test issue timeline updated"
def test_update_task_timeline():
user_watcher= factories.UserFactory()
task = factories.TaskFactory.create(subject="test task timeline")
history_services.take_snapshot(task, user=task.owner)
task.add_watcher(user_watcher)
task.subject = "test task timeline updated"
task.save()
history_services.take_snapshot(task, user=task.owner)
@ -258,11 +288,18 @@ def test_update_task_timeline():
assert project_timeline[0].data["task"]["subject"] == "test task timeline updated"
assert project_timeline[0].data["values_diff"]["subject"][0] == "test task timeline"
assert project_timeline[0].data["values_diff"]["subject"][1] == "test task timeline updated"
user_watcher_timeline = service.get_profile_timeline(user_watcher)
assert user_watcher_timeline[0].event_type == "tasks.task.change"
assert user_watcher_timeline[0].data["task"]["subject"] == "test task timeline updated"
assert user_watcher_timeline[0].data["values_diff"]["subject"][0] == "test task timeline"
assert user_watcher_timeline[0].data["values_diff"]["subject"][1] == "test task timeline updated"
def test_update_wiki_page_timeline():
user_watcher= factories.UserFactory()
page = factories.WikiPageFactory.create(slug="test wiki page timeline")
history_services.take_snapshot(page, user=page.owner)
page.add_watcher(user_watcher)
page.slug = "test wiki page timeline updated"
page.save()
history_services.take_snapshot(page, user=page.owner)
@ -271,6 +308,11 @@ def test_update_wiki_page_timeline():
assert project_timeline[0].data["wikipage"]["slug"] == "test wiki page timeline updated"
assert project_timeline[0].data["values_diff"]["slug"][0] == "test wiki page timeline"
assert project_timeline[0].data["values_diff"]["slug"][1] == "test wiki page timeline updated"
user_watcher_timeline = service.get_profile_timeline(user_watcher)
assert user_watcher_timeline[0].event_type == "wiki.wikipage.change"
assert user_watcher_timeline[0].data["wikipage"]["slug"] == "test wiki page timeline updated"
assert user_watcher_timeline[0].data["values_diff"]["slug"][0] == "test wiki page timeline"
assert user_watcher_timeline[0].data["values_diff"]["slug"][1] == "test wiki page timeline updated"
def test_update_membership_timeline():
@ -298,50 +340,80 @@ def test_update_membership_timeline():
def test_delete_project_timeline():
project = factories.ProjectFactory.create(name="test project timeline")
user_watcher= factories.UserFactory()
project.add_watcher(user_watcher)
history_services.take_snapshot(project, user=project.owner, delete=True)
user_timeline = service.get_project_timeline(project)
assert user_timeline[0].event_type == "projects.project.delete"
assert user_timeline[0].data["project"]["id"] == project.id
user_watcher_timeline = service.get_profile_timeline(user_watcher)
assert user_watcher_timeline[0].event_type == "projects.project.delete"
assert user_watcher_timeline[0].data["project"]["id"] == project.id
def test_delete_milestone_timeline():
milestone = factories.MilestoneFactory.create(name="test milestone timeline")
user_watcher= factories.UserFactory()
milestone.add_watcher(user_watcher)
history_services.take_snapshot(milestone, user=milestone.owner, delete=True)
project_timeline = service.get_project_timeline(milestone.project)
assert project_timeline[0].event_type == "milestones.milestone.delete"
assert project_timeline[0].data["milestone"]["name"] == "test milestone timeline"
user_watcher_timeline = service.get_profile_timeline(user_watcher)
assert user_watcher_timeline[0].event_type == "milestones.milestone.delete"
assert user_watcher_timeline[0].data["milestone"]["name"] == "test milestone timeline"
def test_delete_user_story_timeline():
user_story = factories.UserStoryFactory.create(subject="test us timeline")
user_watcher= factories.UserFactory()
user_story.add_watcher(user_watcher)
history_services.take_snapshot(user_story, user=user_story.owner, delete=True)
project_timeline = service.get_project_timeline(user_story.project)
assert project_timeline[0].event_type == "userstories.userstory.delete"
assert project_timeline[0].data["userstory"]["subject"] == "test us timeline"
user_watcher_timeline = service.get_profile_timeline(user_watcher)
assert user_watcher_timeline[0].event_type == "userstories.userstory.delete"
assert user_watcher_timeline[0].data["userstory"]["subject"] == "test us timeline"
def test_delete_issue_timeline():
issue = factories.IssueFactory.create(subject="test issue timeline")
user_watcher= factories.UserFactory()
issue.add_watcher(user_watcher)
history_services.take_snapshot(issue, user=issue.owner, delete=True)
project_timeline = service.get_project_timeline(issue.project)
assert project_timeline[0].event_type == "issues.issue.delete"
assert project_timeline[0].data["issue"]["subject"] == "test issue timeline"
user_watcher_timeline = service.get_profile_timeline(user_watcher)
assert user_watcher_timeline[0].event_type == "issues.issue.delete"
assert user_watcher_timeline[0].data["issue"]["subject"] == "test issue timeline"
def test_delete_task_timeline():
task = factories.TaskFactory.create(subject="test task timeline")
user_watcher= factories.UserFactory()
task.add_watcher(user_watcher)
history_services.take_snapshot(task, user=task.owner, delete=True)
project_timeline = service.get_project_timeline(task.project)
assert project_timeline[0].event_type == "tasks.task.delete"
assert project_timeline[0].data["task"]["subject"] == "test task timeline"
user_watcher_timeline = service.get_profile_timeline(user_watcher)
assert user_watcher_timeline[0].event_type == "tasks.task.delete"
assert user_watcher_timeline[0].data["task"]["subject"] == "test task timeline"
def test_delete_wiki_page_timeline():
page = factories.WikiPageFactory.create(slug="test wiki page timeline")
user_watcher= factories.UserFactory()
page.add_watcher(user_watcher)
history_services.take_snapshot(page, user=page.owner, delete=True)
project_timeline = service.get_project_timeline(page.project)
assert project_timeline[0].event_type == "wiki.wikipage.delete"
assert project_timeline[0].data["wikipage"]["slug"] == "test wiki page timeline"
user_watcher_timeline = service.get_profile_timeline(user_watcher)
assert user_watcher_timeline[0].event_type == "wiki.wikipage.delete"
assert user_watcher_timeline[0].data["wikipage"]["slug"] == "test wiki page timeline"
def test_delete_membership_timeline():
@ -384,16 +456,6 @@ def test_assigned_to_user_story_timeline():
assert user_timeline[0].data["userstory"]["subject"] == "test us timeline"
def test_watchers_to_user_story_timeline():
membership = factories.MembershipFactory.create()
user_story = factories.UserStoryFactory.create(subject="test us timeline", project=membership.project)
user_story.watchers.add(membership.user)
history_services.take_snapshot(user_story, user=user_story.owner)
user_timeline = service.get_profile_timeline(membership.user)
assert user_timeline[0].event_type == "userstories.userstory.create"
assert user_timeline[0].data["userstory"]["subject"] == "test us timeline"
def test_user_data_for_non_system_users():
user_story = factories.UserStoryFactory.create(subject="test us timeline")
history_services.take_snapshot(user_story, user=user_story.owner)

View File

@ -1,6 +1,7 @@
import pytest
from tempfile import NamedTemporaryFile
from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse
from .. import factories as f
@ -9,6 +10,7 @@ from taiga.base.utils import json
from taiga.users import models
from taiga.auth.tokens import get_token_for_user
from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
from taiga.users.services import get_favourites_list
pytestmark = pytest.mark.django_db
@ -249,3 +251,166 @@ def test_list_contacts_public_projects(client):
response_content = response.data
assert len(response_content) == 1
assert response_content[0]["id"] == user_2.id
def test_get_favourites_list():
fav_user = f.UserFactory()
viewer_user = f.UserFactory()
project = f.ProjectFactory(is_private=False, name="Testing project")
role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"])
membership = f.MembershipFactory(project=project, role=role, user=fav_user)
project.add_watcher(fav_user)
content_type = ContentType.objects.get_for_model(project)
f.VoteFactory(content_type=content_type, object_id=project.id, user=fav_user)
f.VotesFactory(content_type=content_type, object_id=project.id, count=1)
user_story = f.UserStoryFactory(project=project, subject="Testing user story")
user_story.add_watcher(fav_user)
content_type = ContentType.objects.get_for_model(user_story)
f.VoteFactory(content_type=content_type, object_id=user_story.id, user=fav_user)
f.VotesFactory(content_type=content_type, object_id=user_story.id, count=1)
task = f.TaskFactory(project=project, subject="Testing task")
task.add_watcher(fav_user)
content_type = ContentType.objects.get_for_model(task)
f.VoteFactory(content_type=content_type, object_id=task.id, user=fav_user)
f.VotesFactory(content_type=content_type, object_id=task.id, count=1)
issue = f.IssueFactory(project=project, subject="Testing issue")
issue.add_watcher(fav_user)
content_type = ContentType.objects.get_for_model(issue)
f.VoteFactory(content_type=content_type, object_id=issue.id, user=fav_user)
f.VotesFactory(content_type=content_type, object_id=issue.id, count=1)
assert len(get_favourites_list(fav_user, viewer_user)) == 8
assert len(get_favourites_list(fav_user, viewer_user, type="project")) == 2
assert len(get_favourites_list(fav_user, viewer_user, type="userstory")) == 2
assert len(get_favourites_list(fav_user, viewer_user, type="task")) == 2
assert len(get_favourites_list(fav_user, viewer_user, type="issue")) == 2
assert len(get_favourites_list(fav_user, viewer_user, type="unknown")) == 0
assert len(get_favourites_list(fav_user, viewer_user, action="watch")) == 4
assert len(get_favourites_list(fav_user, viewer_user, action="vote")) == 4
assert len(get_favourites_list(fav_user, viewer_user, q="issue")) == 2
assert len(get_favourites_list(fav_user, viewer_user, q="unexisting text")) == 0
def test_get_favourites_list_valid_info_for_project():
fav_user = f.UserFactory()
viewer_user = f.UserFactory()
watcher_user = f.UserFactory()
project = f.ProjectFactory(is_private=False, name="Testing project")
project.add_watcher(watcher_user)
content_type = ContentType.objects.get_for_model(project)
vote = f.VoteFactory(content_type=content_type, object_id=project.id, user=fav_user)
f.VotesFactory(content_type=content_type, object_id=project.id, count=1)
project_vote_info = get_favourites_list(fav_user, viewer_user)[0]
assert project_vote_info["type"] == "project"
assert project_vote_info["action"] == "vote"
assert project_vote_info["id"] == project.id
assert project_vote_info["ref"] == None
assert project_vote_info["slug"] == project.slug
assert project_vote_info["subject"] == project.name
assert project_vote_info["tags"] == project.tags
assert project_vote_info["project"] == project.id
assert project_vote_info["assigned_to"] == None
assert project_vote_info["total_watchers"] == 1
assert project_vote_info["created_date"] == vote.created_date
assert project_vote_info["project_name"] == project.name
assert project_vote_info["project_slug"] == project.slug
assert project_vote_info["project_is_private"] == project.is_private
assert project_vote_info["assigned_to_username"] == None
assert project_vote_info["assigned_to_full_name"] == None
assert project_vote_info["assigned_to_photo"] == None
assert project_vote_info["assigned_to_email"] == None
assert project_vote_info["total_votes"] == 1
def test_get_favourites_list_valid_info_for_not_project_types():
fav_user = f.UserFactory()
viewer_user = f.UserFactory()
watcher_user = f.UserFactory()
assigned_to_user = f.UserFactory()
project = f.ProjectFactory(is_private=False, name="Testing project")
factories = {
"userstory": f.UserStoryFactory,
"task": f.TaskFactory,
"issue": f.IssueFactory
}
for object_type in factories:
instance = factories[object_type](project=project,
subject="Testing",
tags=["test1", "test2"],
assigned_to=assigned_to_user)
instance.add_watcher(watcher_user)
content_type = ContentType.objects.get_for_model(instance)
vote = f.VoteFactory(content_type=content_type, object_id=instance.id, user=fav_user)
f.VotesFactory(content_type=content_type, object_id=instance.id, count=3)
instance_vote_info = get_favourites_list(fav_user, viewer_user, type=object_type)[0]
assert instance_vote_info["type"] == object_type
assert instance_vote_info["action"] == "vote"
assert instance_vote_info["id"] == instance.id
assert instance_vote_info["ref"] == instance.ref
assert instance_vote_info["slug"] == None
assert instance_vote_info["subject"] == instance.subject
assert instance_vote_info["tags"] == instance.tags
assert instance_vote_info["project"] == instance.project.id
assert instance_vote_info["assigned_to"] == assigned_to_user.id
assert instance_vote_info["total_watchers"] == 1
assert instance_vote_info["created_date"] == vote.created_date
assert instance_vote_info["project_name"] == instance.project.name
assert instance_vote_info["project_slug"] == instance.project.slug
assert instance_vote_info["project_is_private"] == instance.project.is_private
assert instance_vote_info["assigned_to_username"] == assigned_to_user.username
assert instance_vote_info["assigned_to_full_name"] == assigned_to_user.full_name
assert instance_vote_info["assigned_to_photo"] == ''
assert instance_vote_info["assigned_to_email"] == assigned_to_user.email
assert instance_vote_info["total_votes"] == 3
def test_get_favourites_list_permissions():
fav_user = f.UserFactory()
viewer_unpriviliged_user = f.UserFactory()
viewer_priviliged_user = f.UserFactory()
project = f.ProjectFactory(is_private=True, name="Testing project")
role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"])
membership = f.MembershipFactory(project=project, role=role, user=viewer_priviliged_user)
content_type = ContentType.objects.get_for_model(project)
f.VoteFactory(content_type=content_type, object_id=project.id, user=fav_user)
f.VotesFactory(content_type=content_type, object_id=project.id, count=1)
user_story = f.UserStoryFactory(project=project, subject="Testing user story")
content_type = ContentType.objects.get_for_model(user_story)
f.VoteFactory(content_type=content_type, object_id=user_story.id, user=fav_user)
f.VotesFactory(content_type=content_type, object_id=user_story.id, count=1)
task = f.TaskFactory(project=project, subject="Testing task")
content_type = ContentType.objects.get_for_model(task)
f.VoteFactory(content_type=content_type, object_id=task.id, user=fav_user)
f.VotesFactory(content_type=content_type, object_id=task.id, count=1)
issue = f.IssueFactory(project=project, subject="Testing issue")
content_type = ContentType.objects.get_for_model(issue)
f.VoteFactory(content_type=content_type, object_id=issue.id, user=fav_user)
f.VotesFactory(content_type=content_type, object_id=issue.id, count=1)
#If the project is private a viewer user without any permission shouldn' see any vote
assert len(get_favourites_list(fav_user, viewer_unpriviliged_user)) == 0
#If the project is private but the viewer user has permissions the votes should be accesible
assert len(get_favourites_list(fav_user, viewer_priviliged_user)) == 4
#If the project is private but has the required anon permissions the votes should be accesible by any user too
project.anon_permissions = ["view_project", "view_us", "view_tasks", "view_issues"]
project.save()
assert len(get_favourites_list(fav_user, viewer_unpriviliged_user)) == 4

View File

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

View File

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

View File

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

View File

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

View File

@ -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:
services.take_snapshot(obj, user=obj.owner, comment="test", delete=True)
assert delete_webhook_mock.call_count == 2
def test_send_request_one_webhook(settings):
settings.WEBHOOKS_ENABLED = True
project = f.ProjectFactory()
f.WebhookFactory.create(project=project)
objects = [
f.IssueFactory.create(project=project),
f.TaskFactory.create(project=project),
f.UserStoryFactory.create(project=project),
f.WikiPageFactory.create(project=project)
]
for obj in objects:
with patch('taiga.webhooks.tasks._send_request') as _send_request_mock:
services.take_snapshot(obj, user=obj.owner, comment="test")
assert _send_request_mock.call_count == 1
for obj in objects:
with patch('taiga.webhooks.tasks._send_request') as _send_request_mock:
services.take_snapshot(obj, user=obj.owner, comment="test", delete=True)
assert _send_request_mock.call_count == 1

View File

@ -16,7 +16,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.db.models import signals
from taiga.base.utils import json
def signals_switch():
@ -69,9 +68,9 @@ def helper_test_http_method(client, method, url, data, users, after_each_request
def helper_test_http_method_and_count(client, method, url, data, users, after_each_request=None):
responses = _helper_test_http_method_responses(client, method, url, data, users, after_each_request)
return list(map(lambda r: (r.status_code, len(json.loads(r.content.decode('utf-8')))), responses))
return list(map(lambda r: (r.status_code, len(r.data) if isinstance(r.data, list) and 200 <= r.status_code < 300 else 0), responses))
def helper_test_http_method_and_keys(client, method, url, data, users, after_each_request=None):
responses = _helper_test_http_method_responses(client, method, url, data, users, after_each_request)
return list(map(lambda r: (r.status_code, set(json.loads(r.content.decode('utf-8')).keys())), responses))
return list(map(lambda r: (r.status_code, set(r.data.keys() if isinstance(r.data, dict) and 200 <= r.status_code < 300 else [])), responses))