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):
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

View File

@ -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):

View File

@ -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,

View File

@ -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()

View File

@ -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()

View File

@ -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:

View File

@ -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})

View File

@ -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

View File

@ -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)

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_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):

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 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):

View File

@ -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)

View File

@ -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)

View File

@ -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