Merge pull request #451 from taigaio/refactoring-watchers-for-projects
Refactoring watchers for projectsremotes/origin/logger
commit
a3d996bf6b
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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_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):
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -16,7 +16,8 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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})
|
||||
|
|
|
@ -45,27 +45,8 @@ 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)
|
||||
|
||||
## - 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()
|
||||
if hasattr(obj, "get_related_people"):
|
||||
related_people = obj.get_related_people()
|
||||
|
||||
_push_to_timeline(related_people, obj, event_type, created_datetime,
|
||||
namespace=build_user_namespace(user),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue