Refactoring modify notifications
parent
6b33c30616
commit
a68785a380
|
@ -332,6 +332,7 @@ TAGS_PREDEFINED_COLORS = ["#fce94f", "#edd400", "#c4a000", "#8ae234",
|
||||||
FEEDBACK_ENABLED = True
|
FEEDBACK_ENABLED = True
|
||||||
FEEDBACK_EMAIL = "support@taiga.io"
|
FEEDBACK_EMAIL = "support@taiga.io"
|
||||||
|
|
||||||
|
CHANGE_NOTIFICATIONS_MIN_INTERVAL = 30 #seconds
|
||||||
|
|
||||||
# NOTE: DON'T INSERT MORE SETTINGS AFTER THIS LINE
|
# NOTE: DON'T INSERT MORE SETTINGS AFTER THIS LINE
|
||||||
TEST_RUNNER="django.test.runner.DiscoverRunner"
|
TEST_RUNNER="django.test.runner.DiscoverRunner"
|
||||||
|
|
|
@ -76,6 +76,18 @@ class HistoryEntry(models.Model):
|
||||||
# snapshot. The rest are partial snapshot.
|
# snapshot. The rest are partial snapshot.
|
||||||
is_snapshot = models.BooleanField(default=False)
|
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
|
@cached_property
|
||||||
def owner(self):
|
def owner(self):
|
||||||
pk = self.user["pk"]
|
pk = self.user["pk"]
|
||||||
|
@ -198,4 +210,3 @@ class HistoryEntry(models.Model):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["created_at"]
|
ordering = ["created_at"]
|
||||||
|
|
||||||
|
|
|
@ -73,6 +73,14 @@ def make_key_from_model_object(obj:object) -> str:
|
||||||
return "{0}:{1}".format(tn, obj.pk)
|
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):
|
def register_values_implementation(typename:str, fn=None):
|
||||||
"""
|
"""
|
||||||
Register values implementation for specified typename.
|
Register values implementation for specified typename.
|
||||||
|
|
|
@ -62,8 +62,7 @@ class WatchedResourceMixin(object):
|
||||||
|
|
||||||
# Get a complete list of notifiable users for current
|
# Get a complete list of notifiable users for current
|
||||||
# object and send the change notification to them.
|
# object and send the change notification to them.
|
||||||
users = services.get_users_to_notify(obj, history=history)
|
services.send_notifications(obj, history=history)
|
||||||
services.send_notifications(obj, history=history, users=users)
|
|
||||||
|
|
||||||
def post_save(self, obj, created=False):
|
def post_save(self, obj, created=False):
|
||||||
self.send_notifications(obj)
|
self.send_notifications(obj)
|
||||||
|
|
|
@ -19,7 +19,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from .choices import NOTIFY_LEVEL_CHOICES
|
from .choices import NOTIFY_LEVEL_CHOICES
|
||||||
|
from taiga.projects.history.choices import HISTORY_TYPE_CHOICES
|
||||||
|
|
||||||
class NotifyPolicy(models.Model):
|
class NotifyPolicy(models.Model):
|
||||||
"""
|
"""
|
||||||
|
@ -43,3 +43,27 @@ class NotifyPolicy(models.Model):
|
||||||
self.modified_at = timezone.now()
|
self.modified_at = timezone.now()
|
||||||
|
|
||||||
return super().save(*args, **kwargs)
|
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)
|
||||||
|
|
|
@ -19,6 +19,9 @@ from functools import partial
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.contrib.contenttypes.models import ContentType
|
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
|
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.base.utils.text import strip_lines
|
||||||
from taiga.projects.notifications.choices import NotifyLevel
|
from taiga.projects.notifications.choices import NotifyLevel
|
||||||
from taiga.projects.history.choices import HistoryType
|
from taiga.projects.history.choices import HistoryType
|
||||||
|
from taiga.projects.history.services import (make_key_from_model_object,
|
||||||
|
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:
|
def notify_policy_exists(project, user) -> bool:
|
||||||
"""
|
"""
|
||||||
|
@ -113,7 +121,7 @@ def analize_object_for_watchers(obj:object, history:object):
|
||||||
obj.watchers.add(user)
|
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
|
Get filtered set of users to notify for specified
|
||||||
model instance and changer.
|
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()))
|
candidates.update(filter(_can_notify_light, obj.get_participants()))
|
||||||
|
|
||||||
# Remove the changer from candidates
|
# Remove the changer from candidates
|
||||||
candidates.discard(history.owner)
|
if discard_users:
|
||||||
|
candidates = candidates - set(discard_users)
|
||||||
|
|
||||||
return frozenset(candidates)
|
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,
|
Ginven an changed model instance and change type,
|
||||||
return the preformated template name for it.
|
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"
|
# Resolve integer enum value from "change_type"
|
||||||
# parameter to human readable string
|
# parameter to human readable string
|
||||||
if change_type == HistoryType.create:
|
if change_type == HistoryType.create:
|
||||||
|
@ -158,7 +166,6 @@ def _resolve_template_name(obj, *, change_type:int) -> str:
|
||||||
change_type = "change"
|
change_type = "change"
|
||||||
else:
|
else:
|
||||||
change_type = "delete"
|
change_type = "delete"
|
||||||
|
|
||||||
tmpl = "{app_label}/{model}-{change}"
|
tmpl = "{app_label}/{model}-{change}"
|
||||||
return tmpl.format(app_label=ct.app_label,
|
return tmpl.format(app_label=ct.app_label,
|
||||||
model=ct.model,
|
model=ct.model,
|
||||||
|
@ -178,19 +185,61 @@ def _make_template_mail(name:str):
|
||||||
return cls()
|
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
|
a complete list for users to notify, send
|
||||||
email to all users.
|
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)
|
email = _make_template_mail(template_name)
|
||||||
|
|
||||||
for user in users:
|
for user in notification.notify_users.distinct():
|
||||||
email.send(user.email, context)
|
email.send(user.email, context)
|
||||||
|
|
||||||
|
notification.delete()
|
||||||
|
|
||||||
|
|
||||||
|
def process_sync_notifications():
|
||||||
|
for notification in HistoryChangeNotification.objects.all():
|
||||||
|
send_sync_notifications(notification.pk)
|
||||||
|
|
|
@ -177,4 +177,3 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi
|
||||||
self.send_notifications(self.object.generated_from_issue, history)
|
self.send_notifications(self.object.generated_from_issue, history)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue