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