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 storiesremotes/origin/enhancement/email-actions
commit
ec29efa606
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
#####################################################################
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')),
|
||||
|
|
|
@ -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'):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -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')
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -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')
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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()
|
||||
|
|
|
@ -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')]),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
|
@ -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"))
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -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')
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'):
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
),
|
||||
]
|
|
@ -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")
|
|
@ -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)
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
]
|
|
@ -47,6 +47,7 @@ class UserPermission(TaigaResourcePermission):
|
|||
starred_perms = AllowAny()
|
||||
change_email_perms = AllowAny()
|
||||
contacts_perms = AllowAny()
|
||||
favourites_perms = AllowAny()
|
||||
|
||||
|
||||
class RolesPermission(TaigaResourcePermission):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
]
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
Loading…
Reference in New Issue