[IMP] sm modules: "neutralise" genders

Purpose
=======
Change all masculine nouns in Odoo's code to neutral nouns (when
possible), making sure that demo data is correctly handled. This is
particularly important since our code is open source, and nowadays lots
of machine learning models are trained on open source repositories.

With this small change we contribute to training more "fair" models, and
teaching models that "employee" or "user" != "he".

This also affects some text visible by the user, hence making it more
inclusive for Odoo users.

Task-2853046

closes odoo/odoo#91292

Related: odoo/enterprise#27302
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Fabio Barbero
2022-05-16 10:09:32 +02:00
parent 1a8389858a
commit fc79bd1e0e
66 changed files with 119 additions and 119 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
2FA Invite mail
===============
Allow the users to invite another user to use Two-Factor authentication
by sending an email to the target user. This email redirect him to :
by sending an email to the target user. This email redirect them to :
- the users security settings if the user is internal.
- the portal security settings page if the user is not internal.
""",
@@ -46,7 +46,7 @@ tour.register('totp_admin_self_invite', {
content: "go to Account security Tab",
trigger: "a.nav-link:contains(Account Security)",
}, {
content: "check that user cannot invite himself to use 2FA.",
content: "check that user cannot invite themselves to use 2FA.",
trigger: "body",
run: function () {
var $inviteBtn = $('button:contains(Invite to use 2FA)');
+3 -3
View File
@@ -1892,7 +1892,7 @@ class Lead(models.Model):
# Each won/lost lead increments a frequency table, where we store, for each field/value couple, the number of
# won and lost leads.
# E.g. : A won lead from Belgium will increase the won count of the frequency country_id='Belgium' by 1.
# The frequencies are split by team_id, so each team has his own frequencies environment. (Team A doesn't impact B)
# The frequencies are split by team_id, so each team has its own frequencies environment. (Team A doesn't impact B)
# There are two main ways to build the frequency table:
# - Live Increment: At each Won/lost, we increment directly the frequencies based on the lead values.
# Done right BEFORE writing the lead as won or lost.
@@ -1980,7 +1980,7 @@ class Lead(models.Model):
field = frequency['variable']
value = frequency['value']
# To avoid that a tag take to much importance if his subset is too small,
# To avoid that a tag take too much importance if its subset is too small,
# we ignore the tag frequencies if we have less than 50 won or lost for this tag.
if field == 'tag_id' and (frequency['won_count'] + frequency['lost_count']) < 50:
continue
@@ -2066,7 +2066,7 @@ class Lead(models.Model):
final state of the lead.
This issue is when the lead leaves a closed state because once the new values have been writen, we do not know
what was the previous state that we need to decrement.
This is why 'is_won' and 'decrement' parameters are used to describe the from / to change of his state.
This is why 'is_won' and 'decrement' parameters are used to describe the from / to change of its state.
"""
new_frequencies_by_team, existing_frequencies_by_team = self._pls_prepare_update_frequency_table(target_state=from_state or to_state)
+2 -2
View File
@@ -408,7 +408,7 @@ class TestLeadMerge(TestLeadMergeCommon):
lead_w_email contact_1 KO (already following the destination lead)
contact_2 OK (active on lead_w_email)
contact_company KO (most recent message on lead_w_email is 35 days ago, message
on lead_w_partner is not counted as he doesn't follow it)
on lead_w_partner is not counted as they don't follow it)
lead_w_partner contact_2 KO (already added with lead_w_email)
lead_w_partner_company
"""
@@ -480,7 +480,7 @@ class TestLeadMerge(TestLeadMergeCommon):
self.assertIn(self.contact_2, new_partner_followers,
'The partner must follow the destination lead')
# "contact_company" posted a message 35 days ago on lead_2, so it's considered as inactive
# "contact_company" posted a message now on lead_3, but he doesn't follow lead_3
# "contact_company" posted a message now on lead_3, but they don't follow lead_3
# so this message is just ignored
self.assertNotIn(self.contact_company, new_partner_followers,
'The partner was not active on the lead')
@@ -90,7 +90,7 @@ class TestCRMLeadMultiCompany(TestCrmCommon):
@users('user_sales_manager_mc')
def test_lead_mc_company_computation_env_user_restrict(self):
""" Check that the computed company is allowed (aka in self.env.companies).
User is logged in company_main even his default default company is
User is logged in company_main even their default default company is
company_2. """
LeadUnsyncCids = self.env['crm.lead'].with_context(allowed_company_ids=[self.company_main.id])
self.assertEqual(LeadUnsyncCids.env.company, self.company_main)
+1 -1
View File
@@ -12,7 +12,7 @@ class TestUi(HttpCase):
self.start_tour("/web", 'crm_tour', login="admin")
def test_02_crm_tour_rainbowman(self):
# we create a new user to make sure he gets the 'Congrats on your first deal!'
# we create a new user to make sure they get the 'Congrats on your first deal!'
# rainbowman message.
self.env['res.users'].create({
'name': 'Temporary CRM User',
@@ -227,7 +227,7 @@ class CRMLeadMiningRequest(models.Model):
def _perform_request(self):
"""
This will perform the request and create the corresponding leads.
The user will be notified if he hasn't enough credits.
The user will be notified if they don't have enough credits.
"""
self.error_type = False
server_payload = self._prepare_iap_payload()
@@ -43,7 +43,7 @@ class ChatbotScriptStep(models.Model):
The whole conversation history will be saved into the lead's description for reference.
This also allows having a question of type 'free_input_multi' to let the visitor explain
his interest / needs before creating the lead. """
their interest / needs before creating the lead. """
customer_values = self._chatbot_prepare_customer_values(
mail_channel, create_partner=False, update_partner=True)
+1 -1
View File
@@ -61,7 +61,7 @@ class TestLivechatLead(TestCrmCommon):
self.assertEqual(lead.name, 'TestLead command')
self.assertEqual(lead.partner_id, self.env['res.partner'])
# public + someone else: no customer (as he was anonymous)
# public + someone else: no customer (as they were anonymous)
channel.write({
'channel_partner_ids': [(4, self.user_sales_manager.partner_id.id)]
})
@@ -53,7 +53,7 @@ class MailPluginController(mail_plugin.MailPluginController):
def _get_contact_data(self, partner):
"""
Return the leads key only if the current user can create leads. So, if he can not
Return the leads key only if the current user can create leads. So, if they can not
create leads, the section won't be visible on the addin side (like if the CRM
module was not installed on the database).
"""
@@ -6,7 +6,7 @@ from odoo import api, fields, models, _
class EventBoothRegistration(models.Model):
"""event.booth.registrations are used to allow multiple partners to book the same booth.
Whenever a partner has paid his registration all the others linked to the booth will be deleted."""
Whenever a partner has paid their registration all the others linked to the booth will be deleted."""
_name = 'event.booth.registration'
_description = 'Event Booth Registration'
@@ -47,7 +47,7 @@ class EventRegistration(models.Model):
There are 2 main use cases
* first is when we update the partner_id of multiple registrations. It
happens when a public user fill its information when he register to
happens when a public user fill its information when they register to
an event;
* second is when we update specific values of one registration like
updating question answers or a contact information (email, phone);
+3 -3
View File
@@ -120,12 +120,12 @@ class GoogleEvent(abc.Set):
def owner(self, env):
# Owner/organizer could be desynchronised between Google and Odoo.
# Let userA, userB be two new users (never synced to Google before).
# UserA creates an event in Odoo (he is the owner) but userB syncs first.
# UserA creates an event in Odoo (they are the owner) but userB syncs first.
# There is no way to insert the event into userA's calendar since we don't have
# any authentication access. The event is therefore inserted into userB's calendar
# (he is the organizer in Google). The "real" owner (in Odoo) is stored as an
# (they are the organizer in Google). The "real" owner (in Odoo) is stored as an
# extended property. There is currently no support to "transfert" ownership when
# userA syncs his calendar the first time.
# userA syncs their calendar the first time.
real_owner_id = self.extendedProperties and self.extendedProperties.get('shared', {}).get('%s_owner_id' % env.cr.dbname)
try:
# If we create an event without user_id, the event properties will be 'false'
+1 -1
View File
@@ -95,7 +95,7 @@ class MailChannel(models.Model):
channel_partner_ids = self.with_context(active_test=False).channel_partner_ids
partners = channel_partner_ids - self.livechat_operator_id
if not partners:
# operator probably testing the livechat with his own user
# operator probably testing the livechat with their own user
partners = channel_partner_ids
first_partner = partners and partners[0]
if first_partner and not first_partner.is_public:
@@ -90,7 +90,7 @@ class TestGetMailChannel(TransactionCase):
self.assertEqual(visitor_info['name'], "Roger")
self.assertEqual(visitor_info['country'], (20, "Belgium"))
# ensure visitor info are correct when operator is testing himself
# ensure visitor info are correct when operator is testing themselves
operator = self.operators[0]
channel_info = self.livechat_channel.with_user(operator)._open_livechat_mail_channel(anonymous_name='whatever', previous_operator_id=operator.partner_id.id, user_id=operator.id)
self.assertEqual(channel_info['operator_pid'], (operator.partner_id.id, "Michel Operator"))
@@ -289,7 +289,7 @@ class PermissionPanel extends Component {
/**
* This method is called after each permission change rpc.
* It will check if a reloading of the article tree or a complete reload is needed in function
* of the new article state (if change of category or if user lost his own access to the current article).
* of the new article state (if change of category or if user lost their own access to the current article).
* return True if the caller should continue after executing this method, and False, if caller should stop.
* @param {Dict} result
* @param {Boolean} lostAccess
@@ -173,7 +173,7 @@ class TestKnowledgeSecurity(KnowledgeArticlePermissionsCase):
self.article_roots.article_member_ids,
'Members: employee should memberships of visible '
)
# remove employee from Shared root, check he cannot read those members
# remove employee from Shared root, check they cannot read those members
self.article_roots[2].article_member_ids.filtered(lambda m: m.partner_id == self.partner_employee).unlink()
my_members = self.env['knowledge.article.member'].search([('article_id', 'in', self.article_roots.ids)])
self.assertEqual(len(my_members), 2)
+1 -1
View File
@@ -77,7 +77,7 @@ class MailController(http.Controller):
record_sudo.with_user(uid).with_context(allowed_company_ids=cids).check_access_rule('read')
except AccessError:
# In case the allowed_company_ids from the cookies (i.e. the last user configuration
# on his browser) is not sufficient to avoid an ir.rule access error, try to following
# on their browser) is not sufficient to avoid an ir.rule access error, try to following
# heuristic:
# - Guess the supposed necessary company to access the record via the method
# _get_mail_redirect_suggested_company
+4 -4
View File
@@ -248,7 +248,7 @@ class Channel(models.Model):
membership_pids = [cmd[2]['partner_id'] for cmd in membership_ids_cmd if cmd[0] == 0]
# always add current user to new channel to have right values for
# is_pinned + ensure he has rights to see channel
# is_pinned + ensure they have rights to see channel
partner_ids_to_add = list(set(partner_ids + [self.env.user.partner_id.id]))
vals['channel_last_seen_partner_ids'] = membership_ids_cmd + [
(0, 0, {'partner_id': pid})
@@ -659,7 +659,7 @@ class Channel(models.Model):
def _message_post_after_hook(self, message, msg_vals):
"""
Automatically set the message posted by the current user as seen for himself.
Automatically set the message posted by the current user as seen for themselves.
"""
self._set_last_seen_message(message)
return super()._message_post_after_hook(message=message, msg_vals=msg_vals)
@@ -940,7 +940,7 @@ class Channel(models.Model):
def channel_fold(self, state=None):
""" Update the fold_state of the given session. In order to syncronize web browser
tabs, the change will be broadcast to himself (the current user channel).
tabs, the change will be broadcast to themselves (the current user channel).
Note: the user need to be logged
:param state : the new status of the session for the current user.
"""
@@ -1252,7 +1252,7 @@ class Channel(models.Model):
def _execute_command_help_message_extra(self):
msg = _("""<br><br>
Type <b>@username</b> to mention someone, and grab his attention.<br>
Type <b>@username</b> to mention someone, and grab their attention.<br>
Type <b>#channel</b> to mention a channel.<br>
Type <b>/command</b> to execute a command.<br>""")
return msg
+2 -2
View File
@@ -757,7 +757,7 @@ class Message(models.Model):
""" Toggle messages as (un)starred. Technically, the notifications related
to uid are set to (un)starred.
"""
# a user should always be able to star a message he can read
# a user should always be able to star a message they can read
self.check_access_rule('read')
starred = not self.starred
if starred:
@@ -1004,7 +1004,7 @@ class Message(models.Model):
messages = self.env['mail.message']
for message in self:
# Check if user has access to the record before displaying a notification about it.
# In case the user switches from one company to another, it might happen that he doesn't
# In case the user switches from one company to another, it might happen that they don't
# have access to the record related to the notification. In this case, we skip it.
# YTI FIXME: check allowed_company_ids if necessary
if message.model and message.res_id:
+3 -3
View File
@@ -1009,7 +1009,7 @@ class MailThread(models.AbstractModel):
subtype_id = thread._creation_subtype().id
# replies to internal message are considered as notes, but parent message
# author is added in recipients to ensure he is notified of a private answer
# author is added in recipients to ensure they are notified of a private answer
parent_message = False
if message_dict.get('parent_id'):
parent_message = self.env['mail.message'].sudo().browse(message_dict['parent_id'])
@@ -2412,7 +2412,7 @@ class MailThread(models.AbstractModel):
msg_vals = {}
# compute send user and its related signature; try to use self.env.user instead of browsing
# user_ids if he is the author will give a sudo user, improving access performances and cache usage.
# user_ids if they are the author will give a sudo user, improving access performances and cache usage.
signature = ''
email_add_signature = msg_vals.get('email_add_signature') if msg_vals and 'email_add_signature' in msg_vals else message.email_add_signature
if email_add_signature:
@@ -2824,7 +2824,7 @@ class MailThread(models.AbstractModel):
documents. This is done using relational fields linking to res.users
with track_visibility set. Since OpenERP v7 it is considered as being
responsible for the document and therefore standard behavior is to
subscribe the user and send him a notification.
subscribe the user and send them a notification.
Override this method to change that behavior and/or to add people to
notify, using possible custom notification.
+1 -1
View File
@@ -45,7 +45,7 @@ FieldChar.include({
/**
* Triggers the 'change' event to refresh the value.
* This method is debounced to run 2 seconds after typing ends.
* (to avoid spamming the server while the user is typing his message)
* (to avoid spamming the server while the user is typing their message)
*
* @private
*/
@@ -62,7 +62,7 @@ var FieldEmojiCommon = {
* By default, the 'change' event is only triggered when the text element is blurred.
*
* We override this method because we want to update the value while
* the user is typing his message (and not only on blur).
* the user is typing their message (and not only on blur).
*
* @override
* @private
@@ -90,7 +90,7 @@ var FieldEmojiCommon = {
/**
* Triggers the 'change' event to refresh the value.
* This method is debounced to run 2 seconds after typing ends.
* (to avoid spamming the server while the user is typing his message)
* (to avoid spamming the server while the user is typing their message)
*
* @private
*/
@@ -193,7 +193,7 @@ registerModel({
/**
* States the counter of this messaging menu. The counter is an integer
* value to give to the current user an estimate of how many things
* (unread threads, notifications, ...) are yet to be processed by him.
* (unread threads, notifications, ...) are yet to be processed by them.
*/
counter: attr({
compute: '_computeCounter',
+1 -1
View File
@@ -1231,7 +1231,7 @@ registerModel({
inverse: 'rtcAsConnectedSession',
}),
/**
* String, peerToken of the current session used to identify him during the peer-to-peer transactions.
* String, peerToken of the current session used to identify them during the peer-to-peer transactions.
*/
currentRtcSession: one('RtcSession', {
inverse: 'rtcAsCurrentSession',
@@ -30,7 +30,7 @@ registerModel({
recordMethods: {
/**
* Shows the dropzone when entering the browser window, to let the user know
* where he can drop its file.
* where they can drop their file.
* Avoids changing state when entering inner dropzones.
*
* @private
@@ -789,7 +789,7 @@ QUnit.test('data-oe-id & data-oe-model link redirection on click', async functio
);
});
QUnit.test('chat with author should be opened after clicking on his avatar', async function (assert) {
QUnit.test('chat with author should be opened after clicking on their avatar', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
@@ -821,11 +821,11 @@ QUnit.test('chat with author should be opened after clicking on his avatar', asy
assert.strictEqual(
document.querySelector('.o_ChatWindow_thread').dataset.correspondentId,
message.author.id.toString(),
"chat with author should be opened after clicking on his avatar"
"chat with author should be opened after clicking on their avatar"
);
});
QUnit.test('chat with author should be opened after clicking on his im status icon', async function (assert) {
QUnit.test('chat with author should be opened after clicking on their im status icon', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
@@ -857,7 +857,7 @@ QUnit.test('chat with author should be opened after clicking on his im status ic
assert.strictEqual(
document.querySelector('.o_ChatWindow_thread').dataset.correspondentId,
message.author.id.toString(),
"chat with author should be opened after clicking on his im status icon"
"chat with author should be opened after clicking on their im status icon"
);
});
+2 -2
View File
@@ -143,7 +143,7 @@ class MockEmail(common.BaseCase, MockSmtplibCase):
def gateway_mail_reply_wrecord(self, template, record, use_in_reply_to=True,
target_model=None, target_field=None):
""" Simulate a reply through the mail gateway. Usage: giving a record,
find an email sent to him and use its message-ID to simulate a reply.
find an email sent to them and use its message-ID to simulate a reply.
Some noise is added in References just to test some robustness. """
mail_mail = self._find_mail_mail_wrecord(record)
@@ -168,7 +168,7 @@ class MockEmail(common.BaseCase, MockSmtplibCase):
def gateway_mail_reply_wemail(self, template, email_to, use_in_reply_to=True,
target_model=None, target_field=None):
""" Simulate a reply through the mail gateway. Usage: giving a record,
find an email sent to him and use its message-ID to simulate a reply.
find an email sent to them and use its message-ID to simulate a reply.
Some noise is added in References just to test some robustness. """
sent_mail = self._find_sent_mail_wemail(email_to)
@@ -118,14 +118,14 @@ class TestMailChannelMembers(MailCommon):
channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.private_channel.id)])
self.assertEqual(len(channel_partners), 1)
# User 2 is not in the private channel, he can not invite user 3
# User 2 is not in the private channel, they can not invite user 3
with self.assertRaises(AccessError):
self.env['mail.channel.partner'].with_user(self.user_2).create({
'partner_id': self.user_portal.partner_id.id,
'channel_id': self.private_channel.id,
})
# User 1 is in the private channel, he can invite other users
# User 1 is in the private channel, they can invite other users
self.env['mail.channel.partner'].with_user(self.user_1).create({
'partner_id': self.user_portal.partner_id.id,
'channel_id': self.private_channel.id,
@@ -147,13 +147,13 @@ class TestMailChannelMembers(MailCommon):
channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.private_channel.id)])
self.assertEqual(channel_partners.mapped('partner_id'), self.user_1.partner_id)
# User 2 is not in the channel, he can not invite user_portal
# User 2 is not in the channel, they can not invite user_portal
with self.assertRaises(AccessError):
self.private_channel.with_user(self.user_2).add_members(self.user_portal.partner_id.ids)
channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.private_channel.id)])
self.assertEqual(channel_partners.mapped('partner_id'), self.user_1.partner_id)
# User 1 is in the channel, he can invite user_portal
# User 1 is in the channel, they can invite user_portal
self.private_channel.with_user(self.user_1).add_members(self.user_portal.partner_id.ids)
channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.private_channel.id)])
self.assertEqual(channel_partners.mapped('partner_id'), self.user_1.partner_id | self.user_portal.partner_id)
@@ -165,11 +165,11 @@ class TestMailChannelMembers(MailCommon):
channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.private_channel.id)])
self.assertEqual(len(channel_partners), 2)
# User 2 is not in the channel, he can not kick user 1
# User 2 is not in the channel, they can not kick user 1
with self.assertRaises(AccessError):
channel_partners.with_user(self.user_2).unlink()
# User 3 is in the channel, he can kick user 1
# User 3 is in the channel, they can kick user 1
channel_partners.with_user(self.user_portal).unlink()
# ------------------------------------------------------------
@@ -181,12 +181,12 @@ class TestMailChannelMembers(MailCommon):
channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.group_channel.id)])
self.assertFalse(channel_partners)
# user 1 is in the group, he can join the channel
# user 1 is in the group, they can join the channel
self.group_channel.with_user(self.user_1).add_members(self.user_1.partner_id.ids)
channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.group_channel.id)])
self.assertEqual(channel_partners.mapped('partner_id'), self.user_1.partner_id)
# user 3 is not in the group, he can not join
# user 3 is not in the group, they can not join
with self.assertRaises(AccessError):
self.group_channel.with_user(self.user_portal).add_members(self.user_portal.partner_id.ids)
@@ -197,7 +197,7 @@ class TestMailChannelMembers(MailCommon):
channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.group_channel.id)])
self.assertEqual(channel_partners.mapped('partner_id'), self.user_1.partner_id)
# user 1 can not invite user 3 because he's not in the group
# user 1 can not invite user 3 because they are not in the group
with self.assertRaises(UserError):
self.group_channel.with_user(self.user_1).add_members(self.user_portal.partner_id.ids)
channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.group_channel.id)])
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Disable odoobot on admin so that devs don't hate him -->
<!-- Disable odoobot on admin so that devs don't hate it -->
<record id="base.user_admin" model="res.users">
<field name="odoobot_state">disabled</field>
</record>
+1 -1
View File
@@ -418,7 +418,7 @@ class MailGroup(models.Model):
for batch_email_member in tools.split_every(GROUP_SEND_BATCH_SIZE, member_emails.items()):
for email_member_normalized, email_member in batch_email_member:
if email_member_normalized == message.email_from_normalized:
# Do not send the email to his author
# Do not send the email to their author
continue
# SMTP headers related to the subscription
+1 -1
View File
@@ -214,7 +214,7 @@ class TestMailGroup(TestMailListCommon):
})]})
self.assertIn(partner, mail_group.member_partner_ids)
# Now that portal is in the member list he should have access
# Now that portal is in the member list they should have access
mail_group.with_user(self.user_employee_2).check_access_rule('read')
with self.assertRaises(AccessError, msg='Only moderator / responsible and admin can write on the group'):
mail_group.with_user(self.user_employee_2).check_access_rule('write')
@@ -9,7 +9,7 @@
Reject the message<span attrs="{'invisible': [('send_email', '=', False)]}"> and send an email to the author (<field name="email_from_normalized"/>)</span>.
</div>
<div class="alert alert-warning" role="alert" attrs="{'invisible': [('action', '!=', 'ban')]}">
Ban the author of the message (<field name="email_from_normalized"/>) <span attrs="{'invisible': [('send_email', '=', False)]}">and send him an email</span>.
Ban the author of the message (<field name="email_from_normalized"/>) <span attrs="{'invisible': [('send_email', '=', False)]}">and send them an email</span>.
</div>
<group>
<field name="send_email" invisible="1"/>
@@ -32,7 +32,7 @@ class Authenticate(http.Controller):
def auth_confirm(self, scope, friendlyname, redirect, info=None, do=None, **kw):
"""
Called by the `app_auth` template. If the user decided to allow the app to access Odoo, a temporary auth code
is generated and he is redirected to `redirect` with this code in the URL. It should redirect to the app, and
is generated and they are redirected to `redirect` with this code in the URL. It should redirect to the app, and
the app should then exchange this auth code for an access token by calling
`/mail_client/auth/access_token`.
+1 -1
View File
@@ -37,7 +37,7 @@ class MassMailController(http.Controller):
raise exceptions.AccessDenied()
if mailing.mailing_model_real == 'mailing.contact':
# Unsubscribe directly + Let the user choose his subscriptions
# Unsubscribe directly + Let the user choose their subscriptions
mailing.update_opt_out(email, mailing.contact_list_ids.ids, True)
contacts = request.env['mailing.contact'].sudo().search([('email_normalized', '=', tools.email_normalize(email))])
+2 -2
View File
@@ -12,7 +12,7 @@ class MassMailingList(models.Model):
_description = 'Mailing List'
_mailing_enabled = True
_order = 'create_date DESC'
# As this model has his own data merge, avoid to enable the generic data_merge on that model.
# As this model has their own data merge, avoid to enable the generic data_merge on that model.
_disable_data_merge = True
name = fields.Char(string='Mailing List', required=True)
@@ -34,7 +34,7 @@ class MassMailingList(models.Model):
copy=True, depends=['contact_ids'])
is_public = fields.Boolean(default=True, string='Show In Preferences',
help='The mailing list can be accessible by recipient in the unsubscription'
' page to allows him to update his subscription preferences.')
' page to allows them to update their subscription preferences.')
# ------------------------------------------------------
# COMPUTE / ONCHANGE
@@ -13,7 +13,7 @@ class ResConfigSettings(models.TransientModel):
mass_mailing_mail_server_id = fields.Many2one('ir.mail_server', string='Mail Server', config_parameter='mass_mailing.mail_server_id')
show_blacklist_buttons = fields.Boolean(string="Blacklist Option when Unsubscribing",
config_parameter='mass_mailing.show_blacklist_buttons',
help="""Allow the recipient to manage himself his state in the blacklist via the unsubscription page.""")
help="""Allow the recipient to manage themselves their state in the blacklist via the unsubscription page.""")
mass_mailing_reports = fields.Boolean(string='24H Stat Mailing Reports', config_parameter='mass_mailing.mass_mailing_reports',
help='Check how well your mailing is doing a day after it has been sent.')
@@ -41,7 +41,7 @@
</div>
</div>
<div class="col-md-6 o_setting_box col-xs-12" name="allow_blacklist_setting_container">
<div class="o_setting_left_pane" title="Allow the recipient to manage himself his state in the blacklist via the unsubscription page.
<div class="o_setting_left_pane" title="Allow the recipient to manage themselves their state in the blacklist via the unsubscription page.
If the option is active, the 'Blacklist Me' button is hidden on the unsubscription page.
The 'come Back' button will always be visible in any case to allow leads and partners to re-subscribe.">
<field name="show_blacklist_buttons"/>
@@ -109,12 +109,12 @@ class MicrosoftEvent(abc.Set):
def owner(self, env):
# Owner/organizer could be desynchronised between Microsoft and Odoo.
# Let userA, userB be two new users (never synced to Microsoft before).
# UserA creates an event in Odoo (he is the owner) but userB syncs first.
# UserA creates an event in Odoo (they are the owner) but userB syncs first.
# There is no way to insert the event into userA's calendar since we don't have
# any authentication access. The event is therefore inserted into userB's calendar
# (he is the orginizer in Microsoft). The "real" owner (in Odoo) is stored as an
# (they are the organizer in Microsoft). The "real" owner (in Odoo) is stored as an
# extended property. There is currently no support to "transfert" ownership when
# userA syncs his calendar the first time.
# userA syncs their calendar the first time.
if self.singleValueExtendedProperties:
microsoft_guid = env['ir.config_parameter'].sudo().get_param('microsoft_calendar.microsoft_guid', False)
real_owner_id = [prop['value'] for prop in self.singleValueExtendedProperties if prop['id'] == 'String {%s} Name owner_odoo_id' % microsoft_guid][0]
+1 -1
View File
@@ -218,7 +218,7 @@ class MailController(mail.MailController):
@classmethod
def _redirect_to_record(cls, model, res_id, access_token=None, **kwargs):
""" If the current user doesn't have access to the document, but provided
a valid access token, redirect him to the front-end view.
a valid access token, redirect them to the front-end view.
If the partner_id and hash parameters are given, add those parameters to the redirect url
to authentify the recipient in the chatter, if any.
@@ -19,7 +19,7 @@ class MailPluginController(mail_plugin.MailPluginController):
This is structured this way to enable the "project" feature on the Outlook side only if the Odoo version
supports it.
Return the tasks key only if the current user can create tasks. So, if he can not
Return the tasks key only if the current user can create tasks. So, if they can not
create tasks, the section won't be visible on the addin side (like if the project
module was not installed on the database).
"""
@@ -84,8 +84,8 @@ class UserInputSession(http.Controller):
Frontend should take the delay into account by displaying the appropriate animations.
Writing the next question on the survey is sudo'ed to avoid potential access right issues.
e.g: a survey user can create a live session from any survey but he can only write
on its own survey.
e.g: a survey user can create a live session from any survey but they can only write
on their own survey.
In addition to return a pre-rendered html template with the next question, we also return the background
to display. Background image depends on the next question to display and cannot be extracted from the
+3 -3
View File
@@ -126,7 +126,7 @@ class Survey(models.Model):
certification_mail_template_id = fields.Many2one(
'mail.template', 'Certified Email Template',
domain="[('model', '=', 'survey.user_input')]",
help="Automated email sent to the user when he succeeds the certification, containing his certification document.")
help="Automated email sent to the user when they succeed the certification, containing their certification document.")
certification_report_layout = fields.Selection([
('modern_purple', 'Modern Purple'),
('modern_blue', 'Modern Blue'),
@@ -993,7 +993,7 @@ class Survey(models.Model):
def action_start_session(self):
""" Sets the necessary fields for the session to take place and starts it.
The write is sudo'ed because a survey user can start a session even if it's
not his own survey. """
not their own survey. """
if not self.env.user.has_group('survey.group_survey_user'):
raise AccessError(_('Only survey users can manage sessions.'))
@@ -1019,7 +1019,7 @@ class Survey(models.Model):
def action_end_session(self):
""" The write is sudo'ed because a survey user can end a session even if it's
not his own survey. """
not their own survey. """
if not self.env.user.has_group('survey.group_survey_user'):
raise AccessError(_('Only survey users can manage sessions.'))
+6 -6
View File
@@ -45,7 +45,7 @@ class SurveyUserInput(models.Model):
invite_token = fields.Char('Invite token', readonly=True, copy=False) # no unique constraint, as it identifies a pool of attempts
partner_id = fields.Many2one('res.partner', string='Contact', readonly=True)
email = fields.Char('Email', readonly=True)
nickname = fields.Char('Nickname', help="Attendee nickname, mainly used to identify him in the survey session leaderboard.")
nickname = fields.Char('Nickname', help="Attendee nickname, mainly used to identify them in the survey session leaderboard.")
# questions / answers
user_input_line_ids = fields.One2many('survey.user_input.line', 'user_input_id', string='Answers', copy=True)
predefined_question_ids = fields.Many2many('survey.question', string='Predefined Questions', readonly=True)
@@ -554,11 +554,11 @@ class SurveyUserInput(models.Model):
- ensure correct scoring
- if the selected answer triggers another question later in the survey, if the answer is not cleared,
a question that should not be displayed to the user will be.
TODO DBE: Maybe this can be the only cleaning method, even for section_per_page or one_page where
conditional questions are, for now, cleared in JS directly. But this can be annoying if user typed a long
answer, changed his mind unchecking depending answer and changed again his mind by rechecking the depending
answer -> For now, the long answer will be lost. If we use this as the master cleaning method,
TODO DBE: Maybe this can be the only cleaning method, even for section_per_page or one_page where
conditional questions are, for now, cleared in JS directly. But this can be annoying if user typed a long
answer, changed their mind unchecking depending answer and changed again their mind by rechecking the depending
answer -> For now, the long answer will be lost. If we use this as the master cleaning method,
long answer will be cleared only during submit.
"""
inactive_questions = self._get_inactive_conditional_questions()
@@ -98,7 +98,7 @@ publicWidget.registry.SurveySessionChart = publicWidget.Widget.extend({
* (see _getBackgroundColor for details)
* - The ticks are bigger and bolded to be able to see them better on a big screen (projector)
* - We don't use tooltips to keep it as simple as possible
* - We don't set a suggestedMin or Max so that Chart will adapt automatically himself based on the given data
* - We don't set a suggestedMin or Max so that Chart will adapt automatically based on the given data
* The '+1' part is a small trick to avoid the datalabels to be clipped in height
* - We use a custom 'datalabels' plugin to be able to display the number value on top of the
* associated bar of the chart.
+2 -2
View File
@@ -49,7 +49,7 @@ class TestActivityRights(TestActivityCommon):
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_activity_security_user_noaccess_automated(self):
def _employee_crash(*args, **kwargs):
""" If employee is test employee, consider he has no access on document """
""" If employee is test employee, consider they have no access on document """
recordset = args[0]
if recordset.env.uid == self.user_employee.id:
raise exceptions.AccessError('Hop hop hop Ernest, please step back.')
@@ -65,7 +65,7 @@ class TestActivityRights(TestActivityCommon):
def test_activity_security_user_noaccess_manual(self):
def _employee_crash(*args, **kwargs):
""" If employee is test employee, consider he has no access on document """
""" If employee is test employee, consider they have no access on document """
recordset = args[0]
if recordset.env.uid == self.user_employee.id:
raise exceptions.AccessError('Hop hop hop Ernest, please step back.')
+1 -1
View File
@@ -604,7 +604,7 @@ class TestMailgateway(TestMailCommon):
@mute_logger('odoo.addons.mail.models.mail_thread')
def test_message_process_create_uid_crash(self):
def _employee_crash(*args, **kwargs):
""" If employee is test employee, consider he has no access on document """
""" If employee is test employee, consider they have no access on document """
recordset = args[0]
if recordset.env.uid == self.user_employee.id and not recordset.env.su:
if kwargs.get('raise_exception', True):
+2 -2
View File
@@ -363,7 +363,7 @@ class TestMessageAccess(TestMailCommon):
self.message.write({'attachment_ids': [(4, attachment.id)]})
self.message.write({'partner_ids': [(4, self.user_employee.partner_id.id)]})
self.message.with_user(self.user_employee).read()
# Test: Bert has access to attachment, ok because he can read message
# Test: Bert has access to attachment, ok because they can read message
attachment.with_user(self.user_employee).read(['name', 'datas'])
def test_mail_message_access_read_author(self):
@@ -372,7 +372,7 @@ class TestMessageAccess(TestMailCommon):
def test_mail_message_access_read_doc(self):
self.message.write({'model': 'mail.channel', 'res_id': self.group_public.id})
# Test: Bert reads the message, ok because linked to a doc he is allowed to read
# Test: Bert reads the message, ok because linked to a doc they are allowed to read
self.message.with_user(self.user_employee).read()
# --------------------------------------------------
@@ -117,7 +117,7 @@ class TestDiscuss(TestMailCommon, TestRecipients):
@mute_logger('openerp.addons.mail.models.mail_mail')
def test_mark_all_as_read(self):
def _employee_crash(*args, **kwargs):
""" If employee is test employee, consider he has no access on document """
""" If employee is test employee, consider they have no access on document """
recordset = args[0]
if recordset.env.uid == self.user_employee.id and not recordset.env.su:
if kwargs.get('raise_exception', True):
+1 -1
View File
@@ -359,7 +359,7 @@ class TestTrackingInternals(TestMailCommon):
self.record.clear_caches()
record_form = Form(self.record.with_user(self.user_employee))
record_form.name = 'TestDoNoCrash'
# the employee user must be able to save the fields on which he can write
# the employee user must be able to save the fields on which they can write
# if we fetch all the tracked fields, ignoring the group of the current user
# it will crash and it shouldn't
record = record_form.save()
@@ -11,12 +11,12 @@ const tourUtils = require('website_sale.tour_utils');
* -> clicks on "buy course"
* -> is redirected to webshop on the product page
* -> buys the course
* -> fails 3 times, exhausting his attempts
* -> fails 3 times, exhausting their attempts
* -> is removed to the members of the course
* -> buys the course again
* -> succeeds the certification
* -> has the course marked as completed
* -> has the certification in his user profile
* -> has the certification in their user profile
*
*/
@@ -62,6 +62,6 @@ class QuizAnswer(models.Model):
is_correct = fields.Boolean('Correct', default=False)
comment = fields.Text(
'Extra Comment', translate=True,
help='''This comment will be displayed to the user if he selects this answer, after submitting the quiz.
help='''This comment will be displayed to the user if they select this answer, after submitting the quiz.
It is used as a small informational text helping to understand why this answer is correct / incorrect.''')
awarded_points = fields.Integer('Points', default=0)
@@ -68,7 +68,7 @@ var Quiz = publicWidget.Widget.extend({
* Overridden to add custom rendering behavior upon start of the widget.
*
* If the user has answered the quiz before having joined the course, we check
* his answers (saved into his session) here as well.
* their answers (saved into their session) here as well.
*
* @override
*/
@@ -13,7 +13,7 @@ class MailChannel(models.Model):
def channel_pin(self, pinned=False):
""" Override to clean an empty livechat channel.
This is typically called when the operator send a chat request to a website.visitor
but don't speak to him and closes the chatter.
but don't speak to them and closes the chatter.
This allows operators to send the visitor a new chat request.
If active empty livechat channel,
delete mail_channel as not useful to keep empty chat
+1 -1
View File
@@ -58,7 +58,7 @@ class ResPartner(models.Model):
def action_view_courses(self):
""" View partners courses. In singleton mode, return courses followed
by all its contacts (if company) or by himself (if not a company).
by all its contacts (if company) or by themselves (if not a company).
Otherwise simply set a domain on required partners. """
action = self.env["ir.actions.actions"]._for_xml_id("website_slides.slide_channel_partner_action")
action['name'] = _('Followed Courses')
@@ -105,7 +105,7 @@ class ChannelUsersRelation(models.Model):
users.add_karma(partner_karma[user.partner_id.id])
def _send_completed_mail(self):
""" Send an email to the attendee when he has successfully completed a course. """
""" Send an email to the attendee when they have successfully completed a course. """
template_to_records = dict()
for record in self:
template = record.channel_id.completed_template_id
@@ -403,7 +403,7 @@ class Channel(models.Model):
@api.depends_context('uid')
def _compute_can_publish(self):
""" For channels of type 'training', only the responsible (see user_id field) can publish slides.
The 'sudo' user needs to be handled because he's the one used for uploads done on the front-end when the
The 'sudo' user needs to be handled because they are the one used for uploads done on the front-end when the
logged in user is not publisher but fulfills the upload_group_ids condition. """
for record in self:
if not record.can_upload:
@@ -481,7 +481,7 @@ class Channel(models.Model):
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
# Ensure creator is member of its channel it is easier for him to manage it (unless it is odoobot)
# Ensure creator is member of its channel it is easier for them to manage it (unless it is odoobot)
if not vals.get('channel_partner_ids') and not self.env.is_superuser():
vals['channel_partner_ids'] = [(0, 0, {
'partner_id': self.env.user.partner_id.id
@@ -56,4 +56,4 @@ class SlideAnswer(models.Model):
question_id = fields.Many2one('slide.question', string="Question", required=True, ondelete='cascade')
text_value = fields.Char("Answer", required=True, translate=True)
is_correct = fields.Boolean("Is correct answer")
comment = fields.Text("Comment", translate=True, help='This comment will be displayed to the user if he selects this answer')
comment = fields.Text("Comment", translate=True, help='This comment will be displayed to the user if they select this answer')
@@ -91,7 +91,7 @@
* Overridden to add custom rendering behavior upon start of the widget.
*
* If the user has answered the quiz before having joined the course, we check
* his answers (saved into his session) here as well.
* their answers (saved into their session) here as well.
*
* @override
*/
@@ -6,9 +6,9 @@ import slidesTourTools from '@website_slides/../tests/tours/slides_tour_tools';
/**
* Global use case:
* a user (website publisher) creates a course;
* he updates it;
* he creates some lessons in it;
* he publishes it;
* they update it;
* they create some lessons in it;
* they publish it;
*/
tour.register('course_publisher_standard', {
url: '/slides',
@@ -6,10 +6,10 @@ import tour from 'web_tour.tour';
* Global use case:
* an user (either employee, website publisher or portal) joins a public
course;
* he has access to the full course content when he's a member of the
* they have access to the full course content when they are a member of the
course;
* he uses fullscreen player to complete the course;
* he rates the course;
* they use fullscreen player to complete the course;
* they rate the course;
*/
tour.register('course_member', {
url: '/slides',
@@ -23,10 +23,10 @@ FullScreen.include({
* Global use case:
* an user (either employee, website publisher or portal) joins a public
course;
* he has access to the full course content when he's a member of the
* they have access to the full course content when they are a member of the
course;
* he uses fullscreen player to complete the course;
* he rates the course;
* they use fullscreen player to complete the course;
* they rate the course;
*/
tour.register('course_member_youtube', {
url: '/slides',
@@ -6,9 +6,9 @@ import slidesTourTools from '@website_slides/../tests/tours/slides_tour_tools';
/**
* Global use case:
* a user (website publisher) creates a course;
* he updates it;
* he creates some lessons in it;
* he publishes it;
* they update it;
* they create some lessons in it;
* they publishe it;
*/
tour.register('course_publisher', {
url: '/slides',
@@ -5,8 +5,8 @@ import tour from 'web_tour.tour';
/**
* Global use case:
* - a user (website publisher) lands on the fullscreen view of a course ;
* - he clicks on the website editor "Edit" button ;
* - he is redirected to the non-fullscreen view with the editor opened.
* - they click on the website editor "Edit" button ;
* - they are redirected to the non-fullscreen view with the editor opened.
*
* This tour tests a fix made when editing a course in fullscreen view.
* See "Fullscreen#_onWebEditorClick" for more information.
+1 -1
View File
@@ -292,7 +292,7 @@ class TestAccessFeatures(common.SlidesCase):
self.assertTrue(channel_manager.can_upload)
self.assertTrue(channel_manager.can_publish)
# superuser should always be able to publish even if he's not the responsible
# superuser should always be able to publish even if they are not the responsible
channel_superuser = self.channel.sudo()
channel_superuser.invalidate_cache(['can_upload', 'can_publish'])
self.assertTrue(channel_superuser.can_upload)
@@ -26,10 +26,10 @@ class SurveyUserInput(models.Model):
return res
def _check_for_failed_attempt(self):
""" If the user fails his last attempt at a course certification,
we remove him from the members of the course (and he has to enroll again).
He receives an email in the process notifying him of his failure and suggesting
he enrolls to the course again.
""" If the user fails their last attempt at a course certification,
we remove them from the members of the course (and they have to enroll again).
They receive an email in the process notifying them of their failure and suggesting
they enroll to the course again.
The purpose is to have a 'certification flow' where the user can re-purchase the
certification when they have failed it."""
@@ -70,7 +70,7 @@ class TestCourseCertificationFailureFlow(TestSurveyCommon):
self.assertFalse(slide_partner.survey_scoring_success, 'Quizz should not be marked as passed with wrong answers')
# forces recompute of partner_ids as we delete directly in relation
self.channel.invalidate_cache()
self.assertIn(self.user_public.partner_id, self.channel.partner_ids, 'Public user should still be a member of the course because he still has attempts left')
self.assertIn(self.user_public.partner_id, self.channel.partner_ids, 'Public user should still be a member of the course because they still have attempts left')
# Step 5: simulate a 'retry'
retry_user_input = self.slide_certification.survey_id.sudo()._create_answer(
@@ -85,7 +85,7 @@ class TestCourseCertificationFailureFlow(TestSurveyCommon):
self.fill_in_answer(retry_user_input, certification.question_ids)
# forces recompute of partner_ids as we delete directly in relation
self.channel.invalidate_cache()
self.assertNotIn(self.user_public.partner_id, self.channel.partner_ids, 'Public user should have been kicked out of the course because he failed his last attempt')
self.assertNotIn(self.user_public.partner_id, self.channel.partner_ids, 'Public user should have been kicked out of the course because they failed their last attempt')
# Step 7: add public user as member of the channel once again
self.channel._action_add_members(self.user_public.partner_id)