[ADD] mail_group: add a new module to manage the mailing lists
Purpose ======= The purpose of this new module is to manage the mailing lists. Now they are no more <mail.channel> (will email_send set to True) but they have their own model. Specifications ============== The mailing list are basically a public discussion that users can have by email. They can respond to email, send new messages, etc... All the members of the mailing list will receive the message by email. Users can moderate the emails of the mailing list in the same way as they did with the "email" mail channel. - *accept*, will accept the emails and send it to the members of the mailing list - *discard*, will drop the email without warning the author - *reject*, will drop the email and send a notification email to the author - *allow*, will accept the email and all other pending emails of the same author and create a whitelist for him - *ban* will drop the email and all other pending emails of the same author and create a blacklist for him Now a portal view is available (in /groups) and replace the old module "Website Mail Channel" that was removed in the previous commit. In this view, users can subscribe / unsubscribe to the mailing lists and some links to this portal view are added in the footer of the emails of the mailing list. As before with mail channels, you can send guidelines to the members, notify the moderators whose an action is required (in a CRON), send back an email to the author of an email to say "Your message is waiting moderation"... Links ===== Task-2510267 See odoo/odoo/pull/71599 See odoo/enterprise/pull/19296 See odoo/upgrade/pull/2600
This commit is contained in:
committed by
Thibault Delavallée
parent
583cc5d439
commit
2d359b909b
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
from . import wizard
|
||||
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': "Mail Group",
|
||||
'summary': "Manage your mailing lists",
|
||||
'description': """
|
||||
Manage your mailing lists from Odoo.
|
||||
""",
|
||||
'version': '1.0',
|
||||
'depends': [
|
||||
'mail',
|
||||
'portal',
|
||||
],
|
||||
'auto_install': False,
|
||||
'data': [
|
||||
'data/ir_cron_data.xml',
|
||||
'data/mail_templates.xml',
|
||||
'data/res_groups.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'security/mail_group_security.xml',
|
||||
'wizard/mail_group_message_reject_views.xml',
|
||||
'views/mail_group_member_views.xml',
|
||||
'views/mail_group_message_views.xml',
|
||||
'views/mail_group_moderation_views.xml',
|
||||
'views/mail_group_views.xml',
|
||||
'views/mail_group_menus.xml',
|
||||
'views/portal_templates.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/mail_group_demo.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_frontend': [
|
||||
'mail_group/static/src/*/*',
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import portal
|
||||
@@ -0,0 +1,279 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import werkzeug
|
||||
|
||||
from odoo import http, fields, tools
|
||||
from odoo.addons.http_routing.models.ir_http import slug
|
||||
from odoo.addons.portal.controllers.portal import pager as portal_pager
|
||||
from odoo.http import request
|
||||
from odoo.osv import expression
|
||||
|
||||
|
||||
class PortalMailGroup(http.Controller):
|
||||
_thread_per_page = 20
|
||||
_replies_per_page = 5
|
||||
|
||||
def _get_archives(self, group_id):
|
||||
"""Return the different date range and message count for the group messages."""
|
||||
results = request.env['mail.group.message']._read_group_raw(
|
||||
[('mail_group_id', '=', group_id)],
|
||||
['subject', 'create_date'],
|
||||
groupby=['create_date'], orderby='create_date DESC')
|
||||
|
||||
groups = []
|
||||
|
||||
for result in results:
|
||||
(dates_range, label) = result['create_date']
|
||||
start, end = dates_range.split('/')
|
||||
|
||||
groups.append({
|
||||
'date': label,
|
||||
'date_begin': fields.Date.to_string(fields.Date.to_date(start)),
|
||||
'date_end': fields.Date.to_string(fields.Date.to_date(end)),
|
||||
'messages_count': result['create_date_count'],
|
||||
})
|
||||
|
||||
return groups
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# MAIN PAGE
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@http.route('/groups', type='http', auth='public', sitemap=True, website=True)
|
||||
def groups_index(self, email='', **kw):
|
||||
"""View of the group lists. Allow the users to subscribe and unsubscribe."""
|
||||
if kw.get('group_id') and kw.get('token'):
|
||||
group_id = int(kw.get('group_id'))
|
||||
token = kw.get('token')
|
||||
group = request.env['mail.group'].browse(group_id).exists().sudo()
|
||||
if not group:
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
|
||||
if token != group._generate_group_access_token():
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
|
||||
mail_groups = group
|
||||
|
||||
else:
|
||||
mail_groups = request.env['mail.group'].search([]).sudo()
|
||||
|
||||
if not request.env.user._is_public():
|
||||
# Force the email if the user is logged
|
||||
email_normalized = request.env.user.email_normalized
|
||||
partner_id = request.env.user.partner_id.id
|
||||
else:
|
||||
email_normalized = tools.email_normalize(email)
|
||||
partner_id = None
|
||||
|
||||
members_data = mail_groups._find_members(email_normalized, partner_id)
|
||||
|
||||
return request.render('mail_group.mail_groups', {
|
||||
'mail_groups': [{
|
||||
'group': group,
|
||||
'is_member': bool(members_data.get(group.id, False)),
|
||||
} for group in mail_groups],
|
||||
'email': email_normalized,
|
||||
})
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# THREAD DISPLAY / MANAGEMENT
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@http.route([
|
||||
'/groups/<model("mail.group"):group>',
|
||||
'/groups/<model("mail.group"):group>/page/<int:page>',
|
||||
], type='http', auth='public', sitemap=True, website=True)
|
||||
def group_view_messages(self, group, page=1, mode='thread', date_begin=None, date_end=None, **post):
|
||||
GroupMessage = request.env['mail.group.message']
|
||||
|
||||
domain = [('mail_group_id', '=', group.id)]
|
||||
if mode == 'thread':
|
||||
domain += [('group_message_parent_id', '=', False)]
|
||||
|
||||
if date_begin and date_end:
|
||||
domain += [('create_date', '>=', date_begin), ('create_date', '<=', date_end)]
|
||||
|
||||
messages = GroupMessage.search(
|
||||
domain, limit=self._thread_per_page,
|
||||
offset=(page - 1) * self._thread_per_page,
|
||||
order='create_date, id DESC')
|
||||
|
||||
pager = portal_pager(
|
||||
url=f'/groups/{slug(group)}',
|
||||
total=GroupMessage.search_count(domain),
|
||||
page=page,
|
||||
step=self._thread_per_page,
|
||||
scope=5,
|
||||
url_args={'date_begin': date_begin, 'date_end': date_end, 'mode': mode}
|
||||
)
|
||||
|
||||
return request.render('mail_group.group_messages', {
|
||||
'group': group,
|
||||
'messages': messages,
|
||||
'archives': self._get_archives(group.id),
|
||||
'date_begin': date_begin,
|
||||
'date_end': date_end,
|
||||
'pager': pager,
|
||||
'replies_per_page': self._replies_per_page,
|
||||
})
|
||||
|
||||
@http.route('/groups/<model("mail.group"):group>/<model("mail.group.message"):message>',
|
||||
type='http', auth='public', sitemap=True, website=True)
|
||||
def group_view_message(self, group, message, mode='thread', date_begin=None, date_end=None, **post):
|
||||
if group != message.mail_group_id:
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
|
||||
GroupMessage = request.env['mail.group.message']
|
||||
base_domain = [
|
||||
('mail_group_id', '=', group.id),
|
||||
('group_message_parent_id', '=', message.group_message_parent_id.id),
|
||||
]
|
||||
|
||||
next_message = GroupMessage.search(
|
||||
expression.AND([base_domain, [('id', '>', message.id)]]),
|
||||
order='id ASC', limit=1)
|
||||
prev_message = GroupMessage.search(
|
||||
expression.AND([base_domain, [('id', '<', message.id)]]),
|
||||
order='id DESC', limit=1)
|
||||
|
||||
# Handle access to attachments for public / portal
|
||||
if request.env.user.has_group('base.group_user'):
|
||||
attachments = message.attachment_ids
|
||||
else:
|
||||
attachments = message.sudo().attachment_ids
|
||||
attachments.generate_access_token()
|
||||
|
||||
values = {
|
||||
'message': message,
|
||||
'attachments': attachments,
|
||||
'group': group,
|
||||
'mode': mode,
|
||||
'archives': self._get_archives(group.id),
|
||||
'date_begin': date_begin,
|
||||
'date_end': date_end,
|
||||
'replies_per_page': self._replies_per_page,
|
||||
'next_message': next_message,
|
||||
'prev_message': prev_message,
|
||||
}
|
||||
return request.render('mail_group.group_message', values)
|
||||
|
||||
@http.route('/groups/<model("mail.group"):group>/<model("mail.group.message"):message>/get_replies',
|
||||
type='json', auth='public', methods=['POST'], website=True)
|
||||
def group_message_get_replies(self, group, message, last_displayed_id, **post):
|
||||
if group != message.mail_group_id:
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
|
||||
replies_domain = [('id', '>', int(last_displayed_id)), ('group_message_parent_id', '=', message.id)]
|
||||
messages = request.env['mail.group.message'].search(replies_domain, limit=self._replies_per_page)
|
||||
message_count = request.env['mail.group.message'].search_count(replies_domain)
|
||||
|
||||
if not messages:
|
||||
return
|
||||
|
||||
values = {
|
||||
'group': group,
|
||||
'parent_message': message,
|
||||
'messages': messages,
|
||||
'msg_more_count': message_count - self._replies_per_page,
|
||||
'replies_per_page': self._replies_per_page,
|
||||
}
|
||||
return request.env.ref('mail_group.messages_short')._render(values, engine='ir.qweb')
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# SUBSCRIPTION
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@http.route('/groups/subscription', type='json', auth='public', website=True)
|
||||
def group_subscribe(self, group_id=0, action='subscribe', email=None, token=None, **kw):
|
||||
"""Subscribe the current logged user or the given email address to the mailing list.
|
||||
|
||||
If the user is logged, the action is automatically done.
|
||||
|
||||
But if the user is not logged (public user) an email will be send with a token
|
||||
to confirm the action.
|
||||
|
||||
:param group_id: Id of the group
|
||||
:param action: Action to perform (subscribe or unsubscribe)
|
||||
:param email: Email to add in the member list
|
||||
:param token: An access token to bypass the <mail.group> access rule
|
||||
:return:
|
||||
'added'
|
||||
if the mmeber was added in the mailing list
|
||||
'removed'
|
||||
if the member was removed from the mailing list
|
||||
'email_sent'
|
||||
if we send a confirmation email
|
||||
'is_already_member'
|
||||
if we try to subscribe but we are already member
|
||||
'is_not_member'
|
||||
if we try to unsubscribe but we are not member
|
||||
"""
|
||||
if action not in ('subscribe', 'unsubscribe'):
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
|
||||
group = request.env['mail.group'].browse(int(group_id)).exists()
|
||||
if not group:
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
|
||||
# SUDO to have access to field of the many2one
|
||||
group_sudo = group.sudo()
|
||||
|
||||
if token and token != group_sudo._generate_group_access_token():
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
|
||||
elif not token:
|
||||
# Check that the current user has access to the group
|
||||
group.check_access_rights('read')
|
||||
group.check_access_rule('read')
|
||||
|
||||
partner_id = None
|
||||
if not request.env.user._is_public():
|
||||
partner_id = request.env.user.partner_id.id
|
||||
|
||||
is_member = bool(group_sudo._find_member(email, partner_id))
|
||||
if action == 'subscribe' and is_member:
|
||||
return 'is_already_member'
|
||||
elif action == 'unsubscribe' and not is_member:
|
||||
return 'is_not_member'
|
||||
|
||||
if not request.env.user._is_public():
|
||||
# For logged user, automatically join / leave without sending a confirmation email
|
||||
if action == 'subscribe':
|
||||
group_sudo._join_group(request.env.user.email, partner_id)
|
||||
return 'added'
|
||||
else:
|
||||
group_sudo._leave_group(request.env.user.email, partner_id)
|
||||
return 'removed'
|
||||
else:
|
||||
# For non-logged user, send an email with a token to confirm the action
|
||||
if action == 'subscribe':
|
||||
group_sudo._send_subscribe_confirmation_email(email, action)
|
||||
else:
|
||||
group_sudo._send_unsubscribe_confirmation_email(email, action)
|
||||
return 'email_sent'
|
||||
|
||||
@http.route('/groups/subscribe', type='http', auth='public', website=True)
|
||||
def groups_subscribe_confirm(self, group_id, email, token, action, **kw):
|
||||
"""Confirm the subscribe / unsubscribe action which was sent by email."""
|
||||
if action not in ('subscribe', 'unsubscribe'):
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
|
||||
# Here we can SUDO because the token will be checked
|
||||
group = request.env['mail.group'].browse(int(group_id)).exists().sudo()
|
||||
if not group:
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
|
||||
excepted_token = group._generate_action_token(email, action)
|
||||
|
||||
if token != excepted_token:
|
||||
return request.render('mail_group.invalid_token_subscription')
|
||||
|
||||
partners = request.env['mail.thread'].sudo()._mail_find_partner_from_emails([email])
|
||||
partner_id = partners[0].id if partners else None
|
||||
if action == 'subscribe':
|
||||
group._join_group(email, partner_id)
|
||||
else:
|
||||
group._leave_group(email, partner_id)
|
||||
|
||||
return request.render('mail_group.confirmation_subscription', {'subscribing': action == 'subscribe'})
|
||||
@@ -0,0 +1,16 @@
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="ir_cron_mail_notify_group_moderators" model="ir.cron">
|
||||
<field name="name">Mail List: Notify group moderators</field>
|
||||
<field name="model_id" ref="model_mail_group"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_notify_moderators()</field>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False"/>
|
||||
<field name="priority">1000</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,164 @@
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Group 1 -->
|
||||
<record id="mail_group_1" model="mail.group">
|
||||
<field name="name">Newsletter</field>
|
||||
<field name="alias_name">newsletter</field>
|
||||
<field name="description">Receive news about "My Company"</field>
|
||||
<field name="moderation" eval="True"/>
|
||||
<field name="moderator_ids" eval="[(4, ref('base.user_admin'))]"/>
|
||||
<field name="access_mode">groups</field>
|
||||
<field name="access_group_id" ref="base.group_user"/>
|
||||
</record>
|
||||
<!-- Members of group 1 -->
|
||||
<record id="mail_group_member_1" model="mail.group.member">
|
||||
<field name="partner_id" ref="base.partner_admin"/>
|
||||
<field name="email">admin@yourcompany.example.com</field>
|
||||
<field name="mail_group_id" ref="mail_group_1"/>
|
||||
</record>
|
||||
<record id="mail_group_member_2" model="mail.group.member">
|
||||
<field name="partner_id" ref="base.partner_demo"/>
|
||||
<field name="email">mark.brown23@example.com</field>
|
||||
<field name="mail_group_id" ref="mail_group_1"/>
|
||||
</record>
|
||||
<!-- Attachment of messages -->
|
||||
<record id="ir_attachment_mail_group_message_1" model="ir.attachment">
|
||||
<field name="datas">U3VwZXIgc2VjcmV0IGF0dGFjaG1lbnQ=</field>
|
||||
<field name="name">attachment.txt</field>
|
||||
</record>
|
||||
<record id="ir_attachment_mail_group_message_2" model="ir.attachment">
|
||||
<field name="datas">QnV0IEphdmFzY3JpcHQgc3Vja3M=</field>
|
||||
<field name="name">attachment.txt</field>
|
||||
</record>
|
||||
<!-- Messages of group 1 -->
|
||||
<record id="mail_group_message_1" model="mail.group.message">
|
||||
<field name="subject">Important Announce</field>
|
||||
<field name="body">This weekend, you should all come to our barbecue!</field>
|
||||
<field name="email_from">mark.brown23@example.com</field>
|
||||
<field name="author_id" ref="base.partner_demo"/>
|
||||
<field name="moderation_status">accepted</field>
|
||||
<field name="group_message_parent_id" eval="False"/>
|
||||
<field name="mail_group_id" ref="mail_group_1"/>
|
||||
<field name="attachment_ids" eval="[(4, ref('ir_attachment_mail_group_message_1'))]"/>
|
||||
</record>
|
||||
<record id="mail_group_message_1_1" model="mail.group.message">
|
||||
<field name="subject">Re: Important Announce</field>
|
||||
<field name="body">Will there be vegetarian food?</field>
|
||||
<field name="email_from">admin@yourcompany.example.com</field>
|
||||
<field name="author_id" ref="base.partner_admin"/>
|
||||
<field name="moderation_status">accepted</field>
|
||||
<field name="group_message_parent_id" ref="mail_group_message_1"/>
|
||||
<field name="mail_group_id" ref="mail_group_1"/>
|
||||
</record>
|
||||
<record id="mail_group_message_1_1_1" model="mail.group.message">
|
||||
<field name="subject">Re: Important Announce</field>
|
||||
<field name="body">Yes of course, and for those who are allergic bring your own food</field>
|
||||
<field name="email_from">mark.brown23@example.com</field>
|
||||
<field name="author_id" ref="base.partner_demo"/>
|
||||
<field name="moderation_status">accepted</field>
|
||||
<field name="group_message_parent_id" ref="mail_group_message_1_1"/>
|
||||
<field name="mail_group_id" ref="mail_group_1"/>
|
||||
</record>
|
||||
<record id="mail_group_message_1_2" model="mail.group.message">
|
||||
<field name="subject">Re: Important Announce</field>
|
||||
<field name="body">Can I come with my dog ?</field>
|
||||
<field name="email_from">joel.willis63@example.com</field>
|
||||
<field name="moderation_status">accepted</field>
|
||||
<field name="group_message_parent_id" ref="mail_group_message_1"/>
|
||||
<field name="mail_group_id" ref="mail_group_1"/>
|
||||
</record>
|
||||
<record id="mail_group_message_1_2_1" model="mail.group.message">
|
||||
<field name="subject">Re: Important Announce</field>
|
||||
<field name="body">No animals please.</field>
|
||||
<field name="email_from">admin@yourcompany.example.com</field>
|
||||
<field name="author_id" ref="base.partner_admin"/>
|
||||
<field name="moderation_status">accepted</field>
|
||||
<field name="group_message_parent_id" ref="mail_group_message_1_2"/>
|
||||
<field name="mail_group_id" ref="mail_group_1"/>
|
||||
</record>
|
||||
<record id="mail_group_message_2" model="mail.group.message">
|
||||
<field name="subject">Recruitment</field>
|
||||
<field name="body">We are pleased to announce that we have hired 10 new people this month.</field>
|
||||
<field name="email_from">admin@yourcompany.example.com</field>
|
||||
<field name="author_id" ref="base.partner_admin"/>
|
||||
<field name="moderation_status">accepted</field>
|
||||
<field name="group_message_parent_id" eval="False"/>
|
||||
<field name="mail_group_id" ref="mail_group_1"/>
|
||||
</record>
|
||||
<record id="mail_group_message_3" model="mail.group.message">
|
||||
<field name="subject">Relocation</field>
|
||||
<field name="body">We are moving our office to Brussels. Take back all your remaining stuff.</field>
|
||||
<field name="email_from">admin@yourcompany.example.com</field>
|
||||
<field name="author_id" ref="base.partner_admin"/>
|
||||
<field name="moderation_status">accepted</field>
|
||||
<field name="group_message_parent_id" eval="False"/>
|
||||
<field name="mail_group_id" ref="mail_group_1"/>
|
||||
</record>
|
||||
<record id="mail_group_message_3_1" model="mail.group.message">
|
||||
<field name="subject">Re: Relocation</field>
|
||||
<field name="body">Is there a swimming pool in this new office?</field>
|
||||
<field name="email_from">joel.willis63@example.com</field>
|
||||
<field name="moderation_status">accepted</field>
|
||||
<field name="group_message_parent_id" ref="mail_group_message_3"/>
|
||||
<field name="mail_group_id" ref="mail_group_1"/>
|
||||
</record>
|
||||
<record id="mail_group_message_3_1_1" model="mail.group.message">
|
||||
<field name="subject">Re: Relocation</field>
|
||||
<field name="body">Of course!</field>
|
||||
<field name="email_from">admin@yourcompany.example.com</field>
|
||||
<field name="author_id" ref="base.partner_admin"/>
|
||||
<field name="moderation_status">accepted</field>
|
||||
<field name="group_message_parent_id" ref="mail_group_message_3_1"/>
|
||||
<field name="mail_group_id" ref="mail_group_1"/>
|
||||
</record>
|
||||
<record id="mail_group_message_3_2" model="mail.group.message">
|
||||
<field name="subject">Re: Relocation</field>
|
||||
<field name="body">What is the deadline for taking back my stuff?</field>
|
||||
<field name="email_from">mark.brown23@example.com</field>
|
||||
<field name="author_id" ref="base.partner_demo"/>
|
||||
<field name="moderation_status">accepted</field>
|
||||
<field name="group_message_parent_id" ref="mail_group_message_3"/>
|
||||
<field name="mail_group_id" ref="mail_group_1"/>
|
||||
</record>
|
||||
<record id="mail_group_message_3_2_1" model="mail.group.message">
|
||||
<field name="subject">Re: Relocation</field>
|
||||
<field name="body">In 4 weeks.</field>
|
||||
<field name="email_from">admin@yourcompany.example.com</field>
|
||||
<field name="author_id" ref="base.partner_admin"/>
|
||||
<field name="moderation_status">pending_moderation</field>
|
||||
<field name="group_message_parent_id" ref="mail_group_message_3_2"/>
|
||||
<field name="mail_group_id" ref="mail_group_1"/>
|
||||
</record>
|
||||
<record id="mail_group_message_4" model="mail.group.message">
|
||||
<field name="subject">I really like CSS & HTML</field>
|
||||
<field name="body">
|
||||
<p style="margin:0px; font-size:13px;">Hi,</p><br> <p style="margin:0px; font-size:13px;"> We <u style="font-size:14px">really</u> like <font style="color:rgb(57, 123, 33); font-weight:bolder">colors</font> and <span style="font-weight:bolder"><font style="color:rgb(255, 0, 0)">CSS</font></span>. </p> <p style="margin:0px; font-size:13px;">Do you know you can add </p> <a href="https://example.com" target="_blank" class="btn btn-primary flat btn-sm">link in email</a> ? <ol> <li>Also image</li> <li>List</li> <li>Any HTML code in the end...</li> </ol>
|
||||
|
||||
</field>
|
||||
<field name="email_from">admin@yourcompany.example.com</field>
|
||||
<field name="author_id" ref="base.partner_admin"/>
|
||||
<field name="moderation_status">pending_moderation</field>
|
||||
<field name="group_message_parent_id" eval="False"/>
|
||||
<field name="mail_group_id" ref="mail_group_1"/>
|
||||
<field name="attachment_ids" eval="[(4, ref('ir_attachment_mail_group_message_2'))]"/>
|
||||
</record>
|
||||
<!-- Group 2 -->
|
||||
<record id="mail_group_2" model="mail.group">
|
||||
<field name="name">Public Mailing List</field>
|
||||
<field name="alias_name">public_group</field>
|
||||
<field name="description">Get the patch notes of our amazing product.</field>
|
||||
<field name="moderation" eval="True"/>
|
||||
<field name="moderator_ids" eval="[(4, ref('base.user_admin'))]"/>
|
||||
</record>
|
||||
<!-- Message of Group 2 -->
|
||||
<record id="mail_group_message_5" model="mail.group.message">
|
||||
<field name="subject">Best patch ever</field>
|
||||
<field name="body">In this patch, we have cleaned the CSS!</field>
|
||||
<field name="email_from">mark.brown23@example.com</field>
|
||||
<field name="author_id" ref="base.partner_demo"/>
|
||||
<field name="moderation_status">accepted</field>
|
||||
<field name="group_message_parent_id" eval="False"/>
|
||||
<field name="mail_group_id" ref="mail_group_2"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="mail_group_send_guidelines">
|
||||
<div>
|
||||
<p>Hello <t t-if="member.partner_id" t-esc="member.partner_id.name"/>,</p>
|
||||
<p>Please find below the guidelines of the <t t-esc="group.name"/> mailing list.</p>
|
||||
<p t-esc="group.moderation_guidelines_msg"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="mail_group_footer" name="Mail Group: Footer">
|
||||
<div>
|
||||
<span>_______________________________________________</span>
|
||||
<span>Mailing-List: <t t-esc="group_url"/></span>
|
||||
<span>Post to: <t t-esc="mailto"/></span>
|
||||
<span>Unsubscribe: <t t-esc="unsub_url"/></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="mail_group_notify_moderation">
|
||||
<div>
|
||||
<p>Hello <t t-esc='record.name'/></p>
|
||||
<p>You have messages to moderate, please go for the proceedings.</p><br/><br/>
|
||||
<div style="text-align: center;">
|
||||
<a href="/web#action=mail_group.mail_group_message_action" class="o_default_snippet_text">Moderate Messages</a>
|
||||
</div>
|
||||
<p>Thank you!</p>
|
||||
</div>
|
||||
</template>
|
||||
</data>
|
||||
|
||||
<!-- Confirm subscription email -->
|
||||
<record id="mail_template_list_subscribe" model="mail.template">
|
||||
<field name="name">Mail Group: Mailing list subscription</field>
|
||||
<field name="model_id" ref="mail_group.model_mail_group"/>
|
||||
<field name="subject">Confirm subscription to ${object.name}</field>
|
||||
<field name="body_html" type="html">
|
||||
<div style="margin: 0px; padding: 0px;">
|
||||
Hello,<br/><br/>
|
||||
You have requested to be subscribed to the mailing list <strong>${object.name}</strong>.
|
||||
<br/><br/>
|
||||
To confirm, please visit the following link: <strong t-if="ctx.get('token_url')"><a href="${ctx['token_url']}">${ctx['token_url']}</a></strong>
|
||||
<br/><br/>
|
||||
If this was a mistake or you did not requested this action, please ignore this message.
|
||||
</div>
|
||||
</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
<!-- Confirm unsubscription email -->
|
||||
<record id="mail_template_list_unsubscribe" model="mail.template">
|
||||
<field name="name">Mail Group: Mailing list unsubscription</field>
|
||||
<field name="model_id" ref="mail_group.model_mail_group"/>
|
||||
<field name="subject">Confirm unsubscription to ${object.name}</field>
|
||||
<field name="body_html" type="html">
|
||||
<div style="margin: 0px; padding: 0px;">
|
||||
Hello,<br/><br/>
|
||||
You have requested to be unsubscribed to the mailing list <strong>${object.name}</strong>.
|
||||
<br/><br/>
|
||||
To confirm, please visit the following link: <strong t-if="ctx.get('token_url')"><a href="${ctx['token_url']}">${ctx['token_url']}</a></strong>.
|
||||
<br/><br/>
|
||||
If this was a mistake or you did not requested this action, please ignore this message.
|
||||
</div>
|
||||
</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="group_mail_group_manager" model="res.groups">
|
||||
<field name="name">Mail Group Administrator</field>
|
||||
<field name="category_id" ref="base.module_category_usability"/>
|
||||
</record>
|
||||
<record id="base.group_system" model="res.groups">
|
||||
<field name="implied_ids" eval="[(4, ref('mail_group.group_mail_group_manager'))]"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import mail_group
|
||||
from . import mail_group_member
|
||||
from . import mail_group_message
|
||||
from . import mail_group_moderation
|
||||
@@ -0,0 +1,571 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
|
||||
from ast import literal_eval
|
||||
from datetime import datetime
|
||||
from dateutil import relativedelta
|
||||
from werkzeug import urls
|
||||
from odoo.addons.http_routing.models.ir_http import slug
|
||||
|
||||
from odoo import _, api, fields, models, modules, tools
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import email_normalize, hmac, generate_tracking_message_id
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
GROUP_SEND_BATCH_SIZE = 500
|
||||
|
||||
|
||||
class MailGroup(models.Model):
|
||||
"""This model represents a mailing list.
|
||||
|
||||
Users send emails to an alias to create new group messages or reply to existing
|
||||
group messages. Moderation can be activated on groups. In that case email have to
|
||||
be validated or rejected.
|
||||
"""
|
||||
_name = 'mail.group'
|
||||
_description = 'Mailing List'
|
||||
# TDE CHECK: use blaclist mixin
|
||||
_inherit = ['mail.alias.mixin']
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super(MailGroup, self).default_get(fields)
|
||||
if not res.get('alias_contact') and (not fields or 'alias_contact' in fields):
|
||||
res['alias_contact'] = 'everyone' if res.get('access_mode') == 'public' else 'followers'
|
||||
return res
|
||||
|
||||
def _get_default_image(self):
|
||||
image_path = modules.get_resource_path('mail', 'static/src/img', 'groupdefault.png')
|
||||
with tools.file_open(image_path, 'rb') as fd:
|
||||
return base64.b64encode(fd.read())
|
||||
|
||||
name = fields.Char('Name', required=True)
|
||||
alias_name = fields.Char('Alias Name', copy=False, related='alias_id.alias_name', readonly=False)
|
||||
description = fields.Text('Description')
|
||||
image_128 = fields.Image('Image', max_width=128, max_height=128, default=_get_default_image)
|
||||
# Messages
|
||||
mail_group_message_ids = fields.One2many('mail.group.message', 'mail_group_id', string='Pending Messages')
|
||||
mail_group_message_last_month_count = fields.Integer('Messages Per Month', compute='_compute_mail_group_message_last_month_count')
|
||||
mail_group_message_moderation_count = fields.Integer('Messages Count', help='Messages that need an action', compute='_compute_mail_group_message_moderation_count')
|
||||
# Members
|
||||
member_ids = fields.One2many('mail.group.member', 'mail_group_id', string='Members')
|
||||
member_partner_ids = fields.Many2many('res.partner', string='Partners Member', compute='_compute_member_partner_ids', search='_search_member_partner_ids')
|
||||
member_count = fields.Integer('Members Count', compute='_compute_member_count')
|
||||
# Moderation
|
||||
is_moderator = fields.Boolean(string='Moderator', help='Current user is a moderator of the group', compute='_compute_is_moderator')
|
||||
moderation = fields.Boolean(string='Moderate this group')
|
||||
moderation_rule_count = fields.Integer(string='Moderated emails count', compute='_compute_moderation_rule_count')
|
||||
moderation_rule_ids = fields.One2many('mail.group.moderation', 'mail_group_id', string='Moderated Emails')
|
||||
moderator_ids = fields.Many2many('res.users', 'mail_group_moderator_rel', string='Moderators',
|
||||
domain=lambda self: [('groups_id', 'in', self.env.ref('base.group_user').id)])
|
||||
moderation_notify = fields.Boolean(
|
||||
string='Automatic notification',
|
||||
help='People receive an automatic notification about their message being waiting for moderation.')
|
||||
moderation_notify_msg = fields.Text(string='Notification message')
|
||||
moderation_guidelines = fields.Boolean(
|
||||
string='Send guidelines to new subscribers',
|
||||
help='Newcomers on this moderated group will automatically receive the guidelines.')
|
||||
moderation_guidelines_msg = fields.Text(string='Guidelines')
|
||||
# ACLs
|
||||
access_mode = fields.Selection([
|
||||
('public', 'Everyone'),
|
||||
('members', 'Members only'),
|
||||
('groups', 'Selected group of users'),
|
||||
], string='Privacy', required=True, default='public')
|
||||
access_group_id = fields.Many2one('res.groups', string='Authorized Group',
|
||||
default=lambda self: self.env.ref('base.group_user'))
|
||||
# UI
|
||||
can_manage_members = fields.Boolean('Can Manage', help='Can manage the members', compute='_compute_can_manage_members')
|
||||
|
||||
@api.depends('mail_group_message_ids.create_date', 'mail_group_message_ids.moderation_status')
|
||||
def _compute_mail_group_message_last_month_count(self):
|
||||
month_date = datetime.today() - relativedelta.relativedelta(months=1)
|
||||
messages_data = self.env['mail.group.message'].read_group([
|
||||
('create_date', '>=', fields.Datetime.to_string(month_date)),
|
||||
('moderation_status', '=', 'accepted'),
|
||||
], ['mail_group_id'], ['mail_group_id'])
|
||||
|
||||
# { mail_discusison_id: number_of_mail_group_message_last_month_count }
|
||||
messages_data = {
|
||||
message['mail_group_id'][0]: message['mail_group_id_count']
|
||||
for message in messages_data
|
||||
}
|
||||
|
||||
for group in self:
|
||||
group.mail_group_message_last_month_count = messages_data.get(group.id, 0)
|
||||
|
||||
@api.depends('mail_group_message_ids.moderation_status')
|
||||
def _compute_mail_group_message_moderation_count(self):
|
||||
results = self.env['mail.group.message'].read_group(
|
||||
[('moderation_status', '=', 'pending_moderation')],
|
||||
['mail_group_id'],
|
||||
['mail_group_id'],
|
||||
)
|
||||
result_per_group = {
|
||||
result['mail_group_id'][0]: result['mail_group_id_count']
|
||||
for result in results
|
||||
}
|
||||
|
||||
for group in self:
|
||||
group.mail_group_message_moderation_count = result_per_group.get(group.id)
|
||||
|
||||
@api.depends('member_ids')
|
||||
def _compute_member_count(self):
|
||||
for group in self:
|
||||
group.member_count = len(group.member_ids)
|
||||
|
||||
@api.depends('member_ids')
|
||||
def _compute_member_partner_ids(self):
|
||||
for group in self:
|
||||
group.member_partner_ids = group.member_ids.partner_id
|
||||
|
||||
def _search_member_partner_ids(self, operator, operand):
|
||||
return [(
|
||||
'member_ids',
|
||||
'in',
|
||||
self.env['mail.group.member'].sudo()._search([
|
||||
('partner_id', operator, operand)
|
||||
])
|
||||
)]
|
||||
|
||||
@api.depends('moderator_ids')
|
||||
@api.depends_context('uid')
|
||||
def _compute_is_moderator(self):
|
||||
for group in self:
|
||||
group.is_moderator = self.env.user.id in group.moderator_ids.ids
|
||||
|
||||
@api.depends('moderation_rule_ids')
|
||||
def _compute_moderation_rule_count(self):
|
||||
for group in self:
|
||||
group.moderation_rule_count = len(group.moderation_rule_ids)
|
||||
|
||||
@api.depends('is_moderator')
|
||||
@api.depends_context('uid')
|
||||
def _compute_can_manage_members(self):
|
||||
is_admin = self.env.user.has_group('mail_group.group_mail_group_manager')
|
||||
for group in self:
|
||||
group.can_manage_members = is_admin or group.is_moderator
|
||||
|
||||
@api.onchange('access_mode')
|
||||
def _onchange_access_mode(self):
|
||||
if self.access_mode == 'public':
|
||||
self.alias_contact = 'everyone'
|
||||
else:
|
||||
self.alias_contact = 'followers'
|
||||
|
||||
@api.onchange('moderation')
|
||||
def _onchange_moderation(self):
|
||||
if self.moderation and self.env.user not in self.moderator_ids:
|
||||
self.moderator_ids |= self.env.user
|
||||
|
||||
# CONSTRAINTS
|
||||
|
||||
@api.constrains('moderator_ids')
|
||||
def _check_moderator_email(self):
|
||||
if any(not moderator.email for group in self for moderator in group.moderator_ids):
|
||||
raise ValidationError(_('Moderators must have an email address.'))
|
||||
|
||||
@api.constrains('moderation_notify', 'moderation_notify_msg')
|
||||
def _check_moderation_notify(self):
|
||||
if any(group.moderation_notify and not group.moderation_notify_msg for group in self):
|
||||
raise ValidationError(_('The notification message is missing.'))
|
||||
|
||||
@api.constrains('moderation_guidelines', 'moderation_guidelines_msg')
|
||||
def _check_moderation_guidelines(self):
|
||||
if any(group.moderation_guidelines and not group.moderation_guidelines_msg for group in self):
|
||||
raise ValidationError(_('The guidelines description is missing.'))
|
||||
|
||||
@api.constrains('moderator_ids', 'moderation')
|
||||
def _check_moderator_existence(self):
|
||||
if any(not group.moderator_ids for group in self if group.moderation):
|
||||
raise ValidationError(_('Moderated group must have moderators.'))
|
||||
|
||||
@api.constrains('access_mode', 'access_group_id')
|
||||
def _check_access_mode(self):
|
||||
if any(group.access_mode == 'groups' and not group.access_group_id for group in self):
|
||||
raise ValidationError(_('The "Authorized Group" is missing.'))
|
||||
|
||||
def _alias_get_creation_values(self):
|
||||
"""Return the default values for the automatically created alias."""
|
||||
values = super(MailGroup, self)._alias_get_creation_values()
|
||||
values['alias_model_id'] = self.env['ir.model']._get('mail.group').id
|
||||
values['alias_force_thread_id'] = self.id
|
||||
values['alias_defaults'] = literal_eval(self.alias_defaults or '{}')
|
||||
return values
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# MAILING
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _alias_get_error_message(self, message, message_dict, alias):
|
||||
self.ensure_one()
|
||||
|
||||
if alias.alias_contact == 'followers':
|
||||
# Members only
|
||||
author_id = message_dict.get('author_id', None)
|
||||
email_from = message_dict.get('email_from')
|
||||
if not self._find_member(email_from, author_id):
|
||||
return _('Only members can send email to the mailing list.')
|
||||
# Skip the verification because the partner is in the member list
|
||||
return
|
||||
|
||||
return super(MailGroup, self)._alias_get_error_message(message, message_dict, alias)
|
||||
|
||||
@api.model
|
||||
def message_new(self, msg_dict, custom_values=None):
|
||||
"""Add the method to make the mail gateway flow work with this model."""
|
||||
return
|
||||
|
||||
@api.model
|
||||
def message_update(self, msg_dict, update_vals=None):
|
||||
"""Add the method to make the mail gateway flow work with this model."""
|
||||
return
|
||||
|
||||
@api.returns('mail.message', lambda value: value.id)
|
||||
def message_post(self, body='', subject=None, email_from=None, author_id=None, **kwargs):
|
||||
""" Custom posting process. This model does not inherit from ``mail.thread``
|
||||
but uses mailgateway so few methods should be defined.
|
||||
|
||||
This custom posting process works as follow
|
||||
|
||||
* create a ``mail.message`` based on incoming email;
|
||||
* create linked ``mail.group.message`` that encapsulates message in a
|
||||
format used in mail groups;
|
||||
* apply moderation rules;
|
||||
|
||||
:return message: newly-created mail.message
|
||||
"""
|
||||
self.ensure_one()
|
||||
# First create the <mail.message>
|
||||
Mailthread = self.env['mail.thread']
|
||||
values = dict((key, val) for key, val in kwargs.items() if key in self.env['mail.message']._fields)
|
||||
author_id, email_from = Mailthread._message_compute_author(author_id, email_from, raise_exception=True)
|
||||
values.update({
|
||||
'author_id': author_id,
|
||||
'body': body or '',
|
||||
'email_from': email_from,
|
||||
'model': self._name,
|
||||
'partner_ids': [],
|
||||
'reply_to': self.env['mail.message']._get_reply_to(values),
|
||||
'res_id': self.id,
|
||||
'subject': subject,
|
||||
})
|
||||
# ensure message ID so that replies go to the right thread
|
||||
if not values.get('message_id'):
|
||||
values['message_id'] = generate_tracking_message_id('%s-mail.group' % self.id)
|
||||
|
||||
attachments = kwargs.get('attachments') or []
|
||||
attachment_ids = kwargs.get('attachment_ids') or []
|
||||
attachement_values = Mailthread._message_post_process_attachments(attachments, attachment_ids, values)
|
||||
values.update(attachement_values)
|
||||
|
||||
mail_message = Mailthread._message_create(values)
|
||||
|
||||
# Find the <mail.group.message> parent
|
||||
group_message_parent_id = False
|
||||
if mail_message.parent_id:
|
||||
group_message_parent = self.env['mail.group.message'].search(
|
||||
[('mail_message_id', '=', mail_message.parent_id.id)])
|
||||
group_message_parent_id = group_message_parent.id if group_message_parent else False
|
||||
|
||||
moderation_status = 'pending_moderation' if self.moderation else 'accepted'
|
||||
|
||||
# Create the group message associated
|
||||
group_message = self.env['mail.group.message'].create({
|
||||
'mail_group_id': self.id,
|
||||
'mail_message_id': mail_message.id,
|
||||
'moderation_status': moderation_status,
|
||||
'group_message_parent_id': group_message_parent_id,
|
||||
})
|
||||
|
||||
# Check the moderation rule to determine if we should accept or reject the email
|
||||
email_normalized = email_normalize(email_from)
|
||||
moderation_rule = self.env['mail.group.moderation'].search([
|
||||
('mail_group_id', '=', self.id),
|
||||
('email', '=', email_normalized),
|
||||
], limit=1)
|
||||
|
||||
if not self.moderation:
|
||||
self._notify_members(group_message)
|
||||
|
||||
elif moderation_rule and moderation_rule.status == 'allow':
|
||||
group_message.action_moderate_accept()
|
||||
|
||||
elif moderation_rule and moderation_rule.status == 'ban':
|
||||
group_message.action_moderate_reject()
|
||||
|
||||
elif self.moderation_notify:
|
||||
self.env['mail.mail'].sudo().create({
|
||||
'author_id': self.env.user.partner_id.id,
|
||||
'auto_delete': True,
|
||||
'body_html': group_message.mail_group_id.moderation_notify_msg,
|
||||
'email_from': self.env.user.company_id.catchall_formatted or self.env.user.company_id.email_formatted,
|
||||
'email_to': email_from,
|
||||
'subject': 'Re: %s' % (subject or ''),
|
||||
'state': 'outgoing'
|
||||
})
|
||||
|
||||
return mail_message
|
||||
|
||||
def action_send_guidelines(self, members=None):
|
||||
""" Send guidelines to given members. """
|
||||
self.ensure_one()
|
||||
|
||||
if not self.env.is_admin() and not self.is_moderator:
|
||||
raise UserError(_('Only an administrator or a moderator can send guidelines to group members.'))
|
||||
|
||||
if not self.moderation_guidelines_msg:
|
||||
raise UserError(_('The guidelines description is empty.'))
|
||||
|
||||
template = self.env.ref('mail_group.mail_group_send_guidelines', raise_if_not_found=False)
|
||||
if not template:
|
||||
raise UserError(_('View "mail_group.mail_group_send_guidelines" was not found. No email has been sent. Please contact an administrator to fix this issue.'))
|
||||
|
||||
banned_emails = self.env['mail.group.moderation'].sudo().search([
|
||||
('status', '=', 'ban'),
|
||||
('mail_group_id', '=', self.id),
|
||||
]).mapped('email')
|
||||
|
||||
if members is None:
|
||||
members = self.member_ids
|
||||
members = members.filtered(lambda member: member.email_normalized not in banned_emails)
|
||||
|
||||
mails_values = []
|
||||
for member in members:
|
||||
company = member.partner_id.company_id or self.env.company
|
||||
mails_values.append({
|
||||
'author_id': self.env.user.partner_id.id,
|
||||
'auto_delete': True,
|
||||
'body_html': template._render({'group': self, 'member': member}, engine='ir.qweb', minimal_qcontext=True),
|
||||
'email_from': company.catchall_formatted or company.email_formatted,
|
||||
'email_to': member.email,
|
||||
'model': 'mail.group',
|
||||
'res_id': self.id,
|
||||
'subject': _('Guidelines of group %s', self.name),
|
||||
})
|
||||
|
||||
mails = self.env['mail.mail'].sudo().create(mails_values)
|
||||
|
||||
_logger.info('Send guidelines to %i members', len(mails))
|
||||
|
||||
def _notify_members(self, message):
|
||||
"""Send the given message to all members of the mail group (except the author)."""
|
||||
self.ensure_one()
|
||||
|
||||
if message.mail_group_id != self:
|
||||
raise UserError(_('The group of the message do not match.'))
|
||||
|
||||
if not message.mail_message_id.reply_to:
|
||||
_logger.error('The alias or the catchall domain is missing, group might not work properly.')
|
||||
|
||||
base_url = self.get_base_url()
|
||||
body = self.env['mail.render.mixin']._replace_local_links(message.body)
|
||||
access_token = self._generate_group_access_token()
|
||||
mail_values = []
|
||||
for batch_members in tools.split_every(GROUP_SEND_BATCH_SIZE, self.member_ids):
|
||||
for member in batch_members:
|
||||
if member.email_normalized == message.email_from_normalized:
|
||||
# Do not send the email to his author
|
||||
continue
|
||||
|
||||
# SMTP headers related to the subscription
|
||||
email_url_encoded = urls.url_quote(member.email_normalized)
|
||||
headers = {
|
||||
'List-Archive': f'<{base_url}/groups/{slug(self)}>',
|
||||
'List-Subscribe': f'<{base_url}/groups?email={email_url_encoded}>',
|
||||
'List-Unsubscribe': f'<{base_url}/groups?unsubscribe&email={email_url_encoded}>',
|
||||
}
|
||||
|
||||
# Add the footer (member specific) in the body
|
||||
template_values = {
|
||||
'mailto': f'{self.alias_name}@{self.alias_domain}',
|
||||
'group_url': f'{base_url}/groups/{slug(self)}',
|
||||
'unsub_url': f'{base_url}/groups?unsubscribe&group_id={self.id}&token={access_token}'
|
||||
}
|
||||
template = self.env.ref('mail_group.mail_group_footer')
|
||||
footer = template._render(template_values, engine='ir.qweb', minimal_qcontext=True)
|
||||
member_body = tools.append_content_to_html(body, footer)
|
||||
|
||||
mail_values.append({
|
||||
'auto_delete': True,
|
||||
'attachment_ids': message.attachment_ids.ids,
|
||||
'body_html': member_body,
|
||||
'email_from': message.email_from,
|
||||
'email_to': member.email,
|
||||
'headers': json.dumps(headers),
|
||||
'mail_message_id': message.mail_message_id.id,
|
||||
'message_id': message.mail_message_id.message_id,
|
||||
'model': 'mail.group',
|
||||
'reply_to': message.mail_message_id.reply_to,
|
||||
'res_id': self.id,
|
||||
'subject': message.subject,
|
||||
})
|
||||
|
||||
if mail_values:
|
||||
self.env['mail.mail'].sudo().create(mail_values)
|
||||
|
||||
@api.model
|
||||
def _cron_notify_moderators(self):
|
||||
moderated_groups = self.env['mail.group'].search([('moderation', '=', True)])
|
||||
return moderated_groups._notify_moderators()
|
||||
|
||||
def _notify_moderators(self):
|
||||
"""Push a notification (Inbox / Email) to the moderators whose an action is waiting."""
|
||||
template = self.env.ref('mail_group.mail_group_notify_moderation', raise_if_not_found=False)
|
||||
if not template:
|
||||
_logger.warning('Template "mail_group.mail_group_notify_moderation" was not found. Cannot send reminder notifications.')
|
||||
return
|
||||
|
||||
results = self.env['mail.group.message'].read_group(
|
||||
[('moderation_status', '=', 'pending_moderation'),
|
||||
('mail_group_id', 'in', self.ids)
|
||||
],
|
||||
['mail_group_id'],
|
||||
['mail_group_id'],
|
||||
)
|
||||
groups = self.browse([result['mail_group_id'][0] for result in results])
|
||||
moderators_to_notify = groups.mapped('moderator_ids')
|
||||
|
||||
MailThread = self.env['mail.thread'].with_context(mail_notify_author=True)
|
||||
for moderator in moderators_to_notify:
|
||||
body = template._render({'record': moderator.partner_id}, engine='ir.qweb', minimal_qcontext=True)
|
||||
email_from = moderator.company_id.catchall_formatted or moderator.company_id.email_formatted
|
||||
MailThread.message_notify(
|
||||
partner_ids=moderator.partner_id.ids,
|
||||
subject=_('Messages are pending moderation'), body=body,
|
||||
email_from=email_from,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# MEMBERSHIP
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _join_group(self, email, partner_id=None):
|
||||
self.ensure_one()
|
||||
|
||||
if partner_id:
|
||||
partner = self.env['res.partner'].browse(partner_id).exists()
|
||||
email = partner.email
|
||||
|
||||
existing_member = self._find_member(email, partner_id)
|
||||
if existing_member:
|
||||
raise UserError(_('The email "%s" is already registered in this group.', email))
|
||||
|
||||
member = self.env['mail.group.member'].create({
|
||||
'partner_id': partner_id,
|
||||
'email': email,
|
||||
'mail_group_id': self.id,
|
||||
})
|
||||
|
||||
if self.moderation_guidelines:
|
||||
# Automatically send the guidelines to the new member
|
||||
self.action_send_guidelines(member)
|
||||
|
||||
def _leave_group(self, email, partner_id=None):
|
||||
self.ensure_one()
|
||||
member = self._find_member(email, partner_id)
|
||||
if member:
|
||||
member.unlink()
|
||||
|
||||
def _send_subscribe_confirmation_email(self, email):
|
||||
"""Send an email to the given address to subscribe / unsubscribe to the mailing list."""
|
||||
self.ensure_one()
|
||||
confirm_action_url = self._generate_action_url(email, 'subscribe')
|
||||
|
||||
template = self.env.ref('mail_group.mail_template_list_subscribe')
|
||||
template.with_context(token_url=confirm_action_url).send_mail(
|
||||
self.id,
|
||||
force_send=True,
|
||||
email_values={
|
||||
'author_id': self.create_uid.partner_id.id,
|
||||
'auto_delete': True,
|
||||
'email_from': self.env.company.email_formatted,
|
||||
'email_to': email,
|
||||
'message_type': 'user_notification',
|
||||
},
|
||||
notif_layout='mail.mail_notification_light',
|
||||
)
|
||||
_logger.info('Subscription email sent to %s.', email)
|
||||
|
||||
def _send_unsubscribe_confirmation_email(self, email, action):
|
||||
"""Send an email to the given address to subscribe / unsubscribe to the mailing list."""
|
||||
self.ensure_one()
|
||||
confirm_action_url = self._generate_action_url(email, 'unsubscribe')
|
||||
|
||||
template = self.env.ref('mail_group.mail_template_list_unsubscribe')
|
||||
template.with_context(token_url=confirm_action_url).send_mail(
|
||||
self.id,
|
||||
force_send=True,
|
||||
email_values={
|
||||
'author_id': self.create_uid.partner_id.id,
|
||||
'auto_delete': True,
|
||||
'email_from': self.env.company.email_formatted,
|
||||
'email_to': email,
|
||||
'message_type': 'user_notification',
|
||||
},
|
||||
notif_layout='mail.mail_notification_light',
|
||||
)
|
||||
_logger.info('Unsubscription email sent to %s.', email)
|
||||
|
||||
def _generate_action_url(self, email, action):
|
||||
"""Generate the confirmation URL to subscribe / unsubscribe from the mailing list."""
|
||||
if action not in ['subscribe', 'unsubscribe']:
|
||||
raise ValueError(_('Invalid action for URL generation (%s)', action))
|
||||
self.ensure_one()
|
||||
|
||||
confirm_action_url = '/groups/subscribe?%s' % urls.url_encode({
|
||||
'group_id': self.id,
|
||||
'email': email,
|
||||
'token': self._generate_action_token(email, action),
|
||||
'action': action,
|
||||
})
|
||||
base_url = self.get_base_url()
|
||||
confirm_action_url = urls.url_join(base_url, confirm_action_url)
|
||||
return confirm_action_url
|
||||
|
||||
def _generate_action_token(self, email, action):
|
||||
"""Generate an action token to be able to subscribe / unsubscribe from the mailing list."""
|
||||
if action not in ['subscribe', 'unsubscribe']:
|
||||
raise ValueError(_('Invalid action for URL generation (%s)', action))
|
||||
self.ensure_one()
|
||||
|
||||
email_normalized = email_normalize(email)
|
||||
if not email_normalized:
|
||||
raise UserError(_('Email %s is invalid', email))
|
||||
|
||||
data = (self.id, email_normalized, action)
|
||||
return hmac(self.env(su=True), 'mail_group-email-subscription', data)
|
||||
|
||||
def _generate_group_access_token(self):
|
||||
"""Generate an action token to be able to subscribe / unsubscribe from the mailing list."""
|
||||
self.ensure_one()
|
||||
return hmac(self.env(su=True), 'mail_group-access-token-portal', self.id)
|
||||
|
||||
def _find_member(self, email, partner_id=None):
|
||||
"""Return the <mail.group.member> corresponding to the given email address."""
|
||||
self.ensure_one()
|
||||
|
||||
result = self._find_members(email, partner_id)
|
||||
return result.get(self.id)
|
||||
|
||||
def _find_members(self, email, partner_id):
|
||||
"""Get all the members record corresponding to the email / partner_id.
|
||||
|
||||
Can be called in batch and return a dictionary
|
||||
{'group_id': <mail.group.member>}
|
||||
"""
|
||||
domain = [('email_normalized', '=', email_normalize(email))]
|
||||
if partner_id:
|
||||
domain = expression.OR([domain, [('partner_id', '=', partner_id)]])
|
||||
domain = expression.AND([domain, [('mail_group_id', 'in', self.ids)]])
|
||||
members_data = self.env['mail.group.member'].sudo().search(domain)
|
||||
return {
|
||||
member.mail_group_id.id: member
|
||||
for member in members_data
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools import email_normalize
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MailGroupMember(models.Model):
|
||||
"""Models a group member that can be either an email address either a full partner."""
|
||||
_name = 'mail.group.member'
|
||||
_description = 'Mailing List Member'
|
||||
_rec_name = 'email'
|
||||
|
||||
email = fields.Char(string='Email', compute='_compute_email', readonly=False, store=True)
|
||||
email_normalized = fields.Char(
|
||||
string='Normalized Email', compute='_compute_email_normalized',
|
||||
index=True, store=True)
|
||||
mail_group_id = fields.Many2one('mail.group', string='Group', required=True, ondelete='cascade')
|
||||
partner_id = fields.Many2one('res.partner', 'Partner', ondelete='cascade')
|
||||
|
||||
_sql_constraints = [(
|
||||
'unique_partner',
|
||||
'UNIQUE(partner_id, mail_group_id)',
|
||||
'This partner is already subscribed to the group',
|
||||
)]
|
||||
|
||||
@api.depends('partner_id.email')
|
||||
def _compute_email(self):
|
||||
for member in self:
|
||||
if member.partner_id:
|
||||
member.email = member.partner_id.email
|
||||
elif not member.email:
|
||||
member.email = False
|
||||
|
||||
@api.depends('email')
|
||||
def _compute_email_normalized(self):
|
||||
for moderation in self:
|
||||
moderation.email_normalized = email_normalize(moderation.email)
|
||||
@@ -0,0 +1,227 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import _, api, fields, models, tools
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import email_normalize
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MailGroupMessage(models.Model):
|
||||
"""Emails belonging to a discussion group.
|
||||
|
||||
Those are build on <mail.message> with additional information related to specific
|
||||
features of <mail.group> like better parent / children management and moderation.
|
||||
"""
|
||||
_name = 'mail.group.message'
|
||||
_description = 'Mailing List Message'
|
||||
_rec_name = 'subject'
|
||||
_order = 'create_date DESC'
|
||||
|
||||
# <mail.message> fields, can not be done with inherits because it will impact
|
||||
# the performance of the <mail.message> model (different cache, so the ORM will need
|
||||
# to do one more SQL query to be able to update the <mail.group.message> cache)
|
||||
attachment_ids = fields.Many2many(related='mail_message_id.attachment_ids', readonly=False)
|
||||
author_id = fields.Many2one(related='mail_message_id.author_id', readonly=False)
|
||||
email_from = fields.Char(related='mail_message_id.email_from', readonly=False)
|
||||
email_from_normalized = fields.Char('Normalized From', compute='_compute_email_from_normalized', store=True)
|
||||
body = fields.Html(related='mail_message_id.body', readonly=False)
|
||||
subject = fields.Char(related='mail_message_id.subject', readonly=False)
|
||||
# thread
|
||||
mail_group_id = fields.Many2one(
|
||||
'mail.group', string='Group',
|
||||
required=True, ondelete='cascade')
|
||||
mail_message_id = fields.Many2one('mail.message', 'Mail Message', required=True, ondelete='cascade', index=True, copy=False)
|
||||
# Parent and children
|
||||
group_message_parent_id = fields.Many2one(
|
||||
'mail.group.message', string='Parent', store=True,
|
||||
inverse='_inverse_group_message_parent_id')
|
||||
group_message_child_ids = fields.One2many('mail.group.message', 'group_message_parent_id', string='Childs')
|
||||
# Moderation
|
||||
moderation_status = fields.Selection(
|
||||
[('pending_moderation', 'Pending Moderation'),
|
||||
('accepted', 'Accepted'),
|
||||
('rejected', 'Rejected')],
|
||||
string='Status', index=True, copy=False,
|
||||
required=True, default='pending_moderation')
|
||||
moderator_id = fields.Many2one('res.users', string='Moderated By')
|
||||
|
||||
@api.depends('email_from')
|
||||
def _compute_email_from_normalized(self):
|
||||
for message in self:
|
||||
message.email_from_normalized = email_normalize(message.email_from)
|
||||
|
||||
def _inverse_group_message_parent_id(self):
|
||||
# TDE CHECK: check to remove
|
||||
for message in self:
|
||||
message.mail_message_id.parent_id = message.group_message_parent_id.mail_message_id
|
||||
|
||||
@api.constrains('mail_message_id')
|
||||
def _constrains_mail_message_id(self):
|
||||
for message in self:
|
||||
if message.mail_message_id.model != 'mail.group':
|
||||
raise AccessError(_(
|
||||
'Group message can only be linked to mail group. Current model is %s.',
|
||||
message.mail_message_id.model,
|
||||
))
|
||||
if message.mail_message_id.res_id != message.mail_group_id.id:
|
||||
raise AccessError(_('The record of the message should be the group.'))
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, values_list):
|
||||
for vals in values_list:
|
||||
if not vals.get('mail_message_id'):
|
||||
vals.update({
|
||||
'res_id': vals.get('mail_group_id'),
|
||||
'model': 'mail.group',
|
||||
})
|
||||
vals['mail_message_id'] = self.env['mail.message'].sudo().create({
|
||||
field: vals.pop(field)
|
||||
for field in self.env['mail.message']._fields
|
||||
if field in vals
|
||||
}).id
|
||||
return super(MailGroupMessage, self).create(values_list)
|
||||
|
||||
def copy(self, default=None):
|
||||
default = dict(default or {})
|
||||
default['mail_message_id'] = self.mail_message_id.copy().id
|
||||
return super(MailGroupMessage, self).copy(default)
|
||||
|
||||
# --------------------------------------------------
|
||||
# MODERATION API
|
||||
# --------------------------------------------------
|
||||
|
||||
def action_moderate_accept(self):
|
||||
"""Accept the incoming email.
|
||||
|
||||
Will send the incoming email to all members of the group.
|
||||
"""
|
||||
self._assert_moderable()
|
||||
self.write({
|
||||
'moderation_status': 'accepted',
|
||||
'moderator_id': self.env.uid,
|
||||
})
|
||||
|
||||
# Send the email to the members of the group
|
||||
for message in self:
|
||||
message.mail_group_id._notify_members(message)
|
||||
|
||||
def action_moderate_reject_with_comment(self, reject_subject, reject_comment):
|
||||
self._assert_moderable()
|
||||
if reject_subject or reject_comment:
|
||||
self._moderate_send_reject_email(reject_subject, reject_comment)
|
||||
self.action_moderate_reject()
|
||||
|
||||
def action_moderate_reject(self):
|
||||
self._assert_moderable()
|
||||
self.write({
|
||||
'moderation_status': 'rejected',
|
||||
'moderator_id': self.env.uid,
|
||||
})
|
||||
|
||||
def action_allow(self):
|
||||
self._assert_moderable()
|
||||
self._create_moderation_rule('allow')
|
||||
|
||||
# Accept all emails of the same authors
|
||||
same_author = self._get_pending_same_author_same_group()
|
||||
same_author.action_moderate_accept()
|
||||
|
||||
def action_ban(self):
|
||||
self._assert_moderable()
|
||||
self._create_moderation_rule('ban')
|
||||
|
||||
# Reject all emails of the same author
|
||||
same_author = self._get_pending_same_author_same_group()
|
||||
same_author.action_moderate_reject()
|
||||
|
||||
def _get_pending_same_author_same_group(self):
|
||||
"""Return the pending messages of the same authors in the same groups."""
|
||||
return self.search(
|
||||
expression.AND([
|
||||
expression.OR([
|
||||
[
|
||||
('mail_group_id', '=', message.mail_group_id.id),
|
||||
('email_from_normalized', '=', message.email_from_normalized),
|
||||
] for message in self
|
||||
]),
|
||||
[('moderation_status', '=', 'pending_moderation')],
|
||||
])
|
||||
)
|
||||
|
||||
def _create_moderation_rule(self, status):
|
||||
"""Create a moderation rule <mail.group.moderation> with the given status.
|
||||
|
||||
Update existing moderation rule for the same email address if found,
|
||||
otherwise create a new rule.
|
||||
"""
|
||||
if status not in ('ban', 'allow'):
|
||||
raise ValueError(_('Wrong status (%s)', status))
|
||||
|
||||
for message in self:
|
||||
if not email_normalize(message.email_from):
|
||||
raise UserError(_('The email "%s" is not valid.', message.email_from))
|
||||
|
||||
existing_moderation = self.env['mail.group.moderation'].search(
|
||||
expression.OR([
|
||||
[
|
||||
('email', '=', email_normalize(message.email_from)),
|
||||
('mail_group_id', '=', message.mail_group_id.id)
|
||||
] for message in self
|
||||
])
|
||||
)
|
||||
existing_moderation.status = status
|
||||
|
||||
# Add the value in a set to create only 1 moderation rule per (email_normalized, group)
|
||||
moderation_to_create = {
|
||||
(email_normalize(message.email_from), message.mail_group_id.id)
|
||||
for message in self
|
||||
if email_normalize(message.email_from) not in existing_moderation.mapped('email')
|
||||
}
|
||||
|
||||
self.env['mail.group.moderation'].create([
|
||||
{
|
||||
'email': email,
|
||||
'mail_group_id': mail_group_id,
|
||||
'status': status,
|
||||
} for email, mail_group_id in moderation_to_create])
|
||||
|
||||
def _assert_moderable(self):
|
||||
"""Raise an error if one of the current message can not be moderated.
|
||||
|
||||
A <mail.group.message> can only be moderated
|
||||
if it's moderation status is "pending_moderation".
|
||||
"""
|
||||
non_moderable_messages = self.filtered_domain([
|
||||
('moderation_status', '!=', 'pending_moderation'),
|
||||
])
|
||||
if non_moderable_messages:
|
||||
if len(self) == 1:
|
||||
raise UserError(_('This message can not be moderated'))
|
||||
raise UserError(_(
|
||||
'Those messages can not be moderated: %s.',
|
||||
', '.join(non_moderable_messages.mapped('subject')),
|
||||
))
|
||||
|
||||
def _moderate_send_reject_email(self, subject, comment):
|
||||
for message in self:
|
||||
if not message.email_from:
|
||||
continue
|
||||
|
||||
# TDE CHECK: markup
|
||||
body_html = tools.append_content_to_html('<div>%s</div>' % tools.ustr(comment), message.body, plaintext=False)
|
||||
body_html = self.env['mail.render.mixin']._replace_local_links(body_html)
|
||||
self.env['mail.mail'].sudo().create({
|
||||
'author_id': self.env.user.partner_id.id,
|
||||
'auto_delete': True,
|
||||
'body_html': body_html,
|
||||
'email_from': self.env.user.email_formatted or self.env.company.catchall_formatted,
|
||||
'email_to': message.email_from,
|
||||
'references': message.mail_message_id.message_id,
|
||||
'subject': subject,
|
||||
'state': 'outgoing',
|
||||
})
|
||||
@@ -0,0 +1,41 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, api, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import email_normalize
|
||||
|
||||
|
||||
class MailGroupModeration(models.Model):
|
||||
"""Represent the moderation rules for an email address in a group."""
|
||||
_name = 'mail.group.moderation'
|
||||
_description = 'Mailing List black/white list'
|
||||
|
||||
email = fields.Char(string='Email', required=True)
|
||||
status = fields.Selection(
|
||||
[('allow', 'Always Allow'), ('ban', 'Permanent Ban')],
|
||||
string='Status', required=True, default='ban')
|
||||
mail_group_id = fields.Many2one('mail.group', string='Group', required=True, ondelete='cascade')
|
||||
|
||||
_sql_constraints = [(
|
||||
'mail_group_email_uniq',
|
||||
'UNIQUE(mail_group_id, email)',
|
||||
'You can create only one rule for a given email address in a group.',
|
||||
)]
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for values in vals_list:
|
||||
email_normalized = email_normalize(values.get('email'))
|
||||
if not email_normalized:
|
||||
raise UserError(_('Invalid email address %r', values.get('email')))
|
||||
values['email'] = email_normalized
|
||||
return super(MailGroupModeration, self).create(vals_list)
|
||||
|
||||
def write(self, values):
|
||||
if 'email' in values:
|
||||
email_normalized = email_normalize(values['email'])
|
||||
if not email_normalized:
|
||||
raise UserError(_('Invalid email address %r', values.get('email')))
|
||||
values['email'] = email_normalized
|
||||
return super(MailGroupModeration, self).write(values)
|
||||
@@ -0,0 +1,8 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_mail_group_all,access_mail_group_all,model_mail_group,,1,0,0,0
|
||||
access_mail_group_user,access_mail_group_user,model_mail_group,base.group_user,1,1,1,1
|
||||
access_mail_group_member,access_mail_group_member,model_mail_group_member,base.group_user,1,1,1,1
|
||||
access_mail_group_message_all,access_mail_group_message_all,model_mail_group_message,,1,0,0,0
|
||||
access_mail_group_message_user,access_mail_group_message_user,model_mail_group_message,base.group_user,1,1,1,1
|
||||
access_mail_group_moderation,access_mail_group_moderation,model_mail_group_moderation,base.group_user,1,1,1,1
|
||||
access_mail_group_message_reject,access_mail_group_message_reject,model_mail_group_message_reject,base.group_user,1,1,1,1
|
||||
|
@@ -0,0 +1,138 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="mail_group_rule_all" model="ir.rule">
|
||||
<field name="name">Mail Group: Access only public and joined groups</field>
|
||||
<field name="model_id" ref="model_mail_group"/>
|
||||
<field name="domain_force">[
|
||||
'|',
|
||||
'|',
|
||||
'|',
|
||||
('moderator_ids', 'in', user.id),
|
||||
('access_mode', '=', 'public'),
|
||||
'&',
|
||||
('access_mode', '=', 'groups'),
|
||||
('access_group_id', 'in', [g.id for g in user.groups_id]),
|
||||
'&',
|
||||
('access_mode', '=', 'members'),
|
||||
('member_partner_ids', 'in', [user.partner_id.id]),
|
||||
]
|
||||
</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_user')), (4, ref('base.group_portal')), (4, ref('base.group_public'))]"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
</record>
|
||||
<record id="mail_group_rule_administrator" model="ir.rule">
|
||||
<field name="name">Mail Group: Administrator have access to all mail group</field>
|
||||
<field name="model_id" ref="model_mail_group"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4, ref('mail_group.group_mail_group_manager'))]"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
</record>
|
||||
<record id="mail_group_message_rule_public" model="ir.rule">
|
||||
<field name="name">Mail Group Message: Only accepted message are accessible</field>
|
||||
<field name="model_id" ref="model_mail_group_message"/>
|
||||
<field name="domain_force">[
|
||||
'&',
|
||||
('moderation_status', '=', 'accepted'),
|
||||
'|',
|
||||
'|',
|
||||
'|',
|
||||
('mail_group_id.moderator_ids', 'in', user.id),
|
||||
('mail_group_id.access_mode', '=', 'public'),
|
||||
'&',
|
||||
('mail_group_id.access_mode', '=', 'groups'),
|
||||
('mail_group_id.access_group_id', 'in', [g.id for g in user.groups_id]),
|
||||
'&',
|
||||
('mail_group_id.access_mode', '=', 'members'),
|
||||
('mail_group_id.member_partner_ids', 'in', [user.partner_id.id]),
|
||||
]
|
||||
</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_public')), (4, ref('base.group_portal'))]"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
</record>
|
||||
<record id="mail_group_message_rule_user" model="ir.rule">
|
||||
<field name="name">Mail Group Message: Non-accepted messages are accessible only by moderators</field>
|
||||
<field name="model_id" ref="model_mail_group_message"/>
|
||||
<field name="domain_force">[
|
||||
'&',
|
||||
'|',
|
||||
('moderation_status', '=', 'accepted'),
|
||||
('mail_group_id.moderator_ids', 'in', user.id),
|
||||
'|',
|
||||
'|',
|
||||
'|',
|
||||
('mail_group_id.moderator_ids', 'in', user.id),
|
||||
('mail_group_id.access_mode', '=', 'public'),
|
||||
'&',
|
||||
('mail_group_id.access_mode', '=', 'groups'),
|
||||
('mail_group_id.access_group_id', 'in', [g.id for g in user.groups_id]),
|
||||
'&',
|
||||
('mail_group_id.access_mode', '=', 'members'),
|
||||
('mail_group_id.member_partner_ids', 'in', [user.partner_id.id]),
|
||||
]
|
||||
</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
</record>
|
||||
<record id="mail_group_message_rule_administrator" model="ir.rule">
|
||||
<field name="name">Mail Group Message: Administrator have access to all messages</field>
|
||||
<field name="model_id" ref="model_mail_group_message"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4, ref('mail_group.group_mail_group_manager'))]"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
</record>
|
||||
<record id="mail_group_member_rule_user" model="ir.rule">
|
||||
<field name="name">Mail Group Member: Members are accessible only by moderators</field>
|
||||
<field name="model_id" ref="model_mail_group_member"/>
|
||||
<field name="domain_force">[('mail_group_id.moderator_ids', 'in', user.id), ('mail_group_id.moderation', '=', True)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
</record>
|
||||
<record id="mail_group_member_rule_administrator" model="ir.rule">
|
||||
<field name="name">Mail Group Member: Administrator have access to all members</field>
|
||||
<field name="model_id" ref="model_mail_group_member"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4, ref('mail_group.group_mail_group_manager'))]"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
</record>
|
||||
<record id="mail_group_moderation_rule_user" model="ir.rule">
|
||||
<field name="name">Mail Group Moderation: Moderation rules are accessible only by moderators</field>
|
||||
<field name="model_id" ref="model_mail_group_moderation"/>
|
||||
<field name="domain_force">[('mail_group_id.moderator_ids', 'in', user.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
</record>
|
||||
<record id="mail_group_moderation_rule_administrator" model="ir.rule">
|
||||
<field name="name">Mail Group Moderation: Administrator have access to all moderation rules</field>
|
||||
<field name="model_id" ref="model_mail_group_moderation"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4, ref('mail_group.group_mail_group_manager'))]"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,26 @@
|
||||
.o_mg_subscribe_btn {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.ribbon {
|
||||
right: 0;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
|
||||
span {
|
||||
left: -20px;
|
||||
top: 20px;
|
||||
transform: rotate(45deg);
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
width: 220px;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
padding: 0 60px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
odoo.define('mail_group.mail_group', function (require) {
|
||||
'use strict';
|
||||
|
||||
const publicWidget = require('web.public.widget');
|
||||
const core = require('web.core');
|
||||
const _t = core._t;
|
||||
|
||||
publicWidget.registry.MailGroup = publicWidget.Widget.extend({
|
||||
selector: '.o_mail_group',
|
||||
events: {
|
||||
'click .o_mg_subscribe_btn': '_onSubscribeBtnClick',
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this.mailgroupId = this.$target.data('id');
|
||||
this.isMember = this.$target.data('isMember') || false;
|
||||
this.token = (new URL(document.location.href)).searchParams.get('token');
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onSubscribeBtnClick: async function (ev) {
|
||||
ev.preventDefault();
|
||||
const $email = this.$target.find(".o_mg_subscribe_email");
|
||||
const email = $email.val();
|
||||
|
||||
if (!email.match(/.+@.+/)) {
|
||||
this.$target.addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid');
|
||||
return false;
|
||||
}
|
||||
|
||||
this.$target.removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid');
|
||||
|
||||
let action = this.isMember ? 'unsubscribe' : 'subscribe';
|
||||
|
||||
const response = await this._rpc({
|
||||
route: '/groups/subscription',
|
||||
params: {
|
||||
'group_id': this.mailgroupId,
|
||||
'action': action,
|
||||
'email': email,
|
||||
'token': this.token,
|
||||
},
|
||||
});
|
||||
|
||||
this.$el.find('.o_mg_alert').remove();
|
||||
|
||||
if (response === 'added') {
|
||||
this.isMember = true;
|
||||
this.$target.find('.o_mg_subscribe_btn').text(_t('Unsubscribe'));
|
||||
} else if (response === 'removed') {
|
||||
this.isMember = false;
|
||||
this.$target.find('.o_mg_subscribe_btn').text(_t('Subscribe'));
|
||||
} else if (response === 'email_sent') {
|
||||
// The confirmation email has been sent
|
||||
this.$target.html(
|
||||
$('<div class="o_mg_alert alert alert-success" role="alert"/>')
|
||||
.text(_t('An email with the instructions has been sent.'))
|
||||
);
|
||||
} else if (response === 'is_already_member') {
|
||||
this.isMember = true;
|
||||
this.$target.find('.o_mg_subscribe_btn').text(_t('Unsubscribe'));
|
||||
this.$target.find('.o_mg_subscribe_form').before(
|
||||
$('<div class="o_mg_alert alert alert-warning" role="alert"/>')
|
||||
.text(_t('This email is already subscribed.'))
|
||||
);
|
||||
} else if (response === 'is_not_member') {
|
||||
this.isMember = false;
|
||||
this.$target.find('.o_mg_subscribe_btn').text(_t('Subscribe'));
|
||||
this.$target.find('.o_mg_subscribe_form').before(
|
||||
$('<div class="o_mg_alert alert alert-warning" role="alert"/>')
|
||||
.text(_t('This email is not subscribed.'))
|
||||
);
|
||||
}
|
||||
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
odoo.define('mail_group.mail_group_message', function (require) {
|
||||
'use strict';
|
||||
|
||||
const publicWidget = require('web.public.widget');
|
||||
const core = require('web.core');
|
||||
const _t = core._t;
|
||||
|
||||
publicWidget.registry.MailGroupMessage = publicWidget.Widget.extend({
|
||||
selector: '.o_mg_message',
|
||||
events: {
|
||||
'click .o_mg_link_hide': '_onHideLinkClick',
|
||||
'click .o_mg_link_show': '_onShowLinkClick',
|
||||
'click button.o_mg_read_more': '_onReadMoreClick',
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
// By default hide the mention of the previous email for which we reply
|
||||
// And add a button "Read more" to show the mention of the parent email
|
||||
const body = this.$el.find('.card-body').first();
|
||||
const quoted = body.find('*[data-o-mail-quote]');
|
||||
const readMore = $('<button class="btn btn-link"/>').text(_t('Read more'));
|
||||
quoted.first().before(readMore);
|
||||
quoted.addClass('d-none');
|
||||
readMore.on('click', () => {
|
||||
quoted.toggleClass('d-none');
|
||||
});
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onHideLinkClick: function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const $link = $(ev.currentTarget);
|
||||
const $container = $link.parents('div').first();
|
||||
$container.find('.o_mg_link_hide').first().addClass('d-none');
|
||||
$container.find('.o_mg_link_show').first().removeClass('d-none');
|
||||
$container.find('.o_mg_link_content').first().removeClass('d-none');
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onShowLinkClick: function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const $link = $(ev.currentTarget);
|
||||
const $container = $link.parents('div').first();
|
||||
$container.find('.o_mg_link_hide').first().removeClass('d-none');
|
||||
$container.find('.o_mg_link_show').first().addClass('d-none');
|
||||
$container.find('.o_mg_link_content').first().addClass('d-none');
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onReadMoreClick: function (ev) {
|
||||
const $link = $(ev.target);
|
||||
this._rpc({
|
||||
route: $link.data('href'),
|
||||
params: {
|
||||
last_displayed_id: $link.data('last-displayed-id'),
|
||||
},
|
||||
}).then(function (data) {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const $threadContainer = $link.parents('.o_mg_replies').first().find('ul.list-unstyled');
|
||||
if ($threadContainer) {
|
||||
const $lastMsg = $threadContainer.find('li.media').last();
|
||||
$(data).find('li.media').insertAfter($lastMsg);
|
||||
$(data).find('.o_mg_read_more').parent().appendTo($threadContainer);
|
||||
}
|
||||
const $showMore = $link.parent();
|
||||
$showMore.remove();
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import data
|
||||
from . import test_mail_group
|
||||
from . import test_mail_group_message
|
||||
from . import test_mail_group_moderation
|
||||
@@ -0,0 +1,78 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import Command, tools
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
from odoo.addons.test_mail.tests.common import TestMailCommon
|
||||
|
||||
|
||||
class TestMailListCommon(TestMailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMailListCommon, cls).setUpClass()
|
||||
|
||||
# Test credentials / from
|
||||
cls.email_from_unknown = tools.formataddr(("Bob Lafrite", "bob.email@test.example.com"))
|
||||
cls.user_employee_2 = mail_new_test_user(
|
||||
cls.env, login='employee_2',
|
||||
company_id=cls.company_admin.id,
|
||||
email='employee_2@test.com',
|
||||
groups='base.group_user',
|
||||
name='Albertine Another Employee',
|
||||
)
|
||||
|
||||
# Test group: members, moderation
|
||||
cls.test_group = cls.env['mail.group'].create({
|
||||
'access_mode': 'public',
|
||||
'alias_name': 'test.mail.group',
|
||||
'moderation': True,
|
||||
'moderator_ids': [Command.link(cls.user_employee.id)],
|
||||
'name': 'Test group',
|
||||
})
|
||||
|
||||
cls.moderation = cls.env['mail.group.moderation'].create({
|
||||
'mail_group_id': cls.test_group.id,
|
||||
'email': 'banned_member@test.com',
|
||||
'status': 'ban',
|
||||
})
|
||||
|
||||
cls.test_group_member_1 = cls.env['mail.group.member'].create({
|
||||
'email': '"Member 1" <member_1@test.com>',
|
||||
'mail_group_id': cls.test_group.id,
|
||||
})
|
||||
cls.test_group_member_2 = cls.env['mail.group.member'].create({
|
||||
'email': 'member_2@test.com',
|
||||
'mail_group_id': cls.test_group.id,
|
||||
})
|
||||
cls.test_group_member_3_banned = cls.env['mail.group.member'].create({
|
||||
'email': '"Banned Member" <banned_member@test.com>',
|
||||
'mail_group_id': cls.test_group.id,
|
||||
})
|
||||
cls.test_group_member_4_emp = cls.env['mail.group.member'].create({
|
||||
'partner_id': cls.partner_employee.id,
|
||||
'mail_group_id': cls.test_group.id,
|
||||
})
|
||||
cls.test_group_valid_members = cls.test_group_member_1 + cls.test_group_member_2 + cls.test_group_member_4_emp
|
||||
|
||||
cls._init_mail_gateway()
|
||||
|
||||
# Create some messages
|
||||
cls.test_group_msg_1_pending = cls.env['mail.group.message'].create({
|
||||
'subject': 'Test message pending',
|
||||
'mail_group_id': cls.test_group.id,
|
||||
'moderation_status': 'pending_moderation',
|
||||
'email_from': '"Bob" <bob@test.com>',
|
||||
})
|
||||
cls.test_group_msg_2_accepted = cls.env['mail.group.message'].create({
|
||||
'subject': 'Test message accepted',
|
||||
'mail_group_id': cls.test_group.id,
|
||||
'moderation_status': 'accepted',
|
||||
'email_from': '"Alice" <alice@test.com>',
|
||||
})
|
||||
cls.test_group_msg_3_rejected = cls.env['mail.group.message'].create({
|
||||
'subject': 'Test message rejected',
|
||||
'mail_group_id': cls.test_group.id,
|
||||
'moderation_status': 'rejected',
|
||||
'email_from': '"Alice" <alice@test.com>',
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
GROUP_TEMPLATE = """Return-Path: <whatever-2a840@postmaster.twitter.com>
|
||||
To: {to}
|
||||
cc: {cc}
|
||||
Received: by mail1.openerp.com (Postfix, from userid 10002)
|
||||
id 5DF9ABFB2A; Fri, 10 Aug 2012 16:16:39 +0200 (CEST)
|
||||
From: {email_from}
|
||||
Subject: {subject}
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="----=_Part_4200734_24778174.1344608186754"
|
||||
Date: Fri, 10 Aug 2012 14:16:26 +0000
|
||||
Message-ID: {msg_id}
|
||||
{extra}
|
||||
------=_Part_4200734_24778174.1344608186754
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
This should be posted on a mail.group. Or not.
|
||||
|
||||
--
|
||||
Sylvie
|
||||
------=_Part_4200734_24778174.1344608186754
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
|
||||
<html>
|
||||
<head>=20
|
||||
<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dutf-8" />
|
||||
</head>=20
|
||||
<body style=3D"margin: 0; padding: 0; background: #ffffff;-webkit-text-size-adjust: 100%;">=20
|
||||
|
||||
<p>This should be posted on a mail.group. Or not.</p>
|
||||
|
||||
<p>--<br/>
|
||||
Sylvie
|
||||
<p>
|
||||
</body>
|
||||
</html>
|
||||
------=_Part_4200734_24778174.1344608186754--
|
||||
"""
|
||||
@@ -0,0 +1,131 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
from odoo.addons.mail_group.tests.common import TestMailListCommon
|
||||
from odoo.exceptions import ValidationError, UserError, AccessError
|
||||
from odoo.tests.common import users
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
class TestMailGroup(TestMailListCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMailGroup, cls).setUpClass()
|
||||
|
||||
cls.user_portal = cls._create_portal_user()
|
||||
|
||||
def test_constraint_valid_email(self):
|
||||
mail_group = self.env['mail.group'].with_user(self.user_employee).browse(self.test_group.ids)
|
||||
user_without_email = mail_new_test_user(
|
||||
self.env, login='user_employee_nomail',
|
||||
company_id=self.company_admin.id,
|
||||
email=False,
|
||||
groups='base.group_user',
|
||||
name='User without email',
|
||||
)
|
||||
|
||||
with self.assertRaises(ValidationError, msg="Moderators must have an email"):
|
||||
mail_group.moderator_ids |= user_without_email
|
||||
|
||||
@users('employee')
|
||||
def test_join_group(self):
|
||||
mail_group = self.env['mail.group'].browse(self.test_group.ids)
|
||||
self.assertEqual(len(mail_group.member_ids), 4)
|
||||
|
||||
mail_group._join_group('"Jack" <jack@test.com>')
|
||||
self.assertEqual(len(mail_group.member_ids), 5)
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
mail_group._join_group('"Jack the developer" <jack@test.com>')
|
||||
self.assertEqual(len(mail_group.member_ids), 5, 'Should not have added the duplicated email')
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_rule', 'odoo.addons.base.models.ir_model')
|
||||
@users('employee')
|
||||
def test_mail_group_access_mode_groups(self):
|
||||
test_group = self.env.ref('base.group_partner_manager')
|
||||
mail_group = self.env['mail.group'].browse(self.test_group.ids)
|
||||
mail_group.write({
|
||||
'access_group_id': test_group.id,
|
||||
'access_mode': 'groups',
|
||||
})
|
||||
|
||||
with self.assertRaises(AccessError):
|
||||
mail_group.with_user(self.user_portal).check_access_rule('read')
|
||||
|
||||
public_user = self.env.ref('base.public_user')
|
||||
with self.assertRaises(AccessError):
|
||||
mail_group.with_user(public_user).check_access_rule('read')
|
||||
|
||||
with self.assertRaises(AccessError):
|
||||
mail_group.with_user(self.user_employee_2).check_access_rule('read')
|
||||
|
||||
# Add the group to the user
|
||||
self.user_employee_2.groups_id |= test_group
|
||||
mail_group.with_user(self.user_employee_2).check_access_rule('read')
|
||||
mail_group.with_user(self.user_employee_2).check_access_rule('write')
|
||||
|
||||
# Remove the group of the user BUT add it in the moderators list
|
||||
self.user_employee_2.groups_id -= test_group
|
||||
mail_group.moderator_ids |= self.user_employee_2
|
||||
mail_group.with_user(self.user_employee_2).check_access_rule('read')
|
||||
mail_group.with_user(self.user_employee_2).check_access_rule('write')
|
||||
|
||||
# Test with public user
|
||||
mail_group.access_group_id = self.env.ref('base.group_public')
|
||||
mail_group.with_user(public_user).check_access_rule('read')
|
||||
mail_group.with_user(public_user).check_access_rights('read')
|
||||
with self.assertRaises(AccessError):
|
||||
mail_group.with_user(public_user).check_access_rule('write')
|
||||
mail_group.with_user(public_user).check_access_rights('write')
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_rule', 'odoo.addons.base.models.ir_model')
|
||||
@users('employee')
|
||||
def test_mail_group_access_mode_public(self):
|
||||
mail_group = self.env['mail.group'].browse(self.test_group.ids)
|
||||
mail_group.access_mode = 'public'
|
||||
|
||||
public_user = self.env.ref('base.public_user')
|
||||
mail_group.with_user(public_user).check_access_rule('read')
|
||||
with self.assertRaises(AccessError):
|
||||
mail_group.with_user(public_user).check_access_rights('write')
|
||||
|
||||
mail_group.with_user(self.user_employee_2).check_access_rule('read')
|
||||
mail_group.with_user(self.user_employee_2).check_access_rights('write')
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_rule', 'odoo.addons.base.models.ir_model')
|
||||
@users('employee')
|
||||
def test_mail_group_access_mode_members(self):
|
||||
mail_group = self.env['mail.group'].browse(self.test_group.ids)
|
||||
mail_group.access_mode = 'members'
|
||||
portal_partner = self.user_portal.partner_id
|
||||
self.assertNotIn(portal_partner, mail_group.member_partner_ids)
|
||||
|
||||
with self.assertRaises(AccessError, msg='Non-member should not have access to the group'):
|
||||
mail_group.with_user(self.user_portal).check_access_rule('read')
|
||||
|
||||
public_user = self.env.ref('base.public_user')
|
||||
with self.assertRaises(AccessError, msg='Non-member should not have access to the group'):
|
||||
mail_group.with_user(public_user).check_access_rule('read')
|
||||
|
||||
mail_group.write({'member_ids': [(0, 0, {
|
||||
'partner_id': portal_partner.id,
|
||||
})]})
|
||||
self.assertIn(portal_partner, mail_group.member_partner_ids)
|
||||
|
||||
# Now that portal is in the member list he should have access
|
||||
mail_group.with_user(self.user_portal).check_access_rule('read')
|
||||
mail_group.with_user(self.user_portal).check_access_rule('write')
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_rule', 'odoo.addons.base.models.ir_model')
|
||||
@users('employee')
|
||||
def test_mail_group_member_security(self):
|
||||
member = self.env['mail.group.member'].browse(self.test_group_member_1.ids)
|
||||
self.assertEqual(member.email, '"Member 1" <member_1@test.com>', msg='Moderators should have access to members')
|
||||
|
||||
# with self.assertRaises(AccessError, msg='Portal should not have access to members'):
|
||||
# self.env['mail.group.member'].with_user(self.user_portal).browse(member.ids).email
|
||||
|
||||
# with self.assertRaises(AccessError, msg='Non moderators should not have access to member'):
|
||||
# self.env['mail.group.member'].with_user(self.user_employee_2).browse(member.ids).email
|
||||
@@ -0,0 +1,110 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.mail_group.tests.common import TestMailListCommon
|
||||
from odoo.addons.mail_group.tests.data import GROUP_TEMPLATE
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
class TestMailGroupMessage(TestMailListCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMailGroupMessage, cls).setUpClass()
|
||||
cls.test_group_member_5_emp_2 = cls.env['mail.group.member'].create({
|
||||
'partner_id': cls.user_employee_2.partner_id.id,
|
||||
'mail_group_id': cls.test_group.id,
|
||||
})
|
||||
cls.user_portal = cls._create_portal_user()
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.addons.mail_group.models.mail_group_message')
|
||||
def test_email_duplicated(self):
|
||||
""" Test gateway does not accept two times same incoming email """
|
||||
self.test_group.write({'moderation': False})
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
self.format_and_process(
|
||||
GROUP_TEMPLATE, self.email_from_unknown, self.test_group.alias_id.display_name,
|
||||
subject='Test subject', msg_id='<test.message.id@localhost>', target_model='mail.group')
|
||||
|
||||
message = self.env['mail.group.message'].search([('mail_message_id.message_id', '=', '<test.message.id@localhost>')])
|
||||
self.assertEqual(message.subject, 'Test subject', 'Should have created a <mail.group.message>')
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
self.format_and_process(
|
||||
GROUP_TEMPLATE, self.email_from_unknown, self.test_group.alias_id.display_name,
|
||||
subject='Another subject', msg_id='<test.message.id@localhost>', target_model='mail.group')
|
||||
|
||||
new_message = self.env['mail.group.message'].search([('mail_message_id.message_id', '=', '<test.message.id@localhost>')])
|
||||
self.assertEqual(new_message, message)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.addons.mail_group.models.mail_group_message')
|
||||
def test_email_not_sent_to_author(self):
|
||||
"""Test that when someone sends an email the group process does not send
|
||||
it back to the original author."""
|
||||
self.test_group.write({'moderation': False})
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
self.format_and_process(
|
||||
GROUP_TEMPLATE, self.test_group_member_1.email,
|
||||
self.test_group.alias_id.display_name,
|
||||
subject='Test subject', target_model='mail.group')
|
||||
|
||||
mails = self.env['mail.mail'].search([('subject', '=', 'Test subject')])
|
||||
self.assertEqual(len(mails), 4)
|
||||
self.assertNotIn(self.test_group_member_1.email, mails.mapped('email_to'), 'Should not have send the email to the original author')
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_rule')
|
||||
def test_mail_group_message_security_groups(self):
|
||||
user_group = self.env.ref('base.group_partner_manager')
|
||||
self.test_group.access_group_id = user_group
|
||||
self.test_group.access_mode = 'groups'
|
||||
|
||||
# Message pending
|
||||
with self.assertRaises(AccessError, msg='Portal should not have access to pending messages'):
|
||||
self.test_group_msg_1_pending.with_user(self.user_portal).check_access_rule('read')
|
||||
|
||||
self.user_portal.groups_id |= user_group
|
||||
with self.assertRaises(AccessError, msg='Non moderator should have access to only accepted message'):
|
||||
self.test_group_msg_1_pending.with_user(self.user_portal).check_access_rule('read')
|
||||
|
||||
self.test_group_msg_1_pending.invalidate_cache()
|
||||
self.assertEqual(self.test_group_msg_1_pending.with_user(self.user_employee).moderation_status, 'pending_moderation',
|
||||
msg='Moderators should have access to pending message')
|
||||
|
||||
# Message accepted
|
||||
self.test_group_msg_2_accepted.invalidate_cache()
|
||||
self.assertEqual(self.test_group_msg_2_accepted.with_user(self.user_portal).moderation_status, 'accepted',
|
||||
msg='Portal should have access to accepted messages')
|
||||
|
||||
self.user_portal.groups_id -= user_group
|
||||
with self.assertRaises(AccessError, msg='User not in the group should not have access to accepted message'):
|
||||
self.test_group_msg_2_accepted.with_user(self.user_portal).check_access_rule('read')
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_rule')
|
||||
def test_mail_group_message_security_public(self):
|
||||
self.test_group.access_mode = 'public'
|
||||
|
||||
# Message pending
|
||||
with self.assertRaises(AccessError, msg='Portal should not have access to pending messages'):
|
||||
self.test_group_msg_1_pending.with_user(self.user_portal).check_access_rule('read')
|
||||
|
||||
with self.assertRaises(AccessError, msg='Non moderator should have access to only accepted message'):
|
||||
self.test_group_msg_1_pending.with_user(self.user_employee_2).check_access_rule('read')
|
||||
|
||||
self.test_group_msg_1_pending.invalidate_cache()
|
||||
self.assertEqual(self.test_group_msg_1_pending.with_user(self.user_employee).moderation_status, 'pending_moderation',
|
||||
msg='Moderators should have access to pending message')
|
||||
|
||||
# Message rejected
|
||||
with self.assertRaises(AccessError, msg='Portal should not have access to pending messages'):
|
||||
self.test_group_msg_1_pending.with_user(self.user_portal).check_access_rule('read')
|
||||
|
||||
# Message accepted
|
||||
self.assertEqual(self.test_group_msg_2_accepted.with_user(self.user_portal).moderation_status, 'accepted',
|
||||
msg='Portal should have access to accepted messages')
|
||||
|
||||
self.test_group_msg_3_rejected.invalidate_cache()
|
||||
self.assertEqual(self.test_group_msg_1_pending.with_user(self.user_admin).moderation_status, 'pending_moderation',
|
||||
msg='Mail Group Administrator should have access to all messages')
|
||||
@@ -0,0 +1,420 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from psycopg2 import IntegrityError
|
||||
|
||||
from odoo import Command, tools
|
||||
from odoo.addons.mail_group.tests.data import GROUP_TEMPLATE
|
||||
from odoo.addons.mail_group.tests.common import TestMailListCommon
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.tests.common import tagged, users
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@tagged('mail_group_moderation')
|
||||
class TestMailGroupModeration(TestMailListCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMailGroupModeration, cls).setUpClass()
|
||||
|
||||
# Test group: members, moderation
|
||||
cls.test_group_2 = cls.env['mail.group'].create({
|
||||
'access_mode': 'public',
|
||||
'alias_name': 'test.mail.group.2',
|
||||
'moderation': True,
|
||||
'moderator_ids': [Command.link(cls.user_employee.id)],
|
||||
'name': 'Another test group',
|
||||
})
|
||||
|
||||
@mute_logger('odoo.sql_db')
|
||||
@users('employee')
|
||||
def test_constraints(self):
|
||||
mail_group = self.env['mail.group'].browse(self.test_group.ids)
|
||||
with self.assertRaises(IntegrityError):
|
||||
moderation = self.env['mail.group.moderation'].create({
|
||||
'mail_group_id': mail_group.id,
|
||||
'email': 'banned_member@test.com',
|
||||
'status': 'ban',
|
||||
})
|
||||
moderation.flush()
|
||||
|
||||
@mute_logger('odoo.models.unlink', 'odoo.addons.mail_group.models.mail_group_message')
|
||||
@users('employee')
|
||||
def test_moderation_rule_api(self):
|
||||
""" Test moderation rule creation / update through API """
|
||||
mail_group = self.env['mail.group'].browse(self.test_group.ids)
|
||||
mail_group_2 = self.env['mail.group'].browse(self.test_group_2.ids)
|
||||
self.assertEqual(
|
||||
set(mail_group.moderation_rule_ids.mapped('email')),
|
||||
set(['banned_member@test.com'])
|
||||
)
|
||||
|
||||
moderation_1, moderation_2, moderation_3 = self.env['mail.group.moderation'].create([{
|
||||
'email': 'std@test.com',
|
||||
'status': 'allow',
|
||||
'mail_group_id': mail_group.id,
|
||||
}, {
|
||||
'email': 'xss@test.com',
|
||||
'status': 'ban',
|
||||
'mail_group_id': mail_group.id,
|
||||
}, {
|
||||
'email': 'xss@test.com',
|
||||
'status': 'ban',
|
||||
'mail_group_id': mail_group_2.id,
|
||||
}])
|
||||
|
||||
self.assertEqual(
|
||||
set(mail_group.moderation_rule_ids.mapped('email')),
|
||||
set(['banned_member@test.com', 'std@test.com', 'xss@test.com'])
|
||||
)
|
||||
|
||||
message_1, message_2, message_3 = self.env['mail.group.message'].create([{
|
||||
'email_from': '"Boum" <sTd@teST.com>',
|
||||
'mail_group_id': mail_group.id,
|
||||
}, {
|
||||
'email_from': '"xSs" <xss@teST.com>',
|
||||
'mail_group_id': mail_group.id,
|
||||
}, {
|
||||
'email_from': '"Bob" <bob@teST.com>',
|
||||
'mail_group_id': mail_group.id,
|
||||
}])
|
||||
|
||||
# status 'bouh' does not exist
|
||||
with self.assertRaises(ValueError):
|
||||
(message_1 | message_2 | message_3)._create_moderation_rule('bouh')
|
||||
|
||||
(message_1 | message_2 | message_3)._create_moderation_rule('allow')
|
||||
|
||||
self.assertEqual(len(mail_group.moderation_rule_ids), 4, "Should have created only one moderation rule")
|
||||
self.assertEqual(
|
||||
set(mail_group.moderation_rule_ids.mapped('email')),
|
||||
set(['banned_member@test.com', 'std@test.com', 'xss@test.com', 'bob@test.com'])
|
||||
)
|
||||
self.assertEqual(moderation_1.status, 'allow')
|
||||
self.assertEqual(moderation_2.status, 'allow', 'Should have write on the existing moderation rule')
|
||||
self.assertEqual(moderation_3.status, 'ban', 'Should not have changed moderation of the other group')
|
||||
new_moderation = mail_group.moderation_rule_ids.filtered(lambda rule: rule.email == 'bob@test.com')
|
||||
self.assertEqual(new_moderation.status, 'allow', 'Should have created the moderation with the right status')
|
||||
|
||||
@users('employee')
|
||||
def test_moderation_rule_email_normalize(self):
|
||||
""" Test emails are automatically normalized """
|
||||
rule = self.env['mail.group.moderation'].create({
|
||||
'mail_group_id': self.test_group.id,
|
||||
'email': '"Bob" <bob@test.com>',
|
||||
'status': 'ban',
|
||||
})
|
||||
self.assertEqual(rule.email, 'bob@test.com')
|
||||
|
||||
rule.email = '"Alice" <alice@test.com>'
|
||||
self.assertEqual(rule.email, 'alice@test.com')
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_rule', 'odoo.addons.base.models.ir_model')
|
||||
def test_moderation_rule_security(self):
|
||||
user_portal = self._create_portal_user()
|
||||
|
||||
with self.assertRaises(AccessError, msg='Portal should not have access to moderation rules'):
|
||||
self.env['mail.group.moderation'].with_user(user_portal).browse(self.moderation.ids).email
|
||||
|
||||
self.test_group.write({
|
||||
'moderator_ids': [(4, self.user_admin.id), (3, self.user_employee.id)]
|
||||
})
|
||||
with self.assertRaises(AccessError, msg='Non moderators should not have access to moderation rules'):
|
||||
self.env['mail.group.moderation'].with_user(self.user_employee).browse(self.moderation.ids).email
|
||||
|
||||
self.assertEqual(
|
||||
self.env['mail.group.moderation'].with_user(self.user_admin).browse(self.moderation.ids).email,
|
||||
'banned_member@test.com',
|
||||
msg='Moderators should have access to moderation rules')
|
||||
|
||||
|
||||
@tagged('mail_group_moderation')
|
||||
class TestModeration(TestMailListCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestModeration, cls).setUpClass()
|
||||
|
||||
# Test group: members, moderation
|
||||
cls.test_group_2 = cls.env['mail.group'].create({
|
||||
'access_mode': 'public',
|
||||
'alias_name': 'test.mail.group.2',
|
||||
'moderation': True,
|
||||
'moderator_ids': [Command.link(cls.user_employee_2.id)],
|
||||
'name': 'Another test group',
|
||||
})
|
||||
cls.test_group_2_member_emp = cls.env['mail.group.member'].create({
|
||||
'partner_id': cls.user_employee_2.partner_id.id,
|
||||
'email': cls.user_employee_2.email,
|
||||
'mail_group_id': cls.test_group_2.id,
|
||||
})
|
||||
|
||||
# Existing messages on group 2
|
||||
cls.test_group_2_msg_1_pending = cls.env['mail.group.message'].create({
|
||||
'email_from': cls.email_from_unknown,
|
||||
'subject': 'Group 2 Pending',
|
||||
'mail_group_id': cls.test_group_2.id,
|
||||
'moderation_status': 'pending_moderation',
|
||||
})
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.addons.mail_group.models.mail_group_message')
|
||||
@users('employee')
|
||||
def test_moderation_flow_accept(self):
|
||||
""" Unknown email sends email on moderated group, test accept """
|
||||
mail_group = self.env['mail.group'].browse(self.test_group.ids)
|
||||
self.assertEqual(len(mail_group.mail_group_message_ids), 3)
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
self.format_and_process(
|
||||
GROUP_TEMPLATE, self.email_from_unknown, self.test_group.alias_id.display_name,
|
||||
subject='Old email', target_model='mail.group')
|
||||
|
||||
self.format_and_process(
|
||||
GROUP_TEMPLATE, self.email_from_unknown, self.test_group.alias_id.display_name,
|
||||
subject='New email', target_model='mail.group')
|
||||
|
||||
# find messages
|
||||
self.assertEqual(len(mail_group.mail_group_message_ids), 5)
|
||||
old_email_message = mail_group.mail_group_message_ids[-2]
|
||||
new_email_message = mail_group.mail_group_message_ids[-1]
|
||||
|
||||
# check message content
|
||||
self.assertEqual(old_email_message.moderation_status, 'pending_moderation')
|
||||
self.assertEqual(old_email_message.subject, 'Old email')
|
||||
self.assertEqual(new_email_message.moderation_status, 'pending_moderation')
|
||||
self.assertEqual(new_email_message.subject, 'New email')
|
||||
|
||||
# accept email without any moderation rule
|
||||
with self.mock_mail_gateway():
|
||||
new_email_message.action_moderate_accept()
|
||||
|
||||
self.assertEqual(len(self._new_mails), 4)
|
||||
for email in self.test_group_valid_members.mapped('email'):
|
||||
self.assertMailMailWEmails([email], 'outgoing',
|
||||
content="This should be posted on a mail.group. Or not.",
|
||||
fields_values={
|
||||
'email_from': self.email_from_unknown,
|
||||
'subject': 'New email',
|
||||
},
|
||||
mail_message=new_email_message.mail_message_id)
|
||||
|
||||
self.assertEqual(new_email_message.moderation_status, 'accepted', 'Should have accepted the message')
|
||||
self.assertEqual(old_email_message.moderation_status, 'pending_moderation', 'Should not have touched other message of the same author')
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.addons.mail_group.models.mail_group_message', 'odoo.models.unlink')
|
||||
@users('employee')
|
||||
def test_moderation_flow_allow(self):
|
||||
""" Unknown email sends email on moderated group, test allow """
|
||||
mail_group = self.env['mail.group'].browse(self.test_group.ids)
|
||||
mail_group_2_as2 = self.env['mail.group'].with_user(self.user_employee_2).browse(self.test_group_2.ids)
|
||||
self.assertEqual(len(mail_group.mail_group_message_ids), 3)
|
||||
self.assertEqual(len(mail_group_2_as2.mail_group_message_ids), 1)
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
self.format_and_process(
|
||||
GROUP_TEMPLATE, self.email_from_unknown, self.test_group.alias_id.display_name,
|
||||
subject='Old email', target_model='mail.group')
|
||||
|
||||
self.format_and_process(
|
||||
GROUP_TEMPLATE, self.email_from_unknown, self.test_group.alias_id.display_name,
|
||||
subject='New email', target_model='mail.group')
|
||||
|
||||
# find messages
|
||||
self.assertEqual(len(mail_group.mail_group_message_ids), 5)
|
||||
old_email_message = mail_group.mail_group_message_ids[-2]
|
||||
new_email_message = mail_group.mail_group_message_ids[-1]
|
||||
|
||||
# check message content
|
||||
self.assertEqual(old_email_message.email_from, self.email_from_unknown)
|
||||
self.assertEqual(old_email_message.moderation_status, 'pending_moderation')
|
||||
self.assertEqual(old_email_message.subject, 'Old email')
|
||||
self.assertEqual(new_email_message.email_from, self.email_from_unknown)
|
||||
self.assertEqual(new_email_message.moderation_status, 'pending_moderation')
|
||||
self.assertEqual(new_email_message.subject, 'New email')
|
||||
|
||||
# Create a moderation rule to always accept this email address
|
||||
with self.mock_mail_gateway():
|
||||
new_email_message.action_allow()
|
||||
|
||||
self.assertEqual(new_email_message.moderation_status, 'accepted', 'Should have accepted the message')
|
||||
self.assertEqual(old_email_message.moderation_status, 'accepted', 'Should have accepted the old message of the same author')
|
||||
|
||||
# Test that the moderation rule has been created
|
||||
new_rule = self.env['mail.group.moderation'].search([
|
||||
('status', '=', 'allow'),
|
||||
('email', '=', tools.email_normalize(self.email_from_unknown))
|
||||
])
|
||||
self.assertEqual(len(new_rule), 1, 'Should have created a moderation rule')
|
||||
|
||||
# Check emails have been sent
|
||||
self.assertEqual(len(self._new_mails), 8)
|
||||
for email in self.test_group_valid_members.mapped('email'):
|
||||
self.assertMailMailWEmails([email], 'outgoing',
|
||||
content="This should be posted on a mail.group. Or not.",
|
||||
fields_values={
|
||||
'email_from': self.email_from_unknown,
|
||||
'subject': 'New email',
|
||||
},
|
||||
mail_message=new_email_message.mail_message_id)
|
||||
self.assertMailMailWEmails([email], 'outgoing',
|
||||
content="This should be posted on a mail.group. Or not.",
|
||||
fields_values={
|
||||
'email_from': self.email_from_unknown,
|
||||
'subject': 'Old email',
|
||||
},
|
||||
mail_message=old_email_message.mail_message_id)
|
||||
|
||||
# Send a second email with the same FROM, but with a different name
|
||||
with self.mock_mail_gateway():
|
||||
self.format_and_process(
|
||||
GROUP_TEMPLATE,
|
||||
tools.formataddr(("Another Name", "bob.email@test.example.com")),
|
||||
self.test_group.alias_id.display_name,
|
||||
subject='Another email', target_model='mail.group')
|
||||
|
||||
# find messages
|
||||
self.assertEqual(len(mail_group.mail_group_message_ids), 6)
|
||||
new_email_message = mail_group.mail_group_message_ids[-1]
|
||||
|
||||
self.assertEqual(new_email_message.email_from, tools.formataddr(("Another Name", "bob.email@test.example.com")))
|
||||
self.assertEqual(new_email_message.moderation_status, 'accepted', msg='Should have automatically accepted the email')
|
||||
self.assertEqual(new_email_message.subject, 'Another email')
|
||||
|
||||
self.assertEqual(
|
||||
self.test_group_2_msg_1_pending.moderation_status, 'pending_moderation',
|
||||
'Should not have accepted message in the other group')
|
||||
self.assertEqual(
|
||||
len(mail_group_2_as2.mail_group_message_ids), 1,
|
||||
'Should never have created message in the other group')
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.addons.mail_group.models.mail_group_message', 'odoo.models.unlink')
|
||||
@users('employee')
|
||||
def test_moderation_flow_ban(self):
|
||||
""" Unknown email sends email on moderated group, test ban """
|
||||
mail_group = self.env['mail.group'].browse(self.test_group.ids)
|
||||
self.assertEqual(len(mail_group.mail_group_message_ids), 3)
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
self.format_and_process(
|
||||
GROUP_TEMPLATE, self.email_from_unknown, self.test_group.alias_id.display_name,
|
||||
subject='Old email', target_model='mail.group')
|
||||
|
||||
self.format_and_process(
|
||||
GROUP_TEMPLATE, self.email_from_unknown, self.test_group.alias_id.display_name,
|
||||
subject='New email', target_model='mail.group')
|
||||
|
||||
# find messages
|
||||
self.assertEqual(len(mail_group.mail_group_message_ids), 5)
|
||||
old_email_message = mail_group.mail_group_message_ids[-2]
|
||||
new_email_message = mail_group.mail_group_message_ids[-1]
|
||||
|
||||
# ban and check moderation rule has been
|
||||
with self.mock_mail_gateway():
|
||||
new_email_message.action_ban()
|
||||
|
||||
self.assertEqual(old_email_message.moderation_status, 'rejected')
|
||||
self.assertEqual(new_email_message.moderation_status, 'rejected')
|
||||
|
||||
# Test that the moderation rule has been created
|
||||
new_rule = self.env['mail.group.moderation'].search([
|
||||
('status', '=', 'ban'),
|
||||
('email', '=', tools.email_normalize(self.email_from_unknown))
|
||||
])
|
||||
self.assertEqual(len(new_rule), 1, 'Should have created a moderation rule')
|
||||
|
||||
# Check no mail.mail has been sent
|
||||
self.assertEqual(len(self._new_mails), 0, 'Should not have send emails')
|
||||
|
||||
# Send a second email with the same FROM, but with a different name
|
||||
with self.mock_mail_gateway():
|
||||
self.format_and_process(
|
||||
GROUP_TEMPLATE,
|
||||
tools.formataddr(("Another Name", "bob.email@test.example.com")),
|
||||
self.test_group.alias_id.display_name,
|
||||
subject='Another email', target_model='mail.group')
|
||||
|
||||
# find messages
|
||||
self.assertEqual(len(mail_group.mail_group_message_ids), 6)
|
||||
new_email_message = mail_group.mail_group_message_ids[-1]
|
||||
self.assertEqual(new_email_message.moderation_status, 'rejected', 'Should have automatically rejected the email')
|
||||
|
||||
# Check no mail.mail has been sent
|
||||
self.assertEqual(len(self._new_mails), 0, 'Should not have send emails')
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.addons.mail_group.models.mail_group_message', 'odoo.models.unlink')
|
||||
@users('employee')
|
||||
def test_moderation_flow_reject(self):
|
||||
""" Unknown email sends email on moderated group, test reject """
|
||||
mail_group = self.env['mail.group'].browse(self.test_group.ids)
|
||||
self.assertEqual(len(mail_group.mail_group_message_ids), 3)
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
self.format_and_process(
|
||||
GROUP_TEMPLATE, self.email_from_unknown, self.test_group.alias_id.display_name,
|
||||
subject='Old email', target_model='mail.group')
|
||||
|
||||
self.format_and_process(
|
||||
GROUP_TEMPLATE, self.email_from_unknown, self.test_group.alias_id.display_name,
|
||||
subject='New email', target_model='mail.group')
|
||||
|
||||
# find messages
|
||||
self.assertEqual(len(mail_group.mail_group_message_ids), 5)
|
||||
old_email_message = mail_group.mail_group_message_ids[-2]
|
||||
new_email_message = mail_group.mail_group_message_ids[-1]
|
||||
|
||||
# reject without moderation rule
|
||||
with self.mock_mail_gateway():
|
||||
new_email_message.action_moderate_reject_with_comment('Test Rejected', 'Bad email')
|
||||
|
||||
self.assertEqual(new_email_message.moderation_status, 'rejected', 'Should have rejected the message')
|
||||
self.assertEqual(old_email_message.moderation_status, 'pending_moderation', 'Should not have rejected old message')
|
||||
|
||||
self.assertEqual(len(self._new_mails), 1, 'Should have sent the reject email')
|
||||
self.assertMailMailWEmails([self.email_from_unknown], 'outgoing',
|
||||
content="This should be posted on a mail.group. Or not.",
|
||||
fields_values={
|
||||
'email_from': self.user_employee.email_formatted,
|
||||
'subject': 'Test Rejected',
|
||||
})
|
||||
|
||||
@mute_logger('odoo.addons.mail_group.models.mail_group')
|
||||
@users('employee')
|
||||
def test_moderation_send_guidelines(self):
|
||||
""" Test sending guidelines """
|
||||
mail_group = self.env['mail.group'].browse(self.test_group.ids)
|
||||
mail_group.write({
|
||||
'moderation_guidelines': True,
|
||||
'moderation_guidelines_msg': 'Test guidelines group',
|
||||
})
|
||||
with self.mock_mail_gateway():
|
||||
mail_group.action_send_guidelines()
|
||||
|
||||
self.assertEqual(len(self._new_mails), 3)
|
||||
for email in self.test_group_valid_members.mapped('email'):
|
||||
self.assertMailMailWEmails([email], 'outgoing',
|
||||
content="Test guidelines group",
|
||||
fields_values={
|
||||
'email_from': self.env.company.catchall_formatted,
|
||||
'subject': 'Guidelines of group %s' % mail_group.name,
|
||||
})
|
||||
|
||||
@mute_logger('odoo.addons.mail_group.models.mail_group')
|
||||
@users('employee')
|
||||
def test_moderation_send_guidelines_on_new_member(self):
|
||||
""" Test sending guidelines when having a new members """
|
||||
mail_group = self.env['mail.group'].browse(self.test_group.ids)
|
||||
mail_group.write({
|
||||
'moderation_guidelines': True,
|
||||
'moderation_guidelines_msg': 'Test guidelines group',
|
||||
})
|
||||
with self.mock_mail_gateway():
|
||||
mail_group._join_group('"New Member" <new.member@test.com>')
|
||||
|
||||
self.assertEqual(len(self._new_mails), 1)
|
||||
self.assertMailMailWEmails(['"New Member" <new.member@test.com>'], 'outgoing',
|
||||
content="Test guidelines group",
|
||||
fields_values={
|
||||
'email_from': self.env.company.catchall_formatted,
|
||||
'subject': 'Guidelines of group %s' % mail_group.name,
|
||||
})
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="mail_group_member_view_tree" model="ir.ui.view">
|
||||
<field name="name">mail.group.member.view.tree</field>
|
||||
<field name="model">mail.group.member</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree editable="top">
|
||||
<field name="email"
|
||||
required="1" force_save="1"
|
||||
attrs="{'readonly': [('partner_id', '!=', False)]}"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="mail_group_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="mail_group_member_view_search" model="ir.ui.view">
|
||||
<field name="name">mail.group.member.view.search</field>
|
||||
<field name="model">mail.group.member</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Mail Group Member">
|
||||
<field name="email" required="1" />
|
||||
<field name="partner_id"/>
|
||||
<field name="mail_group_id"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
<record id="mail_group_member_action" model="ir.actions.act_window">
|
||||
<field name="name">Members</field>
|
||||
<field name="res_model">mail.group.member</field>
|
||||
<field name="view_mode">tree</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<menuitem id="mail_group_menu"
|
||||
name="Mail Groups"
|
||||
action="mail_group_action"
|
||||
parent="mail.mail_menu_technical"
|
||||
sequence="50"/>
|
||||
<menuitem id="mail_group_moderation_menu"
|
||||
name="Moderation Rules"
|
||||
action="mail_group_moderation_action"
|
||||
parent="mail.mail_menu_technical"
|
||||
sequence="51"/>
|
||||
</odoo>
|
||||
@@ -0,0 +1,103 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="mail_group_message_view_list" model="ir.ui.view">
|
||||
<field name="name">mail.group.message.view.list</field>
|
||||
<field name="model">mail.group.message</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="create_date" string="Date"/>
|
||||
<field name="author_id"/>
|
||||
<field name="email_from"/>
|
||||
<field name="subject"/>
|
||||
<field name="mail_group_id"/>
|
||||
<field name="moderation_status"/>
|
||||
<button name="action_moderate_accept" string="Accept" title="Accept"
|
||||
type="object" class="btn btn-primary"
|
||||
attrs="{'invisible': [('moderation_status', '!=', 'pending_moderation')]}"/>
|
||||
<button name="action_moderate_reject" string="Discard" title="Remove message without explanation"
|
||||
type="object" class="btn btn-secondary"
|
||||
attrs="{'invisible': [('moderation_status', '!=', 'pending_moderation')]}"/>
|
||||
<button name="%(mail_group_message_reject_action)d"
|
||||
string="Reject" title="Remove message with explanation"
|
||||
type="action" class="btn btn-secondary"
|
||||
attrs="{'invisible': [('moderation_status', '!=', 'pending_moderation')]}"
|
||||
context="{'default_mail_group_message_id': id}" />
|
||||
<button name="action_allow" string="Always Allow"
|
||||
title="Add this email address to white list of people and accept all pending messages from the same author."
|
||||
type="object" class="btn btn-secondary"
|
||||
attrs="{'invisible': [('moderation_status', '!=', 'pending_moderation')]}"/>
|
||||
<button name="action_ban" type="object" string="Ban"
|
||||
title="Ban this email address and reject all pending messages from the same author"
|
||||
class="btn btn-secondary"
|
||||
attrs="{'invisible': [('moderation_status', '!=', 'pending_moderation')]}"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="mail_group_message_view_form" model="ir.ui.view">
|
||||
<field name="name">mail.group.message.view.form</field>
|
||||
<field name="model">mail.group.message</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Group Message">
|
||||
<header>
|
||||
<button name="action_moderate_accept" string="Accept" title="Accept"
|
||||
type="object" class="btn btn-primary"
|
||||
attrs="{'invisible': [('moderation_status', '!=', 'pending_moderation')]}"/>
|
||||
<button name="action_moderate_reject" string="Discard" title="Remove message without explanation"
|
||||
type="object" class="btn btn-secondary"
|
||||
attrs="{'invisible': [('moderation_status', '!=', 'pending_moderation')]}"/>
|
||||
<button name="%(mail_group_message_reject_action)d"
|
||||
string="Reject" title="Remove message with explanation"
|
||||
type="action" class="btn btn-secondary"
|
||||
attrs="{'invisible': [('moderation_status', '!=', 'pending_moderation')]}"
|
||||
context="{'default_mail_group_message_id': id}" />
|
||||
<button name="action_allow" string="Always Allow"
|
||||
title="Add this email address to white list of people and accept all pending messages from the same author."
|
||||
type="object" class="btn btn-secondary"
|
||||
attrs="{'invisible': [('moderation_status', '!=', 'pending_moderation')]}"/>
|
||||
<button name="action_ban" string="Ban"
|
||||
title="Ban this email address and reject all pending messages from the same author"
|
||||
type="object" class="btn btn-secondary"
|
||||
attrs="{'invisible': [('moderation_status', '!=', 'pending_moderation')]}"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<widget name="web_ribbon" title="Rejected" bg_color="bg-danger"
|
||||
attrs="{'invisible': [('moderation_status', '!=', 'rejected')]}"/>
|
||||
<widget name="web_ribbon" title="Accepted" bg_color="bg-success"
|
||||
attrs="{'invisible': [('moderation_status', '!=', 'accepted')]}"/>
|
||||
<group>
|
||||
<field name="mail_message_id" invisible="1"/>
|
||||
<field name="moderation_status" invisible="1"/>
|
||||
<field name="subject"/>
|
||||
<field name="author_id"/>
|
||||
<field name="email_from"/>
|
||||
<field name="mail_group_id"/>
|
||||
<field name="attachment_ids" widget="many2many_binary"/>
|
||||
<field name="body" options="{'style-inline': true}"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="mail_group_message_view_search" model="ir.ui.view">
|
||||
<field name="name">mail.group.message.view.search</field>
|
||||
<field name="model">mail.group.message</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Group Message">
|
||||
<field name="mail_group_id"/>
|
||||
<field name="email_from"/>
|
||||
<field name="author_id"/>
|
||||
<field name="moderation_status"/>
|
||||
<separator/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="group" name="group_by_group" context="{'group_by': 'mail_group_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
<record id="mail_group_message_action" model="ir.actions.act_window">
|
||||
<field name="name">Messages</field>
|
||||
<field name="res_model">mail.group.message</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="context">{'search_default_moderation_status': 'pending_moderation'}</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<record id="mail_group_moderation_view_tree" model="ir.ui.view">
|
||||
<field name="name">mail.group.moderation.view.tree</field>
|
||||
<field name="model">mail.group.moderation</field>
|
||||
<field name="priority">20</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Moderation Lists" editable="bottom">
|
||||
<field name="mail_group_id"/>
|
||||
<field name="email"/>
|
||||
<field name="status"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="mail_group_moderation_view_search" model="ir.ui.view">
|
||||
<field name="name">mail.group.moderation.view.search</field>
|
||||
<field name="model">mail.group.moderation</field>
|
||||
<field name="priority">25</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Moderation List">
|
||||
<field name="mail_group_id"/>
|
||||
<field name="email"/>
|
||||
<field name="status"/>
|
||||
<filter string="Is Banned"
|
||||
name="status_ban" help="Banned Emails"
|
||||
domain="[('status', '=', 'ban')]"/>
|
||||
<separator/>
|
||||
<filter string="Is Allowed"
|
||||
name="status_allow" help="Allowed Emails"
|
||||
domain="[('status', '=', 'allow')]"/>
|
||||
<filter name="group_by_status" string="Status" domain="[]" context="{'group_by':'status'}"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
<record id="mail_group_moderation_action" model="ir.actions.act_window">
|
||||
<field name="name">Moderation</field>
|
||||
<field name="res_model">mail.group.moderation</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="search_view_id" ref="mail_group_moderation_view_search"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,126 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<record id="mail_group_view_list" model="ir.ui.view">
|
||||
<field name="name">mail.group.view.list</field>
|
||||
<field name="model">mail.group</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="mail_group_view_form" model="ir.ui.view">
|
||||
<field name="name">mail.group.view.form</field>
|
||||
<field name="model">mail.group</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Mail Group">
|
||||
<header>
|
||||
<button name="action_send_guidelines" type="object" class="btn btn-secondary" string="Send Guidelines" attrs="{'invisible': ['|', ('is_moderator', '=', False), ('moderation', '=', False)]}"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box" groups="base.group_user" attrs="{'invisible': [('can_manage_members', '=', False)]}">
|
||||
<button name="%(mail_group.mail_group_member_action)d"
|
||||
type="action"
|
||||
context="{'search_default_mail_group_id': active_id}"
|
||||
class="oe_stat_button"
|
||||
icon="fa-users"
|
||||
help="Members of this group">
|
||||
<field name="member_count" widget="statinfo" string="Members"/>
|
||||
</button>
|
||||
<button name="%(mail_group.mail_group_moderation_action)d"
|
||||
type="action"
|
||||
context="{'search_default_mail_group_id': active_id}"
|
||||
attrs="{'invisible': [('moderation', '=', False)]}"
|
||||
class="oe_stat_button"
|
||||
icon="fa-gavel"
|
||||
help="Moderated emails in this group">
|
||||
<field name="moderation_rule_count" widget="statinfo" string="Moderations"/>
|
||||
</button>
|
||||
<button name="%(mail_group.mail_group_message_action)d"
|
||||
type="action"
|
||||
context="{'search_default_mail_group_id': active_id, 'search_default_moderation_status': 'pending_moderation'}"
|
||||
class="oe_stat_button"
|
||||
icon="fa-envelope"
|
||||
help="Emails waiting an action for this group">
|
||||
<field name="mail_group_message_moderation_count" widget="statinfo" string="Messages"/>
|
||||
</button>
|
||||
</div>
|
||||
<field name="image_128" widget="image" class="oe_avatar" options="{'size': [90, 90]}"/>
|
||||
<div class="oe_title">
|
||||
<label for="name" string="Group Name"/>
|
||||
<h1>
|
||||
<field name="name" class="oe_inline" default_focus="1" placeholder="e.g. newsletter"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<label for="alias_name" string="Email Alias"/>
|
||||
<div class="oe_inline" name="alias_def">
|
||||
<field name="alias_id" class="oe_read_only oe_inline"
|
||||
string="Email Alias" required="0"
|
||||
attrs="{'invisible': [('alias_domain', '=', False)]}"/>
|
||||
<div class="oe_inline" name="edit_alias" style="display: inline;" >
|
||||
<div class="oe_edit_only" attrs="{'invisible': [('alias_domain', '=', False)]}">
|
||||
<field name="alias_name" class="oe_inline"/>@<field name="alias_domain" class="oe_inline" readonly="1"/>
|
||||
</div>
|
||||
<button icon="fa-arrow-right" type="action" name="%(base_setup.action_general_configuration)d" string="Configure a custom domain" class="p-0 btn-link" attrs="{'invisible': [('alias_domain', '!=', False)]}"/>
|
||||
</div>
|
||||
</div>
|
||||
<field name="description"/>
|
||||
<field name="access_mode" widget="radio"/>
|
||||
<field name="access_group_id" attrs="{
|
||||
'invisible': [('access_mode', '!=', 'groups')],
|
||||
'required': [('access_mode', '=', 'groups')],
|
||||
}"/>
|
||||
<field name="alias_contact" invisible="1"/>
|
||||
<td class="o_td_label">
|
||||
<label for="moderator_ids" string="Moderators" attrs="{'invisible': [('moderation', '=', False)]}"/>
|
||||
<label for="moderator_ids" string="Responsibles" attrs="{'invisible': [('moderation', '=', True)]}"/>
|
||||
</td>
|
||||
<field name="moderator_ids" widget="many2many_tags" class="oe_inline" nolabel="1"/>
|
||||
<field name="moderation"/>
|
||||
<field name="is_moderator" invisible="1"/>
|
||||
<field name="can_manage_members" invisible="1"/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page name="members" string="Members" attrs="{'invisible': [('can_manage_members', '=', False)]}">
|
||||
<field name="member_ids">
|
||||
<tree editable="top">
|
||||
<field name="email" attrs="{'readonly': [('partner_id', '!=', False)]}" force_save="1"/>
|
||||
<field name="partner_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page name="moderation" string="Moderation" attrs="{'invisible': [('moderation', '=', False)]}">
|
||||
<group>
|
||||
<field name="moderation_notify"/>
|
||||
<field attrs="{'invisible': [('moderation_notify', '=', False)], 'required': [('moderation_notify', '=', True)]}" name="moderation_notify_msg"/>
|
||||
<field name="moderation_guidelines"/>
|
||||
<field attrs="{'required':[('moderation_guidelines', '=', True)]}" name="moderation_guidelines_msg"/>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="mail_group_view_search" model="ir.ui.view">
|
||||
<field name="name">mail.group.view.search</field>
|
||||
<field name="model">mail.group</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Mailing group">
|
||||
<field name="name"/>
|
||||
<field name="alias_name"/>
|
||||
<separator/>
|
||||
<filter string="Moderated" name="moderation" domain="[('moderation', '=', True)]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Moderation" name="Moderation" context="{'group_by':'moderation'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
<record id="mail_group_action" model="ir.actions.act_window">
|
||||
<field name="name">Mailing group</field>
|
||||
<field name="res_model">mail.group</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,324 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!--
|
||||
Templates used for the portal view.
|
||||
-->
|
||||
|
||||
<template id="mail_groups" name="Mailing Lists">
|
||||
<t t-call="portal.portal_layout">
|
||||
<div class="oe_structure oe_empty">
|
||||
<section class="bg-primary jumbotron mt0 mb0">
|
||||
<div class="container">
|
||||
<h1>Stay in touch with our Community</h1>
|
||||
<p>Alone we can do so little, together we can do so much</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="container mt32">
|
||||
<div t-if="'unsubscribe' in request.params" class="offset-lg-9 col-lg-3 alert alert-info" role="status">
|
||||
<h3>Need to unsubscribe? It's right here! <span class="fa fa-2x fa-arrow-down float-right" role="img" aria-label="" title="Read this !"></span></h3>
|
||||
</div>
|
||||
<div class="row mt8" t-foreach="mail_groups" t-as="group_data">
|
||||
<t t-set="group" t-value="group_data['group']"/>
|
||||
<t t-set="is_member" t-value="group_data['is_member']"/>
|
||||
<div class="col-lg-3">
|
||||
<img t-attf-src="/web/image/mail.group/#{group.id}/image_128" class="o_image_64_cover float-left" alt="Group"/>
|
||||
|
||||
<strong><a t-attf-href="/groups/#{ slug(group) }" t-esc="group.name"/></strong><br />
|
||||
<t t-if="group.alias_id and group.alias_id.alias_name and group.alias_id.alias_domain">
|
||||
<i class='fa fa-envelope-o' role="img" aria-label="Alias" title="Alias"/>
|
||||
<a t-attf-href="mailto:#{group.alias_id.alias_name}@#{group.alias_id.alias_domain}"><span t-field="group.alias_id"/></a>
|
||||
</t>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div t-esc="group.description" class="text-muted"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<i class='fa fa-fw fa-user' role="img" aria-label="Recipients" title="Recipients"/> <t t-esc="group.member_count"/> members<br />
|
||||
<i class='fa fa-fw fa-envelope-o' role="img" aria-label="Traffic" title="Traffic"/> <t t-esc="group.mail_group_message_last_month_count"/> messages / month
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
<t t-set="force_unsubscribe" t-value="'unsubscribe' in request.params and not email"/>
|
||||
<div class="o_mail_group" t-att-data-id="group.id" t-att-data-is-member="force_unsubscribe or is_member">
|
||||
<div class="input-group o_mg_subscribe_form">
|
||||
<input type="email" name="email" class="o_mg_subscribe_email form-control" t-att-value="email" placeholder="your email..." t-att-readonly="int(bool(email))"/>
|
||||
<div t-if="force_unsubscribe or is_member" class="input-group-append">
|
||||
<button href="#" class="btn btn-primary o_mg_subscribe_btn">Unsubscribe</button>
|
||||
</div>
|
||||
<div t-else="" class="input-group-append">
|
||||
<button href="#" class="btn btn-primary o_mg_subscribe_btn">Subscribe</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="group_messages" name="Message Threads">
|
||||
<t t-call="portal.portal_layout">
|
||||
<section class="container">
|
||||
<div class="mt8">
|
||||
<ol class="breadcrumb float-left">
|
||||
<li class="breadcrumb-item"><a href="/groups">Mailing Lists</a></li>
|
||||
<li class="breadcrumb-item">
|
||||
<a t-attf-href="/groups/#{slug(group)}?#{mode and 'mode=%s' % mode or ''}#{date_begin and '&date_begin=%s' % date_begin or ''}#{date_end and '&date_end=%s' % date_end or ''}"><t t-esc="group.name"/></a>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<h1 class="text-center" t-esc="group.name"/>
|
||||
<h4 class="text-center text-muted" t-if="group.alias_id and group.alias_id.alias_name and group.alias_id.alias_domain">
|
||||
<i class='fa fa-envelope-o' role="img" aria-label="Alias" title="Alias"/>
|
||||
<a t-attf-href="mailto:#{group.alias_id.alias_name}@#{group.alias_id.alias_domain}"><span t-field="group.alias_id"/></a>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
<h2>Archives</h2>
|
||||
<ul class="nav nav-pills flex-column" id="group_mode">
|
||||
<li class="nav-item">
|
||||
<a t-attf-href="/groups/#{ slug(group) }?mode=thread" t-attf-class="nav-link#{mode=='thread' and ' active' or ''}">By thread</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a t-attf-href="/groups/#{ slug(group) }?mode=date" t-attf-class="nav-link#{mode=='date' and not date_begin and ' active' or ''}">By date</a>
|
||||
<ul class="nav nav-pills flex-column" style="margin-left: 8px;">
|
||||
<t t-foreach="archives" t-as="month_archive">
|
||||
<li class="nav-item">
|
||||
<a t-ignore="True" t-attf-href="/groups/#{ slug(group) }?mode=date&date_begin=#{ month_archive['date_begin'] }&date_end=#{month_archive['date_end']}"
|
||||
t-attf-class="nav-link#{month_archive['date_begin'] == date_begin and ' active' or ''}">
|
||||
<t t-esc="month_archive['date']"/>
|
||||
<span class="float-right badge badge-pill" t-esc="month_archive['messages_count']"/>
|
||||
</a>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<div>
|
||||
<t t-call="portal.pager"/>
|
||||
</div>
|
||||
<t t-call="mail_group.messages_short">
|
||||
<t t-set="messages" t-value="messages"/>
|
||||
<t t-set="msg_more_count" t-value="0"/>
|
||||
<t t-set="parent_message" t-value="None"/>
|
||||
</t>
|
||||
<div>
|
||||
<t t-call="portal.pager"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="group_message">
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-set="additional_title"><t t-esc="message.subject"/></t>
|
||||
<section class="container">
|
||||
<div class="row mt8">
|
||||
<ol class="breadcrumb float-left">
|
||||
<li class="breadcrumb-item"><a href="/groups">Mailing Lists</a></li>
|
||||
<li class="breadcrumb-item">
|
||||
<a t-attf-href="/groups/#{slug(group)}?#{mode and 'mode=%s' % mode or ''}#{date_begin and '&date_begin=%s' % date_begin or ''}#{date_end and '&date_end=%s' % date_end or ''}"><t t-esc="group.name"/></a>
|
||||
</li>
|
||||
<li t-if="message" class="breadcrumb-item active"><t t-esc="message.subject"/></li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h1 class="text-center">
|
||||
<t t-esc="group.name"/> archives
|
||||
</h1>
|
||||
<h4 class="text-center text-muted" t-if="group.alias_id and group.alias_id.alias_name and group.alias_id.alias_domain">
|
||||
<i class='fa fa-envelope-o' role="img" aria-label="Alias" title="Alias"/>
|
||||
<a t-attf-href="mailto:#{group.alias_id.alias_name}@#{group.alias_id.alias_domain}"><span t-field="group.alias_id"/></a>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-3">
|
||||
<h4>Browse archives</h4>
|
||||
<ul class="nav nav-pills flex-column" id="group_mode">
|
||||
<li class="nav-item">
|
||||
<a t-attf-href="/groups/#{ slug(group) }?mode=thread" t-attf-class="nav-link#{mode=='thread' and ' active' or ''}">By thread</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a t-attf-href="/groups/#{ slug(group) }?mode=date" t-attf-class="nav-link#{mode=='date' and not date_begin and ' active' or ''}">By date</a>
|
||||
<ul class="nav nav-pills flex-column" style="margin-left: 8px;">
|
||||
<t t-foreach="archives" t-as="month_archive">
|
||||
<li class="nav-item">
|
||||
<a t-ignore="True" t-attf-href="/groups/#{ slug(group) }?mode=date&date_begin=#{ month_archive['date_begin'] }&date_end=#{month_archive['date_end']}"
|
||||
t-attf-class="nav-link#{month_archive['date_begin'] == date_begin and ' active' or ''}">
|
||||
<t t-esc="month_archive['date']"/>
|
||||
<span class="float-right badge badge-pill" t-esc="month_archive['messages_count']"/>
|
||||
</a>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<div class="row">
|
||||
<h4 class="col-lg-6">
|
||||
<t t-if="prev_message"><a t-attf-href='/groups/#{slug(group)}/#{slug(prev_message)}?#{mode and "mode=%s" % mode or ""}'>
|
||||
<i class="fa fa-arrow-left" role="img" aria-label="Previous message" title="Previous message"/> <t t-esc="prev_message.subject"/>
|
||||
</a></t>
|
||||
</h4>
|
||||
<h4 class="col-lg-6">
|
||||
<t t-if="next_message"><a class="float-right" t-attf-href='/groups/#{slug(group)}/#{slug(next_message)}?#{mode and "mode=%s" % mode or ""}'>
|
||||
<t t-esc="next_message.subject"/> <i class="fa fa-arrow-right" role="img" aria-label="Next message" title="Next message"/>
|
||||
</a></t>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="media">
|
||||
<div class="media-body o_mg_message">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h4>
|
||||
<img t-if="message.sudo().author_id.active and message.sudo().author_id.image_128" t-att-src="image_data_uri(message.sudo().author_id.image_128)" class="rounded o_image_40_cover"/>
|
||||
<span t-esc="message.subject"/>
|
||||
</h4>
|
||||
<p class="card-text" t-field="message.body"/>
|
||||
<t t-call="mail_group.message_footer"/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p t-if="attachments" class="mt8">
|
||||
<a href="#" class="o_mg_link_hide">
|
||||
<i class="fa fa-chevron-right" role="img" aria-label="Hide attachments" title="Hide attachments"/> <t t-esc="len(attachments)"/> attachments
|
||||
</a>
|
||||
<a href="#" class="o_mg_link_show d-none">
|
||||
<i class="fa fa-chevron-down" role="img" aria-label="Show attachments" title="Show attachments"/> <t t-esc="len(attachments)"/> attachments
|
||||
</a>
|
||||
</p>
|
||||
<div class="o_mg_link_content d-none row">
|
||||
<div class="mx-4 my-3 text-center" t-foreach='attachments' t-as='attachment'>
|
||||
<t t-if="attachment.access_token">
|
||||
<a t-attf-href="/web/content/#{attachment.id}?download=true&access_token=#{attachment.access_token}" target="_blank">
|
||||
<div class='oe_attachment_embedded o_image' t-att-title="attachment.name" t-att-data-mimetype="attachment.mimetype" t-attf-data-src="/web/image/#{attachment.id}/100x80?access_token=#{attachment.access_token}"/>
|
||||
<div class='oe_attachment_name'><t t-esc='attachment.name' /></div>
|
||||
</a>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<a t-attf-href="/web/content/#{attachment.id}?download=true" target="_blank">
|
||||
<div class='oe_attachment_embedded o_image' t-att-title="attachment.name" t-att-data-mimetype="attachment.mimetype" t-attf-data-src="/web/image/#{attachment.id}/100x80"/>
|
||||
<div class='oe_attachment_name'><t t-esc='attachment.name' /></div>
|
||||
</a>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="message.group_message_child_ids" class="o_mg_replies">
|
||||
<h4 class="o_page_header">Follow-Ups</h4>
|
||||
<t t-call="mail_group.messages_short">
|
||||
<t t-set="messages" t-value="message.group_message_child_ids[:replies_per_page]"/>
|
||||
<t t-set="msg_more_count" t-value="len(message.group_message_child_ids) - replies_per_page"/>
|
||||
<t t-set="parent_message" t-value="message"/>
|
||||
</t>
|
||||
</div>
|
||||
<div t-if="message.group_message_parent_id">
|
||||
<h4 class="o_page_header">Reference</h4>
|
||||
<t t-call="mail_group.messages_short">
|
||||
<t t-set="messages" t-value="[message.group_message_parent_id]"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="messages_short">
|
||||
<div class="o_mg_message">
|
||||
<ul class="list-unstyled">
|
||||
<li t-foreach="messages" t-as="message" class="media mt-3">
|
||||
<div class="media-body">
|
||||
<div class="card">
|
||||
<div class="ribbon ribbon-top-right" t-if="message.moderation_status == 'pending_moderation'"><span class="bg-warning">Pending</span></div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<img t-if="message.sudo().author_id.active and message.sudo().author_id.image_128" t-att-src="image_data_uri(message.sudo().author_id.image_128)" class="rounded o_image_40_cover"/>
|
||||
<a t-attf-href="/groups/#{slug(group)}/#{slug(message)}?mode=#{mode}&date_begin=#{date_begin}&date_end=#{date_end}" t-esc="message.subject"/>
|
||||
</h5>
|
||||
<p class="card-text" t-field="message.body"/>
|
||||
<t t-call="mail_group.message_footer"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p t-if="message.group_message_child_ids" class="mt8">
|
||||
<a href="#" class="o_mg_link_hide">
|
||||
<i class="fa fa-chevron-right" role="img" aria-label="Hide replies" title="Hide replies"/> <t t-esc="len(message.group_message_child_ids)"/> replies
|
||||
</a>
|
||||
<a href="#" class="o_mg_link_show d-none">
|
||||
<i class="fa fa-chevron-down" role="img" aria-label="Show replies" title="Show replies"/> <t t-esc="len(message.group_message_child_ids)"/> replies
|
||||
</a>
|
||||
</p>
|
||||
<div class="o_mg_link_content o_mg_replies ml-5 d-none">
|
||||
<t t-call="mail_group.messages_short">
|
||||
<t t-set="messages" t-value="message.group_message_child_ids[:replies_per_page]"/>
|
||||
<t t-set="msg_more_count" t-value="len(message.group_message_child_ids) - replies_per_page"/>
|
||||
<t t-set="parent_message" t-value="message"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<p t-if="messages and (msg_more_count or 0) > 0 and parent_message">
|
||||
<button class="btn btn-link o_mg_read_more"
|
||||
t-attf-data-href="/groups/#{slug(group)}/#{slug(parent_message)}/get_replies"
|
||||
t-attf-data-last-displayed-id="#{messages[-1].id}">
|
||||
show <t t-esc="msg_more_count"/> more replies
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<template id="message_footer" name="Message Footer">
|
||||
<small>
|
||||
<span>
|
||||
by <t t-esc="message.email_from"/>
|
||||
</span>
|
||||
<span class="mx-2">-</span>
|
||||
<span>
|
||||
<i class="fa fa-calendar" role="img" aria-label="Date" title="Date"/>
|
||||
<span t-field="message.create_date"/>
|
||||
</span>
|
||||
<span class="mx-2">-</span>
|
||||
<span>
|
||||
<i class="fa fa-paperclip" role="img" aria-label="Attachments" title="Attachments"/>
|
||||
<t t-esc="len(message.sudo().attachment_ids)"/>
|
||||
</span>
|
||||
</small>
|
||||
</template>
|
||||
|
||||
<template id="confirmation_subscription" name="Mailing List Confirmation">
|
||||
<div id="wrap" class="oe_structure oe_empty">
|
||||
<div class="container">
|
||||
<p>
|
||||
You have been correctly
|
||||
<t t-if="subscribing">subscribed</t>
|
||||
<t t-if="not subscribing">unsubscribed</t>
|
||||
to the mailing list.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="invalid_token_subscription" name="Invalid Token Submitted">
|
||||
<div id="wrap" class="oe_structure oe_empty">
|
||||
<div class="container">
|
||||
<p>
|
||||
Invalid or expired confirmation link.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import mail_group_message_reject
|
||||
@@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class MailGroupMessageReject(models.TransientModel):
|
||||
_name = 'mail.group.message.reject'
|
||||
_description = 'Reject Group Message'
|
||||
|
||||
subject = fields.Char('Subject')
|
||||
body = fields.Html('Contents', default='', sanitize_style=True)
|
||||
|
||||
mail_group_message_id = fields.Many2one('mail.group.message', string="Message", required=True, readonly=True)
|
||||
|
||||
def action_send_mail(self):
|
||||
self.ensure_one()
|
||||
self.mail_group_message_id.action_moderate_reject_with_comment(self.subject, self.body)
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="mail_group_message_reject_form" model="ir.ui.view">
|
||||
<field name="name">mail.group.message.reject.form</field>
|
||||
<field name="model">mail.group.message.reject</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Reject">
|
||||
<group>
|
||||
<field name="mail_group_message_id"/>
|
||||
<field name="subject"/>
|
||||
<field name="body"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button string="Send" name="action_send_mail" type="object" class="btn-primary"/>
|
||||
<button string="Discard" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="mail_group_message_reject_action" model="ir.actions.act_window">
|
||||
<field name="name">Reject Group Message</field>
|
||||
<field name="res_model">mail.group.message.reject</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -51,7 +51,7 @@ class TestMassMailPerformance(TestMassMailPerformanceBase):
|
||||
})
|
||||
|
||||
# runbot needs +50 compared to local
|
||||
with self.assertQueryCount(__system__=1665, marketing=1665):
|
||||
with self.assertQueryCount(__system__=1715, marketing=1715):
|
||||
mailing.action_send_mail()
|
||||
|
||||
self.assertEqual(mailing.sent, 50)
|
||||
@@ -91,7 +91,7 @@ class TestMassMailBlPerformance(TestMassMailPerformanceBase):
|
||||
})
|
||||
|
||||
# runbot needs +62 compared to local
|
||||
with self.assertQueryCount(__system__=1943, marketing=1943):
|
||||
with self.assertQueryCount(__system__=2005, marketing=2005):
|
||||
mailing.action_send_mail()
|
||||
|
||||
self.assertEqual(mailing.sent, 50)
|
||||
|
||||
Reference in New Issue
Block a user