From 8c990e50882993490d7779c49b2f2209dcc78701 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Mon, 7 Sep 2015 15:20:57 +0200 Subject: [PATCH] Refactoring watchers for projects --- taiga/export_import/serializers.py | 6 +- taiga/projects/api.py | 35 +++++--- taiga/projects/models.py | 50 +++++++++++- taiga/projects/notifications/api.py | 16 +--- taiga/projects/notifications/mixins.py | 32 ++++---- taiga/projects/notifications/services.py | 79 +++++++++++-------- taiga/projects/notifications/utils.py | 76 +++++++++++++++++- taiga/timeline/signals.py | 29 ++----- taiga/users/services.py | 54 +++++++++++-- .../test_projects_resource.py | 13 +-- tests/integration/test_importer_api.py | 2 +- tests/integration/test_notifications.py | 47 +++++++---- tests/integration/test_timeline.py | 13 +-- tests/integration/test_watch_projects.py | 9 ++- 14 files changed, 316 insertions(+), 145 deletions(-) diff --git a/taiga/export_import/serializers.py b/taiga/export_import/serializers.py index 98e9d2cf..01f042cf 100644 --- a/taiga/export_import/serializers.py +++ b/taiga/export_import/serializers.py @@ -245,7 +245,7 @@ class WatcheableObjectModelSerializer(serializers.ModelSerializer): def save_watchers(self): new_watcher_emails = set(self._watchers) - old_watcher_emails = set(notifications_services.get_watchers(self.object).values_list("email", flat=True)) + old_watcher_emails = set(self.object.get_watchers().values_list("email", flat=True)) adding_watcher_emails = list(new_watcher_emails.difference(old_watcher_emails)) removing_watcher_emails = list(old_watcher_emails.difference(new_watcher_emails)) @@ -259,11 +259,11 @@ class WatcheableObjectModelSerializer(serializers.ModelSerializer): for user in removing_users: notifications_services.remove_watcher(self.object, user) - self.object.watchers = notifications_services.get_watchers(self.object) + self.object.watchers = self.object.get_watchers() def to_native(self, obj): ret = super(WatcheableObjectModelSerializer, self).to_native(obj) - ret["watchers"] = [user.email for user in notifications_services.get_watchers(obj)] + ret["watchers"] = [user.email for user in obj.get_watchers()] return ret diff --git a/taiga/projects/api.py b/taiga/projects/api.py index 2ea2a7cf..8d3cfe1f 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -32,7 +32,12 @@ from taiga.base.utils.slug import slugify_uniquely from taiga.projects.history.mixins import HistoryResourceMixin from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin -from taiga.projects.notifications.services import set_notify_policy, attach_notify_level_to_project_queryset +from taiga.projects.notifications.choices import NotifyLevel +from taiga.projects.notifications.utils import ( + attach_project_watchers_attrs_to_queryset, + attach_project_is_watched_to_queryset, + attach_notify_level_to_project_queryset) + from taiga.projects.mixins.ordering import BulkUpdateOrderMixin from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin @@ -48,10 +53,11 @@ from . import services from .votes.mixins.viewsets import LikedResourceMixin, VotersViewSetMixin + ###################################################### ## Project ###################################################### -class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet): +class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet): queryset = models.Project.objects.all() serializer_class = serializers.ProjectDetailSerializer admin_serializer_class = serializers.ProjectDetailAdminSerializer @@ -64,19 +70,28 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, WatchedResourceMi def get_queryset(self): qs = super().get_queryset() qs = self.attach_votes_attrs_to_queryset(qs) - qs = self.attach_watchers_attrs_to_queryset(qs) - qs = attach_notify_level_to_project_queryset(qs, self.request.user) + qs = attach_project_watchers_attrs_to_queryset(qs) + if self.request.user.is_authenticated(): + qs = attach_project_is_watched_to_queryset(qs, self.request.user) + qs = attach_notify_level_to_project_queryset(qs, self.request.user) + return qs @detail_route(methods=["POST"]) def watch(self, request, pk=None): - response = super(ProjectViewSet, self).watch(request, pk) - notify_policy = self.get_object().notify_policies.get(user=request.user) - level = request.DATA.get("notify_level", None) - if level is not None: - set_notify_policy(notify_policy, level) + project = self.get_object() + self.check_permissions(request, "watch", project) + notify_level = request.DATA.get("notify_level", NotifyLevel.watch) + project.add_watcher(self.request.user, notify_level=notify_level) + return response.Ok() - return response + @detail_route(methods=["POST"]) + def unwatch(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, "unwatch", project) + user = self.request.user + project.remove_watcher(user) + return response.Ok() @list_route(methods=["POST"]) def bulk_update_order(self, request, **kwargs): diff --git a/taiga/projects/models.py b/taiga/projects/models.py index b2e2566f..482d25da 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -20,7 +20,7 @@ import uuid from django.core.exceptions import ValidationError from django.db import models -from django.db.models import signals +from django.db.models import signals, Q from django.apps import apps from django.conf import settings from django.dispatch import receiver @@ -38,9 +38,14 @@ from taiga.base.utils.dicts import dict_sum from taiga.base.utils.sequence import arithmetic_progression from taiga.base.utils.slug import slugify_uniquely_for_queryset -from . import choices +from taiga.projects.notifications.choices import NotifyLevel +from taiga.projects.notifications.services import ( + get_notify_policy, + set_notify_policy_level, + set_notify_policy_level_to_ignore, + create_notify_policy_if_not_exists) -from . notifications.mixins import WatchedModelMixin +from . import choices class Membership(models.Model): @@ -73,6 +78,10 @@ class Membership(models.Model): user_order = models.IntegerField(default=10000, null=False, blank=False, verbose_name=_("user order")) + def get_related_people(self): + related_people = get_user_model().objects.filter(id=self.user.id) + return related_people + def clean(self): # TODO: Review and do it more robust memberships = Membership.objects.filter(user=self.user, project=self.project) @@ -120,7 +129,7 @@ class ProjectDefaults(models.Model): abstract = True -class Project(ProjectDefaults, WatchedModelMixin, TaggedMixin, models.Model): +class Project(ProjectDefaults, TaggedMixin, models.Model): name = models.CharField(max_length=250, null=False, blank=False, verbose_name=_("name")) slug = models.SlugField(max_length=250, unique=True, null=False, blank=True, @@ -334,6 +343,39 @@ class Project(ProjectDefaults, WatchedModelMixin, TaggedMixin, models.Model): "assigned": self._get_user_stories_points(assigned_user_stories), } + def _get_q_watchers(self): + return Q(notify_policies__project_id=self.id) & ~Q(notify_policies__notify_level=NotifyLevel.ignore) + + def get_watchers(self): + return get_user_model().objects.filter(self._get_q_watchers()) + + def get_related_people(self): + related_people_q = Q() + + ## - Owner + if self.owner_id: + related_people_q.add(Q(id=self.owner_id), Q.OR) + + ## - Watchers + related_people_q.add(self._get_q_watchers(), Q.OR) + + ## - Apply filters + related_people = get_user_model().objects.filter(related_people_q) + + ## - Exclude inactive and system users and remove duplicate + related_people = related_people.exclude(is_active=False) + related_people = related_people.exclude(is_system=True) + related_people = related_people.distinct() + return related_people + + def add_watcher(self, user, notify_level=NotifyLevel.watch): + notify_policy = create_notify_policy_if_not_exists(self, user) + set_notify_policy_level(notify_policy, notify_level) + + def remove_watcher(self, user): + notify_policy = get_notify_policy(self, user) + set_notify_policy_level_to_ignore(notify_policy) + class ProjectModulesConfig(models.Model): project = models.OneToOneField("Project", null=False, blank=False, diff --git a/taiga/projects/notifications/api.py b/taiga/projects/notifications/api.py index 453df821..d4b92c96 100644 --- a/taiga/projects/notifications/api.py +++ b/taiga/projects/notifications/api.py @@ -33,12 +33,9 @@ class NotifyPolicyViewSet(ModelCrudViewSet): permission_classes = (permissions.NotifyPolicyPermission,) def _build_needed_notify_policies(self): - watched_project_ids = user_services.get_watched_content_for_user(self.request.user).get("project", []) - projects = Project.objects.filter( Q(owner=self.request.user) | - Q(memberships__user=self.request.user) | - Q(id__in=watched_project_ids) + Q(memberships__user=self.request.user) ).distinct() for project in projects: @@ -50,13 +47,6 @@ class NotifyPolicyViewSet(ModelCrudViewSet): self._build_needed_notify_policies() - # 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 the project - watched_project_ids = user_services.get_watched_content_for_user(self.request.user).get("project", []) - - return models.NotifyPolicy.objects.filter(user=self.request.user).filter(Q(project__owner=self.request.user) | - Q(project__memberships__user=self.request.user) | - Q(project__id__in=watched_project_ids) + return models.NotifyPolicy.objects.filter(user=self.request.user).filter( + Q(project__owner=self.request.user) | Q(project__memberships__user=self.request.user) ).distinct() diff --git a/taiga/projects/notifications/mixins.py b/taiga/projects/notifications/mixins.py index 627883b5..e6e6fc74 100644 --- a/taiga/projects/notifications/mixins.py +++ b/taiga/projects/notifications/mixins.py @@ -51,7 +51,7 @@ class WatchedResourceMixin: def attach_watchers_attrs_to_queryset(self, queryset): qs = attach_watchers_to_queryset(queryset) if self.request.user.is_authenticated(): - qs = attach_is_watched_to_queryset(self.request.user, qs) + qs = attach_is_watched_to_queryset(qs, self.request.user) return qs @@ -126,21 +126,19 @@ class WatchedModelMixin(object): """ return self.project - def get_watchers(self) -> frozenset: + def get_watchers(self) -> object: """ Default implementation method for obtain a list of watchers for current instance. - - NOTE: the default implementation returns frozen - set of all watchers if "watchers" attribute exists - in a model. - - WARNING: it returns a full evaluated set and in - future, for project with 1000k watchers it can be - very inefficient way for obtain watchers but at - this momment is the simplest way. """ - return frozenset(services.get_watchers(self)) + return services.get_watchers(self) + + def get_related_people(self) -> object: + """ + Default implementation for obtain the related people of + current instance. + """ + return services.get_related_people(self) def get_watched(self, user_or_id): return services.get_watched(user_or_id, type(self)) @@ -194,7 +192,7 @@ class WatchedResourceModelSerializer(serializers.ModelSerializer): instance = super(WatchedResourceModelSerializer, self).restore_object(attrs, instance) if instance is not None and self.validate_watchers(attrs, "watchers"): new_watcher_ids = set(attrs.get("watchers", [])) - old_watcher_ids = set(services.get_watchers(instance).values_list("id", flat=True)) + old_watcher_ids = set(instance.get_watchers().values_list("id", flat=True)) adding_watcher_ids = list(new_watcher_ids.difference(old_watcher_ids)) removing_watcher_ids = list(old_watcher_ids.difference(new_watcher_ids)) @@ -207,7 +205,7 @@ class WatchedResourceModelSerializer(serializers.ModelSerializer): for user in removing_users: services.remove_watcher(instance, user) - instance.watchers = services.get_watchers(instance) + instance.watchers = instance.get_watchers() return instance @@ -215,7 +213,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 obj is not None and not hasattr(obj, "watchers"): - obj.watchers = [user.id for user in services.get_watchers(obj)] + obj.watchers = [user.id for user in obj.get_watchers()] return super(WatchedResourceModelSerializer, self).to_native(obj) @@ -235,7 +233,7 @@ class WatchersViewSetMixin: self.check_permissions(request, 'retrieve', resource) try: - self.object = services.get_watchers(resource).get(pk=pk) + self.object = resource.get_watchers().get(pk=pk) except ObjectDoesNotExist: # or User.DoesNotExist return response.NotFound() @@ -252,4 +250,4 @@ class WatchersViewSetMixin: def get_queryset(self): resource = self.resource_model.objects.get(pk=self.kwargs.get("resource_id")) - return services.get_watchers(resource) + return resource.get_watchers() diff --git a/taiga/projects/notifications/services.py b/taiga/projects/notifications/services.py index 4a954fe6..5d191d32 100644 --- a/taiga/projects/notifications/services.py +++ b/taiga/projects/notifications/services.py @@ -21,6 +21,7 @@ from functools import partial from django.apps import apps from django.db.transaction import atomic from django.db import IntegrityError, transaction +from django.db.models import Q from django.contrib.contenttypes.models import ContentType from django.contrib.auth import get_user_model from django.utils import timezone @@ -30,7 +31,6 @@ from django.utils.translation import ugettext as _ from djmail import template_mail from taiga.base import exceptions as exc -from taiga.base.utils.text import strip_lines from taiga.projects.notifications.choices import NotifyLevel from taiga.projects.history.choices import HistoryType from taiga.projects.history.services import (make_key_from_model_object, @@ -90,31 +90,6 @@ def get_notify_policy(project, user): return instance -def attach_notify_level_to_project_queryset(queryset, user): - """ - Function that attach "notify_level" attribute on each queryset - result for query notification level of current user for each - project in the most efficient way. - - :param queryset: A Django queryset object. - :param user: A User model object. - - :return: Queryset object with the additional `as_field` field. - """ - user_id = getattr(user, "id", None) or "NULL" - default_level = NotifyLevel.notwatch - - sql = strip_lines(""" - COALESCE((SELECT notifications_notifypolicy.notify_level - FROM notifications_notifypolicy - WHERE notifications_notifypolicy.project_id = projects_project.id - AND notifications_notifypolicy.user_id = {user_id}), - {default_level}) - """) - sql = sql.format(user_id=user_id, default_level=default_level) - return queryset.extra(select={"notify_level": sql}) - - def analize_object_for_watchers(obj:object, history:object): """ Generic implementation for analize model objects and @@ -182,9 +157,6 @@ def get_users_to_notify(obj, *, discard_users=None) -> list: 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) @@ -320,15 +292,49 @@ def process_sync_notifications(): send_sync_notifications(notification.pk) +def _get_q_watchers(obj): + obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj) + return Q(watched__content_type=obj_type, watched__object_id=obj.id) + + def get_watchers(obj): """Get the watchers of an object. :param obj: Any Django model instance. - :return: User queryset object representing the users that voted the object. + :return: User queryset object representing the users that watch 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) + return get_user_model().objects.filter(_get_q_watchers(obj)) + + +def get_related_people(obj): + """Get the related people of an object for notifications. + + :param obj: Any Django model instance. + + :return: User queryset object representing the users related to the object. + """ + related_people_q = Q() + + ## - Owner + if hasattr(obj, "owner_id") and obj.owner_id: + related_people_q.add(Q(id=obj.owner_id), Q.OR) + + ## - Assigned to + if hasattr(obj, "assigned_to_id") and obj.assigned_to_id: + related_people_q.add(Q(id=obj.assigned_to_id), Q.OR) + + ## - Watchers + related_people_q.add(_get_q_watchers(obj), Q.OR) + + ## - Apply filters + related_people = get_user_model().objects.filter(related_people_q) + + ## - Exclude inactive and system users and remove duplicate + related_people = related_people.exclude(is_active=False) + related_people = related_people.exclude(is_system=True) + related_people = related_people.distinct() + return related_people def get_watched(user_or_id, model): @@ -389,7 +395,7 @@ def remove_watcher(obj, user): qs.delete() -def set_notify_policy(notify_policy, notify_level): +def set_notify_policy_level(notify_policy, notify_level): """ Set notification level for specified policy. """ @@ -400,6 +406,13 @@ def set_notify_policy(notify_policy, notify_level): notify_policy.save() +def set_notify_policy_level_to_ignore(notify_policy): + """ + Set notification level for specified policy. + """ + set_notify_policy_level(notify_policy, NotifyLevel.ignore) + + def make_ms_thread_index(msg_id, dt): """ Create the 22-byte base of the thread-index string in the format: diff --git a/taiga/projects/notifications/utils.py b/taiga/projects/notifications/utils.py index 14edd0b3..39e6eaff 100644 --- a/taiga/projects/notifications/utils.py +++ b/taiga/projects/notifications/utils.py @@ -16,7 +16,8 @@ # along with this program. If not, see . from django.apps import apps - +from .choices import NotifyLevel +from taiga.base.utils.text import strip_lines def attach_watchers_to_queryset(queryset, as_field="watchers"): """Attach watching user ids to each object of the queryset. @@ -39,7 +40,7 @@ def attach_watchers_to_queryset(queryset, as_field="watchers"): return qs -def attach_is_watched_to_queryset(user, queryset, as_field="is_watched"): +def attach_is_watched_to_queryset(queryset, user, as_field="is_watched"): """Attach is_watched boolean to each object of the queryset. :param user: A users.User object model @@ -61,3 +62,74 @@ def attach_is_watched_to_queryset(user, queryset, as_field="is_watched"): sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id) qs = queryset.extra(select={as_field: sql}) return qs + + +def attach_project_is_watched_to_queryset(queryset, user, as_field="is_watched"): + """Attach is_watched boolean to each object of the projects queryset. + + :param user: A users.User object model + :param queryset: A Django projects queryset object. + :param as_field: Attach the boolean as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model) + sql = ("""SELECT CASE WHEN (SELECT count(*) + FROM notifications_notifypolicy + WHERE notifications_notifypolicy.project_id = {tbl}.id + AND notifications_notifypolicy.user_id = {user_id} + AND notifications_notifypolicy.notify_level != {ignore_notify_level}) > 0 + + THEN TRUE + ELSE FALSE + END""") + sql = sql.format(tbl=model._meta.db_table, user_id=user.id, ignore_notify_level=NotifyLevel.ignore) + qs = queryset.extra(select={as_field: sql}) + return qs + + +def attach_project_watchers_attrs_to_queryset(queryset, as_field="watchers"): + """Attach watching user ids to each project of the queryset. + + :param queryset: A Django projects queryset object. + :param as_field: Attach the watchers as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model) + + sql = ("""SELECT array(SELECT user_id + FROM notifications_notifypolicy + WHERE notifications_notifypolicy.project_id = {tbl}.id + AND notifications_notifypolicy.notify_level != {ignore_notify_level})""") + sql = sql.format(tbl=model._meta.db_table, ignore_notify_level=NotifyLevel.ignore) + qs = queryset.extra(select={as_field: sql}) + + return qs + + +def attach_notify_level_to_project_queryset(queryset, user): + """ + Function that attach "notify_level" attribute on each queryset + result for query notification level of current user for each + project in the most efficient way. + + :param queryset: A Django queryset object. + :param user: A User model object. + + :return: Queryset object with the additional `as_field` field. + """ + user_id = getattr(user, "id", None) or "NULL" + default_level = NotifyLevel.notwatch + + sql = strip_lines(""" + COALESCE((SELECT notifications_notifypolicy.notify_level + FROM notifications_notifypolicy + WHERE notifications_notifypolicy.project_id = projects_project.id + AND notifications_notifypolicy.user_id = {user_id}), + {default_level}) + """) + sql = sql.format(user_id=user_id, default_level=default_level) + return queryset.extra(select={"notify_level": sql}) diff --git a/taiga/timeline/signals.py b/taiga/timeline/signals.py index 5673fbbb..1a379f13 100644 --- a/taiga/timeline/signals.py +++ b/taiga/timeline/signals.py @@ -45,31 +45,12 @@ def _push_to_timelines(project, user, obj, event_type, created_datetime, extra_d namespace=build_project_namespace(project), extra_data=extra_data) - ## User profile timelines - ## - Me - related_people = User.objects.filter(id=user.id) + if hasattr(obj, "get_related_people"): + related_people = obj.get_related_people() - ## - Owner - if hasattr(obj, "owner_id") and obj.owner_id: - related_people |= User.objects.filter(id=obj.owner_id) - - ## - Assigned to - if hasattr(obj, "assigned_to_id") and obj.assigned_to_id: - related_people |= User.objects.filter(id=obj.assigned_to_id) - - ## - Watchers - watchers = notifications_services.get_watchers(obj) - if watchers: - related_people |= watchers - - ## - Exclude inactive and system users and remove duplicate - related_people = related_people.exclude(is_active=False) - related_people = related_people.exclude(is_system=True) - related_people = related_people.distinct() - - _push_to_timeline(related_people, obj, event_type, created_datetime, - namespace=build_user_namespace(user), - extra_data=extra_data) + _push_to_timeline(related_people, obj, event_type, created_datetime, + namespace=build_user_namespace(user), + extra_data=extra_data) else: # Actions not related with a project ## - Me diff --git a/taiga/users/services.py b/taiga/users/services.py index 992f4ae8..0ce318bd 100644 --- a/taiga/users/services.py +++ b/taiga/users/services.py @@ -22,6 +22,7 @@ from django.apps import apps from django.db.models import Q from django.db import connection from django.conf import settings +from django.contrib.contenttypes.models import ContentType from django.utils.translation import ugettext as _ from easy_thumbnails.files import get_thumbnailer @@ -29,6 +30,7 @@ from easy_thumbnails.exceptions import InvalidImageFormatError from taiga.base import exceptions as exc from taiga.base.utils.urls import get_absolute_url +from taiga.projects.notifications.choices import NotifyLevel from .gravatar import get_gravatar_url @@ -179,6 +181,50 @@ def get_watched_content_for_user(user): return user_watches +def _build_favourites_sql_for_projects(for_user): + sql = """ + SELECT projects_project.id AS id, null AS ref, 'project' AS type, 'watch' AS action, + tags, notifications_notifypolicy.project_id AS object_id, projects_project.id AS project, + slug AS slug, projects_project.name AS subject, + notifications_notifypolicy.created_at, coalesce(watchers, 0) as total_watchers, coalesce(votes_votes.count, 0) total_votes, null AS assigned_to + FROM notifications_notifypolicy + INNER JOIN projects_project + ON (projects_project.id = notifications_notifypolicy.project_id) + LEFT JOIN (SELECT project_id, count(*) watchers + FROM notifications_notifypolicy + WHERE notifications_notifypolicy.notify_level != {ignore_notify_level} + GROUP BY project_id + ) type_watchers + ON projects_project.id = type_watchers.project_id + LEFT JOIN votes_votes + ON (projects_project.id = votes_votes.object_id AND {project_content_type_id} = votes_votes.content_type_id) + WHERE notifications_notifypolicy.user_id = {for_user_id} + UNION + SELECT projects_project.id AS id, null AS ref, 'project' AS type, 'vote' AS action, + tags, votes_vote.object_id AS object_id, projects_project.id AS project, + slug AS slug, projects_project.name AS subject, + votes_vote.created_date, coalesce(watchers, 0) as total_watchers, coalesce(votes_votes.count, 0) total_votes, null AS assigned_to + FROM votes_vote + INNER JOIN projects_project + ON (projects_project.id = votes_vote.object_id) + LEFT JOIN (SELECT project_id, count(*) watchers + FROM notifications_notifypolicy + WHERE notifications_notifypolicy.notify_level != {ignore_notify_level} + GROUP BY project_id + ) type_watchers + ON projects_project.id = type_watchers.project_id + LEFT JOIN votes_votes + ON (projects_project.id = votes_votes.object_id AND {project_content_type_id} = votes_votes.content_type_id) + WHERE votes_vote.user_id = {for_user_id} + """ + sql = sql.format( + for_user_id=for_user.id, + ignore_notify_level=NotifyLevel.ignore, + project_content_type_id=ContentType.objects.get(app_label="projects", model="project").id) + return sql + + + def _build_favourites_sql_for_type(for_user, type, table_name, ref_column="ref", project_column="project_id", assigned_to_column="assigned_to_id", slug_column="slug", subject_column="subject"): @@ -217,6 +263,7 @@ def _build_favourites_sql_for_type(for_user, type, table_name, ref_column="ref", ref_column = ref_column, project_column=project_column, assigned_to_column=assigned_to_column, slug_column=slug_column, subject_column=subject_column) + return sql @@ -298,12 +345,7 @@ def get_favourites_list(for_user, from_user, type=None, action=None, q=None): userstories_sql=_build_favourites_sql_for_type(for_user, "userstory", "userstories_userstory", slug_column="null"), tasks_sql=_build_favourites_sql_for_type(for_user, "task", "tasks_task", slug_column="null"), issues_sql=_build_favourites_sql_for_type(for_user, "issue", "issues_issue", slug_column="null"), - projects_sql=_build_favourites_sql_for_type(for_user, "project", "projects_project", - ref_column="null", - project_column="id", - assigned_to_column="null", - subject_column="projects_project.name") - ) + projects_sql=_build_favourites_sql_for_projects(for_user)) cursor = connection.cursor() cursor.execute(sql) diff --git a/tests/integration/resources_permissions/test_projects_resource.py b/tests/integration/resources_permissions/test_projects_resource.py index c76fa68f..26132062 100644 --- a/tests/integration/resources_permissions/test_projects_resource.py +++ b/tests/integration/resources_permissions/test_projects_resource.py @@ -81,13 +81,6 @@ 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 @@ -322,11 +315,11 @@ def test_project_watchers_list(client, data): ] 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)] + assert results == [(200, 1), (200, 1), (200, 1), (200, 1), (200, 1)] 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)] + assert results == [(200, 3), (200, 3), (200, 3), (200, 3), (200, 3)] 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)] + assert results == [(401, 0), (403, 0), (403, 0), (200, 3), (200, 3)] def test_project_watchers_retrieve(client, data): diff --git a/tests/integration/test_importer_api.py b/tests/integration/test_importer_api.py index 073d7585..a0f2219b 100644 --- a/tests/integration/test_importer_api.py +++ b/tests/integration/test_importer_api.py @@ -68,7 +68,7 @@ def test_valid_project_import_without_extra_data(client): ] assert all(map(lambda x: len(response_data[x]) == 0, must_empty_children)) assert response_data["owner"] == user.email - assert response_data["watchers"] == [user_watching.email] + assert response_data["watchers"] == [user.email, user_watching.email] def test_valid_project_import_with_not_existing_memberships(client): diff --git a/tests/integration/test_notifications.py b/tests/integration/test_notifications.py index 183a581a..4442ad92 100644 --- a/tests/integration/test_notifications.py +++ b/tests/integration/test_notifications.py @@ -32,6 +32,7 @@ from .. import factories as f from taiga.base.utils import json from taiga.projects.notifications import services +from taiga.projects.notifications import utils from taiga.projects.notifications import models from taiga.projects.notifications.choices import NotifyLevel from taiga.projects.history.choices import HistoryType @@ -56,7 +57,7 @@ def test_attach_notify_level_to_project_queryset(): f.ProjectFactory.create() qs = project1.__class__.objects.order_by("id") - qs = services.attach_notify_level_to_project_queryset(qs, project1.owner) + qs = utils.attach_notify_level_to_project_queryset(qs, project1.owner) assert len(qs) == 2 assert qs[0].notify_level == NotifyLevel.notwatch @@ -64,7 +65,7 @@ def test_attach_notify_level_to_project_queryset(): services.create_notify_policy(project1, project1.owner, NotifyLevel.watch) qs = project1.__class__.objects.order_by("id") - qs = services.attach_notify_level_to_project_queryset(qs, project1.owner) + qs = utils.attach_notify_level_to_project_queryset(qs, project1.owner) assert qs[0].notify_level == NotifyLevel.watch assert qs[1].notify_level == NotifyLevel.notwatch @@ -143,10 +144,25 @@ def test_users_to_notify(): role2 = f.RoleFactory.create(project=project, permissions=[]) member1 = f.MembershipFactory.create(project=project, role=role1) + policy_member1 = member1.user.notify_policies.get(project=project) + policy_member1.notify_level = NotifyLevel.ignore + policy_member1.save() member2 = f.MembershipFactory.create(project=project, role=role1) + policy_member2 = member2.user.notify_policies.get(project=project) + policy_member2.notify_level = NotifyLevel.ignore + policy_member2.save() member3 = f.MembershipFactory.create(project=project, role=role1) + policy_member3 = member3.user.notify_policies.get(project=project) + policy_member3.notify_level = NotifyLevel.ignore + policy_member3.save() member4 = f.MembershipFactory.create(project=project, role=role1) + policy_member4 = member4.user.notify_policies.get(project=project) + policy_member4.notify_level = NotifyLevel.ignore + policy_member4.save() member5 = f.MembershipFactory.create(project=project, role=role2) + policy_member5 = member5.user.notify_policies.get(project=project) + policy_member5.notify_level = NotifyLevel.ignore + policy_member5.save() inactive_member1 = f.MembershipFactory.create(project=project, role=role1) inactive_member1.user.is_active = False inactive_member1.user.save() @@ -158,14 +174,13 @@ def test_users_to_notify(): policy_model_cls = apps.get_model("notifications", "NotifyPolicy") - policy1 = policy_model_cls.objects.get(user=member1.user) - policy2 = policy_model_cls.objects.get(user=member3.user) - policy3 = policy_model_cls.objects.get(user=inactive_member1.user) - policy3.notify_level = NotifyLevel.watch - policy3.save() - policy4 = policy_model_cls.objects.get(user=system_member1.user) - policy4.notify_level = NotifyLevel.watch - policy4.save() + policy_inactive_member1 = policy_model_cls.objects.get(user=inactive_member1.user) + policy_inactive_member1.notify_level = NotifyLevel.watch + policy_inactive_member1.save() + + policy_system_member1 = policy_model_cls.objects.get(user=system_member1.user) + policy_system_member1.notify_level = NotifyLevel.watch + policy_system_member1.save() history = MagicMock() history.owner = member2.user @@ -174,13 +189,15 @@ def test_users_to_notify(): # Test basic description modifications issue.description = "test1" issue.save() + policy_member4.notify_level = NotifyLevel.watch + policy_member4.save() users = services.get_users_to_notify(issue) assert len(users) == 1 assert tuple(users)[0] == issue.get_owner() # Test watch notify level in one member - policy1.notify_level = NotifyLevel.watch - policy1.save() + policy_member1.notify_level = NotifyLevel.watch + policy_member1.save() users = services.get_users_to_notify(issue) assert len(users) == 2 @@ -188,13 +205,15 @@ def test_users_to_notify(): # Test with watchers issue.add_watcher(member3.user) + policy_member3.notify_level = NotifyLevel.watch + policy_member3.save() users = services.get_users_to_notify(issue) assert len(users) == 3 assert users == {member1.user, member3.user, issue.get_owner()} # Test with watchers with ignore policy - policy2.notify_level = NotifyLevel.ignore - policy2.save() + policy_member3.notify_level = NotifyLevel.ignore + policy_member3.save() issue.add_watcher(member3.user) users = services.get_users_to_notify(issue) diff --git a/tests/integration/test_timeline.py b/tests/integration/test_timeline.py index 4620e881..582c3667 100644 --- a/tests/integration/test_timeline.py +++ b/tests/integration/test_timeline.py @@ -200,6 +200,7 @@ def test_update_project_timeline(): project = factories.ProjectFactory.create(name="test project timeline") history_services.take_snapshot(project, user=project.owner) project.add_watcher(user_watcher) + print("PPPP") project.name = "test project timeline updated" project.save() history_services.take_snapshot(project, user=project.owner) @@ -341,7 +342,7 @@ 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) + 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" @@ -354,7 +355,7 @@ def test_delete_project_timeline(): def test_delete_milestone_timeline(): milestone = factories.MilestoneFactory.create(name="test milestone timeline") user_watcher= factories.UserFactory() - milestone.add_watcher(user_watcher) + 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" @@ -367,7 +368,7 @@ def test_delete_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) + 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" @@ -380,7 +381,7 @@ def test_delete_user_story_timeline(): def test_delete_issue_timeline(): issue = factories.IssueFactory.create(subject="test issue timeline") user_watcher= factories.UserFactory() - issue.add_watcher(user_watcher) + 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" @@ -393,7 +394,7 @@ def test_delete_issue_timeline(): def test_delete_task_timeline(): task = factories.TaskFactory.create(subject="test task timeline") user_watcher= factories.UserFactory() - task.add_watcher(user_watcher) + 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" @@ -406,7 +407,7 @@ def test_delete_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) + 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" diff --git a/tests/integration/test_watch_projects.py b/tests/integration/test_watch_projects.py index 5d643eb4..bbf58a0c 100644 --- a/tests/integration/test_watch_projects.py +++ b/tests/integration/test_watch_projects.py @@ -19,6 +19,8 @@ import pytest import json from django.core.urlresolvers import reverse +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS + from .. import factories as f pytestmark = pytest.mark.django_db @@ -124,8 +126,10 @@ def test_get_project_watchers(client): 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) + project = f.ProjectFactory.create(is_private=False, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS))) + url_detail = reverse("projects-detail", args=(project.id,)) url_watch = reverse("projects-watch", args=(project.id,)) url_unwatch = reverse("projects-unwatch", args=(project.id,)) @@ -133,6 +137,7 @@ def test_get_project_is_watched(client): client.login(user) response = client.get(url_detail) + assert response.status_code == 200 assert response.data['watchers'] == [] assert response.data['is_watched'] == False