Web notifications
parent
4fe58359f4
commit
08e31a2ca9
|
@ -1,4 +0,0 @@
|
|||
from .celery import *
|
||||
|
||||
# To use celery in memory
|
||||
#task_always_eager = True
|
|
@ -91,6 +91,22 @@ def emit_event_for_model(obj, *, type:str="change", channel:str="events",
|
|||
sessionid=sessionid,
|
||||
data=data)
|
||||
|
||||
|
||||
def emit_event_for_user_notification(user_id,
|
||||
*,
|
||||
session_id: str=None,
|
||||
event_type: str=None,
|
||||
data: dict=None):
|
||||
"""
|
||||
Sends a user notification event.
|
||||
"""
|
||||
return emit_event(
|
||||
data,
|
||||
"web_notifications.{}".format(user_id),
|
||||
sessionid=session_id
|
||||
)
|
||||
|
||||
|
||||
def emit_live_notification_for_model(obj, user, history, *, type:str="change", channel:str="events",
|
||||
sessionid:str="not-existing"):
|
||||
"""
|
||||
|
|
|
@ -45,6 +45,7 @@ from taiga.projects.epics.models import Epic
|
|||
from taiga.projects.history.mixins import HistoryResourceMixin
|
||||
from taiga.projects.issues.models import Issue
|
||||
from taiga.projects.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin
|
||||
from taiga.projects.notifications.apps import signal_members_added
|
||||
from taiga.projects.notifications.mixins import WatchersViewSetMixin
|
||||
from taiga.projects.notifications.choices import NotifyLevel
|
||||
from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin
|
||||
|
@ -980,6 +981,10 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
|
|||
invitation_extra_text=invitation_extra_text,
|
||||
callback=self.post_save,
|
||||
precall=self.pre_save)
|
||||
signal_members_added.send(sender=self.__class__,
|
||||
user=self.request.user,
|
||||
project=project,
|
||||
new_members=members)
|
||||
except exc.ValidationError as err:
|
||||
return response.BadRequest(err.message_dict)
|
||||
|
||||
|
|
|
@ -24,6 +24,8 @@ from taiga.base import response
|
|||
from taiga.base.decorators import detail_route
|
||||
from taiga.base.api import ReadOnlyListViewSet
|
||||
from taiga.mdrender.service import render as mdrender
|
||||
from taiga.projects.notifications import services as notifications_services
|
||||
from taiga.projects.notifications.apps import signal_mentions
|
||||
|
||||
from . import permissions
|
||||
from . import serializers
|
||||
|
@ -57,6 +59,11 @@ class HistoryViewSet(ReadOnlyListViewSet):
|
|||
|
||||
return response.Ok(serializer.data)
|
||||
|
||||
def _get_new_mentions(self, obj: object, old_comment: str, new_comment: str):
|
||||
old_mentions = notifications_services.get_mentions(obj.project, old_comment)
|
||||
submitted_mentions = notifications_services.get_mentions(obj, new_comment)
|
||||
return list(set(submitted_mentions) - set(old_mentions))
|
||||
|
||||
@detail_route(methods=['get'])
|
||||
def comment_versions(self, request, pk):
|
||||
obj = self.get_object()
|
||||
|
@ -106,11 +113,20 @@ class HistoryViewSet(ReadOnlyListViewSet):
|
|||
}
|
||||
})
|
||||
|
||||
new_mentions = self._get_new_mentions(obj, history_entry.comment, comment)
|
||||
|
||||
history_entry.edit_comment_date = timezone.now()
|
||||
history_entry.comment = comment
|
||||
history_entry.comment_html = mdrender(obj.project, comment)
|
||||
history_entry.comment_versions = comment_versions
|
||||
history_entry.save()
|
||||
|
||||
if new_mentions:
|
||||
signal_mentions.send(sender=self.__class__,
|
||||
user=self.request.user,
|
||||
obj=obj,
|
||||
mentions=new_mentions)
|
||||
|
||||
return response.Ok()
|
||||
|
||||
@detail_route(methods=['post'])
|
||||
|
|
|
@ -30,7 +30,9 @@ from taiga.base.api.utils import get_object_or_404
|
|||
from taiga.projects.history.mixins import HistoryResourceMixin
|
||||
from taiga.projects.mixins.by_ref import ByRefMixin
|
||||
from taiga.projects.models import Project, IssueStatus, Severity, Priority, IssueType
|
||||
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
|
||||
from taiga.projects.notifications.mixins import AssignedToSignalMixin
|
||||
from taiga.projects.notifications.mixins import WatchedResourceMixin
|
||||
from taiga.projects.notifications.mixins import WatchersViewSetMixin
|
||||
from taiga.projects.occ import OCCResourceMixin
|
||||
from taiga.projects.tagging.api import TaggedResourceMixin
|
||||
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
|
||||
|
@ -44,8 +46,10 @@ from . import serializers
|
|||
from . import validators
|
||||
|
||||
|
||||
class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
||||
ByRefMixin, TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
||||
class IssueViewSet(AssignedToSignalMixin, OCCResourceMixin, VotedResourceMixin,
|
||||
HistoryResourceMixin, WatchedResourceMixin, ByRefMixin,
|
||||
TaggedResourceMixin, BlockedByProjectMixin,
|
||||
ModelCrudViewSet):
|
||||
validator_class = validators.IssueValidator
|
||||
queryset = models.Issue.objects.all()
|
||||
permission_classes = (permissions.IssuePermission, )
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (C) 2014-2018 Taiga Agile LLC
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
default_app_config = "taiga.projects.notifications.apps.NotificationsAppConfig"
|
|
@ -17,8 +17,12 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
|
||||
from taiga.base import response
|
||||
from taiga.base.api import ModelCrudViewSet
|
||||
from taiga.base.api import GenericViewSet
|
||||
from taiga.base.api.utils import get_object_or_404
|
||||
|
||||
from taiga.projects.notifications.choices import NotifyLevel
|
||||
from taiga.projects.models import Project
|
||||
|
@ -50,3 +54,50 @@ class NotifyPolicyViewSet(ModelCrudViewSet):
|
|||
return models.NotifyPolicy.objects.filter(user=self.request.user).filter(
|
||||
Q(project__owner=self.request.user) | Q(project__memberships__user=self.request.user)
|
||||
).distinct()
|
||||
|
||||
|
||||
class WebNotificationsViewSet(GenericViewSet):
|
||||
serializer_class = serializers.WebNotificationSerializer
|
||||
resource_model = models.WebNotification
|
||||
|
||||
def check_permissions(self, request, obj=None):
|
||||
return obj and request.user.is_authenticated() and \
|
||||
request.user.pk == obj.user_id
|
||||
|
||||
def list(self, request):
|
||||
queryset = models.WebNotification.objects\
|
||||
.filter(user=self.request.user)
|
||||
|
||||
if request.GET.get("only_unread", False):
|
||||
queryset = queryset.filter(read__isnull=True)
|
||||
|
||||
queryset = queryset.order_by('-read', '-created')
|
||||
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.get_pagination_serializer(page)
|
||||
return response.Ok({
|
||||
"objects": serializer.data,
|
||||
"total": queryset.count()
|
||||
})
|
||||
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return response.Ok(serializer.data)
|
||||
|
||||
def patch(self, request, *args, **kwargs):
|
||||
self.check_permissions(request)
|
||||
|
||||
resource_id = kwargs.get("resource_id", None)
|
||||
resource = get_object_or_404(self.resource_model, pk=resource_id)
|
||||
resource.read = timezone.now()
|
||||
resource.save()
|
||||
|
||||
return response.Ok({})
|
||||
|
||||
def post(self, request):
|
||||
self.check_permissions(request)
|
||||
|
||||
models.WebNotification.objects.filter(user=self.request.user)\
|
||||
.update(read=timezone.now())
|
||||
|
||||
return response.Ok()
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (C) 2014-2018 Taiga Agile LLC
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django import dispatch
|
||||
from django.apps import AppConfig
|
||||
|
||||
signal_assigned_to = dispatch.Signal(providing_args=["user", "obj"])
|
||||
signal_assigned_users = dispatch.Signal(providing_args=["user", "obj",
|
||||
"new_assigned_users"])
|
||||
signal_watchers_added = dispatch.Signal(providing_args=["user", "obj",
|
||||
"new_watchers"])
|
||||
signal_members_added = dispatch.Signal(providing_args=["user", "project",
|
||||
"new_members"])
|
||||
signal_mentions = dispatch.Signal(providing_args=["user", "obj",
|
||||
"mentions"])
|
||||
signal_comment = dispatch.Signal(providing_args=["user", "obj",
|
||||
"watchers"])
|
||||
signal_comment_mentions = dispatch.Signal(providing_args=["user", "obj",
|
||||
"mentions"])
|
||||
|
||||
|
||||
class NotificationsAppConfig(AppConfig):
|
||||
name = "taiga.projects.notifications"
|
||||
verbose_name = "Notifications"
|
||||
|
||||
def ready(self):
|
||||
from . import signals as handlers
|
||||
signal_assigned_to.connect(handlers.on_assigned_to)
|
||||
signal_assigned_users.connect(handlers.on_assigned_users)
|
||||
signal_watchers_added.connect(handlers.on_watchers_added)
|
||||
signal_members_added.connect(handlers.on_members_added)
|
||||
signal_mentions.connect(handlers.on_mentions)
|
||||
signal_comment.connect(handlers.on_comment)
|
||||
signal_comment_mentions.connect(handlers.on_comment_mentions)
|
|
@ -31,3 +31,22 @@ NOTIFY_LEVEL_CHOICES = (
|
|||
(NotifyLevel.all, _("All")),
|
||||
(NotifyLevel.none, _("None")),
|
||||
)
|
||||
|
||||
|
||||
class WebNotificationType(enum.IntEnum):
|
||||
assigned = 1
|
||||
mentioned = 2
|
||||
added_as_watcher = 3
|
||||
added_as_member = 4
|
||||
comment = 5
|
||||
mentioned_in_comment = 6
|
||||
|
||||
|
||||
WEB_NOTIFICATION_TYPE_CHOICES = (
|
||||
(WebNotificationType.assigned, _("Assigned")),
|
||||
(WebNotificationType.mentioned, _("Mentioned")),
|
||||
(WebNotificationType.added_as_watcher, _("Added as watcher")),
|
||||
(WebNotificationType.added_as_member, _("Added as member")),
|
||||
(WebNotificationType.comment, _("Comment")),
|
||||
(WebNotificationType.mentioned_in_comment, _("Mentioned in comment")),
|
||||
)
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.2 on 2018-10-10 11:24
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
import django.core.serializers.json
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import taiga.base.db.models.fields.json
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('notifications', '0007_notifypolicy_live_notify_level'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='WebNotification',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', models.DateTimeField(db_index=True, default=django.utils.timezone.now)),
|
||||
('read', models.DateTimeField(default=None, null=True)),
|
||||
('event_type', models.PositiveIntegerField()),
|
||||
('data', taiga.base.db.models.fields.json.JSONField(encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='web_notifications', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='notifypolicy',
|
||||
name='web_notify_level',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
|
@ -29,6 +29,12 @@ from taiga.base.api.utils import get_object_or_404
|
|||
from taiga.base.fields import WatchersField, MethodField
|
||||
from taiga.projects.notifications import services
|
||||
|
||||
from . apps import signal_assigned_to
|
||||
from . apps import signal_assigned_users
|
||||
from . apps import signal_comment
|
||||
from . apps import signal_comment_mentions
|
||||
from . apps import signal_mentions
|
||||
from . apps import signal_watchers_added
|
||||
from . serializers import WatcherSerializer
|
||||
|
||||
|
||||
|
@ -47,6 +53,8 @@ class WatchedResourceMixin:
|
|||
"""
|
||||
|
||||
_not_notify = False
|
||||
_old_watchers = None
|
||||
_old_mentions = []
|
||||
|
||||
@detail_route(methods=["POST"])
|
||||
def watch(self, request, pk=None):
|
||||
|
@ -86,13 +94,38 @@ class WatchedResourceMixin:
|
|||
# some text fields for extract mentions and add them
|
||||
# to watchers before obtain a complete list of
|
||||
# notifiable users.
|
||||
services.analize_object_for_watchers(obj, history.comment, history.owner)
|
||||
services.analize_object_for_watchers(obj, history.comment,
|
||||
history.owner)
|
||||
|
||||
# Get a complete list of notifiable users for current
|
||||
# object and send the change notification to them.
|
||||
services.send_notifications(obj, history=history)
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
obj = self.get_object_or_none()
|
||||
if obj and obj.id:
|
||||
if hasattr(obj, "watchers"):
|
||||
self._old_watchers = [
|
||||
watcher.id for watcher in self.get_object().get_watchers()
|
||||
]
|
||||
|
||||
mention_fields = ['description', 'content']
|
||||
for field_name in mention_fields:
|
||||
old_mentions = self._get_old_mentions_in_field(obj, field_name)
|
||||
if not len(old_mentions):
|
||||
continue
|
||||
self._old_mentions = old_mentions
|
||||
|
||||
return super().update(request, *args, **kwargs)
|
||||
|
||||
def post_save(self, obj, created=False):
|
||||
self.create_web_notifications_for_added_watchers(obj)
|
||||
self.create_web_notifications_for_mentioned_users(obj)
|
||||
|
||||
mentions = self.create_web_notifications_for_mentions_in_comments(obj)
|
||||
exclude = mentions + [self.request.user.id]
|
||||
self.create_web_notifications_for_comment(obj, exclude)
|
||||
|
||||
self.send_notifications(obj)
|
||||
super().post_save(obj, created)
|
||||
|
||||
|
@ -100,6 +133,84 @@ class WatchedResourceMixin:
|
|||
self.send_notifications(obj)
|
||||
super().pre_delete(obj)
|
||||
|
||||
def create_web_notifications_for_comment(self, obj, exclude: list=None):
|
||||
if "comment" in self.request.DATA:
|
||||
watchers = [
|
||||
watcher_id for watcher_id in obj.watchers
|
||||
if watcher_id not in exclude
|
||||
]
|
||||
|
||||
signal_comment.send(sender=self.__class__,
|
||||
user=self.request.user,
|
||||
obj=obj,
|
||||
watchers=watchers)
|
||||
|
||||
def create_web_notifications_for_added_watchers(self, obj):
|
||||
if not hasattr(obj, "watchers"):
|
||||
return
|
||||
|
||||
new_watchers = [
|
||||
watcher_id for watcher_id in obj.watchers
|
||||
if watcher_id not in self._old_watchers
|
||||
and watcher_id != self.request.user.id
|
||||
]
|
||||
signal_watchers_added.send(sender=self.__class__,
|
||||
user=self.request.user,
|
||||
obj=obj,
|
||||
new_watchers=new_watchers)
|
||||
|
||||
def create_web_notifications_for_mentioned_users(self, obj):
|
||||
"""
|
||||
Detect and notify mentioned users
|
||||
"""
|
||||
submitted_mentions = self._get_submitted_mentions(obj)
|
||||
new_mentions = list(set(submitted_mentions) - set(self._old_mentions))
|
||||
if new_mentions:
|
||||
signal_mentions.send(sender=self.__class__,
|
||||
user=self.request.user,
|
||||
obj=obj,
|
||||
mentions=new_mentions)
|
||||
|
||||
def create_web_notifications_for_mentions_in_comments(self, obj):
|
||||
"""
|
||||
Detect and notify mentioned users
|
||||
"""
|
||||
new_mentions_in_comment = self._get_mentions_in_comment(obj)
|
||||
if new_mentions_in_comment:
|
||||
signal_comment_mentions.send(sender=self.__class__,
|
||||
user=self.request.user,
|
||||
obj=obj,
|
||||
mentions=new_mentions_in_comment)
|
||||
|
||||
return [user.id for user in new_mentions_in_comment]
|
||||
|
||||
def _get_submitted_mentions(self, obj):
|
||||
mention_fields = ['description', 'content']
|
||||
for field_name in mention_fields:
|
||||
new_mentions = self._get_new_mentions_in_field(obj, field_name)
|
||||
if len(new_mentions) > 0:
|
||||
return new_mentions
|
||||
|
||||
return []
|
||||
|
||||
def _get_mentions_in_comment(self, obj):
|
||||
comment = self.request.DATA.get('comment')
|
||||
if comment:
|
||||
return services.get_mentions(obj, comment)
|
||||
return []
|
||||
|
||||
def _get_old_mentions_in_field(self, obj, field_name):
|
||||
if not hasattr(obj, field_name):
|
||||
return []
|
||||
|
||||
return services.get_mentions(obj, getattr(obj, field_name))
|
||||
|
||||
def _get_new_mentions_in_field(self, obj, field_name):
|
||||
value = self.request.DATA.get(field_name)
|
||||
if not value:
|
||||
return []
|
||||
return services.get_mentions(obj, value)
|
||||
|
||||
|
||||
class WatchedModelMixin(object):
|
||||
"""
|
||||
|
@ -274,3 +385,47 @@ class WatchersViewSetMixin:
|
|||
def get_queryset(self):
|
||||
resource = self.resource_model.objects.get(pk=self.kwargs.get("resource_id"))
|
||||
return resource.get_watchers()
|
||||
|
||||
|
||||
class AssignedToSignalMixin:
|
||||
_old_assigned_to = None
|
||||
|
||||
def pre_save(self, obj):
|
||||
if obj.id:
|
||||
self._old_assigned_to = self.get_object().assigned_to
|
||||
super().pre_save(obj)
|
||||
|
||||
def post_save(self, obj, created=False):
|
||||
if obj.assigned_to and obj.assigned_to != self._old_assigned_to \
|
||||
and self.request.user != obj.assigned_to:
|
||||
signal_assigned_to.send(sender=self.__class__,
|
||||
user=self.request.user,
|
||||
obj=obj)
|
||||
super().post_save(obj, created)
|
||||
|
||||
|
||||
class AssignedUsersSignalMixin:
|
||||
_old_assigned_users = None
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
obj = self.get_object_or_none()
|
||||
if hasattr(obj, "assigned_users") and obj.id:
|
||||
self._old_assigned_users = [
|
||||
user for user in obj.assigned_users.all()
|
||||
].copy()
|
||||
|
||||
result = super().update(request, *args, **kwargs)
|
||||
|
||||
if result and obj.assigned_users:
|
||||
new_assigned_users = [
|
||||
user for user in obj.assigned_users.all()
|
||||
if user not in self._old_assigned_users
|
||||
and user != self.request.user
|
||||
]
|
||||
|
||||
signal_assigned_users.send(sender=self.__class__,
|
||||
user=self.request.user,
|
||||
obj=obj,
|
||||
new_assigned_users=new_assigned_users)
|
||||
|
||||
return result
|
||||
|
|
|
@ -23,6 +23,7 @@ from django.db import models
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils import timezone
|
||||
|
||||
from taiga.base.db.models.fields import JSONField
|
||||
from taiga.projects.history.choices import HISTORY_TYPE_CHOICES
|
||||
|
||||
from .choices import NOTIFY_LEVEL_CHOICES, NotifyLevel
|
||||
|
@ -37,6 +38,7 @@ class NotifyPolicy(models.Model):
|
|||
user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="notify_policies")
|
||||
notify_level = models.SmallIntegerField(choices=NOTIFY_LEVEL_CHOICES)
|
||||
live_notify_level = models.SmallIntegerField(choices=NOTIFY_LEVEL_CHOICES, default=NotifyLevel.involved)
|
||||
web_notify_level = models.BooleanField(default=True, null=False, blank=True)
|
||||
|
||||
created_at = models.DateTimeField(default=timezone.now)
|
||||
modified_at = models.DateTimeField()
|
||||
|
@ -94,3 +96,11 @@ class Watched(models.Model):
|
|||
verbose_name = _("Watched")
|
||||
verbose_name_plural = _("Watched")
|
||||
unique_together = ("content_type", "object_id", "user", "project")
|
||||
|
||||
|
||||
class WebNotification(models.Model):
|
||||
created = models.DateTimeField(default=timezone.now, db_index=True)
|
||||
read = models.DateTimeField(default=None, null=True)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="web_notifications")
|
||||
event_type = models.PositiveIntegerField()
|
||||
data = JSONField()
|
||||
|
|
|
@ -16,8 +16,13 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from taiga.base.api import serializers
|
||||
from taiga.base.fields import Field, DateTimeField, MethodField
|
||||
from taiga.users.gravatar import get_user_gravatar_id
|
||||
from taiga.users.models import get_user_model_safe
|
||||
from taiga.users.services import get_user_photo_url, get_user_big_photo_url
|
||||
|
||||
from . import models
|
||||
|
||||
|
@ -27,7 +32,8 @@ class NotifyPolicySerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = models.NotifyPolicy
|
||||
fields = ('id', 'project', 'project_name', 'notify_level', "live_notify_level")
|
||||
fields = ('id', 'project', 'project_name', 'notify_level',
|
||||
'live_notify_level', 'web_notify_level')
|
||||
|
||||
def get_project_name(self, obj):
|
||||
return obj.project.name
|
||||
|
@ -39,3 +45,67 @@ class WatcherSerializer(serializers.ModelSerializer):
|
|||
class Meta:
|
||||
model = get_user_model_safe()
|
||||
fields = ('id', 'username', 'full_name')
|
||||
|
||||
|
||||
class WebNotificationSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = models.WebNotification
|
||||
fields = ('id', 'event_type', 'user', 'data', 'created', 'read')
|
||||
|
||||
|
||||
class ProjectSerializer(serializers.LightSerializer):
|
||||
id = Field()
|
||||
slug = Field()
|
||||
name = Field()
|
||||
|
||||
|
||||
class ObjectSerializer(serializers.LightSerializer):
|
||||
id = Field()
|
||||
ref = MethodField()
|
||||
subject = MethodField()
|
||||
content_type = MethodField()
|
||||
|
||||
def get_ref(self, obj):
|
||||
return obj.ref if hasattr(obj, 'ref') else None
|
||||
|
||||
def get_subject(self, obj):
|
||||
return obj.subject if hasattr(obj, 'subject') else None
|
||||
|
||||
def get_content_type(self, obj):
|
||||
content_type = ContentType.objects.get_for_model(obj)
|
||||
return content_type.model if content_type else None
|
||||
|
||||
|
||||
class UserSerializer(serializers.LightSerializer):
|
||||
id = Field()
|
||||
name = MethodField()
|
||||
photo = MethodField()
|
||||
big_photo = MethodField()
|
||||
gravatar_id = MethodField()
|
||||
username = Field()
|
||||
is_profile_visible = MethodField()
|
||||
date_joined = DateTimeField()
|
||||
|
||||
def get_name(self, obj):
|
||||
return obj.get_full_name()
|
||||
|
||||
def get_photo(self, obj):
|
||||
return get_user_photo_url(obj)
|
||||
|
||||
def get_big_photo(self, obj):
|
||||
return get_user_big_photo_url(obj)
|
||||
|
||||
def get_gravatar_id(self, obj):
|
||||
return get_user_gravatar_id(obj)
|
||||
|
||||
def get_is_profile_visible(self, obj):
|
||||
return obj.is_active and not obj.is_system
|
||||
|
||||
|
||||
class NotificationDataSerializer(serializers.LightDictSerializer):
|
||||
project = ProjectSerializer()
|
||||
user = UserSerializer()
|
||||
|
||||
|
||||
class ObjectNotificationSerializer(NotificationDataSerializer):
|
||||
obj = ObjectSerializer()
|
||||
|
|
|
@ -73,7 +73,8 @@ def create_notify_policy(project, user, level=NotifyLevel.involved,
|
|||
|
||||
def create_notify_policy_if_not_exists(project, user,
|
||||
level=NotifyLevel.involved,
|
||||
live_level=NotifyLevel.involved):
|
||||
live_level=NotifyLevel.involved,
|
||||
web_level=True):
|
||||
"""
|
||||
Given a project and user, create notification policy for it.
|
||||
"""
|
||||
|
@ -82,7 +83,11 @@ def create_notify_policy_if_not_exists(project, user,
|
|||
result = model_cls.objects.get_or_create(
|
||||
project=project,
|
||||
user=user,
|
||||
defaults={"notify_level": level, "live_notify_level": live_level}
|
||||
defaults={
|
||||
"notify_level": level,
|
||||
"live_notify_level": live_level,
|
||||
"web_notify_level": web_level
|
||||
}
|
||||
)
|
||||
return result[0]
|
||||
except IntegrityError as e:
|
||||
|
@ -95,27 +100,39 @@ def analize_object_for_watchers(obj: object, comment: str, user: object):
|
|||
Generic implementation for analize model objects and
|
||||
extract mentions from it and add it to watchers.
|
||||
"""
|
||||
|
||||
if not hasattr(obj, "get_project"):
|
||||
if not hasattr(obj, "add_watcher"):
|
||||
return
|
||||
|
||||
if not hasattr(obj, "add_watcher"):
|
||||
mentions = get_object_mentions(obj, comment)
|
||||
if mentions:
|
||||
for user in mentions:
|
||||
obj.add_watcher(user)
|
||||
|
||||
# Adding the person who edited the object to the watchers
|
||||
if comment and not user.is_system:
|
||||
obj.add_watcher(user)
|
||||
|
||||
|
||||
def get_object_mentions(obj: object, comment: str):
|
||||
"""
|
||||
Generic implementation for analize model objects and
|
||||
extract mentions from it.
|
||||
"""
|
||||
if not hasattr(obj, "get_project"):
|
||||
return
|
||||
|
||||
texts = (getattr(obj, "description", ""),
|
||||
getattr(obj, "content", ""),
|
||||
comment,)
|
||||
|
||||
return get_mentions(obj.get_project(), "\n".join(texts))
|
||||
|
||||
|
||||
def get_mentions(project: object, text: str):
|
||||
from taiga.mdrender.service import render_and_extract
|
||||
_, data = render_and_extract(obj.get_project(), "\n".join(texts))
|
||||
_, data = render_and_extract(project, text)
|
||||
|
||||
if data["mentions"]:
|
||||
for user in data["mentions"]:
|
||||
obj.add_watcher(user)
|
||||
|
||||
# Adding the person who edited the object to the watchers
|
||||
if comment and not user.is_system:
|
||||
obj.add_watcher(user)
|
||||
return data.get("mentions")
|
||||
|
||||
|
||||
def _filter_by_permissions(obj, user):
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (C) 2014-2018 Taiga Agile LLC
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from taiga.events import events
|
||||
from taiga.events import middleware as mw
|
||||
|
||||
from . import choices
|
||||
from . import models
|
||||
from . import serializers
|
||||
|
||||
|
||||
def _filter_recipients(project, user, recipients):
|
||||
notify_policies = models.NotifyPolicy.objects.filter(
|
||||
user_id__in=recipients,
|
||||
project=project,
|
||||
web_notify_level=True).exclude(user_id=user.id).all()
|
||||
return [notify_policy.user_id for notify_policy in notify_policies]
|
||||
|
||||
|
||||
def _push_to_web_notifications(event_type, data, recipients,
|
||||
serializer_class=None):
|
||||
if not serializer_class:
|
||||
serializer_class = serializers.ObjectNotificationSerializer
|
||||
|
||||
serializer = serializer_class(data)
|
||||
for user_id in recipients:
|
||||
with transaction.atomic():
|
||||
models.WebNotification.objects.create(
|
||||
event_type=event_type.value,
|
||||
created=timezone.now(),
|
||||
user_id=user_id,
|
||||
data=serializer.data,
|
||||
)
|
||||
session_id = mw.get_current_session_id()
|
||||
events.emit_event_for_user_notification(user_id,
|
||||
session_id=session_id,
|
||||
event_type=event_type.value,
|
||||
data=serializer.data)
|
||||
|
||||
|
||||
def on_assigned_to(sender, user, obj, **kwargs):
|
||||
event_type = choices.WebNotificationType.assigned
|
||||
data = {
|
||||
"project": obj.project,
|
||||
"user": user,
|
||||
"obj": obj,
|
||||
}
|
||||
recipients = _filter_recipients(obj.project, user,
|
||||
[obj.assigned_to.id])
|
||||
_push_to_web_notifications(event_type, data, recipients)
|
||||
|
||||
|
||||
def on_assigned_users(sender, user, obj, new_assigned_users, **kwargs):
|
||||
event_type = choices.WebNotificationType.assigned
|
||||
data = {
|
||||
"project": obj.project,
|
||||
"user": user,
|
||||
"obj": obj,
|
||||
}
|
||||
recipients = _filter_recipients(obj.project, user,
|
||||
[user.id for user in new_assigned_users])
|
||||
_push_to_web_notifications(event_type, data, recipients)
|
||||
|
||||
|
||||
def on_watchers_added(sender, user, obj, new_watchers, **kwargs):
|
||||
event_type = choices.WebNotificationType.added_as_watcher
|
||||
data = {
|
||||
"project": obj.project,
|
||||
"user": user,
|
||||
"obj": obj,
|
||||
}
|
||||
recipients = _filter_recipients(obj.project, user, new_watchers)
|
||||
_push_to_web_notifications(event_type, data, recipients)
|
||||
|
||||
|
||||
def on_members_added(sender, user, project, new_members, **kwargs):
|
||||
serializer_class = serializers.NotificationDataSerializer
|
||||
event_type = choices.WebNotificationType.added_as_member
|
||||
data = {
|
||||
"project": project,
|
||||
"user": user,
|
||||
}
|
||||
recipients = _filter_recipients(project, user,
|
||||
[member.user_id for member in new_members
|
||||
if member.user_id])
|
||||
|
||||
_push_to_web_notifications(event_type, data, recipients, serializer_class)
|
||||
|
||||
|
||||
def on_mentions(sender, user, obj, mentions, **kwargs):
|
||||
content_type = ContentType.objects.get_for_model(obj)
|
||||
valid_content_types = ['issue', 'task', 'userstory']
|
||||
if content_type.model in valid_content_types:
|
||||
event_type = choices.WebNotificationType.mentioned
|
||||
data = {
|
||||
"project": obj.project,
|
||||
"user": user,
|
||||
"obj": obj,
|
||||
}
|
||||
recipients = _filter_recipients(obj.project, user,
|
||||
[user.id for user in mentions])
|
||||
_push_to_web_notifications(event_type, data, recipients)
|
||||
|
||||
|
||||
def on_comment_mentions(sender, user, obj, mentions, **kwargs):
|
||||
event_type = choices.WebNotificationType.mentioned_in_comment
|
||||
data = {
|
||||
"project": obj.project,
|
||||
"user": user,
|
||||
"obj": obj,
|
||||
}
|
||||
recipients = _filter_recipients(obj.project, user,
|
||||
[user.id for user in mentions])
|
||||
_push_to_web_notifications(event_type, data, recipients)
|
||||
|
||||
|
||||
def on_comment(sender, user, obj, watchers, **kwargs):
|
||||
event_type = choices.WebNotificationType.comment
|
||||
data = {
|
||||
"project": obj.project,
|
||||
"user": user,
|
||||
"obj": obj,
|
||||
}
|
||||
recipients = _filter_recipients(obj.project, user, watchers)
|
||||
_push_to_web_notifications(event_type, data, recipients)
|
|
@ -30,7 +30,9 @@ from taiga.projects.history.mixins import HistoryResourceMixin
|
|||
from taiga.projects.milestones.models import Milestone
|
||||
from taiga.projects.mixins.by_ref import ByRefMixin
|
||||
from taiga.projects.models import Project, TaskStatus
|
||||
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
|
||||
from taiga.projects.notifications.mixins import AssignedToSignalMixin
|
||||
from taiga.projects.notifications.mixins import WatchedResourceMixin
|
||||
from taiga.projects.notifications.mixins import WatchersViewSetMixin
|
||||
from taiga.projects.occ import OCCResourceMixin
|
||||
from taiga.projects.tagging.api import TaggedResourceMixin
|
||||
from taiga.projects.userstories.models import UserStory
|
||||
|
@ -45,8 +47,10 @@ from . import validators
|
|||
from . import utils as tasks_utils
|
||||
|
||||
|
||||
class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
||||
ByRefMixin, TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
||||
class TaskViewSet(AssignedToSignalMixin, OCCResourceMixin, VotedResourceMixin,
|
||||
HistoryResourceMixin, WatchedResourceMixin, ByRefMixin,
|
||||
TaggedResourceMixin, BlockedByProjectMixin,
|
||||
ModelCrudViewSet):
|
||||
validator_class = validators.TaskValidator
|
||||
queryset = models.Task.objects.all()
|
||||
permission_classes = (permissions.TaskPermission,)
|
||||
|
|
|
@ -39,6 +39,7 @@ from taiga.projects.history.services import take_snapshot
|
|||
from taiga.projects.milestones.models import Milestone
|
||||
from taiga.projects.mixins.by_ref import ByRefMixin
|
||||
from taiga.projects.models import Project, UserStoryStatus
|
||||
from taiga.projects.notifications.mixins import AssignedUsersSignalMixin
|
||||
from taiga.projects.notifications.mixins import WatchedResourceMixin
|
||||
from taiga.projects.notifications.mixins import WatchersViewSetMixin
|
||||
from taiga.projects.occ import OCCResourceMixin
|
||||
|
@ -55,8 +56,10 @@ from . import services
|
|||
from . import validators
|
||||
|
||||
|
||||
class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
||||
ByRefMixin, TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
||||
class UserStoryViewSet(AssignedUsersSignalMixin, OCCResourceMixin,
|
||||
VotedResourceMixin, HistoryResourceMixin,
|
||||
WatchedResourceMixin, ByRefMixin, TaggedResourceMixin,
|
||||
BlockedByProjectMixin, ModelCrudViewSet):
|
||||
validator_class = validators.UserStoryValidator
|
||||
queryset = models.UserStory.objects.all()
|
||||
permission_classes = (permissions.UserStoryPermission,)
|
||||
|
|
|
@ -43,11 +43,14 @@ from taiga.userstorage.api import StorageEntriesViewSet
|
|||
router.register(r"user-storage", StorageEntriesViewSet, base_name="user-storage")
|
||||
|
||||
|
||||
# Notify policies
|
||||
# Notifications & Notify policies
|
||||
from taiga.projects.notifications.api import NotifyPolicyViewSet
|
||||
from taiga.projects.notifications.api import WebNotificationsViewSet
|
||||
|
||||
router.register(r"notify-policies", NotifyPolicyViewSet, base_name="notifications")
|
||||
|
||||
router.register(r"web-notifications", WebNotificationsViewSet, base_name="web-notifications")
|
||||
router.register(r"web-notifications/set-as-read", WebNotificationsViewSet, base_name="web-notifications")
|
||||
router.register(r"web-notifications/(?P<resource_id>\d+)/set-as-read", WebNotificationsViewSet, base_name="web-notifications")
|
||||
|
||||
# Project settings
|
||||
from taiga.projects.settings.api import UserProjectSettingsViewSet, SectionsViewSet
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
|
||||
import pytest
|
||||
import time
|
||||
import math
|
||||
import base64
|
||||
import datetime
|
||||
import hashlib
|
||||
|
@ -35,17 +34,16 @@ from django.utils import timezone
|
|||
from django.apps import apps
|
||||
from .. import factories as f
|
||||
|
||||
from taiga.base.api.settings import api_settings
|
||||
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.notifications.choices import WebNotificationType
|
||||
from taiga.projects.history.choices import HistoryType
|
||||
from taiga.projects.history.services import take_snapshot
|
||||
from taiga.projects.issues.serializers import IssueSerializer
|
||||
from taiga.projects.userstories.serializers import UserStorySerializer
|
||||
from taiga.projects.tasks.serializers import TaskSerializer
|
||||
from taiga.permissions.choices import MEMBERS_PERMISSIONS
|
||||
from taiga.users.gravatar import get_user_gravatar_id
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
@ -1074,3 +1072,340 @@ def parse_ms_thread_index(index):
|
|||
ts.append(ts[-1] + datetime.timedelta(microseconds=(f << 18)//10))
|
||||
|
||||
return guid, ts
|
||||
|
||||
|
||||
def _notification_data(project, user, obj, content_type):
|
||||
return {
|
||||
"project": {
|
||||
"id": project.pk,
|
||||
"slug": project.slug,
|
||||
"name": project.name,
|
||||
},
|
||||
"obj": {
|
||||
"id": obj.pk,
|
||||
"ref": obj.ref,
|
||||
"subject": obj.subject,
|
||||
"content_type": content_type,
|
||||
},
|
||||
"user": {
|
||||
'big_photo': None,
|
||||
'date_joined': user.date_joined.strftime(
|
||||
api_settings.DATETIME_FORMAT),
|
||||
'gravatar_id': get_user_gravatar_id(user),
|
||||
'id': user.pk,
|
||||
'is_profile_visible': True,
|
||||
'name': user.get_full_name(),
|
||||
'photo': None,
|
||||
'username': user.username
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_issue_updated_generates_web_notifications(client):
|
||||
project = f.ProjectFactory.create()
|
||||
role = f.RoleFactory.create(
|
||||
project=project,
|
||||
permissions=['view_issues', 'modify_issue']
|
||||
)
|
||||
member1 = f.MembershipFactory.create(project=project, role=role)
|
||||
member2 = f.MembershipFactory.create(project=project, role=role)
|
||||
member3 = f.MembershipFactory.create(project=project, role=role)
|
||||
member4 = f.MembershipFactory.create(project=project, role=role)
|
||||
issue = f.IssueFactory.create(project=project, owner=member1.user)
|
||||
|
||||
client.login(member1.user)
|
||||
mock_path = "taiga.projects.issues.api.IssueViewSet.pre_conditions_on_save"
|
||||
with patch(mock_path):
|
||||
client.patch(
|
||||
reverse("issues-detail", args=[issue.pk]),
|
||||
json.dumps({
|
||||
"description": "Lorem ipsum @%s dolor sit amet" %
|
||||
member4.user.username,
|
||||
"assigned_to": member2.user.pk,
|
||||
"watchers": [member3.user.pk],
|
||||
"version": issue.version
|
||||
}),
|
||||
content_type="application/json"
|
||||
)
|
||||
|
||||
assert 3 == models.WebNotification.objects.count()
|
||||
|
||||
notifications = models.WebNotification.objects.all()
|
||||
notification_data = _notification_data(project, member1.user, issue,
|
||||
'issue')
|
||||
# Notification assigned_to
|
||||
assert notifications[0].user == member2.user
|
||||
assert notifications[0].event_type == WebNotificationType.assigned.value
|
||||
assert notifications[0].read is None
|
||||
assert notifications[0].data == notification_data
|
||||
|
||||
# Notification added_as_watcher
|
||||
assert notifications[1].user == member3.user
|
||||
assert notifications[1].event_type == WebNotificationType.added_as_watcher
|
||||
assert notifications[1].read is None
|
||||
assert notifications[1].data == notification_data
|
||||
|
||||
# Notification mentioned
|
||||
assert notifications[2].user == member4.user
|
||||
assert notifications[2].event_type == WebNotificationType.mentioned
|
||||
assert notifications[2].read is None
|
||||
assert notifications[2].data == notification_data
|
||||
|
||||
|
||||
def test_comment_on_issue_generates_web_notifications(client):
|
||||
project = f.ProjectFactory.create()
|
||||
role = f.RoleFactory.create(
|
||||
project=project,
|
||||
permissions=['view_issues', 'modify_issue']
|
||||
)
|
||||
member1 = f.MembershipFactory.create(project=project, role=role)
|
||||
member2 = f.MembershipFactory.create(project=project, role=role)
|
||||
issue = f.IssueFactory.create(project=project, owner=member1.user)
|
||||
issue.add_watcher(member2.user)
|
||||
|
||||
client.login(member1.user)
|
||||
mock_path = "taiga.projects.issues.api.IssueViewSet.pre_conditions_on_save"
|
||||
with patch(mock_path):
|
||||
client.patch(
|
||||
reverse("issues-detail", args=[issue.pk]),
|
||||
json.dumps({
|
||||
"version": issue.version,
|
||||
"comment": "Lorem ipsum dolor sit amet",
|
||||
}),
|
||||
content_type="application/json"
|
||||
)
|
||||
|
||||
assert 1 == models.WebNotification.objects.count()
|
||||
|
||||
notification = models.WebNotification.objects.first()
|
||||
notification_data = _notification_data(project, member1.user, issue,
|
||||
'issue')
|
||||
|
||||
# Notification comment
|
||||
assert notification.user == member2.user
|
||||
assert notification.event_type == WebNotificationType.comment
|
||||
assert notification.read is None
|
||||
assert notification.data == notification_data
|
||||
|
||||
|
||||
def test_task_updated_generates_web_notifications(client):
|
||||
project = f.ProjectFactory.create()
|
||||
role = f.RoleFactory.create(
|
||||
project=project,
|
||||
permissions=['view_tasks', 'modify_task']
|
||||
)
|
||||
member1 = f.MembershipFactory.create(project=project, role=role)
|
||||
member2 = f.MembershipFactory.create(project=project, role=role)
|
||||
member3 = f.MembershipFactory.create(project=project, role=role)
|
||||
member4 = f.MembershipFactory.create(project=project, role=role)
|
||||
task = f.TaskFactory.create(project=project, owner=member1.user)
|
||||
|
||||
client.login(member1.user)
|
||||
mock_path = "taiga.projects.tasks.api.TaskViewSet.pre_conditions_on_save"
|
||||
with patch(mock_path):
|
||||
client.patch(
|
||||
reverse("tasks-detail", args=[task.pk]),
|
||||
json.dumps({
|
||||
"description": "Lorem ipsum @%s dolor sit amet" %
|
||||
member4.user.username,
|
||||
"assigned_to": member2.user.pk,
|
||||
"watchers": [member3.user.pk],
|
||||
"version": task.version
|
||||
}),
|
||||
content_type="application/json"
|
||||
)
|
||||
|
||||
assert 3 == models.WebNotification.objects.count()
|
||||
|
||||
notifications = models.WebNotification.objects.all()
|
||||
notification_data = _notification_data(project, member1.user, task, 'task')
|
||||
|
||||
# Notification assigned_to
|
||||
assert notifications[0].user == member2.user
|
||||
assert notifications[0].event_type == WebNotificationType.assigned.value
|
||||
assert notifications[0].read is None
|
||||
assert notifications[0].data == notification_data
|
||||
|
||||
# Notification added_as_watcher
|
||||
assert notifications[1].user == member3.user
|
||||
assert notifications[1].event_type == WebNotificationType.added_as_watcher
|
||||
assert notifications[1].read is None
|
||||
assert notifications[1].data == notification_data
|
||||
|
||||
# Notification mentioned
|
||||
assert notifications[2].user == member4.user
|
||||
assert notifications[2].event_type == WebNotificationType.mentioned
|
||||
assert notifications[2].read is None
|
||||
assert notifications[2].data == notification_data
|
||||
|
||||
|
||||
def test_comment_on_task_generates_web_notifications(client):
|
||||
project = f.ProjectFactory.create()
|
||||
role = f.RoleFactory.create(
|
||||
project=project,
|
||||
permissions=['view_tasks', 'modify_task']
|
||||
)
|
||||
member1 = f.MembershipFactory.create(project=project, role=role)
|
||||
member2 = f.MembershipFactory.create(project=project, role=role)
|
||||
task = f.TaskFactory.create(project=project, owner=member1.user)
|
||||
task.add_watcher(member2.user)
|
||||
|
||||
client.login(member1.user)
|
||||
mock_path = "taiga.projects.tasks.api.TaskViewSet.pre_conditions_on_save"
|
||||
with patch(mock_path):
|
||||
client.patch(
|
||||
reverse("tasks-detail", args=[task.pk]),
|
||||
json.dumps({
|
||||
"version": task.version,
|
||||
"comment": "Lorem ipsum dolor sit amet",
|
||||
}),
|
||||
content_type="application/json"
|
||||
)
|
||||
|
||||
assert 1 == models.WebNotification.objects.count()
|
||||
|
||||
notification = models.WebNotification.objects.first()
|
||||
notification_data = _notification_data(project, member1.user, task, 'task')
|
||||
|
||||
# Notification comment
|
||||
assert notification.user == member2.user
|
||||
assert notification.event_type == WebNotificationType.comment
|
||||
assert notification.read is None
|
||||
assert notification.data == notification_data
|
||||
|
||||
|
||||
def test_us_updated_generates_web_notifications(client):
|
||||
project = f.ProjectFactory.create()
|
||||
role = f.RoleFactory.create(
|
||||
project=project,
|
||||
permissions=['view_us', 'modify_us']
|
||||
)
|
||||
member1 = f.MembershipFactory.create(project=project, role=role)
|
||||
member2 = f.MembershipFactory.create(project=project, role=role)
|
||||
member3 = f.MembershipFactory.create(project=project, role=role)
|
||||
member4 = f.MembershipFactory.create(project=project, role=role)
|
||||
us = f.UserStoryFactory.create(project=project,
|
||||
owner=member1.user,
|
||||
milestone=None)
|
||||
|
||||
client.login(member1.user)
|
||||
mock_path = "taiga.projects.userstories.api.UserStoryViewSet." \
|
||||
"pre_conditions_on_save"
|
||||
with patch(mock_path):
|
||||
client.patch(
|
||||
reverse("userstories-detail", args=[us.pk]),
|
||||
json.dumps({
|
||||
"description": "Lorem ipsum @%s dolor sit amet" %
|
||||
member4.user.username,
|
||||
"assigned_users": [member2.user.pk],
|
||||
"watchers": [member3.user.pk],
|
||||
"version": us.version
|
||||
}),
|
||||
content_type="application/json"
|
||||
)
|
||||
|
||||
assert 3 == models.WebNotification.objects.count()
|
||||
|
||||
notifications = models.WebNotification.objects.all()
|
||||
notification_data = _notification_data(project, member1.user, us,
|
||||
'userstory')
|
||||
|
||||
# Notification added_as_watcher
|
||||
assert notifications[0].user == member3.user
|
||||
assert notifications[0].event_type == WebNotificationType.added_as_watcher
|
||||
assert notifications[0].read is None
|
||||
assert notifications[0].data == notification_data
|
||||
|
||||
# Notification mentioned
|
||||
assert notifications[1].user == member4.user
|
||||
assert notifications[1].event_type == WebNotificationType.mentioned
|
||||
assert notifications[1].read is None
|
||||
assert notifications[1].data == notification_data
|
||||
|
||||
# Notification assigned_users
|
||||
assert notifications[2].user == member2.user
|
||||
assert notifications[2].event_type == WebNotificationType.assigned.value
|
||||
assert notifications[2].read is None
|
||||
assert notifications[2].data == notification_data
|
||||
|
||||
|
||||
def test_comment_on_us_generates_web_notifications(client):
|
||||
project = f.ProjectFactory.create()
|
||||
role = f.RoleFactory.create(
|
||||
project=project,
|
||||
permissions=['view_us', 'modify_us']
|
||||
)
|
||||
member1 = f.MembershipFactory.create(project=project, role=role)
|
||||
member2 = f.MembershipFactory.create(project=project, role=role)
|
||||
us = f.UserStoryFactory.create(project=project,
|
||||
owner=member1.user,
|
||||
milestone=None)
|
||||
us.add_watcher(member2.user)
|
||||
|
||||
client.login(member1.user)
|
||||
mock_path = "taiga.projects.userstories.api.UserStoryViewSet." \
|
||||
"pre_conditions_on_save"
|
||||
with patch(mock_path):
|
||||
client.patch(
|
||||
reverse("userstories-detail", args=[us.pk]),
|
||||
json.dumps({
|
||||
"version": us.version,
|
||||
"comment": "Lorem ipsum dolor sit amet",
|
||||
}),
|
||||
content_type="application/json"
|
||||
)
|
||||
|
||||
assert 1 == models.WebNotification.objects.count()
|
||||
|
||||
notification = models.WebNotification.objects.first()
|
||||
notification_data = _notification_data(project, member1.user, us,
|
||||
'userstory')
|
||||
|
||||
# Notification comment
|
||||
assert notification.user == member2.user
|
||||
assert notification.event_type == WebNotificationType.comment
|
||||
assert notification.read is None
|
||||
assert notification.data == notification_data
|
||||
|
||||
|
||||
def test_new_member_generates_web_notifications(client):
|
||||
project = f.ProjectFactory()
|
||||
john = f.UserFactory.create()
|
||||
joseph = f.UserFactory.create()
|
||||
other = f.UserFactory.create()
|
||||
tester = f.RoleFactory(project=project, name="Tester",
|
||||
permissions=["view_project"])
|
||||
gamer = f.RoleFactory(project=project, name="Gamer",
|
||||
permissions=["view_project"])
|
||||
f.MembershipFactory(project=project, user=john, role=tester, is_admin=True)
|
||||
|
||||
# John and Other are members from another project
|
||||
project2 = f.ProjectFactory()
|
||||
f.MembershipFactory(project=project2, user=john, role=gamer, is_admin=True)
|
||||
f.MembershipFactory(project=project2, user=other, role=gamer)
|
||||
|
||||
url = reverse("memberships-bulk-create")
|
||||
|
||||
data = {
|
||||
"project_id": project.id,
|
||||
"bulk_memberships": [
|
||||
{"role_id": gamer.pk, "username": joseph.email},
|
||||
{"role_id": gamer.pk, "username": other.username},
|
||||
]
|
||||
}
|
||||
client.login(john)
|
||||
client.json.post(url, json.dumps(data))
|
||||
|
||||
assert models.WebNotification.objects.count() == 2
|
||||
|
||||
notifications = models.WebNotification.objects.all()
|
||||
|
||||
# Notification added_as_member
|
||||
assert notifications[0].user == joseph
|
||||
assert notifications[0].event_type == WebNotificationType.added_as_member
|
||||
assert notifications[0].read is None
|
||||
|
||||
# Notification added_as_member
|
||||
assert notifications[1].user == other
|
||||
assert notifications[1].event_type == WebNotificationType.added_as_member
|
||||
assert notifications[1].read is None
|
||||
|
|
Loading…
Reference in New Issue