diff --git a/settings/common.py b/settings/common.py index c79edb63..3aef9bdf 100644 --- a/settings/common.py +++ b/settings/common.py @@ -332,6 +332,7 @@ TAGS_PREDEFINED_COLORS = ["#fce94f", "#edd400", "#c4a000", "#8ae234", FEEDBACK_ENABLED = True FEEDBACK_EMAIL = "support@taiga.io" +CHANGE_NOTIFICATIONS_MIN_INTERVAL = 30 #seconds # NOTE: DON'T INSERT MORE SETTINGS AFTER THIS LINE TEST_RUNNER="django.test.runner.DiscoverRunner" diff --git a/taiga/projects/history/models.py b/taiga/projects/history/models.py index ab58b312..ec79f91f 100644 --- a/taiga/projects/history/models.py +++ b/taiga/projects/history/models.py @@ -76,6 +76,18 @@ class HistoryEntry(models.Model): # snapshot. The rest are partial snapshot. is_snapshot = models.BooleanField(default=False) + @cached_property + def is_change(self): + return self.type == HistoryType.change + + @cached_property + def is_create(self): + return self.type == HistoryType.create + + @cached_property + def is_delete(self): + return self.type == HistoryType.delete + @cached_property def owner(self): pk = self.user["pk"] @@ -198,4 +210,3 @@ class HistoryEntry(models.Model): class Meta: ordering = ["created_at"] - diff --git a/taiga/projects/history/services.py b/taiga/projects/history/services.py index 3401f42a..31e56f99 100644 --- a/taiga/projects/history/services.py +++ b/taiga/projects/history/services.py @@ -73,6 +73,14 @@ def make_key_from_model_object(obj:object) -> str: return "{0}:{1}".format(tn, obj.pk) +def get_model_from_key(key:str) -> object: + """ + Get model from key + """ + class_name, pk = key.split(":", 1) + return apps.get_model(class_name) + + def register_values_implementation(typename:str, fn=None): """ Register values implementation for specified typename. diff --git a/taiga/projects/notifications/mixins.py b/taiga/projects/notifications/mixins.py index f5be6166..362635a4 100644 --- a/taiga/projects/notifications/mixins.py +++ b/taiga/projects/notifications/mixins.py @@ -62,8 +62,7 @@ class WatchedResourceMixin(object): # Get a complete list of notifiable users for current # object and send the change notification to them. - users = services.get_users_to_notify(obj, history=history) - services.send_notifications(obj, history=history, users=users) + services.send_notifications(obj, history=history) def post_save(self, obj, created=False): self.send_notifications(obj) diff --git a/taiga/projects/notifications/models.py b/taiga/projects/notifications/models.py index bdff3d90..1b62ce0a 100644 --- a/taiga/projects/notifications/models.py +++ b/taiga/projects/notifications/models.py @@ -19,7 +19,7 @@ from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from .choices import NOTIFY_LEVEL_CHOICES - +from taiga.projects.history.choices import HISTORY_TYPE_CHOICES class NotifyPolicy(models.Model): """ @@ -43,3 +43,27 @@ class NotifyPolicy(models.Model): self.modified_at = timezone.now() return super().save(*args, **kwargs) + + +class HistoryChangeNotification(models.Model): + """ + This class controls the pending notifications for an object, it should be instantiated + or updated when an object requires notifications. + """ + key = models.CharField(max_length=255, unique=False, editable=False) + owner = models.ForeignKey("users.User", null=False, blank=False, + verbose_name="owner",related_name="+") + created_datetime = models.DateTimeField(null=False, blank=False, auto_now_add=True, + verbose_name=_("created date time")) + updated_datetime = models.DateTimeField(null=False, blank=False, auto_now_add=True, + verbose_name=_("updated date time")) + history_entries = models.ManyToManyField("history.HistoryEntry", null=True, blank=True, + verbose_name="history entries", + related_name="+") + notify_users = models.ManyToManyField("users.User", null=True, blank=True, + verbose_name="notify users", + related_name="+") + project = models.ForeignKey("projects.Project", null=False, blank=False, + verbose_name="project",related_name="+") + + history_type = models.SmallIntegerField(choices=HISTORY_TYPE_CHOICES) diff --git a/taiga/projects/notifications/services.py b/taiga/projects/notifications/services.py index 1b8c86c0..746beace 100644 --- a/taiga/projects/notifications/services.py +++ b/taiga/projects/notifications/services.py @@ -19,6 +19,9 @@ from functools import partial from django.apps import apps from django.db import IntegrityError from django.contrib.contenttypes.models import ContentType +from django.utils import timezone +from django.db import transaction +from django.conf import settings from djmail import template_mail @@ -26,7 +29,12 @@ 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, + get_last_snapshot_for_key, + get_model_from_key) +from taiga.users.models import User +from .models import HistoryChangeNotification def notify_policy_exists(project, user) -> bool: """ @@ -113,7 +121,7 @@ def analize_object_for_watchers(obj:object, history:object): obj.watchers.add(user) -def get_users_to_notify(obj, *, history) -> list: +def get_users_to_notify(obj, *, discard_users=None) -> list: """ Get filtered set of users to notify for specified model instance and changer. @@ -138,18 +146,18 @@ def get_users_to_notify(obj, *, history) -> list: candidates.update(filter(_can_notify_light, obj.get_participants())) # Remove the changer from candidates - candidates.discard(history.owner) + if discard_users: + candidates = candidates - set(discard_users) return frozenset(candidates) -def _resolve_template_name(obj, *, change_type:int) -> str: +def _resolve_template_name(model:object, *, change_type:int) -> str: """ Ginven an changed model instance and change type, return the preformated template name for it. """ - ct = ContentType.objects.get_for_model(obj.__class__) - + ct = ContentType.objects.get_for_model(model) # Resolve integer enum value from "change_type" # parameter to human readable string if change_type == HistoryType.create: @@ -158,7 +166,6 @@ def _resolve_template_name(obj, *, change_type:int) -> str: change_type = "change" else: change_type = "delete" - tmpl = "{app_label}/{model}-{change}" return tmpl.format(app_label=ct.app_label, model=ct.model, @@ -178,19 +185,61 @@ def _make_template_mail(name:str): return cls() -def send_notifications(obj, *, history, users): +@transaction.atomic +def send_notifications(obj, *, history): + key = make_key_from_model_object(obj) + owner = User.objects.get(pk=history.user["pk"]) + notification, created = (HistoryChangeNotification.objects.select_for_update() + .get_or_create(key=key, + owner=owner, + project=obj.project, + history_type = history.type)) + + notification.updated_datetime = timezone.now() + notification.save() + notification.history_entries.add(history) + + # Get a complete list of notifiable users for current + # object and send the change notification to them. + notify_users = get_users_to_notify(obj, discard_users=[notification.owner]) + for notify_user in notify_users: + notification.notify_users.add(notify_user) + + +@transaction.atomic +def send_sync_notifications(notification_id): """ - Given changed instance, history entry and + Given changed instance, calculate the history entry and a complete list for users to notify, send email to all users. """ - context = {"object": obj, - "changer": history.owner, - "comment": history.comment, - "changed_fields": history.values_diff} - template_name = _resolve_template_name(obj, change_type=history.type) + notification = HistoryChangeNotification.objects.select_for_update().get(pk=notification_id) + # If the las modification is too recent we ignore it + now = timezone.now() + time_diff = now - notification.updated_datetime + if time_diff.seconds < settings.CHANGE_NOTIFICATIONS_MIN_INTERVAL: + print(time_diff.seconds) + return + + history_entries = tuple(notification.history_entries.all().order_by("created_at")) + obj, _ = get_last_snapshot_for_key(notification.key) + + context = {"snapshot": obj.snapshot, + "project": notification.project, + "changer": notification.owner, + "history_entries": history_entries} + + model = get_model_from_key(notification.key) + template_name = _resolve_template_name(model, change_type=notification.history_type) email = _make_template_mail(template_name) - for user in users: + for user in notification.notify_users.distinct(): email.send(user.email, context) + + notification.delete() + + +def process_sync_notifications(): + for notification in HistoryChangeNotification.objects.all(): + send_sync_notifications(notification.pk) diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index f5648fec..c1120ff9 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -177,4 +177,3 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi self.send_notifications(self.object.generated_from_issue, history) return response -