Browse Source

[MIG][11.0] mail_digest

pull/228/head
Simone Orsi 7 years ago
committed by Holger Brunn
parent
commit
6a576a5427
  1. 15
      mail_digest/README.rst
  2. 9
      mail_digest/__manifest__.py
  3. 5
      mail_digest/data/config_param.xml
  4. 16
      mail_digest/data/ir_cron.xml
  5. 39
      mail_digest/demo/ir_ui_view.xml
  6. 10
      mail_digest/demo/mail_template.xml
  7. 1
      mail_digest/models/__init__.py
  8. 64
      mail_digest/models/mail_digest.py
  9. 169
      mail_digest/models/res_partner.py
  10. 113
      mail_digest/models/res_users.py
  11. 33
      mail_digest/models/user_notification_conf.py
  12. 4
      mail_digest/security/ir.model.access.csv
  13. 8
      mail_digest/security/record_rules.xml
  14. 2
      mail_digest/templates/digest_default.xml
  15. 146
      mail_digest/tests/test_digest.py
  16. 140
      mail_digest/tests/test_partner_domains.py
  17. 115
      mail_digest/tests/test_subtypes_conf.py
  18. 4
      mail_digest/views/mail_digest_views.xml
  19. 38
      mail_digest/views/partner_views.xml
  20. 28
      mail_digest/views/user_notification_views.xml
  21. 23
      mail_digest/views/user_views.xml

15
mail_digest/README.rst

@ -31,9 +31,11 @@ all the messages are collected inside a `mail.digest` container.
A daily cron and a weekly cron will take care of creating a single email per each digest, A daily cron and a weekly cron will take care of creating a single email per each digest,
which will be sent as a standard email. which will be sent as a standard email.
If the message has a specific subtype, all of this will work only
if personal settings allow to receive notification for that specific subtype.
Specifically:
**Rules**
Given that the user has `Notification management = Handle by Emails`...
a message with subtype assigned *will be sent* via digest if:
* no record for type: message passes * no record for type: message passes
* record disabled for type: message don't pass * record disabled for type: message don't pass
@ -42,6 +44,11 @@ Specifically:
NOTE: under the hood the digest notification logic excludes followers to be notified, NOTE: under the hood the digest notification logic excludes followers to be notified,
since you really want to notify only mail.digest's partner. since you really want to notify only mail.digest's partner.
a message with subtype assigned *will NOT be sent* via digest if:
* global: `mail_digest_enabled_message_types` param disables the message type
* user: digest mode is OFF for the recipient
* user: recipient's user has disabled the subtype in her/his settings
Global settings Global settings
--------------- ---------------
@ -66,7 +73,7 @@ Bug Tracker
Bugs are tracked on `GitHub Issues Bugs are tracked on `GitHub Issues
<https://github.com/OCA/social/issues>`_. In case of trouble, please <https://github.com/OCA/social/issues>`_. In case of trouble, please
check there if your issue has already been reported. If you spotted it first, check there if your issue has already been reported. If you spotted it first,
help us smashing it by providing a detailed and welcomed feedback.
help us smash it by providing a detailed and welcomed feedback.
Credits Credits
======= =======

9
mail_digest/__manifest__.py

@ -1,13 +1,12 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Simone Orsi <simone.orsi@camptocamp.com>
# Copyright 2017-2018 Camptocamp - Simone Orsi
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
{ {
'name': 'Mail digest', 'name': 'Mail digest',
'summary': """Basic digest mail handling.""", 'summary': """Basic digest mail handling.""",
'version': '10.0.1.0.1',
'version': '11.0.1.0.0',
'license': 'AGPL-3', 'license': 'AGPL-3',
'author': 'Camptocamp,Odoo Community Association (OCA)',
'author': 'Camptocamp, Odoo Community Association (OCA)',
'website': 'https://github.com/OCA/social', 'website': 'https://github.com/OCA/social',
'depends': [ 'depends': [
'mail', 'mail',
@ -18,7 +17,7 @@
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'security/record_rules.xml', 'security/record_rules.xml',
'views/mail_digest_views.xml', 'views/mail_digest_views.xml',
'views/partner_views.xml',
'views/user_notification_views.xml',
'views/user_views.xml', 'views/user_views.xml',
'templates/digest_default.xml', 'templates/digest_default.xml',
], ],

5
mail_digest/data/config_param.xml

@ -1,9 +1,8 @@
<odoo>
<data noupdate="1">
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="mail_digest_enabled_message_types" model="ir.config_parameter"> <record id="mail_digest_enabled_message_types" model="ir.config_parameter">
<field name="key">mail_digest.enabled_message_types</field> <field name="key">mail_digest.enabled_message_types</field>
<!-- enabled by default for each message type --> <!-- enabled by default for each message type -->
<field name="value">email,notification,comment</field> <field name="value">email,notification,comment</field>
</record> </record>
</data>
</odoo> </odoo>

16
mail_digest/data/ir_cron.xml

@ -1,27 +1,23 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<odoo noupdate="1">
<record forcecreate="True" id="ir_cron_mail_digest_daily_action" model="ir.cron"> <record forcecreate="True" id="ir_cron_mail_digest_daily_action" model="ir.cron">
<field name="name">Digest mail process - daily</field> <field name="name">Digest mail process - daily</field>
<field name="model_id" ref="model_mail_digest"/>
<field name="user_id" ref="base.user_root"/> <field name="user_id" ref="base.user_root"/>
<field name="interval_number">1</field> <field name="interval_number">1</field>
<field name="interval_type">days</field> <field name="interval_type">days</field>
<field name="numbercall">-1</field> <field name="numbercall">-1</field>
<field eval="False" name="doall"/> <field eval="False" name="doall"/>
<field eval="'mail.digest'" name="model"/>
<field eval="'process'" name="function"/>
<field eval="'()'" name="args"/>
<field name="code">model.process()</field>
</record> </record>
<record forcecreate="True" id="ir_cron_mail_digest_weekly_action" model="ir.cron"> <record forcecreate="True" id="ir_cron_mail_digest_weekly_action" model="ir.cron">
<field name="name">Digest mail process - weekly</field> <field name="name">Digest mail process - weekly</field>
<field name="model_id" ref="model_mail_digest"/>
<field name="user_id" ref="base.user_root"/> <field name="user_id" ref="base.user_root"/>
<field name="interval_number">1</field> <field name="interval_number">1</field>
<field name="interval_type">weeks</field> <field name="interval_type">weeks</field>
<field name="numbercall">-1</field> <field name="numbercall">-1</field>
<field eval="False" name="doall"/>
<field eval="'mail.digest'" name="model"/>
<field eval="'process'" name="function"/>
<field eval="'(\'weekly\',)'" name="args"/>
<field name="doall" eval="False"/>
<field name="code">model.process(frequency='weekly')</field>
</record> </record>
</data>
</odoo> </odoo>

39
mail_digest/demo/ir_ui_view.xml

@ -1,39 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<template id="view_email_template_corporate_identity">
<body>
<html>
<img style="float: right" t-attf-src="data:image;base64,{{env.user.company_id.logo}}" />
<!-- if some template calling us sets this variable,
we print a h1 tag /-->
<h1 t-if="email_heading"><t t-esc="email_heading" /></h1>
<t t-raw="0" />
<!-- use some standard footer if the user doesn't have
a signature /-->
<footer t-if="not email_use_user_signature">
<p>
<a t-att-href="env.user.company_id.website">
<t t-esc="env.user.company_id.name" />
</a>
</p>
<p><t t-esc="env.user.company_id.phone" /></p>
</footer>
<footer t-if="email_use_user_signature">
<t t-raw="env.user.signature" />
</footer>
</html>
</body>
</template>
<template id="view_email_template_demo1">
<!-- because we can simply call the ci here, we don't need to
repeat it /-->
<t t-call="email_template_qweb.view_email_template_corporate_identity">
<!-- the template we call uses this as title if we set it /-->
<t t-set="email_heading" t-value="email_template.subject" />
<h2>Dear <t t-esc="object.name" />,</h2>
<p>
This is an email template using qweb.
</p>
</t>
</template>
</odoo>

10
mail_digest/demo/mail_template.xml

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="email_template_demo1" model="mail.template">
<field name="name">QWeb demo</field>
<field name="body_type">qweb</field>
<field name="body_view_id" ref="view_email_template_demo1" />
<field name="model_id" ref="base.model_res_users" />
<field name="subject">QWeb demo email</field>
</record>
</odoo>

1
mail_digest/models/__init__.py

@ -1,3 +1,4 @@
from . import mail_digest from . import mail_digest
from . import user_notification_conf
from . import res_partner from . import res_partner
from . import res_users from . import res_users

64
mail_digest/models/mail_digest.py

@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Simone Orsi <simone.orsi@camptocamp.com>
# Copyright 2017-2018 Camptocamp - Simone Orsi
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo import fields, models, api, exceptions, _ from odoo import fields, models, api, exceptions, _
@ -19,15 +18,15 @@ class MailDigest(models.Model):
compute="_compute_name", compute="_compute_name",
readonly=True, readonly=True,
) )
partner_id = fields.Many2one(
string='Partner',
comodel_name='res.partner',
user_id = fields.Many2one(
string='User',
comodel_name='res.users',
readonly=True, readonly=True,
required=True, required=True,
ondelete='cascade', ondelete='cascade',
) )
frequency = fields.Selection( frequency = fields.Selection(
related='partner_id.notify_frequency',
related='user_id.digest_frequency',
readonly=True, readonly=True,
) )
message_ids = fields.Many2many( message_ids = fields.Many2many(
@ -52,7 +51,6 @@ class MailDigest(models.Model):
ondelete='set null', ondelete='set null',
default=lambda self: self._default_digest_template_id(), default=lambda self: self._default_digest_template_id(),
domain=[('type', '=', 'qweb')], domain=[('type', '=', 'qweb')],
oldname='template_id',
) )
def _default_digest_template_id(self): def _default_digest_template_id(self):
@ -61,53 +59,47 @@ class MailDigest(models.Model):
raise_if_not_found=False) raise_if_not_found=False)
@api.multi @api.multi
@api.depends("partner_id", "partner_id.notify_frequency")
@api.depends("user_id", "user_id.digest_frequency")
def _compute_name(self): def _compute_name(self):
for rec in self: for rec in self:
rec.name = u'{} - {}'.format(
rec.partner_id.name, rec._get_subject())
rec.name = '{} - {}'.format(
rec.user_id.name, rec._get_subject())
@api.model @api.model
def create_or_update(self, partners, message, subtype_id=None):
def create_or_update(self, partners, message):
"""Create or update digest. """Create or update digest.
:param partners: recipients as `res.partner` browse list :param partners: recipients as `res.partner` browse list
:param message: `mail.message` to include in digest :param message: `mail.message` to include in digest
:param subtype_id: `mail.message.subtype` instance
""" """
subtype_id = subtype_id or message.subtype_id
for partner in partners: for partner in partners:
digest = self._get_or_create_by_partner(partner, message)
digest = self._get_or_create_by_user(partner.real_user_id)
digest.message_ids |= message digest.message_ids |= message
return True return True
@api.model @api.model
def _get_by_partner(self, partner, mail_id=False):
"""Retrieve digest record for given partner.
def _get_by_user(self, user):
"""Retrieve digest record for given user.
:param partner: `res.partner` browse record
:param mail_id: `mail.mail` record for further filtering.
:param user: `res.users` browse record
By default we lookup for pending digest without notification yet. By default we lookup for pending digest without notification yet.
""" """
domain = [ domain = [
('partner_id', '=', partner.id),
('mail_id', '=', mail_id),
('user_id', '=', user.id),
] ]
return self.search(domain, limit=1) return self.search(domain, limit=1)
@api.model @api.model
def _get_or_create_by_partner(self, partner, message=None, mail_id=False):
"""Retrieve digest record or create it by partner.
def _get_or_create_by_user(self, user):
"""Retrieve digest record or create it by user.
:param partner: `res.partner` record to create/get digest for
:param message: `mail.message` to include in digest
:param mail_id: `mail.mail` record to set on digest
:param user: `res.users` record to create/get digest for
""" """
existing = self._get_by_partner(partner, mail_id=mail_id)
existing = self._get_by_user(user)
if existing: if existing:
return existing return existing
values = {'partner_id': partner.id, }
values = {'user_id': user.id, }
return self.create(values) return self.create(values)
@api.model @api.model
@ -158,10 +150,10 @@ class MailDigest(models.Model):
"""Build the full subject for digest's mail.""" """Build the full subject for digest's mail."""
# TODO: shall we move this to computed field? # TODO: shall we move this to computed field?
self.ensure_one() self.ensure_one()
subject = u'[{}] '.format(self._get_site_name())
if self.partner_id.notify_frequency == 'daily':
subject = '[{}] '.format(self._get_site_name())
if self.user_id.digest_frequency == 'daily':
subject += _('Daily update') subject += _('Daily update')
elif self.partner_id.notify_frequency == 'weekly':
elif self.user_id.digest_frequency == 'weekly':
subject += _('Weekly update') subject += _('Weekly update')
return subject return subject
@ -192,7 +184,7 @@ class MailDigest(models.Model):
template_values = self._get_template_values() template_values = self._get_template_values()
values = { values = {
'email_from': self.env.user.company_id.email, 'email_from': self.env.user.company_id.email,
'recipient_ids': [(4, self.partner_id.id)],
'recipient_ids': [(4, self.user_id.partner_id.id)],
'subject': subject, 'subject': subject,
'body_html': template.with_context( 'body_html': template.with_context(
**self._template_context() **self._template_context()
@ -204,7 +196,7 @@ class MailDigest(models.Model):
"""Inject context vars. """Inject context vars.
By default we make sure that digest's email By default we make sure that digest's email
will have only digest's partner among recipients.
will have only digest's user among recipients.
""" """
return { return {
'notify_only_recipients': True, 'notify_only_recipients': True,
@ -214,11 +206,11 @@ class MailDigest(models.Model):
def _template_context(self): def _template_context(self):
"""Rendering context for digest's template. """Rendering context for digest's template.
By default we enforce partner's language.
By default we enforce user's language.
""" """
self.ensure_one() self.ensure_one()
return { return {
'lang': self.partner_id.lang,
'lang': self.user_id.lang,
} }
@api.multi @api.multi
@ -252,12 +244,12 @@ class MailDigest(models.Model):
def process(self, frequency='daily', domain=None): def process(self, frequency='daily', domain=None):
"""Process existing digest records to create emails via cron. """Process existing digest records to create emails via cron.
:param frequency: lookup digest records by partners' `notify_frequency`
:param frequency: lookup digest records by users' `digest_frequency`
:param domain: pass custom domain to lookup only specific digests :param domain: pass custom domain to lookup only specific digests
""" """
if not domain: if not domain:
domain = [ domain = [
('mail_id', '=', False), ('mail_id', '=', False),
('partner_id.notify_frequency', '=', frequency),
('user_id.digest_frequency', '=', frequency),
] ]
self.search(domain).create_email() self.search(domain).create_email()

169
mail_digest/models/res_partner.py

@ -1,88 +1,19 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Simone Orsi <simone.orsi@camptocamp.com>
# Copyright 2017-2018 Camptocamp - Simone Orsi
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo import models, fields, api, _
from odoo import models, api
class ResPartner(models.Model): class ResPartner(models.Model):
_inherit = 'res.partner' _inherit = 'res.partner'
notify_email = fields.Selection(selection_add=[('digest', _('Digest'))])
notify_frequency = fields.Selection(
string='Frequency',
selection=[
('daily', 'Daily'),
('weekly', 'Weekly')
],
default='weekly',
required=True,
)
notify_conf_ids = fields.One2many(
string='Notifications',
inverse_name='partner_id',
comodel_name='partner.notification.conf',
)
enabled_notify_subtype_ids = fields.Many2many(
string='Partner enabled subtypes',
comodel_name='mail.message.subtype',
compute='_compute_enabled_notify_subtype_ids',
search='_search_enabled_notify_subtype_ids',
)
disabled_notify_subtype_ids = fields.Many2many(
string='Partner disabled subtypes',
comodel_name='mail.message.subtype',
compute='_compute_disabled_notify_subtype_ids',
search='_search_disabled_notify_subtype_ids',
)
@api.multi
def _compute_notify_subtypes(self, enabled):
self.ensure_one()
query = (
'SELECT subtype_id FROM partner_notification_conf '
'WHERE partner_id=%s AND enabled = %s'
)
self.env.cr.execute(
query, (self.id, enabled))
return [x[0] for x in self.env.cr.fetchall()]
@api.multi
@api.depends('notify_conf_ids.subtype_id')
def _compute_enabled_notify_subtype_ids(self):
for partner in self:
partner.enabled_notify_subtype_ids = \
partner._compute_notify_subtypes(True)
@api.multi
@api.depends('notify_conf_ids.subtype_id')
def _compute_disabled_notify_subtype_ids(self):
for partner in self:
partner.disabled_notify_subtype_ids = \
partner._compute_notify_subtypes(False)
def _search_notify_subtype_ids_domain(self, operator, value, enabled):
"""Build domain to search notification subtypes by partner settings."""
if operator in ('in', 'not in') and \
not isinstance(value, (tuple, list)):
value = [value, ]
conf_value = value
if isinstance(conf_value, int):
# we search conf records always w/ 'in'
conf_value = [conf_value]
_value = self.env['partner.notification.conf'].search([
('subtype_id', 'in', conf_value),
('enabled', '=', enabled),
]).mapped('partner_id').ids
return [('id', operator, _value)]
def _search_enabled_notify_subtype_ids(self, operator, value):
return self._search_notify_subtype_ids_domain(
operator, value, True)
def _search_disabled_notify_subtype_ids(self, operator, value):
return self._search_notify_subtype_ids_domain(
operator, value, False)
# Shortcut to bypass this weird thing of odoo:
# `partner.user_id` is the "saleman"
# while the user is stored into `user_ids`
# but in the majority of the cases we have one real user per partner.
@property
def real_user_id(self):
return self.user_ids[0] if self.user_ids else False
@api.multi @api.multi
def _notify(self, message, def _notify(self, message,
@ -94,17 +25,14 @@ class ResPartner(models.Model):
# the reason should be that anybody can write messages to a partner # the reason should be that anybody can write messages to a partner
# and you really want to find all ppl to be notified # and you really want to find all ppl to be notified
partners = self.sudo().search(email_domain) partners = self.sudo().search(email_domain)
partners._notify_by_email(
super(ResPartner, partners)._notify(
message, force_send=force_send, message, force_send=force_send,
send_after_commit=send_after_commit, user_signature=user_signature)
send_after_commit=send_after_commit,
user_signature=user_signature)
# notify_by_digest # notify_by_digest
digest_domain = self._get_notify_by_email_domain(
message, digest=True)
digest_domain = self._get_notify_by_email_domain(message, digest=True)
partners = self.sudo().search(digest_domain) partners = self.sudo().search(digest_domain)
partners._notify_by_digest(message) partners._notify_by_digest(message)
# notify_by_chat
self._notify_by_chat(message)
return True return True
def _digest_enabled_message_types(self): def _digest_enabled_message_types(self):
@ -153,12 +81,10 @@ class ResPartner(models.Model):
'|', '|',
('id', 'in', ids), ('id', 'in', ids),
('channel_ids', 'in', channels.ids), ('channel_ids', 'in', channels.ids),
('email', '!=', email)
('email', '!=', email),
('user_ids.digest_mode', '=', digest),
('user_ids.notification_type', '=', 'email'),
] ]
if not digest:
domain.append(('notify_email', 'not in', ('none', 'digest')))
else:
domain.append(('notify_email', '=', 'digest'))
if message.subtype_id: if message.subtype_id:
domain.extend(self._get_domain_subtype_leaf(message.subtype_id)) domain.extend(self._get_domain_subtype_leaf(message.subtype_id))
return domain return domain
@ -167,65 +93,6 @@ class ResPartner(models.Model):
def _get_domain_subtype_leaf(self, subtype): def _get_domain_subtype_leaf(self, subtype):
return [ return [
'|', '|',
('disabled_notify_subtype_ids', 'not in', (subtype.id, )),
('enabled_notify_subtype_ids', 'in', (subtype.id, )),
]
@api.multi
def _notify_update_subtype(self, subtype, enable):
"""Update notification settings by subtype.
:param subtype: `mail.message.subtype` to enable or disable
:param enable: boolean to enable or disable given subtype
"""
self.ensure_one()
exists = self.env['partner.notification.conf'].search([
('subtype_id', '=', subtype.id),
('partner_id', '=', self.id)
], limit=1)
if exists:
exists.enabled = enable
else:
self.write({
'notify_conf_ids': [
(0, 0, {'enabled': enable, 'subtype_id': subtype.id})]
})
@api.multi
def _notify_enable_subtype(self, subtype):
"""Enable given subtype."""
self._notify_update_subtype(subtype, True)
@api.multi
def _notify_disable_subtype(self, subtype):
"""Disable given subtype."""
self._notify_update_subtype(subtype, False)
class PartnerNotificationConf(models.Model):
"""Hold partner's single notification configuration."""
_name = 'partner.notification.conf'
_description = 'Partner notification configuration'
# TODO: add friendly onchange to not yield errors when editin via UI
_sql_constraints = [
('unique_partner_subtype_conf',
'unique (partner_id,subtype_id)',
'You can have only one configuration per subtype!')
('user_ids.disabled_notify_subtype_ids', 'not in', (subtype.id, )),
('user_ids.enabled_notify_subtype_ids', 'in', (subtype.id, )),
] ]
partner_id = fields.Many2one(
string='Partner',
comodel_name='res.partner',
readonly=True,
required=True,
ondelete='cascade',
index=True,
)
subtype_id = fields.Many2one(
'mail.message.subtype',
'Notification type',
ondelete='cascade',
required=True,
index=True,
)
enabled = fields.Boolean(default=True, index=True)

113
mail_digest/models/res_users.py

@ -1,8 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Simone Orsi <simone.orsi@camptocamp.com>
# Copyright 2017-2018 Camptocamp - Simone Orsi
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo import models
from odoo import models, fields, api
class Users(models.Model): class Users(models.Model):
@ -18,11 +17,111 @@ class Users(models.Model):
[copied from mail.models.users] [copied from mail.models.users]
""" """
super(Users, self).__init__(pool, cr) super(Users, self).__init__(pool, cr)
new_fields = [
'digest_mode',
'digest_frequency',
'notify_conf_ids',
]
# duplicate list to avoid modifying the original reference # duplicate list to avoid modifying the original reference
type(self).SELF_WRITEABLE_FIELDS = list(self.SELF_WRITEABLE_FIELDS) type(self).SELF_WRITEABLE_FIELDS = list(self.SELF_WRITEABLE_FIELDS)
type(self).SELF_WRITEABLE_FIELDS.extend(['notify_frequency'])
type(self).SELF_WRITEABLE_FIELDS.extend(['notify_conf_ids'])
type(self).SELF_WRITEABLE_FIELDS.extend(new_fields)
# duplicate list to avoid modifying the original reference # duplicate list to avoid modifying the original reference
type(self).SELF_READABLE_FIELDS = list(self.SELF_READABLE_FIELDS) type(self).SELF_READABLE_FIELDS = list(self.SELF_READABLE_FIELDS)
type(self).SELF_READABLE_FIELDS.extend(['notify_frequency'])
type(self).SELF_READABLE_FIELDS.extend(['notify_conf_ids'])
type(self).SELF_READABLE_FIELDS.extend(new_fields)
digest_mode = fields.Boolean(
default=False,
help='If enabled, email notifications will be sent in digest mode.'
)
digest_frequency = fields.Selection(
string='Frequency',
selection=[
('daily', 'Daily'),
('weekly', 'Weekly')
],
default='weekly',
required=True,
)
notify_conf_ids = fields.One2many(
string='Notifications',
inverse_name='user_id',
comodel_name='user.notification.conf',
)
enabled_notify_subtype_ids = fields.Many2many(
string='User enabled subtypes',
comodel_name='mail.message.subtype',
compute='_compute_notify_subtype_ids',
search='_search_enabled_notify_subtype_ids',
)
disabled_notify_subtype_ids = fields.Many2many(
string='User disabled subtypes',
comodel_name='mail.message.subtype',
compute='_compute_notify_subtype_ids',
search='_search_disabled_notify_subtype_ids',
)
def _notify_subtypes_by_state(self, enabled):
self.ensure_one()
return self.notify_conf_ids.filtered(
lambda x: x.enabled == enabled).mapped('subtype_id')
@api.multi
@api.depends('notify_conf_ids.subtype_id', 'notify_conf_ids.enabled')
def _compute_notify_subtype_ids(self):
for rec in self:
rec.enabled_notify_subtype_ids = \
rec._notify_subtypes_by_state(True)
rec.disabled_notify_subtype_ids = \
rec._notify_subtypes_by_state(False)
def _search_notify_subtype_ids_domain(self, operator, value, enabled):
"""Build domain to search notification subtypes by user conf."""
if operator in ('in', 'not in') and \
not isinstance(value, (tuple, list)):
value = [value, ]
conf_value = value
if isinstance(conf_value, int):
# we search conf records always w/ 'in'
conf_value = [conf_value]
_value = self.env['user.notification.conf'].search([
('subtype_id', 'in', conf_value),
('enabled', '=', enabled),
]).mapped('user_id').ids
return [('id', operator, _value)]
def _search_enabled_notify_subtype_ids(self, operator, value):
return self._search_notify_subtype_ids_domain(operator, value, True)
def _search_disabled_notify_subtype_ids(self, operator, value):
return self._search_notify_subtype_ids_domain(operator, value, False)
def _notify_update_subtype(self, subtype, enable):
"""Update notification settings by subtype.
:param subtype: `mail.message.subtype` to enable or disable
:param enable: boolean to enable or disable given subtype
"""
self.ensure_one()
exists = self.env['user.notification.conf'].search([
('subtype_id', '=', subtype.id),
('user_id', '=', self.id)
], limit=1)
if exists:
exists.enabled = enable
else:
self.write({
'notify_conf_ids': [
(0, 0, {'enabled': enable, 'subtype_id': subtype.id})]
})
@api.multi
def _notify_enable_subtype(self, subtype):
"""Enable given subtype."""
for rec in self:
rec._notify_update_subtype(subtype, True)
@api.multi
def _notify_disable_subtype(self, subtype):
"""Disable given subtype."""
for rec in self:
rec._notify_update_subtype(subtype, False)

33
mail_digest/models/user_notification_conf.py

@ -0,0 +1,33 @@
# Copyright 2017-2018 Camptocamp - Simone Orsi
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo import models, fields
class UserNotificationConf(models.Model):
"""Hold user's single notification configuration."""
_name = 'user.notification.conf'
_description = 'User notification configuration'
# TODO: add friendly onchange to not yield errors when editin via UI
_sql_constraints = [
('unique_user_subtype_conf',
'unique (user_id,subtype_id)',
'You can have only one configuration per subtype!')
]
user_id = fields.Many2one(
string='User',
comodel_name='res.users',
readonly=True,
required=True,
ondelete='cascade',
index=True,
)
subtype_id = fields.Many2one(
'mail.message.subtype',
'Notification type',
ondelete='cascade',
required=True,
index=True,
)
enabled = fields.Boolean(default=True, index=True)

4
mail_digest/security/ir.model.access.csv

@ -1,4 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_partner_notification_conf_user,partner.notification.user,model_partner_notification_conf,base.group_user,1,1,1,1
access_partner_notification_conf_user,partner.notification.user,model_user_notification_conf,base.group_user,1,1,1,1
access_mail_digest_system,mail.digest.sys,model_mail_digest,base.group_system,1,1,1,1 access_mail_digest_system,mail.digest.sys,model_mail_digest,base.group_system,1,1,1,1
access_partner_notification_conf_system,partner.notification.sys,model_partner_notification_conf,base.group_system,1,1,1,1
access_partner_notification_conf_system,partner.notification.sys,model_user_notification_conf,base.group_system,1,1,1,1

8
mail_digest/security/record_rules.xml

@ -1,14 +1,14 @@
<?xml version="1.0"?> <?xml version="1.0"?>
<odoo> <odoo>
<record model="ir.rule" id="partner_notification_conf_owner">
<field name="name">Partners can edit their own notification settings</field>
<field name="model_id" ref="model_partner_notification_conf"/>
<record model="ir.rule" id="user_notification_conf_owner">
<field name="name">Users can edit their own notification settings</field>
<field name="model_id" ref="model_user_notification_conf"/>
<field name="perm_read" eval="False"/> <field name="perm_read" eval="False"/>
<field name="perm_create" eval="False"/> <field name="perm_create" eval="False"/>
<field name="perm_write" eval="True"/> <field name="perm_write" eval="True"/>
<field name="perm_unlink" eval="True"/> <field name="perm_unlink" eval="True"/>
<field name="domain_force">['|',('partner_id', '=', user.partner_id.id), ('create_uid', '=', user.id)]</field>
<field name="domain_force">['|',('user_id', '=', user.id), ('create_uid', '=', user.id)]</field>
</record> </record>
</odoo> </odoo>

2
mail_digest/templates/digest_default.xml

@ -26,7 +26,7 @@
<div id="mail_content"> <div id="mail_content">
<h2>Hello,</h2> <h2>Hello,</h2>
<div id="mail_inner_content"> <div id="mail_inner_content">
<t t-foreach="grouped_messages.iterkeys()" t-as="gkey">
<t t-foreach="grouped_messages.keys()" t-as="gkey">
<t t-set="messages" t-value="grouped_messages[gkey]" /> <t t-set="messages" t-value="grouped_messages[gkey]" />
<t t-foreach="messages" t-as="msg"> <t t-foreach="messages" t-as="msg">
<div style="margin:20px"> <div style="margin:20px">

146
mail_digest/tests/test_digest.py

@ -1,59 +1,58 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Simone Orsi <simone.orsi@camptocamp.com>
# Copyright 2017-2018 Camptocamp - Simone Orsi
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo.tests.common import TransactionCase
from odoo.tests.common import SavepointCase
from odoo import exceptions from odoo import exceptions
class DigestCase(TransactionCase):
class DigestCase(SavepointCase):
def setUp(self):
super(DigestCase, self).setUp()
self.partner_model = self.env['res.partner']
self.message_model = self.env['mail.message']
self.subtype_model = self.env['mail.message.subtype']
self.digest_model = self.env['mail.digest']
self.conf_model = self.env['partner.notification.conf']
@classmethod
def setUpClass(cls):
super(DigestCase, cls).setUpClass()
cls.message_model = cls.env['mail.message']
cls.subtype_model = cls.env['mail.message.subtype']
cls.digest_model = cls.env['mail.digest']
cls.conf_model = cls.env['user.notification.conf']
self.partner1 = self.partner_model.with_context(
tracking_disable=1).create({
'name': 'Partner 1',
'email': 'partner1@test.foo.com',
user_model = cls.env['res.users'].with_context(
no_reset_password=True, tracking_disable=True)
cls.user1 = user_model.create({
'name': 'User 1',
'login': 'testuser1',
'email': 'testuser1@email.com',
}) })
self.partner2 = self.partner_model.with_context(
tracking_disable=1).create({
'name': 'Partner 2',
'email': 'partner2@test.foo.com',
cls.user2 = user_model.create({
'name': 'User 2',
'login': 'testuser2',
'email': 'testuser2@email.com',
}) })
self.partner3 = self.partner_model.with_context(
tracking_disable=1).create({
'name': 'Partner 3',
'email': 'partner3@test.foo.com',
cls.user3 = user_model.create({
'name': 'User 3',
'login': 'testuser3',
'email': 'testuser3@email.com',
}) })
self.subtype1 = self.subtype_model.create({'name': 'Type 1'})
self.subtype2 = self.subtype_model.create({'name': 'Type 2'})
cls.subtype1 = cls.subtype_model.create({'name': 'Type 1'})
cls.subtype2 = cls.subtype_model.create({'name': 'Type 2'})
def test_get_or_create_digest(self): def test_get_or_create_digest(self):
message1 = self.message_model.create({
self.message_model.create({
'body': 'My Body 1', 'body': 'My Body 1',
'subtype_id': self.subtype1.id, 'subtype_id': self.subtype1.id,
}) })
message2 = self.message_model.create({
self.message_model.create({
'body': 'My Body 2', 'body': 'My Body 2',
'subtype_id': self.subtype2.id, 'subtype_id': self.subtype2.id,
}) })
# 2 messages, 1 digest container # 2 messages, 1 digest container
dig1 = self.digest_model._get_or_create_by_partner(
self.partner1, message1)
dig2 = self.digest_model._get_or_create_by_partner(
self.partner1, message2)
dig1 = self.digest_model._get_or_create_by_user(self.user1)
dig2 = self.digest_model._get_or_create_by_user(self.user1)
self.assertEqual(dig1, dig2) self.assertEqual(dig1, dig2)
def test_create_or_update_digest(self): def test_create_or_update_digest(self):
partners = self.partner_model
partners |= self.partner1
partners |= self.partner2
partners = self.env['res.partner']
partners |= self.user1.partner_id
partners |= self.user2.partner_id
message1 = self.message_model.create({ message1 = self.message_model.create({
'body': 'My Body 1', 'body': 'My Body 1',
'subtype_id': self.subtype1.id, 'subtype_id': self.subtype1.id,
@ -63,72 +62,75 @@ class DigestCase(TransactionCase):
'subtype_id': self.subtype2.id, 'subtype_id': self.subtype2.id,
}) })
# partner 1 # partner 1
self.digest_model.create_or_update(self.partner1, message1)
self.digest_model.create_or_update(self.partner1, message2)
p1dig = self.digest_model._get_or_create_by_partner(self.partner1)
self.digest_model.create_or_update(self.user1.partner_id, message1)
self.digest_model.create_or_update(self.user1.partner_id, message2)
p1dig = self.digest_model._get_or_create_by_user(self.user1)
self.assertIn(message1, p1dig.message_ids) self.assertIn(message1, p1dig.message_ids)
self.assertIn(message2, p1dig.message_ids) self.assertIn(message2, p1dig.message_ids)
# partner 2 # partner 2
self.digest_model.create_or_update(self.partner2, message1)
self.digest_model.create_or_update(self.partner2, message2)
p2dig = self.digest_model._get_or_create_by_partner(self.partner2)
self.digest_model.create_or_update(self.user2.partner_id, message1)
self.digest_model.create_or_update(self.user2.partner_id, message2)
p2dig = self.digest_model._get_or_create_by_user(self.user2)
self.assertIn(message1, p2dig.message_ids) self.assertIn(message1, p2dig.message_ids)
self.assertIn(message2, p2dig.message_ids) self.assertIn(message2, p2dig.message_ids)
def test_notify_partner_digest(self):
def test_notify_user_digest(self):
message = self.message_model.create({ message = self.message_model.create({
'body': 'My Body 1', 'body': 'My Body 1',
'subtype_id': self.subtype1.id, 'subtype_id': self.subtype1.id,
}) })
self.partner1.notify_email = 'digest'
self.user1.digest_mode = True
# notify partner # notify partner
self.partner1._notify(message)
self.user1.partner_id._notify(message)
# we should find the message in its digest # we should find the message in its digest
dig1 = self.digest_model._get_or_create_by_partner(
self.partner1, message)
dig1 = self.digest_model._get_or_create_by_user(self.user1)
self.assertIn(message, dig1.message_ids) self.assertIn(message, dig1.message_ids)
def test_notify_partner_digest_followers(self): def test_notify_partner_digest_followers(self):
self.partner3.message_subscribe(self.partner2.ids)
self.partner1.notify_email = 'digest'
self.partner2.notify_email = 'digest'
partners = self.partner1 + self.partner2
# subscribe a partner to the other one
self.user3.partner_id.message_subscribe(
partner_ids=self.user2.partner_id.ids)
self.user1.digest_mode = True
self.user2.digest_mode = True
partners = self.user1.partner_id + self.user2.partner_id
message = self.message_model.create({ message = self.message_model.create({
'body': 'My Body 1', 'body': 'My Body 1',
'subtype_id': self.subtype1.id, 'subtype_id': self.subtype1.id,
'res_id': self.partner3.id,
'res_id': self.user3.partner_id.id,
'model': 'res.partner', 'model': 'res.partner',
'partner_ids': [(4, self.partner1.id)]
'partner_ids': [(4, self.user1.partner_id.id)]
}) })
# notify partners # notify partners
partners._notify(message)
partners.with_context(foo=1)._notify(message)
# we should find the a digest for each partner # we should find the a digest for each partner
dig1 = self.digest_model._get_by_partner(self.partner1)
dig2 = self.digest_model._get_by_partner(self.partner2)
dig1 = self.digest_model._get_by_user(self.user1)
dig2 = self.digest_model._get_by_user(self.user2)
# and the message in them # and the message in them
self.assertIn(message, dig1.message_ids) self.assertIn(message, dig1.message_ids)
self.assertIn(message, dig2.message_ids) self.assertIn(message, dig2.message_ids)
# now we exclude followers # now we exclude followers
dig1.unlink() dig1.unlink()
dig2.unlink() dig2.unlink()
partners.with_context(notify_only_recipients=1)._notify(message)
partners.with_context(notify_only_recipients=True)._notify(message)
# we should find the a digest for each partner # we should find the a digest for each partner
self.assertTrue(self.digest_model._get_by_partner(self.partner1))
self.assertFalse(self.digest_model._get_by_partner(self.partner2))
self.assertTrue(self.digest_model._get_by_user(self.user1))
self.assertFalse(self.digest_model._get_by_user(self.user2))
def test_global_conf(self): def test_global_conf(self):
for k in ('email', 'comment', 'notification'): for k in ('email', 'comment', 'notification'):
self.assertIn(k, self.partner1._digest_enabled_message_types())
self.assertIn(
k, self.env['res.partner']._digest_enabled_message_types())
self.env['ir.config_parameter'].set_param( self.env['ir.config_parameter'].set_param(
'mail_digest.enabled_message_types', 'mail_digest.enabled_message_types',
'email,notification' 'email,notification'
) )
for k in ('email', 'notification'): for k in ('email', 'notification'):
self.assertIn(k, self.partner1._digest_enabled_message_types())
self.assertIn(
k, self.env['res.partner']._digest_enabled_message_types())
self.assertNotIn( self.assertNotIn(
'comment', self.partner1._digest_enabled_message_types())
'comment', self.env['res.partner']._digest_enabled_message_types())
def test_notify_partner_digest_global_disabled(self):
def test_notify_user_digest_global_disabled(self):
# change global conf # change global conf
self.env['ir.config_parameter'].set_param( self.env['ir.config_parameter'].set_param(
'mail_digest.enabled_message_types', 'mail_digest.enabled_message_types',
@ -140,46 +142,46 @@ class DigestCase(TransactionCase):
# globally disabled type # globally disabled type
'message_type': 'notification', 'message_type': 'notification',
}) })
self.partner1.notify_email = 'digest'
self.user1.digest_mode = True
# notify partner # notify partner
self.partner1._notify(message)
self.user1.partner_id._notify(message)
# we should not find any digest # we should not find any digest
self.assertFalse(self.digest_model._get_by_partner(self.partner1))
self.assertFalse(self.digest_model._get_by_user(self.user1))
def _create_for_partner(self, partner): def _create_for_partner(self, partner):
messages = {} messages = {}
for type_id in (self.subtype1.id, self.subtype2.id): for type_id in (self.subtype1.id, self.subtype2.id):
for k in xrange(1, 3):
for k in range(1, 3):
key = '{}_{}'.format(type_id, k) key = '{}_{}'.format(type_id, k)
messages[key] = self.message_model.create({ messages[key] = self.message_model.create({
'subject': 'My Subject {}'.format(key), 'subject': 'My Subject {}'.format(key),
'body': 'My Body {}'.format(key), 'body': 'My Body {}'.format(key),
'subtype_id': type_id, 'subtype_id': type_id,
}) })
self.digest_model.create_or_update(
partner, messages[key])
return self.digest_model._get_or_create_by_partner(partner)
self.digest_model.create_or_update(partner, messages[key])
return self.digest_model._get_by_user(partner.real_user_id)
def test_digest_group_messages(self): def test_digest_group_messages(self):
dig = self._create_for_partner(self.partner1)
dig = self._create_for_partner(self.user1.partner_id)
grouped = dig._message_group_by() grouped = dig._message_group_by()
for type_id in (self.subtype1.id, self.subtype2.id): for type_id in (self.subtype1.id, self.subtype2.id):
self.assertIn(type_id, grouped) self.assertIn(type_id, grouped)
self.assertEqual(len(grouped[type_id]), 2) self.assertEqual(len(grouped[type_id]), 2)
def test_digest_mail_values(self): def test_digest_mail_values(self):
dig = self._create_for_partner(self.partner1)
dig = self._create_for_partner(self.user1.partner_id)
values = dig._get_email_values() values = dig._get_email_values()
expected = ('recipient_ids', 'subject', 'body_html') expected = ('recipient_ids', 'subject', 'body_html')
for k in expected: for k in expected:
self.assertIn(k, values) self.assertIn(k, values)
self.assertEqual(self.env.user.company_id.email, values['email_from']) self.assertEqual(self.env.user.company_id.email, values['email_from'])
self.assertEqual([(4, self.partner1.id)], values['recipient_ids'])
self.assertEqual(
[(4, self.user1.partner_id.id)], values['recipient_ids'])
def test_digest_template(self): def test_digest_template(self):
default = self.env.ref('mail_digest.default_digest_tmpl') default = self.env.ref('mail_digest.default_digest_tmpl')
dig = self._create_for_partner(self.partner1)
dig = self._create_for_partner(self.user1.partner_id)
# check default # check default
self.assertEqual(dig.digest_template_id, default) self.assertEqual(dig.digest_template_id, default)
self.assertTrue(dig._get_email_values()) self.assertTrue(dig._get_email_values())

140
mail_digest/tests/test_partner_domains.py

@ -1,38 +1,44 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Simone Orsi <simone.orsi@camptocamp.com>
# Copyright 2017-2018 Camptocamp - Simone Orsi
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo.tests.common import TransactionCase
from odoo.tests.common import SavepointCase
class PartnerDomainCase(TransactionCase):
class PartnerDomainCase(SavepointCase):
def setUp(self):
super(PartnerDomainCase, self).setUp()
self.partner_model = self.env['res.partner']
self.message_model = self.env['mail.message']
self.subtype_model = self.env['mail.message.subtype']
@classmethod
def setUpClass(cls):
super(PartnerDomainCase, cls).setUpClass()
self.partner1 = self.partner_model.with_context(
tracking_disable=1).create({
'name': 'Partner 1',
'email': 'partner1@test.foo.com',
cls.partner_model = cls.env['res.partner']
cls.message_model = cls.env['mail.message']
cls.subtype_model = cls.env['mail.message.subtype']
user_model = cls.env['res.users'].with_context(
no_reset_password=True, tracking_disable=True)
cls.user1 = user_model.create({
'name': 'User 1',
'login': 'testuser1',
'email': 'testuser1@email.com',
}) })
self.partner2 = self.partner_model.with_context(
tracking_disable=1).create({
'name': 'Partner 2',
'email': 'partner2@test.foo.com',
cls.user2 = user_model.create({
'name': 'User 2',
'login': 'testuser2',
'email': 'testuser2@email.com',
}) })
self.partner3 = self.partner_model.with_context(
tracking_disable=1).create({
'name': 'Partner 3',
'email': 'partner3@test.foo.com',
cls.user3 = user_model.create({
'name': 'User 3',
'login': 'testuser3',
'email': 'testuser3@email.com',
}) })
self.subtype1 = self.subtype_model.create({'name': 'Type 1'})
self.subtype2 = self.subtype_model.create({'name': 'Type 2'})
cls.partner1 = cls.user1.partner_id
cls.partner2 = cls.user2.partner_id
cls.partner3 = cls.user3.partner_id
cls.subtype1 = cls.subtype_model.create({'name': 'Type 1'})
cls.subtype2 = cls.subtype_model.create({'name': 'Type 2'})
def _assert_found(self, domain, not_found=False, partner=None):
partner = partner or self.partner1
def _assert_found(self, partner, domain, not_found=False):
if not_found: if not_found:
self.assertNotIn(partner, self.partner_model.search(domain)) self.assertNotIn(partner, self.partner_model.search(domain))
else: else:
@ -43,17 +49,17 @@ class PartnerDomainCase(TransactionCase):
# because we call `_get_notify_by_email_domain` directly # because we call `_get_notify_by_email_domain` directly
message = self.message_model.create({'body': 'My Body', }) message = self.message_model.create({'body': 'My Body', })
partner = self.partner1 partner = self.partner1
partner.notify_email = 'always'
partner.real_user_id.notification_type = 'email'
domain = partner._get_notify_by_email_domain(message) domain = partner._get_notify_by_email_domain(message)
self._assert_found(domain)
domain = partner._get_notify_by_email_domain(message, digest=1)
self._assert_found(domain, not_found=1)
self._assert_found(partner, domain)
domain = partner._get_notify_by_email_domain(message, digest=True)
self._assert_found(partner, domain, not_found=True)
def test_notify_domains_only_recipients(self): def test_notify_domains_only_recipients(self):
# we don't set recipients # we don't set recipients
# because we call `_get_notify_by_email_domain` directly # because we call `_get_notify_by_email_domain` directly
self.partner1.notify_email = 'always'
self.partner2.notify_email = 'always'
self.partner1.real_user_id.notification_type = 'email'
self.partner2.real_user_id.notification_type = 'email'
partners = self.partner1 + self.partner2 partners = self.partner1 + self.partner2
# followers # followers
self.partner3.message_subscribe(self.partner2.ids) self.partner3.message_subscribe(self.partner2.ids)
@ -66,42 +72,47 @@ class PartnerDomainCase(TransactionCase):
}) })
domain = partners._get_notify_by_email_domain(message) domain = partners._get_notify_by_email_domain(message)
# we find both of them since partner2 is a follower # we find both of them since partner2 is a follower
self._assert_found(domain)
self._assert_found(domain, partner=self.partner2)
self._assert_found(self.partner1, domain)
self._assert_found(self.partner2, domain)
# no one here in digest mode # no one here in digest mode
domain = partners._get_notify_by_email_domain(message, digest=1)
self._assert_found(domain, not_found=1)
self._assert_found(domain, not_found=1, partner=self.partner2)
domain = partners._get_notify_by_email_domain(message, digest=True)
self._assert_found(self.partner1, domain, not_found=True)
self._assert_found(self.partner2, domain, not_found=True)
# include only recipients # include only recipients
domain = partners.with_context( domain = partners.with_context(
notify_only_recipients=1)._get_notify_by_email_domain(message) notify_only_recipients=1)._get_notify_by_email_domain(message)
self._assert_found(domain)
self._assert_found(domain, partner=self.partner2, not_found=1)
self._assert_found(self.partner1, domain)
self._assert_found(self.partner2, domain, not_found=True)
def test_notify_domains_digest(self): def test_notify_domains_digest(self):
# we don't set recipients # we don't set recipients
# because we call `_get_notify_by_email_domain` directly # because we call `_get_notify_by_email_domain` directly
message = self.message_model.create({'body': 'My Body', }) message = self.message_model.create({'body': 'My Body', })
partner = self.partner1 partner = self.partner1
partner.notify_email = 'digest'
partner.real_user_id.write({
'notification_type': 'email',
'digest_mode': True,
})
domain = partner._get_notify_by_email_domain(message) domain = partner._get_notify_by_email_domain(message)
self._assert_found(domain, not_found=1)
domain = partner._get_notify_by_email_domain(message, digest=1)
self._assert_found(domain)
self._assert_found(partner, domain, not_found=True)
domain = partner._get_notify_by_email_domain(message, digest=True)
self._assert_found(partner, domain)
def test_notify_domains_none(self): def test_notify_domains_none(self):
message = self.message_model.create({'body': 'My Body', }) message = self.message_model.create({'body': 'My Body', })
partner = self.partner1 partner = self.partner1
partner.notify_email = 'none'
partner.real_user_id.write({
'notification_type': 'inbox',
})
domain = partner._get_notify_by_email_domain(message) domain = partner._get_notify_by_email_domain(message)
self._assert_found(domain, not_found=1)
domain = partner._get_notify_by_email_domain(message, digest=1)
self._assert_found(domain, not_found=1)
self._assert_found(partner, domain, not_found=True)
domain = partner._get_notify_by_email_domain(message, digest=True)
self._assert_found(partner, domain, not_found=True)
def test_notify_domains_match_type_digest(self): def test_notify_domains_match_type_digest(self):
# Test message subtype matches partner settings. # Test message subtype matches partner settings.
# The partner can have several `partner.notification.conf` records.
# The partner can have several `user.notification.conf` records.
# Each records establish notification rules by type. # Each records establish notification rules by type.
# If you don't have any record in it, you allow all subtypes. # If you don't have any record in it, you allow all subtypes.
# Record `typeX` with `enable=True` enables notification for `typeX`. # Record `typeX` with `enable=True` enables notification for `typeX`.
@ -109,7 +120,10 @@ class PartnerDomainCase(TransactionCase):
partner = self.partner1 partner = self.partner1
# enable digest # enable digest
partner.notify_email = 'digest'
partner.real_user_id.write({
'notification_type': 'email',
'digest_mode': True,
})
message_t1 = self.message_model.create({ message_t1 = self.message_model.create({
'body': 'My Body', 'body': 'My Body',
'subtype_id': self.subtype1.id, 'subtype_id': self.subtype1.id,
@ -119,25 +133,25 @@ class PartnerDomainCase(TransactionCase):
'subtype_id': self.subtype2.id, 'subtype_id': self.subtype2.id,
}) })
# enable subtype on partner # enable subtype on partner
partner._notify_enable_subtype(self.subtype1)
partner.real_user_id._notify_enable_subtype(self.subtype1)
domain = partner._get_notify_by_email_domain( domain = partner._get_notify_by_email_domain(
message_t1, digest=True) message_t1, digest=True)
# notification enabled: we find the partner. # notification enabled: we find the partner.
self._assert_found(domain)
self._assert_found(partner, domain)
# for subtype2 we don't have any explicit rule: we find the partner # for subtype2 we don't have any explicit rule: we find the partner
domain = partner._get_notify_by_email_domain( domain = partner._get_notify_by_email_domain(
message_t2, digest=True) message_t2, digest=True)
self._assert_found(domain)
self._assert_found(partner, domain)
# enable subtype2: find the partner anyway # enable subtype2: find the partner anyway
partner._notify_enable_subtype(self.subtype2)
partner.real_user_id._notify_enable_subtype(self.subtype2)
domain = partner._get_notify_by_email_domain( domain = partner._get_notify_by_email_domain(
message_t2, digest=True) message_t2, digest=True)
self._assert_found(domain)
self._assert_found(partner, domain)
# disable subtype2: we don't find the partner anymore # disable subtype2: we don't find the partner anymore
partner._notify_disable_subtype(self.subtype2)
partner.real_user_id._notify_disable_subtype(self.subtype2)
domain = partner._get_notify_by_email_domain( domain = partner._get_notify_by_email_domain(
message_t2, digest=True) message_t2, digest=True)
self._assert_found(domain, not_found=1)
self._assert_found(partner, domain, not_found=True)
def test_notify_domains_match_type_always(self): def test_notify_domains_match_type_always(self):
message_t1 = self.message_model.create({ message_t1 = self.message_model.create({
@ -150,20 +164,20 @@ class PartnerDomainCase(TransactionCase):
}) })
# enable always # enable always
partner = self.partner1 partner = self.partner1
partner.notify_email = 'always'
partner.real_user_id.notification_type = 'email'
# enable subtype on partner # enable subtype on partner
partner._notify_enable_subtype(self.subtype1)
partner.real_user_id._notify_enable_subtype(self.subtype1)
domain = partner._get_notify_by_email_domain(message_t1) domain = partner._get_notify_by_email_domain(message_t1)
# notification enabled: we find the partner. # notification enabled: we find the partner.
self._assert_found(domain)
self._assert_found(partner, domain)
# for subtype2 we don't have any explicit rule: we find the partner # for subtype2 we don't have any explicit rule: we find the partner
domain = partner._get_notify_by_email_domain(message_t2) domain = partner._get_notify_by_email_domain(message_t2)
self._assert_found(domain)
self._assert_found(partner, domain)
# enable subtype2: find the partner anyway # enable subtype2: find the partner anyway
partner._notify_enable_subtype(self.subtype2)
partner.real_user_id._notify_enable_subtype(self.subtype2)
domain = partner._get_notify_by_email_domain(message_t2) domain = partner._get_notify_by_email_domain(message_t2)
self._assert_found(domain)
self._assert_found(partner, domain)
# disable subtype2: we don't find the partner anymore # disable subtype2: we don't find the partner anymore
partner._notify_disable_subtype(self.subtype2)
partner.real_user_id._notify_disable_subtype(self.subtype2)
domain = partner._get_notify_by_email_domain(message_t2) domain = partner._get_notify_by_email_domain(message_t2)
self._assert_found(domain, not_found=1)
self._assert_found(partner, domain, not_found=True)

115
mail_digest/tests/test_subtypes_conf.py

@ -1,136 +1,109 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Simone Orsi <simone.orsi@camptocamp.com>
# Copyright 2017-2018 Camptocamp - Simone Orsi
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo.tests.common import TransactionCase
from odoo.tests.common import SavepointCase
class SubtypesCase(TransactionCase):
class SubtypesCase(SavepointCase):
def setUp(self):
super(SubtypesCase, self).setUp()
self.partner_model = self.env['res.partner']
self.message_model = self.env['mail.message']
self.subtype_model = self.env['mail.message.subtype']
@classmethod
def setUpClass(cls):
super(SubtypesCase, cls).setUpClass()
self.partner1 = self.partner_model.with_context(
tracking_disable=1).create({
'name': 'Partner 1!',
'email': 'partner1@test.foo.com',
user_model = cls.env['res.users'].with_context(
no_reset_password=True, tracking_disable=True)
cls.user1 = user_model.create({
'name': 'User 1',
'login': 'testuser1',
'email': 'testuser1@email.com',
}) })
self.partner2 = self.partner_model.with_context(
tracking_disable=1).create({
'name': 'Partner 2!',
'email': 'partner2@test.foo.com',
cls.user2 = user_model.create({
'name': 'User 2',
'login': 'testuser2',
'email': 'testuser2@email.com',
}) })
self.subtype1 = self.subtype_model.create({'name': 'Type 1'})
self.subtype2 = self.subtype_model.create({'name': 'Type 2'})
subtype_model = cls.env['mail.message.subtype']
cls.subtype1 = subtype_model.create({'name': 'Type 1'})
cls.subtype2 = subtype_model.create({'name': 'Type 2'})
cls.subtype3 = subtype_model.create({'name': 'Type 3'})
cls.subtype4 = subtype_model.create({'name': 'Type 4'})
def _test_subtypes_rel(self): def _test_subtypes_rel(self):
# setup: # setup:
# t1, t2 enabled # t1, t2 enabled
# t3 disabled # t3 disabled
# t4 no conf # t4 no conf
self.subtype3 = self.subtype_model.create({'name': 'Type 3'})
self.subtype4 = self.subtype_model.create({'name': 'Type 4'})
# enable t1 t2 # enable t1 t2
self.partner1._notify_enable_subtype(self.subtype1)
self.partner1._notify_enable_subtype(self.subtype2)
self.user1._notify_enable_subtype(self.subtype1)
self.user1._notify_enable_subtype(self.subtype2)
# disable t3 # disable t3
self.partner1._notify_disable_subtype(self.subtype3)
self.user1._notify_disable_subtype(self.subtype3)
def test_partner_computed_subtype(self):
def test_user_computed_subtype(self):
self._test_subtypes_rel() self._test_subtypes_rel()
# check computed fields # check computed fields
self.assertIn( self.assertIn(
self.subtype1, self.partner1.enabled_notify_subtype_ids)
self.subtype1, self.user1.enabled_notify_subtype_ids)
self.assertNotIn( self.assertNotIn(
self.subtype1, self.partner1.disabled_notify_subtype_ids)
self.subtype1, self.user1.disabled_notify_subtype_ids)
self.assertIn( self.assertIn(
self.subtype2, self.partner1.enabled_notify_subtype_ids)
self.subtype2, self.user1.enabled_notify_subtype_ids)
self.assertNotIn( self.assertNotIn(
self.subtype2, self.partner1.disabled_notify_subtype_ids)
self.subtype2, self.user1.disabled_notify_subtype_ids)
self.assertIn( self.assertIn(
self.subtype3, self.partner1.disabled_notify_subtype_ids)
self.subtype3, self.user1.disabled_notify_subtype_ids)
self.assertNotIn( self.assertNotIn(
self.subtype3, self.partner1.enabled_notify_subtype_ids)
self.subtype3, self.user1.enabled_notify_subtype_ids)
self.assertNotIn( self.assertNotIn(
self.subtype4, self.subtype4,
self.partner1.enabled_notify_subtype_ids)
self.user1.enabled_notify_subtype_ids)
self.assertNotIn( self.assertNotIn(
self.subtype4, self.subtype4,
self.partner1.disabled_notify_subtype_ids)
self.user1.disabled_notify_subtype_ids)
def test_partner_find_by_subtype_incl(self):
def test_find_user_by_subtype_incl(self):
self._test_subtypes_rel() self._test_subtypes_rel()
domain = [( domain = [(
'enabled_notify_subtype_ids', 'enabled_notify_subtype_ids',
'in', (self.subtype1.id, self.subtype2.id), 'in', (self.subtype1.id, self.subtype2.id),
)] )]
self.assertIn(
self.partner1,
self.partner_model.search(domain)
)
self.assertIn(self.user1, self.env['res.users'].search(domain))
domain = [( domain = [(
'disabled_notify_subtype_ids', 'in', self.subtype3.id, 'disabled_notify_subtype_ids', 'in', self.subtype3.id,
)] )]
self.assertIn(
self.partner1,
self.partner_model.search(domain)
)
self.assertIn(self.user1, self.env['res.users'].search(domain))
domain = [( domain = [(
'enabled_notify_subtype_ids', 'in', (self.subtype3.id, ), 'enabled_notify_subtype_ids', 'in', (self.subtype3.id, ),
)] )]
self.assertNotIn(
self.partner1,
self.partner_model.search(domain)
)
self.assertNotIn(self.user1, self.env['res.users'].search(domain))
domain = [( domain = [(
'enabled_notify_subtype_ids', 'in', (self.subtype4.id, ), 'enabled_notify_subtype_ids', 'in', (self.subtype4.id, ),
)] )]
self.assertNotIn(
self.partner1,
self.partner_model.search(domain)
)
self.assertNotIn(self.user1, self.env['res.users'].search(domain))
domain = [( domain = [(
'disabled_notify_subtype_ids', 'in', (self.subtype4.id, ), 'disabled_notify_subtype_ids', 'in', (self.subtype4.id, ),
)] )]
self.assertNotIn(
self.partner1,
self.partner_model.search(domain)
)
self.assertNotIn(self.user1, self.env['res.users'].search(domain))
def test_partner_find_by_subtype_escl(self):
def test_find_user_by_subtype_escl(self):
self._test_subtypes_rel() self._test_subtypes_rel()
domain = [( domain = [(
'enabled_notify_subtype_ids', 'enabled_notify_subtype_ids',
'not in', (self.subtype4.id, ), 'not in', (self.subtype4.id, ),
)] )]
self.assertIn(
self.partner1,
self.partner_model.search(domain)
)
self.assertIn(self.user1, self.env['res.users'].search(domain))
domain = [( domain = [(
'disabled_notify_subtype_ids', 'disabled_notify_subtype_ids',
'not in', (self.subtype4.id, ), 'not in', (self.subtype4.id, ),
)] )]
self.assertIn(
self.partner1,
self.partner_model.search(domain)
)
self.assertIn(self.user1, self.env['res.users'].search(domain))
domain = [( domain = [(
'enabled_notify_subtype_ids', 'enabled_notify_subtype_ids',
'not in', (self.subtype3.id, ), 'not in', (self.subtype3.id, ),
)] )]
self.assertIn(
self.partner1,
self.partner_model.search(domain)
)
self.assertIn(self.user1, self.env['res.users'].search(domain))
domain = [( domain = [(
'disabled_notify_subtype_ids', 'disabled_notify_subtype_ids',
'not in', (self.subtype1.id, self.subtype2.id), 'not in', (self.subtype1.id, self.subtype2.id),
)] )]
self.assertIn(
self.partner1,
self.partner_model.search(domain)
)
self.assertIn(self.user1, self.env['res.users'].search(domain))

4
mail_digest/views/mail_digest_views.xml

@ -6,7 +6,7 @@
<field name="model">mail.digest</field> <field name="model">mail.digest</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="Mail digest"> <tree string="Mail digest">
<field name="partner_id" />
<field name="user_id" />
<field name="mail_id" /> <field name="mail_id" />
<field name="state" /> <field name="state" />
</tree> </tree>
@ -27,7 +27,7 @@
</div> </div>
<group name="main" col="2"> <group name="main" col="2">
<field name="name" /> <field name="name" />
<field name="partner_id" />
<field name="user_id" />
</group> </group>
<group name="settings" col="2"> <group name="settings" col="2">
<field name="frequency" /> <field name="frequency" />

38
mail_digest/views/partner_views.xml

@ -1,38 +0,0 @@
<?xml version="1.0"?>
<odoo>
<record id="notifications_emails_partner_info_form" model="ir.ui.view">
<field name="name">mail.notifications res.partner.form</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="mail.view_emails_partner_info_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='notify_email']" position="after">
<field name="notify_conf_ids" attrs="{'invisible': [('notify_email','=', 'none')]}"/>
</xpath>
</field>
</record>
<record model="ir.ui.view" id="notification_form">
<field name="name">partner.notification.conf form</field>
<field name="model">partner.notification.conf</field>
<field name="arch" type="xml">
<form string="Notification">
<group name="main">
<field name="enabled" />
<field name="subtype_id" options="{'no_create': True}" />
</group>
</form>
</field>
</record>
<record model="ir.ui.view" id="notification_tree">
<field name="name">partner.notification.conf tree</field>
<field name="model">partner.notification.conf</field>
<field name="arch" type="xml">
<tree string="Notifications" editable="top">
<field name="enabled" />
<field name="subtype_id" />
</tree>
</field>
</record>
</odoo>

28
mail_digest/views/user_notification_views.xml

@ -0,0 +1,28 @@
<?xml version="1.0"?>
<odoo>
<record model="ir.ui.view" id="notification_form">
<field name="name">user.notification.conf form</field>
<field name="model">user.notification.conf</field>
<field name="arch" type="xml">
<form string="Notification">
<group name="main">
<field name="enabled" />
<field name="subtype_id" options="{'no_create': True}" />
</group>
</form>
</field>
</record>
<record model="ir.ui.view" id="notification_tree">
<field name="name">user.notification.conf tree</field>
<field name="model">user.notification.conf</field>
<field name="arch" type="xml">
<tree string="Notifications" editable="top">
<field name="enabled" />
<field name="subtype_id" />
</tree>
</field>
</record>
</odoo>

23
mail_digest/views/user_views.xml

@ -5,13 +5,14 @@
<field name="model">res.users</field> <field name="model">res.users</field>
<field name="inherit_id" ref="mail.view_users_form_mail"/> <field name="inherit_id" ref="mail.view_users_form_mail"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<field name="notify_email" position="replace">
<field name="notification_type" position="replace">
<group name="notif_left"> <group name="notif_left">
<field name="notify_email" widget="radio" readonly="0"/>
<field name="notify_frequency" readonly="0"
attrs="{'invisible': [('notify_email', '!=', 'digest')]}" />
<field name="notification_type" widget="radio" readonly="0"/>
<field name="digest_mode" />
<field name="digest_frequency" readonly="0"
attrs="{'invisible': [('digest_mode', '=', False)]}" />
</group> </group>
<group name="notif_right" attrs="{'invisible': [('notify_email','=', 'none')]}">
<group name="notif_right" attrs="{'invisible': [('notification_type','=', 'inbox')]}">
<label string="Enable/disable notifications by type" colspan="4" /> <label string="Enable/disable notifications by type" colspan="4" />
<field name="notify_conf_ids" nolabel="1" colspan="4" readonly="0" /> <field name="notify_conf_ids" nolabel="1" colspan="4" readonly="0" />
</group> </group>
@ -25,17 +26,19 @@
<field name="model">res.users</field> <field name="model">res.users</field>
<field name="inherit_id" ref="mail.view_users_form_simple_modif_mail"/> <field name="inherit_id" ref="mail.view_users_form_simple_modif_mail"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<field name="notify_email" position="replace">
<field name="notification_type" position="replace">
<group name="notif_left"> <group name="notif_left">
<field name="notify_email" widget="radio" readonly="0"/>
<field name="notify_frequency" readonly="0"
attrs="{'invisible': [('notify_email', '!=', 'digest')]}" />
<field name="digest_mode" />
<field name="notification_type" widget="radio" readonly="0"/>
<field name="digest_frequency" readonly="0"
attrs="{'invisible': [('digest_mode', '=', False)]}" />
</group> </group>
<group name="notif_right" attrs="{'invisible': [('notify_email','=', 'none')]}">
<group name="notif_right" attrs="{'invisible': [('notification_type','=', 'inbox')]}">
<label string="Enable/disable notifications by type" colspan="4" /> <label string="Enable/disable notifications by type" colspan="4" />
<field name="notify_conf_ids" nolabel="1" colspan="4" readonly="0" /> <field name="notify_conf_ids" nolabel="1" colspan="4" readonly="0" />
</group> </group>
</field> </field>
</field> </field>
</record> </record>
</odoo> </odoo>
Loading…
Cancel
Save