Merge pull request #144 from taigaio/notification-refactor
Notification refactorremotes/origin/enhancement/email-actions
commit
dc90036e55
|
@ -332,6 +332,10 @@ TAGS_PREDEFINED_COLORS = ["#fce94f", "#edd400", "#c4a000", "#8ae234",
|
|||
FEEDBACK_ENABLED = True
|
||||
FEEDBACK_EMAIL = "support@taiga.io"
|
||||
|
||||
# 0 notifications will work in a synchronous way
|
||||
# >0 an external process will check the pending notifications and will send them
|
||||
# collapsed during that interval
|
||||
CHANGE_NOTIFICATIONS_MIN_INTERVAL = 0 #seconds
|
||||
|
||||
# NOTE: DON'T INSERT MORE SETTINGS AFTER THIS LINE
|
||||
TEST_RUNNER="django.test.runner.DiscoverRunner"
|
||||
|
|
|
@ -31,7 +31,7 @@ from taiga.base import filters
|
|||
from taiga.base import exceptions as exc
|
||||
from taiga.users.models import User
|
||||
|
||||
from taiga.projects.notifications import WatchedResourceMixin
|
||||
from taiga.projects.notifications.mixins import WatchedResourceMixin
|
||||
from taiga.projects.history.mixins import HistoryResourceMixin
|
||||
|
||||
from . import permissions
|
||||
|
|
|
@ -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"]
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -99,12 +99,12 @@
|
|||
{# DESCRIPTIONS #}
|
||||
{% elif field_name in ["description_diff"] %}
|
||||
<dd style="background: #eee; padding: 5px 15px; color: #444">
|
||||
<b>diff:</b> <i>{{ mdrender(object.project, values.1) }}</i>
|
||||
<b>diff:</b> <i>{{ mdrender(project, values.1) }}</i>
|
||||
</dd>
|
||||
{# CONTENT #}
|
||||
{% elif field_name in ["content_diff"] %}
|
||||
<dd style="background: #eee; padding: 5px 15px; color: #444">
|
||||
<b>diff:</b> <i>{{ mdrender(object.project, values.1) }}</i>
|
||||
<b>diff:</b> <i>{{ mdrender(project, values.1) }}</i>
|
||||
</dd>
|
||||
{# ASSIGNED TO #}
|
||||
{% elif field_name == "assigned_to" %}
|
||||
|
|
|
@ -30,7 +30,7 @@ from taiga.base import tags
|
|||
|
||||
from taiga.users.models import User
|
||||
|
||||
from taiga.projects.notifications import WatchedResourceMixin
|
||||
from taiga.projects.notifications.mixins import WatchedResourceMixin
|
||||
from taiga.projects.occ import OCCResourceMixin
|
||||
from taiga.projects.history.mixins import HistoryResourceMixin
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ from django.dispatch import receiver
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from taiga.projects.occ import OCCModelMixin
|
||||
from taiga.projects.notifications import WatchedModelMixin
|
||||
from taiga.projects.notifications.mixins import WatchedModelMixin
|
||||
from taiga.projects.mixins.blocked import BlockedMixin
|
||||
from taiga.base.tags import TaggedMixin
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ from taiga.base import exceptions as exc
|
|||
from taiga.base.decorators import detail_route
|
||||
from taiga.base.api import ModelCrudViewSet
|
||||
|
||||
from taiga.projects.notifications import WatchedResourceMixin
|
||||
from taiga.projects.notifications.mixins import WatchedResourceMixin
|
||||
from taiga.projects.history.mixins import HistoryResourceMixin
|
||||
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ from django.core.exceptions import ValidationError
|
|||
|
||||
from taiga.base.utils.slug import slugify_uniquely
|
||||
from taiga.base.utils.dicts import dict_sum
|
||||
from taiga.projects.notifications import WatchedModelMixin
|
||||
from taiga.projects.notifications.mixins import WatchedModelMixin
|
||||
from taiga.projects.userstories.models import UserStory
|
||||
|
||||
import itertools
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||
# 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 .mixins import WatchedResourceMixin
|
||||
from .mixins import WatchedModelMixin
|
||||
|
||||
__all__ = ["WatchedModelMixin", "WatchedResourceMixin"]
|
|
@ -0,0 +1,29 @@
|
|||
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||
# 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.core.management.base import BaseCommand
|
||||
|
||||
from taiga.base.utils.iterators import iter_queryset
|
||||
from taiga.projects.notifications.models import HistoryChangeNotification
|
||||
from taiga.projects.notifications.services import send_sync_notifications
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
def handle(self, *args, **options):
|
||||
qs = HistoryChangeNotification.objects.all()
|
||||
for change_notification in iter_queryset(qs, itersize=100):
|
||||
send_sync_notifications(change_notification.pk)
|
|
@ -0,0 +1,35 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('projects', '0005_membership_invitation_extra_text'),
|
||||
('history', '0004_historyentry_is_hidden'),
|
||||
('notifications', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='HistoryChangeNotification',
|
||||
fields=[
|
||||
('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)),
|
||||
('key', models.CharField(max_length=255, editable=False)),
|
||||
('created_datetime', models.DateTimeField(verbose_name='created date time', auto_now_add=True)),
|
||||
('updated_datetime', models.DateTimeField(verbose_name='updated date time', auto_now_add=True)),
|
||||
('history_type', models.SmallIntegerField(choices=[(1, 'Change'), (2, 'Create'), (3, 'Delete')])),
|
||||
('history_entries', models.ManyToManyField(blank=True, null=True, to='history.HistoryEntry', verbose_name='history entries', related_name='+')),
|
||||
('notify_users', models.ManyToManyField(blank=True, null=True, to=settings.AUTH_USER_MODEL, verbose_name='notify users', related_name='+')),
|
||||
('owner', models.ForeignKey(related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='owner')),
|
||||
('project', models.ForeignKey(related_name='+', to='projects.Project', verbose_name='project')),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,63 @@ 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)
|
||||
|
||||
# If we are the min interval is 0 it just work in a synchronous and spamming way
|
||||
if settings.CHANGE_NOTIFICATIONS_MIN_INTERVAL == 0:
|
||||
send_sync_notifications(notification.id)
|
||||
|
||||
@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:
|
||||
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)
|
||||
|
|
|
@ -1,22 +1,24 @@
|
|||
{% extends "emails/base.jinja" %}
|
||||
|
||||
{% set final_url = resolve_front_url("issue", object.project.slug, object.ref) %}
|
||||
{% set final_url_name = "Taiga - View issue #{0}".format(object.ref) %}
|
||||
{% set final_url = resolve_front_url("issue", project.slug, snapshot.ref) %}
|
||||
{% set final_url_name = "Taiga - View issue #{0}".format(snapshot.ref) %}
|
||||
|
||||
{% block body %}
|
||||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
||||
<tr>
|
||||
<td>
|
||||
<h1>Project: {{ object.project.name }}</h1>
|
||||
<h2>Issue #{{ object.ref }}: {{ object.subject }}</h2>
|
||||
<h1>Project: {{ project.name }}</h1>
|
||||
<h2>Issue #{{ snapshot.ref }}: {{ snapshot.subject }}</h2>
|
||||
<p>Updated by <b>{{ changer.get_full_name() }}</b>.</p>
|
||||
{% if comment %}
|
||||
<p>Comment <b>{{ mdrender(object.project, comment) }}</b></p>
|
||||
{% endif %}
|
||||
{% if changed_fields %}
|
||||
<p>Updated fields:</p>
|
||||
{% include "emails/includes/fields_diff-html.jinja" %}
|
||||
{% endif %}
|
||||
{% for entry in history_entries%}
|
||||
{% if entry.comment %}
|
||||
<p>Comment <b>{{ mdrender(project, entry.comment) }}</b></p>
|
||||
{% endif %}
|
||||
{% set changed_fields = entry.values_diff %}
|
||||
{% if changed_fields %}
|
||||
{% include "emails/includes/fields_diff-html.jinja" %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
{% set final_url = resolve_front_url("issue", object.project.slug, object.ref) %}
|
||||
{% set final_url_name = "Taiga - View issue #{0}".format(object.ref) %}
|
||||
{% set final_url = resolve_front_url("issue", project.slug, snapshot.ref) %}
|
||||
{% set final_url_name = "Taiga - View issue #{0}".format(snapshot.ref) %}
|
||||
|
||||
- Project: {{ object.project.name }}
|
||||
- Issue #{{ object.ref }}: {{ object.subject }}
|
||||
- Project: {{ project.name }}
|
||||
- Issue #{{ snapshot.ref }}: {{ snapshot.subject }}
|
||||
- Updated by {{ changer.get_full_name() }}
|
||||
{% if comment %}
|
||||
Comment: {{ comment|linebreaksbr }}
|
||||
{% endif %}
|
||||
{% if changed_fields %}
|
||||
- Updated fields:
|
||||
{% include "emails/includes/fields_diff-text.jinja" %}
|
||||
{% endif %}
|
||||
{% for entry in history_entries%}
|
||||
{% if entry.comment %}
|
||||
Comment: {{ entry.comment|linebreaksbr }}
|
||||
{% endif %}
|
||||
{% set changed_fields = entry.values_diff %}
|
||||
{% if changed_fields %}
|
||||
{% include "emails/includes/fields_diff-text.jinja" %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
** More info at {{ final_url_name }} ({{ final_url }}) **
|
||||
|
|
|
@ -1 +1 @@
|
|||
[{{ object.project.name|safe }}] Updated the issue #{{ object.ref|safe }} "{{ object.subject|safe }}"
|
||||
[{{ project.name|safe }}] Updated the issue #{{ snapshot.ref|safe }} "{{ snapshot.subject|safe }}"
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
{% extends "emails/base.jinja" %}
|
||||
|
||||
{% set final_url = resolve_front_url("issue", object.project.slug, object.ref) %}
|
||||
{% set final_url_name = "Taiga - View issue #{0}".format(object.ref) %}
|
||||
{% set final_url = resolve_front_url("issue", project.slug, snapshot.ref) %}
|
||||
{% set final_url_name = "Taiga - View issue #{0}".format(snapshot.ref) %}
|
||||
|
||||
{% block body %}
|
||||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
||||
<tr>
|
||||
<td>
|
||||
<h1>Project: {{ object.project.name }}</h1>
|
||||
<h2>Issue #{{ object.ref }}: {{ object.subject }}</h2>
|
||||
<h1>Project: {{ project.name }}</h1>
|
||||
<h2>Issue #{{ snapshot.ref }}: {{ snapshot.subject }}</h2>
|
||||
<p>Created by <b>{{ changer.get_full_name() }}</b>.</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% set final_url = resolve_front_url("issue", object.project.slug, object.ref) %}
|
||||
{% set final_url_name = "Taiga - View issue #{0}".format(object.ref) %}
|
||||
{% set final_url = resolve_front_url("issue", project.slug, snapshot.ref) %}
|
||||
{% set final_url_name = "Taiga - View issue #{0}".format(snapshot.ref) %}
|
||||
|
||||
- Project: {{ object.project.name }}
|
||||
- US #{{ object.ref }}: {{ object.subject }}
|
||||
- Project: {{ project.name }}
|
||||
- US #{{ snapshot.ref }}: {{ snapshot.subject }}
|
||||
- Created by {{ changer.get_full_name() }}
|
||||
|
||||
** More info at {{ final_url_name }} ({{ final_url }}) **
|
||||
|
|
|
@ -1 +1 @@
|
|||
[{{ object.project.name|safe }}] Created the issue #{{ object.ref|safe }} "{{ object.subject|safe }}"
|
||||
[{{ project.name|safe }}] Created the issue #{{ snapshot.ref|safe }} "{{ snapshot.subject|safe }}"
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
||||
<tr>
|
||||
<td>
|
||||
<h1>{{ object.project.name }}</h1>
|
||||
<h2>Issue #{{ object.ref }}: {{ object.subject }}</h2>
|
||||
<h1>{{ project.name }}</h1>
|
||||
<h2>Issue #{{ snapshot.ref }}: {{ snapshot.subject }}</h2>
|
||||
<p>Deleted by <b>{{ changer.get_full_name() }}</b></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
- Project: {{ object.project.name }}
|
||||
- Issue #{{ object.ref }}: {{ object.subject }}
|
||||
- Project: {{ project.name }}
|
||||
- Issue #{{ snapshot.ref }}: {{ snapshot.subject }}
|
||||
- Deleted by {{ changer.get_full_name() }}
|
||||
|
|
|
@ -1 +1 @@
|
|||
[{{ object.project.name|safe }}] Deleted the issue #{{ object.ref|safe }} "{{ object.subject|safe }}"
|
||||
[{{ project.name|safe }}] Deleted the issue #{{ snapshot.ref|safe }} "{{ snapshot.subject|safe }}"
|
||||
|
|
|
@ -1,22 +1,24 @@
|
|||
{% extends "emails/base.jinja" %}
|
||||
|
||||
{% set final_url = resolve_front_url("taskboard", object.project.slug, object.slug) %}
|
||||
{% set final_url_name = "Taiga - View milestone #{0}".format(object.slug) %}
|
||||
{% set final_url = resolve_front_url("taskboard", project.slug, snapshot.slug) %}
|
||||
{% set final_url_name = "Taiga - View milestone #{0}".format(snapshot.slug) %}
|
||||
|
||||
{% block body %}
|
||||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
||||
<tr>
|
||||
<td>
|
||||
<h1>Project: {{ object.project.name }}</h1>
|
||||
<h2>Milestone #{{ object.slug }}: {{ object.name }}</h2>
|
||||
<h1>Project: {{ project.name }}</h1>
|
||||
<h2>Milestone #{{ snapshot.slug }}: {{ snapshot.name }}</h2>
|
||||
<p>Updated by <b>{{ changer.get_full_name() }}</b>.</p>
|
||||
{% if comment %}
|
||||
<p>Comment <b>{{ comment|linebreaksbr }}</b></p>
|
||||
{% endif %}
|
||||
{% if changed_fields %}
|
||||
<p>Updated fields:</p>
|
||||
{% include "emails/includes/fields_diff-html.jinja" %}
|
||||
{% endif %}
|
||||
{% for entry in history_entries%}
|
||||
{% if entry.comment %}
|
||||
<p>Comment <b>{{ entry.comment|linebreaksbr }}</b></p>
|
||||
{% endif %}
|
||||
{% set changed_fields = entry.values_diff %}
|
||||
{% if changed_fields %}
|
||||
{% include "emails/includes/fields_diff-html.jinja" %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
{% set final_url = resolve_front_url("taskboard", object.project.slug, object.slug) %}
|
||||
{% set final_url_name = "Taiga - View milestone #{0}".format(object.slug) %}
|
||||
{% set final_url = resolve_front_url("taskboard", project.slug, snapshot.slug) %}
|
||||
{% set final_url_name = "Taiga - View milestone #{0}".format(snapshot.slug) %}
|
||||
|
||||
- Project: {{ object.project.name }}
|
||||
- Milestone #{{ object.slug }}: {{ object.name }}
|
||||
- Project: {{ project.name }}
|
||||
- Milestone #{{ snapshot.slug }}: {{ snapshot.name }}
|
||||
- Updated by {{ changer.get_full_name() }}
|
||||
{% if comment %}
|
||||
Comment: {{ comment|linebreaksbr }}
|
||||
{% endif %}
|
||||
{% if changed_fields %}
|
||||
- Updated fields:
|
||||
{% include "emails/includes/fields_diff-text.jinja" %}
|
||||
{% endif %}
|
||||
{% for entry in history_entries%}
|
||||
{% if entry.comment %}
|
||||
Comment: {{ entry.comment|linebreaksbr }}
|
||||
{% endif %}
|
||||
{% set changed_fields = entry.values_diff %}
|
||||
{% if changed_fields %}
|
||||
{% include "emails/includes/fields_diff-text.jinja" %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
** More info at {{ final_url_name }} ({{ final_url }}) **
|
||||
|
|
|
@ -1 +1 @@
|
|||
[{{ object.project.name|safe }}] Updated the milestone #{{ object.slug|safe }} "{{ object.name|safe }}"
|
||||
[{{ project.name|safe }}] Updated the milestone #{{ snapshot.slug|safe }} "{{ snapshot.name|safe }}"
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
{% extends "emails/base.jinja" %}
|
||||
|
||||
{% set final_url = resolve_front_url("taskboard", object.project.slug, object.slug) %}
|
||||
{% set final_url_name = "Taiga - View milestone #{0}".format(object.slug) %}
|
||||
{% set final_url = resolve_front_url("taskboard", project.slug, snapshot.slug) %}
|
||||
{% set final_url_name = "Taiga - View milestone #{0}".format(snapshot.slug) %}
|
||||
|
||||
{% block body %}
|
||||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
||||
<tr>
|
||||
<td>
|
||||
<h1>Project: {{ object.project.name }}</h1>
|
||||
<h2>Milestone #{{ object.slug }}: {{ object.name }}</h2>
|
||||
<h1>Project: {{ project.name }}</h1>
|
||||
<h2>Milestone #{{ snapshot.slug }}: {{ snapshot.name }}</h2>
|
||||
<p>Created by <b>{{ changer.get_full_name() }}</b>.</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% set final_url = resolve_front_url("taskboard", object.project.slug, object.slug) %}
|
||||
{% set final_url_name = "Taiga - View milestone #{0}".format(object.slug) %}
|
||||
{% set final_url = resolve_front_url("taskboard", project.slug, snapshot.slug) %}
|
||||
{% set final_url_name = "Taiga - View milestone #{0}".format(snapshot.slug) %}
|
||||
|
||||
- Project: {{ object.project.name }}
|
||||
- Milestone #{{ object.slug }}: {{ object.name }}
|
||||
- Project: {{ project.name }}
|
||||
- Milestone #{{ snapshot.slug }}: {{ snapshot.name }}
|
||||
- Created by {{ changer.get_full_name() }}
|
||||
|
||||
** More info at {{ final_url_name }} ({{ final_url }}) **
|
||||
|
|
|
@ -1 +1 @@
|
|||
[{{ object.project.name|safe }}] Created the milestone #{{ object.slug|safe }} "{{ object.name|safe }}"
|
||||
[{{ project.name|safe }}] Created the milestone #{{ snapshot.slug|safe }} "{{ snapshot.name|safe }}"
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
||||
<tr>
|
||||
<td>
|
||||
<h1>{{ object.project.name }}</h1>
|
||||
<h2>Milestone #{{ object.slug }}: {{ object.name }}</h2>
|
||||
<h1>{{ project.name }}</h1>
|
||||
<h2>Milestone #{{ snapshot.slug }}: {{ snapshot.name }}</h2>
|
||||
<p>Deleted by <b>{{ changer.get_full_name() }}</b></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
- Project: {{ object.project.name }}
|
||||
- Milestone #{{ object.slug }}: {{ object.name }}
|
||||
- Project: {{ project.name }}
|
||||
- Milestone #{{ snapshot.slug }}: {{ snapshot.name }}
|
||||
- Deleted by {{ changer.get_full_name() }}
|
||||
|
|
|
@ -1 +1 @@
|
|||
[{{ object.project.name|safe }}] Deleted the milestone #{{ object.slug|safe }} "{{ object.name|safe }}"
|
||||
[{{ project.name|safe }}] Deleted the milestone #{{ snapshot.slug|safe }} "{{ snapshot.name|safe }}"
|
||||
|
|
|
@ -1,21 +1,23 @@
|
|||
{% extends "emails/base.jinja" %}
|
||||
|
||||
{% set final_url = resolve_front_url("project-admin", object.slug) %}
|
||||
{% set final_url_name = "Taiga - View Project #{0}".format(object.slug) %}
|
||||
{% set final_url = resolve_front_url("project-admin", snapshot.slug) %}
|
||||
{% set final_url_name = "Taiga - View Project #{0}".format(snapshot.slug) %}
|
||||
|
||||
{% block body %}
|
||||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
||||
<tr>
|
||||
<td>
|
||||
<h2>Project #{{ object.slug }}: {{ object.name }}</h2>
|
||||
<h2>Project #{{ snapshot.slug }}: {{ snapshot.name }}</h2>
|
||||
<p>Updated by <b>{{ changer.get_full_name() }}</b>.</p>
|
||||
{% if comment %}
|
||||
<p>Comment <b>{{ comment|linebreaksbr }}</b></p>
|
||||
{% endif %}
|
||||
{% if changed_fields %}
|
||||
<p>Updated fields:</p>
|
||||
{% include "emails/includes/fields_diff-html.jinja" %}
|
||||
{% endif %}
|
||||
{% for entry in history_entries%}
|
||||
{% if entry.comment %}
|
||||
<p>Comment <b>{{ entry.comment|linebreaksbr }}</b></p>
|
||||
{% endif %}
|
||||
{% set changed_fields = entry.values_diff %}
|
||||
{% if changed_fields %}
|
||||
{% include "emails/includes/fields_diff-html.jinja" %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
{% set final_url = resolve_front_url("project-admin", object.slug) %}
|
||||
{% set final_url_name = "Taiga - View Project #{0}".format(object.slug) %}
|
||||
{% set final_url = resolve_front_url("project-admin", snapshot.slug) %}
|
||||
{% set final_url_name = "Taiga - View Project #{0}".format(snapshot.slug) %}
|
||||
|
||||
- Project #{{ object.slug }}: {{ object.name }}
|
||||
- Project #{{ snapshot.slug }}: {{ snapshot.name }}
|
||||
- Updated by {{ changer.get_full_name() }}
|
||||
{% if comment %}
|
||||
Comment: {{ comment|linebreaksbr }}
|
||||
{% endif %}
|
||||
{% if changed_fields %}
|
||||
- Updated fields:
|
||||
{% for field_name, values in changed_fields.items() %}
|
||||
* {{ verbose_name(object, field_name) }}</b>: from '{{ values.0 }}' to '{{ values.1 }}'.
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% for entry in history_entries%}
|
||||
{% if entry.comment %}
|
||||
Comment: {{ entry.comment|linebreaksbr }}
|
||||
{% endif %}
|
||||
{% set changed_fields = entry.values_diff %}
|
||||
{% for field_name, values in changed_fields.items() %}
|
||||
* {{ verbose_name(object, field_name) }}</b>: from '{{ values.0 }}' to '{{ values.1 }}'.
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
** More info at {{ final_url_name }} ({{ final_url }}) **
|
||||
|
|
|
@ -1 +1 @@
|
|||
[{{ object.name|safe }}] Updated the project #{{ object.slug|safe }}
|
||||
[{{ snapshot.name|safe }}] Updated the project #{{ snapshot.slug|safe }}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
{% set final_url = resolve_front_url("project-admin", object.slug) %}
|
||||
{% set final_url_name = "Taiga - View Project #{0}".format(object.slug) %}
|
||||
{% set final_url = resolve_front_url("project-admin", snapshot.slug) %}
|
||||
{% set final_url_name = "Taiga - View Project #{0}".format(snapshot.slug) %}
|
||||
|
||||
{% block body %}
|
||||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
||||
<tr>
|
||||
<td>
|
||||
<h1>Project: {{ object.project.name }}</h1>
|
||||
<h2>Project #{{ object.slug }}: {{ object.name }}</h2>
|
||||
<h1>Project: {{ project.name }}</h1>
|
||||
<h2>Project #{{ snapshot.slug }}: {{ snapshot.name }}</h2>
|
||||
<p>Created by <b>{{ changer.get_full_name() }}</b>.</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% set final_url = resolve_front_url("project-admin", object.slug) %}
|
||||
{% set final_url_name = "Taiga - View Project #{0}".format(object.slug) %}
|
||||
{% set final_url = resolve_front_url("project-admin", snapshot.slug) %}
|
||||
{% set final_url_name = "Taiga - View Project #{0}".format(snapshot.slug) %}
|
||||
|
||||
- Project #{{ object.slug }}: {{ object.name }}
|
||||
- Project #{{ snapshot.slug }}: {{ snapshot.name }}
|
||||
- Created by {{ changer.get_full_name() }}
|
||||
|
||||
** More info at {{ final_url_name }} ({{ final_url }}) **
|
||||
|
|
|
@ -1 +1 @@
|
|||
[{{ object.name|safe }}] Created the project #{{ object.slug|safe }}
|
||||
[{{ snapshot.name|safe }}] Created the project #{{ snapshot.slug|safe }}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
||||
<tr>
|
||||
<td>
|
||||
<h2>Project #{{ object.slug }}: {{ object.name }}</h2>
|
||||
<h2>Project #{{ snapshot.slug }}: {{ snapshot.name }}</h2>
|
||||
<p>Deleted by <b>{{ changer.get_full_name() }}</b></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
- Project #{{ object.slug }}: {{ object.name }}
|
||||
- Project #{{ snapshot.slug }}: {{ snapshot.name }}
|
||||
- Deleted by {{ changer.get_full_name() }}
|
||||
|
|
|
@ -1 +1 @@
|
|||
[{{ object.name|safe }}] Deleted the project #{{ object.slug|safe }}
|
||||
[{{ snapshot.name|safe }}] Deleted the project #{{ snapshot.slug|safe }}
|
||||
|
|
|
@ -1,22 +1,24 @@
|
|||
{% extends "emails/base.jinja" %}
|
||||
|
||||
{% set final_url = resolve_front_url("task", object.project.slug, object.ref) %}
|
||||
{% set final_url_name = "Taiga - View task #{0}".format(object.ref) %}
|
||||
{% set final_url = resolve_front_url("task", project.slug, snapshot.ref) %}
|
||||
{% set final_url_name = "Taiga - View task #{0}".format(snapshot.ref) %}
|
||||
|
||||
{% block body %}
|
||||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
||||
<tr>
|
||||
<td>
|
||||
<h1>Project: {{ object.project.name }}</h1>
|
||||
<h2>Task #{{ object.ref }}: {{ object.subject }}</h2>
|
||||
<h1>Project: {{ project.name }}</h1>
|
||||
<h2>Task #{{ snapshot.ref }}: {{ snapshot.subject }}</h2>
|
||||
<p>Updated by <b>{{ changer.get_full_name() }}</b>.</p>
|
||||
{% if comment %}
|
||||
<p>Comment <b>{{ mdrender(object.project, comment) }}</b></p>
|
||||
{% endif %}
|
||||
{% if changed_fields %}
|
||||
<p>Updated fields:</p>
|
||||
{% include "emails/includes/fields_diff-html.jinja" %}
|
||||
{% endif %}
|
||||
{% for entry in history_entries%}
|
||||
{% if entry.comment %}
|
||||
<p>Comment <b>{{ mdrender(project, entry.comment) }}</b></p>
|
||||
{% endif %}
|
||||
{% set changed_fields = entry.values_diff %}
|
||||
{% if changed_fields %}
|
||||
{% include "emails/includes/fields_diff-html.jinja" %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
{% set final_url = resolve_front_url("task", object.project.slug, object.ref) %}
|
||||
{% set final_url_name = "Taiga - View task #{0}".format(object.ref) %}
|
||||
{% set final_url = resolve_front_url("task", project.slug, snapshot.ref) %}
|
||||
{% set final_url_name = "Taiga - View task #{0}".format(snapshot.ref) %}
|
||||
|
||||
- Project: {{ object.project.name }}
|
||||
- Task #{{ object.ref }}: {{ object.subject }}
|
||||
- Project: {{ project.name }}
|
||||
- Task #{{ snapshot.ref }}: {{ snapshot.subject }}
|
||||
- Updated by {{ changer.get_full_name() }}
|
||||
{% if comment %}
|
||||
Comment: {{ comment|linebreaksbr }}
|
||||
{% endif %}
|
||||
{% if changed_fields %}
|
||||
- Updated fields:
|
||||
{% include "emails/includes/fields_diff-text.jinja" %}
|
||||
{% endif %}
|
||||
{% for entry in history_entries%}
|
||||
{% if entry.comment %}
|
||||
Comment: {{ entry.comment|linebreaksbr }}
|
||||
{% endif %}
|
||||
{% set changed_fields = entry.values_diff %}
|
||||
{% if changed_fields %}
|
||||
{% include "emails/includes/fields_diff-text.jinja" %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
** More info at {{ final_url_name }} ({{ final_url }}) **
|
||||
|
|
|
@ -1 +1 @@
|
|||
[{{ object.project.name|safe }}] Updated the task #{{ object.ref }} "{{ object.subject|safe }}"
|
||||
[{{ project.name|safe }}] Updated the task #{{ snapshot.ref }} "{{ snapshot.subject|safe }}"
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
{% extends "emails/base.jinja" %}
|
||||
|
||||
{% set final_url = resolve_front_url("task", object.project.slug, object.ref) %}
|
||||
{% set final_url_name = "Taiga - View task #{0}".format(object.ref) %}
|
||||
{% set final_url = resolve_front_url("task", project.slug, snapshot.ref) %}
|
||||
{% set final_url_name = "Taiga - View task #{0}".format(snapshot.ref) %}
|
||||
|
||||
{% block body %}
|
||||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
||||
<tr>
|
||||
<td>
|
||||
<h1>Project: {{ object.project.name }}</h1>
|
||||
<h2>Task #{{ object.ref }}: {{ object.subject }}</h2>
|
||||
<h1>Project: {{ project.name }}</h1>
|
||||
<h2>Task #{{ snapshot.ref }}: {{ snapshot.subject }}</h2>
|
||||
<p>Created by <b>{{ changer.get_full_name() }}</b>.</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% set final_url = resolve_front_url("task", object.project.slug, object.ref) %}
|
||||
{% set final_url_name = "Taiga - View task #{0}".format(object.ref) %}
|
||||
{% set final_url = resolve_front_url("task", project.slug, snapshot.ref) %}
|
||||
{% set final_url_name = "Taiga - View task #{0}".format(snapshot.ref) %}
|
||||
|
||||
- Project: {{ object.project.name }}
|
||||
- Task #{{ object.ref }}: {{ object.subject }}
|
||||
- Project: {{ project.name }}
|
||||
- Task #{{ snapshot.ref }}: {{ snapshot.subject }}
|
||||
- Created by {{ changer.get_full_name() }}
|
||||
|
||||
** More info at {{ final_url_name }} ({{ final_url }}) **
|
||||
|
|
|
@ -1 +1 @@
|
|||
[{{ object.project.name|safe }}] Created the task #{{ object.ref }} "{{ object.subject|safe }}"
|
||||
[{{ project.name|safe }}] Created the task #{{ snapshot.ref }} "{{ snapshot.subject|safe }}"
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
||||
<tr>
|
||||
<td>
|
||||
<h1>{{ object.project.name }}</h1>
|
||||
<h2>Task #{{ object.ref }}: {{ object.subject }}</h2>
|
||||
<h1>{{ project.name }}</h1>
|
||||
<h2>Task #{{ snapshot.ref }}: {{ snapshot.subject }}</h2>
|
||||
<p>Deleted by <b>{{ changer.get_full_name() }}</b></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
- Project: {{ object.project.name }}
|
||||
- Task #{{ object.ref }}: {{ object.subject }}
|
||||
- Project: {{ project.name }}
|
||||
- Task #{{ snapshot.ref }}: {{ snapshot.subject }}
|
||||
- Deleted by {{ changer.get_full_name() }}
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
[{{ object.project.name|safe }}] Deleted the task #{{ object.ref }} "{{ object.subject|safe }}"
|
||||
[{{ project.name|safe }}] Deleted the task #{{ snapshot.ref }} "{{ snapshot.subject|safe }}"
|
||||
|
|
|
@ -1,22 +1,24 @@
|
|||
{% extends "emails/base.jinja" %}
|
||||
|
||||
{% set final_url = resolve_front_url("userstory", object.project.slug, object.ref) %}
|
||||
{% set final_url_name = "Taiga - View US #{0}".format(object.ref) %}
|
||||
{% set final_url = resolve_front_url("userstory", project.slug, snapshot.ref) %}
|
||||
{% set final_url_name = "Taiga - View US #{0}".format(snapshot.ref) %}
|
||||
|
||||
{% block body %}
|
||||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
||||
<tr>
|
||||
<td>
|
||||
<h1>Project: {{ object.project.name }}</h1>
|
||||
<h2>US #{{ object.ref }}: {{ object.subject }}</h2>
|
||||
<h1>Project: {{ project.name }}</h1>
|
||||
<h2>US #{{ snapshot.ref }}: {{ snapshot.subject }}</h2>
|
||||
<p>Updated by <b>{{ changer.get_full_name() }}</b>.</p>
|
||||
{% if comment %}
|
||||
<p>Comment <b>{{ mdrender(object.project, comment)}}</b></p>
|
||||
{% endif %}
|
||||
{% if changed_fields %}
|
||||
<p>Updated fields:</p>
|
||||
{% include "emails/includes/fields_diff-html.jinja" %}
|
||||
{% endif %}
|
||||
{% for entry in history_entries%}
|
||||
{% if entry.comment %}
|
||||
<p>Comment <b>{{ mdrender(project, entry.comment) }}</b></p>
|
||||
{% endif %}
|
||||
{% set changed_fields = entry.values_diff %}
|
||||
{% if changed_fields %}
|
||||
{% include "emails/includes/fields_diff-html.jinja" %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
{% set final_url = resolve_front_url("userstory", object.project.slug, object.ref) %}
|
||||
{% set final_url_name = "Taiga - View US #{0}".format(object.ref) %}
|
||||
{% set final_url = resolve_front_url("userstory", project.slug, snapshot.ref) %}
|
||||
{% set final_url_name = "Taiga - View US #{0}".format(snapshot.ref) %}
|
||||
|
||||
- Project: {{ object.project.name }}
|
||||
- US #{{ object.ref }}: {{ object.subject }}
|
||||
- Project: {{ project.name }}
|
||||
- US #{{ snapshot.ref }}: {{ snapshot.subject }}
|
||||
- Updated by {{ changer.get_full_name() }}
|
||||
{% if comment %}
|
||||
Comment: {{ comment|linebreaksbr }}
|
||||
{% endif %}
|
||||
{% if changed_fields %}
|
||||
- Updated fields:
|
||||
{% include "emails/includes/fields_diff-text.jinja" %}
|
||||
{% endif %}
|
||||
{% for entry in history_entries%}
|
||||
{% if entry.comment %}
|
||||
Comment: {{ entry.comment|linebreaksbr }}
|
||||
{% endif %}
|
||||
{% set changed_fields = entry.values_diff %}
|
||||
{% if changed_fields %}
|
||||
{% include "emails/includes/fields_diff-text.jinja" %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
** More info at {{ final_url_name }} ({{ final_url }}) **
|
||||
|
|
|
@ -1 +1 @@
|
|||
[{{ object.project.name|safe }}] Updated the US #{{ object.ref }} "{{ object.subject|safe }}"
|
||||
[{{ project.name|safe }}] Updated the US #{{ snapshot.ref }} "{{ snapshot.subject|safe }}"
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
{% extends "emails/base.jinja" %}
|
||||
|
||||
{% set final_url = resolve_front_url("userstory", object.project.slug, object.ref) %}
|
||||
{% set final_url_name = "Taiga - View US #{0}".format(object.ref) %}
|
||||
{% set final_url = resolve_front_url("userstory", project.slug, snapshot.ref) %}
|
||||
{% set final_url_name = "Taiga - View US #{0}".format(snapshot.ref) %}
|
||||
|
||||
{% block body %}
|
||||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
||||
<tr>
|
||||
<td>
|
||||
<h1>Project: {{ object.project.name }}</h1>
|
||||
<h2>US #{{ object.ref }}: {{ object.subject }}</h2>
|
||||
<h1>Project: {{ project.name }}</h1>
|
||||
<h2>US #{{ snapshot.ref }}: {{ snapshot.subject }}</h2>
|
||||
<p>Created by <b>{{ changer.get_full_name() }}</b>.</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% set final_url = resolve_front_url("userstory", object.project.slug, object.ref) %}
|
||||
{% set final_url_name = "Taiga - View US #{0}".format(object.ref) %}
|
||||
{% set final_url = resolve_front_url("userstory", project.slug, snapshot.ref) %}
|
||||
{% set final_url_name = "Taiga - View US #{0}".format(snapshot.ref) %}
|
||||
|
||||
- Project: {{ object.project.name }}
|
||||
- US #{{ object.ref }}: {{ object.subject }}
|
||||
- Project: {{ project.name }}
|
||||
- US #{{ snapshot.ref }}: {{ snapshot.subject }}
|
||||
- Created by {{ changer.get_full_name() }}
|
||||
|
||||
** More info at {{ final_url_name }} ({{ final_url }}) **
|
||||
|
|
|
@ -1 +1 @@
|
|||
[{{ object.project.name|safe }}] Created the US #{{ object.ref }} "{{ object.subject|safe }}"
|
||||
[{{ project.name|safe }}] Created the US #{{ snapshot.ref }} "{{ snapshot.subject|safe }}"
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
||||
<tr>
|
||||
<td>
|
||||
<h1>{{ object.project.name }}</h1>
|
||||
<h2>US #{{ object.ref }}: {{ object.subject }}</h2>
|
||||
<h1>{{ project.name }}</h1>
|
||||
<h2>US #{{ snapshot.ref }}: {{ snapshot.subject }}</h2>
|
||||
<p>Deleted by <b>{{ changer.get_full_name() }}</b></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
- Project: {{ object.project.name }}
|
||||
- US #{{ object.ref }}: {{ object.subject }}
|
||||
- Project: {{ project.name }}
|
||||
- US #{{ snapshot.ref }}: {{ snapshot.subject }}
|
||||
- Deleted by {{ changer.get_full_name() }}
|
||||
|
|
|
@ -1 +1 @@
|
|||
[{{ object.project.name|safe }}] Deleted the US #{{ object.ref }} "{{ object.subject|safe }}"
|
||||
[{{ project.name|safe }}] Deleted the US #{{ snapshot.ref }} "{{ snapshot.subject|safe }}"
|
||||
|
|
|
@ -1,22 +1,24 @@
|
|||
{% extends "emails/base.jinja" %}
|
||||
|
||||
{% set final_url = resolve_front_url("wiki", object.project.slug, object.slug) %}
|
||||
{% set final_url_name = "Taiga - View Wiki Page '{0}'".format(object.slug) %}
|
||||
{% set final_url = resolve_front_url("wiki", project.slug, snapshot.slug) %}
|
||||
{% set final_url_name = "Taiga - View Wiki Page '{0}'".format(snapshot.slug) %}
|
||||
|
||||
{% block body %}
|
||||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
||||
<tr>
|
||||
<td>
|
||||
<h1>Project: {{ object.project.name }}</h1>
|
||||
<h2>Wiki Page: {{ object.slug }}</h2>
|
||||
<h1>Project: {{ project.name }}</h1>
|
||||
<h2>Wiki Page: {{ snapshot.slug }}</h2>
|
||||
<p>Updated by <b>{{ changer.get_full_name() }}</b>.</p>
|
||||
{% if comment %}
|
||||
<p>Comment <b>{{ mdrender(object.project, comment) }}</b></p>
|
||||
{% endif %}
|
||||
{% if changed_fields %}
|
||||
<p>Updated fields:</p>
|
||||
{% include "emails/includes/fields_diff-html.jinja" %}
|
||||
{% endif %}
|
||||
{% for entry in history_entries%}
|
||||
{% if entry.comment %}
|
||||
<p>Comment <b>{{ mdrender(project, entry.comment) }}</b></p>
|
||||
{% endif %}
|
||||
{% set changed_fields = entry.values_diff %}
|
||||
{% if changed_fields %}
|
||||
{% include "emails/includes/fields_diff-html.jinja" %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
{% set final_url = resolve_front_url("wiki", object.project.slug, object.slug) %}
|
||||
{% set final_url_name = "Taiga - View Wiki Page '{0}'".format(object.slug) %}
|
||||
{% set final_url = resolve_front_url("wiki", project.slug, snapshot.slug) %}
|
||||
{% set final_url_name = "Taiga - View Wiki Page '{0}'".format(snapshot.slug) %}
|
||||
|
||||
- Project: {{ object.project.name }}
|
||||
- Wiki Page: {{ object.slug }}
|
||||
- Project: {{ project.name }}
|
||||
- Wiki Page: {{ snapshot.slug }}
|
||||
- Updated by {{ changer.get_full_name() }}
|
||||
{% if comment %}
|
||||
Comment: {{ comment|linebreaksbr }}
|
||||
{% endif %}
|
||||
{% if changed_fields %}
|
||||
- Updated fields:
|
||||
{% include "emails/includes/fields_diff-text.jinja" %}
|
||||
{% endif %}
|
||||
{% for entry in history_entries%}
|
||||
{% if entry.comment %}
|
||||
Comment: {{ entry.comment|linebreaksbr }}
|
||||
{% endif %}
|
||||
{% set changed_fields = entry.values_diff %}
|
||||
{% if changed_fields %}
|
||||
{% include "emails/includes/fields_diff-text.jinja" %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
** More info at {{ final_url_name }} ({{ final_url }}) **
|
||||
|
|
|
@ -1 +1 @@
|
|||
[{{ object.project.name|safe }}] Updated the Wiki Page "{{ object.slug }}"
|
||||
[{{ project.name|safe }}] Updated the Wiki Page "{{ snapshot.slug }}"
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
{% extends "emails/base.jinja" %}
|
||||
|
||||
{% set final_url = resolve_front_url("wiki", object.project.slug, object.slug) %}
|
||||
{% set final_url_name = "Taiga - View Wiki Page '{0}'".format(object.slug) %}
|
||||
{% set final_url = resolve_front_url("wiki", project.slug, snapshot.slug) %}
|
||||
{% set final_url_name = "Taiga - View Wiki Page '{0}'".format(snapshot.slug) %}
|
||||
|
||||
{% block body %}
|
||||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
||||
<tr>
|
||||
<td>
|
||||
<h1>Project: {{ object.project.name }}</h1>
|
||||
<h2>Wiki Page: {{ object.slug }}</h2>
|
||||
<h1>Project: {{ project.name }}</h1>
|
||||
<h2>Wiki Page: {{ snapshot.slug }}</h2>
|
||||
<p>Created by <b>{{ changer.get_full_name() }}</b>.</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% set final_url = resolve_front_url("wiki", object.project.slug, object.slug) %}
|
||||
{% set final_url_name = "Taiga - View Wiki Page '{0}'".format(object.slug) %}
|
||||
{% set final_url = resolve_front_url("wiki", project.slug, snapshot.slug) %}
|
||||
{% set final_url_name = "Taiga - View Wiki Page '{0}'".format(snapshot.slug) %}
|
||||
|
||||
- Project: {{ object.project.name }}
|
||||
- Wiki Page: {{ object.slug }}
|
||||
- Project: {{ project.name }}
|
||||
- Wiki Page: {{ snapshot.slug }}
|
||||
- Created by {{ changer.get_full_name() }}
|
||||
|
||||
** More info at {{ final_url_name }} ({{ final_url }}) **
|
||||
|
|
|
@ -1 +1 @@
|
|||
[{{ object.project.name|safe }}] Created the Wiki Page "{{ object.slug }}"
|
||||
[{{ project.name|safe }}] Created the Wiki Page "{{ snapshot.slug }}"
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
||||
<tr>
|
||||
<td>
|
||||
<h1>{{ object.project.name }}</h1>
|
||||
<h2>Wiki Page: {{ object.slug }}</h2>
|
||||
<h1>{{ project.name }}</h1>
|
||||
<h2>Wiki Page: {{ snapshot.slug }}</h2>
|
||||
<p>Deleted by <b>{{ changer.get_full_name() }}</b></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
- Project: {{ object.project.name }}
|
||||
- Wiki Page: {{ object.slug }}
|
||||
- Project: {{ project.name }}
|
||||
- Wiki Page: {{ snapshot.slug }}
|
||||
- Deleted by {{ changer.get_full_name() }}
|
||||
|
|
|
@ -1 +1 @@
|
|||
[{{ object.project.name|safe }}] Deleted the Wiki Page "{{ object.slug }}"
|
||||
[{{ project.name|safe }}] Deleted the Wiki Page "{{ snapshot.slug }}"
|
||||
|
|
|
@ -22,7 +22,7 @@ from taiga.base.decorators import list_route
|
|||
from taiga.base.api import ModelCrudViewSet
|
||||
from taiga.projects.models import Project
|
||||
|
||||
from taiga.projects.notifications import WatchedResourceMixin
|
||||
from taiga.projects.notifications.mixins import WatchedResourceMixin
|
||||
from taiga.projects.history.mixins import HistoryResourceMixin
|
||||
from taiga.projects.occ import OCCResourceMixin
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ from django.utils import timezone
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from taiga.projects.occ import OCCModelMixin
|
||||
from taiga.projects.notifications import WatchedModelMixin
|
||||
from taiga.projects.notifications.mixins import WatchedModelMixin
|
||||
from taiga.projects.mixins.blocked import BlockedMixin
|
||||
from taiga.base.tags import TaggedMixin
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ from taiga.base import exceptions as exc
|
|||
from taiga.base.decorators import list_route
|
||||
from taiga.base.api import ModelCrudViewSet
|
||||
|
||||
from taiga.projects.notifications import WatchedResourceMixin
|
||||
from taiga.projects.notifications.mixins import WatchedResourceMixin
|
||||
from taiga.projects.history.mixins import HistoryResourceMixin
|
||||
from taiga.projects.occ import OCCResourceMixin
|
||||
|
||||
|
@ -177,4 +177,3 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi
|
|||
self.send_notifications(self.object.generated_from_issue, history)
|
||||
|
||||
return response
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from django.utils import timezone
|
||||
|
||||
from taiga.projects.occ import OCCModelMixin
|
||||
from taiga.projects.notifications import WatchedModelMixin
|
||||
from taiga.projects.notifications.mixins import WatchedModelMixin
|
||||
from taiga.projects.mixins.blocked import BlockedMixin
|
||||
from taiga.base.tags import TaggedMixin
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ from taiga.base.decorators import list_route
|
|||
from taiga.projects.models import Project
|
||||
from taiga.mdrender.service import render as mdrender
|
||||
|
||||
from taiga.projects.notifications import WatchedResourceMixin
|
||||
from taiga.projects.notifications.mixins import WatchedResourceMixin
|
||||
from taiga.projects.history.mixins import HistoryResourceMixin
|
||||
from taiga.projects.occ import OCCResourceMixin
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ from django.contrib.contenttypes import generic
|
|||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils import timezone
|
||||
from taiga.projects.notifications import WatchedModelMixin
|
||||
from taiga.projects.notifications.mixins import WatchedModelMixin
|
||||
from taiga.projects.occ import OCCModelMixin
|
||||
|
||||
|
||||
|
|
|
@ -17,20 +17,23 @@
|
|||
|
||||
import json
|
||||
import pytest
|
||||
import time
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.apps import apps
|
||||
from .. import factories as f
|
||||
from .. utils import set_settings
|
||||
|
||||
from taiga.projects.notifications import services
|
||||
from taiga.projects.notifications import models
|
||||
from taiga.projects.notifications.choices import NotifyLevel
|
||||
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
|
||||
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
|
@ -118,7 +121,7 @@ def test_users_to_notify():
|
|||
# Test basic description modifications
|
||||
issue.description = "test1"
|
||||
issue.save()
|
||||
users = services.get_users_to_notify(issue, history=history)
|
||||
users = services.get_users_to_notify(issue)
|
||||
assert len(users) == 1
|
||||
assert tuple(users)[0] == issue.get_owner()
|
||||
|
||||
|
@ -126,13 +129,13 @@ def test_users_to_notify():
|
|||
policy1.notify_level = NotifyLevel.watch
|
||||
policy1.save()
|
||||
|
||||
users = services.get_users_to_notify(issue, history=history)
|
||||
users = services.get_users_to_notify(issue)
|
||||
assert len(users) == 2
|
||||
assert users == {member1.user, issue.get_owner()}
|
||||
|
||||
# Test with watchers
|
||||
issue.watchers.add(member3.user)
|
||||
users = services.get_users_to_notify(issue, history=history)
|
||||
users = services.get_users_to_notify(issue)
|
||||
assert len(users) == 3
|
||||
assert users == {member1.user, member3.user, issue.get_owner()}
|
||||
|
||||
|
@ -141,90 +144,87 @@ def test_users_to_notify():
|
|||
policy3.save()
|
||||
|
||||
issue.watchers.add(member3.user)
|
||||
users = services.get_users_to_notify(issue, history=history)
|
||||
users = services.get_users_to_notify(issue)
|
||||
assert len(users) == 2
|
||||
assert users == {member1.user, issue.get_owner()}
|
||||
|
||||
|
||||
@set_settings(CHANGE_NOTIFICATIONS_MIN_INTERVAL=1)
|
||||
def test_send_notifications_using_services_method(mail):
|
||||
project = f.ProjectFactory.create()
|
||||
member1 = f.MembershipFactory.create(project=project)
|
||||
member2 = f.MembershipFactory.create(project=project)
|
||||
|
||||
history_change = MagicMock()
|
||||
history_change.owner = member1.user
|
||||
history_change.user = {"pk": member1.user.pk}
|
||||
history_change.comment = ""
|
||||
history_change.type = HistoryType.change
|
||||
|
||||
history_create = MagicMock()
|
||||
history_create.owner = member1.user
|
||||
history_create.user = {"pk": member1.user.pk}
|
||||
history_create.comment = ""
|
||||
history_create.type = HistoryType.create
|
||||
|
||||
history_delete = MagicMock()
|
||||
history_delete.owner = member1.user
|
||||
history_delete.user = {"pk": member1.user.pk}
|
||||
history_delete.comment = ""
|
||||
history_delete.type = HistoryType.delete
|
||||
|
||||
# Issues
|
||||
issue = f.IssueFactory.create(project=project)
|
||||
take_snapshot(issue)
|
||||
services.send_notifications(issue,
|
||||
history=history_create,
|
||||
users={member1.user, member2.user})
|
||||
history=history_create)
|
||||
|
||||
services.send_notifications(issue,
|
||||
history=history_change,
|
||||
users={member1.user, member2.user})
|
||||
history=history_change)
|
||||
|
||||
services.send_notifications(issue,
|
||||
history=history_delete,
|
||||
users={member1.user, member2.user})
|
||||
history=history_delete)
|
||||
|
||||
|
||||
# Userstories
|
||||
us = f.UserStoryFactory.create()
|
||||
take_snapshot(us)
|
||||
services.send_notifications(us,
|
||||
history=history_create,
|
||||
users={member1.user, member2.user})
|
||||
history=history_create)
|
||||
|
||||
services.send_notifications(us,
|
||||
history=history_change,
|
||||
users={member1.user, member2.user})
|
||||
history=history_change)
|
||||
|
||||
services.send_notifications(us,
|
||||
history=history_delete,
|
||||
users={member1.user, member2.user})
|
||||
history=history_delete)
|
||||
|
||||
# Tasks
|
||||
task = f.TaskFactory.create()
|
||||
take_snapshot(task)
|
||||
services.send_notifications(task,
|
||||
history=history_create,
|
||||
users={member1.user, member2.user})
|
||||
history=history_create)
|
||||
|
||||
services.send_notifications(task,
|
||||
history=history_change,
|
||||
users={member1.user, member2.user})
|
||||
history=history_change)
|
||||
|
||||
services.send_notifications(task,
|
||||
history=history_delete,
|
||||
users={member1.user, member2.user})
|
||||
history=history_delete)
|
||||
|
||||
# Wiki pages
|
||||
wiki = f.WikiPageFactory.create()
|
||||
take_snapshot(wiki)
|
||||
services.send_notifications(wiki,
|
||||
history=history_create,
|
||||
users={member1.user, member2.user})
|
||||
history=history_create)
|
||||
|
||||
services.send_notifications(wiki,
|
||||
history=history_change,
|
||||
users={member1.user, member2.user})
|
||||
history=history_change)
|
||||
|
||||
services.send_notifications(wiki,
|
||||
history=history_delete,
|
||||
users={member1.user, member2.user})
|
||||
|
||||
assert len(mail.outbox) == 24
|
||||
|
||||
history=history_delete)
|
||||
|
||||
assert models.HistoryChangeNotification.objects.count() == 12
|
||||
assert len(mail.outbox) == 0
|
||||
time.sleep(1)
|
||||
services.process_sync_notifications()
|
||||
assert len(mail.outbox) == 12
|
||||
|
||||
@set_settings(CHANGE_NOTIFICATIONS_MIN_INTERVAL=1)
|
||||
def test_resource_notification_test(client, mail):
|
||||
user1 = f.UserFactory.create()
|
||||
user2 = f.UserFactory.create()
|
||||
|
@ -242,13 +242,23 @@ def test_resource_notification_test(client, mail):
|
|||
with patch(mock_path) as m:
|
||||
data = {"subject": "Fooooo", "version": issue.version}
|
||||
response = client.patch(url, json.dumps(data), content_type="application/json")
|
||||
assert len(mail.outbox) == 1
|
||||
assert response.status_code == 200
|
||||
assert len(mail.outbox) == 0
|
||||
assert models.HistoryChangeNotification.objects.count() == 1
|
||||
time.sleep(1)
|
||||
services.process_sync_notifications()
|
||||
assert len(mail.outbox) == 1
|
||||
assert models.HistoryChangeNotification.objects.count() == 0
|
||||
|
||||
with patch(mock_path) as m:
|
||||
response = client.delete(url)
|
||||
assert response.status_code == 204
|
||||
assert len(mail.outbox) == 1
|
||||
assert models.HistoryChangeNotification.objects.count() == 1
|
||||
time.sleep(1)
|
||||
services.process_sync_notifications()
|
||||
assert len(mail.outbox) == 2
|
||||
assert models.HistoryChangeNotification.objects.count() == 0
|
||||
|
||||
|
||||
def test_watchers_assignation_for_issue(client):
|
||||
|
|
Loading…
Reference in New Issue