[IMP] portal: allow portal users to deactivate their accounts
Purpose ======= Allow portal users to deactivate their accounts from the portal view. After the deactivation, they are redirected to the login page, so they can verify that they can not longer login with their credentials. We first archived the record and remove sensitive information (password, login, so he can not log in again with the same credentials). After the deactivation, we blacklist the email and the phone of the user, so we are sure that we never send him again email / SMS. Then, we create a <res.users.deletion> to delete the user and the partner in a CRON because this operation can be heavy (write_uid, create_uid field on all models). Task-2629544 Part-of: odoo/odoo#78298
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<odoo>
|
||||
<template id="totp_portal_hook" name="TOTP Portal hook" inherit_id="portal.portal_my_security">
|
||||
<xpath expr="//div[hasclass('o_portal_security_body')]">
|
||||
<xpath expr="//section[@name='portal_change_password']" position="after">
|
||||
<!--
|
||||
portal users don't have access to non-qweb views anymore, so
|
||||
embed the target view as a *data island* of sorts, a JSON embed
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, exceptions, fields, models, modules
|
||||
from odoo import _, api, exceptions, fields, models, modules, tools
|
||||
from odoo.addons.base.models.res_users import is_selection_groups
|
||||
|
||||
|
||||
@@ -148,6 +148,34 @@ class Users(models.Model):
|
||||
return '%s (%s)' % (body, self.partner_id.email)
|
||||
return body
|
||||
|
||||
def _deactivate_portal_user(self, **post):
|
||||
"""Blacklist the email of the user after deleting it.
|
||||
|
||||
Log a note on the related partner so we know why it's archived.
|
||||
"""
|
||||
current_user = self.env.user
|
||||
for user in self:
|
||||
user.partner_id._message_log(
|
||||
body=_('Archived because %(user_name)s (#%(user_id)s) deleted the portal account',
|
||||
user_name=current_user.name, user_id=current_user.id)
|
||||
)
|
||||
|
||||
if post.get('request_blacklist'):
|
||||
users_to_blacklist = self.filtered(
|
||||
lambda user: tools.email_normalize(user.email))
|
||||
else:
|
||||
users_to_blacklist = []
|
||||
|
||||
super(Users, self)._deactivate_portal_user(**post)
|
||||
|
||||
for user in users_to_blacklist:
|
||||
blacklist = self.env['mail.blacklist']._add(user.email)
|
||||
blacklist._message_log(
|
||||
body=_('Blocked by deletion of portal account %(portal_user_name)s by %(user_name)s (#%(user_id)s)',
|
||||
user_name=current_user.name, user_id=current_user.id,
|
||||
portal_user_name=user.name),
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# DISCUSS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
from . import phone_blacklist
|
||||
from . import mail_thread_phone
|
||||
from . import res_partner
|
||||
from . import res_users
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, models
|
||||
from odoo.addons.phone_validation.tools import phone_validation
|
||||
|
||||
|
||||
class Users(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
def _deactivate_portal_user(self, **post):
|
||||
"""Blacklist the phone of the user after deleting it."""
|
||||
numbers_to_blacklist = {} # numbers to blacklist and the related user
|
||||
if post.get('request_blacklist'):
|
||||
for user in self:
|
||||
sanitized = phone_validation.phone_sanitize_numbers_w_record([user.phone, user.mobile], user)
|
||||
user_phone = sanitized[user.phone]['sanitized']
|
||||
user_mobile = sanitized[user.mobile]['sanitized']
|
||||
if user_phone:
|
||||
numbers_to_blacklist[user_phone] = user
|
||||
if user_mobile:
|
||||
numbers_to_blacklist[user_mobile] = user
|
||||
|
||||
super(Users, self)._deactivate_portal_user(**post)
|
||||
|
||||
if numbers_to_blacklist:
|
||||
current_user = self.env.user
|
||||
blacklists = self.env['phone.blacklist']._add(
|
||||
list(numbers_to_blacklist.keys()))
|
||||
for blacklist in blacklists:
|
||||
user = numbers_to_blacklist[blacklist.number]
|
||||
blacklist._message_log(
|
||||
body=_('Blocked by deletion of portal account %(portal_user_name)s by %(user_name)s (#%(user_id)s)',
|
||||
user_name=current_user.name, user_id=current_user.id,
|
||||
portal_user_name=user.name),
|
||||
)
|
||||
@@ -225,6 +225,7 @@ class CustomerPortal(Controller):
|
||||
values = self._prepare_portal_layout_values()
|
||||
values['get_error'] = get_error
|
||||
values['allow_api_keys'] = bool(request.env['ir.config_parameter'].sudo().get_param('portal.allow_api_keys'))
|
||||
values['open_deactivate_modal'] = False
|
||||
|
||||
if request.httprequest.method == 'POST':
|
||||
values.update(self._update_password(
|
||||
@@ -262,6 +263,30 @@ class CustomerPortal(Controller):
|
||||
|
||||
return {'success': {'password': True}}
|
||||
|
||||
@route('/my/deactivate_account', type='http', auth='user', website=True, methods=['POST'])
|
||||
def deactivate_account(self, validation, password, **post):
|
||||
values = self._prepare_portal_layout_values()
|
||||
values['get_error'] = get_error
|
||||
values['open_deactivate_modal'] = True
|
||||
|
||||
if validation != request.env.user.login:
|
||||
values['errors'] = {'deactivate': 'validation'}
|
||||
else:
|
||||
try:
|
||||
request.env['res.users']._check_credentials(password, {'interactive': True})
|
||||
request.env.user.sudo()._deactivate_portal_user(**post)
|
||||
request.session.logout()
|
||||
return request.redirect('/web/login?message=%s' % urls.url_quote(_('Account deleted!')))
|
||||
except AccessDenied:
|
||||
values['errors'] = {'deactivate': 'password'}
|
||||
except UserError as e:
|
||||
values['errors'] = {'deactivate': {'other': str(e)}}
|
||||
|
||||
return request.render('portal.portal_my_security', values, headers={
|
||||
'X-Frame-Options': 'SAMEORIGIN',
|
||||
'Content-Security-Policy': "frame-ancestors 'self'",
|
||||
})
|
||||
|
||||
@http.route('/portal/attachment/add', type='http', auth='public', methods=['POST'], website=True)
|
||||
def attachment_add(self, name, file, res_model, res_id, access_token=None, **kwargs):
|
||||
"""Process a file uploaded from the portal chatter and create the
|
||||
|
||||
@@ -237,6 +237,30 @@ publicWidget.registry.RemoveAPIKeyButton = publicWidget.Widget.extend({
|
||||
}
|
||||
});
|
||||
|
||||
publicWidget.registry.portalSecurity = publicWidget.Widget.extend({
|
||||
selector: '.o_portal_security_body',
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init: function () {
|
||||
// Show the "deactivate your account" modal if needed
|
||||
$('.modal.show#portal_deactivate_account_modal').removeClass('d-block').modal('show');
|
||||
|
||||
// Remove the error messages when we close the modal,
|
||||
// so when we re-open it again we get a fresh new form
|
||||
$('.modal#portal_deactivate_account_modal').on('hide.bs.modal', (event) => {
|
||||
const $target = $(event.currentTarget);
|
||||
$target.find('.alert').remove();
|
||||
$target.find('.invalid-feedback').remove();
|
||||
$target.find('.is-invalid').removeClass('is-invalid');
|
||||
});
|
||||
|
||||
return this._super(...arguments);
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* Wraps an RPC call in a check for the result being an identity check action
|
||||
* descriptor. If no such result is found, just returns the wrapped promise's
|
||||
|
||||
@@ -542,6 +542,11 @@ form label {
|
||||
@extend .text-nowrap;
|
||||
}
|
||||
}
|
||||
section[name="portal_deactivate_account"] {
|
||||
label {
|
||||
white-space: normal!important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copyright
|
||||
|
||||
@@ -479,7 +479,7 @@
|
||||
<div class="alert alert-danger" role="alert" t-if="get_error(errors)">
|
||||
<t t-esc="errors"/>
|
||||
</div>
|
||||
<section>
|
||||
<section name="portal_change_password">
|
||||
<h3>Change Password</h3>
|
||||
<t t-set="path">password</t>
|
||||
<div class="alert alert-success" role="alert" t-if="success and success.get('password')">
|
||||
@@ -556,6 +556,73 @@
|
||||
<button type="submit" class="btn btn-secondary o_portal_new_api_key">New API Key</button>
|
||||
</div>
|
||||
</section>
|
||||
<section name="portal_deactivate_account" groups="base.group_portal">
|
||||
<h3>Delete Account</h3>
|
||||
<t t-set="deactivate_error" t-value="get_error(errors, 'deactivate')"/>
|
||||
<button class="btn btn-secondary" data-toggle="modal"
|
||||
data-target="#portal_deactivate_account_modal">
|
||||
Delete Account
|
||||
</button>
|
||||
<div t-attf-class="modal #{'show d-block' if open_deactivate_modal else ''}"
|
||||
id="portal_deactivate_account_modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-danger">
|
||||
<h5 class="modal-title">Are you sure you want to do this?</h5>
|
||||
<button type="button" class="close" data-dismiss="modal">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<form action="/my/deactivate_account" method="post" class="modal-body"
|
||||
id="portal_deactivate_account_form">
|
||||
<div>
|
||||
<div class="alert alert-danger"
|
||||
t-esc="get_error(errors, 'deactivate.other')"/>
|
||||
<p class="text-muted">
|
||||
Disable your account, preventing any further login.<br/>
|
||||
<b>
|
||||
<i class="fa fa-exclamation-triangle text-danger"></i>
|
||||
This action cannot be undone.
|
||||
</b>
|
||||
</p>
|
||||
<hr/>
|
||||
<p>1. Enter your password to confirm you own this account</p>
|
||||
<input name="password" type="password" required="1"
|
||||
t-attf-class="form-control #{'is-invalid' if deactivate_error == 'password' else ''}"
|
||||
placeholder="Password"/>
|
||||
<div t-if="deactivate_error == 'password'" class="invalid-feedback">
|
||||
Wrong password.
|
||||
</div>
|
||||
<hr/>
|
||||
<p>
|
||||
2. Confirm you want to delete your account by
|
||||
copying down your login (<t t-esc="env.user.login"/>).
|
||||
</p>
|
||||
<input name="validation" type="text" required="1"
|
||||
t-attf-class="form-control #{'is-invalid' if deactivate_error == 'validation' else ''}"/>
|
||||
<div t-if="deactivate_error == 'validation'" class="invalid-feedback">
|
||||
You should enter "<t t-esc="env.user.login"/>" to validate your action.
|
||||
</div>
|
||||
<div class="d-flex flex-row align-items-center">
|
||||
<input type="checkbox" name="request_blacklist" id="request_blacklist" checked="1"/>
|
||||
<label for="request_blacklist" class="ml-2 mw-100 font-weight-normal mt-3">
|
||||
Put my email and phone in a block list to make sure I'm never contacted again
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
</form>
|
||||
<div class="modal-footer justify-content-start">
|
||||
<input type="submit" class="btn btn-danger" form="portal_deactivate_account_form"
|
||||
value="Delete Account"/>
|
||||
<button type="button" class="btn" data-dismiss="modal">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div></t>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from . import test_mass_mailing_statistics
|
||||
from . import test_mass_sms
|
||||
from . import test_portal
|
||||
from . import test_rating
|
||||
from . import test_res_users
|
||||
from . import test_sms_composer
|
||||
from . import test_sms_management
|
||||
from . import test_sms_performance
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.mail.tests.common import MailCommon, mail_new_test_user
|
||||
|
||||
|
||||
class TestResUsers(MailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestResUsers, cls).setUpClass()
|
||||
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',
|
||||
email='portal@test.example.com',
|
||||
groups='base.group_portal',
|
||||
)
|
||||
|
||||
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',
|
||||
email='portal_2@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()
|
||||
|
||||
def test_deactivate_portal_users_blacklist(self):
|
||||
"""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.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')
|
||||
|
||||
blacklist = self.env['mail.blacklist'].search([
|
||||
('email', 'in', ('portal@test.example.com', 'portal_2@test.example.com')),
|
||||
])
|
||||
self.assertEqual(len(blacklist), 2, 'Should have blacklisted the users email')
|
||||
|
||||
blacklists = self.env['phone.blacklist'].search([
|
||||
('number', 'in', ('+32494123489', '+32494123456', '+32494123422')),
|
||||
])
|
||||
self.assertEqual(len(blacklists), 3, 'Should have blacklisted the user phone and mobile')
|
||||
|
||||
blacklist = self.env['phone.blacklist'].search([('number', '=', 'invalid phone')])
|
||||
self.assertFalse(blacklist, 'Should have skipped invalid phone')
|
||||
|
||||
def test_deactivate_portal_users_no_blacklist(self):
|
||||
"""Test the case when the user do not want to blacklist his email / phone."""
|
||||
(self.portal_user | self.portal_user_2)._deactivate_portal_user(request_blacklist=False)
|
||||
|
||||
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')
|
||||
|
||||
blacklists = self.env['mail.blacklist'].search([])
|
||||
self.assertFalse(blacklists, 'Should not have blacklisted the users email')
|
||||
|
||||
blacklists = self.env['phone.blacklist'].search([])
|
||||
self.assertFalse(blacklists, 'Should not have blacklisted the user phone and mobile')
|
||||
@@ -44,5 +44,6 @@ from . import res_config
|
||||
from . import res_currency
|
||||
from . import res_company
|
||||
from . import res_users
|
||||
from . import res_users_deletion
|
||||
|
||||
from . import decimal_precision
|
||||
|
||||
@@ -816,6 +816,61 @@ class Users(models.Model):
|
||||
# use self.env.user here, because it has uid=SUPERUSER_ID
|
||||
return self.env.user.write({'password': new_passwd})
|
||||
|
||||
def _deactivate_portal_user(self, **post):
|
||||
"""Try to remove the current portal user.
|
||||
|
||||
This is used to give the opportunity to portal users to de-activate their accounts.
|
||||
Indeed, as the portal users can easily create accounts, they will sometimes wish
|
||||
it removed because they don't use this Odoo portal anymore.
|
||||
|
||||
Before this feature, they would have to contact the website or the support to get
|
||||
their account removed, which could be tedious.
|
||||
"""
|
||||
non_portal_users = self.filtered(lambda user: not user.share)
|
||||
if non_portal_users:
|
||||
raise AccessDenied(_(
|
||||
'Only the portal users can delete their accounts. '
|
||||
'The user(s) %s can not be deleted.',
|
||||
', '.join(non_portal_users.mapped('name')),
|
||||
))
|
||||
|
||||
ip = request.httprequest.environ['REMOTE_ADDR'] if request else 'n/a'
|
||||
|
||||
res_users_deletion_values = []
|
||||
|
||||
for user in self:
|
||||
_logger.info(
|
||||
'Account deletion asked for "%s" (#%i) from %s. '
|
||||
'Archive the user and remove login information.',
|
||||
user.login, user.id, ip,
|
||||
)
|
||||
|
||||
user.write({
|
||||
'login': f'__deleted_user_{user.id}_{time.time()}',
|
||||
'password': '',
|
||||
'api_key_ids': Command.clear(),
|
||||
})
|
||||
|
||||
res_users_deletion_values.append({
|
||||
'user_id': user.id,
|
||||
'state': 'todo',
|
||||
})
|
||||
|
||||
# Here we try to archive the user / partner, and then add the user in a deletion
|
||||
# queue, to remove it from the database. As the deletion might fail (if the
|
||||
# partner is related to an invoice e.g.) it's important to archive it here.
|
||||
try:
|
||||
# A user can not self-deactivate
|
||||
self.with_user(SUPERUSER_ID).action_archive()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.partner_id.action_archive()
|
||||
except Exception:
|
||||
pass
|
||||
# Add users in the deletion queue
|
||||
self.env['res.users.deletion'].create(res_users_deletion_values)
|
||||
|
||||
def preference_save(self):
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ResUsersDeletion(models.Model):
|
||||
"""User deletion requests.
|
||||
|
||||
Those requests are logged in a different model to keep a trace of this action and the
|
||||
deletion is done in a CRON. Indeed, removing a user can be a heavy operation on
|
||||
large database (because of create_uid, write_uid on each model, which are not always
|
||||
indexed). This model just remove the users added in the deletion queue, remaining code
|
||||
must deal with other consideration (archiving, blacklist email...).
|
||||
"""
|
||||
|
||||
_name = 'res.users.deletion'
|
||||
_description = 'Users Deletion Request'
|
||||
_rec_name = 'user_id'
|
||||
|
||||
# Integer field because the related user might be deleted from the database
|
||||
user_id = fields.Many2one('res.users', string='User', ondelete='set null')
|
||||
user_id_int = fields.Integer('User Id', compute='_compute_user_id_int', store=True)
|
||||
state = fields.Selection([('todo', 'To Do'), ('done', 'Done'), ('fail', 'Failed')],
|
||||
string='State', required=True, default='todo')
|
||||
|
||||
@api.depends('user_id')
|
||||
def _compute_user_id_int(self):
|
||||
for user_deletion in self:
|
||||
if user_deletion.user_id:
|
||||
user_deletion.user_id_int = user_deletion.user_id.id
|
||||
|
||||
@api.autovacuum
|
||||
def _gc_portal_users(self):
|
||||
"""Remove the portal users that asked to deactivate their account.
|
||||
|
||||
(see <res.users>::_deactivate_portal_user)
|
||||
|
||||
Removing a user can be an heavy operation on large database (because of
|
||||
create_uid, write_uid on each models, which are not always indexed). Because of
|
||||
that, this operation is done in a CRON.
|
||||
"""
|
||||
delete_requests = self.search([('state', '=', 'todo')])
|
||||
|
||||
# filter the requests related to a deleted user
|
||||
done_requests = delete_requests.filtered(lambda request: not request.user_id)
|
||||
done_requests.state = 'done'
|
||||
|
||||
for delete_request in (delete_requests - done_requests):
|
||||
user = delete_request.user_id
|
||||
user_name = user.name
|
||||
try:
|
||||
with self.env.cr.savepoint():
|
||||
partner = user.partner_id
|
||||
user.unlink()
|
||||
partner.unlink()
|
||||
_logger.info('User #%i %r, deleted. Original request from %r.',
|
||||
user.id, user_name, delete_request.create_uid.name)
|
||||
delete_request.state = 'done'
|
||||
except Exception:
|
||||
delete_request.state = 'fail'
|
||||
@@ -70,6 +70,8 @@
|
||||
"access_res_partner_title_group_partner_manager","res_partner_title group_user","model_res_partner_title",,1,0,0,0
|
||||
"access_res_users_all","res_users all","model_res_users",,1,0,0,0
|
||||
"access_res_users_group_erp_manager","res_users group_erp_manager","model_res_users","group_erp_manager",1,1,1,1
|
||||
"access_res_users_deletion_all","res_users_deletion all","model_res_users_deletion",,0,0,0,0
|
||||
"access_res_users_deletion_group_erp_manager","res_users_deletion group_erp_manager","model_res_users_deletion","group_erp_manager",1,1,1,1
|
||||
"access_res_users_log_all","res_users_log_all","model_res_users_log",,1,0,1,0
|
||||
"access_res_users_identitycheck_employee","id check employees","model_res_users_identitycheck","group_user",1,1,1,0
|
||||
"access_res_users_identitycheck_portal","id check portal","model_res_users_identitycheck","group_portal",1,1,1,0
|
||||
|
||||
|
@@ -2,7 +2,9 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.base.models.res_users import is_selection_groups, get_selection_groups
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests.common import TransactionCase, Form, tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
class TestUsers(TransactionCase):
|
||||
@@ -116,6 +118,77 @@ class TestUsers(TransactionCase):
|
||||
"the company_id of the partner_id shall be updated"
|
||||
)
|
||||
|
||||
@mute_logger('odoo.sql_db')
|
||||
def test_deactivate_portal_users_access(self):
|
||||
"""Test that only a portal users can deactivate his account."""
|
||||
user_internal = self.env['res.users'].create({
|
||||
'name': 'Internal',
|
||||
'login': 'user_internal',
|
||||
'password': 'password',
|
||||
'groups_id': [self.env.ref('base.group_user').id],
|
||||
})
|
||||
|
||||
with self.assertRaises(UserError, msg='Internal users should not be able to deactivate their account'):
|
||||
user_internal._deactivate_portal_user()
|
||||
|
||||
@mute_logger('odoo.sql_db')
|
||||
def test_deactivate_portal_users_archive_and_remove(self):
|
||||
"""Test that if the account can not be removed, it's archived instead
|
||||
and sensitive information are removed.
|
||||
|
||||
In this test, the deletion of "portal_user" will succeed,
|
||||
but the deletion of "portal_user_2" will fail.
|
||||
"""
|
||||
User = self.env['res.users']
|
||||
portal_user = User.create({
|
||||
'name': 'Portal',
|
||||
'login': 'portal_user',
|
||||
'password': 'password',
|
||||
'groups_id': [self.env.ref('base.group_portal').id],
|
||||
})
|
||||
portal_partner = portal_user.partner_id
|
||||
|
||||
portal_user_2 = User.create({
|
||||
'name': 'Portal',
|
||||
'login': 'portal_user_2',
|
||||
'password': 'password',
|
||||
'groups_id': [self.env.ref('base.group_portal').id],
|
||||
})
|
||||
portal_partner_2 = portal_user_2.partner_id
|
||||
|
||||
(portal_user | portal_user_2)._deactivate_portal_user()
|
||||
|
||||
self.assertTrue(portal_user.exists() and not portal_user.active, 'Should have archived the user 1')
|
||||
|
||||
self.assertEqual(portal_user.name, 'Portal', 'Should have kept the user name')
|
||||
self.assertEqual(portal_user.partner_id.name, 'Portal', 'Should have kept the partner name')
|
||||
self.assertNotEqual(portal_user.login, 'portal_user', 'Should have removed the user login')
|
||||
|
||||
asked_deletion_1 = self.env['res.users.deletion'].search([('user_id', '=', portal_user.id)])
|
||||
asked_deletion_2 = self.env['res.users.deletion'].search([('user_id', '=', portal_user_2.id)])
|
||||
|
||||
self.assertTrue(asked_deletion_1, 'Should have added the user 1 in the deletion queue')
|
||||
self.assertTrue(asked_deletion_2, 'Should have added the user 2 in the deletion queue')
|
||||
|
||||
# The deletion will fail for "portal_user_2",
|
||||
# because of the absence of "ondelete=cascade"
|
||||
self.cron = self.env['ir.cron'].create({
|
||||
'name': 'Test Cron',
|
||||
'user_id': portal_user_2.id,
|
||||
'model_id': self.env.ref('base.model_res_partner').id,
|
||||
})
|
||||
|
||||
self.env['res.users.deletion']._gc_portal_users()
|
||||
|
||||
self.assertFalse(portal_user.exists(), 'Should have removed the user')
|
||||
self.assertFalse(portal_partner.exists(), 'Should have removed the partner')
|
||||
self.assertEqual(asked_deletion_1.state, 'done', 'Should have marked the deletion as done')
|
||||
|
||||
self.assertTrue(portal_user_2.exists(), 'Should have kept the user')
|
||||
self.assertTrue(portal_partner_2.exists(), 'Should have kept the partner')
|
||||
self.assertEqual(asked_deletion_2.state, 'fail', 'Should have marked the deletion as failed')
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestUsers2(TransactionCase):
|
||||
def test_reified_groups(self):
|
||||
|
||||
Reference in New Issue
Block a user