19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:39 +01:00
parent 38c6088dcc
commit d9452d2060
243 changed files with 30797 additions and 10815 deletions

View file

@ -13,7 +13,6 @@ pip install odoo-bringout-oca-ocb-test_mail_full
## Dependencies
This addon depends on:
- mail
- mail_bot
- portal
@ -26,34 +25,12 @@ This addon depends on:
- test_mail_sms
- test_mass_mailing
## Manifest Information
- **Name**: Mail Tests (Full)
- **Version**: 1.0
- **Category**: Hidden
- **License**: LGPL-3
- **Installable**: True
## Source
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `test_mail_full`.
- Repository: https://github.com/OCA/OCB
- Branch: 19.0
- Path: addons/test_mail_full
## 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
This package preserves the original LGPL-3 license.

View file

@ -1,22 +1,24 @@
[project]
name = "odoo-bringout-oca-ocb-test_mail_full"
version = "16.0.0"
description = "Mail Tests (Full) - Mail Tests: performances and tests specific to mail with all sub-modules"
description = "Mail Tests (Full) -
Mail Tests: performances and tests specific to mail with all sub-modules
"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-ocb-mail>=16.0.0",
"odoo-bringout-oca-ocb-mail_bot>=16.0.0",
"odoo-bringout-oca-ocb-portal>=16.0.0",
"odoo-bringout-oca-ocb-rating>=16.0.0",
"odoo-bringout-oca-ocb-mass_mailing>=16.0.0",
"odoo-bringout-oca-ocb-mass_mailing_sms>=16.0.0",
"odoo-bringout-oca-ocb-phone_validation>=16.0.0",
"odoo-bringout-oca-ocb-sms>=16.0.0",
"odoo-bringout-oca-ocb-test_mail>=16.0.0",
"odoo-bringout-oca-ocb-test_mail_sms>=16.0.0",
"odoo-bringout-oca-ocb-test_mass_mailing>=16.0.0",
"odoo-bringout-oca-ocb-mail>=19.0.0",
"odoo-bringout-oca-ocb-mail_bot>=19.0.0",
"odoo-bringout-oca-ocb-portal>=19.0.0",
"odoo-bringout-oca-ocb-rating>=19.0.0",
"odoo-bringout-oca-ocb-mass_mailing>=19.0.0",
"odoo-bringout-oca-ocb-mass_mailing_sms>=19.0.0",
"odoo-bringout-oca-ocb-phone_validation>=19.0.0",
"odoo-bringout-oca-ocb-sms>=19.0.0",
"odoo-bringout-oca-ocb-test_mail>=19.0.0",
"odoo-bringout-oca-ocb-test_mail_sms>=19.0.0",
"odoo-bringout-oca-ocb-test_mass_mailing>=19.0.0",
"requests>=2.25.1"
]
readme = "README.md"
@ -26,7 +28,7 @@ classifiers = [
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Office/Business",
]

View file

@ -1,5 +1,2 @@
# -*- coding: utf-8 -*-
from . import controllers
from . import models
from . import tests

View file

@ -28,15 +28,18 @@ real applications. """,
'data/mail_message_subtype_data.xml',
'security/ir.model.access.csv',
'security/ir_rule_data.xml',
'views/test_portal_template.xml',
],
'assets': {
'web.qunit_suite_tests': [
'test_mail_full/static/tests/qunit_suite_tests/*.js',
'web.assets_unit_tests': [
'test_mail_full/static/tests/**/*',
('remove', 'test_mail_full/static/tests/tours/**/*'),
],
'web.tests_assets': [
'test_mail_full/static/tests/helpers/*.js',
'web.assets_tests': [
'test_mail_full/static/tests/tours/**/*',
],
},
'installable': True,
'author': 'Odoo S.A.',
'license': 'LGPL-3',
}

View file

@ -1 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import portal

View file

@ -9,6 +9,34 @@ class PortalTest(http.Controller):
def test_portal_record_view(self, res_id, access_token=None, **kwargs):
return request.make_response(f'Record view of test_portal {res_id} ({access_token}, {kwargs})')
@http.route("/my/test_portal_records/<int:res_id>", type="http", auth="public", website=True)
def test_portal_record_page(self, res_id, **kwargs):
record = request.env["mail.test.portal"]._get_thread_with_access(res_id, **kwargs)
values = {
"object": record,
"token": kwargs.get("token"),
"hash": kwargs.get("hash", None),
"pid": kwargs.get("pid", None),
}
return request.render("test_mail_full.test_portal_template", values)
@http.route("/my/test_portal_rating_records/<int:res_id>", type="http", auth="public", website=True)
def test_portal_rating_record_page(self, res_id, **kwargs):
access_params = {
"hash": kwargs.get("hash"),
"pid": kwargs.get("pid"),
"token": kwargs.get("token"),
}
record = request.env["mail.test.rating"]._get_thread_with_access(res_id, **access_params)
return request.render(
"test_mail_full.test_portal_template",
{
**access_params,
"object": record,
"display_rating": kwargs.get("display_rating"),
},
)
@http.route('/test_portal/public_type/<int:res_id>', type='http', auth='public', methods=['GET'])
def test_public_record_view(self, res_id):
return request.make_response(f'Testing public controller for {res_id}')

View file

@ -1,224 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * test_mail_full
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server saas~12.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-09-09 10:49+0000\n"
"PO-Revision-Date: 2019-09-09 10:49+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms__message_needaction
msgid "Action Needed"
msgstr "Potrebna akcija"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms__message_attachment_count
msgid "Attachment Count"
msgstr "Broj priloga"
#. module: test_mail_full
#: model:ir.model,name:test_mail_full.model_mail_test_sms
msgid "Chatter Model for SMS Gateway"
msgstr "Chatter model za SMS pristupnik"
#. module: test_mail_full
#: model:ir.model,name:test_mail_full.model_mail_test_sms_partner
msgid "Chatter Model for SMS Gateway (Partner only)"
msgstr "Chatter model za SMS pristupnik (samo partner)"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms__create_uid
msgid "Created by"
msgstr "Kreirao"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms__create_date
msgid "Created on"
msgstr "Kreirano"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms__customer_id
msgid "Customer"
msgstr "Kupac"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms__display_name
msgid "Display Name"
msgstr "Prikazani naziv"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms__email_from
msgid "Email From"
msgstr "Email od"
#. module: test_mail_full
#: model:ir.model.fields,help:test_mail_full.field_mail_test_sms_bl__phone_sanitized
msgid ""
"Field used to store sanitized phone number. Helps speeding up searches and "
"comparisons."
msgstr ""
"Polje koje se koristi za pohranu sanitiziranog telefonskog broja. Pomaže "
"ubrzati pretraživanja i usporedbe."
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms__message_follower_ids
msgid "Followers"
msgstr "Pratioci"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms__message_channel_ids
msgid "Followers (Channels)"
msgstr "Pratioci (Kanali)"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms__message_partner_ids
msgid "Followers (Partners)"
msgstr "Pratioci (Partneri)"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms__id
msgid "ID"
msgstr "ID"
#. module: test_mail_full
#: model:ir.model.fields,help:test_mail_full.field_mail_test_sms__message_needaction
msgid "If checked, new messages require your attention."
msgstr "Ako je zakačeno, nove poruke će zahtjevati vašu pažnju"
#. module: test_mail_full
#: model:ir.model.fields,help:test_mail_full.field_mail_test_sms__message_has_error
msgid "If checked, some messages have a delivery error."
msgstr "Ako je označeno neke poruke mogu imati grešku u dostavi."
#. module: test_mail_full
#: model:ir.model.fields,help:test_mail_full.field_mail_test_sms_bl__phone_blacklisted
msgid ""
"If the email address is on the blacklist, the contact won't receive mass "
"mailing anymore, from any list"
msgstr ""
"Ako je adresa e-pošte na crnoj listi, kontakt više neće primati masovnu "
"poštu, ni s jedne liste"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms__message_is_follower
msgid "Is Follower"
msgstr "Pratilac"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms____last_update
msgid "Last Modified on"
msgstr "Zadnje mijenjano"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms__write_uid
msgid "Last Updated by"
msgstr "Zadnji ažurirao"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms__write_date
msgid "Last Updated on"
msgstr "Zadnje ažurirano"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms__message_main_attachment_id
msgid "Main Attachment"
msgstr "Glavna zakačka"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms__message_has_error
msgid "Message Delivery error"
msgstr "Greška pri isporuci poruke"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms__message_ids
msgid "Messages"
msgstr "Poruke"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms__mobile_nbr
msgid "Mobile Nbr"
msgstr "Broj mobilnog"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms__name
msgid "Name"
msgstr "Naziv:"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms__message_needaction_counter
msgid "Number of Actions"
msgstr "Broj akcija"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms__message_has_error_counter
msgid "Number of errors"
msgstr "Broj grešaka"
#. module: test_mail_full
#: model:ir.model.fields,help:test_mail_full.field_mail_test_sms__message_needaction_counter
msgid "Number of messages requiring action"
msgstr "Broj poruka koje zahtijevaju aktivnost"
#. module: test_mail_full
#: model:ir.model.fields,help:test_mail_full.field_mail_test_sms__message_has_error_counter
msgid "Number of messages with delivery error"
msgstr "Broj poruka sa greškama pri isporuci"
#. module: test_mail_full
#: model:ir.model.fields,help:test_mail_full.field_mail_test_sms__message_unread_counter
msgid "Number of unread messages"
msgstr "Broj nepročitanih poruka"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms_bl__phone_blacklisted
msgid "Phone Blacklisted"
msgstr "Telefon je stavljen na crnu listu"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms__phone_nbr
msgid "Phone Nbr"
msgstr "Broj telefona"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms__message_has_sms_error
msgid "SMS Delivery error"
msgstr "Greška u slanju SMSa"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms_bl__phone_sanitized
msgid "Sanitized Number"
msgstr "Sanirani broj"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms__subject
msgid "Subject"
msgstr "Tema"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms__message_unread
msgid "Unread Messages"
msgstr "Nepročitane poruke"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms__message_unread_counter
msgid "Unread Messages Counter"
msgstr "Brojač nepročitanih poruka"
#. module: test_mail_full
#: model:ir.model.fields,field_description:test_mail_full.field_mail_test_sms__website_message_ids
msgid "Website Messages"
msgstr "Poruke sa website-a"
#. module: test_mail_full
#: model:ir.model.fields,help:test_mail_full.field_mail_test_sms__website_message_ids
msgid "Website communication history"
msgstr "Povijest komunikacije Web stranice"

View file

@ -5,16 +5,16 @@ from odoo import api, fields, models
class MailTestPortal(models.Model):
""" A model intheriting from mail.thread with some fields used for portal
sharing, like a partner, ..."""
_description = 'Chatter Model for Portal'
""" A model inheriting from mail.thread and portal.mixin with some fields
used for portal sharing, like a partner, ..."""
_name = 'mail.test.portal'
_description = 'Chatter Model for Portal'
_inherit = [
'mail.thread',
'portal.mixin',
'mail.thread',
]
name = fields.Char()
name = fields.Char('Name')
partner_id = fields.Many2one('res.partner', 'Customer')
user_id = fields.Many2one(comodel_name='res.users', string="Salesperson")
@ -26,8 +26,8 @@ class MailTestPortal(models.Model):
class MailTestPortalNoPartner(models.Model):
""" A model inheriting from portal, but without any partner field """
_description = 'Chatter Model for Portal (no partner field)'
_name = 'mail.test.portal.no.partner'
_description = 'Chatter Model for Portal (no partner field)'
_inherit = [
'mail.thread',
'portal.mixin',
@ -66,26 +66,24 @@ class MailTestPortalPublicAccessAction(models.Model):
class MailTestRating(models.Model):
""" A model inheriting from mail.thread with some fields used for SMS
""" A model inheriting from rating.mixin (which inherits from mail.thread) with some fields used for SMS
gateway, like a partner, a specific mobile phone, ... """
_description = 'Rating Model (ticket-like)'
_name = 'mail.test.rating'
_description = 'Rating Model (ticket-like)'
_inherit = [
'mail.thread',
'mail.activity.mixin',
'rating.mixin',
'mail.activity.mixin',
'portal.mixin',
]
_mailing_enabled = True
_order = 'name asc, id asc'
_order = 'id asc'
name = fields.Char()
subject = fields.Char()
name = fields.Char('Name')
subject = fields.Char('Subject')
company_id = fields.Many2one('res.company', 'Company')
customer_id = fields.Many2one('res.partner', 'Customer')
email_from = fields.Char(compute='_compute_email_from', precompute=True, readonly=False, store=True)
mobile_nbr = fields.Char(compute='_compute_mobile_nbr', precompute=True, readonly=False, store=True)
phone_nbr = fields.Char(compute='_compute_phone_nbr', precompute=True, readonly=False, store=True)
email_from = fields.Char('From', compute='_compute_email_from', precompute=True, readonly=False, store=True)
phone_nbr = fields.Char('Phone Number', compute='_compute_phone_nbr', precompute=True, readonly=False, store=True)
user_id = fields.Many2one('res.users', 'Responsible', tracking=1)
@api.depends('customer_id')
@ -96,14 +94,6 @@ class MailTestRating(models.Model):
elif not rating.email_from:
rating.email_from = False
@api.depends('customer_id')
def _compute_mobile_nbr(self):
for rating in self:
if rating.customer_id.mobile:
rating.mobile_nbr = rating.customer_id.mobile
elif not rating.mobile_nbr:
rating.mobile_nbr = False
@api.depends('customer_id')
def _compute_phone_nbr(self):
for rating in self:
@ -112,11 +102,50 @@ class MailTestRating(models.Model):
elif not rating.phone_nbr:
rating.phone_nbr = False
def _mail_get_partner_fields(self):
def _mail_get_partner_fields(self, introspect_fields=False):
return ['customer_id']
def _phone_get_number_fields(self):
return ['phone_nbr']
def _rating_apply_get_default_subtype_id(self):
return self.env['ir.model.data']._xmlid_to_res_id("test_mail_full.mt_mail_test_rating_rating_done")
def _rating_get_partner(self):
return self.customer_id
@api.model
def _allow_publish_rating_stats(self):
return True
class MailTestRatingThread(models.Model):
"""A model inheriting from mail.thread with minimal fields for testing
rating submission without the rating mixin but with the same test code:
- partner_id: value returned by the base _rating_get_partner method
- user_id: value returned by the base _rating_get_operator method
"""
_name = 'mail.test.rating.thread'
_description = 'Model for testing rating without the rating mixin'
_inherit = ['mail.thread']
_order = 'name asc, id asc'
name = fields.Char('Name')
customer_id = fields.Many2one('res.partner', 'Customer')
user_id = fields.Many2one('res.users', 'Responsible', tracking=1)
def _mail_get_partner_fields(self, introspect_fields=False):
return ['customer_id']
def _rating_get_partner(self):
return self.customer_id or super()._rating_get_partner()
class MailTestRatingThreadRead(models.Model):
"""Same as MailTestRatingThread but post accessible on read by portal users."""
_name = 'mail.test.rating.thread.read'
_description = "Read-post rating model"
_inherit = ["mail.test.rating.thread"]
_order = "name asc, id asc"
_mail_post_access = "read"

View file

@ -1,10 +1,13 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_mail_test_portal_all,mail.test.portal.all,model_mail_test_portal,,0,0,0,0
access_mail_test_portal_user,mail.test.portal.user,model_mail_test_portal,base.group_user,1,1,1,1
access_mail_test_portal_no_partner_all,mail.test.portal.no.partner.all,model_mail_test_portal_no_partner,,1,0,0,0
access_mail_test_portal_no_partner_portal,mail.test.portal.no.partner.all,model_mail_test_portal_no_partner,base.group_portal,1,0,0,0
access_mail_test_portal_no_partner_user,mail.test.portal.no.partner.user,model_mail_test_portal_no_partner,base.group_user,1,1,1,1
access_mail_test_portal_public_access_action_portal,mail.test.portal.public.access.action.portal,model_mail_test_portal_public_access_action,base.group_portal,1,0,0,0
access_mail_test_portal_public_access_action_user,mail.test.portal.public.access.action.user,model_mail_test_portal_public_access_action,base.group_user,1,1,1,1
access_mail_test_rating_all,mail.test.rating.all,model_mail_test_rating,,0,0,0,0
access_mail_test_rating_portal,mail.test.rating.portal,model_mail_test_rating,base.group_portal,1,0,0,0
access_mail_test_rating_user,mail.test.rating.user,model_mail_test_rating,base.group_user,1,1,1,1
access_mail_test_rating_thread_all,mail.test.rating.thread.all,model_mail_test_rating_thread,,0,0,0,0
access_mail_test_rating_thread_portal,mail.test.rating.thread.portal,model_mail_test_rating_thread,base.group_portal,1,0,0,0
access_mail_test_rating_thread_user,mail.test.rating.thread.user,model_mail_test_rating_thread,base.group_user,1,1,1,1
access_mail_test_rating_thread_read_portal,mail.test.rating.thread.read.portal,model_mail_test_rating_thread_read,base.group_portal,1,0,0,0
access_mail_test_rating_thread_read_user,mail.test.rating.thread.read.user,model_mail_test_rating_thread_read,base.group_user,1,1,1,1

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
access_mail_test_portal_all mail.test.portal.all model_mail_test_portal 0 0 0 0
2 access_mail_test_portal_user mail.test.portal.user model_mail_test_portal base.group_user 1 1 1 1
3 access_mail_test_portal_no_partner_all access_mail_test_portal_no_partner_portal mail.test.portal.no.partner.all model_mail_test_portal_no_partner base.group_portal 1 0 0 0
4 access_mail_test_portal_no_partner_user mail.test.portal.no.partner.user model_mail_test_portal_no_partner base.group_user 1 1 1 1
5 access_mail_test_portal_public_access_action_portal mail.test.portal.public.access.action.portal model_mail_test_portal_public_access_action base.group_portal 1 0 0 0
6 access_mail_test_portal_public_access_action_user mail.test.portal.public.access.action.user model_mail_test_portal_public_access_action base.group_user 1 1 1 1
access_mail_test_rating_all mail.test.rating.all model_mail_test_rating 0 0 0 0
7 access_mail_test_rating_portal mail.test.rating.portal model_mail_test_rating base.group_portal 1 0 0 0
8 access_mail_test_rating_user mail.test.rating.user model_mail_test_rating base.group_user 1 1 1 1
9 access_mail_test_rating_thread_all mail.test.rating.thread.all model_mail_test_rating_thread 0 0 0 0
10 access_mail_test_rating_thread_portal mail.test.rating.thread.portal model_mail_test_rating_thread base.group_portal 1 0 0 0
11 access_mail_test_rating_thread_user mail.test.rating.thread.user model_mail_test_rating_thread base.group_user 1 1 1 1
12 access_mail_test_rating_thread_read_portal mail.test.rating.thread.read.portal model_mail_test_rating_thread_read base.group_portal 1 0 0 0
13 access_mail_test_rating_thread_read_user mail.test.rating.thread.read.user model_mail_test_rating_thread_read base.group_user 1 1 1 1

View file

@ -1,5 +0,0 @@
/** @odoo-module **/
import { addModelNamesToFetch } from '@bus/../tests/helpers/model_definitions_helpers';
addModelNamesToFetch(['mail.test.rating']);

View file

@ -0,0 +1,61 @@
import { click, contains, start, startServer } from "@mail/../tests/mail_test_helpers";
import { test } from "@odoo/hoot";
import { defineTestMailFullModels } from "@test_mail_full/../tests/test_mail_full_test_helpers";
import { serverState } from "@web/../tests/web_test_helpers";
defineTestMailFullModels();
test("rating value displayed on the preview", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({});
const channelId = pyEnv["discuss.channel"].create({});
const messageId = pyEnv["mail.message"].create({
author_id: partnerId,
body: "non-empty",
model: "discuss.channel",
res_id: channelId,
});
pyEnv["rating.rating"].create({
consumed: true,
message_id: messageId,
partner_id: partnerId,
rating_image_url: "/rating/static/src/img/rating_5.png",
rating_text: "top",
});
await start();
await click(".o_menu_systray i[aria-label='Messages']");
await contains(".o-mail-NotificationItem-text", { text: "Rating:" });
await contains(".o-rating-preview-image[alt='top']");
await contains(".o-rating-preview-image[data-src='/rating/static/src/img/rating_5.png']");
});
test("rating value displayed on the needaction preview", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({});
const ratingId = pyEnv["mail.test.rating"].create({ name: "Test rating" });
const messageId = pyEnv["mail.message"].create({
model: "mail.test.rating",
needaction: true,
res_id: ratingId,
});
pyEnv["mail.notification"].create({
mail_message_id: messageId,
notification_status: "sent",
notification_type: "inbox",
res_partner_id: serverState.partnerId,
});
pyEnv["rating.rating"].create([
{
consumed: true,
message_id: messageId,
partner_id: partnerId,
rating_image_url: "/rating/static/src/img/rating_5.png",
rating_text: "top",
},
]);
await start();
await click(".o_menu_systray i[aria-label='Messages']");
await contains(".o-mail-NotificationItem-text", { text: "Rating:" });
await contains(".o-rating-preview-image[alt='top']");
await contains(".o-rating-preview-image[data-src='/rating/static/src/img/rating_5.png']");
});

View file

@ -0,0 +1,5 @@
import { models } from "@web/../tests/web_test_helpers";
export class MailTestRating extends models.ServerModel {
_name = "mail.test.rating";
}

View file

@ -1,55 +0,0 @@
/** @odoo-module **/
import { afterNextRender, start, startServer } from '@mail/../tests/helpers/test_utils';
QUnit.module('test_mail_full', {}, function () {
QUnit.module('channel_preview_view_tests.js');
QUnit.test('rating value displayed on the thread preview', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const mailChannelId1 = pyEnv['mail.channel'].create({});
const mailMessageId1 = pyEnv['mail.message'].create([
{ author_id: resPartnerId1, model: 'mail.channel', res_id: mailChannelId1 },
]);
pyEnv['rating.rating'].create({
consumed: true,
message_id: mailMessageId1,
partner_id: resPartnerId1,
rating_image_url: "/rating/static/src/img/rating_5.png",
rating_text: "top",
});
const { afterEvent, messaging } = await start();
await afterNextRender(() => afterEvent({
eventName: 'o-thread-cache-loaded-messages',
func: () => document.querySelector('.o_MessagingMenu_toggler').click(),
message: "should wait until inbox loaded initial needaction messages",
predicate: ({ threadCache }) => {
return threadCache.thread === messaging.inbox.thread;
},
}));
assert.strictEqual(
document.querySelector('.o_ChannelPreviewView_ratingText').textContent,
"Rating:",
"should display the correct content (Rating:)"
);
assert.containsOnce(
document.body,
'.o_ChannelPreviewView_ratingImage',
"should have a rating image in the body"
);
assert.strictEqual(
$('.o_ChannelPreviewView_ratingImage').attr('data-src'),
"/rating/static/src/img/rating_5.png",
"should contain the correct rating image"
);
assert.strictEqual(
$('.o_ChannelPreviewView_ratingImage').attr('data-alt'),
"top",
"should contain the correct rating text"
);
});
});

View file

@ -1,64 +0,0 @@
/** @odoo-module **/
import { afterNextRender, start, startServer } from '@mail/../tests/helpers/test_utils';
QUnit.module('test_mail_full', {}, function () {
QUnit.module('thread_needaction_preview_tests.js');
QUnit.test('rating value displayed on the thread needaction preview', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const mailTestRating1 = pyEnv['mail.test.rating'].create({});
const mailMessageId1 = pyEnv['mail.message'].create({
model: 'mail.test.rating',
needaction: true,
needaction_partner_ids: [pyEnv.currentPartnerId],
res_id: mailTestRating1,
});
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1,
notification_status: 'sent',
notification_type: 'inbox',
res_partner_id: pyEnv.currentPartnerId,
});
pyEnv['rating.rating'].create([{
consumed: true,
message_id: mailMessageId1,
partner_id: resPartnerId1,
rating_image_url: "/rating/static/src/img/rating_5.png",
rating_text: "top",
}]);
const { afterEvent, messaging } = await start();
await afterNextRender(() => afterEvent({
eventName: 'o-thread-cache-loaded-messages',
func: () => document.querySelector('.o_MessagingMenu_toggler').click(),
message: "should wait until inbox loaded initial needaction messages",
predicate: ({ threadCache }) => {
return threadCache.thread === messaging.inbox.thread;
},
}));
assert.strictEqual(
document.querySelector('.o_ThreadNeedactionPreview_ratingText').textContent,
"Rating:",
"should display the correct content (Rating:)"
);
assert.containsOnce(
document.body,
'.o_ThreadNeedactionPreview_ratingImage',
"should have a rating image in the body"
);
assert.strictEqual(
$('.o_ThreadNeedactionPreview_ratingImage').attr('data-src'),
"/rating/static/src/img/rating_5.png",
"should contain the correct rating image"
);
assert.strictEqual(
$('.o_ThreadNeedactionPreview_ratingImage').attr('data-alt'),
"top",
"should contain the correct rating text"
);
});
});

View file

@ -0,0 +1,9 @@
import { ratingModels } from "@rating/../tests/rating_test_helpers";
import { MailTestRating } from "@test_mail_full/../tests/mock_server/models/mail_test_rating";
import { defineModels } from "@web/../tests/web_test_helpers";
export const testMailFullModels = { ...ratingModels, MailTestRating };
export function defineTestMailFullModels() {
defineModels(testMailFullModels);
}

View file

@ -0,0 +1,19 @@
import { registry } from "@web/core/registry";
import { contains } from "@web/../tests/utils";
registry.category("web_tour.tours").add("load_more_tour", {
steps: () => [
{
trigger: "#chatterRoot:shadow .o-mail-Thread .o-mail-Message",
run: async function () {
await contains(".o-mail-Thread .o-mail-Message", {
count: 30,
target: document.querySelector("#chatterRoot").shadowRoot,
});
},
},
{
trigger: "#chatterRoot:shadow .o-mail-Thread button:contains(Load More):not(:visible)",
},
],
});

View file

@ -0,0 +1,87 @@
import { registry } from "@web/core/registry";
import { contains } from "@web/../tests/utils";
registry.category("web_tour.tours").add("star_message_tour", {
steps: () => [
{
trigger:
"#chatterRoot:shadow .o-mail-Message:not([data-starred]):contains(Test Message)",
run: "hover && click #chatterRoot:shadow [title='Add Star']",
},
{
trigger: "#chatterRoot:shadow .o-mail-Message[data-starred]:contains(Test Message)",
},
],
});
registry.category("web_tour.tours").add("message_actions_tour", {
steps: () => [
{
trigger: "#chatterRoot:shadow .o-mail-Thread .o-mail-Message",
run: async function () {
await contains(".o-mail-Thread .o-mail-Message", {
count: 1,
target: document.querySelector("#chatterRoot").shadowRoot,
});
},
},
{
trigger: "#chatterRoot:shadow .o-mail-Composer-input",
run: "edit New message",
},
{
trigger: "#chatterRoot:shadow .o-mail-Composer button:contains(Send):enabled",
run: "click",
},
{
trigger: "#chatterRoot:shadow .o-mail-Thread .o-mail-Message",
run: async function () {
await contains(".o-mail-Thread .o-mail-Message", {
count: 2,
target: document.querySelector("#chatterRoot").shadowRoot,
});
},
},
{
trigger: "#chatterRoot:shadow .o-mail-Message[data-persistent]:contains(New message)",
run: "hover && click #chatterRoot:shadow button[title='Add a Reaction']",
},
{
trigger: "#chatterRoot:shadow .o-mail-QuickReactionMenu-emoji span:contains(❤️)",
run: "click",
},
{
trigger:
"#chatterRoot:shadow .o-mail-Message:contains(New message) .o-mail-MessageReaction:contains(❤️)",
},
{
trigger: "#chatterRoot:shadow .o-mail-Message:contains(New message)",
run: "hover && click #chatterRoot:shadow button[title='Edit']",
},
{
trigger: "#chatterRoot:shadow .o-mail-Message .o-mail-Composer-input",
run: "edit Message content changed",
},
{
trigger: "#chatterRoot:shadow .o-mail-Message button:contains(save)",
run: "click",
},
{
trigger: "#chatterRoot:shadow .o-mail-Message:contains(Message content changed)",
run: "hover && click #chatterRoot:shadow button[title='Delete']",
},
{
trigger: "#chatterRoot:shadow button:contains(Delete)",
run: "click",
},
{
trigger: "#chatterRoot:shadow .o-mail-Thread .o-mail-Message",
run: async function () {
await contains(".o-mail-Thread .o-mail-Message", {
count: 1,
target: document.querySelector("#chatterRoot").shadowRoot,
});
},
},
],
});

View file

@ -0,0 +1,34 @@
import { registry } from "@web/core/registry";
const cannedResponseButtonSelector = "button[title='Insert a Canned response']";
registry.category("web_tour.tours").add("portal_composer_actions_tour_internal_user", {
steps: () => [
{
trigger: `#chatterRoot:shadow .o-mail-Composer ${cannedResponseButtonSelector}`,
run: "click",
},
{
trigger: "#chatterRoot:shadow .o-mail-Composer-input",
run() {
if (this.anchor.value !== "::") {
console.error(
"Clicking on the canned response button should insert the '::' into the composer."
);
}
},
},
{
trigger:
"#chatterRoot:shadow .o-mail-Composer-suggestion:contains(Hello, how may I help you?)",
},
],
});
registry.category("web_tour.tours").add("portal_composer_actions_tour_portal_user", {
steps: () => [
{
trigger: `#chatterRoot:shadow .o-mail-Composer:not(:has(${cannedResponseButtonSelector}))`,
},
],
});

View file

@ -0,0 +1,22 @@
import { messageActionsRegistry } from "@mail/core/common/message_actions";
import { registry } from "@web/core/registry";
import { patch } from "@web/core/utils/patch";
registry.category("web_tour.tours").add("portal_copy_link_tour", {
steps: () => [
{
trigger: "#chatterRoot:shadow .o-mail-Message",
run: () => {
const copyLinkAction = messageActionsRegistry.get("copy-link");
patch(copyLinkAction, { sequence: 1 }); // make sure the action is visible without expanding
}
},
{
trigger: "#chatterRoot:shadow .o-mail-Message:contains(Test Message)",
run: "hover && click",
},
{
trigger: "#chatterRoot:shadow .o-mail-Message-actions [title='Copy Link']",
},
],
});

View file

@ -0,0 +1,28 @@
import { messageActionsRegistry } from "@mail/core/common/message_actions";
import { registry } from "@web/core/registry";
import { patch } from "@web/core/utils/patch";
registry.category("web_tour.tours").add("portal_no_copy_link_tour", {
steps: () => [
{
trigger: "#chatterRoot:shadow .o-mail-Message",
run: () => {
const copyLinkAction = messageActionsRegistry.get("copy-link");
patch(copyLinkAction, { sequence: 1 }); // make sure the action is visible without expanding
}
},
{
trigger: "#chatterRoot:shadow .o-mail-Message:contains(Test Message)",
run: "hover && click",
},
{
trigger: "#chatterRoot:shadow .o-mail-Message-actions",
run: async () => {
const copyLinkButton = document.querySelector('#chatterRoot').shadowRoot.querySelector("[title='Copy Link']");
if (copyLinkButton) {
throw new Error("Users without read access should not be able to copy the link to a message");
}
},
},
],
});

View file

@ -0,0 +1,46 @@
import { registry } from "@web/core/registry";
const ratingCardSelector = ".o_website_rating_card_container";
registry.category("web_tour.tours").add("portal_rating_tour", {
steps: () => [
{
// Ensure that the rating data has been fetched before making a negative assertion for rating cards.
trigger: "#chatterRoot:shadow .o-mail-Message-body:text(Message without rating)",
},
{
trigger: `#chatterRoot:shadow .o-mail-Chatter-top:not(:has(${ratingCardSelector}))`,
},
{
trigger: "#chatterRoot:shadow .o-mail-Composer-input",
run: "edit Excellent service!",
},
{
trigger: "#chatterRoot:shadow .o-mail-Composer-send:enabled",
run: "click",
},
{
trigger: `#chatterRoot:shadow .o-mail-Chatter-top ${ratingCardSelector} .o_website_rating_table_row[data-star='4']:has(:text(100%))`,
},
],
});
registry.category("web_tour.tours").add("portal_display_rating_tour", {
steps: () => [
{
trigger: `#chatterRoot:shadow .o-mail-Chatter-top ${ratingCardSelector}`,
},
],
});
registry.category("web_tour.tours").add("portal_not_display_rating_tour", {
steps: () => [
{
// Ensure that the rating data has been fetched before making a negative assertion for rating cards.
trigger: "#chatterRoot:shadow .o-mail-Message-body:text(Message with rating)",
},
{
trigger: `#chatterRoot:shadow .o-mail-Chatter-top:not(:has(${ratingCardSelector}))`,
},
],
});

View file

@ -1,10 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_odoobot
from . import test_controller_attachment
from . import test_controller_reaction
from . import test_controller_update
from . import test_controller_thread
from . import test_ir_mail_server
from . import test_mail_bot
from . import test_mail_performance
from . import test_mail_thread_internals
from . import test_mass_mailing
from . import test_portal
from . import test_rating
from . import test_res_users
from . import test_ui

View file

@ -0,0 +1,39 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import odoo
from odoo.addons.mail.tests.common_controllers import MailControllerAttachmentCommon
@odoo.tests.tagged("-at_install", "post_install", "mail_controller")
class TestPortalAttachmentController(MailControllerAttachmentCommon):
def test_attachment_upload_portal(self):
"""Test access to upload an attachment on portal"""
record = self.env["mail.test.portal.no.partner"].create({"name": "Test"})
token, bad_token, sign, bad_sign, _ = self._get_sign_token_params(record)
self._execute_subtests_upload(
record,
(
(self.user_public, False),
(self.user_public, True, token),
(self.user_public, True, sign),
(self.guest, False),
(self.guest, True, token),
(self.guest, True, sign),
(self.user_portal, False),
(self.user_portal, False, bad_token),
(self.user_portal, False, bad_sign),
(self.user_portal, True, token),
(self.user_portal, True, sign),
(self.user_employee, True),
(self.user_employee, True, bad_token),
(self.user_employee, True, bad_sign),
(self.user_employee, True, token),
(self.user_employee, True, sign),
(self.user_admin, True),
(self.user_admin, True, bad_token),
(self.user_admin, True, bad_sign),
(self.user_admin, True, token),
(self.user_admin, True, sign),
),
)

View file

@ -0,0 +1,80 @@
from odoo.addons.mail.tests.common_controllers import MailControllerReactionCommon
from odoo.tests import tagged
@tagged("-at_install", "post_install", "mail_controller")
class TestPortalMessageReactionController(MailControllerReactionCommon):
def test_message_reaction_nomsg(self):
"""Test access of message reaction for a non-existing message."""
self._execute_subtests(
self.fake_message,
((user, False) for user in [self.user_public, self.guest, self.user_portal, self.user_employee]),
)
def test_message_reaction_portal_no_partner(self):
"""Test access of message reaction for portal without partner."""
record = self.env["mail.test.portal.no.partner"].create({"name": "Test"})
token, bad_token, sign, bad_sign, partner = self._get_sign_token_params(record)
message = record.message_post(body="portal no partner")
self._execute_subtests(
message,
(
(self.user_public, False),
(self.user_public, False, bad_token),
(self.user_public, False, bad_sign),
# False because no portal partner, no guest
(self.user_public, False, token),
(self.user_public, True, sign, {"partner": partner}),
(self.guest, False),
(self.guest, False, bad_token),
(self.guest, False, bad_sign),
(self.guest, True, token),
(self.guest, True, sign, {"partner": partner}),
(self.user_portal, False),
(self.user_portal, False, bad_token),
(self.user_portal, False, bad_sign),
(self.user_portal, True, token),
(self.user_portal, True, sign),
(self.user_employee, True),
(self.user_employee, True, bad_token),
(self.user_employee, True, bad_sign),
(self.user_employee, True, token),
(self.user_employee, True, sign),
),
)
def test_message_reaction_portal_assigned_partner(self):
"""Test access of message reaction for portal with partner."""
rec_partner = self.env["res.partner"].create({"name": "Record Partner"})
record = self.env["mail.test.portal"].create({"name": "Test", "partner_id": rec_partner.id})
message = record.message_post(body="portal with partner")
token, bad_token, sign, bad_sign, partner = self._get_sign_token_params(record)
self._execute_subtests(
message,
(
(self.user_public, False),
(self.user_public, False, bad_token),
(self.user_public, False, bad_sign),
(self.user_public, True, token, {"partner": rec_partner}),
(self.user_public, True, sign, {"partner": partner}),
# sign has priority over token when both are provided
(self.user_public, True, token | sign, {"partner": partner}),
(self.guest, False),
(self.guest, False, bad_token),
(self.guest, False, bad_sign),
(self.guest, True, token, {"partner": rec_partner}),
(self.guest, True, sign, {"partner": partner}),
(self.guest, True, token | sign, {"partner": partner}),
(self.user_portal, False),
(self.user_portal, False, bad_token),
(self.user_portal, False, bad_sign),
(self.user_portal, True, token),
(self.user_portal, True, sign),
(self.user_employee, True),
(self.user_employee, True, bad_token),
(self.user_employee, True, bad_sign),
(self.user_employee, True, token),
(self.user_employee, True, sign),
),
)

View file

@ -0,0 +1,171 @@
from odoo.addons.mail.tests.common_controllers import MailControllerThreadCommon, MessagePostSubTestData
from odoo.tests import tagged
@tagged("-at_install", "post_install", "mail_controller")
class TestPortalThreadController(MailControllerThreadCommon):
def test_message_post_portal_no_partner(self):
"""Test access of message post for portal without partner."""
record = self.env["mail.test.portal.no.partner"].create({"name": "Test"})
token, bad_token, sign, bad_sign, partner = self._get_sign_token_params(record)
def test_access(user, allowed, route_kw=None, exp_author=None):
return MessagePostSubTestData(user, allowed, route_kw=route_kw, exp_author=exp_author)
self._execute_message_post_subtests(
record,
(
test_access(self.user_public, False),
test_access(self.user_public, False, route_kw=bad_token),
test_access(self.user_public, False, route_kw=bad_sign),
test_access(self.user_public, True, route_kw=token),
test_access(self.user_public, True, route_kw=sign, exp_author=partner),
test_access(self.guest, False),
test_access(self.guest, False, route_kw=bad_token),
test_access(self.guest, False, route_kw=bad_sign),
test_access(self.guest, True, route_kw=token),
test_access(self.guest, True, route_kw=sign, exp_author=partner),
test_access(self.user_portal, False),
test_access(self.user_portal, False, route_kw=bad_token),
test_access(self.user_portal, False, route_kw=bad_sign),
test_access(self.user_portal, True, route_kw=token),
test_access(self.user_portal, True, route_kw=sign),
test_access(self.user_employee, True),
test_access(self.user_employee, True, route_kw=bad_token),
test_access(self.user_employee, True, route_kw=bad_sign),
test_access(self.user_employee, True, route_kw=token),
test_access(self.user_employee, True, route_kw=sign),
),
)
def test_message_post_portal_with_partner(self):
"""Test access of message post for portal with partner."""
rec_partner = self.env["res.partner"].create({"name": "Record Partner"})
record = self.env["mail.test.portal"].create({"name": "Test", "partner_id": rec_partner.id})
token, bad_token, sign, bad_sign, partner = self._get_sign_token_params(record)
def test_access(user, allowed, route_kw=None, exp_author=None):
return MessagePostSubTestData(user, allowed, route_kw=route_kw, exp_author=exp_author)
self._execute_message_post_subtests(
record,
(
test_access(self.user_public, False),
test_access(self.user_public, False, route_kw=bad_token),
test_access(self.user_public, False, route_kw=bad_sign),
test_access(self.user_public, True, route_kw=token, exp_author=rec_partner),
test_access(self.user_public, True, route_kw=sign, exp_author=partner),
# sign has priority over token when both are provided
test_access(self.user_public, True, route_kw=token | sign, exp_author=partner),
test_access(self.guest, False),
test_access(self.guest, False, route_kw=bad_token),
test_access(self.guest, False, route_kw=bad_sign),
test_access(self.guest, True, route_kw=token, exp_author=rec_partner),
test_access(self.guest, True, route_kw=sign, exp_author=partner),
test_access(self.guest, True, route_kw=token | sign, exp_author=partner),
test_access(self.user_portal, False),
test_access(self.user_portal, False, route_kw=bad_token),
test_access(self.user_portal, False, route_kw=bad_sign),
test_access(self.user_portal, True, route_kw=token),
test_access(self.user_portal, True, route_kw=sign),
test_access(self.user_employee, True),
test_access(self.user_employee, True, route_kw=bad_token),
test_access(self.user_employee, True, route_kw=bad_sign),
test_access(self.user_employee, True, route_kw=token),
test_access(self.user_employee, True, route_kw=sign),
),
)
def test_message_post_partner_ids_mention_token(self):
"""Test partner_ids of message_post for portal record without partner.
All users are allowed to mention with specific message_mention token."""
record = self.env["mail.test.portal.no.partner"].create({"name": "Test"})
token, bad_token, sign, bad_sign, partner = self._get_sign_token_params(record)
all_partners = (
self.user_portal + self.user_employee + self.user_admin
).partner_id
record.message_subscribe(partner_ids=self.user_employee.partner_id.ids)
def test_partners(user, allowed, exp_partners, route_kw=None, exp_author=None):
return MessagePostSubTestData(
user,
allowed,
partners=all_partners,
route_kw=route_kw,
exp_author=exp_author,
exp_partners=exp_partners,
add_mention_token=True,
)
self._execute_message_post_subtests(
record,
(
test_partners(self.user_public, False, all_partners),
test_partners(self.user_public, False, all_partners, route_kw=bad_token),
test_partners(self.user_public, False, all_partners, route_kw=bad_sign),
test_partners(self.user_public, True, all_partners, route_kw=token),
test_partners(self.user_public, True, all_partners, route_kw=sign, exp_author=partner),
test_partners(self.guest, False, all_partners),
test_partners(self.guest, False, all_partners, route_kw=bad_token),
test_partners(self.guest, False, all_partners, route_kw=bad_sign),
test_partners(self.guest, True, all_partners, route_kw=token),
test_partners(self.guest, True, all_partners, route_kw=sign, exp_author=partner),
test_partners(self.user_portal, False, all_partners),
test_partners(self.user_portal, False, all_partners, route_kw=bad_token),
test_partners(self.user_portal, False, all_partners, route_kw=bad_sign),
test_partners(self.user_portal, True, all_partners, route_kw=token),
test_partners(self.user_portal, True, all_partners, route_kw=sign),
test_partners(self.user_employee, True, all_partners),
test_partners(self.user_employee, True, all_partners, route_kw=bad_token),
test_partners(self.user_employee, True, all_partners, route_kw=bad_sign),
test_partners(self.user_employee, True, all_partners, route_kw=token),
test_partners(self.user_employee, True, all_partners, route_kw=sign),
),
)
def test_message_post_partner_ids_portal(self):
"""Test partner_ids of message_post for portal record without partner.
Only internal users are allowed to mention without specific message_mention token."""
record = self.env["mail.test.portal.no.partner"].create({"name": "Test"})
token, bad_token, sign, bad_sign, partner = self._get_sign_token_params(record)
all_partners = (
self.user_portal + self.user_employee + self.user_admin
).partner_id
record.message_subscribe(partner_ids=self.user_employee.partner_id.ids)
def test_partners(user, allowed, exp_partners, route_kw=None, exp_author=None):
return MessagePostSubTestData(
user,
allowed,
partners=all_partners,
route_kw=route_kw,
exp_author=exp_author,
exp_partners=exp_partners,
)
self._execute_message_post_subtests(
record,
(
test_partners(self.user_public, False, self.env["res.partner"]),
test_partners(self.user_public, False, self.env["res.partner"], route_kw=bad_token),
test_partners(self.user_public, False, self.env["res.partner"], route_kw=bad_sign),
test_partners(self.user_public, True, self.env["res.partner"], route_kw=token),
test_partners(self.user_public, True, self.env["res.partner"], route_kw=sign, exp_author=partner),
test_partners(self.guest, False, self.env["res.partner"]),
test_partners(self.guest, False, self.env["res.partner"], route_kw=bad_token),
test_partners(self.guest, False, self.env["res.partner"], route_kw=bad_sign),
test_partners(self.guest, True, self.env["res.partner"], route_kw=token),
test_partners(self.guest, True, self.env["res.partner"], route_kw=sign, exp_author=partner),
test_partners(self.user_portal, False, self.env["res.partner"]),
test_partners(self.user_portal, False, self.env["res.partner"], route_kw=bad_token),
test_partners(self.user_portal, False, self.env["res.partner"], route_kw=bad_sign),
test_partners(self.user_portal, True, self.env["res.partner"], route_kw=token),
test_partners(self.user_portal, True, self.env["res.partner"], route_kw=sign),
test_partners(self.user_employee, True, all_partners),
test_partners(self.user_employee, True, all_partners, route_kw=bad_token),
test_partners(self.user_employee, True, all_partners, route_kw=bad_sign),
test_partners(self.user_employee, True, all_partners, route_kw=token),
test_partners(self.user_employee, True, all_partners, route_kw=sign),
),
)

View file

@ -0,0 +1,48 @@
from odoo.addons.mail.tests.common_controllers import MailControllerUpdateCommon
from odoo.tests import tagged
@tagged("-at_install", "post_install", "mail_controller")
class TestPortalMessageUpdateController(MailControllerUpdateCommon):
def test_message_update_no_message(self):
"""Test update a non-existing message."""
self._execute_subtests(
self.fake_message,
((user, False) for user in [self.guest, self.user_admin, self.user_employee, self.user_portal, self.user_public]),
)
def test_message_update_portal(self):
"""Test only admin and author can modify content of a message, works if
author is a portal user. """
record = self.env["mail.test.portal.no.partner"].create({"name": "Test"})
token, bad_token, sign, bad_sign, _ = self._get_sign_token_params(record)
message = record.message_post(
body=self.message_body,
author_id=self.user_portal.partner_id.id,
message_type="comment",
)
self._execute_subtests(
message,
(
(self.user_public, False),
(self.user_public, False, token),
(self.user_public, False, sign),
(self.guest, False),
(self.guest, False, token),
(self.guest, False, sign),
(self.user_portal, False),
(self.user_portal, False, bad_token),
(self.user_portal, False, bad_sign),
(self.user_portal, True, token),
(self.user_portal, True, sign),
(self.user_employee, False),
(self.user_employee, False, token),
(self.user_employee, False, sign),
(self.user_admin, True),
(self.user_admin, True, bad_token),
(self.user_admin, True, bad_sign),
(self.user_admin, True, token),
(self.user_admin, True, sign),
),
)

View file

@ -0,0 +1,122 @@
from contextlib import contextmanager
from unittest.mock import patch
from odoo.addons.mail.tests.common import MailCommon
from odoo.tests import tagged, users
from odoo.addons.base.models.ir_mail_server import IrMail_Server
from odoo.exceptions import UserError, ValidationError
@tagged('mail_server')
class TestIrMailServerPersonal(MailCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env['ir.config_parameter'].sudo().set_param('mail.disable_personal_mail_servers', False)
cls.user_admin.email = 'admin@test.lan'
cls.user_employee.email = 'employee@test.lan'
cls.user_employee.group_ids += cls.env.ref('mass_mailing.group_mass_mailing_user')
cls.test_partner = cls.env['res.partner'].create({
'name': 'test partner', 'email': 'test.partner@test.lan'
})
cls.mail_server_user.write({
'from_filter': cls.user_employee.email,
'owner_user_id': cls.user_employee,
'smtp_user': cls.user_employee.email,
})
cls.user_employee.invalidate_recordset(['outgoing_mail_server_id'])
@contextmanager
def mock_mail_connect(self):
original_connect = IrMail_Server._connect__
self.connected_server_ids = []
def patched_connect(mail_server, *args, **kwargs):
self.connected_server_ids.append(kwargs.get('mail_server_id'))
original_connect(mail_server, *args, **kwargs)
with patch.object(IrMail_Server, '_connect__', autospec=True, wraps=IrMail_Server, side_effect=patched_connect):
yield
@users('admin', 'employee')
def test_personal_mail_server_allowed_post(self):
"""Check that only the owner of the mail server can create mails that will be sent from it."""
test_record = self.test_partner.with_user(self.env.user)
with self.mock_mail_connect():
test_record.message_post(
body='hello',
author_id=self.user_employee.partner_id.id, email_from=self.user_employee.email,
partner_ids=test_record.ids,
)
self.assertEqual(len(self.connected_server_ids), 1)
if self.env.user == self.mail_server_user.owner_user_id:
self.assertEqual(self.connected_server_ids[0], self.mail_server_user.id)
else:
self.assertNotEqual(self.connected_server_ids[0], self.mail_server_user.id)
# check disallowed exceptions
if self.env.user != self.mail_server_user.owner_user_id:
# check raise on invalid server at create
with self.assertRaises(ValidationError):
test_record.message_post(
body='hello',
author_id=self.user_employee.partner_id.id, email_from=self.user_employee.email,
mail_server_id=self.mail_server_user.id,
partner_ids=test_record.ids,
)
# check raise on invalid server at send (should not happen in normal flow)
mail = self.env['mail.mail'].sudo().create({
'body_html': 'hello',
'email_from': self.user_employee.email,
'author_id': self.user_employee.partner_id.id,
'partner_ids': test_record.ids,
})
with self.mock_mail_gateway(), self.assertRaisesRegex(UserError, "Unauthorized server for some of the sending mails."):
mail._send(self, mail_server=self.mail_server_user)
def test_personal_mail_server_find_mail_server(self):
"""Check that _find_mail_server only finds 'public' servers unless otherwise allowed."""
IrMailServer = self.env['ir.mail_server']
all_servers = IrMailServer.search([])
test_cases = [
(None, False),
(all_servers, True),
]
for mail_servers, should_find_personal in test_cases:
with self.subTest(mail_servers=mail_servers):
found_server, found_email_from = IrMailServer._find_mail_server(self.user_employee.email, mail_servers=mail_servers)
if should_find_personal:
self.assertEqual(
(found_server, found_email_from), (self.mail_server_user, self.user_employee.email),
'Passing in a server that is owned should allow finding it.'
)
else:
self.assertNotEqual(
found_server, self.mail_server_user,
'Finding a server for an email_from without specifying a list of servers should not find owned servers.'
)
@users('employee')
def test_immutable_create_uid(self):
"""Make sure create_uid is not writable, as it's a security assumption for these tests."""
message = self.test_partner.with_user(self.env.user).message_post(
body='hello',
author_id=self.user_employee.partner_id.id, email_from=self.user_employee.email,
partner_ids=self.test_partner.ids,
)
self.assertEqual(message.create_uid, self.user_employee)
message.create_uid = self.user_admin
self.assertEqual(message.create_uid, self.user_employee)
def test_personal_mail_server_mail_for_existing_message(self):
"""Crons should be able to send a mail from a personal server for an existing message."""
message = self.test_partner.with_user(self.user_employee).message_post(body='hello')
message.partner_ids += self.test_partner
with self.mock_mail_connect():
self.test_partner.with_user(self.env.ref('base.user_root'))._notify_thread(message)
self.assertEqual(self.connected_server_ids, [self.mail_server_user.id], "Should have used message creator's server.")

View file

@ -1,19 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from unittest.mock import patch
from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients
from odoo.addons.mail.tests.common import MailCommon
from odoo.addons.test_mail.tests.common import TestRecipients
from odoo.tests import tagged
from odoo.tools import mute_logger
@tagged("odoobot")
class TestOdoobot(TestMailCommon, TestRecipients):
class TestOdoobot(MailCommon, TestRecipients):
@classmethod
def setUpClass(cls):
super(TestOdoobot, cls).setUpClass()
super().setUpClass()
cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'})
cls.odoobot = cls.env.ref("base.partner_root")
@ -24,14 +22,14 @@ class TestOdoobot(TestMailCommon, TestRecipients):
'partner_ids': [],
'subtype_xmlid': 'mail.mt_comment'
}
cls.odoobot_ping_body = '<a href="http://odoo.com/web#model=res.partner&amp;id=%s" class="o_mail_redirect" data-oe-id="%s" data-oe-model="res.partner" target="_blank">@OdooBot</a>' % (cls.odoobot.id, cls.odoobot.id)
cls.odoobot_ping_body = f'<a href="http://odoo.com/odoo/res.partner/{cls.odoobot.id}" class="o_mail_redirect" data-oe-id="{cls.odoobot.id}" data-oe-model="res.partner" target="_blank">@OdooBot</a>'
cls.test_record_employe = cls.test_record.with_user(cls.user_employee)
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_fetch_listener(self):
channel = self.user_employee.with_user(self.user_employee)._init_odoobot()
odoobot = self.env.ref("base.partner_root")
odoobot_in_fetch_listeners = self.env['mail.channel.member'].search([('channel_id', '=', channel.id), ('partner_id', '=', odoobot.id)])
odoobot_in_fetch_listeners = self.env['discuss.channel.member'].search([('channel_id', '=', channel.id), ('partner_id', '=', odoobot.id)])
self.assertEqual(len(odoobot_in_fetch_listeners), 1, 'odoobot should appear only once in channel_fetch_listeners')
@mute_logger('odoo.addons.mail.models.mail_mail')

View file

@ -1,76 +1,68 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta
from markupsafe import Markup
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.test_mail.tests.test_performance import BaseMailPerformance
from odoo import Command
from odoo.addons.test_mail.tests.test_performance import BaseMailPostPerformance
from odoo.tests.common import users, warmup
from odoo.tests import tagged
from odoo.tools import mute_logger
@tagged('mail_performance', 'post_install', '-at_install')
class TestMailPerformance(BaseMailPerformance):
class FullBaseMailPerformance(BaseMailPostPerformance):
@classmethod
def setUpClass(cls):
super(TestMailPerformance, cls).setUpClass()
super().setUpClass()
# users / followers
cls.user_emp_email = mail_new_test_user(
cls.env,
company_id=cls.user_admin.company_id.id,
company_ids=[(4, cls.user_admin.company_id.id)],
email='user.emp.email@test.example.com',
login='user_emp_email',
groups='base.group_user,base.group_partner_manager',
name='Emmanuel Email',
notification_type='email',
signature='--\nEmmanuel',
)
cls.user_portal = mail_new_test_user(
cls.env,
company_id=cls.user_admin.company_id.id,
company_ids=[(4, cls.user_admin.company_id.id)],
email='user.portal@test.example.com',
login='user_portal',
groups='base.group_portal',
name='Paul Portal',
)
cls.customers = cls.env['res.partner'].create([
{'country_id': cls.env.ref('base.be').id,
'email': 'customer.full.test.1@example.com',
'name': 'Test Full Customer 1',
'mobile': '0456112233',
'phone': '0456112233',
# records
cls.record_containers = cls.env['mail.test.container.mc'].create([
{
'alias_name': 'test-alias-0',
'customer_id': cls.customers[0].id,
'name': 'Test Container 1',
},
{'country_id': cls.env.ref('base.be').id,
'email': 'customer.full.test.2@example.com',
'name': 'Test Full Customer 2',
'mobile': '0456223344',
'phone': '0456112233',
{
'alias_name': 'test-alias-1',
'customer_id': cls.customers[1].id,
'name': 'Test Container 2',
},
])
# record
cls.record_container = cls.env['mail.test.container.mc'].create({
'alias_name': 'test-alias',
'customer_id': cls.customer.id,
'name': 'Test Container',
})
cls.record_ticket = cls.env['mail.test.ticket.mc'].create({
cls.record_ticket_mc = cls.env['mail.test.ticket.mc'].create({
'email_from': 'email.from@test.example.com',
'container_id': cls.record_container.id,
'container_id': cls.record_containers[0].id,
'customer_id': False,
'name': 'Test Ticket',
'user_id': cls.user_emp_email.id,
'user_id': cls.user_follower_emp_email.id,
})
cls.record_ticket.message_subscribe(cls.customers.ids + cls.user_admin.partner_id.ids + cls.user_portal.partner_id.ids)
cls.record_ticket_mc.message_subscribe(
cls.customers.ids + cls.user_admin.partner_id.ids + cls.user_follower_portal.partner_id.ids
)
def test_initial_values(self):
cls.tracking_values_ids = [
(0, 0, {
'field_id': cls.env['ir.model.fields']._get(cls.record_ticket._name, 'email_from').id,
'new_value_char': 'new_value',
'old_value_char': 'old_value',
}),
(0, 0, {
'field_id': cls.env['ir.model.fields']._get(cls.record_ticket._name, 'customer_id').id,
'new_value_char': 'New Fake',
'new_value_integer': 2,
'old_value_char': 'Old Fake',
'old_value_integer': 1,
}),
]
@tagged('mail_performance', 'post_install', '-at_install')
class TestMailPerformance(FullBaseMailPerformance):
def test_assert_initial_values(self):
""" Simply ensure some values through all tests """
record_ticket = self.env['mail.test.ticket.mc'].browse(self.record_ticket.ids)
record_ticket = self.env['mail.test.ticket.mc'].browse(self.record_ticket_mc.ids)
self.assertEqual(record_ticket.message_partner_ids,
self.user_emp_email.partner_id + self.user_admin.partner_id + self.customers + self.user_portal.partner_id)
self.user_follower_emp_email.partner_id + self.user_admin.partner_id + self.customers + self.user_follower_portal.partner_id)
self.assertEqual(len(record_ticket.message_ids), 1)
@mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
@ -78,19 +70,384 @@ class TestMailPerformance(BaseMailPerformance):
@warmup
def test_message_post_w_followers(self):
""" Aims to cover as much features of message_post as possible """
record_ticket = self.env['mail.test.ticket.mc'].browse(self.record_ticket.ids)
record_ticket = self.env['mail.test.ticket.mc'].browse(self.record_ticket_mc.ids)
attachments = self.env['ir.attachment'].create(self.test_attachments_vals)
self.push_to_end_point_mocked.reset_mock() # reset as executed twice
self.flush_tracking()
with self.assertQueryCount(employee=91): # tmf: 60
with self.assertQueryCount(employee=108): # test_mail_full: 106
new_message = record_ticket.message_post(
attachment_ids=attachments.ids,
body='<p>Test Content</p>',
body=Markup('<p>Test Content</p>'),
email_add_signature=True,
mail_auto_delete=True,
message_type='comment',
subject='Test Subject',
subtype_xmlid='mail.mt_comment',
tracking_value_ids=self.tracking_values_ids,
)
self.assertEqual(
new_message.notified_partner_ids,
self.user_emp_email.partner_id + self.user_admin.partner_id + self.customers + self.user_portal.partner_id
self.user_follower_emp_email.partner_id + self.user_admin.partner_id + self.customers + self.user_follower_portal.partner_id
)
self.assertEqual(self.push_to_end_point_mocked.call_count, 8, "Not sure why 8")
@tagged('mail_performance', 'post_install', '-at_install')
class TestPortalFormatPerformance(FullBaseMailPerformance):
"""Test performance of `portal_message_format` with multiple messages
with multiple attachments, with ratings.
Those messages might not make sense functionally but they are crafted to
cover as much of the code as possible in regard to number of queries.
Setup :
* 5 records (self.containers -> 5 mail.test.rating records, with
a different customer_id each)
* 2 messages / record
* 2 attachments / message
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.test_users = cls.user_employee + cls.user_emp_inbox + cls.user_emp_email + cls.user_follower_emp_email + cls.user_follower_portal
# rating-enabled test records
with cls.mock_push_to_end_point(cls):
cls.record_ratings = cls.env['mail.test.rating'].create([
{
'customer_id': cls.customers[idx].id,
'name': f'TestRating_{idx}',
'user_id': cls.test_users[idx].id,
}
for idx in range(5)
])
# messages and ratings
user_id_field = cls.env['ir.model.fields']._get(cls.record_ratings._name, 'user_id')
comment_subtype_id = cls.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment')
cls.link_previews = cls.env["mail.link.preview"].create(
[
{"source_url": "https://www.odoo.com"},
{"source_url": "https://www.example.com"},
]
)
cls.messages_all = cls.env['mail.message'].sudo().create([
{
'attachment_ids': [
(0, 0, {
'datas': 'data',
'name': f'Test file {att_idx}',
'res_id': record.id,
'res_model': record._name,
})
for att_idx in range(2)
],
'author_id': record.customer_id.id,
'body': f'<p>Test {msg_idx}</p>',
'date': datetime(2023, 5, 15, 10, 30, 5),
'email_from': record.customer_id.email_formatted,
"message_link_preview_ids": [
Command.create({"link_preview_id": cls.link_previews[0].id}),
Command.create({"link_preview_id": cls.link_previews[1].id}),
],
'notification_ids': [
(0, 0, {
'is_read': False,
'notification_type': 'inbox',
'res_partner_id': cls.customers[(msg_idx * 2)].id,
}),
(0, 0, {
'is_read': True,
'notification_type': 'email',
'notification_status': 'sent',
'res_partner_id': cls.customers[(msg_idx * 2) + 1].id,
}),
],
'message_type': 'comment',
'model': record._name,
'partner_ids': [
(4, cls.customers[(msg_idx * 2)].id),
(4, cls.customers[record_idx].id),
],
'reaction_ids': [
(0, 0, {
'content': 'https://www.odoo.com',
'partner_id': cls.customers[(msg_idx * 2) + 1].id
}), (0, 0, {
'content': 'https://www.example.com',
'partner_id': cls.customers[record_idx].id
}),
],
'res_id': record.id,
'subject': f'Test Rating {msg_idx}',
'subtype_id': comment_subtype_id,
'starred_partner_ids': [
(4, cls.customers[(msg_idx * 2)].id),
(4, cls.customers[(msg_idx * 2) + 1].id),
],
'tracking_value_ids': [
(0, 0, {
'field_id': user_id_field.id,
'new_value_char': 'new 1',
'new_value_integer': record.user_id.id,
'old_value_char': 'old 1',
'old_value_integer': cls.user_admin.id,
}),
]
}
for msg_idx in range(2)
for record_idx, record in enumerate(cls.record_ratings)
])
cls.messages_records = [cls.env[message.model].browse(message.res_id) for message in cls.messages_all]
# ratings values related to rating-enabled records
cls.ratings_all = cls.env['rating.rating'].sudo().create([
{
'consumed': True,
'message_id': message.id,
'partner_id': record.customer_id.id,
'publisher_comment': 'Comment',
'publisher_id': cls.user_admin.partner_id.id,
'publisher_datetime': datetime(2023, 5, 15, 10, 30, 5) - timedelta(days=2),
'rated_partner_id': record.user_id.partner_id.id,
'rating': 4,
'res_id': message.res_id,
'res_model_id': cls.env['ir.model']._get_id(message.model),
}
for rating_idx in range(2)
for message, record in zip(cls.messages_all, cls.messages_records)
])
def test_assert_initial_values(self):
self.assertEqual(len(self.messages_all), 5 * 2)
self.assertEqual(len(self.ratings_all), len(self.messages_all) * 2)
@mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
@users('employee')
@warmup
def test_portal_message_format_norating(self):
messages_all = self.messages_all.with_user(self.env.user)
with self.assertQueryCount(employee=14):
# res = messages_all.portal_message_format(options=None)
res = messages_all.portal_message_format(options={'rating_include': False})
comment_subtype = self.env.ref('mail.mt_comment')
self.assertEqual(len(res), len(messages_all))
for format_res, message, record in zip(res, messages_all, self.messages_records):
self.assertEqual(len(format_res['attachment_ids']), 2)
self.maxDiff = None
self.assertEqual(
format_res['attachment_ids'],
[
{
'checksum': message.attachment_ids[0].checksum,
'filename': 'Test file 1',
'id': message.attachment_ids[0].id,
'mimetype': 'text/plain',
'name': 'Test file 1',
'raw_access_token': message.attachment_ids[0]._get_raw_access_token(),
'res_id': record.id,
'res_model': record._name,
}, {
'checksum': message.attachment_ids[1].checksum,
'filename': 'Test file 0',
'id': message.attachment_ids[1].id,
'mimetype': 'text/plain',
'name': 'Test file 0',
'raw_access_token': message.attachment_ids[1]._get_raw_access_token(),
'res_id': record.id,
'res_model': record._name,
}
]
)
self.assertEqual(format_res["author_id"]["id"], record.customer_id.id)
self.assertEqual(format_res["author_id"]["name"], record.customer_id.display_name)
self.assertEqual(format_res['author_avatar_url'], f'/web/image/mail.message/{message.id}/author_avatar/50x50')
self.assertEqual(format_res['date'], datetime(2023, 5, 15, 10, 30, 5))
self.assertEqual(' '.join(format_res['published_date_str'].split()), '05/15/2023 10:30:05 AM')
self.assertEqual(format_res['id'], message.id)
self.assertFalse(format_res['is_internal'])
self.assertFalse(format_res['is_message_subtype_note'])
self.assertEqual(format_res['subtype_id'], (comment_subtype.id, comment_subtype.name))
# should not be in, not asked
self.assertNotIn('rating_id', format_res)
self.assertNotIn('rating_stats', format_res)
self.assertNotIn('rating_value', format_res)
@mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
@users('employee')
@warmup
def test_portal_message_format_rating(self):
messages_all = self.messages_all.with_user(self.env.user)
with self.assertQueryCount(employee=28): # sometimes +1
res = messages_all.portal_message_format(options={'rating_include': True})
self.assertEqual(len(res), len(messages_all))
for format_res, _message, _record in zip(res, messages_all, self.messages_records):
self.assertEqual(format_res['rating_id']['publisher_avatar'], f'/web/image/res.partner/{self.partner_admin.id}/avatar_128/50x50')
self.assertEqual(format_res['rating_id']['publisher_comment'], 'Comment')
self.assertEqual(format_res['rating_id']['publisher_id'], self.partner_admin.id)
self.assertEqual(" ".join(format_res['rating_id']['publisher_datetime'].split()), '05/13/2023 10:30:05 AM')
self.assertEqual(format_res['rating_id']['publisher_name'], self.partner_admin.display_name)
self.assertDictEqual(
format_res['rating_stats'],
{'avg': 4.0, 'total': 4, 'percent': {1: 0.0, 2: 0.0, 3: 0.0, 4: 100.0, 5: 0.0}}
)
self.assertEqual(format_res['rating_value'], 4)
@mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
@users('employee')
@warmup
def test_portal_message_format_monorecord(self):
message = self.messages_all[0].with_user(self.env.user)
with self.assertQueryCount(employee=19): # randomness: 18+1
res = message.portal_message_format(options={'rating_include': True})
self.assertEqual(len(res), 1)
@mute_logger("odoo.tests", "odoo.addons.mail.models.mail_mail", "odoo.models.unlink")
@users("employee")
@warmup
def test_portal_attachment_as_author(self):
message = self.env["mail.message"].create(
{
"attachment_ids": [Command.create({"name": "test attachment"})],
"author_id": self.user_employee.partner_id.id,
}
)
res = message.portal_message_format()
self.assertEqual(
res[0]["attachment_ids"][0]["ownership_token"],
message.attachment_ids[0]._get_ownership_token(),
)
@tagged('rating', 'mail_performance', 'post_install', '-at_install')
class TestRatingPerformance(FullBaseMailPerformance):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.RECORD_COUNT = 20
cls.partners = cls.env['res.partner'].sudo().create([
{'name': 'Jean-Luc %s' % (idx), 'email': 'jean-luc-%s@opoo.com' % (idx)}
for idx in range(cls.RECORD_COUNT)])
# create records with 2 ratings to check batch statistics on them
responsibles = [cls.user_admin, cls.user_employee, cls.env['res.users']]
with cls.mock_push_to_end_point(cls):
cls.record_ratings = cls.env['mail.test.rating'].create([{
'customer_id': cls.partners[idx].id,
'name': f'Test Rating {idx}',
'user_id': responsibles[idx % 3].id,
} for idx in range(cls.RECORD_COUNT)])
rates = [enum % 5 for enum, _rec in enumerate(cls.record_ratings)]
# create rating from 1 -> 5 for each record
for rate, record in zip(rates, cls.record_ratings, strict=True):
record.rating_apply(rate + 1, token=record._rating_get_access_token())
# create rating with 4 or 5 (half records)
for record in cls.record_ratings[:10]:
record.rating_apply(4, token=record._rating_get_access_token())
for record in cls.record_ratings[10:]:
record.rating_apply(5, token=record._rating_get_access_token())
def apply_ratings(self, rate):
for record in self.record_ratings:
access_token = record._rating_get_access_token()
record.rating_apply(rate, token=access_token)
self.flush_tracking()
def create_ratings(self, model):
self.record_ratings = self.env[model].create([{
'customer_id': self.partners[idx].id,
'name': 'Test Rating',
'user_id': self.user_admin.id,
} for idx in range(self.RECORD_COUNT)])
self.flush_tracking()
@users('employee')
@warmup
def test_rating_api_rating_get_operator(self):
user_names = []
with self.assertQueryCount(employee=4): # tmf: 4
ratings = self.record_ratings.with_env(self.env)
for rating in ratings:
user_names.append(rating._rating_get_operator().name)
expected_names = ['Mitchell Admin', 'Ernest Employee', False] * 6 + ['Mitchell Admin', 'Ernest Employee']
for partner_name, expected_name in zip(user_names, expected_names, strict=True):
self.assertEqual(partner_name, expected_name)
@users('employee')
@warmup
def test_rating_api_rating_get_partner(self):
partner_names = []
with self.assertQueryCount(employee=3): # tmf: 3
ratings = self.record_ratings.with_env(self.env)
for rating in ratings:
partner_names.append(rating._rating_get_partner().name)
for partner_name, expected in zip(partner_names, self.partners, strict=True):
self.assertEqual(partner_name, expected.name)
@users('employee')
@warmup
def test_rating_get_grades_perfs(self):
with self.assertQueryCount(employee=1):
ratings = self.record_ratings.with_env(self.env)
grades = ratings.rating_get_grades()
self.assertDictEqual(grades, {'great': 28, 'okay': 4, 'bad': 8})
@users('employee')
@warmup
def test_rating_get_stats_perfs(self):
with self.assertQueryCount(employee=1):
ratings = self.record_ratings.with_env(self.env)
stats = ratings.rating_get_stats()
self.assertDictEqual(stats, {'avg': 3.75, 'total': 40, 'percent': {1: 10.0, 2: 10.0, 3: 10.0, 4: 35.0, 5: 35.0}})
@users('employee')
@warmup
def test_rating_last_value_perfs(self):
with self.assertQueryCount(employee=274): # tmf: 274
self.create_ratings('mail.test.rating.thread')
with self.assertQueryCount(employee=283): # tmf: 283
self.apply_ratings(1)
with self.assertQueryCount(employee=242): # tmf: 242
self.apply_ratings(5)
@users('employee')
@warmup
def test_rating_last_value_perfs_with_rating_mixin(self):
with self.assertQueryCount(employee=317): # tmf: 317
self.create_ratings('mail.test.rating')
with self.assertQueryCount(employee=325): # tmf: 325
self.apply_ratings(1)
with self.assertQueryCount(employee=304): # tmf: 304
self.apply_ratings(5)
with self.assertQueryCount(employee=1):
self.record_ratings._compute_rating_last_value()
vals = (val == 5 for val in self.record_ratings.mapped('rating_last_value'))
self.assertTrue(all(vals), "The last rating is kept.")
@users('employee')
@warmup
def test_rating_stat_fields(self):
expected_texts = ['ok', 'ok', 'ok', 'top', 'top'] * 2 + ['ok', 'ok', 'top', 'top', 'top'] * 2
expected_satis = [50.0, 50.0, 50.0, 100.0, 100.0] * 4
with self.assertQueryCount(employee=2):
ratings = self.record_ratings.with_env(self.env)
for rating, text, satisfaction in zip(ratings, expected_texts, expected_satis, strict=True):
self.assertEqual(rating.rating_avg_text, text)
self.assertEqual(rating.rating_percentage_satisfaction, satisfaction)

View file

@ -48,7 +48,7 @@ class TestMailThreadInternals(TestMailThreadInternalsCommon):
with self.subTest(test_record=test_record):
is_portal = test_record._name != 'mail.test.simple'
has_customer = test_record._name != 'mail.test.portal.no.partner'
partner_fnames = test_record._mail_get_partner_fields()
partner_fnames = test_record._mail_get_partner_fields(introspect_fields=False)
if is_portal:
self.assertFalse(
@ -56,7 +56,9 @@ class TestMailThreadInternals(TestMailThreadInternalsCommon):
'By default access tokens are False with portal'
)
groups = test_record._notify_get_recipients_groups()
groups = test_record._notify_get_recipients_groups(
self.env['mail.message'], False,
)
portal_customer_group = next(
(group for group in groups if group[0] == 'portal_customer'),
False

View file

@ -23,22 +23,23 @@ class TestMassMailing(TestMailFullCommon):
# optout records 1 and 2
(recipients[1] | recipients[2]).write({'opt_out': True})
recipients[1].email_from = f'"Format Me" <{recipients[1].email_from}>'
recipients[1].email_from = f'"Format Me" <{recipients[1].email_normalized}>'
# blacklist records 3 and 4
self.env['mail.blacklist'].create({'email': recipients[3].email_normalized})
self.env['mail.blacklist'].create({'email': recipients[4].email_normalized})
recipients[3].email_from = f'"Format Me" <{recipients[3].email_from}>'
recipients[3].email_from = f'"Format Me" <{recipients[3].email_normalized}>'
# have a duplicate email for 9
recipients[9].email_from = f'"Format Me" <{recipients[9].email_from}>'
recipients[9].email_from = f'"Format Me" <{recipients[9].email_normalized}>'
recipient_dup_1 = recipients[9].copy()
recipient_dup_1.email_from = f'"Format Me" <{recipient_dup_1.email_from}>'
recipient_dup_1.email_from = f'"Format Me" <{recipient_dup_1.email_normalized}>'
# have another duplicate for 9, but with multi emails already done
recipient_dup_2 = recipients[9].copy()
recipient_dup_2.email_from += f'; "TestDupe" <{recipients[8].email_from}>'
recipient_dup_2.email_from += f'; "TestDupe" <{recipients[8].email_normalized}>'
# have another duplicate for 9, but with multi emails, one is different
recipient_dup_3 = recipients[9].copy() # this one will passthrough (best-effort)
recipient_dup_3.email_from += '; "TestMulti" <test.multi@test.example.com>'
recipient_dup_4 = recipient_dup_2.copy() # this one will be discarded (youpi)
# have a void mail
recipient_void_1 = self.env['mailing.test.optout'].create({'name': 'TestRecord_void_1'})
# have a falsy mail
@ -57,71 +58,79 @@ class TestMassMailing(TestMailFullCommon):
mailing.action_send_mail()
for recipient in recipients_all:
recipient_info = {
'email': recipient.email_normalized,
'content': f'Hello {recipient.name}',
'mail_values': {
'subject': f'Subject {recipient.name}',
},
}
# opt-out: cancel (cancel mail)
if recipient in recipients[1] | recipients[2]:
recipient_info['trace_status'] = "cancel"
recipient_info['failure_type'] = "mail_optout"
# blacklisted: cancel (cancel mail)
elif recipient in recipients[3] | recipients[4]:
recipient_info['trace_status'] = "cancel"
recipient_info['failure_type'] = "mail_bl"
# duplicates: cancel (cancel mail)
elif recipient in (recipient_dup_1, recipient_dup_2, recipient_dup_4):
recipient_info['trace_status'] = "cancel"
recipient_info['failure_type'] = "mail_dup"
# void: error (failed mail)
elif recipient == recipient_void_1:
recipient_info['trace_status'] = 'cancel'
recipient_info['failure_type'] = "mail_email_missing"
# falsy: error (failed mail)
elif recipient == recipient_falsy_1:
recipient_info['trace_status'] = "cancel"
recipient_info['failure_type'] = "mail_email_invalid"
recipient_info['email'] = recipient.email_from # normalized is False but email should be falsymail
else:
# multi email -> outgoing email contains all emails
with self.subTest(recipient_from=recipient.email_from):
recipient_info = {
'content': f'Hello {recipient.name}',
'email': recipient.email_normalized or '',
'email_to_mail': recipient.email_from or '',
'email_to_recipients': [[recipient.email_from]],
'mail_values': {
'subject': f'Subject {recipient.name}',
},
}
# ; transformed into comma
if recipient == recipient_dup_2:
recipient_info['email_to_mail'] = '"Format Me" <test.record.09@test.example.com>,"TestDupe" <test.record.08@test.example.com>'
if recipient == recipient_dup_3:
email = self._find_sent_email(self.user_marketing.email_formatted, ['test.record.09@test.example.com', 'test.multi@test.example.com'])
recipient_info['email_to_mail'] = '"Format Me" <test.record.09@test.example.com>,"TestMulti" <test.multi@test.example.com>'
# multi email -> outgoing email contains all emails
recipient_info['email_to_recipients'] = [['"Format Me" <test.record.09@test.example.com>', '"TestMulti" <test.multi@test.example.com>']]
if recipient == recipient_dup_4:
recipient_info['email_to_mail'] = '"Format Me" <test.record.09@test.example.com>,"TestDupe" <test.record.08@test.example.com>'
# opt-out: cancel (cancel mail)
if recipient in recipients[1] | recipients[2]:
recipient_info['trace_status'] = "cancel"
recipient_info['failure_type'] = "mail_optout"
# blacklisted: cancel (cancel mail)
elif recipient in recipients[3] | recipients[4]:
recipient_info['trace_status'] = "cancel"
recipient_info['failure_type'] = "mail_bl"
# duplicates: cancel (cancel mail)
elif recipient in (recipient_dup_1, recipient_dup_2, recipient_dup_4):
recipient_info['trace_status'] = "cancel"
recipient_info['failure_type'] = "mail_dup"
# void: cancel (cancel mail)
elif recipient == recipient_void_1:
recipient_info['trace_status'] = 'cancel'
recipient_info['failure_type'] = "mail_email_missing"
# falsy: cancel (cancel mail)
elif recipient == recipient_falsy_1:
recipient_info['trace_status'] = "cancel"
recipient_info['failure_type'] = "mail_email_invalid"
recipient_info['email'] = recipient.email_from # normalized is False but email should be falsymail
else:
email = self._find_sent_email(self.user_marketing.email_formatted, [recipient.email_normalized])
# preview correctly integrated rendered qweb
self.assertIn(
'Hi %s :)' % recipient.name,
email['body'])
# rendered unsubscribe
self.assertIn(
'%s/mailing/%s/confirm_unsubscribe' % (mailing.get_base_url(), mailing.id),
email['body'])
unsubscribe_href = self._get_href_from_anchor_id(email['body'], "url6")
unsubscribe_url = werkzeug.urls.url_parse(unsubscribe_href)
unsubscribe_params = unsubscribe_url.decode_query().to_dict(flat=True)
self.assertEqual(int(unsubscribe_params['res_id']), recipient.id)
self.assertEqual(unsubscribe_params['email'], recipient.email_normalized)
self.assertEqual(
mailing._unsubscribe_token(unsubscribe_params['res_id'], (unsubscribe_params['email'])),
unsubscribe_params['token']
)
# rendered view
self.assertIn(
'%s/mailing/%s/view' % (mailing.get_base_url(), mailing.id),
email['body'])
view_href = self._get_href_from_anchor_id(email['body'], "url6")
view_url = werkzeug.urls.url_parse(view_href)
view_params = view_url.decode_query().to_dict(flat=True)
self.assertEqual(int(view_params['res_id']), recipient.id)
self.assertEqual(view_params['email'], recipient.email_normalized)
self.assertEqual(
mailing._unsubscribe_token(view_params['res_id'], (view_params['email'])),
view_params['token']
)
email = self._find_sent_email(self.user_marketing.email_formatted, recipient_info['email_to_recipients'][0])
# preview correctly integrated rendered qweb
self.assertIn(
'Hi %s :)' % recipient.name,
email['body'])
# rendered unsubscribe
self.assertIn(
'%s/mailing/%s/confirm_unsubscribe' % (mailing.get_base_url(), mailing.id),
email['body'])
unsubscribe_href = self._get_href_from_anchor_id(email['body'], "url6")
unsubscribe_url = werkzeug.urls.url_parse(unsubscribe_href)
unsubscribe_params = unsubscribe_url.decode_query().to_dict(flat=True)
self.assertEqual(int(unsubscribe_params['document_id']), recipient.id)
self.assertEqual(unsubscribe_params['email'], recipient.email_normalized)
self.assertEqual(
mailing._generate_mailing_recipient_token(unsubscribe_params['document_id'], (unsubscribe_params['email'])),
unsubscribe_params['hash_token']
)
# rendered view
self.assertIn(
'%s/mailing/%s/view' % (mailing.get_base_url(), mailing.id),
email['body'])
view_href = self._get_href_from_anchor_id(email['body'], "url6")
view_url = werkzeug.urls.url_parse(view_href)
view_params = view_url.decode_query().to_dict(flat=True)
self.assertEqual(int(view_params['document_id']), recipient.id)
self.assertEqual(view_params['email'], recipient.email_normalized)
self.assertEqual(
mailing._generate_mailing_recipient_token(view_params['document_id'], (view_params['email'])),
view_params['hash_token']
)
self.assertMailTraces(
[recipient_info], mailing, recipient,

View file

@ -2,28 +2,27 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from werkzeug.urls import url_parse, url_decode, url_encode
import json
from odoo import http
from odoo.addons.auth_signup.models.res_partner import ResPartner
from odoo.addons.mail.tests.common import MailCommon
from odoo.addons.test_mail_full.tests.common import TestMailFullCommon
from odoo.addons.test_mail_sms.tests.common import TestSMSRecipients
from odoo.exceptions import AccessError
from odoo.tests import tagged, users
from odoo.tests.common import HttpCase
from odoo.tools import mute_logger
from odoo.tools import html_escape, mute_logger
@tagged('portal')
class TestPortal(HttpCase, TestMailFullCommon, TestSMSRecipients):
class TestPortal(TestMailFullCommon, TestSMSRecipients):
def setUp(self):
super(TestPortal, self).setUp()
super().setUp()
self.record_portal = self.env['mail.test.portal'].create({
'partner_id': self.partner_1.id,
'name': 'Test Portal Record',
})
self.record_portal._portal_ensure_token()
@ -36,12 +35,22 @@ class TestPortalControllers(TestPortal):
'model': self.record_portal._name,
'res_id': self.record_portal.id,
})
response = self.url_open(f'/mail/avatar/mail.message/{mail_record.id}/author_avatar/50x50?access_token={self.record_portal.access_token}')
token = self.record_portal.access_token
formatted_record = mail_record.portal_message_format(options={"token": token})[0]
self.assertEqual(
formatted_record.get("author_avatar_url"),
f"/mail/avatar/mail.message/{mail_record.id}/author_avatar/50x50?access_token={token}",
)
response = self.url_open(
f"/mail/avatar/mail.message/{mail_record.id}/author_avatar/50x50?access_token={token}"
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers.get('Content-Type'), 'image/png')
self.assertRegex(response.headers.get('Content-Disposition', ''), r'mail_message-\d+-author_avatar\.png')
self.assertEqual(response.headers.get('Content-Type'), 'image/svg+xml; charset=utf-8')
self.assertRegex(response.headers.get('Content-Disposition', ''), r'mail_message-\d+-author_avatar\.svg')
placeholder_response = self.url_open(f'/mail/avatar/mail.message/{mail_record.id}/author_avatar/50x50?access_token={self.record_portal.access_token + "a"}') # false token
placeholder_response = self.url_open(
f'/mail/avatar/mail.message/{mail_record.id}/author_avatar/50x50?access_token={token + "a"}'
) # false token
self.assertEqual(placeholder_response.status_code, 200)
self.assertEqual(placeholder_response.headers.get('Content-Type'), 'image/png')
self.assertRegex(placeholder_response.headers.get('Content-Disposition', ''), r'placeholder\.png')
@ -53,107 +62,71 @@ class TestPortalControllers(TestPortal):
def test_portal_avatar_with_hash_pid(self):
self.authenticate(None, None)
post_url = f"{self.record_portal.get_base_url()}/mail/chatter_post"
res = self.opener.post(
post_url = f"{self.record_portal.get_base_url()}/mail/message/post"
pid = self.partner_2.id
_hash = self.record_portal._sign_token(pid)
res = self.url_open(
url=post_url,
json={
'params': {
'csrf_token': http.Request.csrf_token(self),
'message': 'Test',
'res_model': self.record_portal._name,
'res_id': self.record_portal.id,
'hash': self.record_portal._sign_token(self.partner_2.id),
'pid': self.partner_2.id,
'thread_model': self.record_portal._name,
'thread_id': self.record_portal.id,
'post_data': {'body': "Test"},
'hash': _hash,
'pid': pid,
},
},
)
res.raise_for_status()
self.assertNotIn("error", res.json())
message = self.record_portal.message_ids[0]
formatted_message = message.portal_message_format(options={"hash": _hash, "pid": pid})[0]
self.assertEqual(
formatted_message.get("author_avatar_url"),
f"/mail/avatar/mail.message/{message.id}/author_avatar/50x50?_hash={_hash}&pid={pid}",
)
response = self.url_open(
f'/mail/avatar/mail.message/{message.id}/author_avatar/50x50?_hash={self.record_portal._sign_token(self.partner_2.id)}&pid={self.partner_2.id}')
f"/mail/avatar/mail.message/{message.id}/author_avatar/50x50?_hash={_hash}&pid={pid}"
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers.get('Content-Type'), 'image/png')
self.assertRegex(response.headers.get('Content-Disposition', ''), r'mail_message-\d+-author_avatar\.png')
self.assertEqual(response.headers.get('Content-Type'), 'image/svg+xml; charset=utf-8')
self.assertRegex(response.headers.get('Content-Disposition', ''), r'mail_message-\d+-author_avatar\.svg')
placeholder_response = self.url_open(
f'/mail/avatar/mail.message/{message.id}/author_avatar/50x50?_hash={self.record_portal._sign_token(self.partner_2.id) + "a"}&pid={self.partner_2.id}') # false hash
f'/mail/avatar/mail.message/{message.id}/author_avatar/50x50?_hash={_hash + "a"}&pid={pid}'
) # false hash
self.assertEqual(placeholder_response.status_code, 200)
self.assertEqual(placeholder_response.headers.get('Content-Type'), 'image/png')
self.assertRegex(placeholder_response.headers.get('Content-Disposition', ''), r'placeholder\.png')
def test_portal_message_fetch(self):
"""Test retrieving chatter messages through the portal controller"""
self.authenticate(None, None)
message_fetch_url = '/mail/chatter_fetch'
payload = json.dumps({
'jsonrpc': '2.0',
'method': 'call',
'id': 0,
'params': {
'res_model': 'mail.test.portal',
'res_id': self.record_portal.id,
'token': self.record_portal.access_token,
},
})
def get_chatter_message_count():
res = self.url_open(
url=message_fetch_url,
data=payload,
headers={'Content-Type': 'application/json'}
)
return res.json().get('result', {}).get('message_count', 0)
self.assertEqual(get_chatter_message_count(), 0)
for _ in range(8):
self.record_portal.message_post(
body='Test',
author_id=self.partner_1.id,
message_type='comment',
subtype_id=self.env.ref('mail.mt_comment').id,
)
self.assertEqual(get_chatter_message_count(), 8)
# Empty the body of a few messages
for i in (2, 5, 6):
self.record_portal.message_ids[i].body = ""
# Empty messages should be ignored
self.assertEqual(get_chatter_message_count(), 5)
def test_portal_share_comment(self):
""" Test posting through portal controller allowing to use a hash to
post wihtout access rights. """
self.authenticate(None, None)
post_url = f"{self.record_portal.get_base_url()}/mail/chatter_post"
post_url = f"{self.record_portal.get_base_url()}/mail/message/post"
# test as not logged
self.opener.post(
self.url_open(
url=post_url,
json={
'params': {
'csrf_token': http.Request.csrf_token(self),
'hash': self.record_portal._sign_token(self.partner_2.id),
'message': 'Test',
'pid': self.partner_2.id,
'redirect': '/',
'res_model': self.record_portal._name,
'res_id': self.record_portal.id,
'thread_model': self.record_portal._name,
'thread_id': self.record_portal.id,
'post_data': {'body': "Test"},
'token': self.record_portal.access_token,
'hash': self.record_portal._sign_token(self.partner_2.id),
'pid': self.partner_2.id,
},
},
)
message = self.record_portal.message_ids[0]
# Only messages from the current user not OdooBot
messages = self.record_portal.message_ids.filtered(lambda msg: msg.author_id == self.partner_2)
self.assertIn('Test', message.body)
self.assertEqual(message.author_id, self.partner_2)
self.assertIn('Test', messages[0].body)
@tagged('-at_install', 'post_install', 'portal', 'mail_controller')
class TestPortalFlow(TestMailFullCommon, HttpCase):
class TestPortalFlow(MailCommon, HttpCase):
""" Test shared links, mail/view links and redirection (backend, customer
portal or frontend for specific addons). """
@ -164,7 +137,6 @@ class TestPortalFlow(TestMailFullCommon, HttpCase):
'country_id': cls.env.ref('base.fr').id,
'email': 'mdelvaux34@example.com',
'lang': 'en_US',
'mobile': '+33639982325',
'name': 'Mathias Delvaux',
'phone': '+33353011823',
})
@ -198,6 +170,16 @@ class TestPortalFlow(TestMailFullCommon, HttpCase):
})
cls._create_portal_user()
# The test relies on `record_access_url` to check the validity of mails being sent,
# however, when auth_signup is installed, a new token is generated each time the url
# is being requested.
# By removing the time-based hashing from this function we can ensure the stability of
# the url during the tests.
def patched_generate_signup_token(self, *_, **__):
self.ensure_one()
return str([self.id, self._get_login_date(), self.signup_type])
cls.classPatch(ResPartner, '_generate_signup_token', patched_generate_signup_token)
# prepare access URLs on self to ease tests
# ------------------------------------------------------------
base_url = cls.record_portal.get_base_url()
@ -220,7 +202,9 @@ class TestPortalFlow(TestMailFullCommon, HttpCase):
cls.record_url_no_model = f'{cls.record_portal.get_base_url()}/mail/view?model=this.should.not.exists&res_id=1'
# find portal + auth data url
for group_name, group_func, group_data in cls.record_portal.sudo()._notify_get_recipients_groups(False):
for group_name, group_func, group_data in cls.record_portal.sudo()._notify_get_recipients_groups(
cls.env['mail.message'], False
):
if group_name == 'portal_customer' and group_func(cls.customer):
cls.record_portal_url_auth = group_data['button_access']['url']
break
@ -244,25 +228,25 @@ class TestPortalFlow(TestMailFullCommon, HttpCase):
cls.portal_web_url = f'{base_url}/my/test_portal/{cls.record_portal.id}'
cls.portal_web_url_with_token = f'{base_url}/my/test_portal/{cls.record_portal.id}?{url_encode({"access_token": cls.record_portal.access_token, "pid": cls.customer.id, "hash": cls.record_portal_hash}, sort=True)}'
cls.public_act_url_share = f'{base_url}/test_portal/public_type/{cls.record_public_act_url.id}'
cls.internal_backend_local_url = f'/web#{url_encode({"model": cls.record_internal._name, "id": cls.record_internal.id, "active_id": cls.record_internal.id, "cids": cls.company_admin.id}, sort=True)}'
cls.portal_backend_local_url = f'/web#{url_encode({"model": cls.record_portal._name, "id": cls.record_portal.id, "active_id": cls.record_portal.id, "cids": cls.company_admin.id}, sort=True)}'
cls.read_backend_local_url = f'/web#{url_encode({"model": cls.record_read._name, "id": cls.record_read.id, "active_id": cls.record_read.id, "cids": cls.company_admin.id}, sort=True)}'
cls.public_act_url_backend_local_url = f'/web#{url_encode({"model": cls.record_public_act_url._name, "id": cls.record_public_act_url.id, "active_id": cls.record_public_act_url.id, "cids": cls.company_admin.id}, sort=True)}'
cls.discuss_local_url = '/web#action=mail.action_discuss'
cls.internal_backend_local_url = f'/odoo/{cls.record_internal._name}/{cls.record_internal.id}'
cls.portal_backend_local_url = f'/odoo/{cls.record_portal._name}/{cls.record_portal.id}'
cls.read_backend_local_url = f'/odoo/{cls.record_read._name}/{cls.record_read.id}'
cls.public_act_url_backend_local_url = f'/odoo/{cls.record_public_act_url._name}/{cls.record_public_act_url.id}'
cls.discuss_local_url = '/odoo/action-mail.action_discuss'
def test_assert_initial_data(self):
""" Test some initial values. Test that record_access_url is a valid URL
""" Test some initial values. Test that record_portal_url_auth is a valid URL
to view the record_portal and that record_access_url_wrong_token only differs
from record_access_url by a different access_token. """
self.record_internal.with_user(self.user_employee).check_access_rule('read')
self.record_portal.with_user(self.user_employee).check_access_rule('read')
self.record_read.with_user(self.user_employee).check_access_rule('read')
from record_portal_url_auth by a different access_token. """
self.record_internal.with_user(self.user_employee).check_access('read')
self.record_portal.with_user(self.user_employee).check_access('read')
self.record_read.with_user(self.user_employee).check_access('read')
with self.assertRaises(AccessError):
self.record_internal.with_user(self.user_portal).check_access_rights('read')
self.record_internal.with_user(self.user_portal).check_access('read')
with self.assertRaises(AccessError):
self.record_portal.with_user(self.user_portal).check_access_rights('read')
self.record_read.with_user(self.user_portal).check_access_rights('read')
self.record_portal.with_user(self.user_portal).check_access('read')
self.record_read.with_user(self.user_portal).check_access('read')
self.assertNotEqual(self.record_portal_url_auth, self.record_portal_url_auth_wrong_token)
url_params = []
@ -331,7 +315,7 @@ class TestPortalFlow(TestMailFullCommon, HttpCase):
# std url, read record -> redirect to my with parameters being record portal action parameters (???)
(
'Access record (no customer portal)', self.record_read_url_base,
f'{self.test_base_url}/my#{url_encode({"model": self.record_read._name, "id": self.record_read.id, "active_id": self.record_read.id, "cids": self.company_admin.id}, sort=True)}',
f'{self.test_base_url}/my?{url_encode({"subpath": f"{self.record_read._name}/{self.record_read.id}"})}',
),
# std url, no access to record -> redirect to my
(
@ -445,6 +429,63 @@ class TestPortalFlow(TestMailFullCommon, HttpCase):
'Failed with %s - %s' % (model, res_id)
)
def assert_URL(self, url, expected_path, expected_fragment_params=None, expected_query=None):
"""Asserts that the URL has the expected path and if set, the expected fragment parameters and query."""
parsed_url = url_parse(url)
fragment_params = url_decode(parsed_url.fragment)
self.assertEqual(parsed_url.path, expected_path)
if expected_fragment_params:
for key, expected_value in expected_fragment_params.items():
self.assertEqual(fragment_params.get(key), expected_value,
f'Expected: "{key}={expected_value}" (for path: {expected_path})')
if expected_query:
self.assertEqual(expected_query, parsed_url.query,
f'Expected: query="{expected_query}" (for path: {expected_path})')
@users('employee')
def test_send_message_to_customer(self):
"""Same as test_send_message_to_customer_using_template but without a template."""
composer = self.env['mail.compose.message'].with_context(
self._get_mail_composer_web_context(
self.record_portal,
default_email_layout_xmlid='mail.mail_notification_layout_with_responsible_signature',
)
).create({
'body': '<p>Hello Mathias Delvaux, your quotation is ready for review.</p>',
'partner_ids': self.customer.ids,
'subject': 'Your Quotation "a white table"',
})
with self.mock_mail_gateway(mail_unlink_sent=True):
composer._action_send_mail()
self.assertEqual(len(self._mails), 1)
self.assertIn(f'"{html_escape(self.record_portal_url_auth)}"', self._mails[0].get('body'))
# Check that the template is not used (not the same subject)
self.assertEqual('Your Quotation "a white table"', self._mails[0].get('subject'))
self.assertIn('Hello Mathias Delvaux', self._mails[0].get('body'))
@users('employee')
def test_send_message_to_customer_using_template(self):
"""Send a mail to a customer without an account and check that it contains a link to view the record.
Other tests below check that that same link has the correct behavior.
This test follows the common use case by using a template while the next send the mail without a template."""
composer = self.env['mail.compose.message'].with_context(
self._get_mail_composer_web_context(
self.record_portal,
default_email_layout_xmlid='mail.mail_notification_layout_with_responsible_signature',
default_template_id=self.mail_template.id,
)
).create({})
with self.mock_mail_gateway(mail_unlink_sent=True):
composer._action_send_mail()
self.assertEqual(len(self._mails), 1)
self.assertIn(f'"{html_escape(self.record_portal_url_auth)}"', self._mails[0].get('body'))
self.assertEqual(f'Your quotation "{self.record_portal.name}"', self._mails[0].get('subject')) # Check that the template is used
@tagged('portal')
class TestPortalMixin(TestPortal):

View file

@ -8,11 +8,12 @@ from odoo import http
from odoo.addons.test_mail_full.tests.common import TestMailFullCommon
from odoo.addons.test_mail_sms.tests.common import TestSMSRecipients
from odoo.tests import tagged
from odoo.tests.common import HttpCase, users, warmup
from odoo.tests.common import users, warmup
from odoo.tools import mute_logger
class TestRatingCommon(TestMailFullCommon, TestSMSRecipients):
@classmethod
def setUpClass(cls):
super(TestRatingCommon, cls).setUpClass()
@ -22,88 +23,104 @@ class TestRatingCommon(TestMailFullCommon, TestSMSRecipients):
'name': 'Test Rating',
'user_id': cls.user_admin.id,
})
cls.record_rating_thread = cls.env['mail.test.rating.thread'].create({
'customer_id': cls.partner_1.id,
'name': 'Test rating without rating mixin',
'user_id': cls.user_admin.id,
})
@tagged('rating')
class TestRatingFlow(TestRatingCommon):
def test_initial_values(self):
record_rating = self.record_rating.with_env(self.env)
self.assertFalse(record_rating.rating_ids)
self.assertEqual(record_rating.message_partner_ids, self.partner_admin)
self.assertEqual(len(record_rating.message_ids), 1)
for record_rating in [self.record_rating, self.record_rating_thread]:
record_rating = record_rating.with_env(self.env)
self.assertFalse(record_rating.rating_ids)
self.assertEqual(record_rating.message_partner_ids, self.partner_admin)
self.assertEqual(len(record_rating.message_ids), 1)
@users('employee')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_rating_prepare(self):
record_rating = self.record_rating.with_env(self.env)
for record_rating, desc in ((self.record_rating, 'With rating mixin'),
(self.record_rating_thread, 'Without rating mixin')):
with self.subTest(desc):
record_rating = record_rating.with_env(self.env)
# prepare rating token
access_token = record_rating._rating_get_access_token()
# prepare rating token
access_token = record_rating._rating_get_access_token()
# check rating creation
rating = record_rating.rating_ids
self.assertEqual(rating.access_token, access_token)
self.assertFalse(rating.consumed)
self.assertFalse(rating.is_internal)
self.assertEqual(rating.partner_id, self.partner_1)
self.assertEqual(rating.rated_partner_id, self.user_admin.partner_id)
self.assertFalse(rating.rating)
# check rating creation
rating = record_rating.rating_ids
self.assertEqual(rating.access_token, access_token)
self.assertFalse(rating.consumed)
self.assertFalse(rating.is_internal)
self.assertEqual(rating.partner_id, self.partner_1)
self.assertEqual(rating.rated_partner_id, self.user_admin.partner_id)
self.assertFalse(rating.rating)
@users('employee')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_rating_rating_apply(self):
record_rating = self.record_rating.with_env(self.env)
record_messages = record_rating.message_ids
for record_rating, expected_subtype, is_rating_mixin_test in (
(self.record_rating_thread, self.env.ref('mail.mt_comment'), False),
(self.record_rating, self.env.ref('test_mail_full.mt_mail_test_rating_rating_done'), True),
):
with self.subTest('With rating mixin' if is_rating_mixin_test else 'Without rating mixin'):
record_rating = record_rating.with_env(self.env)
record_messages = record_rating.message_ids
# prepare rating token
access_token = record_rating._rating_get_access_token()
# prepare rating token
access_token = record_rating._rating_get_access_token()
# simulate an email click: notification should be delayed
with self.mock_mail_gateway(mail_unlink_sent=False), self.mock_mail_app():
record_rating.rating_apply(5, token=access_token, feedback='Top Feedback', notify_delay_send=True)
message = record_rating.message_ids[0]
rating = record_rating.rating_ids
# simulate an email click: notification should be delayed
with self.mock_mail_gateway(mail_unlink_sent=False), self.mock_mail_app():
record_rating.rating_apply(5, token=access_token, feedback='Top Feedback', notify_delay_send=True)
message = record_rating.message_ids[0]
rating = record_rating.rating_ids
# check posted message
self.assertEqual(record_rating.message_ids, record_messages + message)
self.assertIn('Top Feedback', message.body)
self.assertIn('/rating/static/src/img/rating_5.png', message.body)
self.assertEqual(message.author_id, self.partner_1)
self.assertEqual(message.rating_ids, rating)
self.assertFalse(message.notified_partner_ids)
self.assertEqual(message.subtype_id, self.env.ref('test_mail_full.mt_mail_test_rating_rating_done'))
# check posted message
self.assertEqual(record_rating.message_ids, record_messages + message)
self.assertIn('Top Feedback', message.body)
self.assertIn('/rating/static/src/img/rating_5.png', message.body)
self.assertEqual(message.author_id, self.partner_1)
self.assertEqual(message.rating_ids, rating)
self.assertFalse(message.notified_partner_ids)
self.assertEqual(message.subtype_id, expected_subtype)
# check rating update
self.assertTrue(rating.consumed)
self.assertEqual(rating.feedback, 'Top Feedback')
self.assertEqual(rating.message_id, message)
self.assertEqual(rating.rating, 5)
self.assertEqual(record_rating.rating_last_value, 5)
# check rating update
self.assertTrue(rating.consumed)
self.assertEqual(rating.feedback, 'Top Feedback')
self.assertEqual(rating.message_id, message)
self.assertEqual(rating.rating, 5)
if is_rating_mixin_test:
self.assertEqual(record_rating.rating_last_value, 5)
# give a feedback: send notifications (notify_delay_send set to False)
with self.mock_mail_gateway(mail_unlink_sent=False), self.mock_mail_app():
record_rating.rating_apply(1, token=access_token, feedback='Bad Feedback')
# give a feedback: send notifications (notify_delay_send set to False)
with self.mock_mail_gateway(mail_unlink_sent=False), self.mock_mail_app():
record_rating.rating_apply(1, token=access_token, feedback='Bad Feedback')
# check posted message: message is updated
update_message = record_rating.message_ids[0]
self.assertEqual(update_message, message, 'Should update first message')
self.assertEqual(record_rating.message_ids, record_messages + update_message)
self.assertIn('Bad Feedback', update_message.body)
self.assertIn('/rating/static/src/img/rating_1.png', update_message.body)
self.assertEqual(update_message.author_id, self.partner_1)
self.assertEqual(update_message.rating_ids, rating)
self.assertEqual(update_message.notified_partner_ids, self.partner_admin)
self.assertEqual(update_message.subtype_id, self.env.ref("test_mail_full.mt_mail_test_rating_rating_done"))
# check posted message: message is updated
update_message = record_rating.message_ids[0]
self.assertEqual(update_message, message, 'Should update first message')
self.assertEqual(record_rating.message_ids, record_messages + update_message)
self.assertIn('Bad Feedback', update_message.body)
self.assertIn('/rating/static/src/img/rating_1.png', update_message.body)
self.assertEqual(update_message.author_id, self.partner_1)
self.assertEqual(update_message.rating_ids, rating)
self.assertEqual(update_message.notified_partner_ids, self.partner_admin)
self.assertEqual(update_message.subtype_id, expected_subtype)
# check rating update
new_rating = record_rating.rating_ids
self.assertEqual(new_rating, rating, 'Should update first rating')
self.assertTrue(new_rating.consumed)
self.assertEqual(new_rating.feedback, 'Bad Feedback')
self.assertEqual(new_rating.message_id, update_message)
self.assertEqual(new_rating.rating, 1)
self.assertEqual(record_rating.rating_last_value, 1)
# check rating update
new_rating = record_rating.rating_ids
self.assertEqual(new_rating, rating, 'Should update first rating')
self.assertTrue(new_rating.consumed)
self.assertEqual(new_rating.feedback, 'Bad Feedback')
self.assertEqual(new_rating.message_id, update_message)
self.assertEqual(new_rating.rating, 1)
if is_rating_mixin_test:
self.assertEqual(record_rating.rating_last_value, 1)
@tagged('rating')
@ -131,107 +148,141 @@ class TestRatingMixin(TestRatingCommon):
self.assertEqual(record_rating.rating_avg, 3, "The average should be equal to 3")
@tagged('rating', 'mail_performance', 'post_install', '-at_install')
class TestRatingPerformance(TestRatingCommon):
@users('employee')
@warmup
def test_rating_last_value_perfs(self):
RECORD_COUNT = 100
partners = self.env['res.partner'].sudo().create([
{'name': 'Jean-Luc %s' % (idx), 'email': 'jean-luc-%s@opoo.com' % (idx)} for idx in range(RECORD_COUNT)])
with self.assertQueryCount(employee=1516): # tmf 1516 / com 5510
record_ratings = self.env['mail.test.rating'].create([{
'customer_id': partners[idx].id,
'name': 'Test Rating',
'user_id': self.user_admin.id,
} for idx in range(RECORD_COUNT)])
self.flush_tracking()
with self.assertQueryCount(employee=2004): # tmf 2004
for record in record_ratings:
access_token = record._rating_get_access_token()
record.rating_apply(1, token=access_token)
self.flush_tracking()
with self.assertQueryCount(employee=2003): # tmf 2003
for record in record_ratings:
access_token = record._rating_get_access_token()
record.rating_apply(5, token=access_token)
self.flush_tracking()
with self.assertQueryCount(employee=1):
record_ratings._compute_rating_last_value()
vals = [val == 5 for val in record_ratings.mapped('rating_last_value')]
self.assertTrue(all(vals), "The last rating is kept.")
@tagged('rating')
class TestRatingRoutes(HttpCase, TestRatingCommon):
@tagged("rating", "rating_portal")
class TestRatingRoutes(TestRatingCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls._create_portal_user()
def test_open_rating_route(self):
"""
16.0 + expected behavior
1) Clicking on the smiley image triggers the /rate/<string:token>/<int:rate>
route should not update the rating of the record but simply redirect
to the feedback form
2) Customer interacts with webpage and submits FORM. Triggers /rate/<string:token>/submit_feedback
route. Should update the rating of the record with the data in the POST request
"""
self.authenticate(None, None) # set up session for public user
access_token = self.record_rating._rating_get_access_token()
for record_rating, is_rating_mixin_test in ((self.record_rating_thread, False),
(self.record_rating, True)):
with self.subTest('With rating mixin' if is_rating_mixin_test else 'Without rating mixin'):
"""
16.0 + expected behavior
1) Clicking on the smiley image triggers the /rate/<string:token>/<int:rate>
route should not update the rating of the record but simply redirect
to the feedback form
2) Customer interacts with webpage and submits FORM. Triggers /rate/<string:token>/submit_feedback
route. Should update the rating of the record with the data in the POST request
"""
self.authenticate(None, None) # set up session for public user
access_token = record_rating._rating_get_access_token()
# First round of clicking the URL and then submitting FORM data
response_click_one = self.url_open(f"/rate/{access_token}/5")
response_click_one.raise_for_status()
# First round of clicking the URL and then submitting FORM data
response_click_one = self.url_open(f"/rate/{access_token}/5")
response_click_one.raise_for_status()
# there should be a form to post to validate the feedback and avoid one-click anyway
forms = lxml.html.fromstring(response_click_one.content).xpath('//form')
self.assertEqual(forms[0].get('method'), 'post')
self.assertEqual(forms[0].get('action', ''), f'/rate/{access_token}/submit_feedback')
# there should be a form to post to validate the feedback and avoid one-click anyway
forms = lxml.html.fromstring(response_click_one.content).xpath('//form')
matching_rate_form = next((form for form in forms if form.get("action", "").startswith("/rate")), None)
self.assertEqual(matching_rate_form.get('method'), 'post')
self.assertEqual(matching_rate_form.get('action', ''), f'/rate/{access_token}/submit_feedback')
# rating should not change, i.e. default values
rating = self.record_rating.rating_ids
self.assertFalse(rating.consumed)
self.assertEqual(rating.rating, 0)
self.assertFalse(rating.feedback)
self.assertEqual(self.record_rating.rating_last_value, 0)
# rating should not change, i.e. default values
rating = record_rating.rating_ids
self.assertFalse(rating.consumed)
self.assertEqual(rating.rating, 0)
self.assertFalse(rating.feedback)
if is_rating_mixin_test:
self.assertEqual(record_rating.rating_last_value, 0)
response_submit_one = self.url_open(
f"/rate/{access_token}/submit_feedback",
data={
"rate": 5,
"csrf_token": http.Request.csrf_token(self),
"feedback": "good",
response_submit_one = self.url_open(
f"/rate/{access_token}/submit_feedback",
data={
"rate": 5,
"csrf_token": http.Request.csrf_token(self),
"feedback": "good",
}
)
response_submit_one.raise_for_status()
rating_post_submit_one = record_rating.rating_ids
self.assertTrue(rating_post_submit_one.consumed)
self.assertEqual(rating_post_submit_one.rating, 5)
self.assertEqual(rating_post_submit_one.feedback, "good")
if is_rating_mixin_test:
self.assertEqual(record_rating.rating_last_value, 5)
# Second round of clicking the URL and then submitting FORM data
response_click_two = self.url_open(f"/rate/{access_token}/1")
response_click_two.raise_for_status()
if is_rating_mixin_test:
self.assertEqual(record_rating.rating_last_value, 5) # should not be updated to 1
# check returned form
forms = lxml.html.fromstring(response_click_two.content).xpath('//form')
matching_rate_form = next((form for form in forms if form.get("action", "").startswith("/rate")), None)
self.assertEqual(matching_rate_form.get('method'), 'post')
self.assertEqual(matching_rate_form.get('action', ''), f'/rate/{access_token}/submit_feedback')
response_submit_two = self.url_open(
f"/rate/{access_token}/submit_feedback",
data={
"rate": 1,
"csrf_token": http.Request.csrf_token(self),
"feedback": "bad job"
}
)
response_submit_two.raise_for_status()
rating_post_submit_second = record_rating.rating_ids
self.assertTrue(rating_post_submit_second.consumed)
self.assertEqual(rating_post_submit_second.rating, 1)
self.assertEqual(rating_post_submit_second.feedback, "bad job")
if is_rating_mixin_test:
self.assertEqual(record_rating.rating_last_value, 1)
def test_portal_user_can_post_message_with_rating(self):
"""Test portal user can post a message with a rating on a thread with
_mail_post_access as read. In this case, sudo() is not necessary for
message_post itself, but it is necessary for adding the rating. This
tests covers the rating part is properly allowed."""
record_rating = self.env["mail.test.rating.thread.read"].create(
{
"customer_id": self.partner_1.id,
"name": "Test read access post + rating",
"user_id": self.user_admin.id,
}
)
response_submit_one.raise_for_status()
rating_post_submit_one = self.record_rating.rating_ids
self.assertTrue(rating_post_submit_one.consumed)
self.assertEqual(rating_post_submit_one.rating, 5)
self.assertEqual(rating_post_submit_one.feedback, "good")
self.assertEqual(self.record_rating.rating_last_value, 5)
# Second round of clicking the URL and then submitting FORM data
response_click_two = self.url_open(f"/rate/{access_token}/1")
response_click_two.raise_for_status()
self.assertEqual(self.record_rating.rating_last_value, 5) # should not be updated to 1
# check returned form
forms = lxml.html.fromstring(response_click_two.content).xpath('//form')
self.assertEqual(forms[0].get('method'), 'post')
self.assertEqual(forms[0].get('action', ''), f'/rate/{access_token}/submit_feedback')
response_submit_two = self.url_open(f"/rate/{access_token}/submit_feedback",
data={"rate": 1,
"csrf_token": http.Request.csrf_token(self),
"feedback": "bad job"})
response_submit_two.raise_for_status()
rating_post_submit_second = self.record_rating.rating_ids
self.assertTrue(rating_post_submit_second.consumed)
self.assertEqual(rating_post_submit_second.rating, 1)
self.assertEqual(rating_post_submit_second.feedback, "bad job")
self.assertEqual(self.record_rating.rating_last_value, 1)
# from model
message = record_rating.with_user(self.user_portal).message_post(
body="Not bad",
message_type="comment",
rating_value=3,
subtype_xmlid="mail.mt_comment",
)
rating = message.sudo().rating_id
self.assertEqual(rating.rating, 3, "rating was properly set")
# stealing attempt from another user
message2 = record_rating.message_post(
body="Attempt to steal rating with another user",
message_type="comment",
rating_id=rating.id,
subtype_xmlid="mail.mt_comment",
)
self.assertEqual(message.sudo().rating_id, rating, "rating was not removed from m1")
self.assertFalse(message2.rating_id, "rating was not added to m2")
# from controller
self.authenticate("portal_test", "portal_test")
res = self.make_jsonrpc_request(
"/mail/message/post",
{
"post_data": {
"body": "Good service",
"message_type": "comment",
"rating_value": 5,
"subtype_xmlid": "mail.mt_comment",
},
"thread_id": record_rating.id,
"thread_model": "mail.test.rating.thread.read",
},
)
message = next(
m for m in res["store_data"]["mail.message"] if m["id"] == res["message_id"]
)
rating = next(
r for r in res["store_data"]["rating.rating"] if r["id"] == message["rating_id"]
)
self.assertEqual(rating["rating"], 5)

View file

@ -13,7 +13,6 @@ class TestResUsers(TestMailFullCommon):
cls.portal_user = mail_new_test_user(
cls.env,
login='portal_user',
mobile='+32 494 12 34 56',
phone='+32 494 12 34 89',
password='password',
name='Portal User',
@ -24,7 +23,6 @@ class TestResUsers(TestMailFullCommon):
cls.portal_user_2 = mail_new_test_user(
cls.env,
login='portal_user_2',
mobile='+32 494 12 34 22',
phone='invalid phone',
password='password',
name='Portal User 2',
@ -32,6 +30,16 @@ class TestResUsers(TestMailFullCommon):
groups='base.group_portal',
)
cls.portal_user_3 = mail_new_test_user(
cls.env,
login='portal_user_3',
phone='+32 494 12 34 22',
password='password',
name='Portal User 3',
email='portal_3@test.example.com',
groups='base.group_portal',
)
# Remove existing blacklisted email / phone (they will be sanitized, so we avoid to sanitize them here)
cls.env['mail.blacklist'].search([]).unlink()
cls.env['phone.blacklist'].search([]).unlink()
@ -40,22 +48,24 @@ class TestResUsers(TestMailFullCommon):
"""Test that the email and the phone are blacklisted
when a portal user deactivate his own account.
"""
(self.portal_user | self.portal_user_2)._deactivate_portal_user(request_blacklist=True)
(self.portal_user | self.portal_user_2 | self.portal_user_3)._deactivate_portal_user(request_blacklist=True)
self.assertFalse(self.portal_user.active, 'Should have archived the user')
self.assertFalse(self.portal_user.partner_id.active, 'Should have archived the partner')
self.assertFalse(self.portal_user_2.active, 'Should have archived the user')
self.assertFalse(self.portal_user_2.partner_id.active, 'Should have archived the partner')
self.assertFalse(self.portal_user_3.active, 'Should have archived the user')
self.assertFalse(self.portal_user_3.partner_id.active, 'Should have archived the partner')
blacklist = self.env['mail.blacklist'].search([
('email', 'in', ('portal@test.example.com', 'portal_2@test.example.com')),
('email', 'in', ('portal@test.example.com', 'portal_2@test.example.com', 'portal_3@test.example.com')),
])
self.assertEqual(len(blacklist), 2, 'Should have blacklisted the users email')
self.assertEqual(len(blacklist), 3, 'Should have blacklisted the users email')
blacklists = self.env['phone.blacklist'].search([
('number', 'in', ('+32494123489', '+32494123456', '+32494123422')),
('number', 'in', ('+32494123489', '+32494123422')),
])
self.assertEqual(len(blacklists), 3, 'Should have blacklisted the user phone and mobile')
self.assertEqual(len(blacklists), 2, 'Should have blacklisted the user phone')
blacklist = self.env['phone.blacklist'].search([('number', '=', 'invalid phone')])
self.assertFalse(blacklist, 'Should have skipped invalid phone')

View file

@ -0,0 +1,111 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from urllib.parse import urlencode
from odoo import tests
from odoo.addons.test_mail_full.tests.test_portal import TestPortal
@tests.common.tagged("post_install", "-at_install")
class TestUIPortal(TestPortal):
def setUp(self):
super().setUp()
self.env["mail.message"].create(
{
"author_id": self.user_employee.partner_id.id,
"body": "Test Message",
"model": self.record_portal._name,
"res_id": self.record_portal.id,
"subtype_id": self.ref("mail.mt_comment"),
}
)
def test_star_message(self):
self.start_tour(
f"/my/test_portal_records/{self.record_portal.id}",
"star_message_tour",
login=self.user_employee.login,
)
def test_no_copy_link_for_non_readable_portal_record(self):
# mail.test.portal has read access only for base.group_user
self.start_tour(
f"/my/test_portal_records/{self.record_portal.id}?{urlencode({'token': self.record_portal.access_token})}",
"portal_no_copy_link_tour",
login=None,
)
def test_copy_link_for_readable_portal_record(self):
# mail.test.portal has read access only for base.group_user
self.start_tour(
f"/my/test_portal_records/{self.record_portal.id}?{urlencode({'token': self.record_portal.access_token})}",
"portal_copy_link_tour",
login=self.user_employee.login,
)
def test_load_more(self):
self.env["mail.message"].create(
[
{
"author_id": self.user_employee.partner_id.id,
"body": f"Test Message {i + 1}",
"model": self.record_portal._name,
"res_id": self.record_portal.id,
"subtype_id": self.ref("mail.mt_comment"),
}
for i in range(30)
]
)
self.start_tour(
f"/my/test_portal_records/{self.record_portal.id}",
"load_more_tour",
login=self.user_employee.login,
)
def test_message_actions_without_login(self):
self.start_tour(
f"/my/test_portal_records/{self.record_portal.id}?token={self.record_portal._portal_ensure_token()}",
"message_actions_tour",
)
def test_rating_record_portal(self):
record_rating = self.env["mail.test.rating"].create({"name": "Test rating record"})
# To check if there is no message with rating, there is no rating cards feature.
record_rating.message_post(
body="Message without rating",
message_type="comment",
subtype_xmlid="mail.mt_comment",
)
self.start_tour(
f"/my/test_portal_rating_records/{record_rating.id}?display_rating=True&token={record_rating._portal_ensure_token()}",
"portal_rating_tour"
)
def test_display_rating_portal(self):
record_rating = self.env["mail.test.rating"].create({"name": "Test rating record"})
record_rating.message_post(
body="Message with rating",
message_type="comment",
rating_value="5",
subtype_xmlid="mail.mt_comment",
)
self.start_tour(
f"/my/test_portal_rating_records/{record_rating.id}?display_rating=True&token={record_rating._portal_ensure_token()}",
"portal_display_rating_tour",
)
self.start_tour(
f"/my/test_portal_rating_records/{record_rating.id}?display_rating=False&token={record_rating._portal_ensure_token()}",
"portal_not_display_rating_tour",
)
def test_composer_actions_portal(self):
self.start_tour(
f"/my/test_portal_records/{self.record_portal.id}",
"portal_composer_actions_tour_internal_user",
login=self.user_employee.login,
)
self.start_tour(
f"/my/test_portal_records/{self.record_portal.id}?token={self.record_portal._portal_ensure_token()}",
"portal_composer_actions_tour_portal_user",
)

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="test_portal_template" name="Test Portal" inherit_id="portal.portal_sidebar" primary="True">
<xpath expr="//div[hasclass('o_portal_sidebar')]" position="inside">
<!-- chatter -->
<div>
<h3>Communication history</h3>
<t t-call="portal.message_thread"/>
</div>
</xpath>
</template>
</odoo>