mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-22 01:11:59 +02:00
Initial commit: Core packages
This commit is contained in:
commit
12c29a983b
9512 changed files with 8379910 additions and 0 deletions
49
odoo-bringout-oca-ocb-mail_group/README.md
Normal file
49
odoo-bringout-oca-ocb-mail_group/README.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# Mail Group
|
||||
|
||||
|
||||
Manage your mailing lists from Odoo.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-ocb-mail_group
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
This addon depends on:
|
||||
- mail
|
||||
- portal
|
||||
|
||||
## Manifest Information
|
||||
|
||||
- **Name**: Mail Group
|
||||
- **Version**: 1.0
|
||||
- **Category**: N/A
|
||||
- **License**: LGPL-3
|
||||
- **Installable**: False
|
||||
|
||||
## Source
|
||||
|
||||
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `mail_group`.
|
||||
|
||||
## License
|
||||
|
||||
This package maintains the original LGPL-3 license from the upstream Odoo project.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Overview: doc/OVERVIEW.md
|
||||
- Architecture: doc/ARCHITECTURE.md
|
||||
- Models: doc/MODELS.md
|
||||
- Controllers: doc/CONTROLLERS.md
|
||||
- Wizards: doc/WIZARDS.md
|
||||
- Reports: doc/REPORTS.md
|
||||
- Security: doc/SECURITY.md
|
||||
- Install: doc/INSTALL.md
|
||||
- Usage: doc/USAGE.md
|
||||
- Configuration: doc/CONFIGURATION.md
|
||||
- Dependencies: doc/DEPENDENCIES.md
|
||||
- Troubleshooting: doc/TROUBLESHOOTING.md
|
||||
- FAQ: doc/FAQ.md
|
||||
32
odoo-bringout-oca-ocb-mail_group/doc/ARCHITECTURE.md
Normal file
32
odoo-bringout-oca-ocb-mail_group/doc/ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
U[Users] -->|HTTP| V[Views and QWeb Templates]
|
||||
V --> C[Controllers]
|
||||
V --> W[Wizards – Transient Models]
|
||||
C --> M[Models and ORM]
|
||||
W --> M
|
||||
M --> R[Reports]
|
||||
DX[Data XML] --> M
|
||||
S[Security – ACLs and Groups] -. enforces .-> M
|
||||
|
||||
subgraph Mail_group Module - mail_group
|
||||
direction LR
|
||||
M:::layer
|
||||
W:::layer
|
||||
C:::layer
|
||||
V:::layer
|
||||
R:::layer
|
||||
S:::layer
|
||||
DX:::layer
|
||||
end
|
||||
|
||||
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
|
||||
```
|
||||
|
||||
Notes
|
||||
- Views include tree/form/kanban templates and report templates.
|
||||
- Controllers provide website/portal routes when present.
|
||||
- Wizards are UI flows implemented with `models.TransientModel`.
|
||||
- Data XML loads data/demo records; Security defines groups and access.
|
||||
3
odoo-bringout-oca-ocb-mail_group/doc/CONFIGURATION.md
Normal file
3
odoo-bringout-oca-ocb-mail_group/doc/CONFIGURATION.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Configuration
|
||||
|
||||
Refer to Odoo settings for mail_group. Configure related models, access rights, and options as needed.
|
||||
17
odoo-bringout-oca-ocb-mail_group/doc/CONTROLLERS.md
Normal file
17
odoo-bringout-oca-ocb-mail_group/doc/CONTROLLERS.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Controllers
|
||||
|
||||
HTTP routes provided by this module.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as User/Client
|
||||
participant C as Module Controllers
|
||||
participant O as ORM/Views
|
||||
|
||||
U->>C: HTTP GET/POST (routes)
|
||||
C->>O: ORM operations, render templates
|
||||
O-->>U: HTML/JSON/PDF
|
||||
```
|
||||
|
||||
Notes
|
||||
- See files in controllers/ for route definitions.
|
||||
6
odoo-bringout-oca-ocb-mail_group/doc/DEPENDENCIES.md
Normal file
6
odoo-bringout-oca-ocb-mail_group/doc/DEPENDENCIES.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Dependencies
|
||||
|
||||
This addon depends on:
|
||||
|
||||
- [mail](../../odoo-bringout-oca-ocb-mail)
|
||||
- [portal](../../odoo-bringout-oca-ocb-portal)
|
||||
4
odoo-bringout-oca-ocb-mail_group/doc/FAQ.md
Normal file
4
odoo-bringout-oca-ocb-mail_group/doc/FAQ.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# FAQ
|
||||
|
||||
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
|
||||
- Q: How to enable? A: Start server with --addon mail_group or install in UI.
|
||||
7
odoo-bringout-oca-ocb-mail_group/doc/INSTALL.md
Normal file
7
odoo-bringout-oca-ocb-mail_group/doc/INSTALL.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Install
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-ocb-mail_group"
|
||||
# or
|
||||
uv pip install odoo-bringout-oca-ocb-mail_group"
|
||||
```
|
||||
15
odoo-bringout-oca-ocb-mail_group/doc/MODELS.md
Normal file
15
odoo-bringout-oca-ocb-mail_group/doc/MODELS.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Models
|
||||
|
||||
Detected core models and extensions in mail_group.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class mail_group
|
||||
class mail_group_member
|
||||
class mail_group_message
|
||||
class mail_group_moderation
|
||||
```
|
||||
|
||||
Notes
|
||||
- Classes show model technical names; fields omitted for brevity.
|
||||
- Items listed under _inherit are extensions of existing models.
|
||||
6
odoo-bringout-oca-ocb-mail_group/doc/OVERVIEW.md
Normal file
6
odoo-bringout-oca-ocb-mail_group/doc/OVERVIEW.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Overview
|
||||
|
||||
Packaged Odoo addon: mail_group. Provides features documented in upstream Odoo 16 under this addon.
|
||||
|
||||
- Source: OCA/OCB 16.0, addon mail_group
|
||||
- License: LGPL-3
|
||||
3
odoo-bringout-oca-ocb-mail_group/doc/REPORTS.md
Normal file
3
odoo-bringout-oca-ocb-mail_group/doc/REPORTS.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Reports
|
||||
|
||||
This module does not define custom reports.
|
||||
41
odoo-bringout-oca-ocb-mail_group/doc/SECURITY.md
Normal file
41
odoo-bringout-oca-ocb-mail_group/doc/SECURITY.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Security
|
||||
|
||||
Access control and security definitions in mail_group.
|
||||
|
||||
## Access Control Lists (ACLs)
|
||||
|
||||
Model access permissions defined in:
|
||||
- **[ir.model.access.csv](../mail_group/security/ir.model.access.csv)**
|
||||
- 7 model access rules
|
||||
|
||||
## Record Rules
|
||||
|
||||
Row-level security rules defined in:
|
||||
|
||||
## Security Groups & Configuration
|
||||
|
||||
Security groups and permissions defined in:
|
||||
- **[mail_group_security.xml](../mail_group/security/mail_group_security.xml)**
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Security Layers"
|
||||
A[Users] --> B[Groups]
|
||||
B --> C[Access Control Lists]
|
||||
C --> D[Models]
|
||||
B --> E[Record Rules]
|
||||
E --> F[Individual Records]
|
||||
end
|
||||
```
|
||||
|
||||
Security files overview:
|
||||
- **[ir.model.access.csv](../mail_group/security/ir.model.access.csv)**
|
||||
- Model access permissions (CRUD rights)
|
||||
- **[mail_group_security.xml](../mail_group/security/mail_group_security.xml)**
|
||||
- Security groups, categories, and XML-based rules
|
||||
|
||||
Notes
|
||||
- Access Control Lists define which groups can access which models
|
||||
- Record Rules provide row-level security (filter records by user/group)
|
||||
- Security groups organize users and define permission sets
|
||||
- All security is enforced at the ORM level by Odoo
|
||||
5
odoo-bringout-oca-ocb-mail_group/doc/TROUBLESHOOTING.md
Normal file
5
odoo-bringout-oca-ocb-mail_group/doc/TROUBLESHOOTING.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Troubleshooting
|
||||
|
||||
- Ensure Python and Odoo environment matches repo guidance.
|
||||
- Check database connectivity and logs if startup fails.
|
||||
- Validate that dependent addons listed in DEPENDENCIES.md are installed.
|
||||
7
odoo-bringout-oca-ocb-mail_group/doc/USAGE.md
Normal file
7
odoo-bringout-oca-ocb-mail_group/doc/USAGE.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Usage
|
||||
|
||||
Start Odoo including this addon (from repo root):
|
||||
|
||||
```bash
|
||||
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon mail_group
|
||||
```
|
||||
8
odoo-bringout-oca-ocb-mail_group/doc/WIZARDS.md
Normal file
8
odoo-bringout-oca-ocb-mail_group/doc/WIZARDS.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Wizards
|
||||
|
||||
Transient models exposed as UI wizards in mail_group.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class MailGroupMessageReject
|
||||
```
|
||||
6
odoo-bringout-oca-ocb-mail_group/mail_group/__init__.py
Normal file
6
odoo-bringout-oca-ocb-mail_group/mail_group/__init__.py
Normal file
|
|
@ -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
|
||||
43
odoo-bringout-oca-ocb-mail_group/mail_group/__manifest__.py
Normal file
43
odoo-bringout-oca-ocb-mail_group/mail_group/__manifest__.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# -*- 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',
|
||||
],
|
||||
'data': [
|
||||
'data/ir_cron_data.xml',
|
||||
'data/mail_templates.xml',
|
||||
'data/mail_template_data.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/css/mail_group.scss',
|
||||
'mail_group/static/src/js/*',
|
||||
],
|
||||
'web.assets_backend': [
|
||||
'mail_group/static/src/css/mail_group_backend.scss',
|
||||
],
|
||||
},
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
|
|
@ -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,387 @@
|
|||
# -*- 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.exceptions import AccessError
|
||||
from odoo.http import request, Response
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import consteq
|
||||
|
||||
|
||||
class PortalMailGroup(http.Controller):
|
||||
_thread_per_page = 20
|
||||
_replies_per_page = 5
|
||||
|
||||
def _get_website_domain(self):
|
||||
# Base group domain in addition to the security access rules
|
||||
# Do not show rejected message on the portal view even for admin
|
||||
return [('moderation_status', '!=', 'rejected')]
|
||||
|
||||
def _get_archives(self, group_id):
|
||||
"""Return the different date range and message count for the group messages."""
|
||||
domain = expression.AND([self._get_website_domain(), [('mail_group_id', '=', group_id)]])
|
||||
results = request.env['mail.group.message']._read_group_raw(
|
||||
domain,
|
||||
['subject', 'create_date'],
|
||||
groupby=['create_date'], orderby='create_date')
|
||||
|
||||
date_groups = []
|
||||
|
||||
for result in results:
|
||||
(dates_range, label) = result['create_date']
|
||||
start, end = dates_range.split('/')
|
||||
|
||||
date_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'],
|
||||
})
|
||||
|
||||
thread_domain = expression.AND([domain, [('group_message_parent_id', '=', False)]])
|
||||
threads_count = request.env['mail.group.message'].search_count(thread_domain)
|
||||
|
||||
return {
|
||||
'threads_count': threads_count,
|
||||
'threads_time_data': date_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,
|
||||
'is_mail_group_manager': request.env.user.has_group('mail_group.group_mail_group_manager'),
|
||||
})
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 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 = expression.AND([self._get_website_domain(), [('mail_group_id', '=', group.id)]])
|
||||
if mode == 'thread':
|
||||
domain = expression.AND([domain, [('group_message_parent_id', '=', False)]])
|
||||
|
||||
if date_begin and date_end:
|
||||
domain = expression.AND([domain, [('create_date', '>', date_begin), ('create_date', '<=', date_end)]])
|
||||
|
||||
# SUDO after the search to apply access rules but be able to read attachments
|
||||
messages_sudo = GroupMessage.search(
|
||||
domain, limit=self._thread_per_page,
|
||||
offset=(page - 1) * self._thread_per_page).sudo()
|
||||
|
||||
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}
|
||||
)
|
||||
|
||||
self._generate_attachments_access_token(messages_sudo)
|
||||
|
||||
return request.render('mail_group.group_messages', {
|
||||
'page_name': 'groups',
|
||||
'group': group,
|
||||
'messages': messages_sudo,
|
||||
'archives': self._get_archives(group.id),
|
||||
'date_begin': date_begin,
|
||||
'date_end': date_end,
|
||||
'pager': pager,
|
||||
'replies_per_page': self._replies_per_page,
|
||||
'mode': mode,
|
||||
})
|
||||
|
||||
@http.route('/groups/<model("mail.group"):group>/<model("mail.group.message"):message>',
|
||||
type='http', auth='public', sitemap=False, 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 = expression.AND([
|
||||
self._get_website_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)
|
||||
|
||||
message_sudo = message.sudo()
|
||||
self._generate_attachments_access_token(message_sudo)
|
||||
|
||||
values = {
|
||||
'page_name': 'groups',
|
||||
'message': message_sudo,
|
||||
'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 = expression.AND([
|
||||
self._get_website_domain(),
|
||||
[('id', '>', int(last_displayed_id)), ('group_message_parent_id', '=', message.id)],
|
||||
])
|
||||
# SUDO after the search to apply access rules but be able to read attachments
|
||||
replies_sudo = request.env['mail.group.message'].search(replies_domain, limit=self._replies_per_page).sudo()
|
||||
message_count = request.env['mail.group.message'].search_count(replies_domain)
|
||||
|
||||
if not replies_sudo:
|
||||
return
|
||||
|
||||
message_sudo = message.sudo()
|
||||
|
||||
self._generate_attachments_access_token(message_sudo | replies_sudo)
|
||||
|
||||
values = {
|
||||
'group': group,
|
||||
'parent_message': message_sudo,
|
||||
'messages': replies_sudo,
|
||||
'msg_more_count': message_count - self._replies_per_page,
|
||||
'replies_per_page': self._replies_per_page,
|
||||
}
|
||||
return request.env['ir.qweb']._render('mail_group.messages_short', values)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# SUBSCRIPTION
|
||||
# ------------------------------------------------------------
|
||||
|
||||
# csrf is disabled here because it will be called by the MUA with unpredictable session at that time
|
||||
@http.route('/group/<int:group_id>/unsubscribe_oneclick', website=True, type='http', auth='public',
|
||||
methods=['POST'], csrf=False)
|
||||
def group_unsubscribe_oneclick(self, group_id, token, email):
|
||||
""" Unsubscribe a given user from a given group. One-click unsubscribe
|
||||
allow mail user agent to propose a one click button to the user to
|
||||
unsubscribe as defined in rfc8058. Only POST method is allowed preventing
|
||||
the risk that anti-spam trigger unwanted unsubscribe (scenario explained
|
||||
in the same rfc).
|
||||
|
||||
:param int group_id: group ID from which user wants to unsubscribe;
|
||||
:param str token: optional access token ensuring security;
|
||||
:param email: email to unsubscribe;
|
||||
"""
|
||||
group_sudo = request.env['mail.group'].sudo().browse(group_id).exists()
|
||||
# new route parameters
|
||||
if group_sudo and token and email:
|
||||
correct_token = group_sudo._generate_email_access_token(email)
|
||||
if not consteq(correct_token, token):
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
group_sudo._leave_group(email)
|
||||
else:
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
return Response(status=200)
|
||||
|
||||
@http.route('/group/subscribe', type='json', auth='public', website=True)
|
||||
def group_subscribe(self, group_id=0, 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 email: Email to add in the member list
|
||||
:param token: An access token to bypass the <mail.group> access rule
|
||||
:return:
|
||||
'added'
|
||||
if the member was added in the mailing list
|
||||
'email_sent'
|
||||
if we send a confirmation email
|
||||
'is_already_member'
|
||||
if we try to subscribe but we are already member
|
||||
"""
|
||||
group_sudo, is_member, partner_id = self._group_subscription_get_group(group_id, email, token)
|
||||
|
||||
if is_member:
|
||||
return 'is_already_member'
|
||||
|
||||
if not request.env.user._is_public():
|
||||
# For logged user, automatically join / leave without sending a confirmation email
|
||||
group_sudo._join_group(request.env.user.email, partner_id)
|
||||
return 'added'
|
||||
|
||||
# For non-logged user, send an email with a token to confirm the action
|
||||
group_sudo._send_subscribe_confirmation_email(email)
|
||||
return 'email_sent'
|
||||
|
||||
@http.route('/group/unsubscribe', type='json', auth='public', website=True)
|
||||
def group_unsubscribe(self, group_id=0, email=None, token=None, **kw):
|
||||
"""Unsubscribe 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 email: Email to add in the member list
|
||||
:param token: An access token to bypass the <mail.group> access rule
|
||||
:return:
|
||||
'removed'
|
||||
if the member was removed from the mailing list
|
||||
'email_sent'
|
||||
if we send a confirmation email
|
||||
'is_not_member'
|
||||
if we try to unsubscribe but we are not member
|
||||
"""
|
||||
group_sudo, is_member, partner_id = self._group_subscription_get_group(group_id, email, token)
|
||||
|
||||
if 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
|
||||
group_sudo._leave_group(request.env.user.email, partner_id)
|
||||
return 'removed'
|
||||
|
||||
# For non-logged user, send an email with a token to confirm the action
|
||||
group_sudo._send_unsubscribe_confirmation_email(email)
|
||||
return 'email_sent'
|
||||
|
||||
def _group_subscription_get_group(self, group_id, email, token):
|
||||
"""Check the given token and return,
|
||||
|
||||
:return:
|
||||
- The group sudo-ed
|
||||
- True if the email is member of the group
|
||||
- The partner of the current user
|
||||
:raise NotFound: if the given token is not valid
|
||||
"""
|
||||
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:
|
||||
try:
|
||||
# Check that the current user has access to the group
|
||||
group.check_access_rights('read')
|
||||
group.check_access_rule('read')
|
||||
except AccessError:
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
|
||||
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))
|
||||
|
||||
return group_sudo, is_member, partner_id
|
||||
|
||||
@http.route('/group/subscribe-confirm', type='http', auth='public', website=True)
|
||||
def group_subscribe_confirm(self, group_id, email, token, **kw):
|
||||
"""Confirm the subscribe / unsubscribe action which was sent by email."""
|
||||
group = self._group_subscription_confirm_get_group(group_id, email, token, 'subscribe')
|
||||
if not group:
|
||||
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
|
||||
group._join_group(email, partner_id)
|
||||
|
||||
return request.render('mail_group.confirmation_subscription', {
|
||||
'group': group,
|
||||
'email': email,
|
||||
'subscribing': True,
|
||||
})
|
||||
|
||||
@http.route('/group/unsubscribe-confirm', type='http', auth='public', website=True)
|
||||
def group_unsubscribe_confirm(self, group_id, email, token, **kw):
|
||||
"""Confirm the subscribe / unsubscribe action which was sent by email."""
|
||||
group = self._group_subscription_confirm_get_group(group_id, email, token, 'unsubscribe')
|
||||
if not group:
|
||||
return request.render('mail_group.invalid_token_subscription')
|
||||
|
||||
group._leave_group(email, all_members=True)
|
||||
|
||||
return request.render('mail_group.confirmation_subscription', {
|
||||
'group': group,
|
||||
'email': email,
|
||||
'subscribing': False,
|
||||
})
|
||||
|
||||
def _group_subscription_confirm_get_group(self, group_id, email, token, action):
|
||||
"""Retrieve the group and check the token use to perform the given action."""
|
||||
if not group_id or not email or not token:
|
||||
return False
|
||||
# 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)
|
||||
return group if token == excepted_token else False
|
||||
|
||||
def _generate_attachments_access_token(self, messages):
|
||||
for message in messages:
|
||||
if message.attachment_ids:
|
||||
message.attachment_ids.generate_access_token()
|
||||
self._generate_attachments_access_token(message.group_message_child_ids)
|
||||
|
|
@ -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">My Company News</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,57 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="mail_template_guidelines" model="mail.template">
|
||||
<field name="name">Mail Group: Send Guidelines</field>
|
||||
<field name="model_id" ref="mail_group.model_mail_group_member"/>
|
||||
<field name="subject">Guidelines of group {{ object.mail_group_id.name }}</field>
|
||||
<field name="email_to">{{ object.email }}</field>
|
||||
<field name="description">Sent to people who subscribed to a mailing group with group guidelines</field>
|
||||
<field name="body_html" type="html">
|
||||
<div>
|
||||
<p>Hello <t t-out="object.partner_id.name or ''"></t>,</p>
|
||||
<p>Please find below the guidelines of the <t t-out="object.mail_group_id.name"></t> mailing list.</p>
|
||||
<p><t t-out="object.mail_group_id.moderation_guidelines_msg or ''"></t></p>
|
||||
</div>
|
||||
</field>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
<!-- 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="description">Subscription confirmation to a mailing group</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 t-out="object.name or ''"></strong>.
|
||||
<br/><br/>
|
||||
To confirm, please visit the following link: <strong t-if="ctx.get('token_url')"><a t-att-href="ctx['token_url']"><t t-out="ctx['token_url'] or ''"></t></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="description">Sent to people who unsubscribed from a mailing group</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 t-out="object.name or ''"></strong>.
|
||||
<br/><br/>
|
||||
To confirm, please visit the following link: <strong t-if="ctx.get('token_url')"><a t-att-href="ctx['token_url']"><t t-out="ctx['token_url'] or ''"></t></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,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="mail_group_footer" name="Mail Group: Footer">
|
||||
<div id="o_mg_message_footer">
|
||||
<p>_______________________________________________</p>
|
||||
<p>Mailing-List: <t t-esc="group_url"/></p>
|
||||
<p>Post to: <t t-esc="mailto"/></p>
|
||||
<p>Unsubscribe: <a t-att-href="unsub_url" t-esc="unsub_label"/></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="mail_group_notify_moderation">
|
||||
<div style="max-width: 600px">
|
||||
<p>Hello <t t-esc="moderator.partner_id.name"/>,</p>
|
||||
<p>You have messages to moderate, please go for the proceedings.</p>
|
||||
<p><a t-attf-href="/web#action=mail_group.mail_group_action&id={{group.id}}&view_type=form" class="o_default_snippet_text">Moderate Messages</a></p>
|
||||
<p>Thank you!</p>
|
||||
</div>
|
||||
</template>
|
||||
</data>
|
||||
</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>
|
||||
1473
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/af.po
Normal file
1473
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/af.po
Normal file
File diff suppressed because it is too large
Load diff
1469
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/am.po
Normal file
1469
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/am.po
Normal file
File diff suppressed because it is too large
Load diff
1576
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/ar.po
Normal file
1576
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/ar.po
Normal file
File diff suppressed because it is too large
Load diff
1505
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/az.po
Normal file
1505
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/az.po
Normal file
File diff suppressed because it is too large
Load diff
1473
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/be.po
Normal file
1473
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/be.po
Normal file
File diff suppressed because it is too large
Load diff
1524
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/bg.po
Normal file
1524
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/bg.po
Normal file
File diff suppressed because it is too large
Load diff
1476
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/bs.po
Normal file
1476
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/bs.po
Normal file
File diff suppressed because it is too large
Load diff
1599
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/ca.po
Normal file
1599
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/ca.po
Normal file
File diff suppressed because it is too large
Load diff
1544
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/cs.po
Normal file
1544
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/cs.po
Normal file
File diff suppressed because it is too large
Load diff
1525
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/da.po
Normal file
1525
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/da.po
Normal file
File diff suppressed because it is too large
Load diff
1592
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/de.po
Normal file
1592
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/de.po
Normal file
File diff suppressed because it is too large
Load diff
1595
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/es.po
Normal file
1595
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/es.po
Normal file
File diff suppressed because it is too large
Load diff
1594
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/es_MX.po
Normal file
1594
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/es_MX.po
Normal file
File diff suppressed because it is too large
Load diff
1535
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/et.po
Normal file
1535
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/et.po
Normal file
File diff suppressed because it is too large
Load diff
1577
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/fa.po
Normal file
1577
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/fa.po
Normal file
File diff suppressed because it is too large
Load diff
1594
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/fi.po
Normal file
1594
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/fi.po
Normal file
File diff suppressed because it is too large
Load diff
1593
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/fr.po
Normal file
1593
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/fr.po
Normal file
File diff suppressed because it is too large
Load diff
1477
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/gu.po
Normal file
1477
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/gu.po
Normal file
File diff suppressed because it is too large
Load diff
1526
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/he.po
Normal file
1526
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/he.po
Normal file
File diff suppressed because it is too large
Load diff
1486
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/hi.po
Normal file
1486
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/hi.po
Normal file
File diff suppressed because it is too large
Load diff
1515
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/hr.po
Normal file
1515
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/hr.po
Normal file
File diff suppressed because it is too large
Load diff
1531
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/hu.po
Normal file
1531
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/hu.po
Normal file
File diff suppressed because it is too large
Load diff
1469
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/hy.po
Normal file
1469
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/hy.po
Normal file
File diff suppressed because it is too large
Load diff
1583
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/id.po
Normal file
1583
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/id.po
Normal file
File diff suppressed because it is too large
Load diff
1477
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/is.po
Normal file
1477
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/is.po
Normal file
File diff suppressed because it is too large
Load diff
1592
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/it.po
Normal file
1592
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/it.po
Normal file
File diff suppressed because it is too large
Load diff
1532
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/ja.po
Normal file
1532
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/ja.po
Normal file
File diff suppressed because it is too large
Load diff
1518
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/km.po
Normal file
1518
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/km.po
Normal file
File diff suppressed because it is too large
Load diff
1546
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/ko.po
Normal file
1546
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/ko.po
Normal file
File diff suppressed because it is too large
Load diff
1498
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/lo.po
Normal file
1498
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/lo.po
Normal file
File diff suppressed because it is too large
Load diff
1519
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/lt.po
Normal file
1519
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/lt.po
Normal file
File diff suppressed because it is too large
Load diff
1512
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/lv.po
Normal file
1512
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/lv.po
Normal file
File diff suppressed because it is too large
Load diff
1476
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/mail_group.pot
Normal file
1476
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/mail_group.pot
Normal file
File diff suppressed because it is too large
Load diff
1474
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/ml.po
Normal file
1474
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/ml.po
Normal file
File diff suppressed because it is too large
Load diff
1523
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/mn.po
Normal file
1523
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/mn.po
Normal file
File diff suppressed because it is too large
Load diff
1478
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/ms.po
Normal file
1478
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/ms.po
Normal file
File diff suppressed because it is too large
Load diff
1493
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/nb.po
Normal file
1493
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/nb.po
Normal file
File diff suppressed because it is too large
Load diff
1585
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/nl.po
Normal file
1585
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/nl.po
Normal file
File diff suppressed because it is too large
Load diff
1469
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/no.po
Normal file
1469
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/no.po
Normal file
File diff suppressed because it is too large
Load diff
1592
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/pl.po
Normal file
1592
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/pl.po
Normal file
File diff suppressed because it is too large
Load diff
1522
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/pt.po
Normal file
1522
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/pt.po
Normal file
File diff suppressed because it is too large
Load diff
1591
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/pt_BR.po
Normal file
1591
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/pt_BR.po
Normal file
File diff suppressed because it is too large
Load diff
1565
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/ro.po
Normal file
1565
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/ro.po
Normal file
File diff suppressed because it is too large
Load diff
1590
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/ru.po
Normal file
1590
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/ru.po
Normal file
File diff suppressed because it is too large
Load diff
1509
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/sk.po
Normal file
1509
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/sk.po
Normal file
File diff suppressed because it is too large
Load diff
1554
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/sl.po
Normal file
1554
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/sl.po
Normal file
File diff suppressed because it is too large
Load diff
1469
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/sq.po
Normal file
1469
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/sq.po
Normal file
File diff suppressed because it is too large
Load diff
1546
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/sr.po
Normal file
1546
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/sr.po
Normal file
File diff suppressed because it is too large
Load diff
1574
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/sv.po
Normal file
1574
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/sv.po
Normal file
File diff suppressed because it is too large
Load diff
1469
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/sw.po
Normal file
1469
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/sw.po
Normal file
File diff suppressed because it is too large
Load diff
1469
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/ta.po
Normal file
1469
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/ta.po
Normal file
File diff suppressed because it is too large
Load diff
1557
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/th.po
Normal file
1557
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/th.po
Normal file
File diff suppressed because it is too large
Load diff
1557
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/tr.po
Normal file
1557
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/tr.po
Normal file
File diff suppressed because it is too large
Load diff
1582
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/uk.po
Normal file
1582
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/uk.po
Normal file
File diff suppressed because it is too large
Load diff
1576
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/vi.po
Normal file
1576
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/vi.po
Normal file
File diff suppressed because it is too large
Load diff
1544
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/zh_CN.po
Normal file
1544
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/zh_CN.po
Normal file
File diff suppressed because it is too large
Load diff
1535
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/zh_TW.po
Normal file
1535
odoo-bringout-oca-ocb-mail_group/mail_group/i18n/zh_TW.po
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
739
odoo-bringout-oca-ocb-mail_group/mail_group/models/mail_group.py
Normal file
739
odoo-bringout-oca-ocb-mail_group/mail_group/models/mail_group.py
Normal file
|
|
@ -0,0 +1,739 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import lxml
|
||||
|
||||
from ast import literal_eval
|
||||
from datetime import datetime
|
||||
from dateutil import relativedelta
|
||||
from werkzeug import urls
|
||||
|
||||
from odoo import _, api, fields, models, tools
|
||||
from odoo.addons.http_routing.models.ir_http import slug
|
||||
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__)
|
||||
|
||||
# TODO remove me master
|
||||
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 = 'Mail Group'
|
||||
# TDE CHECK: use blaclist mixin
|
||||
_inherit = ['mail.alias.mixin']
|
||||
_order = 'create_date DESC, id DESC'
|
||||
|
||||
@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
|
||||
|
||||
active = fields.Boolean('Active', default=True)
|
||||
name = fields.Char('Name', required=True, translate=True)
|
||||
alias_name = fields.Char('Alias Name', copy=False, related='alias_id.alias_name', readonly=False)
|
||||
alias_fullname = fields.Char('Alias Full Name', compute='_compute_alias_fullname')
|
||||
description = fields.Text('Description')
|
||||
image_128 = fields.Image('Image', max_width=128, max_height=128)
|
||||
# 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_count = fields.Integer('Messages Count', help='Number of message in this group', compute='_compute_mail_group_message_count')
|
||||
mail_group_message_moderation_count = fields.Integer('Pending Messages Count', help='Messages that need an action', compute='_compute_mail_group_message_moderation_count')
|
||||
# Members
|
||||
is_member = fields.Boolean('Is Member', compute='_compute_is_member')
|
||||
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.Html(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.Html(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_group = fields.Boolean('Can Manage', help='Can manage the members', compute='_compute_can_manage_group')
|
||||
|
||||
@api.depends('alias_name', 'alias_domain')
|
||||
def _compute_alias_fullname(self):
|
||||
for group in self:
|
||||
if group.alias_name and group.alias_domain:
|
||||
group.alias_fullname = f'{group.alias_name}@{group.alias_domain}'
|
||||
else:
|
||||
group.alias_fullname = group.alias_name
|
||||
|
||||
@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([
|
||||
('mail_group_id', 'in', self.ids),
|
||||
('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')
|
||||
def _compute_mail_group_message_count(self):
|
||||
if not self:
|
||||
self.mail_group_message_count = 0
|
||||
return
|
||||
|
||||
results = self.env['mail.group.message']._read_group(
|
||||
[('mail_group_id', 'in', self.ids)],
|
||||
['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_count = result_per_group.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(
|
||||
[('mail_group_id', 'in', self.ids), ('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, 0)
|
||||
|
||||
@api.depends('member_ids')
|
||||
def _compute_member_count(self):
|
||||
for group in self:
|
||||
group.member_count = len(group.member_ids)
|
||||
|
||||
@api.depends_context('uid')
|
||||
def _compute_is_member(self):
|
||||
if not self or self.env.user._is_public():
|
||||
self.is_member = False
|
||||
return
|
||||
|
||||
# SUDO to bypass the ACL rules
|
||||
members = self.env['mail.group.member'].sudo().search([
|
||||
('partner_id', '=', self.env.user.partner_id.id),
|
||||
('mail_group_id', 'in', self.ids),
|
||||
])
|
||||
is_member = {member.mail_group_id.id: True for member in members}
|
||||
|
||||
for group in self:
|
||||
group.is_member = is_member.get(group.id, False)
|
||||
|
||||
@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_group(self):
|
||||
is_admin = self.env.user.has_group('mail_group.group_mail_group_manager') or self.env.su
|
||||
for group in self:
|
||||
group.can_manage_group = 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):
|
||||
""" Checks for access errors related to sending email to the mailing list.
|
||||
Returns None if the mailing list is public or if no error cases are detected. """
|
||||
self.ensure_one()
|
||||
|
||||
# Error Case: Selected group of users, but no user found for that email
|
||||
email = email_normalize(message_dict.get('email_from', ''))
|
||||
email_has_access = self.search_count([('id', '=', self.id), ('access_group_id.users.email_normalized', '=', email)])
|
||||
if self.access_mode == 'groups' and not email_has_access:
|
||||
return _('Only selected groups of users can send email to the mailing list.')
|
||||
|
||||
# Error Case: Access for members, but no member found for that email
|
||||
elif self.access_mode == 'members' and not self._find_member(message_dict.get('email_from')):
|
||||
return _('Only members can send email to the mailing list.')
|
||||
|
||||
return None
|
||||
|
||||
@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 the mail gateway 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_on_email=True)
|
||||
|
||||
values.update({
|
||||
'author_id': author_id,
|
||||
'body': self._clean_email_body(body),
|
||||
'email_from': email_from,
|
||||
'model': self._name,
|
||||
'partner_ids': [],
|
||||
'res_id': self.id,
|
||||
'subject': subject,
|
||||
})
|
||||
|
||||
# Force the "reply-to" to make the mail group flow work
|
||||
values['reply_to'] = self.env['mail.message']._get_reply_to(values)
|
||||
|
||||
# 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_template_guidelines', raise_if_not_found=False)
|
||||
if not template:
|
||||
raise UserError(_('Template "mail_group.mail_template_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)
|
||||
|
||||
for member in members:
|
||||
company = member.partner_id.company_id or self.env.company
|
||||
template.send_mail(
|
||||
member.id,
|
||||
email_values={
|
||||
'author_id': self.env.user.partner_id.id,
|
||||
'email_from': company.email_formatted or company.catchall_formatted,
|
||||
'reply_to': company.email_formatted or company.catchall_formatted,
|
||||
},
|
||||
)
|
||||
|
||||
_logger.info('Send guidelines to %i members', len(members))
|
||||
|
||||
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)
|
||||
|
||||
# Email added in a dict to be sure to send only once the email to each address
|
||||
member_emails = {
|
||||
email_normalize(member.email): member.email
|
||||
for member in self.member_ids
|
||||
}
|
||||
|
||||
batch_size = int(self.env['ir.config_parameter'].sudo().get_param('mail.session.batch.size', GROUP_SEND_BATCH_SIZE))
|
||||
for batch_email_member in tools.split_every(batch_size, member_emails.items()):
|
||||
mail_values = []
|
||||
for email_member_normalized, email_member in batch_email_member:
|
||||
if email_member_normalized == message.email_from_normalized:
|
||||
# Do not send the email to their author
|
||||
continue
|
||||
|
||||
# SMTP headers related to the subscription
|
||||
email_url_encoded = urls.url_quote(email_member)
|
||||
unsubscribe_url = self._get_email_unsubscribe_url(email_member_normalized)
|
||||
|
||||
headers = {
|
||||
** self._notify_by_email_get_headers(),
|
||||
'List-Archive': f'<{base_url}/groups/{slug(self)}>',
|
||||
'List-Subscribe': f'<{base_url}/groups?email={email_url_encoded}>',
|
||||
'List-Unsubscribe': f'<{unsubscribe_url}>',
|
||||
'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click',
|
||||
'Precedence': 'list',
|
||||
'X-Auto-Response-Suppress': 'OOF', # avoid out-of-office replies from MS Exchange
|
||||
}
|
||||
if self.alias_name and self.alias_domain:
|
||||
headers.update({
|
||||
'List-Id': f'<{self.alias_name}.{self.alias_domain}>',
|
||||
'List-Post': f'<mailto:{self.alias_name}@{self.alias_domain}>',
|
||||
'X-Forge-To': f'"{self.name}" <{self.alias_name}@{self.alias_domain}>',
|
||||
})
|
||||
|
||||
if message.mail_message_id.parent_id:
|
||||
headers['In-Reply-To'] = message.mail_message_id.parent_id.message_id
|
||||
|
||||
# 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_label': f'{base_url}/groups?unsubscribe',
|
||||
'unsub_url': unsubscribe_url,
|
||||
}
|
||||
footer = self.env['ir.qweb']._render('mail_group.mail_group_footer', template_values, minimal_qcontext=True)
|
||||
member_body = tools.append_content_to_html(body, footer, plaintext=False)
|
||||
|
||||
mail_values.append({
|
||||
'auto_delete': True,
|
||||
'attachment_ids': message.attachment_ids.ids,
|
||||
'body_html': member_body,
|
||||
'email_from': message.email_from,
|
||||
'email_to': email_member,
|
||||
'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(
|
||||
[('mail_group_id', 'in', self.ids), ('moderation_status', '=', 'pending_moderation')],
|
||||
['mail_group_id'],
|
||||
['mail_group_id'],
|
||||
)
|
||||
groups = self.browse([result['mail_group_id'][0] for result in results])
|
||||
|
||||
for group in groups:
|
||||
moderators_to_notify = group.moderator_ids
|
||||
MailThread = self.env['mail.thread'].with_context(mail_notify_author=True)
|
||||
for moderator in moderators_to_notify:
|
||||
body = self.env['ir.qweb']._render('mail_group.mail_group_notify_moderation', {
|
||||
'moderator': moderator,
|
||||
'group': group,
|
||||
}, 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,
|
||||
model='mail.group',
|
||||
res_id=group.id,
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _clean_email_body(self, body_html):
|
||||
"""When we receive an email, we want to clean it before storing it in the database."""
|
||||
tree = lxml.html.fromstring(body_html or '')
|
||||
# Remove the mailing footer
|
||||
xpath_footer = ".//div[contains(@id, 'o_mg_message_footer')]"
|
||||
for parent_footer in tree.xpath(xpath_footer + "/.."):
|
||||
for footer in parent_footer.xpath(xpath_footer):
|
||||
parent_footer.remove(footer)
|
||||
|
||||
return lxml.etree.tostring(tree, encoding='utf-8').decode()
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# MEMBERSHIP
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def action_join(self):
|
||||
self.check_access_rights('read')
|
||||
self.check_access_rule('read')
|
||||
partner = self.env.user.partner_id
|
||||
self.sudo()._join_group(partner.email, partner.id)
|
||||
|
||||
_logger.info('"%s" (#%s) joined mail.group "%s" (#%s)', partner.name, partner.id, self.name, self.id)
|
||||
|
||||
def action_leave(self):
|
||||
self.check_access_rights('read')
|
||||
self.check_access_rule('read')
|
||||
partner = self.env.user.partner_id
|
||||
self.sudo()._leave_group(partner.email, partner.id)
|
||||
|
||||
_logger.info('"%s" (#%s) leaved mail.group "%s" (#%s)', partner.name, partner.id, self.name, self.id)
|
||||
|
||||
def _join_group(self, email, partner_id=None):
|
||||
self.ensure_one()
|
||||
|
||||
if partner_id:
|
||||
partner = self.env['res.partner'].browse(partner_id).exists()
|
||||
if not partner:
|
||||
raise ValidationError(_('The partner can not be found.'))
|
||||
email = partner.email
|
||||
|
||||
existing_member = self._find_member(email, partner_id)
|
||||
if existing_member:
|
||||
# Update the information of the partner to force the synchronization
|
||||
# If one the value is not up to date (e.g. if our email is subscribed
|
||||
# but our partner was not set)
|
||||
existing_member.write({
|
||||
'email': email,
|
||||
'partner_id': partner_id,
|
||||
})
|
||||
return
|
||||
|
||||
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, all_members=False):
|
||||
"""Remove the given email / partner from the group.
|
||||
|
||||
If the "all_members" parameter is set to True, remove all members with the given
|
||||
email address (multiple members might have the same email address).
|
||||
|
||||
Otherwise, remove the most appropriate.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if all_members and not partner_id:
|
||||
self.env['mail.group.member'].search([
|
||||
('mail_group_id', '=', self.id),
|
||||
('email_normalized', '=', email_normalize(email)),
|
||||
]).unlink()
|
||||
else:
|
||||
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,
|
||||
email_layout_xmlid='mail.mail_notification_light',
|
||||
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',
|
||||
},
|
||||
force_send=True,
|
||||
)
|
||||
_logger.info('Subscription email sent to %s.', email)
|
||||
|
||||
def _send_unsubscribe_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, 'unsubscribe')
|
||||
|
||||
template = self.env.ref('mail_group.mail_template_list_unsubscribe')
|
||||
template.with_context(token_url=confirm_action_url).send_mail(
|
||||
self.id,
|
||||
email_layout_xmlid='mail.mail_notification_light',
|
||||
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',
|
||||
},
|
||||
force_send=True,
|
||||
)
|
||||
_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 = '/group/%s-confirm?%s' % (
|
||||
action,
|
||||
urls.url_encode({
|
||||
'group_id': self.id,
|
||||
'email': email,
|
||||
'token': self._generate_action_token(email, 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_email_access_token(self, email):
|
||||
"""Generate an action token to be able to unsubscribe from the mailing
|
||||
list, while hashing the target email to avoid spoofind other emails.
|
||||
|
||||
:param str email: email included in hash, should be normalized
|
||||
"""
|
||||
return tools.hmac(self.env(su=True), 'mail_group-access-token-portal-email', (self.id, email))
|
||||
|
||||
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 _get_email_unsubscribe_url(self, email_to):
|
||||
params = urls.url_encode({
|
||||
'email': email_to,
|
||||
'token': self._generate_email_access_token(email_to),
|
||||
})
|
||||
return urls.url_join(
|
||||
self.get_base_url(),
|
||||
f'group/{self.id}/unsubscribe_oneclick?{params}'
|
||||
)
|
||||
|
||||
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>}
|
||||
|
||||
Multiple members might have the same email address, but with different partner
|
||||
because there's no unique constraint on the email field of the <res.partner>
|
||||
model.
|
||||
|
||||
When a partner is given for the search, return in priority
|
||||
- The member whose partner match the given partner
|
||||
- The member without partner but whose email match the given email
|
||||
|
||||
When no partner is given for the search, return in priority
|
||||
- A member whose email match the given email and has no partner
|
||||
- A member whose email match the given email and has partner
|
||||
"""
|
||||
order = 'partner_id ASC'
|
||||
if not email_normalize(email):
|
||||
# empty email should match nobody
|
||||
return {}
|
||||
|
||||
domain = [('email_normalized', '=', email_normalize(email))]
|
||||
if partner_id:
|
||||
domain = expression.OR([
|
||||
expression.AND([
|
||||
[('partner_id', '=', False)],
|
||||
domain,
|
||||
]),
|
||||
[('partner_id', '=', partner_id)],
|
||||
])
|
||||
order = 'partner_id DESC'
|
||||
|
||||
domain = expression.AND([domain, [('mail_group_id', 'in', self.ids)]])
|
||||
members_data = self.env['mail.group.member'].sudo().search(domain, order=order)
|
||||
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,247 @@
|
|||
# -*- 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.exceptions import AccessError, UserError
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import email_normalize, append_content_to_html, ustr
|
||||
|
||||
_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'
|
||||
_primary_email = 'email_from'
|
||||
|
||||
# <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)
|
||||
group_message_child_ids = fields.One2many('mail.group.message', 'group_message_parent_id', string='Children')
|
||||
# Moderation
|
||||
author_moderation = fields.Selection([('ban', 'Banned'), ('allow', 'Whitelisted')], string='Author Moderation Status',
|
||||
compute='_compute_author_moderation')
|
||||
is_group_moderated = fields.Boolean('Is Group Moderated', related='mail_group_id.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')
|
||||
create_date = fields.Datetime(string='Posted')
|
||||
|
||||
@api.depends('email_from')
|
||||
def _compute_email_from_normalized(self):
|
||||
for message in self:
|
||||
message.email_from_normalized = email_normalize(message.email_from)
|
||||
|
||||
@api.depends('email_from_normalized', 'mail_group_id')
|
||||
def _compute_author_moderation(self):
|
||||
moderations = self.env['mail.group.moderation'].search([
|
||||
('mail_group_id', 'in', self.mail_group_id.ids),
|
||||
])
|
||||
all_emails = set(self.mapped('email_from_normalized'))
|
||||
moderations = {
|
||||
(moderation.mail_group_id, moderation.email): moderation.status
|
||||
for moderation in moderations
|
||||
if moderation.email in all_emails
|
||||
}
|
||||
for message in self:
|
||||
message.author_moderation = moderations.get((message.mail_group_id, message.email_from_normalized), False)
|
||||
|
||||
@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_moderate_allow(self):
|
||||
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_moderate_ban(self):
|
||||
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 action_moderate_ban_with_comment(self, ban_subject, ban_comment):
|
||||
self._create_moderation_rule('ban')
|
||||
|
||||
if ban_subject or ban_comment:
|
||||
self._moderate_send_reject_email(ban_subject, ban_comment)
|
||||
|
||||
# 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
|
||||
|
||||
body_html = append_content_to_html('<div>%s</div>' % 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,148 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="mail_group_rule_read_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', user.groups_id.ids),
|
||||
'&',
|
||||
('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="False"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
<record id="mail_group_rule_write_all" model="ir.rule">
|
||||
<field name="name">Mail Group: Moderator have write access on their group</field>
|
||||
<field name="model_id" ref="model_mail_group"/>
|
||||
<field name="domain_force">[('moderator_ids', 'in', user.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_read" eval="False"/>
|
||||
<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', user.groups_id.ids),
|
||||
'&',
|
||||
('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', user.groups_id.ids),
|
||||
'&',
|
||||
('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)]</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,44 @@
|
|||
.o_mg_subscribe_btn {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.o_mg_page_description {
|
||||
background-image: url(/mail_group/static/src/img/mail_group_portal.jpg);
|
||||
}
|
||||
|
||||
.o_mg_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;
|
||||
}
|
||||
}
|
||||
|
||||
.o_mg_message *[data-o-mail-quote]:not(.visible) {
|
||||
// Hide the quotation of the parent email in the body
|
||||
// The "visible" class with be toggled in JS
|
||||
display: none!important;
|
||||
}
|
||||
|
||||
.o_mg_attachment {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.o_mg_message {
|
||||
min-width: 250px;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
.o_mail_group_message_form.o_xxs_form_view {
|
||||
.o_group tr td {
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.o_mg_description {
|
||||
height: 20px;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
|
|
@ -0,0 +1,92 @@
|
|||
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;
|
||||
const searchParams = (new URL(document.location.href)).searchParams;
|
||||
this.token = searchParams.get('token');
|
||||
this.forceUnsubscribe = searchParams.has('unsubscribe');
|
||||
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, .form-select').addClass('is-invalid');
|
||||
return false;
|
||||
}
|
||||
|
||||
this.$target.removeClass('o_has_error').find('.form-control, .form-select').removeClass('is-invalid');
|
||||
|
||||
const action = (this.isMember || this.forceUnsubscribe) ? 'unsubscribe' : 'subscribe';
|
||||
|
||||
const response = await this._rpc({
|
||||
route: '/group/' + action,
|
||||
params: {
|
||||
'group_id': this.mailgroupId,
|
||||
'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')).removeClass('btn-primary').addClass('btn-outline-primary');
|
||||
} else if (response === 'removed') {
|
||||
this.isMember = false;
|
||||
this.$target.find('.o_mg_subscribe_btn').text(_t('Subscribe')).removeClass('btn-outline-primary').addClass('btn-primary');
|
||||
} 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 instructions has been sent.'))
|
||||
);
|
||||
} else if (response === 'is_already_member') {
|
||||
this.isMember = true;
|
||||
this.$target.find('.o_mg_subscribe_btn').text(_t('Unsubscribe')).removeClass('btn-primary').addClass('btn-outline-primary');
|
||||
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') {
|
||||
if (!this.forceUnsubscribe) {
|
||||
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.'))
|
||||
);
|
||||
}
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
return publicWidget.registry.MailGroup;
|
||||
});
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
odoo.define('mail_group.mail_group_message', function (require) {
|
||||
'use strict';
|
||||
|
||||
const publicWidget = require('web.public.widget');
|
||||
|
||||
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-light btn-sm ms-1"/>').text('. . .');
|
||||
quoted.first().before(readMore);
|
||||
readMore.on('click', () => {
|
||||
quoted.toggleClass('visible');
|
||||
});
|
||||
|
||||
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.closest('.o_mg_link_parent');
|
||||
$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.closest('.o_mg_link_parent');
|
||||
$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').first();
|
||||
if ($threadContainer) {
|
||||
const $data = $(data);
|
||||
const $lastMsg = $threadContainer.children('li.media').last();
|
||||
const $newMessages = $data.find('ul.list-unstyled').first().children('li.media');
|
||||
$newMessages.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 test_mail_group
|
||||
from . import test_mail_group_mailing
|
||||
from . import test_mail_group_message
|
||||
from . import test_mail_group_moderation
|
||||
80
odoo-bringout-oca-ocb-mail_group/mail_group/tests/common.py
Normal file
80
odoo-bringout-oca-ocb-mail_group/mail_group/tests/common.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# -*- 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>',
|
||||
})
|
||||
|
||||
cls.user_portal = cls._create_portal_user()
|
||||
44
odoo-bringout-oca-ocb-mail_group/mail_group/tests/data.py
Normal file
44
odoo-bringout-oca-ocb-mail_group/mail_group/tests/data.py
Normal file
|
|
@ -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,254 @@
|
|||
# -*- 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, AccessError
|
||||
from odoo.tests.common import tagged, users
|
||||
from odoo.tools import mute_logger, append_content_to_html
|
||||
|
||||
|
||||
@tagged("mail_group")
|
||||
class TestMailGroup(TestMailListCommon):
|
||||
|
||||
def test_clean_email_body(self):
|
||||
footer = self.env['ir.qweb']._render('mail_group.mail_group_footer', {'group_url': 'Test remove footer'}, minimal_qcontext=True)
|
||||
body = append_content_to_html("<div>Test email body</div>", footer, plaintext=False)
|
||||
|
||||
result = self.env['mail.group']._clean_email_body(body)
|
||||
self.assertIn('Test email body', result, 'Should have kept the original email body')
|
||||
self.assertNotIn('Test remove footer', result, 'Should have removed the mailing list footer')
|
||||
self.assertNotIn('o_mg_message_footer', result, 'Should have removed the entire HTML element')
|
||||
|
||||
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
|
||||
|
||||
def test_find_group_user_for_alias(self):
|
||||
"""Check for mail incoming from an allowed group. Specifically for a situation where
|
||||
the sender is a part of the allowed USER group, but is NOT a member of the mailing list."""
|
||||
group_user_not_member = mail_new_test_user(self.env, login='group user not member', email="group_user_not_member@example.com")
|
||||
|
||||
self.assertIn(group_user_not_member, self.test_group.access_group_id.users,
|
||||
"User, that sends e-mail, must be part of the access group (in this test scenario)")
|
||||
self.assertNotIn(group_user_not_member.id, self.test_group.member_ids.ids,
|
||||
"User, that sends e-mail, shan't be a member of the mail group (in this test scenario)")
|
||||
|
||||
self.test_group.alias_id.alias_contact = 'followers'
|
||||
self.test_group.access_mode = 'groups'
|
||||
err_msg = self.test_group._alias_get_error_message({}, {'email_from': group_user_not_member.email}, self.test_group.alias_id)
|
||||
self.assertFalse(err_msg, "Mail with sender belonging to allowed user group (not a member of the mail group) was rejected")
|
||||
|
||||
def test_find_member(self):
|
||||
"""Test the priority to retrieve a member of a mail group from a partner_id
|
||||
and an email address.
|
||||
|
||||
When a partner is given for the search, return in priority
|
||||
- The member whose partner match the given partner
|
||||
- The member without partner but whose email match the given email
|
||||
|
||||
When no partner is given for the search, return in priority
|
||||
- A member whose email match the given email and has no partner
|
||||
- A member whose email match the given email and has partner
|
||||
"""
|
||||
member_1 = self.test_group_member_1
|
||||
email = member_1.email_normalized
|
||||
|
||||
partner_2 = self.user_portal.partner_id
|
||||
partner_2.email = '"Bob" <%s>' % email
|
||||
member_2 = self.env['mail.group.member'].create({
|
||||
'partner_id': partner_2.id,
|
||||
'mail_group_id': self.test_group.id,
|
||||
})
|
||||
|
||||
partner_3 = self.partner_root
|
||||
partner_3.email = '"Bob" <%s>' % email
|
||||
member_3 = self.env['mail.group.member'].create({
|
||||
'partner_id': partner_3.id,
|
||||
'mail_group_id': self.test_group.id,
|
||||
})
|
||||
|
||||
self.env['mail.group.member'].create({
|
||||
'email': "Alice",
|
||||
'mail_group_id': self.test_group.id,
|
||||
})
|
||||
|
||||
member = self.test_group._find_member(email)
|
||||
self.assertEqual(member, member_1, 'When no partner is provided, return the member without partner in priority')
|
||||
|
||||
member = self.test_group._find_member(email, partner_2.id)
|
||||
self.assertEqual(member, member_2, 'Should return the member with the right partner')
|
||||
|
||||
member = self.test_group._find_member(email, partner_3.id)
|
||||
self.assertEqual(member, member_3, 'Should return the member with the right partner')
|
||||
|
||||
member_2.unlink()
|
||||
member = self.test_group._find_member(email, partner_2.id)
|
||||
self.assertEqual(member, member_1, 'Should return the member without partner')
|
||||
|
||||
member_1.unlink()
|
||||
member = self.test_group._find_member(email, partner_2.id)
|
||||
self.assertFalse(member, 'Should not return any member because the only one with the same email has a different partner')
|
||||
|
||||
member = self.test_group._find_member('', None)
|
||||
self.assertEqual(member, None, 'When no email nor partner is provided, return nobody')
|
||||
|
||||
|
||||
def test_find_member_for_alias(self):
|
||||
"""Test the matching of a mail_group.members, when 2 users have the same partner email, and
|
||||
that the first user was subscribed."""
|
||||
user = self.user_portal
|
||||
user2 = mail_new_test_user(self.env, login='login_2', email=user.email)
|
||||
|
||||
member = self.env['mail.group.member'].create({
|
||||
# subscribe with the first user
|
||||
'partner_id': user.partner_id.id,
|
||||
'mail_group_id': self.test_group.id,
|
||||
})
|
||||
self.assertEqual(member.email, user.email)
|
||||
|
||||
# In case of matching, function return a falsy value.
|
||||
# Should not return string (exception) if at least one members have the same email, whatever
|
||||
# the partner (author_id) that could match this email.
|
||||
msg_dict = {
|
||||
# send mail with the second user
|
||||
'author_id': user2.partner_id.id,
|
||||
'email_from': user2.email,
|
||||
}
|
||||
self.test_group.alias_id.alias_contact = 'followers'
|
||||
self.assertFalse(self.test_group._alias_get_error_message({}, msg_dict, self.test_group.alias_id))
|
||||
|
||||
@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)
|
||||
self.assertTrue(mail_group._find_member('"Test" <jack@test.com>'))
|
||||
|
||||
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')
|
||||
|
||||
# Join a group with a different email than the partner
|
||||
portal_partner = self.user_portal.partner_id
|
||||
mail_group._join_group('"Bob" <email_different_than_partner@test.com>', portal_partner.id)
|
||||
self.assertEqual(len(mail_group.member_ids), 6, 'Should have added the new member')
|
||||
member = mail_group._find_member('email_different_than_partner@test.com', portal_partner.id)
|
||||
self.assertTrue(member)
|
||||
self.assertEqual(member.partner_id, portal_partner, 'Should have set the partner')
|
||||
self.assertEqual(member.email, portal_partner.email, 'Should have force the email to the email of the partner')
|
||||
self.assertEqual(member.email_normalized, portal_partner.email_normalized)
|
||||
|
||||
portal_partner.email = 'new_portal_email@example.com'
|
||||
self.assertEqual(member.email, 'new_portal_email@example.com', 'Should have change the email of the partner')
|
||||
self.assertEqual(member.email_normalized, 'new_portal_email@example.com')
|
||||
|
||||
@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')
|
||||
with self.assertRaises(AccessError, msg='Only moderator / responsible and admin can write on the group'):
|
||||
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')
|
||||
with self.assertRaises(AccessError, msg='Only moderator / responsible and admin can write on the group'):
|
||||
mail_group.with_user(self.user_employee_2).check_access_rule('write')
|
||||
|
||||
mail_group.moderator_ids |= self.user_employee_2
|
||||
mail_group.with_user(self.user_employee_2).check_access_rule('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'
|
||||
partner = self.user_employee_2.partner_id
|
||||
self.assertNotIn(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_employee_2).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': partner.id,
|
||||
})]})
|
||||
self.assertIn(partner, mail_group.member_partner_ids)
|
||||
|
||||
# Now that portal is in the member list they should have access
|
||||
mail_group.with_user(self.user_employee_2).check_access_rule('read')
|
||||
with self.assertRaises(AccessError, msg='Only moderator / responsible and admin can write on the group'):
|
||||
mail_group.with_user(self.user_employee_2).check_access_rule('write')
|
||||
|
||||
mail_group.moderator_ids |= self.user_employee_2
|
||||
mail_group.with_user(self.user_employee_2).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'):
|
||||
member.with_user(self.user_portal).check_access_rule('read')
|
||||
member.with_user(self.user_portal).check_access_rights('read')
|
||||
|
||||
with self.assertRaises(AccessError, msg='Non moderators should not have access to member'):
|
||||
member.with_user(self.user_portal).check_access_rule('read')
|
||||
member.with_user(self.user_portal).check_access_rights('read')
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
from ast import literal_eval
|
||||
|
||||
from odoo.addons.mail_group.tests.common import TestMailListCommon
|
||||
from odoo.tests.common import HttpCase, tagged, users
|
||||
|
||||
|
||||
@tagged("mail_group", "mail_mail", "post_install", "-at_install")
|
||||
class TestMailGroupMailing(TestMailListCommon, HttpCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.test_group.moderation = False
|
||||
|
||||
@users("employee")
|
||||
def test_mail_mail_headers(self):
|
||||
""" Test headers notably unsubscribe headers """
|
||||
test_group = self.test_group.with_env(self.env)
|
||||
# don't contact yourself, banned people receive outgoing emails
|
||||
expected_recipients = self.test_group_member_1 + self.test_group_member_2 + self.test_group_member_3_banned
|
||||
|
||||
with self.mock_mail_gateway(mail_unlink_sent=False):
|
||||
test_group.message_post(
|
||||
body="<p>Test Body</p>",
|
||||
)
|
||||
|
||||
self.assertEqual(len(self._new_mails), len(expected_recipients))
|
||||
|
||||
for member in expected_recipients:
|
||||
mail = self._find_mail_mail_wemail(member.email, "outgoing")
|
||||
unsubscribe_url = literal_eval(mail.headers).get("List-Unsubscribe").strip('<>')
|
||||
_response = self.opener.post(unsubscribe_url)
|
||||
|
||||
self.assertEqual(test_group.member_ids, self.test_group_member_4_emp,
|
||||
"Mail Group: people should have been unsubscribed")
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
# -*- 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):
|
||||
|
||||
def test_batch_send(self):
|
||||
"""Test that when someone sends an email to a large group that it is
|
||||
delivered exactly to those people"""
|
||||
self.test_group.write({
|
||||
'access_mode': 'members',
|
||||
'alias_contact': 'followers',
|
||||
'moderation': False,
|
||||
})
|
||||
self.test_group.member_ids.unlink()
|
||||
|
||||
for num in range(42):
|
||||
self.env['mail.group.member'].create({
|
||||
'email': f'emu-{num}@example.com',
|
||||
'mail_group_id': self.test_group.id,
|
||||
})
|
||||
|
||||
self.assertEqual(len(self.test_group.member_ids), 42)
|
||||
|
||||
# force a batch split with a low limit
|
||||
self.env['ir.config_parameter'].sudo().set_param('mail.session.batch.size', 10)
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
self.format_and_process(
|
||||
GROUP_TEMPLATE, self.test_group.member_ids[0].email,
|
||||
self.test_group.alias_id.display_name,
|
||||
subject='Never Surrender', msg_id='<glory.to.the.hypnotoad@localhost>', target_model='mail.group')
|
||||
|
||||
message = self.env['mail.group.message'].search([('mail_message_id.message_id', '=', '<glory.to.the.hypnotoad@localhost>')])
|
||||
self.assertEqual(message.subject, 'Never Surrender', 'Should have created a <mail.group.message>')
|
||||
|
||||
mails = self.env['mail.mail'].search([('mail_message_id', '=', message.mail_message_id.id)])
|
||||
|
||||
# 42 -1 as the sender doesn't get an email
|
||||
self.assertEqual(len(mails), 41, 'Should have send one and only one email per recipient')
|
||||
|
||||
@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), len(self.test_group.member_ids) - 1)
|
||||
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_recordset()
|
||||
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_recordset()
|
||||
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_recordset()
|
||||
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_recordset()
|
||||
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')
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.addons.mail_group.models.mail_group_message')
|
||||
def test_email_empty_from(self):
|
||||
"""Test that when someone sends an email the group process does not send
|
||||
it back to the original author."""
|
||||
self.test_group.write({
|
||||
'access_mode': 'members',
|
||||
'alias_contact': 'followers',
|
||||
'moderation': False,
|
||||
})
|
||||
# new member without email
|
||||
self.env['mail.group.member'].create({
|
||||
'email': '',
|
||||
'mail_group_id': self.test_group.id,
|
||||
})
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
self.format_and_process(
|
||||
GROUP_TEMPLATE, "Foo",
|
||||
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), 0, "Email should not be delivered when no email is specified")
|
||||
|
|
@ -0,0 +1,415 @@
|
|||
# -*- 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()
|
||||
|
||||
cls.test_group_2 = cls.env['mail.group'].create({
|
||||
'access_mode': 'members',
|
||||
'alias_name': 'test.mail.group.2',
|
||||
'moderation': True,
|
||||
'moderator_ids': [Command.link(cls.user_employee.id)],
|
||||
'name': 'Test group 2',
|
||||
})
|
||||
|
||||
@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',
|
||||
})
|
||||
|
||||
@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):
|
||||
with self.assertRaises(AccessError, msg='Portal should not have access to moderation rules'):
|
||||
self.env['mail.group.moderation'].with_user(self.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': 'members',
|
||||
'alias_name': 'test.mail.group.2',
|
||||
'moderation': True,
|
||||
'moderator_ids': [Command.link(cls.user_employee.id)],
|
||||
'name': 'Test group 2',
|
||||
})
|
||||
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.test_group
|
||||
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)
|
||||
group_2_message_count = len(mail_group_2_as2.mail_group_message_ids)
|
||||
|
||||
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_moderate_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), group_2_message_count,
|
||||
'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_moderate_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.email_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.email_formatted,
|
||||
'subject': 'Guidelines of group %s' % mail_group.name,
|
||||
})
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?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" sample="1">
|
||||
<field name="email"
|
||||
required="1" force_save="1"
|
||||
attrs="{'readonly': [('partner_id', '!=', False)]}"/>
|
||||
<field name="email_normalized" invisible="1" force_save="1"/>
|
||||
<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>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">No Members in this list yet!</p>
|
||||
<p>Let people subscribe to your list online or manually add them here.</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue