diff --git a/flectra/addons/base/models/assetsbundle.py b/flectra/addons/base/models/assetsbundle.py index d3dc551251..fb449a0871 100644 --- a/flectra/addons/base/models/assetsbundle.py +++ b/flectra/addons/base/models/assetsbundle.py @@ -219,6 +219,18 @@ class AssetsBundle(object): **self._get_asset_url_values(id=id, unique=unique, extra=extra, name=name, sep=sep, type=type) ) + def _unlink_attachments(self, attachments): + """ Unlinks attachments without actually calling unlink, so that the ORM cache is not cleared. + + Specifically, if an attachment is generated while a view is rendered, clearing the ORM cache + could unload fields loaded with a sudo(), and expected to be readable by the view. + Such a view would be website.layout when main_object is an ir.ui.view. + """ + to_delete = set(attach.store_fname for attach in attachments if attach.store_fname) + self.env.cr.execute(f"DELETE FROM {attachments._table} WHERE id IN %s", [tuple(attachments.ids)]) + for file_path in to_delete: + attachments._file_delete(file_path) + def clean_attachments(self, type): """ Takes care of deleting any outdated ir.attachment records associated to a bundle before saving a fresh one. @@ -244,7 +256,7 @@ class AssetsBundle(object): # avoid to invalidate cache if it's already empty (mainly useful for test) if attachments: - attachments.unlink() + self._unlink_attachments(attachments) # force bundle invalidation on other workers self.env['ir.qweb'].clear_caches() @@ -478,7 +490,7 @@ class AssetsBundle(object): compiled = self.run_rtlcss(compiled) if not self.css_errors and old_attachments: - old_attachments.unlink() + self._unlink_attachments(old_attachments) old_attachments = None fragments = self.rx_css_split.split(compiled) diff --git a/flectra/addons/base/models/ir_attachment.py b/flectra/addons/base/models/ir_attachment.py index 3ef5a67790..72604fbdb7 100644 --- a/flectra/addons/base/models/ir_attachment.py +++ b/flectra/addons/base/models/ir_attachment.py @@ -128,6 +128,7 @@ class IrAttachment(models.Model): def _mark_for_gc(self, fname): """ Add ``fname`` in a checklist for the filestore garbage collection. """ + fname = re.sub('[.]', '', fname).strip('/\\') # we use a spooldir: add an empty file in the subdirectory 'checklist' full_path = os.path.join(self._full_path('checklist'), fname) if not os.path.exists(full_path): @@ -564,7 +565,9 @@ class IrAttachment(models.Model): def copy(self, default=None): self.check('write') - default = dict(default or {}, datas=self.datas) + if not (default or {}).keys() & {'datas', 'db_datas', 'raw'}: + # ensure the content is kept and recomputes checksum/store_fname + default = dict(default or {}, raw=self.raw) return super(IrAttachment, self).copy(default) def unlink(self): diff --git a/flectra/addons/base/models/ir_mail_server.py b/flectra/addons/base/models/ir_mail_server.py index b468fc384f..dbe751bd3b 100644 --- a/flectra/addons/base/models/ir_mail_server.py +++ b/flectra/addons/base/models/ir_mail_server.py @@ -195,6 +195,9 @@ class IrMailServer(models.Model): elif not host: mail_server = self.sudo().search([], order='sequence', limit=1) + if not mail_server: + mail_server = self.env['ir.mail_server'] + if mail_server: smtp_server = mail_server.smtp_host smtp_port = mail_server.smtp_port @@ -243,7 +246,7 @@ class IrMailServer(models.Model): local, at, domain = smtp_user.rpartition('@') if at: smtp_user = local + at + idna.encode(domain).decode('ascii') - connection.login(smtp_user, smtp_password or '') + mail_server._smtp_login(connection, smtp_user, smtp_password or '') # Some methods of SMTP don't check whether EHLO/HELO was sent. # Anyway, as it may have been sent by login(), all subsequent usages should consider this command as sent. @@ -251,6 +254,18 @@ class IrMailServer(models.Model): return connection + def _smtp_login(self, connection, smtp_user, smtp_password): + """Authenticate the SMTP connection. + + Can be overridden in other module for different authentication methods.Can be + called on the model itself or on a singleton. + + :param connection: The SMTP connection to authenticate + :param smtp_user: The user to used for the authentication + :param smtp_password: The password to used for the authentication + """ + connection.login(smtp_user, smtp_password) + def build_email(self, email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False, attachments=None, message_id=None, references=None, object_id=False, subtype='plain', headers=None, body_alternative=None, subtype_alternative='plain'): diff --git a/flectra/addons/base/models/ir_module.py b/flectra/addons/base/models/ir_module.py index 73e56ac8e9..6980e6e4e2 100644 --- a/flectra/addons/base/models/ir_module.py +++ b/flectra/addons/base/models/ir_module.py @@ -963,7 +963,6 @@ class Module(models.Model): [('id', 'not in', excluded_category_ids)], ]) - Module = self.env['ir.module.module'] records = self.env['ir.module.category'].search_read(domain, ['display_name'], order="sequence") values_range = OrderedDict() @@ -976,7 +975,7 @@ class Module(models.Model): kwargs.get('filter_domain', []), [('category_id', 'child_of', record_id), ('category_id', 'not in', excluded_category_ids)] ]) - record['__count'] = Module.search_count(model_domain) + record['__count'] = self.env['ir.module.module'].search_count(model_domain) values_range[record_id] = record return { diff --git a/flectra/addons/base/models/res_users.py b/flectra/addons/base/models/res_users.py index 5ce18b9c6a..984deb5141 100644 --- a/flectra/addons/base/models/res_users.py +++ b/flectra/addons/base/models/res_users.py @@ -999,6 +999,10 @@ class Users(models.Model): return 'base/static/img/user-slash.png' return super()._get_placeholder_filename(field=field) + def _mfa_type(self): + """ If an MFA method is enabled, returns its type as a string. """ + return + def _mfa_url(self): """ If an MFA method is enabled, returns the URL for its second step. """ return diff --git a/flectra/addons/base/tests/test_ir_attachment.py b/flectra/addons/base/tests/test_ir_attachment.py index 9b2fa982c1..57dbf0fb29 100644 --- a/flectra/addons/base/tests/test_ir_attachment.py +++ b/flectra/addons/base/tests/test_ir_attachment.py @@ -24,11 +24,12 @@ class TestIrAttachment(TransactionCase): # Blob1 self.blob1 = b'blob1' self.blob1_b64 = base64.b64encode(self.blob1) - blob1_hash = hashlib.sha1(self.blob1).hexdigest() - self.blob1_fname = blob1_hash[:HASH_SPLIT] + '/' + blob1_hash + self.blob1_hash = hashlib.sha1(self.blob1).hexdigest() + self.blob1_fname = self.blob1_hash[:HASH_SPLIT] + '/' + self.blob1_hash # Blob2 self.blob2 = b'blob2' + self.blob2_b64 = base64.b64encode(self.blob2) def assertApproximately(self, value, expectedSize, delta=1): # we don't used bin_size in context, because on write, the cached value is the data and not @@ -239,3 +240,23 @@ class TestIrAttachment(TransactionCase): self.env['ir.config_parameter'].set_param('base.image_autoresize_max_px', '0') attach.raw = img_bin self.assertApproximately(attach.raw, fullsize) + + def test_11_copy(self): + """ + Copying an attachment preserves the data + """ + document = self.Attachment.create({'name': 'document', 'datas': self.blob2_b64}) + document2 = document.copy({'name': "document (copy)"}) + self.assertEqual(document2.name, "document (copy)") + self.assertEqual(document2.datas, document.datas) + self.assertEqual(document2.db_datas, document.db_datas) + self.assertEqual(document2.store_fname, document.store_fname) + self.assertEqual(document2.checksum, document.checksum) + + document3 = document.copy({'datas': self.blob1_b64}) + self.assertEqual(document3.datas, self.blob1_b64) + self.assertEqual(document3.raw, self.blob1) + self.assertTrue(self.filestore) # no data in db but has a store_fname + self.assertEqual(document3.db_datas, False) + self.assertEqual(document3.store_fname, self.blob1_fname) + self.assertEqual(document3.checksum, self.blob1_hash)