diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index 6223adb0..43f78475 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -31,7 +31,7 @@ from taiga.base.api.utils import get_object_or_404
from taiga.base.utils.slug import slugify_uniquely
from taiga.projects.history.mixins import HistoryResourceMixin
-from taiga.projects.notifications.mixins import WatchedResourceMixin
+from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
from taiga.projects.mixins.ordering import BulkUpdateOrderMixin
from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin
@@ -270,6 +270,10 @@ class ProjectFansViewSet(VotersViewSetMixin, ModelListViewSet):
resource_model = models.Project
+class ProjectWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
+ permission_classes = (permissions.ProjectWatchersPermission,)
+ resource_model = models.Project
+
######################################################
## Custom values for selectors
######################################################
diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py
index 1007bdf1..5035418b 100644
--- a/taiga/projects/issues/api.py
+++ b/taiga/projects/issues/api.py
@@ -27,7 +27,7 @@ from taiga.base.api.utils import get_object_or_404
from taiga.users.models import User
-from taiga.projects.notifications.mixins import WatchedResourceMixin
+from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
from taiga.projects.occ import OCCResourceMixin
from taiga.projects.history.mixins import HistoryResourceMixin
@@ -243,3 +243,8 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
class IssueVotersViewSet(VotersViewSetMixin, ModelListViewSet):
permission_classes = (permissions.IssueVotersPermission,)
resource_model = models.Issue
+
+
+class IssueWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
+ permission_classes = (permissions.IssueWatchersPermission,)
+ resource_model = models.Issue
diff --git a/taiga/projects/issues/migrations/0006_remove_issue_watchers.py b/taiga/projects/issues/migrations/0006_remove_issue_watchers.py
index c41e387e..dd3ee037 100644
--- a/taiga/projects/issues/migrations/0006_remove_issue_watchers.py
+++ b/taiga/projects/issues/migrations/0006_remove_issue_watchers.py
@@ -1,17 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
+from django.db import connection
from django.db import models, migrations
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.management import update_all_contenttypes
def create_notifications(apps, schema_editor):
- update_all_contenttypes()
- 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))
-
+ update_all_contenttypes()
+ sql="""
+INSERT INTO notifications_watched (object_id, created_date, content_type_id, user_id, project_id)
+SELECT issue_id AS object_id, now() AS created_date, {content_type_id} AS content_type_id, user_id, project_id
+FROM issues_issue_watchers INNER JOIN issues_issue ON issues_issue_watchers.issue_id = issues_issue.id""".format(content_type_id=ContentType.objects.get(model='issue').id)
+ cursor = connection.cursor()
+ cursor.execute(sql)
class Migration(migrations.Migration):
diff --git a/taiga/projects/issues/permissions.py b/taiga/projects/issues/permissions.py
index 82120e14..91f988ca 100644
--- a/taiga/projects/issues/permissions.py
+++ b/taiga/projects/issues/permissions.py
@@ -52,3 +52,10 @@ class IssueVotersPermission(TaigaResourcePermission):
global_perms = None
retrieve_perms = HasProjectPerm('view_issues')
list_perms = HasProjectPerm('view_issues')
+
+
+class IssueWatchersPermission(TaigaResourcePermission):
+ enought_perms = IsProjectOwner() | IsSuperUser()
+ global_perms = None
+ retrieve_perms = HasProjectPerm('view_issues')
+ list_perms = HasProjectPerm('view_issues')
diff --git a/taiga/projects/issues/services.py b/taiga/projects/issues/services.py
index 9a553cd3..d1156227 100644
--- a/taiga/projects/issues/services.py
+++ b/taiga/projects/issues/services.py
@@ -27,7 +27,7 @@ from taiga.base.utils import db, text
from taiga.projects.issues.apps import (
connect_issues_signals,
disconnect_issues_signals)
-
+from taiga.projects.votes import services as votes_services
from . import models
@@ -84,7 +84,8 @@ def issues_to_csv(project, queryset):
fieldnames = ["ref", "subject", "description", "milestone", "owner",
"owner_full_name", "assigned_to", "assigned_to_full_name",
"status", "severity", "priority", "type", "is_closed",
- "attachments", "external_reference", "tags"]
+ "attachments", "external_reference", "tags",
+ "watchers", "voters"]
for custom_attr in project.issuecustomattributes.all():
fieldnames.append(custom_attr.name)
@@ -108,6 +109,8 @@ def issues_to_csv(project, queryset):
"attachments": issue.attachments.count(),
"external_reference": issue.external_reference,
"tags": ",".join(issue.tags or []),
+ "watchers": [u.id for u in issue.get_watchers()],
+ "voters": votes_services.get_voters(issue).count(),
}
for custom_attr in project.issuecustomattributes.all():
diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py
index 0a5e583e..6c5a4bcc 100644
--- a/taiga/projects/management/commands/sample_data.py
+++ b/taiga/projects/management/commands/sample_data.py
@@ -37,6 +37,7 @@ from taiga.projects.wiki.models import *
from taiga.projects.attachments.models import *
from taiga.projects.custom_attributes.models import *
from taiga.projects.history.services import take_snapshot
+from taiga.projects.votes.services import add_vote
from taiga.events.apps import disconnect_events_signals
@@ -97,7 +98,8 @@ NUM_TASKS = getattr(settings, "SAMPLE_DATA_NUM_TASKS", (0, 4))
NUM_USS_BACK = getattr(settings, "SAMPLE_DATA_NUM_USS_BACK", (8, 20))
NUM_ISSUES = getattr(settings, "SAMPLE_DATA_NUM_ISSUES", (12, 25))
NUM_ATTACHMENTS = getattr(settings, "SAMPLE_DATA_NUM_ATTACHMENTS", (0, 4))
-
+NUM_VOTES = getattr(settings, "SAMPLE_DATA_NUM_VOTES", (0, 3))
+NUM_PROJECT_WATCHERS = getattr(settings, "SAMPLE_DATA_NUM_PROJECT_WATCHERS", (0, 3))
class Command(BaseCommand):
sd = SampleDataHelper(seed=12345678901)
@@ -215,6 +217,7 @@ class Command(BaseCommand):
project.total_story_points = int(defined_points * self.sd.int(5,12) / 10)
project.save()
+ self.create_votes(project, project)
def create_attachment(self, obj, order):
attached_file = self.sd.file_from_directory(*ATTACHMENT_SAMPLE_DATA)
@@ -287,7 +290,7 @@ class Command(BaseCommand):
bug.save()
watching_user = self.sd.db_object_from_queryset(project.memberships.filter(user__isnull=False)).user
- bug.watchers.add(watching_user)
+ bug.add_watcher(watching_user)
take_snapshot(bug,
comment=self.sd.paragraph(),
@@ -300,6 +303,7 @@ class Command(BaseCommand):
comment=self.sd.paragraph(),
user=bug.owner)
+ self.create_votes(bug, project)
return bug
def create_task(self, project, milestone, us, min_date, max_date, closed=False):
@@ -338,7 +342,7 @@ class Command(BaseCommand):
user=task.owner)
watching_user = self.sd.db_object_from_queryset(project.memberships.filter(user__isnull=False)).user
- task.watchers.add(watching_user)
+ task.add_watcher(watching_user)
# Add history entry
task.status=self.sd.db_object_from_queryset(project.task_statuses.all())
@@ -347,6 +351,7 @@ class Command(BaseCommand):
comment=self.sd.paragraph(),
user=task.owner)
+ self.create_votes(task, project)
return task
def create_us(self, project, milestone=None, computable_project_roles=[]):
@@ -387,7 +392,7 @@ class Command(BaseCommand):
us.save()
watching_user = self.sd.db_object_from_queryset(project.memberships.filter(user__isnull=False)).user
- us.watchers.add(watching_user)
+ us.add_watcher(watching_user)
take_snapshot(us,
comment=self.sd.paragraph(),
@@ -400,6 +405,7 @@ class Command(BaseCommand):
comment=self.sd.paragraph(),
user=us.owner)
+ self.create_votes(us, project)
return us
def create_milestone(self, project, start_date, end_date):
@@ -434,6 +440,11 @@ class Command(BaseCommand):
project.is_kanban_activated = True
project.save()
take_snapshot(project, user=project.owner)
+
+ for i in range(self.sd.int(*NUM_PROJECT_WATCHERS)):
+ watching_user = self.sd.db_object_from_queryset(User.objects.all())
+ project.add_watcher(watching_user)
+
return project
def create_user(self, counter=None, username=None, full_name=None, email=None):
@@ -452,3 +463,8 @@ class Command(BaseCommand):
user.save()
return user
+
+ def create_votes(self, obj, project):
+ for i in range(self.sd.int(*NUM_VOTES)):
+ voting_user=self.sd.db_object_from_queryset(project.members.all())
+ add_vote(obj, voting_user)
diff --git a/taiga/projects/migrations/0024_auto_20150810_1247.py b/taiga/projects/migrations/0024_auto_20150810_1247.py
index ebe758fc..f057816b 100644
--- a/taiga/projects/migrations/0024_auto_20150810_1247.py
+++ b/taiga/projects/migrations/0024_auto_20150810_1247.py
@@ -14,12 +14,6 @@ class Migration(migrations.Migration):
]
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',
diff --git a/taiga/projects/migrations/0025_remove_project_watchers.py b/taiga/projects/migrations/0025_remove_project_watchers.py
deleted file mode 100644
index 5748edc9..00000000
--- a/taiga/projects/migrations/0025_remove_project_watchers.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# -*- 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',
- ),
- ]
diff --git a/taiga/projects/milestones/api.py b/taiga/projects/milestones/api.py
index 93a09831..3194dc39 100644
--- a/taiga/projects/milestones/api.py
+++ b/taiga/projects/milestones/api.py
@@ -17,10 +17,10 @@
from taiga.base import filters
from taiga.base import response
from taiga.base.decorators import detail_route
-from taiga.base.api import ModelCrudViewSet
+from taiga.base.api import ModelCrudViewSet, ModelListViewSet
from taiga.base.api.utils import get_object_or_404
-from taiga.projects.notifications.mixins import WatchedResourceMixin
+from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
from taiga.projects.history.mixins import HistoryResourceMixin
@@ -36,9 +36,11 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudView
permission_classes = (permissions.MilestonePermission,)
filter_backends = (filters.CanViewMilestonesFilterBackend,)
filter_fields = ("project", "closed")
+ queryset = models.Milestone.objects.all()
def get_queryset(self):
- qs = models.Milestone.objects.all()
+ qs = super().get_queryset()
+ qs = self.attach_watchers_attrs_to_queryset(qs)
qs = qs.prefetch_related("user_stories",
"user_stories__role_points",
"user_stories__role_points__points",
@@ -91,3 +93,8 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudView
optimal_points -= optimal_points_per_day
return response.Ok(milestone_stats)
+
+
+class MilestoneWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
+ permission_classes = (permissions.MilestoneWatchersPermission,)
+ resource_model = models.Milestone
diff --git a/taiga/projects/milestones/migrations/0002_remove_milestone_watchers.py b/taiga/projects/milestones/migrations/0002_remove_milestone_watchers.py
index 897f47bf..69d6aacd 100644
--- a/taiga/projects/milestones/migrations/0002_remove_milestone_watchers.py
+++ b/taiga/projects/milestones/migrations/0002_remove_milestone_watchers.py
@@ -1,16 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
+from django.db import connection
from django.db import models, migrations
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.management import update_all_contenttypes
def create_notifications(apps, schema_editor):
- update_all_contenttypes()
- 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)),
+ update_all_contenttypes()
+ sql="""
+INSERT INTO notifications_watched (object_id, created_date, content_type_id, user_id, project_id)
+SELECT milestone_id AS object_id, now() AS created_date, {content_type_id} AS content_type_id, user_id, project_id
+FROM milestones_milestone_watchers INNER JOIN milestones_milestone ON milestones_milestone_watchers.milestone_id = milestones_milestone.id""".format(content_type_id=ContentType.objects.get(model='milestone').id)
+ cursor = connection.cursor()
+ cursor.execute(sql)
class Migration(migrations.Migration):
diff --git a/taiga/projects/milestones/permissions.py b/taiga/projects/milestones/permissions.py
index 9823c8de..843c0c8a 100644
--- a/taiga/projects/milestones/permissions.py
+++ b/taiga/projects/milestones/permissions.py
@@ -15,8 +15,8 @@
# along with this program. If not, see .
from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
- IsProjectOwner, AllowAny,
- PermissionComponent, IsSuperUser)
+ IsAuthenticated, IsProjectOwner, AllowAny,
+ IsSuperUser)
class MilestonePermission(TaigaResourcePermission):
@@ -29,3 +29,11 @@ class MilestonePermission(TaigaResourcePermission):
destroy_perms = HasProjectPerm('delete_milestone')
list_perms = AllowAny()
stats_perms = HasProjectPerm('view_milestones')
+ watch_perms = IsAuthenticated() & HasProjectPerm('view_milestones')
+ unwatch_perms = IsAuthenticated() & HasProjectPerm('view_milestones')
+
+class MilestoneWatchersPermission(TaigaResourcePermission):
+ enought_perms = IsProjectOwner() | IsSuperUser()
+ global_perms = None
+ retrieve_perms = HasProjectPerm('view_milestones')
+ list_perms = HasProjectPerm('view_milestones')
diff --git a/taiga/projects/milestones/serializers.py b/taiga/projects/milestones/serializers.py
index a2a483e5..471b9546 100644
--- a/taiga/projects/milestones/serializers.py
+++ b/taiga/projects/milestones/serializers.py
@@ -17,7 +17,6 @@
from django.utils.translation import ugettext as _
from taiga.base.api import serializers
-
from taiga.base.utils import json
from taiga.projects.notifications.mixins import WatchedResourceModelSerializer
from taiga.projects.notifications.validators import WatchersValidator
diff --git a/taiga/projects/notifications/api.py b/taiga/projects/notifications/api.py
index 2431c7c2..b2f2d260 100644
--- a/taiga/projects/notifications/api.py
+++ b/taiga/projects/notifications/api.py
@@ -19,8 +19,9 @@ from django.db.models import Q
from taiga.base.api import ModelCrudViewSet
from taiga.projects.notifications.choices import NotifyLevel
+from taiga.projects.notifications.models import Watched
from taiga.projects.models import Project
-
+from taiga.users import services as user_services
from . import serializers
from . import models
from . import permissions
@@ -32,9 +33,13 @@ class NotifyPolicyViewSet(ModelCrudViewSet):
permission_classes = (permissions.NotifyPolicyPermission,)
def _build_needed_notify_policies(self):
+ watched_content = user_services.get_watched_content_for_user(self.request.user)
+ watched_content_project_ids = watched_content.values_list("project__id", flat=True).distinct()
+
projects = Project.objects.filter(
Q(owner=self.request.user) |
- Q(memberships__user=self.request.user)
+ Q(memberships__user=self.request.user) |
+ Q(id__in=watched_content_project_ids)
).distinct()
for project in projects:
@@ -45,5 +50,14 @@ class NotifyPolicyViewSet(ModelCrudViewSet):
return models.NotifyPolicy.objects.none()
self._build_needed_notify_policies()
- qs = models.NotifyPolicy.objects.filter(user=self.request.user)
- return qs.distinct()
+
+ # With really want to include the policies related to any content:
+ # - The user is the owner of the project
+ # - The user is member of the project
+ # - The user is watching any object from the project
+ watched_content = user_services.get_watched_content_for_user(self.request.user)
+ watched_content_project_ids = watched_content.values_list("project__id", flat=True).distinct()
+ return models.NotifyPolicy.objects.filter(Q(project__owner=self.request.user) |
+ Q(project__memberships__user=self.request.user) |
+ Q(project__id__in=watched_content_project_ids)
+ ).distinct()
diff --git a/taiga/projects/notifications/migrations/0004_watched.py b/taiga/projects/notifications/migrations/0004_watched.py
index 53c85560..ab0878b7 100644
--- a/taiga/projects/notifications/migrations/0004_watched.py
+++ b/taiga/projects/notifications/migrations/0004_watched.py
@@ -5,10 +5,6 @@ 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 = [
@@ -21,15 +17,22 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Watched',
fields=[
- ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)),
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('object_id', models.PositiveIntegerField()),
('created_date', models.DateTimeField(verbose_name='created date', auto_now_add=True)),
('content_type', models.ForeignKey(to='contenttypes.ContentType')),
('user', models.ForeignKey(related_name='watched', verbose_name='user', to=settings.AUTH_USER_MODEL)),
+ ('project', models.ForeignKey(to='projects.Project', verbose_name='project', related_name='watched')),
+
],
options={
+ 'verbose_name': 'Watched',
+ 'verbose_name_plural': 'Watched',
},
bases=(models.Model,),
),
- migrations.RunPython(fill_watched_table),
+ migrations.AlterUniqueTogether(
+ name='watched',
+ unique_together=set([('content_type', 'object_id', 'user', 'project')]),
+ ),
]
diff --git a/taiga/projects/notifications/mixins.py b/taiga/projects/notifications/mixins.py
index c5ffbb84..75740a3d 100644
--- a/taiga/projects/notifications/mixins.py
+++ b/taiga/projects/notifications/mixins.py
@@ -19,17 +19,21 @@ from operator import is_not
from django.apps import apps
from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.utils.translation import ugettext_lazy as _
from taiga.base import response
from taiga.base.decorators import detail_route
from taiga.base.api import serializers
+from taiga.base.api.utils import get_object_or_404
from taiga.base.fields import WatchersField
from taiga.projects.notifications import services
from taiga.projects.notifications.utils import attach_watchers_to_queryset, attach_is_watched_to_queryset
from taiga.users.models import User
from . import models
+from . serializers import WatcherSerializer
+
class WatchedResourceMixin:
@@ -121,7 +125,6 @@ class WatchedModelMixin(object):
that should works in almost all cases.
"""
return self.project
- t
def get_watchers(self) -> frozenset:
"""
@@ -139,6 +142,9 @@ class WatchedModelMixin(object):
"""
return frozenset(services.get_watchers(self))
+ def get_watched(self, user_or_id):
+ return services.get_watched(user_or_id, type(self))
+
def add_watcher(self, user):
services.add_watcher(self, user)
@@ -209,7 +215,7 @@ class WatchedResourceModelSerializer(serializers.ModelSerializer):
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)
+ obj.watchers = [user.id for user in services.get_watchers(obj)]
return super(WatchedResourceModelSerializer, self).to_native(obj)
diff --git a/taiga/projects/notifications/models.py b/taiga/projects/notifications/models.py
index 753b8878..603bdd85 100644
--- a/taiga/projects/notifications/models.py
+++ b/taiga/projects/notifications/models.py
@@ -22,7 +22,7 @@ from django.utils import timezone
from taiga.projects.history.choices import HISTORY_TYPE_CHOICES
-from .choices import NOTIFY_LEVEL_CHOICES
+from .choices import NOTIFY_LEVEL_CHOICES, NotifyLevel
class NotifyPolicy(models.Model):
@@ -84,8 +84,9 @@ class Watched(models.Model):
related_name="watched", verbose_name=_("user"))
created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False,
verbose_name=_("created date"))
-
+ project = models.ForeignKey("projects.Project", null=False, blank=False,
+ verbose_name=_("project"),related_name="watched")
class Meta:
verbose_name = _("Watched")
verbose_name_plural = _("Watched")
- unique_together = ("content_type", "object_id", "user")
+ unique_together = ("content_type", "object_id", "user", "project")
diff --git a/taiga/projects/notifications/serializers.py b/taiga/projects/notifications/serializers.py
index c60e4bc9..9b0b99cd 100644
--- a/taiga/projects/notifications/serializers.py
+++ b/taiga/projects/notifications/serializers.py
@@ -17,9 +17,10 @@
import json
from taiga.base.api import serializers
+from taiga.users.models import User
from . import models
-
+from . import choices
class NotifyPolicySerializer(serializers.ModelSerializer):
@@ -31,3 +32,11 @@ class NotifyPolicySerializer(serializers.ModelSerializer):
def get_project_name(self, obj):
return obj.project.name
+
+
+class WatcherSerializer(serializers.ModelSerializer):
+ full_name = serializers.CharField(source='get_full_name', required=False)
+
+ class Meta:
+ model = User
+ fields = ('id', 'username', 'full_name')
diff --git a/taiga/projects/notifications/services.py b/taiga/projects/notifications/services.py
index be19a493..adc94a69 100644
--- a/taiga/projects/notifications/services.py
+++ b/taiga/projects/notifications/services.py
@@ -20,6 +20,7 @@ from django.apps import apps
from django.db.transaction import atomic
from django.db import IntegrityError, transaction
from django.contrib.contenttypes.models import ContentType
+from django.contrib.auth import get_user_model
from django.utils import timezone
from django.conf import settings
from django.utils.translation import ugettext as _
@@ -170,15 +171,19 @@ def get_users_to_notify(obj, *, discard_users=None) -> list:
candidates = set()
candidates.update(filter(_can_notify_hard, project.members.all()))
candidates.update(filter(_can_notify_light, obj.get_watchers()))
+ candidates.update(filter(_can_notify_light, obj.project.get_watchers()))
candidates.update(filter(_can_notify_light, obj.get_participants()))
+ #TODO: coger los watchers del proyecto que quieren ser notificados por correo
+ #Filtrar los watchers segĂșn su nivel de watched y su nivel en el proyecto
+
# Remove the changer from candidates
if discard_users:
candidates = candidates - set(discard_users)
- candidates = filter(partial(_filter_by_permissions, obj), candidates)
+ candidates = set(filter(partial(_filter_by_permissions, obj), candidates))
# Filter disabled and system users
- candidates = filter(partial(_filter_notificable), candidates)
+ candidates = set(filter(partial(_filter_notificable), candidates))
return frozenset(candidates)
@@ -285,27 +290,54 @@ def process_sync_notifications():
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)
+ """Get the watchers of an object.
+
+ :param obj: Any Django model instance.
+
+ :return: User queryset object representing the users that voted the object.
+ """
+ obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj)
+ return get_user_model().objects.filter(watched__content_type=obj_type, watched__object_id=obj.id)
+
+
+def get_watched(user_or_id, model):
+ """Get the objects watched by an user.
+
+ :param user_or_id: :class:`~taiga.users.models.User` instance or id.
+ :param model: Show only objects of this kind. Can be any Django model class.
+
+ :return: Queryset of objects representing the votes of the user.
+ """
+ obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model)
+ conditions = ('notifications_watched.content_type_id = %s',
+ '%s.id = notifications_watched.object_id' % model._meta.db_table,
+ 'notifications_watched.user_id = %s')
+
+ if isinstance(user_or_id, get_user_model()):
+ user_id = user_or_id.id
+ else:
+ user_id = user_or_id
+
+ return model.objects.extra(where=conditions, tables=('notifications_watched',),
+ params=(obj_type.id, user_id))
def add_watcher(obj, user):
"""Add a watcher to an object.
- If the user is already watching the object nothing happends, so this function can be considered
- idempotent.
+ If the user is already watching the object nothing happents (except if there is a level update),
+ so this function can be considered idempotent.
:param obj: Any Django model instance.
:param user: User adding the watch. :class:`~taiga.users.models.User` instance.
"""
obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj)
- with atomic():
- watched, created = Watched.objects.get_or_create(content_type=obj_type, object_id=obj.id, user=user)
- if not created:
- return
+ watched, created = Watched.objects.get_or_create(content_type=obj_type,
+ object_id=obj.id, user=user, project=obj.project)
+
+ notify_policy, _ = apps.get_model("notifications", "NotifyPolicy").objects.get_or_create(
+ project=obj.project, user=user, defaults={"notify_level": NotifyLevel.watch})
+
return watched
@@ -319,9 +351,8 @@ def remove_watcher(obj, user):
: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 = Watched.objects.filter(content_type=obj_type, object_id=obj.id, user=user)
+ if not qs.exists():
+ return
- qs.delete()
+ qs.delete()
diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py
index 9cabdc97..1ec6f984 100644
--- a/taiga/projects/permissions.py
+++ b/taiga/projects/permissions.py
@@ -75,6 +75,13 @@ class ProjectFansPermission(TaigaResourcePermission):
list_perms = HasProjectPerm('view_project')
+class ProjectWatchersPermission(TaigaResourcePermission):
+ enought_perms = IsProjectOwner() | IsSuperUser()
+ global_perms = None
+ retrieve_perms = HasProjectPerm('view_project')
+ list_perms = HasProjectPerm('view_project')
+
+
class MembershipPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project')
create_perms = IsProjectOwner()
diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py
index 2e2ad619..812458f1 100644
--- a/taiga/projects/tasks/api.py
+++ b/taiga/projects/tasks/api.py
@@ -24,7 +24,7 @@ from taiga.base.api import ModelCrudViewSet, ModelListViewSet
from taiga.projects.models import Project, TaskStatus
from django.http import HttpResponse
-from taiga.projects.notifications.mixins import WatchedResourceMixin
+from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.occ import OCCResourceMixin
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
@@ -177,3 +177,8 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
class TaskVotersViewSet(VotersViewSetMixin, ModelListViewSet):
permission_classes = (permissions.TaskVotersPermission,)
resource_model = models.Task
+
+
+class TaskWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
+ permission_classes = (permissions.TaskWatchersPermission,)
+ resource_model = models.Task
diff --git a/taiga/projects/tasks/migrations/0008_remove_task_watchers.py b/taiga/projects/tasks/migrations/0008_remove_task_watchers.py
index 813eaad9..4c934957 100644
--- a/taiga/projects/tasks/migrations/0008_remove_task_watchers.py
+++ b/taiga/projects/tasks/migrations/0008_remove_task_watchers.py
@@ -1,16 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
+from django.db import connection
from django.db import models, migrations
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.management import update_all_contenttypes
def create_notifications(apps, schema_editor):
- update_all_contenttypes()
- 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)),
+ update_all_contenttypes()
+ sql="""
+INSERT INTO notifications_watched (object_id, created_date, content_type_id, user_id, project_id)
+SELECT task_id AS object_id, now() AS created_date, {content_type_id} AS content_type_id, user_id, project_id
+FROM tasks_task_watchers INNER JOIN tasks_task ON tasks_task_watchers.task_id = tasks_task.id""".format(content_type_id=ContentType.objects.get(model='task').id)
+ cursor = connection.cursor()
+ cursor.execute(sql)
class Migration(migrations.Migration):
diff --git a/taiga/projects/tasks/permissions.py b/taiga/projects/tasks/permissions.py
index c9cbd667..cf12a283 100644
--- a/taiga/projects/tasks/permissions.py
+++ b/taiga/projects/tasks/permissions.py
@@ -42,3 +42,10 @@ class TaskVotersPermission(TaigaResourcePermission):
global_perms = None
retrieve_perms = HasProjectPerm('view_tasks')
list_perms = HasProjectPerm('view_tasks')
+
+
+class TaskWatchersPermission(TaigaResourcePermission):
+ enought_perms = IsProjectOwner() | IsSuperUser()
+ global_perms = None
+ retrieve_perms = HasProjectPerm('view_tasks')
+ list_perms = HasProjectPerm('view_tasks')
diff --git a/taiga/projects/tasks/services.py b/taiga/projects/tasks/services.py
index 61225aff..8864d893 100644
--- a/taiga/projects/tasks/services.py
+++ b/taiga/projects/tasks/services.py
@@ -23,6 +23,7 @@ from taiga.projects.tasks.apps import (
connect_tasks_signals,
disconnect_tasks_signals)
from taiga.events import events
+from taiga.projects.votes import services as votes_services
from . import models
@@ -95,7 +96,8 @@ def tasks_to_csv(project, queryset):
fieldnames = ["ref", "subject", "description", "user_story", "milestone", "owner",
"owner_full_name", "assigned_to", "assigned_to_full_name",
"status", "is_iocaine", "is_closed", "us_order",
- "taskboard_order", "attachments", "external_reference", "tags"]
+ "taskboard_order", "attachments", "external_reference", "tags",
+ "watchers", "voters"]
for custom_attr in project.taskcustomattributes.all():
fieldnames.append(custom_attr.name)
@@ -120,6 +122,8 @@ def tasks_to_csv(project, queryset):
"attachments": task.attachments.count(),
"external_reference": task.external_reference,
"tags": ",".join(task.tags or []),
+ "watchers": [u.id for u in task.get_watchers()],
+ "voters": votes_services.get_voters(task).count(),
}
for custom_attr in project.taskcustomattributes.all():
value = task.custom_attributes_values.attributes_values.get(str(custom_attr.id), None)
diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py
index 653fdc56..802e3e73 100644
--- a/taiga/projects/userstories/api.py
+++ b/taiga/projects/userstories/api.py
@@ -31,7 +31,7 @@ from taiga.base.decorators import list_route
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
from taiga.base.api.utils import get_object_or_404
-from taiga.projects.notifications.mixins import WatchedResourceMixin
+from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.occ import OCCResourceMixin
from taiga.projects.models import Project, UserStoryStatus
@@ -270,3 +270,8 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
class UserStoryVotersViewSet(VotersViewSetMixin, ModelListViewSet):
permission_classes = (permissions.UserStoryVotersPermission,)
resource_model = models.UserStory
+
+
+class UserStoryWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
+ permission_classes = (permissions.UserStoryWatchersPermission,)
+ resource_model = models.UserStory
diff --git a/taiga/projects/userstories/migrations/0010_remove_userstory_watchers.py b/taiga/projects/userstories/migrations/0010_remove_userstory_watchers.py
index d3e24f62..0d897aca 100644
--- a/taiga/projects/userstories/migrations/0010_remove_userstory_watchers.py
+++ b/taiga/projects/userstories/migrations/0010_remove_userstory_watchers.py
@@ -1,16 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
+from django.db import connection
from django.db import models, migrations
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.management import update_all_contenttypes
def create_notifications(apps, schema_editor):
- update_all_contenttypes()
- 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)),
+ update_all_contenttypes()
+ sql="""
+INSERT INTO notifications_watched (object_id, created_date, content_type_id, user_id, project_id)
+SELECT userstory_id AS object_id, now() AS created_date, {content_type_id} AS content_type_id, user_id, project_id
+FROM userstories_userstory_watchers INNER JOIN userstories_userstory ON userstories_userstory_watchers.userstory_id = userstories_userstory.id""".format(content_type_id=ContentType.objects.get(model='userstory').id)
+ cursor = connection.cursor()
+ cursor.execute(sql)
class Migration(migrations.Migration):
diff --git a/taiga/projects/userstories/permissions.py b/taiga/projects/userstories/permissions.py
index 0a1c7b8a..fb9361ab 100644
--- a/taiga/projects/userstories/permissions.py
+++ b/taiga/projects/userstories/permissions.py
@@ -35,8 +35,16 @@ class UserStoryPermission(TaigaResourcePermission):
watch_perms = IsAuthenticated() & HasProjectPerm('view_us')
unwatch_perms = IsAuthenticated() & HasProjectPerm('view_us')
+
class UserStoryVotersPermission(TaigaResourcePermission):
enought_perms = IsProjectOwner() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_us')
list_perms = HasProjectPerm('view_us')
+
+
+class UserStoryWatchersPermission(TaigaResourcePermission):
+ enought_perms = IsProjectOwner() | IsSuperUser()
+ global_perms = None
+ retrieve_perms = HasProjectPerm('view_us')
+ list_perms = HasProjectPerm('view_us')
diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py
index 9d913707..c06309be 100644
--- a/taiga/projects/userstories/services.py
+++ b/taiga/projects/userstories/services.py
@@ -31,6 +31,7 @@ from taiga.projects.userstories.apps import (
disconnect_userstories_signals)
from taiga.events import events
+from taiga.projects.votes import services as votes_services
from . import models
@@ -138,7 +139,8 @@ def userstories_to_csv(project,queryset):
"created_date", "modified_date", "finish_date",
"client_requirement", "team_requirement", "attachments",
"generated_from_issue", "external_reference", "tasks",
- "tags"]
+ "tags",
+ "watchers", "voters"]
for custom_attr in project.userstorycustomattributes.all():
fieldnames.append(custom_attr.name)
@@ -170,6 +172,8 @@ def userstories_to_csv(project,queryset):
"external_reference": us.external_reference,
"tasks": ",".join([str(task.ref) for task in us.tasks.all()]),
"tags": ",".join(us.tags or []),
+ "watchers": [u.id for u in us.get_watchers()],
+ "voters": votes_services.get_voters(us).count(),
}
for role in us.project.roles.filter(computable=True).order_by('name'):
diff --git a/taiga/projects/wiki/api.py b/taiga/projects/wiki/api.py
index a2e39eda..dba3ccb0 100644
--- a/taiga/projects/wiki/api.py
+++ b/taiga/projects/wiki/api.py
@@ -21,13 +21,13 @@ from taiga.base.api.permissions import IsAuthenticated
from taiga.base import filters
from taiga.base import exceptions as exc
from taiga.base import response
-from taiga.base.api import ModelCrudViewSet
+from taiga.base.api import ModelCrudViewSet, ModelListViewSet
from taiga.base.api.utils import get_object_or_404
from taiga.base.decorators import list_route
from taiga.projects.models import Project
from taiga.mdrender.service import render as mdrender
-from taiga.projects.notifications.mixins import WatchedResourceMixin
+from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.occ import OCCResourceMixin
@@ -43,6 +43,12 @@ class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
permission_classes = (permissions.WikiPagePermission,)
filter_backends = (filters.CanViewWikiPagesFilterBackend,)
filter_fields = ("project", "slug")
+ queryset = models.WikiPage.objects.all()
+
+ def get_queryset(self):
+ qs = super().get_queryset()
+ qs = self.attach_watchers_attrs_to_queryset(qs)
+ return qs
@list_route(methods=["GET"])
def by_slug(self, request):
@@ -77,6 +83,11 @@ class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
super().pre_save(obj)
+class WikiWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
+ permission_classes = (permissions.WikiPageWatchersPermission,)
+ resource_model = models.WikiPage
+
+
class WikiLinkViewSet(ModelCrudViewSet):
model = models.WikiLink
serializer_class = serializers.WikiLinkSerializer
diff --git a/taiga/projects/wiki/migrations/0002_remove_wikipage_watchers.py b/taiga/projects/wiki/migrations/0002_remove_wikipage_watchers.py
index d0c1c832..f2cb8159 100644
--- a/taiga/projects/wiki/migrations/0002_remove_wikipage_watchers.py
+++ b/taiga/projects/wiki/migrations/0002_remove_wikipage_watchers.py
@@ -1,17 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
-from django.apps import apps
+from django.db import connection
from django.db import models, migrations
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.management import update_all_contenttypes
def create_notifications(apps, schema_editor):
- update_all_contenttypes()
- 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)),
+ update_all_contenttypes()
+ sql="""
+INSERT INTO notifications_watched (object_id, created_date, content_type_id, user_id, project_id)
+SELECT wikipage_id AS object_id, now() AS created_date, {content_type_id} AS content_type_id, user_id, project_id
+FROM wiki_wikipage_watchers INNER JOIN wiki_wikipage ON wiki_wikipage_watchers.wikipage_id = wiki_wikipage.id""".format(content_type_id=ContentType.objects.get(model='wikipage').id)
+ cursor = connection.cursor()
+ cursor.execute(sql)
class Migration(migrations.Migration):
diff --git a/taiga/projects/wiki/permissions.py b/taiga/projects/wiki/permissions.py
index 684880a8..c64ac985 100644
--- a/taiga/projects/wiki/permissions.py
+++ b/taiga/projects/wiki/permissions.py
@@ -15,7 +15,8 @@
# along with this program. If not, see .
from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
- IsProjectOwner, AllowAny, IsSuperUser)
+ IsAuthenticated, IsProjectOwner, AllowAny,
+ IsSuperUser)
class WikiPagePermission(TaigaResourcePermission):
@@ -29,6 +30,16 @@ class WikiPagePermission(TaigaResourcePermission):
destroy_perms = HasProjectPerm('delete_wiki_page')
list_perms = AllowAny()
render_perms = AllowAny()
+ watch_perms = IsAuthenticated() & HasProjectPerm('view_wiki_pages')
+ unwatch_perms = IsAuthenticated() & HasProjectPerm('view_wiki_pages')
+
+
+class WikiPageWatchersPermission(TaigaResourcePermission):
+ enought_perms = IsProjectOwner() | IsSuperUser()
+ global_perms = None
+ retrieve_perms = HasProjectPerm('view_wiki_pages')
+ list_perms = HasProjectPerm('view_wiki_pages')
+
class WikiLinkPermission(TaigaResourcePermission):
enought_perms = IsProjectOwner() | IsSuperUser()
diff --git a/taiga/projects/wiki/serializers.py b/taiga/projects/wiki/serializers.py
index a528fdd8..22c9b8bd 100644
--- a/taiga/projects/wiki/serializers.py
+++ b/taiga/projects/wiki/serializers.py
@@ -22,10 +22,6 @@ from taiga.mdrender.service import render as mdrender
from . import models
-from taiga.projects.history import services as history_service
-
-from taiga.mdrender.service import render as mdrender
-
class WikiPageSerializer(WatchersValidator, WatchedResourceModelSerializer, serializers.ModelSerializer):
html = serializers.SerializerMethodField("get_html")
diff --git a/taiga/routers.py b/taiga/routers.py
index ff7ceff0..5a587b59 100644
--- a/taiga/routers.py
+++ b/taiga/routers.py
@@ -49,6 +49,7 @@ router.register(r"notify-policies", NotifyPolicyViewSet, base_name="notification
# Projects & Selectors
from taiga.projects.api import ProjectViewSet
from taiga.projects.api import ProjectFansViewSet
+from taiga.projects.api import ProjectWatchersViewSet
from taiga.projects.api import MembershipViewSet
from taiga.projects.api import InvitationViewSet
from taiga.projects.api import UserStoryStatusViewSet
@@ -62,6 +63,7 @@ from taiga.projects.api import ProjectTemplateViewSet
router.register(r"projects", ProjectViewSet, base_name="projects")
router.register(r"projects/(?P\d+)/fans", ProjectFansViewSet, base_name="project-fans")
+router.register(r"projects/(?P\d+)/watchers", ProjectWatchersViewSet, base_name="project-watchers")
router.register(r"project-templates", ProjectTemplateViewSet, base_name="project-templates")
router.register(r"memberships", MembershipViewSet, base_name="memberships")
router.register(r"invitations", InvitationViewSet, base_name="invitations")
@@ -124,22 +126,33 @@ router.register(r"wiki/attachments", WikiAttachmentViewSet, base_name="wiki-atta
# Project components
from taiga.projects.milestones.api import MilestoneViewSet
+from taiga.projects.milestones.api import MilestoneWatchersViewSet
from taiga.projects.userstories.api import UserStoryViewSet
from taiga.projects.userstories.api import UserStoryVotersViewSet
+from taiga.projects.userstories.api import UserStoryWatchersViewSet
from taiga.projects.tasks.api import TaskViewSet
from taiga.projects.tasks.api import TaskVotersViewSet
+from taiga.projects.tasks.api import TaskWatchersViewSet
from taiga.projects.issues.api import IssueViewSet
from taiga.projects.issues.api import IssueVotersViewSet
-from taiga.projects.wiki.api import WikiViewSet, WikiLinkViewSet
+from taiga.projects.issues.api import IssueWatchersViewSet
+from taiga.projects.wiki.api import WikiViewSet
+from taiga.projects.wiki.api import WikiLinkViewSet
+from taiga.projects.wiki.api import WikiWatchersViewSet
router.register(r"milestones", MilestoneViewSet, base_name="milestones")
+router.register(r"milestones/(?P\d+)/watchers", MilestoneWatchersViewSet, base_name="milestone-watchers")
router.register(r"userstories", UserStoryViewSet, base_name="userstories")
router.register(r"userstories/(?P\d+)/voters", UserStoryVotersViewSet, base_name="userstory-voters")
+router.register(r"userstories/(?P\d+)/watchers", UserStoryWatchersViewSet, base_name="userstory-watchers")
router.register(r"tasks", TaskViewSet, base_name="tasks")
router.register(r"tasks/(?P\d+)/voters", TaskVotersViewSet, base_name="task-voters")
+router.register(r"tasks/(?P\d+)/watchers", TaskWatchersViewSet, base_name="task-watchers")
router.register(r"issues", IssueViewSet, base_name="issues")
router.register(r"issues/(?P\d+)/voters", IssueVotersViewSet, base_name="issue-voters")
+router.register(r"issues/(?P\d+)/watchers", IssueWatchersViewSet, base_name="issue-watchers")
router.register(r"wiki", WikiViewSet, base_name="wiki")
+router.register(r"wiki/(?P\d+)/watchers", WikiWatchersViewSet, base_name="wiki-watchers")
router.register(r"wiki-links", WikiLinkViewSet, base_name="wiki-links")
diff --git a/taiga/timeline/signals.py b/taiga/timeline/signals.py
index d56db4ac..5673fbbb 100644
--- a/taiga/timeline/signals.py
+++ b/taiga/timeline/signals.py
@@ -22,14 +22,12 @@ from taiga.projects.history import services as history_services
from taiga.projects.models import Project
from taiga.users.models import User
from taiga.projects.history.choices import HistoryType
+from taiga.projects.notifications import services as notifications_services
from taiga.timeline.service import (push_to_timeline,
build_user_namespace,
build_project_namespace,
extract_user_info)
-# TODO: Add events to followers timeline when followers are implemented.
-# TODO: Add events to project watchers timeline when project watchers are implemented.
-
def _push_to_timeline(*args, **kwargs):
if settings.CELERY_ENABLED:
@@ -60,9 +58,9 @@ def _push_to_timelines(project, user, obj, event_type, created_datetime, extra_d
related_people |= User.objects.filter(id=obj.assigned_to_id)
## - Watchers
- watchers = getattr(obj, "watchers", None)
+ watchers = notifications_services.get_watchers(obj)
if watchers:
- related_people |= obj.get_watchers()
+ related_people |= watchers
## - Exclude inactive and system users and remove duplicate
related_people = related_people.exclude(is_active=False)
diff --git a/tests/factories.py b/tests/factories.py
index 8a351d49..a0950733 100644
--- a/tests/factories.py
+++ b/tests/factories.py
@@ -441,6 +441,17 @@ class VotesFactory(Factory):
object_id = factory.Sequence(lambda n: n)
+class WatchedFactory(Factory):
+ class Meta:
+ model = "notifications.Watched"
+ strategy = factory.CREATE_STRATEGY
+
+ content_type = factory.SubFactory("tests.factories.ContentTypeFactory")
+ object_id = factory.Sequence(lambda n: n)
+ user = factory.SubFactory("tests.factories.UserFactory")
+ project = factory.SubFactory("tests.factories.ProjectFactory")
+
+
class ContentTypeFactory(Factory):
class Meta:
model = "contenttypes.ContentType"
diff --git a/tests/integration/resources_permissions/test_issues_resources.py b/tests/integration/resources_permissions/test_issues_resources.py
index d9950e00..469efacc 100644
--- a/tests/integration/resources_permissions/test_issues_resources.py
+++ b/tests/integration/resources_permissions/test_issues_resources.py
@@ -9,6 +9,7 @@ from taiga.base.utils import json
from tests import factories as f
from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals
from taiga.projects.votes.services import add_vote
+from taiga.projects.notifications.services import add_watcher
from taiga.projects.occ import OCCResourceMixin
from unittest import mock
@@ -616,3 +617,51 @@ def test_issue_action_unwatch(client, data):
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
+
+
+def test_issue_watchers_list(client, data):
+ public_url = reverse('issue-watchers-list', kwargs={"resource_id": data.public_issue.pk})
+ private_url1 = reverse('issue-watchers-list', kwargs={"resource_id": data.private_issue1.pk})
+ private_url2 = reverse('issue-watchers-list', kwargs={"resource_id": data.private_issue2.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'get', public_url, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url1, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url2, None, users)
+ assert results == [401, 403, 403, 200, 200]
+
+
+def test_issue_watchers_retrieve(client, data):
+ add_watcher(data.public_issue, data.project_owner)
+ public_url = reverse('issue-watchers-detail', kwargs={"resource_id": data.public_issue.pk,
+ "pk": data.project_owner.pk})
+ add_watcher(data.private_issue1, data.project_owner)
+ private_url1 = reverse('issue-watchers-detail', kwargs={"resource_id": data.private_issue1.pk,
+ "pk": data.project_owner.pk})
+ add_watcher(data.private_issue2, data.project_owner)
+ private_url2 = reverse('issue-watchers-detail', kwargs={"resource_id": data.private_issue2.pk,
+ "pk": data.project_owner.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'get', public_url, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url1, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url2, None, users)
+ assert results == [401, 403, 403, 200, 200]
diff --git a/tests/integration/resources_permissions/test_milestones_resources.py b/tests/integration/resources_permissions/test_milestones_resources.py
index 955754f9..40a8c008 100644
--- a/tests/integration/resources_permissions/test_milestones_resources.py
+++ b/tests/integration/resources_permissions/test_milestones_resources.py
@@ -3,6 +3,7 @@ from django.core.urlresolvers import reverse
from taiga.base.utils import json
from taiga.projects.milestones.serializers import MilestoneSerializer
from taiga.projects.milestones.models import Milestone
+from taiga.projects.notifications.services import add_watcher
from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
from tests import factories as f
@@ -274,3 +275,93 @@ def test_milestone_action_stats(client, data):
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]
+
+
+def test_milestone_action_watch(client, data):
+ public_url = reverse('milestones-watch', kwargs={"pk": data.public_milestone.pk})
+ private_url1 = reverse('milestones-watch', kwargs={"pk": data.private_milestone1.pk})
+ private_url2 = reverse('milestones-watch', kwargs={"pk": data.private_milestone2.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'post', public_url, "", users)
+ assert results == [401, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'post', private_url1, "", users)
+ assert results == [401, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'post', private_url2, "", users)
+ assert results == [404, 404, 404, 200, 200]
+
+
+def test_milestone_action_unwatch(client, data):
+ public_url = reverse('milestones-unwatch', kwargs={"pk": data.public_milestone.pk})
+ private_url1 = reverse('milestones-unwatch', kwargs={"pk": data.private_milestone1.pk})
+ private_url2 = reverse('milestones-unwatch', kwargs={"pk": data.private_milestone2.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'post', public_url, "", users)
+ assert results == [401, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'post', private_url1, "", users)
+ assert results == [401, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'post', private_url2, "", users)
+ assert results == [404, 404, 404, 200, 200]
+
+
+def test_milestone_watchers_list(client, data):
+ public_url = reverse('milestone-watchers-list', kwargs={"resource_id": data.public_milestone.pk})
+ private_url1 = reverse('milestone-watchers-list', kwargs={"resource_id": data.private_milestone1.pk})
+ private_url2 = reverse('milestone-watchers-list', kwargs={"resource_id": data.private_milestone2.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'get', public_url, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url1, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url2, None, users)
+ assert results == [401, 403, 403, 200, 200]
+
+
+def test_milestone_watchers_retrieve(client, data):
+ add_watcher(data.public_milestone, data.project_owner)
+ public_url = reverse('milestone-watchers-detail', kwargs={"resource_id": data.public_milestone.pk,
+ "pk": data.project_owner.pk})
+ add_watcher(data.private_milestone1, data.project_owner)
+ private_url1 = reverse('milestone-watchers-detail', kwargs={"resource_id": data.private_milestone1.pk,
+ "pk": data.project_owner.pk})
+ add_watcher(data.private_milestone2, data.project_owner)
+ private_url2 = reverse('milestone-watchers-detail', kwargs={"resource_id": data.private_milestone2.pk,
+ "pk": data.project_owner.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'get', public_url, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url1, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url2, None, users)
+ assert results == [401, 403, 403, 200, 200]
diff --git a/tests/integration/resources_permissions/test_projects_resource.py b/tests/integration/resources_permissions/test_projects_resource.py
index 888b1ef4..27c08d1f 100644
--- a/tests/integration/resources_permissions/test_projects_resource.py
+++ b/tests/integration/resources_permissions/test_projects_resource.py
@@ -81,6 +81,13 @@ def data():
f.VotesFactory(content_type=project_ct, object_id=m.private_project1.pk, count=2)
f.VotesFactory(content_type=project_ct, object_id=m.private_project2.pk, count=2)
+ f.WatchedFactory(content_type=project_ct, object_id=m.public_project.pk, user=m.project_member_with_perms)
+ f.WatchedFactory(content_type=project_ct, object_id=m.public_project.pk, user=m.project_owner)
+ f.WatchedFactory(content_type=project_ct, object_id=m.private_project1.pk, user=m.project_member_with_perms)
+ f.WatchedFactory(content_type=project_ct, object_id=m.private_project1.pk, user=m.project_owner)
+ f.WatchedFactory(content_type=project_ct, object_id=m.private_project2.pk, user=m.project_member_with_perms)
+ f.WatchedFactory(content_type=project_ct, object_id=m.private_project2.pk, user=m.project_owner)
+
return m
@@ -109,6 +116,7 @@ def test_project_update(client, data):
project_data = ProjectDetailSerializer(data.private_project2).data
project_data["is_private"] = False
+
project_data = json.dumps(project_data)
users = [
@@ -300,6 +308,51 @@ def test_project_fans_retrieve(client, data):
assert results == [401, 403, 403, 200, 200]
+def test_project_watchers_list(client, data):
+ public_url = reverse('project-watchers-list', kwargs={"resource_id": data.public_project.pk})
+ private1_url = reverse('project-watchers-list', kwargs={"resource_id": data.private_project1.pk})
+ private2_url = reverse('project-watchers-list', kwargs={"resource_id": data.private_project2.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method_and_count(client, 'get', public_url, None, users)
+ assert results == [(200, 2), (200, 2), (200, 2), (200, 2), (200, 2)]
+ results = helper_test_http_method_and_count(client, 'get', private1_url, None, users)
+ assert results == [(200, 2), (200, 2), (200, 2), (200, 2), (200, 2)]
+ results = helper_test_http_method_and_count(client, 'get', private2_url, None, users)
+ assert results == [(401, 0), (403, 0), (403, 0), (200, 2), (200, 2)]
+
+
+def test_project_watchers_retrieve(client, data):
+ public_url = reverse('project-watchers-detail', kwargs={"resource_id": data.public_project.pk,
+ "pk": data.project_owner.pk})
+ private1_url = reverse('project-watchers-detail', kwargs={"resource_id": data.private_project1.pk,
+ "pk": data.project_owner.pk})
+ private2_url = reverse('project-watchers-detail', kwargs={"resource_id": data.private_project2.pk,
+ "pk": data.project_owner.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'get', public_url, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private1_url, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private2_url, None, users)
+ assert results == [401, 403, 403, 200, 200]
+
+
def test_project_action_create_template(client, data):
public_url = reverse('projects-create-template', kwargs={"pk": data.public_project.pk})
private1_url = reverse('projects-create-template', kwargs={"pk": data.private_project1.pk})
diff --git a/tests/integration/resources_permissions/test_tasks_resources.py b/tests/integration/resources_permissions/test_tasks_resources.py
index bd4d0e09..4a871e8e 100644
--- a/tests/integration/resources_permissions/test_tasks_resources.py
+++ b/tests/integration/resources_permissions/test_tasks_resources.py
@@ -10,6 +10,7 @@ from taiga.projects.occ import OCCResourceMixin
from tests import factories as f
from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals
from taiga.projects.votes.services import add_vote
+from taiga.projects.notifications.services import add_watcher
from unittest import mock
@@ -571,3 +572,51 @@ def test_task_action_unwatch(client, data):
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
+
+
+def test_task_watchers_list(client, data):
+ public_url = reverse('task-watchers-list', kwargs={"resource_id": data.public_task.pk})
+ private_url1 = reverse('task-watchers-list', kwargs={"resource_id": data.private_task1.pk})
+ private_url2 = reverse('task-watchers-list', kwargs={"resource_id": data.private_task2.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'get', public_url, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url1, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url2, None, users)
+ assert results == [401, 403, 403, 200, 200]
+
+
+def test_task_watchers_retrieve(client, data):
+ add_watcher(data.public_task, data.project_owner)
+ public_url = reverse('task-watchers-detail', kwargs={"resource_id": data.public_task.pk,
+ "pk": data.project_owner.pk})
+ add_watcher(data.private_task1, data.project_owner)
+ private_url1 = reverse('task-watchers-detail', kwargs={"resource_id": data.private_task1.pk,
+ "pk": data.project_owner.pk})
+ add_watcher(data.private_task2, data.project_owner)
+ private_url2 = reverse('task-watchers-detail', kwargs={"resource_id": data.private_task2.pk,
+ "pk": data.project_owner.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'get', public_url, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url1, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url2, None, users)
+ assert results == [401, 403, 403, 200, 200]
diff --git a/tests/integration/resources_permissions/test_userstories_resources.py b/tests/integration/resources_permissions/test_userstories_resources.py
index 219c4e2a..20881aed 100644
--- a/tests/integration/resources_permissions/test_userstories_resources.py
+++ b/tests/integration/resources_permissions/test_userstories_resources.py
@@ -10,6 +10,7 @@ from taiga.projects.occ import OCCResourceMixin
from tests import factories as f
from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals
from taiga.projects.votes.services import add_vote
+from taiga.projects.notifications.services import add_watcher
from unittest import mock
@@ -570,3 +571,51 @@ def test_user_story_action_unwatch(client, data):
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
+
+
+def test_userstory_watchers_list(client, data):
+ public_url = reverse('userstory-watchers-list', kwargs={"resource_id": data.public_user_story.pk})
+ private_url1 = reverse('userstory-watchers-list', kwargs={"resource_id": data.private_user_story1.pk})
+ private_url2 = reverse('userstory-watchers-list', kwargs={"resource_id": data.private_user_story2.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'get', public_url, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url1, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url2, None, users)
+ assert results == [401, 403, 403, 200, 200]
+
+
+def test_userstory_watchers_retrieve(client, data):
+ add_watcher(data.public_user_story, data.project_owner)
+ public_url = reverse('userstory-watchers-detail', kwargs={"resource_id": data.public_user_story.pk,
+ "pk": data.project_owner.pk})
+ add_watcher(data.private_user_story1, data.project_owner)
+ private_url1 = reverse('userstory-watchers-detail', kwargs={"resource_id": data.private_user_story1.pk,
+ "pk": data.project_owner.pk})
+ add_watcher(data.private_user_story2, data.project_owner)
+ private_url2 = reverse('userstory-watchers-detail', kwargs={"resource_id": data.private_user_story2.pk,
+ "pk": data.project_owner.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'get', public_url, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url1, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url2, None, users)
+ assert results == [401, 403, 403, 200, 200]
diff --git a/tests/integration/resources_permissions/test_wiki_resources.py b/tests/integration/resources_permissions/test_wiki_resources.py
index cf6089b7..14f2f92b 100644
--- a/tests/integration/resources_permissions/test_wiki_resources.py
+++ b/tests/integration/resources_permissions/test_wiki_resources.py
@@ -1,10 +1,11 @@
from django.core.urlresolvers import reverse
from taiga.base.utils import json
+from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
+from taiga.projects.notifications.services import add_watcher
+from taiga.projects.occ import OCCResourceMixin
from taiga.projects.wiki.serializers import WikiPageSerializer, WikiLinkSerializer
from taiga.projects.wiki.models import WikiPage, WikiLink
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
-from taiga.projects.occ import OCCResourceMixin
from tests import factories as f
from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals
@@ -436,3 +437,93 @@ def test_wiki_link_patch(client, data):
patch_data = json.dumps({"title": "test"})
results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
assert results == [401, 403, 403, 200, 200]
+
+
+def test_wikipage_action_watch(client, data):
+ public_url = reverse('wiki-watch', kwargs={"pk": data.public_wiki_page.pk})
+ private_url1 = reverse('wiki-watch', kwargs={"pk": data.private_wiki_page1.pk})
+ private_url2 = reverse('wiki-watch', kwargs={"pk": data.private_wiki_page2.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'post', public_url, "", users)
+ assert results == [401, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'post', private_url1, "", users)
+ assert results == [401, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'post', private_url2, "", users)
+ assert results == [404, 404, 404, 200, 200]
+
+
+def test_wikipage_action_unwatch(client, data):
+ public_url = reverse('wiki-unwatch', kwargs={"pk": data.public_wiki_page.pk})
+ private_url1 = reverse('wiki-unwatch', kwargs={"pk": data.private_wiki_page1.pk})
+ private_url2 = reverse('wiki-unwatch', kwargs={"pk": data.private_wiki_page2.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'post', public_url, "", users)
+ assert results == [401, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'post', private_url1, "", users)
+ assert results == [401, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'post', private_url2, "", users)
+ assert results == [404, 404, 404, 200, 200]
+
+
+def test_wikipage_watchers_list(client, data):
+ public_url = reverse('wiki-watchers-list', kwargs={"resource_id": data.public_wiki_page.pk})
+ private_url1 = reverse('wiki-watchers-list', kwargs={"resource_id": data.private_wiki_page1.pk})
+ private_url2 = reverse('wiki-watchers-list', kwargs={"resource_id": data.private_wiki_page2.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'get', public_url, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url1, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url2, None, users)
+ assert results == [401, 403, 403, 200, 200]
+
+
+def test_wikipage_watchers_retrieve(client, data):
+ add_watcher(data.public_wiki_page, data.project_owner)
+ public_url = reverse('wiki-watchers-detail', kwargs={"resource_id": data.public_wiki_page.pk,
+ "pk": data.project_owner.pk})
+ add_watcher(data.private_wiki_page1, data.project_owner)
+ private_url1 = reverse('wiki-watchers-detail', kwargs={"resource_id": data.private_wiki_page1.pk,
+ "pk": data.project_owner.pk})
+ add_watcher(data.private_wiki_page2, data.project_owner)
+ private_url2 = reverse('wiki-watchers-detail', kwargs={"resource_id": data.private_wiki_page2.pk,
+ "pk": data.project_owner.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'get', public_url, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url1, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url2, None, users)
+ assert results == [401, 403, 403, 200, 200]
diff --git a/tests/integration/test_issues.py b/tests/integration/test_issues.py
index 84727427..54722c93 100644
--- a/tests/integration/test_issues.py
+++ b/tests/integration/test_issues.py
@@ -412,6 +412,6 @@ def test_custom_fields_csv_generation():
data.seek(0)
reader = csv.reader(data)
row = next(reader)
- assert row[16] == attr.name
+ assert row[18] == attr.name
row = next(reader)
- assert row[16] == "val1"
+ assert row[18] == "val1"
diff --git a/tests/integration/test_notifications.py b/tests/integration/test_notifications.py
index ab0fdb42..87ea400d 100644
--- a/tests/integration/test_notifications.py
+++ b/tests/integration/test_notifications.py
@@ -211,6 +211,124 @@ def test_users_to_notify():
assert users == {member1.user, issue.get_owner()}
+def test_watching_users_to_notify_on_issue_modification_1():
+ # If:
+ # - the user is watching the issue
+ # - the user is not watching the project
+ # - the notify policy is watch
+ # Then:
+ # - email is sent
+ project = f.ProjectFactory.create(anon_permissions= ["view_issues"])
+ issue = f.IssueFactory.create(project=project)
+ watching_user = f.UserFactory()
+ issue.add_watcher(watching_user)
+ watching_user_policy = services.get_notify_policy(project, watching_user)
+ issue.description = "test1"
+ issue.save()
+ watching_user_policy.notify_level = NotifyLevel.watch
+ users = services.get_users_to_notify(issue)
+ assert users == {watching_user, issue.owner}
+
+
+def test_watching_users_to_notify_on_issue_modification_2():
+ # If:
+ # - the user is watching the issue
+ # - the user is not watching the project
+ # - the notify policy is notwatch
+ # Then:
+ # - email is sent
+ project = f.ProjectFactory.create(anon_permissions= ["view_issues"])
+ issue = f.IssueFactory.create(project=project)
+ watching_user = f.UserFactory()
+ issue.add_watcher(watching_user)
+ watching_user_policy = services.get_notify_policy(project, watching_user)
+ issue.description = "test1"
+ issue.save()
+ watching_user_policy.notify_level = NotifyLevel.notwatch
+ users = services.get_users_to_notify(issue)
+ assert users == {watching_user, issue.owner}
+
+
+def test_watching_users_to_notify_on_issue_modification_3():
+ # If:
+ # - the user is watching the issue
+ # - the user is not watching the project
+ # - the notify policy is ignore
+ # Then:
+ # - email is not sent
+ project = f.ProjectFactory.create(anon_permissions= ["view_issues"])
+ issue = f.IssueFactory.create(project=project)
+ watching_user = f.UserFactory()
+ issue.add_watcher(watching_user)
+ watching_user_policy = services.get_notify_policy(project, watching_user)
+ issue.description = "test1"
+ issue.save()
+ watching_user_policy.notify_level = NotifyLevel.ignore
+ watching_user_policy.save()
+ users = services.get_users_to_notify(issue)
+ assert users == {issue.owner}
+
+
+def test_watching_users_to_notify_on_issue_modification_4():
+ # If:
+ # - the user is not watching the issue
+ # - the user is watching the project
+ # - the notify policy is ignore
+ # Then:
+ # - email is not sent
+ project = f.ProjectFactory.create(anon_permissions= ["view_issues"])
+ issue = f.IssueFactory.create(project=project)
+ watching_user = f.UserFactory()
+ project.add_watcher(watching_user)
+ watching_user_policy = services.get_notify_policy(project, watching_user)
+ issue.description = "test1"
+ issue.save()
+ watching_user_policy.notify_level = NotifyLevel.ignore
+ watching_user_policy.save()
+ users = services.get_users_to_notify(issue)
+ assert users == {issue.owner}
+
+
+def test_watching_users_to_notify_on_issue_modification_5():
+ # If:
+ # - the user is not watching the issue
+ # - the user is watching the project
+ # - the notify policy is watch
+ # Then:
+ # - email is sent
+ project = f.ProjectFactory.create(anon_permissions= ["view_issues"])
+ issue = f.IssueFactory.create(project=project)
+ watching_user = f.UserFactory()
+ project.add_watcher(watching_user)
+ watching_user_policy = services.get_notify_policy(project, watching_user)
+ issue.description = "test1"
+ issue.save()
+ watching_user_policy.notify_level = NotifyLevel.watch
+ watching_user_policy.save()
+ users = services.get_users_to_notify(issue)
+ assert users == {watching_user, issue.owner}
+
+
+def test_watching_users_to_notify_on_issue_modification_6():
+ # If:
+ # - the user is not watching the issue
+ # - the user is watching the project
+ # - the notify policy is notwatch
+ # Then:
+ # - email is sent
+ project = f.ProjectFactory.create(anon_permissions= ["view_issues"])
+ issue = f.IssueFactory.create(project=project)
+ watching_user = f.UserFactory()
+ project.add_watcher(watching_user)
+ watching_user_policy = services.get_notify_policy(project, watching_user)
+ issue.description = "test1"
+ issue.save()
+ watching_user_policy.notify_level = NotifyLevel.notwatch
+ watching_user_policy.save()
+ users = services.get_users_to_notify(issue)
+ assert users == {watching_user, issue.owner}
+
+
def test_send_notifications_using_services_method(settings, mail):
settings.CHANGE_NOTIFICATIONS_MIN_INTERVAL = 1
diff --git a/tests/integration/test_tasks.py b/tests/integration/test_tasks.py
index 382bfc6f..08825955 100644
--- a/tests/integration/test_tasks.py
+++ b/tests/integration/test_tasks.py
@@ -163,6 +163,6 @@ def test_custom_fields_csv_generation():
data.seek(0)
reader = csv.reader(data)
row = next(reader)
- assert row[17] == attr.name
+ assert row[19] == attr.name
row = next(reader)
- assert row[17] == "val1"
+ assert row[19] == "val1"
diff --git a/tests/integration/test_timeline.py b/tests/integration/test_timeline.py
index 00e61fc3..4620e881 100644
--- a/tests/integration/test_timeline.py
+++ b/tests/integration/test_timeline.py
@@ -196,8 +196,10 @@ def test_create_membership_timeline():
def test_update_project_timeline():
+ user_watcher= factories.UserFactory()
project = factories.ProjectFactory.create(name="test project timeline")
history_services.take_snapshot(project, user=project.owner)
+ project.add_watcher(user_watcher)
project.name = "test project timeline updated"
project.save()
history_services.take_snapshot(project, user=project.owner)
@@ -206,11 +208,18 @@ def test_update_project_timeline():
assert project_timeline[0].data["project"]["name"] == "test project timeline updated"
assert project_timeline[0].data["values_diff"]["name"][0] == "test project timeline"
assert project_timeline[0].data["values_diff"]["name"][1] == "test project timeline updated"
+ user_watcher_timeline = service.get_profile_timeline(user_watcher)
+ assert user_watcher_timeline[0].event_type == "projects.project.change"
+ assert user_watcher_timeline[0].data["project"]["name"] == "test project timeline updated"
+ assert user_watcher_timeline[0].data["values_diff"]["name"][0] == "test project timeline"
+ assert user_watcher_timeline[0].data["values_diff"]["name"][1] == "test project timeline updated"
def test_update_milestone_timeline():
+ user_watcher= factories.UserFactory()
milestone = factories.MilestoneFactory.create(name="test milestone timeline")
history_services.take_snapshot(milestone, user=milestone.owner)
+ milestone.add_watcher(user_watcher)
milestone.name = "test milestone timeline updated"
milestone.save()
history_services.take_snapshot(milestone, user=milestone.owner)
@@ -219,11 +228,18 @@ def test_update_milestone_timeline():
assert project_timeline[0].data["milestone"]["name"] == "test milestone timeline updated"
assert project_timeline[0].data["values_diff"]["name"][0] == "test milestone timeline"
assert project_timeline[0].data["values_diff"]["name"][1] == "test milestone timeline updated"
+ user_watcher_timeline = service.get_profile_timeline(user_watcher)
+ assert user_watcher_timeline[0].event_type == "milestones.milestone.change"
+ assert user_watcher_timeline[0].data["milestone"]["name"] == "test milestone timeline updated"
+ assert user_watcher_timeline[0].data["values_diff"]["name"][0] == "test milestone timeline"
+ assert user_watcher_timeline[0].data["values_diff"]["name"][1] == "test milestone timeline updated"
def test_update_user_story_timeline():
+ user_watcher= factories.UserFactory()
user_story = factories.UserStoryFactory.create(subject="test us timeline")
history_services.take_snapshot(user_story, user=user_story.owner)
+ user_story.add_watcher(user_watcher)
user_story.subject = "test us timeline updated"
user_story.save()
history_services.take_snapshot(user_story, user=user_story.owner)
@@ -232,11 +248,18 @@ def test_update_user_story_timeline():
assert project_timeline[0].data["userstory"]["subject"] == "test us timeline updated"
assert project_timeline[0].data["values_diff"]["subject"][0] == "test us timeline"
assert project_timeline[0].data["values_diff"]["subject"][1] == "test us timeline updated"
+ user_watcher_timeline = service.get_profile_timeline(user_watcher)
+ assert user_watcher_timeline[0].event_type == "userstories.userstory.change"
+ assert user_watcher_timeline[0].data["userstory"]["subject"] == "test us timeline updated"
+ assert user_watcher_timeline[0].data["values_diff"]["subject"][0] == "test us timeline"
+ assert user_watcher_timeline[0].data["values_diff"]["subject"][1] == "test us timeline updated"
def test_update_issue_timeline():
+ user_watcher= factories.UserFactory()
issue = factories.IssueFactory.create(subject="test issue timeline")
history_services.take_snapshot(issue, user=issue.owner)
+ issue.add_watcher(user_watcher)
issue.subject = "test issue timeline updated"
issue.save()
history_services.take_snapshot(issue, user=issue.owner)
@@ -245,11 +268,18 @@ def test_update_issue_timeline():
assert project_timeline[0].data["issue"]["subject"] == "test issue timeline updated"
assert project_timeline[0].data["values_diff"]["subject"][0] == "test issue timeline"
assert project_timeline[0].data["values_diff"]["subject"][1] == "test issue timeline updated"
+ user_watcher_timeline = service.get_profile_timeline(user_watcher)
+ assert user_watcher_timeline[0].event_type == "issues.issue.change"
+ assert user_watcher_timeline[0].data["issue"]["subject"] == "test issue timeline updated"
+ assert user_watcher_timeline[0].data["values_diff"]["subject"][0] == "test issue timeline"
+ assert user_watcher_timeline[0].data["values_diff"]["subject"][1] == "test issue timeline updated"
def test_update_task_timeline():
+ user_watcher= factories.UserFactory()
task = factories.TaskFactory.create(subject="test task timeline")
history_services.take_snapshot(task, user=task.owner)
+ task.add_watcher(user_watcher)
task.subject = "test task timeline updated"
task.save()
history_services.take_snapshot(task, user=task.owner)
@@ -258,11 +288,18 @@ def test_update_task_timeline():
assert project_timeline[0].data["task"]["subject"] == "test task timeline updated"
assert project_timeline[0].data["values_diff"]["subject"][0] == "test task timeline"
assert project_timeline[0].data["values_diff"]["subject"][1] == "test task timeline updated"
+ user_watcher_timeline = service.get_profile_timeline(user_watcher)
+ assert user_watcher_timeline[0].event_type == "tasks.task.change"
+ assert user_watcher_timeline[0].data["task"]["subject"] == "test task timeline updated"
+ assert user_watcher_timeline[0].data["values_diff"]["subject"][0] == "test task timeline"
+ assert user_watcher_timeline[0].data["values_diff"]["subject"][1] == "test task timeline updated"
def test_update_wiki_page_timeline():
+ user_watcher= factories.UserFactory()
page = factories.WikiPageFactory.create(slug="test wiki page timeline")
history_services.take_snapshot(page, user=page.owner)
+ page.add_watcher(user_watcher)
page.slug = "test wiki page timeline updated"
page.save()
history_services.take_snapshot(page, user=page.owner)
@@ -271,6 +308,11 @@ def test_update_wiki_page_timeline():
assert project_timeline[0].data["wikipage"]["slug"] == "test wiki page timeline updated"
assert project_timeline[0].data["values_diff"]["slug"][0] == "test wiki page timeline"
assert project_timeline[0].data["values_diff"]["slug"][1] == "test wiki page timeline updated"
+ user_watcher_timeline = service.get_profile_timeline(user_watcher)
+ assert user_watcher_timeline[0].event_type == "wiki.wikipage.change"
+ assert user_watcher_timeline[0].data["wikipage"]["slug"] == "test wiki page timeline updated"
+ assert user_watcher_timeline[0].data["values_diff"]["slug"][0] == "test wiki page timeline"
+ assert user_watcher_timeline[0].data["values_diff"]["slug"][1] == "test wiki page timeline updated"
def test_update_membership_timeline():
@@ -298,50 +340,80 @@ def test_update_membership_timeline():
def test_delete_project_timeline():
project = factories.ProjectFactory.create(name="test project timeline")
+ user_watcher= factories.UserFactory()
+ project.add_watcher(user_watcher)
history_services.take_snapshot(project, user=project.owner, delete=True)
user_timeline = service.get_project_timeline(project)
assert user_timeline[0].event_type == "projects.project.delete"
assert user_timeline[0].data["project"]["id"] == project.id
+ user_watcher_timeline = service.get_profile_timeline(user_watcher)
+ assert user_watcher_timeline[0].event_type == "projects.project.delete"
+ assert user_watcher_timeline[0].data["project"]["id"] == project.id
def test_delete_milestone_timeline():
milestone = factories.MilestoneFactory.create(name="test milestone timeline")
+ user_watcher= factories.UserFactory()
+ milestone.add_watcher(user_watcher)
history_services.take_snapshot(milestone, user=milestone.owner, delete=True)
project_timeline = service.get_project_timeline(milestone.project)
assert project_timeline[0].event_type == "milestones.milestone.delete"
assert project_timeline[0].data["milestone"]["name"] == "test milestone timeline"
+ user_watcher_timeline = service.get_profile_timeline(user_watcher)
+ assert user_watcher_timeline[0].event_type == "milestones.milestone.delete"
+ assert user_watcher_timeline[0].data["milestone"]["name"] == "test milestone timeline"
def test_delete_user_story_timeline():
user_story = factories.UserStoryFactory.create(subject="test us timeline")
+ user_watcher= factories.UserFactory()
+ user_story.add_watcher(user_watcher)
history_services.take_snapshot(user_story, user=user_story.owner, delete=True)
project_timeline = service.get_project_timeline(user_story.project)
assert project_timeline[0].event_type == "userstories.userstory.delete"
assert project_timeline[0].data["userstory"]["subject"] == "test us timeline"
+ user_watcher_timeline = service.get_profile_timeline(user_watcher)
+ assert user_watcher_timeline[0].event_type == "userstories.userstory.delete"
+ assert user_watcher_timeline[0].data["userstory"]["subject"] == "test us timeline"
def test_delete_issue_timeline():
issue = factories.IssueFactory.create(subject="test issue timeline")
+ user_watcher= factories.UserFactory()
+ issue.add_watcher(user_watcher)
history_services.take_snapshot(issue, user=issue.owner, delete=True)
project_timeline = service.get_project_timeline(issue.project)
assert project_timeline[0].event_type == "issues.issue.delete"
assert project_timeline[0].data["issue"]["subject"] == "test issue timeline"
+ user_watcher_timeline = service.get_profile_timeline(user_watcher)
+ assert user_watcher_timeline[0].event_type == "issues.issue.delete"
+ assert user_watcher_timeline[0].data["issue"]["subject"] == "test issue timeline"
def test_delete_task_timeline():
task = factories.TaskFactory.create(subject="test task timeline")
+ user_watcher= factories.UserFactory()
+ task.add_watcher(user_watcher)
history_services.take_snapshot(task, user=task.owner, delete=True)
project_timeline = service.get_project_timeline(task.project)
assert project_timeline[0].event_type == "tasks.task.delete"
assert project_timeline[0].data["task"]["subject"] == "test task timeline"
+ user_watcher_timeline = service.get_profile_timeline(user_watcher)
+ assert user_watcher_timeline[0].event_type == "tasks.task.delete"
+ assert user_watcher_timeline[0].data["task"]["subject"] == "test task timeline"
def test_delete_wiki_page_timeline():
page = factories.WikiPageFactory.create(slug="test wiki page timeline")
+ user_watcher= factories.UserFactory()
+ page.add_watcher(user_watcher)
history_services.take_snapshot(page, user=page.owner, delete=True)
project_timeline = service.get_project_timeline(page.project)
assert project_timeline[0].event_type == "wiki.wikipage.delete"
assert project_timeline[0].data["wikipage"]["slug"] == "test wiki page timeline"
+ user_watcher_timeline = service.get_profile_timeline(user_watcher)
+ assert user_watcher_timeline[0].event_type == "wiki.wikipage.delete"
+ assert user_watcher_timeline[0].data["wikipage"]["slug"] == "test wiki page timeline"
def test_delete_membership_timeline():
diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py
index d8972da2..6b49568a 100644
--- a/tests/integration/test_userstories.py
+++ b/tests/integration/test_userstories.py
@@ -483,6 +483,6 @@ def test_custom_fields_csv_generation():
data.seek(0)
reader = csv.reader(data)
row = next(reader)
- assert row[24] == attr.name
+ assert row[26] == attr.name
row = next(reader)
- assert row[24] == "val1"
+ assert row[26] == "val1"
diff --git a/tests/integration/test_watch_issues.py b/tests/integration/test_watch_issues.py
index f51d5075..09ba4f7b 100644
--- a/tests/integration/test_watch_issues.py
+++ b/tests/integration/test_watch_issues.py
@@ -16,6 +16,7 @@
# along with this program. If not, see .
import pytest
+import json
from django.core.urlresolvers import reverse
from .. import factories as f
@@ -45,3 +46,78 @@ def test_unwatch_issue(client):
response = client.post(url)
assert response.status_code == 200
+
+
+def test_list_issue_watchers(client):
+ user = f.UserFactory.create()
+ issue = f.IssueFactory(owner=user)
+ f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
+ f.WatchedFactory.create(content_object=issue, user=user)
+ url = reverse("issue-watchers-list", args=(issue.id,))
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data[0]['id'] == user.id
+
+
+def test_get_issue_watcher(client):
+ user = f.UserFactory.create()
+ issue = f.IssueFactory(owner=user)
+ f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
+ watch = f.WatchedFactory.create(content_object=issue, user=user)
+ url = reverse("issue-watchers-detail", args=(issue.id, watch.user.id))
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data['id'] == watch.user.id
+
+
+def test_get_issue_watchers(client):
+ user = f.UserFactory.create()
+ issue = f.IssueFactory(owner=user)
+ f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
+ url = reverse("issues-detail", args=(issue.id,))
+
+ f.WatchedFactory.create(content_object=issue, user=user)
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data['watchers'] == [user.id]
+
+
+def test_get_issue_is_watched(client):
+ user = f.UserFactory.create()
+ issue = f.IssueFactory(owner=user)
+ f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
+ url_detail = reverse("issues-detail", args=(issue.id,))
+ url_watch = reverse("issues-watch", args=(issue.id,))
+ url_unwatch = reverse("issues-unwatch", args=(issue.id,))
+
+ client.login(user)
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == []
+ assert response.data['is_watched'] == False
+
+ response = client.post(url_watch)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == [user.id]
+ assert response.data['is_watched'] == True
+
+ response = client.post(url_unwatch)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == []
+ assert response.data['is_watched'] == False
diff --git a/tests/integration/test_watch_projects.py b/tests/integration/test_watch_projects.py
index 8bb765ce..358c15f2 100644
--- a/tests/integration/test_watch_projects.py
+++ b/tests/integration/test_watch_projects.py
@@ -16,6 +16,7 @@
# along with this program. If not, see .
import pytest
+import json
from django.core.urlresolvers import reverse
from .. import factories as f
@@ -45,3 +46,78 @@ def test_unwacth_project(client):
response = client.post(url)
assert response.status_code == 200
+
+
+def test_list_project_watchers(client):
+ user = f.UserFactory.create()
+ project = f.create_project(owner=user)
+ f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.WatchedFactory.create(content_object=project, user=user)
+ url = reverse("project-watchers-list", args=(project.id,))
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data[0]['id'] == user.id
+
+
+def test_get_project_watcher(client):
+ user = f.UserFactory.create()
+ project = f.create_project(owner=user)
+ f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ watch = f.WatchedFactory.create(content_object=project, user=user)
+ url = reverse("project-watchers-detail", args=(project.id, watch.user.id))
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data['id'] == watch.user.id
+
+
+def test_get_project_watchers(client):
+ user = f.UserFactory.create()
+ project = f.create_project(owner=user)
+ f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ url = reverse("projects-detail", args=(project.id,))
+
+ f.WatchedFactory.create(content_object=project, user=user)
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data['watchers'] == [user.id]
+
+
+def test_get_project_is_watched(client):
+ user = f.UserFactory.create()
+ project = f.create_project(owner=user)
+ f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ url_detail = reverse("projects-detail", args=(project.id,))
+ url_watch = reverse("projects-watch", args=(project.id,))
+ url_unwatch = reverse("projects-unwatch", args=(project.id,))
+
+ client.login(user)
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == []
+ assert response.data['is_watched'] == False
+
+ response = client.post(url_watch)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == [user.id]
+ assert response.data['is_watched'] == True
+
+ response = client.post(url_unwatch)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == []
+ assert response.data['is_watched'] == False
diff --git a/tests/integration/test_watch_tasks.py b/tests/integration/test_watch_tasks.py
index f62e4c7b..7444a948 100644
--- a/tests/integration/test_watch_tasks.py
+++ b/tests/integration/test_watch_tasks.py
@@ -16,6 +16,7 @@
# along with this program. If not, see .
import pytest
+import json
from django.core.urlresolvers import reverse
from .. import factories as f
@@ -45,3 +46,78 @@ def test_unwatch_task(client):
response = client.post(url)
assert response.status_code == 200
+
+
+def test_list_task_watchers(client):
+ user = f.UserFactory.create()
+ task = f.TaskFactory(owner=user)
+ f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ f.WatchedFactory.create(content_object=task, user=user)
+ url = reverse("task-watchers-list", args=(task.id,))
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data[0]['id'] == user.id
+
+
+def test_get_task_watcher(client):
+ user = f.UserFactory.create()
+ task = f.TaskFactory(owner=user)
+ f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ watch = f.WatchedFactory.create(content_object=task, user=user)
+ url = reverse("task-watchers-detail", args=(task.id, watch.user.id))
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data['id'] == watch.user.id
+
+
+def test_get_task_watchers(client):
+ user = f.UserFactory.create()
+ task = f.TaskFactory(owner=user)
+ f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ url = reverse("tasks-detail", args=(task.id,))
+
+ f.WatchedFactory.create(content_object=task, user=user)
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data['watchers'] == [user.id]
+
+
+def test_get_task_is_watched(client):
+ user = f.UserFactory.create()
+ task = f.TaskFactory(owner=user)
+ f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ url_detail = reverse("tasks-detail", args=(task.id,))
+ url_watch = reverse("tasks-watch", args=(task.id,))
+ url_unwatch = reverse("tasks-unwatch", args=(task.id,))
+
+ client.login(user)
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == []
+ assert response.data['is_watched'] == False
+
+ response = client.post(url_watch)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == [user.id]
+ assert response.data['is_watched'] == True
+
+ response = client.post(url_unwatch)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == []
+ assert response.data['is_watched'] == False
diff --git a/tests/integration/test_watch_userstories.py b/tests/integration/test_watch_userstories.py
index a6a7123e..cad86151 100644
--- a/tests/integration/test_watch_userstories.py
+++ b/tests/integration/test_watch_userstories.py
@@ -16,6 +16,7 @@
# along with this program. If not, see .
import pytest
+import json
from django.core.urlresolvers import reverse
from .. import factories as f
@@ -45,3 +46,78 @@ def test_unwatch_user_story(client):
response = client.post(url)
assert response.status_code == 200
+
+
+def test_list_user_story_watchers(client):
+ user = f.UserFactory.create()
+ user_story = f.UserStoryFactory(owner=user)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ f.WatchedFactory.create(content_object=user_story, user=user)
+ url = reverse("userstory-watchers-list", args=(user_story.id,))
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data[0]['id'] == user.id
+
+
+def test_get_user_story_watcher(client):
+ user = f.UserFactory.create()
+ user_story = f.UserStoryFactory(owner=user)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ watch = f.WatchedFactory.create(content_object=user_story, user=user)
+ url = reverse("userstory-watchers-detail", args=(user_story.id, watch.user.id))
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data['id'] == watch.user.id
+
+
+def test_get_user_story_watchers(client):
+ user = f.UserFactory.create()
+ user_story = f.UserStoryFactory(owner=user)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ url = reverse("userstories-detail", args=(user_story.id,))
+
+ f.WatchedFactory.create(content_object=user_story, user=user)
+
+ client.login(user)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert response.data['watchers'] == [user.id]
+
+
+def test_get_user_story_is_watched(client):
+ user = f.UserFactory.create()
+ user_story = f.UserStoryFactory(owner=user)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ url_detail = reverse("userstories-detail", args=(user_story.id,))
+ url_watch = reverse("userstories-watch", args=(user_story.id,))
+ url_unwatch = reverse("userstories-unwatch", args=(user_story.id,))
+
+ client.login(user)
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == []
+ assert response.data['is_watched'] == False
+
+ response = client.post(url_watch)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == [user.id]
+ assert response.data['is_watched'] == True
+
+ response = client.post(url_unwatch)
+ assert response.status_code == 200
+
+ response = client.get(url_detail)
+ assert response.status_code == 200
+ assert response.data['watchers'] == []
+ assert response.data['is_watched'] == False
diff --git a/tests/integration/test_webhooks.py b/tests/integration/test_webhooks.py
index 0b3b32f0..9f4ecd71 100644
--- a/tests/integration/test_webhooks.py
+++ b/tests/integration/test_webhooks.py
@@ -90,3 +90,26 @@ def test_new_object_with_two_webhook(settings):
with patch('taiga.webhooks.tasks.delete_webhook') as delete_webhook_mock:
services.take_snapshot(obj, user=obj.owner, comment="test", delete=True)
assert delete_webhook_mock.call_count == 2
+
+
+def test_send_request_one_webhook(settings):
+ settings.WEBHOOKS_ENABLED = True
+ project = f.ProjectFactory()
+ f.WebhookFactory.create(project=project)
+
+ objects = [
+ f.IssueFactory.create(project=project),
+ f.TaskFactory.create(project=project),
+ f.UserStoryFactory.create(project=project),
+ f.WikiPageFactory.create(project=project)
+ ]
+
+ for obj in objects:
+ with patch('taiga.webhooks.tasks._send_request') as _send_request_mock:
+ services.take_snapshot(obj, user=obj.owner, comment="test")
+ assert _send_request_mock.call_count == 1
+
+ for obj in objects:
+ with patch('taiga.webhooks.tasks._send_request') as _send_request_mock:
+ services.take_snapshot(obj, user=obj.owner, comment="test", delete=True)
+ assert _send_request_mock.call_count == 1