Refactoring watchers for projects

remotes/origin/logger
Alejandro Alonso 2015-09-07 15:20:57 +02:00 committed by David Barragán Merino
parent fd46f00ccd
commit 8c990e5088
14 changed files with 316 additions and 145 deletions

View File

@ -245,7 +245,7 @@ class WatcheableObjectModelSerializer(serializers.ModelSerializer):
def save_watchers(self): def save_watchers(self):
new_watcher_emails = set(self._watchers) 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)) adding_watcher_emails = list(new_watcher_emails.difference(old_watcher_emails))
removing_watcher_emails = list(old_watcher_emails.difference(new_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: for user in removing_users:
notifications_services.remove_watcher(self.object, user) 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): def to_native(self, obj):
ret = super(WatcheableObjectModelSerializer, self).to_native(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 return ret

View File

@ -32,7 +32,12 @@ from taiga.base.utils.slug import slugify_uniquely
from taiga.projects.history.mixins import HistoryResourceMixin from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin 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.ordering import BulkUpdateOrderMixin
from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin
@ -48,10 +53,11 @@ from . import services
from .votes.mixins.viewsets import LikedResourceMixin, VotersViewSetMixin from .votes.mixins.viewsets import LikedResourceMixin, VotersViewSetMixin
###################################################### ######################################################
## Project ## Project
###################################################### ######################################################
class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet): class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet):
queryset = models.Project.objects.all() queryset = models.Project.objects.all()
serializer_class = serializers.ProjectDetailSerializer serializer_class = serializers.ProjectDetailSerializer
admin_serializer_class = serializers.ProjectDetailAdminSerializer admin_serializer_class = serializers.ProjectDetailAdminSerializer
@ -64,19 +70,28 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, WatchedResourceMi
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset() qs = super().get_queryset()
qs = self.attach_votes_attrs_to_queryset(qs) qs = self.attach_votes_attrs_to_queryset(qs)
qs = self.attach_watchers_attrs_to_queryset(qs) qs = attach_project_watchers_attrs_to_queryset(qs)
qs = attach_notify_level_to_project_queryset(qs, self.request.user) 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 return qs
@detail_route(methods=["POST"]) @detail_route(methods=["POST"])
def watch(self, request, pk=None): def watch(self, request, pk=None):
response = super(ProjectViewSet, self).watch(request, pk) project = self.get_object()
notify_policy = self.get_object().notify_policies.get(user=request.user) self.check_permissions(request, "watch", project)
level = request.DATA.get("notify_level", None) notify_level = request.DATA.get("notify_level", NotifyLevel.watch)
if level is not None: project.add_watcher(self.request.user, notify_level=notify_level)
set_notify_policy(notify_policy, 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"]) @list_route(methods=["POST"])
def bulk_update_order(self, request, **kwargs): def bulk_update_order(self, request, **kwargs):

View File

@ -20,7 +20,7 @@ import uuid
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models 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.apps import apps
from django.conf import settings from django.conf import settings
from django.dispatch import receiver 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.sequence import arithmetic_progression
from taiga.base.utils.slug import slugify_uniquely_for_queryset 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): class Membership(models.Model):
@ -73,6 +78,10 @@ class Membership(models.Model):
user_order = models.IntegerField(default=10000, null=False, blank=False, user_order = models.IntegerField(default=10000, null=False, blank=False,
verbose_name=_("user order")) 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): def clean(self):
# TODO: Review and do it more robust # TODO: Review and do it more robust
memberships = Membership.objects.filter(user=self.user, project=self.project) memberships = Membership.objects.filter(user=self.user, project=self.project)
@ -120,7 +129,7 @@ class ProjectDefaults(models.Model):
abstract = True 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, name = models.CharField(max_length=250, null=False, blank=False,
verbose_name=_("name")) verbose_name=_("name"))
slug = models.SlugField(max_length=250, unique=True, null=False, blank=True, slug = models.SlugField(max_length=250, unique=True, null=False, blank=True,
@ -334,6 +343,39 @@ class Project(ProjectDefaults, WatchedModelMixin, TaggedMixin, models.Model):
"assigned": self._get_user_stories_points(assigned_user_stories), "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): class ProjectModulesConfig(models.Model):
project = models.OneToOneField("Project", null=False, blank=False, project = models.OneToOneField("Project", null=False, blank=False,

View File

@ -33,12 +33,9 @@ class NotifyPolicyViewSet(ModelCrudViewSet):
permission_classes = (permissions.NotifyPolicyPermission,) permission_classes = (permissions.NotifyPolicyPermission,)
def _build_needed_notify_policies(self): 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( projects = Project.objects.filter(
Q(owner=self.request.user) | Q(owner=self.request.user) |
Q(memberships__user=self.request.user) | Q(memberships__user=self.request.user)
Q(id__in=watched_project_ids)
).distinct() ).distinct()
for project in projects: for project in projects:
@ -50,13 +47,6 @@ class NotifyPolicyViewSet(ModelCrudViewSet):
self._build_needed_notify_policies() self._build_needed_notify_policies()
# With really want to include the policies related to any content: return models.NotifyPolicy.objects.filter(user=self.request.user).filter(
# - The user is the owner of the project Q(project__owner=self.request.user) | Q(project__memberships__user=self.request.user)
# - 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)
).distinct() ).distinct()

View File

@ -51,7 +51,7 @@ class WatchedResourceMixin:
def attach_watchers_attrs_to_queryset(self, queryset): def attach_watchers_attrs_to_queryset(self, queryset):
qs = attach_watchers_to_queryset(queryset) qs = attach_watchers_to_queryset(queryset)
if self.request.user.is_authenticated(): 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 return qs
@ -126,21 +126,19 @@ class WatchedModelMixin(object):
""" """
return self.project return self.project
def get_watchers(self) -> frozenset: def get_watchers(self) -> object:
""" """
Default implementation method for obtain a list of Default implementation method for obtain a list of
watchers for current instance. 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): def get_watched(self, user_or_id):
return services.get_watched(user_or_id, type(self)) 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) instance = super(WatchedResourceModelSerializer, self).restore_object(attrs, instance)
if instance is not None and self.validate_watchers(attrs, "watchers"): if instance is not None and self.validate_watchers(attrs, "watchers"):
new_watcher_ids = set(attrs.get("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)) adding_watcher_ids = list(new_watcher_ids.difference(old_watcher_ids))
removing_watcher_ids = list(old_watcher_ids.difference(new_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: for user in removing_users:
services.remove_watcher(instance, user) services.remove_watcher(instance, user)
instance.watchers = services.get_watchers(instance) instance.watchers = instance.get_watchers()
return instance return instance
@ -215,7 +213,7 @@ class WatchedResourceModelSerializer(serializers.ModelSerializer):
def to_native(self, obj): def to_native(self, obj):
#watchers is wasn't attached via the get_queryset of the viewset we need to manually add it #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"): 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) return super(WatchedResourceModelSerializer, self).to_native(obj)
@ -235,7 +233,7 @@ class WatchersViewSetMixin:
self.check_permissions(request, 'retrieve', resource) self.check_permissions(request, 'retrieve', resource)
try: try:
self.object = services.get_watchers(resource).get(pk=pk) self.object = resource.get_watchers().get(pk=pk)
except ObjectDoesNotExist: # or User.DoesNotExist except ObjectDoesNotExist: # or User.DoesNotExist
return response.NotFound() return response.NotFound()
@ -252,4 +250,4 @@ class WatchersViewSetMixin:
def get_queryset(self): def get_queryset(self):
resource = self.resource_model.objects.get(pk=self.kwargs.get("resource_id")) resource = self.resource_model.objects.get(pk=self.kwargs.get("resource_id"))
return services.get_watchers(resource) return resource.get_watchers()

View File

@ -21,6 +21,7 @@ from functools import partial
from django.apps import apps from django.apps import apps
from django.db.transaction import atomic from django.db.transaction import atomic
from django.db import IntegrityError, transaction from django.db import IntegrityError, transaction
from django.db.models import Q
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.utils import timezone from django.utils import timezone
@ -30,7 +31,6 @@ from django.utils.translation import ugettext as _
from djmail import template_mail from djmail import template_mail
from taiga.base import exceptions as exc 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.notifications.choices import NotifyLevel
from taiga.projects.history.choices import HistoryType from taiga.projects.history.choices import HistoryType
from taiga.projects.history.services import (make_key_from_model_object, from taiga.projects.history.services import (make_key_from_model_object,
@ -90,31 +90,6 @@ def get_notify_policy(project, user):
return instance 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): def analize_object_for_watchers(obj:object, history:object):
""" """
Generic implementation for analize model objects and 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.project.get_watchers()))
candidates.update(filter(_can_notify_light, obj.get_participants())) 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 # Remove the changer from candidates
if discard_users: if discard_users:
candidates = candidates - set(discard_users) candidates = candidates - set(discard_users)
@ -320,15 +292,49 @@ def process_sync_notifications():
send_sync_notifications(notification.pk) 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): def get_watchers(obj):
"""Get the watchers of an object. """Get the watchers of an object.
:param obj: Any Django model instance. :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(_get_q_watchers(obj))
return get_user_model().objects.filter(watched__content_type=obj_type, watched__object_id=obj.id)
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): def get_watched(user_or_id, model):
@ -389,7 +395,7 @@ def remove_watcher(obj, user):
qs.delete() 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. Set notification level for specified policy.
""" """
@ -400,6 +406,13 @@ def set_notify_policy(notify_policy, notify_level):
notify_policy.save() 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): def make_ms_thread_index(msg_id, dt):
""" """
Create the 22-byte base of the thread-index string in the format: Create the 22-byte base of the thread-index string in the format:

View File

@ -16,7 +16,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.apps import apps 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"): def attach_watchers_to_queryset(queryset, as_field="watchers"):
"""Attach watching user ids to each object of the queryset. """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 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. """Attach is_watched boolean to each object of the queryset.
:param user: A users.User object model :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) sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id)
qs = queryset.extra(select={as_field: sql}) qs = queryset.extra(select={as_field: sql})
return qs 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})

View File

@ -45,31 +45,12 @@ def _push_to_timelines(project, user, obj, event_type, created_datetime, extra_d
namespace=build_project_namespace(project), namespace=build_project_namespace(project),
extra_data=extra_data) extra_data=extra_data)
## User profile timelines if hasattr(obj, "get_related_people"):
## - Me related_people = obj.get_related_people()
related_people = User.objects.filter(id=user.id)
## - Owner _push_to_timeline(related_people, obj, event_type, created_datetime,
if hasattr(obj, "owner_id") and obj.owner_id: namespace=build_user_namespace(user),
related_people |= User.objects.filter(id=obj.owner_id) extra_data=extra_data)
## - 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)
else: else:
# Actions not related with a project # Actions not related with a project
## - Me ## - Me

View File

@ -22,6 +22,7 @@ from django.apps import apps
from django.db.models import Q from django.db.models import Q
from django.db import connection from django.db import connection
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from easy_thumbnails.files import get_thumbnailer 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 import exceptions as exc
from taiga.base.utils.urls import get_absolute_url from taiga.base.utils.urls import get_absolute_url
from taiga.projects.notifications.choices import NotifyLevel
from .gravatar import get_gravatar_url from .gravatar import get_gravatar_url
@ -179,6 +181,50 @@ def get_watched_content_for_user(user):
return user_watches 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", def _build_favourites_sql_for_type(for_user, type, table_name, ref_column="ref",
project_column="project_id", assigned_to_column="assigned_to_id", project_column="project_id", assigned_to_column="assigned_to_id",
slug_column="slug", subject_column="subject"): 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, ref_column = ref_column, project_column=project_column,
assigned_to_column=assigned_to_column, slug_column=slug_column, assigned_to_column=assigned_to_column, slug_column=slug_column,
subject_column=subject_column) subject_column=subject_column)
return sql 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"), 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"), 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"), 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", projects_sql=_build_favourites_sql_for_projects(for_user))
ref_column="null",
project_column="id",
assigned_to_column="null",
subject_column="projects_project.name")
)
cursor = connection.cursor() cursor = connection.cursor()
cursor.execute(sql) cursor.execute(sql)

View File

@ -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_project1.pk, count=2)
f.VotesFactory(content_type=project_ct, object_id=m.private_project2.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 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) 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) 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) 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): def test_project_watchers_retrieve(client, data):

View File

@ -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 all(map(lambda x: len(response_data[x]) == 0, must_empty_children))
assert response_data["owner"] == user.email assert response_data["owner"] == user.email
assert response_data["watchers"] == [user_watching.email] assert response_data["watchers"] == [user.email, user_watching.email]
def test_valid_project_import_with_not_existing_memberships(client): def test_valid_project_import_with_not_existing_memberships(client):

View File

@ -32,6 +32,7 @@ from .. import factories as f
from taiga.base.utils import json from taiga.base.utils import json
from taiga.projects.notifications import services from taiga.projects.notifications import services
from taiga.projects.notifications import utils
from taiga.projects.notifications import models from taiga.projects.notifications import models
from taiga.projects.notifications.choices import NotifyLevel from taiga.projects.notifications.choices import NotifyLevel
from taiga.projects.history.choices import HistoryType from taiga.projects.history.choices import HistoryType
@ -56,7 +57,7 @@ def test_attach_notify_level_to_project_queryset():
f.ProjectFactory.create() f.ProjectFactory.create()
qs = project1.__class__.objects.order_by("id") 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 len(qs) == 2
assert qs[0].notify_level == NotifyLevel.notwatch 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) services.create_notify_policy(project1, project1.owner, NotifyLevel.watch)
qs = project1.__class__.objects.order_by("id") 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[0].notify_level == NotifyLevel.watch
assert qs[1].notify_level == NotifyLevel.notwatch assert qs[1].notify_level == NotifyLevel.notwatch
@ -143,10 +144,25 @@ def test_users_to_notify():
role2 = f.RoleFactory.create(project=project, permissions=[]) role2 = f.RoleFactory.create(project=project, permissions=[])
member1 = f.MembershipFactory.create(project=project, role=role1) 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) 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) 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) 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) 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 = f.MembershipFactory.create(project=project, role=role1)
inactive_member1.user.is_active = False inactive_member1.user.is_active = False
inactive_member1.user.save() inactive_member1.user.save()
@ -158,14 +174,13 @@ def test_users_to_notify():
policy_model_cls = apps.get_model("notifications", "NotifyPolicy") policy_model_cls = apps.get_model("notifications", "NotifyPolicy")
policy1 = policy_model_cls.objects.get(user=member1.user) policy_inactive_member1 = policy_model_cls.objects.get(user=inactive_member1.user)
policy2 = policy_model_cls.objects.get(user=member3.user) policy_inactive_member1.notify_level = NotifyLevel.watch
policy3 = policy_model_cls.objects.get(user=inactive_member1.user) policy_inactive_member1.save()
policy3.notify_level = NotifyLevel.watch
policy3.save() policy_system_member1 = policy_model_cls.objects.get(user=system_member1.user)
policy4 = policy_model_cls.objects.get(user=system_member1.user) policy_system_member1.notify_level = NotifyLevel.watch
policy4.notify_level = NotifyLevel.watch policy_system_member1.save()
policy4.save()
history = MagicMock() history = MagicMock()
history.owner = member2.user history.owner = member2.user
@ -174,13 +189,15 @@ def test_users_to_notify():
# Test basic description modifications # Test basic description modifications
issue.description = "test1" issue.description = "test1"
issue.save() issue.save()
policy_member4.notify_level = NotifyLevel.watch
policy_member4.save()
users = services.get_users_to_notify(issue) users = services.get_users_to_notify(issue)
assert len(users) == 1 assert len(users) == 1
assert tuple(users)[0] == issue.get_owner() assert tuple(users)[0] == issue.get_owner()
# Test watch notify level in one member # Test watch notify level in one member
policy1.notify_level = NotifyLevel.watch policy_member1.notify_level = NotifyLevel.watch
policy1.save() policy_member1.save()
users = services.get_users_to_notify(issue) users = services.get_users_to_notify(issue)
assert len(users) == 2 assert len(users) == 2
@ -188,13 +205,15 @@ def test_users_to_notify():
# Test with watchers # Test with watchers
issue.add_watcher(member3.user) issue.add_watcher(member3.user)
policy_member3.notify_level = NotifyLevel.watch
policy_member3.save()
users = services.get_users_to_notify(issue) users = services.get_users_to_notify(issue)
assert len(users) == 3 assert len(users) == 3
assert users == {member1.user, member3.user, issue.get_owner()} assert users == {member1.user, member3.user, issue.get_owner()}
# Test with watchers with ignore policy # Test with watchers with ignore policy
policy2.notify_level = NotifyLevel.ignore policy_member3.notify_level = NotifyLevel.ignore
policy2.save() policy_member3.save()
issue.add_watcher(member3.user) issue.add_watcher(member3.user)
users = services.get_users_to_notify(issue) users = services.get_users_to_notify(issue)

View File

@ -200,6 +200,7 @@ def test_update_project_timeline():
project = factories.ProjectFactory.create(name="test project timeline") project = factories.ProjectFactory.create(name="test project timeline")
history_services.take_snapshot(project, user=project.owner) history_services.take_snapshot(project, user=project.owner)
project.add_watcher(user_watcher) project.add_watcher(user_watcher)
print("PPPP")
project.name = "test project timeline updated" project.name = "test project timeline updated"
project.save() project.save()
history_services.take_snapshot(project, user=project.owner) history_services.take_snapshot(project, user=project.owner)
@ -341,7 +342,7 @@ def test_update_membership_timeline():
def test_delete_project_timeline(): def test_delete_project_timeline():
project = factories.ProjectFactory.create(name="test project timeline") project = factories.ProjectFactory.create(name="test project timeline")
user_watcher= factories.UserFactory() user_watcher= factories.UserFactory()
project.add_watcher(user_watcher) project.add_watcher(user_watcher)
history_services.take_snapshot(project, user=project.owner, delete=True) history_services.take_snapshot(project, user=project.owner, delete=True)
user_timeline = service.get_project_timeline(project) user_timeline = service.get_project_timeline(project)
assert user_timeline[0].event_type == "projects.project.delete" assert user_timeline[0].event_type == "projects.project.delete"
@ -354,7 +355,7 @@ def test_delete_project_timeline():
def test_delete_milestone_timeline(): def test_delete_milestone_timeline():
milestone = factories.MilestoneFactory.create(name="test milestone timeline") milestone = factories.MilestoneFactory.create(name="test milestone timeline")
user_watcher= factories.UserFactory() user_watcher= factories.UserFactory()
milestone.add_watcher(user_watcher) milestone.add_watcher(user_watcher)
history_services.take_snapshot(milestone, user=milestone.owner, delete=True) history_services.take_snapshot(milestone, user=milestone.owner, delete=True)
project_timeline = service.get_project_timeline(milestone.project) project_timeline = service.get_project_timeline(milestone.project)
assert project_timeline[0].event_type == "milestones.milestone.delete" assert project_timeline[0].event_type == "milestones.milestone.delete"
@ -367,7 +368,7 @@ def test_delete_milestone_timeline():
def test_delete_user_story_timeline(): def test_delete_user_story_timeline():
user_story = factories.UserStoryFactory.create(subject="test us timeline") user_story = factories.UserStoryFactory.create(subject="test us timeline")
user_watcher= factories.UserFactory() 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) history_services.take_snapshot(user_story, user=user_story.owner, delete=True)
project_timeline = service.get_project_timeline(user_story.project) project_timeline = service.get_project_timeline(user_story.project)
assert project_timeline[0].event_type == "userstories.userstory.delete" assert project_timeline[0].event_type == "userstories.userstory.delete"
@ -380,7 +381,7 @@ def test_delete_user_story_timeline():
def test_delete_issue_timeline(): def test_delete_issue_timeline():
issue = factories.IssueFactory.create(subject="test issue timeline") issue = factories.IssueFactory.create(subject="test issue timeline")
user_watcher= factories.UserFactory() user_watcher= factories.UserFactory()
issue.add_watcher(user_watcher) issue.add_watcher(user_watcher)
history_services.take_snapshot(issue, user=issue.owner, delete=True) history_services.take_snapshot(issue, user=issue.owner, delete=True)
project_timeline = service.get_project_timeline(issue.project) project_timeline = service.get_project_timeline(issue.project)
assert project_timeline[0].event_type == "issues.issue.delete" assert project_timeline[0].event_type == "issues.issue.delete"
@ -393,7 +394,7 @@ def test_delete_issue_timeline():
def test_delete_task_timeline(): def test_delete_task_timeline():
task = factories.TaskFactory.create(subject="test task timeline") task = factories.TaskFactory.create(subject="test task timeline")
user_watcher= factories.UserFactory() user_watcher= factories.UserFactory()
task.add_watcher(user_watcher) task.add_watcher(user_watcher)
history_services.take_snapshot(task, user=task.owner, delete=True) history_services.take_snapshot(task, user=task.owner, delete=True)
project_timeline = service.get_project_timeline(task.project) project_timeline = service.get_project_timeline(task.project)
assert project_timeline[0].event_type == "tasks.task.delete" assert project_timeline[0].event_type == "tasks.task.delete"
@ -406,7 +407,7 @@ def test_delete_task_timeline():
def test_delete_wiki_page_timeline(): def test_delete_wiki_page_timeline():
page = factories.WikiPageFactory.create(slug="test wiki page timeline") page = factories.WikiPageFactory.create(slug="test wiki page timeline")
user_watcher= factories.UserFactory() user_watcher= factories.UserFactory()
page.add_watcher(user_watcher) page.add_watcher(user_watcher)
history_services.take_snapshot(page, user=page.owner, delete=True) history_services.take_snapshot(page, user=page.owner, delete=True)
project_timeline = service.get_project_timeline(page.project) project_timeline = service.get_project_timeline(page.project)
assert project_timeline[0].event_type == "wiki.wikipage.delete" assert project_timeline[0].event_type == "wiki.wikipage.delete"

View File

@ -19,6 +19,8 @@ import pytest
import json import json
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
from .. import factories as f from .. import factories as f
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
@ -124,8 +126,10 @@ def test_get_project_watchers(client):
def test_get_project_is_watched(client): def test_get_project_is_watched(client):
user = f.UserFactory.create() user = f.UserFactory.create()
project = f.create_project(owner=user) project = f.ProjectFactory.create(is_private=False,
f.MembershipFactory.create(project=project, user=user, is_owner=True) 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_detail = reverse("projects-detail", args=(project.id,))
url_watch = reverse("projects-watch", args=(project.id,)) url_watch = reverse("projects-watch", args=(project.id,))
url_unwatch = reverse("projects-unwatch", 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) client.login(user)
response = client.get(url_detail) response = client.get(url_detail)
assert response.status_code == 200 assert response.status_code == 200
assert response.data['watchers'] == [] assert response.data['watchers'] == []
assert response.data['is_watched'] == False assert response.data['is_watched'] == False