Refactoring modify notifications

remotes/origin/enhancement/email-actions
Alejandro Alonso 2014-10-28 18:06:58 +01:00
parent 6b33c30616
commit a68785a380
7 changed files with 110 additions and 19 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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