Refactoring and improving watchers
parent
44eee5212a
commit
f3641f5cfb
|
@ -13,6 +13,8 @@
|
||||||
- Now profile timelines only show content about the objects (US/Tasks/Issues/Wiki pages) you are involved.
|
- Now profile timelines only show content about the objects (US/Tasks/Issues/Wiki pages) you are involved.
|
||||||
- US, tasks and Issues can be upvoted or downvoted and the voters list can be obtained.
|
- 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.
|
- Project can be starred or unstarred and the fans list can be obtained.
|
||||||
|
- Now users can watch public issues, tasks and user stories.
|
||||||
|
- Add endpoints to show the watchers list for issues, tasks and user stories.
|
||||||
- i18n.
|
- i18n.
|
||||||
- Add polish (pl) translation.
|
- Add polish (pl) translation.
|
||||||
- Add portuguese (Brazil) (pt_BR) translation.
|
- Add portuguese (Brazil) (pt_BR) translation.
|
||||||
|
|
|
@ -110,3 +110,12 @@ class TagsColorsField(serializers.WritableField):
|
||||||
|
|
||||||
def from_native(self, data):
|
def from_native(self, data):
|
||||||
return list(data.items())
|
return list(data.items())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class WatchersField(serializers.WritableField):
|
||||||
|
def to_native(self, obj):
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def from_native(self, data):
|
||||||
|
return data
|
||||||
|
|
|
@ -18,6 +18,7 @@ from functools import reduce
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
@ -451,6 +452,33 @@ class TagsFilter(FilterBackend):
|
||||||
return super().filter_queryset(request, queryset, view)
|
return super().filter_queryset(request, queryset, view)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class WatchersFilter(FilterBackend):
|
||||||
|
filter_name = 'watchers'
|
||||||
|
|
||||||
|
def __init__(self, filter_name=None):
|
||||||
|
if filter_name:
|
||||||
|
self.filter_name = filter_name
|
||||||
|
|
||||||
|
def _get_watchers_queryparams(self, params):
|
||||||
|
watchers = params.get(self.filter_name, None)
|
||||||
|
if watchers:
|
||||||
|
return watchers.split(",")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def filter_queryset(self, request, queryset, view):
|
||||||
|
query_watchers = self._get_watchers_queryparams(request.QUERY_PARAMS)
|
||||||
|
model = queryset.model
|
||||||
|
if query_watchers:
|
||||||
|
WatchedModel = apps.get_model("notifications", "Watched")
|
||||||
|
watched_type = ContentType.objects.get_for_model(queryset.model)
|
||||||
|
watched_ids = WatchedModel.objects.filter(content_type=watched_type, user__id__in=query_watchers).values_list("object_id", flat=True)
|
||||||
|
queryset = queryset.filter(id__in=watched_ids)
|
||||||
|
|
||||||
|
return super().filter_queryset(request, queryset, view)
|
||||||
|
|
||||||
|
|
||||||
#####################################################################
|
#####################################################################
|
||||||
# Text search filters
|
# Text search filters
|
||||||
#####################################################################
|
#####################################################################
|
||||||
|
|
|
@ -19,6 +19,7 @@ import copy
|
||||||
import os
|
import os
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
@ -43,6 +44,7 @@ from taiga.projects.attachments import models as attachments_models
|
||||||
from taiga.timeline import models as timeline_models
|
from taiga.timeline import models as timeline_models
|
||||||
from taiga.timeline import service as timeline_service
|
from taiga.timeline import service as timeline_service
|
||||||
from taiga.users import models as users_models
|
from taiga.users import models as users_models
|
||||||
|
from taiga.projects.notifications import services as notifications_services
|
||||||
from taiga.projects.votes import services as votes_service
|
from taiga.projects.votes import services as votes_service
|
||||||
from taiga.projects.history import services as history_service
|
from taiga.projects.history import services as history_service
|
||||||
|
|
||||||
|
@ -223,6 +225,48 @@ class HistoryDiffField(JsonField):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class WatcheableObjectModelSerializer(serializers.ModelSerializer):
|
||||||
|
watchers = UserRelatedField(many=True, required=False)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self._watchers_field = self.base_fields.pop("watchers", None)
|
||||||
|
super(WatcheableObjectModelSerializer, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
"""
|
||||||
|
watchers is not a field from the model so we need to do some magic to make it work like a normal field
|
||||||
|
It's supposed to be represented as an email list but internally it's treated like notifications.Watched instances
|
||||||
|
"""
|
||||||
|
|
||||||
|
def restore_object(self, attrs, instance=None):
|
||||||
|
watcher_field = self.fields.pop("watchers", None)
|
||||||
|
instance = super(WatcheableObjectModelSerializer, self).restore_object(attrs, instance)
|
||||||
|
self._watchers = self.init_data.get("watchers", [])
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def save_watchers(self):
|
||||||
|
new_watcher_emails = set(self._watchers)
|
||||||
|
old_watcher_emails = set(notifications_services.get_watchers(self.object).values_list("email", flat=True))
|
||||||
|
adding_watcher_emails = list(new_watcher_emails.difference(old_watcher_emails))
|
||||||
|
removing_watcher_emails = list(old_watcher_emails.difference(new_watcher_emails))
|
||||||
|
|
||||||
|
User = apps.get_model("users", "User")
|
||||||
|
adding_users = User.objects.filter(email__in=adding_watcher_emails)
|
||||||
|
removing_users = User.objects.filter(email__in=removing_watcher_emails)
|
||||||
|
|
||||||
|
for user in adding_users:
|
||||||
|
notifications_services.add_watcher(self.object, user)
|
||||||
|
|
||||||
|
for user in removing_users:
|
||||||
|
notifications_services.remove_watcher(self.object, user)
|
||||||
|
|
||||||
|
self.object.watchers = notifications_services.get_watchers(self.object)
|
||||||
|
|
||||||
|
def to_native(self, obj):
|
||||||
|
ret = super(WatcheableObjectModelSerializer, self).to_native(obj)
|
||||||
|
ret["watchers"] = [user.email for user in notifications_services.get_watchers(obj)]
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
class HistoryExportSerializer(serializers.ModelSerializer):
|
class HistoryExportSerializer(serializers.ModelSerializer):
|
||||||
user = HistoryUserField()
|
user = HistoryUserField()
|
||||||
diff = HistoryDiffField(required=False)
|
diff = HistoryDiffField(required=False)
|
||||||
|
@ -447,9 +491,8 @@ class RolePointsExportSerializer(serializers.ModelSerializer):
|
||||||
exclude = ('id', 'user_story')
|
exclude = ('id', 'user_story')
|
||||||
|
|
||||||
|
|
||||||
class MilestoneExportSerializer(serializers.ModelSerializer):
|
class MilestoneExportSerializer(WatcheableObjectModelSerializer):
|
||||||
owner = UserRelatedField(required=False)
|
owner = UserRelatedField(required=False)
|
||||||
watchers = UserRelatedField(many=True, required=False)
|
|
||||||
modified_date = serializers.DateTimeField(required=False)
|
modified_date = serializers.DateTimeField(required=False)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -475,13 +518,12 @@ class MilestoneExportSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
|
||||||
class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin,
|
class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin,
|
||||||
AttachmentExportSerializerMixin, serializers.ModelSerializer):
|
AttachmentExportSerializerMixin, WatcheableObjectModelSerializer):
|
||||||
owner = UserRelatedField(required=False)
|
owner = UserRelatedField(required=False)
|
||||||
status = ProjectRelatedField(slug_field="name")
|
status = ProjectRelatedField(slug_field="name")
|
||||||
user_story = ProjectRelatedField(slug_field="ref", required=False)
|
user_story = ProjectRelatedField(slug_field="ref", required=False)
|
||||||
milestone = ProjectRelatedField(slug_field="name", required=False)
|
milestone = ProjectRelatedField(slug_field="name", required=False)
|
||||||
assigned_to = UserRelatedField(required=False)
|
assigned_to = UserRelatedField(required=False)
|
||||||
watchers = UserRelatedField(many=True, required=False)
|
|
||||||
modified_date = serializers.DateTimeField(required=False)
|
modified_date = serializers.DateTimeField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -493,13 +535,12 @@ class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryE
|
||||||
|
|
||||||
|
|
||||||
class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin,
|
class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin,
|
||||||
AttachmentExportSerializerMixin, serializers.ModelSerializer):
|
AttachmentExportSerializerMixin, WatcheableObjectModelSerializer):
|
||||||
role_points = RolePointsExportSerializer(many=True, required=False)
|
role_points = RolePointsExportSerializer(many=True, required=False)
|
||||||
owner = UserRelatedField(required=False)
|
owner = UserRelatedField(required=False)
|
||||||
assigned_to = UserRelatedField(required=False)
|
assigned_to = UserRelatedField(required=False)
|
||||||
status = ProjectRelatedField(slug_field="name")
|
status = ProjectRelatedField(slug_field="name")
|
||||||
milestone = ProjectRelatedField(slug_field="name", required=False)
|
milestone = ProjectRelatedField(slug_field="name", required=False)
|
||||||
watchers = UserRelatedField(many=True, required=False)
|
|
||||||
modified_date = serializers.DateTimeField(required=False)
|
modified_date = serializers.DateTimeField(required=False)
|
||||||
generated_from_issue = ProjectRelatedField(slug_field="ref", required=False)
|
generated_from_issue = ProjectRelatedField(slug_field="ref", required=False)
|
||||||
|
|
||||||
|
@ -512,7 +553,7 @@ class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, His
|
||||||
|
|
||||||
|
|
||||||
class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin,
|
class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin,
|
||||||
AttachmentExportSerializerMixin, serializers.ModelSerializer):
|
AttachmentExportSerializerMixin, WatcheableObjectModelSerializer):
|
||||||
owner = UserRelatedField(required=False)
|
owner = UserRelatedField(required=False)
|
||||||
status = ProjectRelatedField(slug_field="name")
|
status = ProjectRelatedField(slug_field="name")
|
||||||
assigned_to = UserRelatedField(required=False)
|
assigned_to = UserRelatedField(required=False)
|
||||||
|
@ -520,7 +561,6 @@ class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, History
|
||||||
severity = ProjectRelatedField(slug_field="name")
|
severity = ProjectRelatedField(slug_field="name")
|
||||||
type = ProjectRelatedField(slug_field="name")
|
type = ProjectRelatedField(slug_field="name")
|
||||||
milestone = ProjectRelatedField(slug_field="name", required=False)
|
milestone = ProjectRelatedField(slug_field="name", required=False)
|
||||||
watchers = UserRelatedField(many=True, required=False)
|
|
||||||
votes = serializers.SerializerMethodField("get_votes")
|
votes = serializers.SerializerMethodField("get_votes")
|
||||||
modified_date = serializers.DateTimeField(required=False)
|
modified_date = serializers.DateTimeField(required=False)
|
||||||
|
|
||||||
|
@ -536,10 +576,9 @@ class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, History
|
||||||
|
|
||||||
|
|
||||||
class WikiPageExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin,
|
class WikiPageExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin,
|
||||||
serializers.ModelSerializer):
|
WatcheableObjectModelSerializer):
|
||||||
owner = UserRelatedField(required=False)
|
owner = UserRelatedField(required=False)
|
||||||
last_modifier = UserRelatedField(required=False)
|
last_modifier = UserRelatedField(required=False)
|
||||||
watchers = UserRelatedField(many=True, required=False)
|
|
||||||
modified_date = serializers.DateTimeField(required=False)
|
modified_date = serializers.DateTimeField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -586,7 +625,7 @@ class TimelineExportSerializer(serializers.ModelSerializer):
|
||||||
exclude = ('id', 'project', 'namespace', 'object_id')
|
exclude = ('id', 'project', 'namespace', 'object_id')
|
||||||
|
|
||||||
|
|
||||||
class ProjectExportSerializer(serializers.ModelSerializer):
|
class ProjectExportSerializer(WatcheableObjectModelSerializer):
|
||||||
owner = UserRelatedField(required=False)
|
owner = UserRelatedField(required=False)
|
||||||
default_points = serializers.SlugRelatedField(slug_field="name", required=False)
|
default_points = serializers.SlugRelatedField(slug_field="name", required=False)
|
||||||
default_us_status = serializers.SlugRelatedField(slug_field="name", required=False)
|
default_us_status = serializers.SlugRelatedField(slug_field="name", required=False)
|
||||||
|
|
|
@ -71,6 +71,7 @@ def store_project(data):
|
||||||
if serialized.is_valid():
|
if serialized.is_valid():
|
||||||
serialized.object._importing = True
|
serialized.object._importing = True
|
||||||
serialized.object.save()
|
serialized.object.save()
|
||||||
|
serialized.save_watchers()
|
||||||
return serialized
|
return serialized
|
||||||
add_errors("project", serialized.errors)
|
add_errors("project", serialized.errors)
|
||||||
return None
|
return None
|
||||||
|
@ -217,6 +218,7 @@ def store_task(project, data):
|
||||||
serialized.object._not_notify = True
|
serialized.object._not_notify = True
|
||||||
|
|
||||||
serialized.save()
|
serialized.save()
|
||||||
|
serialized.save_watchers()
|
||||||
|
|
||||||
if serialized.object.ref:
|
if serialized.object.ref:
|
||||||
sequence_name = refs.make_sequence_name(project)
|
sequence_name = refs.make_sequence_name(project)
|
||||||
|
@ -257,6 +259,7 @@ def store_milestone(project, milestone):
|
||||||
serialized.object.project = project
|
serialized.object.project = project
|
||||||
serialized.object._importing = True
|
serialized.object._importing = True
|
||||||
serialized.save()
|
serialized.save()
|
||||||
|
serialized.save_watchers()
|
||||||
|
|
||||||
for task_without_us in milestone.get("tasks_without_us", []):
|
for task_without_us in milestone.get("tasks_without_us", []):
|
||||||
task_without_us["user_story"] = None
|
task_without_us["user_story"] = None
|
||||||
|
@ -320,6 +323,7 @@ def store_wiki_page(project, wiki_page):
|
||||||
serialized.object._importing = True
|
serialized.object._importing = True
|
||||||
serialized.object._not_notify = True
|
serialized.object._not_notify = True
|
||||||
serialized.save()
|
serialized.save()
|
||||||
|
serialized.save_watchers()
|
||||||
|
|
||||||
for attachment in wiki_page.get("attachments", []):
|
for attachment in wiki_page.get("attachments", []):
|
||||||
store_attachment(project, serialized.object, attachment)
|
store_attachment(project, serialized.object, attachment)
|
||||||
|
@ -382,6 +386,7 @@ def store_user_story(project, data):
|
||||||
serialized.object._not_notify = True
|
serialized.object._not_notify = True
|
||||||
|
|
||||||
serialized.save()
|
serialized.save()
|
||||||
|
serialized.save_watchers()
|
||||||
|
|
||||||
if serialized.object.ref:
|
if serialized.object.ref:
|
||||||
sequence_name = refs.make_sequence_name(project)
|
sequence_name = refs.make_sequence_name(project)
|
||||||
|
@ -442,6 +447,7 @@ def store_issue(project, data):
|
||||||
serialized.object._not_notify = True
|
serialized.object._not_notify = True
|
||||||
|
|
||||||
serialized.save()
|
serialized.save()
|
||||||
|
serialized.save_watchers()
|
||||||
|
|
||||||
if serialized.object.ref:
|
if serialized.object.ref:
|
||||||
sequence_name = refs.make_sequence_name(project)
|
sequence_name = refs.make_sequence_name(project)
|
||||||
|
|
|
@ -15,11 +15,12 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from taiga.projects.models import Membership, Project
|
|
||||||
from .permissions import OWNERS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
|
from .permissions import OWNERS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
|
|
||||||
def _get_user_project_membership(user, project):
|
def _get_user_project_membership(user, project):
|
||||||
|
Membership = apps.get_model("projects", "Membership")
|
||||||
if user.is_anonymous():
|
if user.is_anonymous():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -30,7 +31,7 @@ def _get_user_project_membership(user, project):
|
||||||
|
|
||||||
def _get_object_project(obj):
|
def _get_object_project(obj):
|
||||||
project = None
|
project = None
|
||||||
|
Project = apps.get_model("projects", "Project")
|
||||||
if isinstance(obj, Project):
|
if isinstance(obj, Project):
|
||||||
project = obj
|
project = obj
|
||||||
elif obj and hasattr(obj, 'project'):
|
elif obj and hasattr(obj, 'project'):
|
||||||
|
|
|
@ -31,6 +31,7 @@ from taiga.base.api.utils import get_object_or_404
|
||||||
from taiga.base.utils.slug import slugify_uniquely
|
from taiga.base.utils.slug import slugify_uniquely
|
||||||
|
|
||||||
from taiga.projects.history.mixins import HistoryResourceMixin
|
from taiga.projects.history.mixins import HistoryResourceMixin
|
||||||
|
from taiga.projects.notifications.mixins import WatchedResourceMixin
|
||||||
from taiga.projects.mixins.ordering import BulkUpdateOrderMixin
|
from taiga.projects.mixins.ordering import BulkUpdateOrderMixin
|
||||||
from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin
|
from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin
|
||||||
|
|
||||||
|
@ -50,7 +51,7 @@ from .votes.mixins.viewsets import StarredResourceMixin, VotersViewSetMixin
|
||||||
## Project
|
## Project
|
||||||
######################################################
|
######################################################
|
||||||
|
|
||||||
class ProjectViewSet(StarredResourceMixin, HistoryResourceMixin, ModelCrudViewSet):
|
class ProjectViewSet(StarredResourceMixin, HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet):
|
||||||
queryset = models.Project.objects.all()
|
queryset = models.Project.objects.all()
|
||||||
serializer_class = serializers.ProjectDetailSerializer
|
serializer_class = serializers.ProjectDetailSerializer
|
||||||
admin_serializer_class = serializers.ProjectDetailAdminSerializer
|
admin_serializer_class = serializers.ProjectDetailAdminSerializer
|
||||||
|
@ -62,7 +63,8 @@ class ProjectViewSet(StarredResourceMixin, HistoryResourceMixin, ModelCrudViewSe
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = super().get_queryset()
|
qs = super().get_queryset()
|
||||||
return self.attach_votes_attrs_to_queryset(qs)
|
qs = self.attach_votes_attrs_to_queryset(qs)
|
||||||
|
return self.attach_watchers_attrs_to_queryset(qs)
|
||||||
|
|
||||||
@list_route(methods=["POST"])
|
@list_route(methods=["POST"])
|
||||||
def bulk_update_order(self, request, **kwargs):
|
def bulk_update_order(self, request, **kwargs):
|
||||||
|
|
|
@ -27,12 +27,7 @@ def connect_memberships_signals():
|
||||||
sender=apps.get_model("projects", "Membership"),
|
sender=apps.get_model("projects", "Membership"),
|
||||||
dispatch_uid='membership_pre_delete')
|
dispatch_uid='membership_pre_delete')
|
||||||
|
|
||||||
# On membership object is deleted, update watchers of all objects relation.
|
# On membership object is deleted, update notify policies of all objects relation.
|
||||||
signals.post_delete.connect(handlers.update_watchers_on_membership_post_delete,
|
|
||||||
sender=apps.get_model("projects", "Membership"),
|
|
||||||
dispatch_uid='update_watchers_on_membership_post_delete')
|
|
||||||
|
|
||||||
# On membership object is deleted, update watchers of all objects relation.
|
|
||||||
signals.post_save.connect(handlers.create_notify_policy,
|
signals.post_save.connect(handlers.create_notify_policy,
|
||||||
sender=apps.get_model("projects", "Membership"),
|
sender=apps.get_model("projects", "Membership"),
|
||||||
dispatch_uid='create-notify-policy')
|
dispatch_uid='create-notify-policy')
|
||||||
|
@ -67,7 +62,6 @@ def connect_task_status_signals():
|
||||||
|
|
||||||
def disconnect_memberships_signals():
|
def disconnect_memberships_signals():
|
||||||
signals.pre_delete.disconnect(sender=apps.get_model("projects", "Membership"), dispatch_uid='membership_pre_delete')
|
signals.pre_delete.disconnect(sender=apps.get_model("projects", "Membership"), dispatch_uid='membership_pre_delete')
|
||||||
signals.post_delete.disconnect(sender=apps.get_model("projects", "Membership"), dispatch_uid='update_watchers_on_membership_post_delete')
|
|
||||||
signals.post_save.disconnect(sender=apps.get_model("projects", "Membership"), dispatch_uid='create-notify-policy')
|
signals.post_save.disconnect(sender=apps.get_model("projects", "Membership"), dispatch_uid='create-notify-policy')
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -288,7 +288,7 @@ def userstory_freezer(us) -> dict:
|
||||||
"milestone": us.milestone_id,
|
"milestone": us.milestone_id,
|
||||||
"client_requirement": us.client_requirement,
|
"client_requirement": us.client_requirement,
|
||||||
"team_requirement": us.team_requirement,
|
"team_requirement": us.team_requirement,
|
||||||
"watchers": [x.id for x in us.watchers.all()],
|
"watchers": [x.id for x in us.get_watchers()],
|
||||||
"attachments": extract_attachments(us),
|
"attachments": extract_attachments(us),
|
||||||
"tags": us.tags,
|
"tags": us.tags,
|
||||||
"points": points,
|
"points": points,
|
||||||
|
@ -315,7 +315,7 @@ def issue_freezer(issue) -> dict:
|
||||||
"description": issue.description,
|
"description": issue.description,
|
||||||
"description_html": mdrender(issue.project, issue.description),
|
"description_html": mdrender(issue.project, issue.description),
|
||||||
"assigned_to": issue.assigned_to_id,
|
"assigned_to": issue.assigned_to_id,
|
||||||
"watchers": [x.pk for x in issue.watchers.all()],
|
"watchers": [x.pk for x in issue.get_watchers()],
|
||||||
"attachments": extract_attachments(issue),
|
"attachments": extract_attachments(issue),
|
||||||
"tags": issue.tags,
|
"tags": issue.tags,
|
||||||
"is_blocked": issue.is_blocked,
|
"is_blocked": issue.is_blocked,
|
||||||
|
@ -337,7 +337,7 @@ def task_freezer(task) -> dict:
|
||||||
"description": task.description,
|
"description": task.description,
|
||||||
"description_html": mdrender(task.project, task.description),
|
"description_html": mdrender(task.project, task.description),
|
||||||
"assigned_to": task.assigned_to_id,
|
"assigned_to": task.assigned_to_id,
|
||||||
"watchers": [x.pk for x in task.watchers.all()],
|
"watchers": [x.pk for x in task.get_watchers()],
|
||||||
"attachments": extract_attachments(task),
|
"attachments": extract_attachments(task),
|
||||||
"taskboard_order": task.taskboard_order,
|
"taskboard_order": task.taskboard_order,
|
||||||
"us_order": task.us_order,
|
"us_order": task.us_order,
|
||||||
|
@ -359,7 +359,7 @@ def wikipage_freezer(wiki) -> dict:
|
||||||
"owner": wiki.owner_id,
|
"owner": wiki.owner_id,
|
||||||
"content": wiki.content,
|
"content": wiki.content,
|
||||||
"content_html": mdrender(wiki.project, wiki.content),
|
"content_html": mdrender(wiki.project, wiki.content),
|
||||||
"watchers": [x.pk for x in wiki.watchers.all()],
|
"watchers": [x.pk for x in wiki.get_watchers()],
|
||||||
"attachments": extract_attachments(wiki),
|
"attachments": extract_attachments(wiki),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,6 +53,7 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
|
||||||
filters.SeveritiesFilter,
|
filters.SeveritiesFilter,
|
||||||
filters.PrioritiesFilter,
|
filters.PrioritiesFilter,
|
||||||
filters.TagsFilter,
|
filters.TagsFilter,
|
||||||
|
filters.WatchersFilter,
|
||||||
filters.QFilter,
|
filters.QFilter,
|
||||||
filters.OrderByFilterMixin)
|
filters.OrderByFilterMixin)
|
||||||
retrieve_exclude_filters = (filters.OwnersFilter,
|
retrieve_exclude_filters = (filters.OwnersFilter,
|
||||||
|
@ -61,11 +62,11 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
|
||||||
filters.IssueTypesFilter,
|
filters.IssueTypesFilter,
|
||||||
filters.SeveritiesFilter,
|
filters.SeveritiesFilter,
|
||||||
filters.PrioritiesFilter,
|
filters.PrioritiesFilter,
|
||||||
filters.TagsFilter,)
|
filters.TagsFilter,
|
||||||
|
filters.WatchersFilter,)
|
||||||
|
|
||||||
filter_fields = ("project",
|
filter_fields = ("project",
|
||||||
"status__is_closed",
|
"status__is_closed")
|
||||||
"watchers")
|
|
||||||
order_by_fields = ("type",
|
order_by_fields = ("type",
|
||||||
"status",
|
"status",
|
||||||
"severity",
|
"severity",
|
||||||
|
@ -142,7 +143,8 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = super().get_queryset()
|
qs = super().get_queryset()
|
||||||
qs = qs.prefetch_related("attachments")
|
qs = qs.prefetch_related("attachments")
|
||||||
return self.attach_votes_attrs_to_queryset(qs)
|
qs = self.attach_votes_attrs_to_queryset(qs)
|
||||||
|
return self.attach_watchers_attrs_to_queryset(qs)
|
||||||
|
|
||||||
def pre_save(self, obj):
|
def pre_save(self, obj):
|
||||||
if not obj.id:
|
if not obj.id:
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
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()
|
||||||
|
migrations.RunSQL(sql="""
|
||||||
|
INSERT INTO notifications_watched (object_id, created_date, content_type_id, user_id)
|
||||||
|
SELECT issue_id AS object_id, now() AS created_date, {content_type_id} AS content_type_id, user_id
|
||||||
|
FROM issues_issue_watchers""".format(content_type_id=ContentType.objects.get(model='issue').id))
|
||||||
|
|
||||||
|
|
||||||
|
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',
|
||||||
|
),
|
||||||
|
]
|
|
@ -35,6 +35,8 @@ class IssuePermission(TaigaResourcePermission):
|
||||||
delete_comment_perms= HasProjectPerm('modify_issue')
|
delete_comment_perms= HasProjectPerm('modify_issue')
|
||||||
upvote_perms = IsAuthenticated() & HasProjectPerm('view_issues')
|
upvote_perms = IsAuthenticated() & HasProjectPerm('view_issues')
|
||||||
downvote_perms = IsAuthenticated() & HasProjectPerm('view_issues')
|
downvote_perms = IsAuthenticated() & HasProjectPerm('view_issues')
|
||||||
|
watch_perms = IsAuthenticated() & HasProjectPerm('view_issues')
|
||||||
|
unwatch_perms = IsAuthenticated() & HasProjectPerm('view_issues')
|
||||||
|
|
||||||
|
|
||||||
class HasIssueIdUrlParam(PermissionComponent):
|
class HasIssueIdUrlParam(PermissionComponent):
|
||||||
|
|
|
@ -23,6 +23,7 @@ from taiga.mdrender.service import render as mdrender
|
||||||
from taiga.projects.validators import ProjectExistsValidator
|
from taiga.projects.validators import ProjectExistsValidator
|
||||||
from taiga.projects.notifications.validators import WatchersValidator
|
from taiga.projects.notifications.validators import WatchersValidator
|
||||||
from taiga.projects.serializers import BasicIssueStatusSerializer
|
from taiga.projects.serializers import BasicIssueStatusSerializer
|
||||||
|
from taiga.projects.notifications.mixins import WatchedResourceModelSerializer
|
||||||
from taiga.projects.votes.mixins.serializers import VotedResourceSerializerMixin
|
from taiga.projects.votes.mixins.serializers import VotedResourceSerializerMixin
|
||||||
|
|
||||||
from taiga.users.serializers import UserBasicInfoSerializer
|
from taiga.users.serializers import UserBasicInfoSerializer
|
||||||
|
@ -30,7 +31,7 @@ from taiga.users.serializers import UserBasicInfoSerializer
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
class IssueSerializer(WatchersValidator, VotedResourceSerializerMixin, serializers.ModelSerializer):
|
class IssueSerializer(WatchersValidator, VotedResourceSerializerMixin, WatchedResourceModelSerializer, serializers.ModelSerializer):
|
||||||
tags = TagsField(required=False)
|
tags = TagsField(required=False)
|
||||||
external_reference = PgArrayField(required=False)
|
external_reference = PgArrayField(required=False)
|
||||||
is_closed = serializers.Field(source="is_closed")
|
is_closed = serializers.Field(source="is_closed")
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# -*- 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.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='watchers',
|
||||||
|
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, related_name='projects_project+', null=True, verbose_name='watchers'),
|
||||||
|
preserve_default=True,
|
||||||
|
),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,28 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
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()
|
||||||
|
migrations.RunSQL(sql="""
|
||||||
|
INSERT INTO notifications_watched (object_id, created_date, content_type_id, user_id)
|
||||||
|
SELECT project_id AS object_id, now() AS created_date, {content_type_id} AS content_type_id, user_id
|
||||||
|
FROM projects_project_watchers""".format(content_type_id=ContentType.objects.get(model='project').id))
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('notifications', '0004_watched'),
|
||||||
|
('projects', '0024_auto_20150810_1247'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(create_notifications),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='project',
|
||||||
|
name='watchers',
|
||||||
|
),
|
||||||
|
]
|
|
@ -44,9 +44,7 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudView
|
||||||
"user_stories__role_points__points",
|
"user_stories__role_points__points",
|
||||||
"user_stories__role_points__role",
|
"user_stories__role_points__role",
|
||||||
"user_stories__generated_from_issue",
|
"user_stories__generated_from_issue",
|
||||||
"user_stories__project",
|
"user_stories__project")
|
||||||
"watchers",
|
|
||||||
"user_stories__watchers")
|
|
||||||
qs = qs.select_related("project")
|
qs = qs.select_related("project")
|
||||||
qs = qs.order_by("-estimated_start")
|
qs = qs.order_by("-estimated_start")
|
||||||
return qs
|
return qs
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
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()
|
||||||
|
migrations.RunSQL(sql="""
|
||||||
|
INSERT INTO notifications_watched (object_id, created_date, content_type_id, user_id)
|
||||||
|
SELECT milestone_id AS object_id, now() AS created_date, {content_type_id} AS content_type_id, user_id
|
||||||
|
FROM milestones_milestone_watchers""".format(content_type_id=ContentType.objects.get(model='milestone').id)),
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('notifications', '0004_watched'),
|
||||||
|
('milestones', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(create_notifications),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='milestone',
|
||||||
|
name='watchers',
|
||||||
|
),
|
||||||
|
]
|
|
@ -19,12 +19,14 @@ from django.utils.translation import ugettext as _
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
from taiga.base.utils import json
|
from taiga.base.utils import json
|
||||||
|
from taiga.projects.notifications.mixins import WatchedResourceModelSerializer
|
||||||
|
from taiga.projects.notifications.validators import WatchersValidator
|
||||||
|
|
||||||
from ..userstories.serializers import UserStorySerializer
|
from ..userstories.serializers import UserStorySerializer
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
class MilestoneSerializer(serializers.ModelSerializer):
|
class MilestoneSerializer(WatchersValidator, WatchedResourceModelSerializer, serializers.ModelSerializer):
|
||||||
user_stories = UserStorySerializer(many=True, required=False, read_only=True)
|
user_stories = UserStorySerializer(many=True, required=False, read_only=True)
|
||||||
total_points = serializers.SerializerMethodField("get_total_points")
|
total_points = serializers.SerializerMethodField("get_total_points")
|
||||||
closed_points = serializers.SerializerMethodField("get_closed_points")
|
closed_points = serializers.SerializerMethodField("get_closed_points")
|
||||||
|
|
|
@ -40,6 +40,8 @@ from taiga.base.utils.slug import slugify_uniquely_for_queryset
|
||||||
|
|
||||||
from . import choices
|
from . import choices
|
||||||
|
|
||||||
|
from . notifications.mixins import WatchedModelMixin
|
||||||
|
|
||||||
|
|
||||||
class Membership(models.Model):
|
class Membership(models.Model):
|
||||||
# This model stores all project memberships. Also
|
# This model stores all project memberships. Also
|
||||||
|
@ -118,7 +120,7 @@ class ProjectDefaults(models.Model):
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class Project(ProjectDefaults, TaggedMixin, models.Model):
|
class Project(ProjectDefaults, WatchedModelMixin, TaggedMixin, models.Model):
|
||||||
name = models.CharField(max_length=250, null=False, blank=False,
|
name = models.CharField(max_length=250, null=False, blank=False,
|
||||||
verbose_name=_("name"))
|
verbose_name=_("name"))
|
||||||
slug = models.SlugField(max_length=250, unique=True, null=False, blank=True,
|
slug = models.SlugField(max_length=250, unique=True, null=False, blank=True,
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
def fill_watched_table(apps, schema_editor):
|
||||||
|
Watched = apps.get_model("notifications", "Watched")
|
||||||
|
print("test")
|
||||||
|
|
||||||
|
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(primary_key=True, serialize=False, verbose_name='ID', auto_created=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)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
},
|
||||||
|
bases=(models.Model,),
|
||||||
|
),
|
||||||
|
migrations.RunPython(fill_watched_table),
|
||||||
|
]
|
|
@ -17,14 +17,22 @@
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from operator import is_not
|
from operator import is_not
|
||||||
|
|
||||||
from django.conf import settings
|
from django.apps import apps
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from taiga.base import response
|
||||||
|
from taiga.base.decorators import detail_route
|
||||||
|
from taiga.base.api import serializers
|
||||||
|
from taiga.base.fields import WatchersField
|
||||||
from taiga.projects.notifications import services
|
from taiga.projects.notifications import services
|
||||||
|
from taiga.projects.notifications.utils import attach_watchers_to_queryset, attach_is_watched_to_queryset
|
||||||
|
from taiga.users.models import User
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
class WatchedResourceMixin(object):
|
class WatchedResourceMixin:
|
||||||
"""
|
"""
|
||||||
Rest Framework resource mixin for resources susceptible
|
Rest Framework resource mixin for resources susceptible
|
||||||
to be notifiable about their changes.
|
to be notifiable about their changes.
|
||||||
|
@ -36,6 +44,27 @@ class WatchedResourceMixin(object):
|
||||||
|
|
||||||
_not_notify = False
|
_not_notify = False
|
||||||
|
|
||||||
|
def attach_watchers_attrs_to_queryset(self, queryset):
|
||||||
|
qs = attach_watchers_to_queryset(queryset)
|
||||||
|
if self.request.user.is_authenticated():
|
||||||
|
qs = attach_is_watched_to_queryset(self.request.user, qs)
|
||||||
|
|
||||||
|
return qs
|
||||||
|
|
||||||
|
@detail_route(methods=["POST"])
|
||||||
|
def watch(self, request, pk=None):
|
||||||
|
obj = self.get_object()
|
||||||
|
self.check_permissions(request, "watch", obj)
|
||||||
|
services.add_watcher(obj, request.user)
|
||||||
|
return response.Ok()
|
||||||
|
|
||||||
|
@detail_route(methods=["POST"])
|
||||||
|
def unwatch(self, request, pk=None):
|
||||||
|
obj = self.get_object()
|
||||||
|
self.check_permissions(request, "unwatch", obj)
|
||||||
|
services.remove_watcher(obj, request.user)
|
||||||
|
return response.Ok()
|
||||||
|
|
||||||
def send_notifications(self, obj, history=None):
|
def send_notifications(self, obj, history=None):
|
||||||
"""
|
"""
|
||||||
Shortcut method for resources with special save
|
Shortcut method for resources with special save
|
||||||
|
@ -73,7 +102,7 @@ class WatchedResourceMixin(object):
|
||||||
super().pre_delete(obj)
|
super().pre_delete(obj)
|
||||||
|
|
||||||
|
|
||||||
class WatchedModelMixin(models.Model):
|
class WatchedModelMixin(object):
|
||||||
"""
|
"""
|
||||||
Generic model mixin that makes model compatible
|
Generic model mixin that makes model compatible
|
||||||
with notification system.
|
with notification system.
|
||||||
|
@ -82,11 +111,6 @@ class WatchedModelMixin(models.Model):
|
||||||
this mixin if you want send notifications about
|
this mixin if you want send notifications about
|
||||||
your model class.
|
your model class.
|
||||||
"""
|
"""
|
||||||
watchers = models.ManyToManyField(settings.AUTH_USER_MODEL, null=True, blank=True,
|
|
||||||
related_name="%(app_label)s_%(class)s+",
|
|
||||||
verbose_name=_("watchers"))
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
def get_project(self) -> object:
|
def get_project(self) -> object:
|
||||||
"""
|
"""
|
||||||
|
@ -97,6 +121,7 @@ class WatchedModelMixin(models.Model):
|
||||||
that should works in almost all cases.
|
that should works in almost all cases.
|
||||||
"""
|
"""
|
||||||
return self.project
|
return self.project
|
||||||
|
t
|
||||||
|
|
||||||
def get_watchers(self) -> frozenset:
|
def get_watchers(self) -> frozenset:
|
||||||
"""
|
"""
|
||||||
|
@ -112,7 +137,13 @@ class WatchedModelMixin(models.Model):
|
||||||
very inefficient way for obtain watchers but at
|
very inefficient way for obtain watchers but at
|
||||||
this momment is the simplest way.
|
this momment is the simplest way.
|
||||||
"""
|
"""
|
||||||
return frozenset(self.watchers.all())
|
return frozenset(services.get_watchers(self))
|
||||||
|
|
||||||
|
def add_watcher(self, user):
|
||||||
|
services.add_watcher(self, user)
|
||||||
|
|
||||||
|
def remove_watcher(self, user):
|
||||||
|
services.remove_watcher(self, user)
|
||||||
|
|
||||||
def get_owner(self) -> object:
|
def get_owner(self) -> object:
|
||||||
"""
|
"""
|
||||||
|
@ -140,3 +171,79 @@ class WatchedModelMixin(models.Model):
|
||||||
self.get_owner(),)
|
self.get_owner(),)
|
||||||
is_not_none = partial(is_not, None)
|
is_not_none = partial(is_not, None)
|
||||||
return frozenset(filter(is_not_none, participants))
|
return frozenset(filter(is_not_none, participants))
|
||||||
|
|
||||||
|
|
||||||
|
class WatchedResourceModelSerializer(serializers.ModelSerializer):
|
||||||
|
is_watched = serializers.SerializerMethodField("get_is_watched")
|
||||||
|
watchers = WatchersField(required=False)
|
||||||
|
|
||||||
|
def get_is_watched(self, obj):
|
||||||
|
# The "is_watched" attribute is attached in the get_queryset of the viewset.
|
||||||
|
return getattr(obj, "is_watched", False) or False
|
||||||
|
|
||||||
|
def restore_object(self, attrs, instance=None):
|
||||||
|
#watchers is not a field from the model but can be attached in the get_queryset of the viewset.
|
||||||
|
#If that's the case we need to remove it before calling the super method
|
||||||
|
watcher_field = self.fields.pop("watchers", None)
|
||||||
|
instance = super(WatchedResourceModelSerializer, self).restore_object(attrs, instance)
|
||||||
|
if instance is not None and self.validate_watchers(attrs, "watchers"):
|
||||||
|
new_watcher_ids = set(attrs.get("watchers", []))
|
||||||
|
old_watcher_ids = set(services.get_watchers(instance).values_list("id", flat=True))
|
||||||
|
adding_watcher_ids = list(new_watcher_ids.difference(old_watcher_ids))
|
||||||
|
removing_watcher_ids = list(old_watcher_ids.difference(new_watcher_ids))
|
||||||
|
|
||||||
|
User = apps.get_model("users", "User")
|
||||||
|
adding_users = User.objects.filter(id__in=adding_watcher_ids)
|
||||||
|
removing_users = User.objects.filter(id__in=removing_watcher_ids)
|
||||||
|
for user in adding_users:
|
||||||
|
services.add_watcher(instance, user)
|
||||||
|
|
||||||
|
for user in removing_users:
|
||||||
|
services.remove_watcher(instance, user)
|
||||||
|
|
||||||
|
instance.watchers = services.get_watchers(instance)
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
def to_native(self, obj):
|
||||||
|
#watchers is wasn't attached via the get_queryset of the viewset we need to manually add it
|
||||||
|
if not hasattr(obj, "watchers"):
|
||||||
|
obj.watchers = 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,6 +14,8 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.contenttypes import generic
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -72,3 +74,18 @@ class HistoryChangeNotification(models.Model):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ("key", "owner", "project", "history_type")
|
unique_together = ("key", "owner", "project", "history_type")
|
||||||
|
|
||||||
|
|
||||||
|
class Watched(models.Model):
|
||||||
|
content_type = models.ForeignKey("contenttypes.ContentType")
|
||||||
|
object_id = models.PositiveIntegerField()
|
||||||
|
content_object = generic.GenericForeignKey("content_type", "object_id")
|
||||||
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=False, null=False,
|
||||||
|
related_name="watched", verbose_name=_("user"))
|
||||||
|
created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False,
|
||||||
|
verbose_name=_("created date"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Watched")
|
||||||
|
verbose_name_plural = _("Watched")
|
||||||
|
unique_together = ("content_type", "object_id", "user")
|
||||||
|
|
|
@ -17,10 +17,10 @@
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db import IntegrityError
|
from django.db.transaction import atomic
|
||||||
|
from django.db import IntegrityError, transaction
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.db import transaction
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ from taiga.projects.history.services import (make_key_from_model_object,
|
||||||
from taiga.permissions.service import user_has_perm
|
from taiga.permissions.service import user_has_perm
|
||||||
from taiga.users.models import User
|
from taiga.users.models import User
|
||||||
|
|
||||||
from .models import HistoryChangeNotification
|
from .models import HistoryChangeNotification, Watched
|
||||||
|
|
||||||
|
|
||||||
def notify_policy_exists(project, user) -> bool:
|
def notify_policy_exists(project, user) -> bool:
|
||||||
|
@ -121,11 +121,11 @@ def analize_object_for_watchers(obj:object, history:object):
|
||||||
|
|
||||||
if data["mentions"]:
|
if data["mentions"]:
|
||||||
for user in data["mentions"]:
|
for user in data["mentions"]:
|
||||||
obj.watchers.add(user)
|
obj.add_watcher(user)
|
||||||
|
|
||||||
# Adding the person who edited the object to the watchers
|
# Adding the person who edited the object to the watchers
|
||||||
if history.comment and not history.owner.is_system:
|
if history.comment and not history.owner.is_system:
|
||||||
obj.watchers.add(history.owner)
|
obj.add_watcher(history.owner)
|
||||||
|
|
||||||
def _filter_by_permissions(obj, user):
|
def _filter_by_permissions(obj, user):
|
||||||
UserStory = apps.get_model("userstories", "UserStory")
|
UserStory = apps.get_model("userstories", "UserStory")
|
||||||
|
@ -282,3 +282,46 @@ def send_sync_notifications(notification_id):
|
||||||
def process_sync_notifications():
|
def process_sync_notifications():
|
||||||
for notification in HistoryChangeNotification.objects.all():
|
for notification in HistoryChangeNotification.objects.all():
|
||||||
send_sync_notifications(notification.pk)
|
send_sync_notifications(notification.pk)
|
||||||
|
|
||||||
|
|
||||||
|
def get_watchers(obj):
|
||||||
|
User = apps.get_model("users", "User")
|
||||||
|
Watched = apps.get_model("notifications", "Watched")
|
||||||
|
content_type = ContentType.objects.get_for_model(obj)
|
||||||
|
watching_user_ids = Watched.objects.filter(content_type=content_type, object_id=obj.id).values_list("user__id", flat=True)
|
||||||
|
return User.objects.filter(id__in=watching_user_ids)
|
||||||
|
|
||||||
|
|
||||||
|
def add_watcher(obj, user):
|
||||||
|
"""Add a watcher to an object.
|
||||||
|
|
||||||
|
If the user is already watching the object nothing happends, 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)
|
||||||
|
with atomic():
|
||||||
|
watched, created = Watched.objects.get_or_create(content_type=obj_type, object_id=obj.id, user=user)
|
||||||
|
if not created:
|
||||||
|
return
|
||||||
|
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)
|
||||||
|
with atomic():
|
||||||
|
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:
|
class WatchersValidator:
|
||||||
def validate_watchers(self, attrs, source):
|
def validate_watchers(self, attrs, source):
|
||||||
users = attrs[source]
|
users = attrs.get(source, [])
|
||||||
|
|
||||||
# Try obtain a valid project
|
# Try obtain a valid project
|
||||||
if self.object is None and "project" in attrs:
|
if self.object is None and "project" in attrs:
|
||||||
|
@ -39,7 +39,8 @@ class WatchersValidator:
|
||||||
|
|
||||||
# Check if incoming watchers are contained
|
# Check if incoming watchers are contained
|
||||||
# in project members list
|
# in project members list
|
||||||
result = set(users).difference(set(project.members.all()))
|
member_ids = project.members.values_list("id", flat=True)
|
||||||
|
result = set(users).difference(member_ids)
|
||||||
if result:
|
if result:
|
||||||
raise serializers.ValidationError(_("Watchers contains invalid users"))
|
raise serializers.ValidationError(_("Watchers contains invalid users"))
|
||||||
|
|
||||||
|
|
|
@ -62,6 +62,8 @@ class ProjectPermission(TaigaResourcePermission):
|
||||||
tags_colors_perms = HasProjectPerm('view_project')
|
tags_colors_perms = HasProjectPerm('view_project')
|
||||||
star_perms = IsAuthenticated() & HasProjectPerm('view_project')
|
star_perms = IsAuthenticated() & HasProjectPerm('view_project')
|
||||||
unstar_perms = IsAuthenticated() & HasProjectPerm('view_project')
|
unstar_perms = IsAuthenticated() & HasProjectPerm('view_project')
|
||||||
|
watch_perms = IsAuthenticated() & HasProjectPerm('view_project')
|
||||||
|
unwatch_perms = IsAuthenticated() & HasProjectPerm('view_project')
|
||||||
create_template_perms = IsSuperUser()
|
create_template_perms = IsSuperUser()
|
||||||
leave_perms = CanLeaveProject()
|
leave_perms = CanLeaveProject()
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,8 @@ from taiga.base.fields import PgArrayField
|
||||||
from taiga.base.fields import TagsField
|
from taiga.base.fields import TagsField
|
||||||
from taiga.base.fields import TagsColorsField
|
from taiga.base.fields import TagsColorsField
|
||||||
|
|
||||||
|
from taiga.projects.notifications.validators import WatchersValidator
|
||||||
|
|
||||||
from taiga.users.services import get_photo_or_gravatar_url
|
from taiga.users.services import get_photo_or_gravatar_url
|
||||||
from taiga.users.serializers import UserSerializer
|
from taiga.users.serializers import UserSerializer
|
||||||
from taiga.users.serializers import UserBasicInfoSerializer
|
from taiga.users.serializers import UserBasicInfoSerializer
|
||||||
|
@ -40,6 +42,7 @@ from .validators import ProjectExistsValidator
|
||||||
from .custom_attributes.serializers import UserStoryCustomAttributeSerializer
|
from .custom_attributes.serializers import UserStoryCustomAttributeSerializer
|
||||||
from .custom_attributes.serializers import TaskCustomAttributeSerializer
|
from .custom_attributes.serializers import TaskCustomAttributeSerializer
|
||||||
from .custom_attributes.serializers import IssueCustomAttributeSerializer
|
from .custom_attributes.serializers import IssueCustomAttributeSerializer
|
||||||
|
from .notifications.mixins import WatchedResourceModelSerializer
|
||||||
from .votes.mixins.serializers import StarredResourceSerializerMixin
|
from .votes.mixins.serializers import StarredResourceSerializerMixin
|
||||||
|
|
||||||
######################################################
|
######################################################
|
||||||
|
@ -305,7 +308,7 @@ class ProjectMemberSerializer(serializers.ModelSerializer):
|
||||||
## Projects
|
## Projects
|
||||||
######################################################
|
######################################################
|
||||||
|
|
||||||
class ProjectSerializer(StarredResourceSerializerMixin, serializers.ModelSerializer):
|
class ProjectSerializer(WatchersValidator, StarredResourceSerializerMixin, WatchedResourceModelSerializer, serializers.ModelSerializer):
|
||||||
tags = TagsField(default=[], required=False)
|
tags = TagsField(default=[], required=False)
|
||||||
anon_permissions = PgArrayField(required=False)
|
anon_permissions = PgArrayField(required=False)
|
||||||
public_permissions = PgArrayField(required=False)
|
public_permissions = PgArrayField(required=False)
|
||||||
|
|
|
@ -45,24 +45,6 @@ def membership_post_delete(sender, instance, using, **kwargs):
|
||||||
instance.project.update_role_points()
|
instance.project.update_role_points()
|
||||||
|
|
||||||
|
|
||||||
def update_watchers_on_membership_post_delete(sender, instance, using, **kwargs):
|
|
||||||
models = [apps.get_model("userstories", "UserStory"),
|
|
||||||
apps.get_model("tasks", "Task"),
|
|
||||||
apps.get_model("issues", "Issue")]
|
|
||||||
|
|
||||||
# `user_id` is used beacuse in some momments
|
|
||||||
# instance.user can contain pointer to now
|
|
||||||
# removed object from a database.
|
|
||||||
for model in models:
|
|
||||||
#filter(project=instance.project)
|
|
||||||
filter = {
|
|
||||||
"user_id": instance.user_id,
|
|
||||||
"%s__project"%(model._meta.model_name): instance.project,
|
|
||||||
}
|
|
||||||
|
|
||||||
model.watchers.through.objects.filter(**filter).delete()
|
|
||||||
|
|
||||||
|
|
||||||
def create_notify_policy(sender, instance, using, **kwargs):
|
def create_notify_policy(sender, instance, using, **kwargs):
|
||||||
if instance.user:
|
if instance.user:
|
||||||
create_notify_policy_if_not_exists(instance.project, instance.user)
|
create_notify_policy_if_not_exists(instance.project, instance.user)
|
||||||
|
|
|
@ -40,9 +40,10 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
|
||||||
ModelCrudViewSet):
|
ModelCrudViewSet):
|
||||||
queryset = models.Task.objects.all()
|
queryset = models.Task.objects.all()
|
||||||
permission_classes = (permissions.TaskPermission,)
|
permission_classes = (permissions.TaskPermission,)
|
||||||
filter_backends = (filters.CanViewTasksFilterBackend,)
|
filter_backends = (filters.CanViewTasksFilterBackend, filters.WatchersFilter)
|
||||||
|
retrieve_exclude_filters = (filters.WatchersFilter,)
|
||||||
filter_fields = ["user_story", "milestone", "project", "assigned_to",
|
filter_fields = ["user_story", "milestone", "project", "assigned_to",
|
||||||
"status__is_closed", "watchers"]
|
"status__is_closed"]
|
||||||
|
|
||||||
def get_serializer_class(self, *args, **kwargs):
|
def get_serializer_class(self, *args, **kwargs):
|
||||||
if self.action in ["retrieve", "by_ref"]:
|
if self.action in ["retrieve", "by_ref"]:
|
||||||
|
@ -86,7 +87,8 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = super().get_queryset()
|
qs = super().get_queryset()
|
||||||
return self.attach_votes_attrs_to_queryset(qs)
|
qs = self.attach_votes_attrs_to_queryset(qs)
|
||||||
|
return self.attach_watchers_attrs_to_queryset(qs)
|
||||||
|
|
||||||
def pre_save(self, obj):
|
def pre_save(self, obj):
|
||||||
if obj.user_story:
|
if obj.user_story:
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
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()
|
||||||
|
migrations.RunSQL(sql="""
|
||||||
|
INSERT INTO notifications_watched (object_id, created_date, content_type_id, user_id)
|
||||||
|
SELECT task_id AS object_id, now() AS created_date, {content_type_id} AS content_type_id, user_id
|
||||||
|
FROM tasks_task_watchers""".format(content_type_id=ContentType.objects.get(model='task').id)),
|
||||||
|
|
||||||
|
|
||||||
|
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',
|
||||||
|
),
|
||||||
|
]
|
|
@ -33,6 +33,8 @@ class TaskPermission(TaigaResourcePermission):
|
||||||
bulk_update_order_perms = HasProjectPerm('modify_task')
|
bulk_update_order_perms = HasProjectPerm('modify_task')
|
||||||
upvote_perms = IsAuthenticated() & HasProjectPerm('view_tasks')
|
upvote_perms = IsAuthenticated() & HasProjectPerm('view_tasks')
|
||||||
downvote_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):
|
class TaskVotersPermission(TaigaResourcePermission):
|
||||||
|
|
|
@ -27,6 +27,7 @@ from taiga.projects.milestones.validators import SprintExistsValidator
|
||||||
from taiga.projects.tasks.validators import TaskExistsValidator
|
from taiga.projects.tasks.validators import TaskExistsValidator
|
||||||
from taiga.projects.notifications.validators import WatchersValidator
|
from taiga.projects.notifications.validators import WatchersValidator
|
||||||
from taiga.projects.serializers import BasicTaskStatusSerializerSerializer
|
from taiga.projects.serializers import BasicTaskStatusSerializerSerializer
|
||||||
|
from taiga.projects.notifications.mixins import WatchedResourceModelSerializer
|
||||||
from taiga.projects.votes.mixins.serializers import VotedResourceSerializerMixin
|
from taiga.projects.votes.mixins.serializers import VotedResourceSerializerMixin
|
||||||
|
|
||||||
from taiga.users.serializers import UserBasicInfoSerializer
|
from taiga.users.serializers import UserBasicInfoSerializer
|
||||||
|
@ -34,7 +35,7 @@ from taiga.users.serializers import UserBasicInfoSerializer
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
class TaskSerializer(WatchersValidator, VotedResourceSerializerMixin, serializers.ModelSerializer):
|
class TaskSerializer(WatchersValidator, VotedResourceSerializerMixin, WatchedResourceModelSerializer, serializers.ModelSerializer):
|
||||||
tags = TagsField(required=False, default=[])
|
tags = TagsField(required=False, default=[])
|
||||||
external_reference = PgArrayField(required=False)
|
external_reference = PgArrayField(required=False)
|
||||||
comment = serializers.SerializerMethodField("get_comment")
|
comment = serializers.SerializerMethodField("get_comment")
|
||||||
|
|
|
@ -53,19 +53,20 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
|
||||||
filters.AssignedToFilter,
|
filters.AssignedToFilter,
|
||||||
filters.StatusesFilter,
|
filters.StatusesFilter,
|
||||||
filters.TagsFilter,
|
filters.TagsFilter,
|
||||||
|
filters.WatchersFilter,
|
||||||
filters.QFilter,
|
filters.QFilter,
|
||||||
filters.OrderByFilterMixin)
|
filters.OrderByFilterMixin)
|
||||||
retrieve_exclude_filters = (filters.OwnersFilter,
|
retrieve_exclude_filters = (filters.OwnersFilter,
|
||||||
filters.AssignedToFilter,
|
filters.AssignedToFilter,
|
||||||
filters.StatusesFilter,
|
filters.StatusesFilter,
|
||||||
filters.TagsFilter)
|
filters.TagsFilter,
|
||||||
|
filters.WatchersFilter)
|
||||||
filter_fields = ["project",
|
filter_fields = ["project",
|
||||||
"milestone",
|
"milestone",
|
||||||
"milestone__isnull",
|
"milestone__isnull",
|
||||||
"is_closed",
|
"is_closed",
|
||||||
"status__is_archived",
|
"status__is_archived",
|
||||||
"status__is_closed",
|
"status__is_closed"]
|
||||||
"watchers"]
|
|
||||||
order_by_fields = ["backlog_order",
|
order_by_fields = ["backlog_order",
|
||||||
"sprint_order",
|
"sprint_order",
|
||||||
"kanban_order"]
|
"kanban_order"]
|
||||||
|
@ -113,10 +114,10 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
|
||||||
qs = super().get_queryset()
|
qs = super().get_queryset()
|
||||||
qs = qs.prefetch_related("role_points",
|
qs = qs.prefetch_related("role_points",
|
||||||
"role_points__points",
|
"role_points__points",
|
||||||
"role_points__role",
|
"role_points__role")
|
||||||
"watchers")
|
|
||||||
qs = qs.select_related("milestone", "project")
|
qs = qs.select_related("milestone", "project")
|
||||||
return self.attach_votes_attrs_to_queryset(qs)
|
qs = self.attach_votes_attrs_to_queryset(qs)
|
||||||
|
return self.attach_watchers_attrs_to_queryset(qs)
|
||||||
|
|
||||||
def pre_save(self, obj):
|
def pre_save(self, obj):
|
||||||
# This is very ugly hack, but having
|
# This is very ugly hack, but having
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
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()
|
||||||
|
migrations.RunSQL(sql="""
|
||||||
|
INSERT INTO notifications_watched (object_id, created_date, content_type_id, user_id)
|
||||||
|
SELECT userstory_id AS object_id, now() AS created_date, {content_type_id} AS content_type_id, user_id
|
||||||
|
FROM userstories_userstory_watchers""".format(content_type_id=ContentType.objects.get(model='userstory').id)),
|
||||||
|
|
||||||
|
|
||||||
|
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',
|
||||||
|
),
|
||||||
|
]
|
|
@ -32,7 +32,8 @@ class UserStoryPermission(TaigaResourcePermission):
|
||||||
bulk_update_order_perms = HasProjectPerm('modify_us')
|
bulk_update_order_perms = HasProjectPerm('modify_us')
|
||||||
upvote_perms = IsAuthenticated() & HasProjectPerm('view_us')
|
upvote_perms = IsAuthenticated() & HasProjectPerm('view_us')
|
||||||
downvote_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):
|
class UserStoryVotersPermission(TaigaResourcePermission):
|
||||||
enought_perms = IsProjectOwner() | IsSuperUser()
|
enought_perms = IsProjectOwner() | IsSuperUser()
|
||||||
|
|
|
@ -27,6 +27,7 @@ from taiga.projects.validators import UserStoryStatusExistsValidator
|
||||||
from taiga.projects.userstories.validators import UserStoryExistsValidator
|
from taiga.projects.userstories.validators import UserStoryExistsValidator
|
||||||
from taiga.projects.notifications.validators import WatchersValidator
|
from taiga.projects.notifications.validators import WatchersValidator
|
||||||
from taiga.projects.serializers import BasicUserStoryStatusSerializer
|
from taiga.projects.serializers import BasicUserStoryStatusSerializer
|
||||||
|
from taiga.projects.notifications.mixins import WatchedResourceModelSerializer
|
||||||
from taiga.projects.votes.mixins.serializers import VotedResourceSerializerMixin
|
from taiga.projects.votes.mixins.serializers import VotedResourceSerializerMixin
|
||||||
|
|
||||||
from taiga.users.serializers import UserBasicInfoSerializer
|
from taiga.users.serializers import UserBasicInfoSerializer
|
||||||
|
@ -44,7 +45,7 @@ class RolePointsField(serializers.WritableField):
|
||||||
return json.loads(obj)
|
return json.loads(obj)
|
||||||
|
|
||||||
|
|
||||||
class UserStorySerializer(WatchersValidator, VotedResourceSerializerMixin, serializers.ModelSerializer):
|
class UserStorySerializer(WatchersValidator, VotedResourceSerializerMixin, WatchedResourceModelSerializer, serializers.ModelSerializer):
|
||||||
tags = TagsField(default=[], required=False)
|
tags = TagsField(default=[], required=False)
|
||||||
external_reference = PgArrayField(required=False)
|
external_reference = PgArrayField(required=False)
|
||||||
points = RolePointsField(source="role_points", required=False)
|
points = RolePointsField(source="role_points", required=False)
|
||||||
|
|
|
@ -16,8 +16,10 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
|
from taiga.base.fields import TagsField
|
||||||
|
|
||||||
from taiga.users.models import User
|
from taiga.users.models import User
|
||||||
|
from taiga.users.services import get_photo_or_gravatar_url
|
||||||
|
|
||||||
|
|
||||||
class VoterSerializer(serializers.ModelSerializer):
|
class VoterSerializer(serializers.ModelSerializer):
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
|
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()
|
||||||
|
migrations.RunSQL(sql="""
|
||||||
|
INSERT INTO notifications_watched (object_id, created_date, content_type_id, user_id)
|
||||||
|
SELECT wikipage_id AS object_id, now() AS created_date, {content_type_id} AS content_type_id, user_id
|
||||||
|
FROM wiki_wikipage_watchers""".format(content_type_id=ContentType.objects.get(model='wikipage').id)),
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('notifications', '0004_watched'),
|
||||||
|
('wiki', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(create_notifications),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='wikipage',
|
||||||
|
name='watchers',
|
||||||
|
),
|
||||||
|
]
|
|
@ -15,6 +15,10 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
|
from taiga.projects.history import services as history_service
|
||||||
|
from taiga.projects.notifications.mixins import WatchedResourceModelSerializer
|
||||||
|
from taiga.projects.notifications.validators import WatchersValidator
|
||||||
|
from taiga.mdrender.service import render as mdrender
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
@ -23,7 +27,7 @@ from taiga.projects.history import services as history_service
|
||||||
from taiga.mdrender.service import render as mdrender
|
from taiga.mdrender.service import render as mdrender
|
||||||
|
|
||||||
|
|
||||||
class WikiPageSerializer(serializers.ModelSerializer):
|
class WikiPageSerializer(WatchersValidator, WatchedResourceModelSerializer, serializers.ModelSerializer):
|
||||||
html = serializers.SerializerMethodField("get_html")
|
html = serializers.SerializerMethodField("get_html")
|
||||||
editions = serializers.SerializerMethodField("get_editions")
|
editions = serializers.SerializerMethodField("get_editions")
|
||||||
|
|
||||||
|
@ -39,6 +43,5 @@ class WikiPageSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
|
||||||
class WikiLinkSerializer(serializers.ModelSerializer):
|
class WikiLinkSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.WikiLink
|
model = models.WikiLink
|
||||||
|
|
|
@ -62,7 +62,7 @@ def _push_to_timelines(project, user, obj, event_type, created_datetime, extra_d
|
||||||
## - Watchers
|
## - Watchers
|
||||||
watchers = getattr(obj, "watchers", None)
|
watchers = getattr(obj, "watchers", None)
|
||||||
if watchers:
|
if watchers:
|
||||||
related_people |= obj.watchers.all()
|
related_people |= obj.get_watchers()
|
||||||
|
|
||||||
## - Exclude inactive and system users and remove duplicate
|
## - Exclude inactive and system users and remove duplicate
|
||||||
related_people = related_people.exclude(is_active=False)
|
related_people = related_people.exclude(is_active=False)
|
||||||
|
|
|
@ -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,
|
||||||
|
),
|
||||||
|
]
|
|
@ -23,8 +23,9 @@ from taiga.projects.userstories import models as us_models
|
||||||
from taiga.projects.tasks import models as task_models
|
from taiga.projects.tasks import models as task_models
|
||||||
from taiga.projects.issues import models as issue_models
|
from taiga.projects.issues import models as issue_models
|
||||||
from taiga.projects.milestones import models as milestone_models
|
from taiga.projects.milestones import models as milestone_models
|
||||||
from taiga.projects.history import models as history_models
|
|
||||||
from taiga.projects.wiki import models as wiki_models
|
from taiga.projects.wiki import models as wiki_models
|
||||||
|
from taiga.projects.history import models as history_models
|
||||||
|
from taiga.projects.notifications.mixins import WatchedResourceModelSerializer
|
||||||
|
|
||||||
from .models import Webhook, WebhookLog
|
from .models import Webhook, WebhookLog
|
||||||
|
|
||||||
|
@ -103,7 +104,8 @@ class PointSerializer(serializers.Serializer):
|
||||||
return obj.value
|
return obj.value
|
||||||
|
|
||||||
|
|
||||||
class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.ModelSerializer):
|
class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, WatchedResourceModelSerializer,
|
||||||
|
serializers.ModelSerializer):
|
||||||
tags = TagsField(default=[], required=False)
|
tags = TagsField(default=[], required=False)
|
||||||
external_reference = PgArrayField(required=False)
|
external_reference = PgArrayField(required=False)
|
||||||
owner = UserSerializer()
|
owner = UserSerializer()
|
||||||
|
@ -119,7 +121,8 @@ class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, serializ
|
||||||
return project.userstorycustomattributes.all()
|
return project.userstorycustomattributes.all()
|
||||||
|
|
||||||
|
|
||||||
class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.ModelSerializer):
|
class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, WatchedResourceModelSerializer,
|
||||||
|
serializers.ModelSerializer):
|
||||||
tags = TagsField(default=[], required=False)
|
tags = TagsField(default=[], required=False)
|
||||||
owner = UserSerializer()
|
owner = UserSerializer()
|
||||||
assigned_to = UserSerializer()
|
assigned_to = UserSerializer()
|
||||||
|
@ -132,7 +135,8 @@ class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.M
|
||||||
return project.taskcustomattributes.all()
|
return project.taskcustomattributes.all()
|
||||||
|
|
||||||
|
|
||||||
class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.ModelSerializer):
|
class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, WatchedResourceModelSerializer,
|
||||||
|
serializers.ModelSerializer):
|
||||||
tags = TagsField(default=[], required=False)
|
tags = TagsField(default=[], required=False)
|
||||||
owner = UserSerializer()
|
owner = UserSerializer()
|
||||||
assigned_to = UserSerializer()
|
assigned_to = UserSerializer()
|
||||||
|
|
|
@ -574,3 +574,45 @@ def test_issues_csv(client, data):
|
||||||
|
|
||||||
results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users)
|
results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users)
|
||||||
assert results == [200, 200, 200, 200, 200]
|
assert results == [200, 200, 200, 200, 200]
|
||||||
|
|
||||||
|
|
||||||
|
def test_issue_action_watch(client, data):
|
||||||
|
public_url = reverse('issues-watch', kwargs={"pk": data.public_issue.pk})
|
||||||
|
private_url1 = reverse('issues-watch', kwargs={"pk": data.private_issue1.pk})
|
||||||
|
private_url2 = reverse('issues-watch', kwargs={"pk": data.private_issue2.pk})
|
||||||
|
|
||||||
|
users = [
|
||||||
|
None,
|
||||||
|
data.registered_user,
|
||||||
|
data.project_member_without_perms,
|
||||||
|
data.project_member_with_perms,
|
||||||
|
data.project_owner
|
||||||
|
]
|
||||||
|
|
||||||
|
results = helper_test_http_method(client, 'post', public_url, "", users)
|
||||||
|
assert results == [401, 200, 200, 200, 200]
|
||||||
|
results = helper_test_http_method(client, 'post', private_url1, "", users)
|
||||||
|
assert results == [401, 200, 200, 200, 200]
|
||||||
|
results = helper_test_http_method(client, 'post', private_url2, "", users)
|
||||||
|
assert results == [404, 404, 404, 200, 200]
|
||||||
|
|
||||||
|
|
||||||
|
def test_issue_action_unwatch(client, data):
|
||||||
|
public_url = reverse('issues-unwatch', kwargs={"pk": data.public_issue.pk})
|
||||||
|
private_url1 = reverse('issues-unwatch', kwargs={"pk": data.private_issue1.pk})
|
||||||
|
private_url2 = reverse('issues-unwatch', kwargs={"pk": data.private_issue2.pk})
|
||||||
|
|
||||||
|
users = [
|
||||||
|
None,
|
||||||
|
data.registered_user,
|
||||||
|
data.project_member_without_perms,
|
||||||
|
data.project_member_with_perms,
|
||||||
|
data.project_owner
|
||||||
|
]
|
||||||
|
|
||||||
|
results = helper_test_http_method(client, 'post', public_url, "", users)
|
||||||
|
assert results == [401, 200, 200, 200, 200]
|
||||||
|
results = helper_test_http_method(client, 'post', private_url1, "", users)
|
||||||
|
assert results == [401, 200, 200, 200, 200]
|
||||||
|
results = helper_test_http_method(client, 'post', private_url2, "", users)
|
||||||
|
assert results == [404, 404, 404, 200, 200]
|
||||||
|
|
|
@ -416,3 +416,41 @@ def test_regenerate_issues_csv_uuid(client, data):
|
||||||
|
|
||||||
results = helper_test_http_method(client, 'post', private2_url, None, users)
|
results = helper_test_http_method(client, 'post', private2_url, None, users)
|
||||||
assert results == [404, 404, 403, 200]
|
assert results == [404, 404, 403, 200]
|
||||||
|
|
||||||
|
|
||||||
|
def test_project_action_watch(client, data):
|
||||||
|
public_url = reverse('projects-watch', kwargs={"pk": data.public_project.pk})
|
||||||
|
private1_url = reverse('projects-watch', kwargs={"pk": data.private_project1.pk})
|
||||||
|
private2_url = reverse('projects-watch', kwargs={"pk": data.private_project2.pk})
|
||||||
|
|
||||||
|
users = [
|
||||||
|
None,
|
||||||
|
data.registered_user,
|
||||||
|
data.project_member_with_perms,
|
||||||
|
data.project_owner
|
||||||
|
]
|
||||||
|
results = helper_test_http_method(client, 'post', public_url, None, users)
|
||||||
|
assert results == [401, 200, 200, 200]
|
||||||
|
results = helper_test_http_method(client, 'post', private1_url, None, users)
|
||||||
|
assert results == [401, 200, 200, 200]
|
||||||
|
results = helper_test_http_method(client, 'post', private2_url, None, users)
|
||||||
|
assert results == [404, 404, 200, 200]
|
||||||
|
|
||||||
|
|
||||||
|
def test_project_action_unwatch(client, data):
|
||||||
|
public_url = reverse('projects-unwatch', kwargs={"pk": data.public_project.pk})
|
||||||
|
private1_url = reverse('projects-unwatch', kwargs={"pk": data.private_project1.pk})
|
||||||
|
private2_url = reverse('projects-unwatch', kwargs={"pk": data.private_project2.pk})
|
||||||
|
|
||||||
|
users = [
|
||||||
|
None,
|
||||||
|
data.registered_user,
|
||||||
|
data.project_member_with_perms,
|
||||||
|
data.project_owner
|
||||||
|
]
|
||||||
|
results = helper_test_http_method(client, 'post', public_url, None, users)
|
||||||
|
assert results == [401, 200, 200, 200]
|
||||||
|
results = helper_test_http_method(client, 'post', private1_url, None, users)
|
||||||
|
assert results == [401, 200, 200, 200]
|
||||||
|
results = helper_test_http_method(client, 'post', private2_url, None, users)
|
||||||
|
assert results == [404, 404, 200, 200]
|
||||||
|
|
|
@ -529,3 +529,45 @@ def test_tasks_csv(client, data):
|
||||||
|
|
||||||
results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users)
|
results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users)
|
||||||
assert results == [200, 200, 200, 200, 200]
|
assert results == [200, 200, 200, 200, 200]
|
||||||
|
|
||||||
|
|
||||||
|
def test_task_action_watch(client, data):
|
||||||
|
public_url = reverse('tasks-watch', kwargs={"pk": data.public_task.pk})
|
||||||
|
private_url1 = reverse('tasks-watch', kwargs={"pk": data.private_task1.pk})
|
||||||
|
private_url2 = reverse('tasks-watch', kwargs={"pk": data.private_task2.pk})
|
||||||
|
|
||||||
|
users = [
|
||||||
|
None,
|
||||||
|
data.registered_user,
|
||||||
|
data.project_member_without_perms,
|
||||||
|
data.project_member_with_perms,
|
||||||
|
data.project_owner
|
||||||
|
]
|
||||||
|
|
||||||
|
results = helper_test_http_method(client, 'post', public_url, "", users)
|
||||||
|
assert results == [401, 200, 200, 200, 200]
|
||||||
|
results = helper_test_http_method(client, 'post', private_url1, "", users)
|
||||||
|
assert results == [401, 200, 200, 200, 200]
|
||||||
|
results = helper_test_http_method(client, 'post', private_url2, "", users)
|
||||||
|
assert results == [404, 404, 404, 200, 200]
|
||||||
|
|
||||||
|
|
||||||
|
def test_task_action_unwatch(client, data):
|
||||||
|
public_url = reverse('tasks-unwatch', kwargs={"pk": data.public_task.pk})
|
||||||
|
private_url1 = reverse('tasks-unwatch', kwargs={"pk": data.private_task1.pk})
|
||||||
|
private_url2 = reverse('tasks-unwatch', kwargs={"pk": data.private_task2.pk})
|
||||||
|
|
||||||
|
users = [
|
||||||
|
None,
|
||||||
|
data.registered_user,
|
||||||
|
data.project_member_without_perms,
|
||||||
|
data.project_member_with_perms,
|
||||||
|
data.project_owner
|
||||||
|
]
|
||||||
|
|
||||||
|
results = helper_test_http_method(client, 'post', public_url, "", users)
|
||||||
|
assert results == [401, 200, 200, 200, 200]
|
||||||
|
results = helper_test_http_method(client, 'post', private_url1, "", users)
|
||||||
|
assert results == [401, 200, 200, 200, 200]
|
||||||
|
results = helper_test_http_method(client, 'post', private_url2, "", users)
|
||||||
|
assert results == [404, 404, 404, 200, 200]
|
||||||
|
|
|
@ -528,3 +528,45 @@ def test_user_stories_csv(client, data):
|
||||||
|
|
||||||
results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users)
|
results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users)
|
||||||
assert results == [200, 200, 200, 200, 200]
|
assert results == [200, 200, 200, 200, 200]
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_story_action_watch(client, data):
|
||||||
|
public_url = reverse('userstories-watch', kwargs={"pk": data.public_user_story.pk})
|
||||||
|
private_url1 = reverse('userstories-watch', kwargs={"pk": data.private_user_story1.pk})
|
||||||
|
private_url2 = reverse('userstories-watch', kwargs={"pk": data.private_user_story2.pk})
|
||||||
|
|
||||||
|
users = [
|
||||||
|
None,
|
||||||
|
data.registered_user,
|
||||||
|
data.project_member_without_perms,
|
||||||
|
data.project_member_with_perms,
|
||||||
|
data.project_owner
|
||||||
|
]
|
||||||
|
|
||||||
|
results = helper_test_http_method(client, 'post', public_url, "", users)
|
||||||
|
assert results == [401, 200, 200, 200, 200]
|
||||||
|
results = helper_test_http_method(client, 'post', private_url1, "", users)
|
||||||
|
assert results == [401, 200, 200, 200, 200]
|
||||||
|
results = helper_test_http_method(client, 'post', private_url2, "", users)
|
||||||
|
assert results == [404, 404, 404, 200, 200]
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_story_action_unwatch(client, data):
|
||||||
|
public_url = reverse('userstories-unwatch', kwargs={"pk": data.public_user_story.pk})
|
||||||
|
private_url1 = reverse('userstories-unwatch', kwargs={"pk": data.private_user_story1.pk})
|
||||||
|
private_url2 = reverse('userstories-unwatch', kwargs={"pk": data.private_user_story2.pk})
|
||||||
|
|
||||||
|
users = [
|
||||||
|
None,
|
||||||
|
data.registered_user,
|
||||||
|
data.project_member_without_perms,
|
||||||
|
data.project_member_with_perms,
|
||||||
|
data.project_owner
|
||||||
|
]
|
||||||
|
|
||||||
|
results = helper_test_http_method(client, 'post', public_url, "", users)
|
||||||
|
assert results == [401, 200, 200, 200, 200]
|
||||||
|
results = helper_test_http_method(client, 'post', private_url1, "", users)
|
||||||
|
assert results == [401, 200, 200, 200, 200]
|
||||||
|
results = helper_test_http_method(client, 'post', private_url2, "", users)
|
||||||
|
assert results == [404, 404, 404, 200, 200]
|
||||||
|
|
|
@ -47,13 +47,15 @@ def test_invalid_project_import(client):
|
||||||
|
|
||||||
def test_valid_project_import_without_extra_data(client):
|
def test_valid_project_import_without_extra_data(client):
|
||||||
user = f.UserFactory.create()
|
user = f.UserFactory.create()
|
||||||
|
user_watching = f.UserFactory.create(email="testing@taiga.io")
|
||||||
client.login(user)
|
client.login(user)
|
||||||
|
|
||||||
url = reverse("importer-list")
|
url = reverse("importer-list")
|
||||||
data = {
|
data = {
|
||||||
"name": "Imported project",
|
"name": "Imported project",
|
||||||
"description": "Imported project",
|
"description": "Imported project",
|
||||||
"roles": [{"name": "Role"}]
|
"roles": [{"name": "Role"}],
|
||||||
|
"watchers": ["testing@taiga.io"]
|
||||||
}
|
}
|
||||||
|
|
||||||
response = client.post(url, json.dumps(data), content_type="application/json")
|
response = client.post(url, json.dumps(data), content_type="application/json")
|
||||||
|
@ -66,6 +68,7 @@ def test_valid_project_import_without_extra_data(client):
|
||||||
]
|
]
|
||||||
assert all(map(lambda x: len(response_data[x]) == 0, must_empty_children))
|
assert all(map(lambda x: len(response_data[x]) == 0, must_empty_children))
|
||||||
assert response_data["owner"] == user.email
|
assert response_data["owner"] == user.email
|
||||||
|
assert response_data["watchers"] == [user_watching.email]
|
||||||
|
|
||||||
|
|
||||||
def test_valid_project_import_with_not_existing_memberships(client):
|
def test_valid_project_import_with_not_existing_memberships(client):
|
||||||
|
@ -383,6 +386,7 @@ def test_valid_issue_import_with_custom_attributes_values(client):
|
||||||
|
|
||||||
def test_valid_issue_import_with_extra_data(client):
|
def test_valid_issue_import_with_extra_data(client):
|
||||||
user = f.UserFactory.create()
|
user = f.UserFactory.create()
|
||||||
|
user_watching = f.UserFactory.create(email="testing@taiga.io")
|
||||||
project = f.ProjectFactory.create(owner=user)
|
project = f.ProjectFactory.create(owner=user)
|
||||||
f.MembershipFactory(project=project, user=user, is_owner=True)
|
f.MembershipFactory(project=project, user=user, is_owner=True)
|
||||||
project.default_issue_type = f.IssueTypeFactory.create(project=project)
|
project.default_issue_type = f.IssueTypeFactory.create(project=project)
|
||||||
|
@ -403,7 +407,8 @@ def test_valid_issue_import_with_extra_data(client):
|
||||||
"name": "imported attachment",
|
"name": "imported attachment",
|
||||||
"data": base64.b64encode(b"TEST").decode("utf-8")
|
"data": base64.b64encode(b"TEST").decode("utf-8")
|
||||||
}
|
}
|
||||||
}]
|
}],
|
||||||
|
"watchers": ["testing@taiga.io"]
|
||||||
}
|
}
|
||||||
|
|
||||||
response = client.post(url, json.dumps(data), content_type="application/json")
|
response = client.post(url, json.dumps(data), content_type="application/json")
|
||||||
|
@ -413,6 +418,7 @@ def test_valid_issue_import_with_extra_data(client):
|
||||||
assert response_data["owner"] == user.email
|
assert response_data["owner"] == user.email
|
||||||
assert response_data["ref"] is not None
|
assert response_data["ref"] is not None
|
||||||
assert response_data["finished_date"] == "2014-10-24T00:00:00+0000"
|
assert response_data["finished_date"] == "2014-10-24T00:00:00+0000"
|
||||||
|
assert response_data["watchers"] == [user_watching.email]
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_issue_import_with_extra_data(client):
|
def test_invalid_issue_import_with_extra_data(client):
|
||||||
|
@ -535,6 +541,7 @@ def test_valid_us_import_without_extra_data(client):
|
||||||
|
|
||||||
def test_valid_us_import_with_extra_data(client):
|
def test_valid_us_import_with_extra_data(client):
|
||||||
user = f.UserFactory.create()
|
user = f.UserFactory.create()
|
||||||
|
user_watching = f.UserFactory.create(email="testing@taiga.io")
|
||||||
project = f.ProjectFactory.create(owner=user)
|
project = f.ProjectFactory.create(owner=user)
|
||||||
f.MembershipFactory(project=project, user=user, is_owner=True)
|
f.MembershipFactory(project=project, user=user, is_owner=True)
|
||||||
project.default_us_status = f.UserStoryStatusFactory.create(project=project)
|
project.default_us_status = f.UserStoryStatusFactory.create(project=project)
|
||||||
|
@ -551,7 +558,8 @@ def test_valid_us_import_with_extra_data(client):
|
||||||
"name": "imported attachment",
|
"name": "imported attachment",
|
||||||
"data": base64.b64encode(b"TEST").decode("utf-8")
|
"data": base64.b64encode(b"TEST").decode("utf-8")
|
||||||
}
|
}
|
||||||
}]
|
}],
|
||||||
|
"watchers": ["testing@taiga.io"]
|
||||||
}
|
}
|
||||||
|
|
||||||
response = client.post(url, json.dumps(data), content_type="application/json")
|
response = client.post(url, json.dumps(data), content_type="application/json")
|
||||||
|
@ -560,6 +568,7 @@ def test_valid_us_import_with_extra_data(client):
|
||||||
assert len(response_data["attachments"]) == 1
|
assert len(response_data["attachments"]) == 1
|
||||||
assert response_data["owner"] == user.email
|
assert response_data["owner"] == user.email
|
||||||
assert response_data["ref"] is not None
|
assert response_data["ref"] is not None
|
||||||
|
assert response_data["watchers"] == [user_watching.email]
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_us_import_with_extra_data(client):
|
def test_invalid_us_import_with_extra_data(client):
|
||||||
|
@ -664,6 +673,7 @@ def test_valid_task_import_with_custom_attributes_values(client):
|
||||||
|
|
||||||
def test_valid_task_import_with_extra_data(client):
|
def test_valid_task_import_with_extra_data(client):
|
||||||
user = f.UserFactory.create()
|
user = f.UserFactory.create()
|
||||||
|
user_watching = f.UserFactory.create(email="testing@taiga.io")
|
||||||
project = f.ProjectFactory.create(owner=user)
|
project = f.ProjectFactory.create(owner=user)
|
||||||
f.MembershipFactory(project=project, user=user, is_owner=True)
|
f.MembershipFactory(project=project, user=user, is_owner=True)
|
||||||
project.default_task_status = f.TaskStatusFactory.create(project=project)
|
project.default_task_status = f.TaskStatusFactory.create(project=project)
|
||||||
|
@ -680,7 +690,8 @@ def test_valid_task_import_with_extra_data(client):
|
||||||
"name": "imported attachment",
|
"name": "imported attachment",
|
||||||
"data": base64.b64encode(b"TEST").decode("utf-8")
|
"data": base64.b64encode(b"TEST").decode("utf-8")
|
||||||
}
|
}
|
||||||
}]
|
}],
|
||||||
|
"watchers": ["testing@taiga.io"]
|
||||||
}
|
}
|
||||||
|
|
||||||
response = client.post(url, json.dumps(data), content_type="application/json")
|
response = client.post(url, json.dumps(data), content_type="application/json")
|
||||||
|
@ -689,6 +700,7 @@ def test_valid_task_import_with_extra_data(client):
|
||||||
assert len(response_data["attachments"]) == 1
|
assert len(response_data["attachments"]) == 1
|
||||||
assert response_data["owner"] == user.email
|
assert response_data["owner"] == user.email
|
||||||
assert response_data["ref"] is not None
|
assert response_data["ref"] is not None
|
||||||
|
assert response_data["watchers"] == [user_watching.email]
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_task_import_with_extra_data(client):
|
def test_invalid_task_import_with_extra_data(client):
|
||||||
|
@ -787,6 +799,7 @@ def test_valid_wiki_page_import_without_extra_data(client):
|
||||||
|
|
||||||
def test_valid_wiki_page_import_with_extra_data(client):
|
def test_valid_wiki_page_import_with_extra_data(client):
|
||||||
user = f.UserFactory.create()
|
user = f.UserFactory.create()
|
||||||
|
user_watching = f.UserFactory.create(email="testing@taiga.io")
|
||||||
project = f.ProjectFactory.create(owner=user)
|
project = f.ProjectFactory.create(owner=user)
|
||||||
f.MembershipFactory(project=project, user=user, is_owner=True)
|
f.MembershipFactory(project=project, user=user, is_owner=True)
|
||||||
client.login(user)
|
client.login(user)
|
||||||
|
@ -801,7 +814,8 @@ def test_valid_wiki_page_import_with_extra_data(client):
|
||||||
"name": "imported attachment",
|
"name": "imported attachment",
|
||||||
"data": base64.b64encode(b"TEST").decode("utf-8")
|
"data": base64.b64encode(b"TEST").decode("utf-8")
|
||||||
}
|
}
|
||||||
}]
|
}],
|
||||||
|
"watchers": ["testing@taiga.io"]
|
||||||
}
|
}
|
||||||
|
|
||||||
response = client.post(url, json.dumps(data), content_type="application/json")
|
response = client.post(url, json.dumps(data), content_type="application/json")
|
||||||
|
@ -809,6 +823,7 @@ def test_valid_wiki_page_import_with_extra_data(client):
|
||||||
response_data = response.data
|
response_data = response.data
|
||||||
assert len(response_data["attachments"]) == 1
|
assert len(response_data["attachments"]) == 1
|
||||||
assert response_data["owner"] == user.email
|
assert response_data["owner"] == user.email
|
||||||
|
assert response_data["watchers"] == [user_watching.email]
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_wiki_page_import_with_extra_data(client):
|
def test_invalid_wiki_page_import_with_extra_data(client):
|
||||||
|
@ -877,6 +892,7 @@ def test_invalid_milestone_import(client):
|
||||||
|
|
||||||
def test_valid_milestone_import(client):
|
def test_valid_milestone_import(client):
|
||||||
user = f.UserFactory.create()
|
user = f.UserFactory.create()
|
||||||
|
user_watching = f.UserFactory.create(email="testing@taiga.io")
|
||||||
project = f.ProjectFactory.create(owner=user)
|
project = f.ProjectFactory.create(owner=user)
|
||||||
f.MembershipFactory(project=project, user=user, is_owner=True)
|
f.MembershipFactory(project=project, user=user, is_owner=True)
|
||||||
client.login(user)
|
client.login(user)
|
||||||
|
@ -886,11 +902,12 @@ def test_valid_milestone_import(client):
|
||||||
"name": "Imported milestone",
|
"name": "Imported milestone",
|
||||||
"estimated_start": "2014-10-10",
|
"estimated_start": "2014-10-10",
|
||||||
"estimated_finish": "2014-10-20",
|
"estimated_finish": "2014-10-20",
|
||||||
|
"watchers": ["testing@taiga.io"]
|
||||||
}
|
}
|
||||||
|
|
||||||
response = client.post(url, json.dumps(data), content_type="application/json")
|
response = client.post(url, json.dumps(data), content_type="application/json")
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
response.data
|
assert response.data["watchers"] == [user_watching.email]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -97,7 +97,7 @@ def test_analize_object_for_watchers():
|
||||||
history.comment = ""
|
history.comment = ""
|
||||||
|
|
||||||
services.analize_object_for_watchers(issue, history)
|
services.analize_object_for_watchers(issue, history)
|
||||||
assert issue.watchers.add.call_count == 2
|
assert issue.add_watcher.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
def test_analize_object_for_watchers_adding_owner_non_empty_comment():
|
def test_analize_object_for_watchers_adding_owner_non_empty_comment():
|
||||||
|
@ -112,7 +112,7 @@ def test_analize_object_for_watchers_adding_owner_non_empty_comment():
|
||||||
history.owner = user1
|
history.owner = user1
|
||||||
|
|
||||||
services.analize_object_for_watchers(issue, history)
|
services.analize_object_for_watchers(issue, history)
|
||||||
assert issue.watchers.add.call_count == 1
|
assert issue.add_watcher.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_analize_object_for_watchers_no_adding_owner_empty_comment():
|
def test_analize_object_for_watchers_no_adding_owner_empty_comment():
|
||||||
|
@ -127,7 +127,7 @@ def test_analize_object_for_watchers_no_adding_owner_empty_comment():
|
||||||
history.owner = user1
|
history.owner = user1
|
||||||
|
|
||||||
services.analize_object_for_watchers(issue, history)
|
services.analize_object_for_watchers(issue, history)
|
||||||
assert issue.watchers.add.call_count == 0
|
assert issue.add_watcher.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
def test_users_to_notify():
|
def test_users_to_notify():
|
||||||
|
@ -180,7 +180,7 @@ def test_users_to_notify():
|
||||||
assert users == {member1.user, issue.get_owner()}
|
assert users == {member1.user, issue.get_owner()}
|
||||||
|
|
||||||
# Test with watchers
|
# Test with watchers
|
||||||
issue.watchers.add(member3.user)
|
issue.add_watcher(member3.user)
|
||||||
users = services.get_users_to_notify(issue)
|
users = services.get_users_to_notify(issue)
|
||||||
assert len(users) == 3
|
assert len(users) == 3
|
||||||
assert users == {member1.user, member3.user, issue.get_owner()}
|
assert users == {member1.user, member3.user, issue.get_owner()}
|
||||||
|
@ -189,24 +189,24 @@ def test_users_to_notify():
|
||||||
policy2.notify_level = NotifyLevel.ignore
|
policy2.notify_level = NotifyLevel.ignore
|
||||||
policy2.save()
|
policy2.save()
|
||||||
|
|
||||||
issue.watchers.add(member3.user)
|
issue.add_watcher(member3.user)
|
||||||
users = services.get_users_to_notify(issue)
|
users = services.get_users_to_notify(issue)
|
||||||
assert len(users) == 2
|
assert len(users) == 2
|
||||||
assert users == {member1.user, issue.get_owner()}
|
assert users == {member1.user, issue.get_owner()}
|
||||||
|
|
||||||
# Test with watchers without permissions
|
# Test with watchers without permissions
|
||||||
issue.watchers.add(member5.user)
|
issue.add_watcher(member5.user)
|
||||||
users = services.get_users_to_notify(issue)
|
users = services.get_users_to_notify(issue)
|
||||||
assert len(users) == 2
|
assert len(users) == 2
|
||||||
assert users == {member1.user, issue.get_owner()}
|
assert users == {member1.user, issue.get_owner()}
|
||||||
|
|
||||||
# Test with inactive user
|
# Test with inactive user
|
||||||
issue.watchers.add(inactive_member1.user)
|
issue.add_watcher(inactive_member1.user)
|
||||||
assert len(users) == 2
|
assert len(users) == 2
|
||||||
assert users == {member1.user, issue.get_owner()}
|
assert users == {member1.user, issue.get_owner()}
|
||||||
|
|
||||||
# Test with system user
|
# Test with system user
|
||||||
issue.watchers.add(system_member1.user)
|
issue.add_watcher(system_member1.user)
|
||||||
assert len(users) == 2
|
assert len(users) == 2
|
||||||
assert users == {member1.user, issue.get_owner()}
|
assert users == {member1.user, issue.get_owner()}
|
||||||
|
|
||||||
|
@ -344,7 +344,7 @@ def test_watchers_assignation_for_issue(client):
|
||||||
|
|
||||||
issue = f.create_issue(project=project1, owner=user1)
|
issue = f.create_issue(project=project1, owner=user1)
|
||||||
data = {"version": issue.version,
|
data = {"version": issue.version,
|
||||||
"watchers": [user1.pk]}
|
"watchersa": [user1.pk]}
|
||||||
|
|
||||||
url = reverse("issues-detail", args=[issue.pk])
|
url = reverse("issues-detail", args=[issue.pk])
|
||||||
response = client.json.patch(url, json.dumps(data))
|
response = client.json.patch(url, json.dumps(data))
|
||||||
|
|
|
@ -265,7 +265,7 @@ def test_leave_project_respect_watching_items(client):
|
||||||
url = reverse("projects-leave", args=(project.id,))
|
url = reverse("projects-leave", args=(project.id,))
|
||||||
response = client.post(url)
|
response = client.post(url)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert list(issue.watchers.all()) == [user]
|
assert issue.watchers == [user]
|
||||||
|
|
||||||
|
|
||||||
def test_delete_membership_only_owner(client):
|
def test_delete_membership_only_owner(client):
|
||||||
|
|
|
@ -384,16 +384,6 @@ def test_assigned_to_user_story_timeline():
|
||||||
assert user_timeline[0].data["userstory"]["subject"] == "test us timeline"
|
assert user_timeline[0].data["userstory"]["subject"] == "test us timeline"
|
||||||
|
|
||||||
|
|
||||||
def test_watchers_to_user_story_timeline():
|
|
||||||
membership = factories.MembershipFactory.create()
|
|
||||||
user_story = factories.UserStoryFactory.create(subject="test us timeline", project=membership.project)
|
|
||||||
user_story.watchers.add(membership.user)
|
|
||||||
history_services.take_snapshot(user_story, user=user_story.owner)
|
|
||||||
user_timeline = service.get_profile_timeline(membership.user)
|
|
||||||
assert user_timeline[0].event_type == "userstories.userstory.create"
|
|
||||||
assert user_timeline[0].data["userstory"]["subject"] == "test us timeline"
|
|
||||||
|
|
||||||
|
|
||||||
def test_user_data_for_non_system_users():
|
def test_user_data_for_non_system_users():
|
||||||
user_story = factories.UserStoryFactory.create(subject="test us timeline")
|
user_story = factories.UserStoryFactory.create(subject="test us timeline")
|
||||||
history_services.take_snapshot(user_story, user=user_story.owner)
|
history_services.take_snapshot(user_story, user=user_story.owner)
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
# 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_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
|
|
@ -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,47 @@
|
||||||
|
# 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_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
|
|
@ -0,0 +1,47 @@
|
||||||
|
# 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_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
|
|
@ -0,0 +1,47 @@
|
||||||
|
# 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_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
|
|
@ -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
|
Loading…
Reference in New Issue