# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import base64 from collections import defaultdict from contextlib import contextmanager, ExitStack from datetime import datetime, timedelta from unittest import skip from unittest.mock import patch from odoo import Command, http from odoo.exceptions import AccessError, UserError, ValidationError from odoo.tests.common import new_test_user from odoo.tests import users from odoo.tools import mute_logger from .test_documents_common import TransactionCaseDocuments, GIF, TEXT, WEBP DATA = "data:application/zip;base64,R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=" file_a = {'name': 'doc.zip', 'datas': 'data:application/zip;base64,R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs='} class TestCaseDocuments(TransactionCaseDocuments): @skip("TODO: move to controller") @users('documents@example.com') def test_documents_action_log_access_archived(self): access = self.env['documents.access'].search([ ('document_id', '=', self.document_txt.id), ('partner_id', '=', self.env.user.partner_id.id), ]) self.assertFalse(access) self.document_txt.action_archive() self.env['documents.document'].action_log_access( self.document_txt.access_token) access = self.env['documents.access'].search([ ('document_id', '=', self.document_txt.id), ('partner_id', '=', self.env.user.partner_id.id), ]) self.assertTrue(access) def test_documents_action_create_shortcut(self): # Make sure we test copying m2o too self.document_gif.partner_id = self.env.user.partner_id shortcut = self.document_gif.action_create_shortcut() self.assertFalse(shortcut.attachment_id) original_file_size = self.document_gif.file_size for field_name in self.env['documents.document']._get_shortcuts_copy_fields(): with self.subTest(field_name=field_name): self.assertEqual(shortcut[field_name], self.document_gif[field_name]) attachment = self.env['ir.attachment'].create({ **file_a, 'res_model': False, 'res_id': False, }) self.document_gif.attachment_id = attachment self.assertNotEqual(self.document_gif.file_size, original_file_size) self.assertEqual(shortcut.file_size, self.document_gif.file_size) self.assertEqual(shortcut.file_extension, self.document_gif.file_extension) def test_documents_action_delete_from_history(self): """Test the `action_delete_from_history` action of documents.""" current_attachment = self.document_gif.attachment_id # We can not delete the attachment if the history is empty with self.assertRaises(UserError): self.document_gif.action_delete_from_history(current_attachment.id) self.assertTrue(current_attachment.exists()) new_attachment, other_attachment = self.env['ir.attachment'].create([{'name': 'Test'}] * 2) self.document_gif.previous_attachment_ids = new_attachment # We can not delete the attachment if it's not in the history of the document with self.assertRaises(UserError): self.document_gif.action_delete_from_history(other_attachment.id) self.assertTrue(current_attachment.exists()) self.assertTrue(new_attachment.exists()) self.assertTrue(other_attachment.exists()) # Delete the history version delete the attachment self.document_gif.action_delete_from_history(new_attachment.id) self.assertFalse(new_attachment.exists(), "The attachment should have been deleted") self.assertTrue(current_attachment.exists()) # Deleting the current attachment restore the most recent version current_attachment = self.document_gif.attachment_id versions = self.env['ir.attachment'].create([{'name': 'Test'}] * 3) self.document_gif.previous_attachment_ids = versions self.document_gif.action_delete_from_history(current_attachment.id) self.assertFalse(current_attachment.exists()) self.assertEqual(self.document_gif.attachment_id.id, max(versions.ids)) def test_documents_create_from_attachment(self): """ Tests a documents.document create method when created from an already existing ir.attachment. """ attachment = self.env['ir.attachment'].create({ 'datas': GIF, 'name': 'attachmentGif.gif', 'res_model': 'documents.document', 'res_id': 0, }) document_a = self.env['documents.document'].create({ 'folder_id': self.folder_b.id, 'name': 'new name', 'attachment_id': attachment.id, }) self.assertEqual(document_a.attachment_id.id, attachment.id, 'the attachment should be the attachment given in the create values') self.assertEqual(document_a.name, 'new name', 'the name given should be used') self.assertFalse(document_a.res_model) self.assertFalse(document_a.res_id) def test_documents_create_res_model(self): """Test the model set on a document and its attachment.""" attachment = self.env['ir.attachment'].create({'name': 'test', 'res_model': 'documents.document'}) doc = self.env['documents.document'].create({'attachment_id': attachment.id}) self.assertEqual(attachment.res_model, 'documents.document') self.assertEqual(attachment.res_id, doc.id) attachment = self.env['ir.attachment'].create({'name': 'test', 'res_model': 'documents.document'}) partner = self.env['res.partner'].create({'name': 'partner'}) doc = self.env['documents.document'].create({'attachment_id': attachment.id, 'res_model': 'res.partner', 'res_id': partner.id}) self.assertEqual(attachment.res_model, 'res.partner') self.assertEqual(attachment.res_id, partner.id) self.assertEqual(doc.res_model, 'res.partner') self.assertEqual(doc.res_id, partner.id) user = self.portal_user attachment = self.env['ir.attachment'].create({'name': 'test', 'res_model': 'res.users', 'res_id': user.id}) doc = self.env['documents.document'].create({'attachment_id': attachment.id}) self.assertEqual(attachment.res_model, 'res.users') self.assertEqual(attachment.res_id, user.id) self.assertEqual(doc.res_model, 'res.users') self.assertEqual(doc.res_id, user.id) doc.attachment_id = False doc.name = 'test' doc.datas = 'ZGF0YQ==' self.assertEqual(doc.attachment_id.res_model, 'res.users') self.assertEqual(doc.attachment_id.res_id, user.id) self.assertEqual(doc.attachment_id.name, 'test') doc.write({'attachment_id': False, 'res_model': False, 'res_id': False}) doc.datas = 'ZGF0YQ==' self.assertEqual(doc.attachment_id.res_model, 'documents.document') self.assertEqual(doc.attachment_id.res_id, doc.id) self.assertEqual(doc.attachment_id.name, 'test') doc = self.env['documents.document'].create({'datas': 'ZGF0YQ=='}) self.assertFalse(doc.res_model) self.assertFalse(doc.res_id) self.assertEqual(doc.attachment_id.res_model, 'documents.document') self.assertEqual(doc.attachment_id.res_id, doc.id) @users('documents@example.com') def test_documents_create_write(self): """ Tests a documents.document create and write method, documents should automatically create a new ir.attachments in relevant cases. """ document_a = self.env['documents.document'].create({ 'name': 'Test mimetype gif', 'datas': GIF, 'folder_id': self.folder_b.id, }) self.assertFalse(document_a.res_model) self.assertFalse(document_a.res_id) self.assertEqual(document_a.attachment_id.res_model, 'documents.document', 'the res_model should be set as document by default') self.assertEqual(document_a.attachment_id.res_id, document_a.id, 'the res_id should be set as the document id by default to allow access right inheritance') self.assertEqual(document_a.attachment_id.datas, GIF, 'the document should have a GIF data') document_no_attachment = self.env['documents.document'].create({ 'name': 'Test mimetype gif', 'folder_id': self.folder_b.id, }) self.assertFalse(document_no_attachment.attachment_id, 'the new document shouldnt have any attachment_id') document_no_attachment.write({'datas': TEXT}) self.assertEqual(document_no_attachment.attachment_id.datas, TEXT, 'the document should have an attachment') def test_documents_create_performance(self): folders = self.env['documents.document'].create([ {'type': 'folder', 'name': f'Folder {i}', 'access_internal': 'view'} for i in range(50) ]) folders.flush_recordset() folders.invalidate_recordset() with self.assertQueryCount(161): self.env['documents.document'].create([{ 'folder_id': folder.id, 'type': 'binary', } for folder in folders]) def test_default_res_id_model(self): """ Test default res_id and res_model from context are used for linking attachment to document. """ document = self.env['documents.document'].create({'folder_id': self.folder_b.id}) attachment = self.env['ir.attachment'].with_context( default_res_id=document.id, default_res_model=document._name, ).create({ 'name': 'attachmentGif.gif', 'datas': GIF, }) self.assertEqual(attachment.res_id, document.id, "It should be linked to the default res_id") self.assertEqual(attachment.res_model, document._name, "It should be linked to the default res_model") self.assertEqual(document.attachment_id, attachment, "Document should be linked to the created attachment") @users('documents@example.com') def test_versioning(self): """ Tests the versioning/history of documents """ document = self.env["documents.document"].create( { "datas": GIF, "folder_id": self.folder_b.id, "res_model": "res.users", "res_id": self.doc_user.id, } ) def check_attachment_res_fields( attachment, expected_res_model, expected_res_id ): self.assertEqual( attachment.res_model, expected_res_model, "The attachment should be linked to the right model", ) self.assertEqual( attachment.res_id, expected_res_id, "The attachment should be linked to the right record", ) self.assertEqual(len(document.previous_attachment_ids.ids), 0, "The history should be empty") original_attachment = document.attachment_id check_attachment_res_fields(original_attachment, "res.users", self.doc_user.id) document.write({'datas': TEXT}) new_attachment = document.previous_attachment_ids check_attachment_res_fields(original_attachment, "res.users", self.doc_user.id) check_attachment_res_fields(new_attachment, "documents.document", document.id) self.assertEqual(len(document.previous_attachment_ids), 1) self.assertNotEqual(document.previous_attachment_ids, original_attachment) self.assertEqual(document.previous_attachment_ids[0].datas, GIF, "The history should have the right content") self.assertEqual(document.attachment_id.datas, TEXT, "The document should have the right content") old_attachment = document.attachment_id document.write({'attachment_id': new_attachment.id}) check_attachment_res_fields(new_attachment, "res.users", self.doc_user.id) check_attachment_res_fields(old_attachment, "documents.document", document.id) self.assertEqual(document.attachment_id.id, new_attachment.id, "the document should contain the new attachment") self.assertEqual(document.previous_attachment_ids, original_attachment, "the history should contain the original attachment") document.write({"attachment_id": document.attachment_id.id}) check_attachment_res_fields(new_attachment, "res.users", self.doc_user.id) self.assertEqual( document.attachment_id.id, new_attachment.id, "the document attachment should not have changed", ) self.assertTrue( new_attachment not in document.previous_attachment_ids, "the history should not contain the new attachment", ) document.write({'datas': DATA}) self.assertEqual(document.attachment_id, new_attachment) def test_write_mimetype(self): """ Tests the consistency of documents' mimetypes """ document = self.env['documents.document'].with_user(self.doc_user.id).create({'datas': GIF, 'folder_id': self.folder_b.id}) document.with_user(self.doc_user.id).write({'datas': TEXT, 'mimetype': 'text/plain'}) self.assertEqual(document.mimetype, 'text/plain', "the new mimetype should be the one given on write") document.with_user(self.doc_user.id).write({'datas': TEXT, 'mimetype': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'}) self.assertEqual(document.mimetype, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', "should preserve office mime type") def test_cascade_delete(self): """ Makes sure that documents are unlinked when their attachment is unlinked. """ document = self.env['documents.document'].create({'datas': GIF, 'folder_id': self.folder_b.id}) self.assertTrue(document.exists(), 'the document should exist') document.attachment_id.unlink() self.assertFalse(document.exists(), 'the document should not exist') def test_is_favorited(self): user = new_test_user(self.env, "test user", groups='documents.group_documents_user') document = self.env['documents.document'].create({'datas': GIF, 'folder_id': self.folder_b.id}) document.favorited_ids = user self.assertFalse(document.is_favorited) self.assertTrue(document.with_user(user).is_favorited) def test_neuter_mimetype(self): """ Tests that potentially harmful mimetypes (XML mimetypes that can lead to XSS attacks) are converted to text In fact this logic is implemented in the base `IrAttachment` model but was originally duplicated. The test stays duplicated here to ensure the de-duplicated logic still catches our use cases. """ self.folder_b.action_update_access_rights(partners={self.doc_user.partner_id: ('edit', False)}) document = self.env['documents.document'].create({'datas': GIF, 'folder_id': self.folder_b.id}) document.with_user(self.doc_user.id).write({'datas': TEXT, 'mimetype': 'text/xml'}) self.assertEqual(document.mimetype, 'text/plain', "XML mimetype should be forced to text") document.with_user(self.doc_user.id).write({'datas': TEXT, 'mimetype': 'image/svg+xml'}) self.assertEqual(document.mimetype, 'text/plain', "SVG mimetype should be forced to text") document.with_user(self.doc_user.id).write({'datas': TEXT, 'mimetype': 'text/html'}) self.assertEqual(document.mimetype, 'text/plain', "HTML mimetype should be forced to text") document.with_user(self.doc_user.id).write({'datas': TEXT, 'mimetype': 'application/xhtml+xml'}) self.assertEqual(document.mimetype, 'text/plain', "XHTML mimetype should be forced to text") def test_create_from_message_invalid_tags(self): """ Create a new document from message with a deleted tag, it should keep only existing tags. """ message = self.env['documents.document'].message_new({ 'subject': 'Test', }, { 'tag_ids': [(6, 0, [self.tag_b.id, -1])], 'folder_id': self.folder_a.id, }) self.assertEqual(message.tag_ids.ids, [self.tag_b.id], "Should only keep the existing tag") def test_file_extension(self): """ Test the detection of the file extension and its edition. """ sanitized_extension = 'txt' for extension in ('.txt', ' .txt', '..txt', '.txt ', ' .txt ', ' .txt '): document = self.env['documents.document'].create({ 'datas': base64.b64encode(b"Test"), 'name': f'name{extension}', 'mimetype': 'text/plain', 'folder_id': self.folder_b.id, }) self.assertEqual(document.file_extension, sanitized_extension, f'"{extension}" must be sanitized to "{sanitized_extension}" at creation') for extension in ('txt', ' txt', ' txt ', '.txt', ' .txt', ' .txt ', '..txt', ' ..txt '): document.file_extension = extension self.assertEqual(document.file_extension, sanitized_extension, f'"{extension}" must be sanitized to "{sanitized_extension}" at edition') # test extension when filename is changed (i.e. name is edited or file is replaced) document.name = 'test.png' self.assertEqual(document.file_extension, 'png', "extension must be updated on change in filename") def test_restricted_folder_multi_company(self): """ Tests the behavior of a restricted folder in a multi-company environment """ company_a = self.env.company company_b = self.env['res.company'].create({'name': 'Company B'}) user_b = self.env['res.users'].create({ 'name': 'User of company B', 'login': 'user_b', 'group_ids': [(6, 0, [self.ref('documents.group_documents_manager')])], 'company_id': company_b.id, 'company_ids': [(6, 0, [company_b.id])] }) self.folder_a.company_id = company_a self.assertEqual(self.folder_a.display_name, 'folder A', "The parent folder's name should not be hidden") self.assertEqual(self.folder_a.with_user(user_b).display_name, 'Restricted', "The parent folder's name should be hidden") self.assertEqual(self.folder_a_a.display_name, "folder A - A", "The parent folder name should not be included in the name") def test_unlink_attachments_with_documents(self): """ Tests a documents.document unlink method. Attachments should be deleted when related documents are deleted, for which res_model is not 'documents.document' or `False`. Test case description: Case 1: - upload a document with res_model 'res.partner'. - check if attachment exists. - unlink the document. - check if attachment exists or not. Case 2: - ensure the existing flow for res_model 'documents.document' does not break. Case 3: - ensure the existing flow for res_model `False` does not break. """ documents = self.env['documents.document'].create([{ 'datas': GIF, 'folder_id': self.folder_b.id, 'res_model': res_model, 'res_id': res_id, } for res_model, res_id in (('res.partner', self.internal_user.partner_id.id), (False, False)) ]) self.assertFalse(documents[1].res_model) self.assertEqual(documents[1].attachment_id.res_model, 'documents.document') for document in documents: with self.subTest(res_model=document.res_model): self.assertTrue(document.attachment_id.exists()) attachment = document.attachment_id document.unlink() self.assertFalse(attachment.exists()) def test_archive_and_unarchive_document(self): self.document_txt.action_archive() self.assertFalse(self.document_txt.active, 'the document should be inactive') self.document_txt.action_unarchive() self.assertTrue(self.document_txt.active, 'the document should be active') def test_archived_documents_operations(self): """Check (un)authorized moves of archived documents""" # Initial set up: # # folder_1 # ├─ folder_3 # │ ├─ request # folder_2 folder_1, folder_2, folder_3 = self.env['documents.document'].create([ {'name': f'Test Folder {idx + 1}', 'type': 'folder'} for idx in range(3) ]) folder_3.folder_id = folder_1 request = self.env['documents.document'].create({'name': 'Test Request', 'folder_id': folder_3.id}) # Can't move into archived folders folder_2.action_archive() with self.assertRaises(UserError): folder_3.folder_id = folder_2 folder_2.action_unarchive() # No-op doesn't fail folder_1.action_archive() folder_3.folder_id = folder_1 # Can't simply move archived documents with self.assertRaises(UserError): folder_3.folder_id = folder_2 with self.assertRaises(UserError): folder_3.user_folder_id = str(folder_2.id) # Move is allowed if specifying `active` as well (without children) *if the current folder is active* with self.assertRaises(UserError): folder_3.write({'folder_id': folder_2.id, 'active': True}) (folder_1 | folder_3).write({'folder_id': folder_2.id, 'active': True}) self.assertFalse(request.active, "folder_3's content should still be in the trash") def test_unarchive_document_with_archived_parent(self): """Unarchive a document whose parent folder is archived should send an error.""" document = self.document_txt def check_error_message(document): with self.assertRaises(UserError) as err: document.action_unarchive() self.assertEqual( err.exception.args[0], "Item(s) you wish to restore are included in archived folders. " "To restore these items, you must restore the following including folders instead:" "\n" "- folder B" ) self.folder_b.folder_id = self.folder_a # when the parent has folder_id self.folder_b.action_archive() check_error_message(document) self.folder_b.folder_id = False # when the parent has folder_id False check_error_message(document) def test_delete_document(self): self.document_txt.action_archive() self.assertFalse(self.document_txt.active, 'the document should be inactive') self.document_txt.unlink() self.assertFalse(self.document_txt.exists(), 'the document should not exist') def test_copy_document(self): @contextmanager def patched_compute_methods(): fields_to_recompute = self.env['documents.document']._get_fields_to_recompute(depends=['attachment_id']) self.assertSetEqual({f.name for f in fields_to_recompute}, { 'name', 'is_multipage', 'thumbnail', 'thumbnail_status', 'url_preview_image', 'file_extension', }) # A compute method might be used by multiple fields, # and we can't doubly mock the same compute computes_to_mock = defaultdict(list) for field in fields_to_recompute: computes_to_mock[field.compute].append(field) with ExitStack() as stack: for compute, fields in computes_to_mock.items(): stack.enter_context(patch.object( self.registry['documents.document'], compute, autospec=True, side_effect=self.failureException( f"The compute stored field `{'|'.join(f.name for f in fields)}` must not be triggered " "after a copy upon flushing, its value should be just copied or explicitly set." ), )) yield with patched_compute_methods(): with mute_logger('odoo.addons.documents.models.documents_document'): # Creating document(s) as superuser copy = self.document_txt.copy() self.assertEqual(copy.name, "file.txt (copy)") self.assertNotEqual( copy.attachment_id.ensure_one().id, self.document_txt.attachment_id.id, "There must be a new attachment" ) self.assertEqual(copy.raw, self.document_txt.raw) self.env.flush_all() # trigger recomputes self.assertEqual(copy.is_multipage, self.document_txt.is_multipage) with mute_logger('odoo.addons.documents.models.documents_document'): # Creating document(s) as superuser copy_with_default = self.document_txt.copy({"name": "test"}) self.assertEqual(copy_with_default.name, "test") self.assertNotEqual( copy.attachment_id.ensure_one().id, self.document_txt.attachment_id.id, "There must be a new attachment" ) self.assertEqual(copy.raw, self.document_txt.raw) # check that we can copy in a folder inside the company folder self.assertFalse(self.folder_a.folder_id) self.folder_a.owner_id = False self.folder_a.access_internal = 'edit' # Special case where we can not write, but `user_permission == edit` because # the folder is in the company root with self.assertRaises(AccessError): self.folder_a.with_user(self.internal_user).check_access('write') self.assertEqual(self.folder_a.with_user(self.internal_user).user_permission, 'edit') self.document_txt.folder_id = self.folder_a self.document_txt.with_user(self.internal_user).copy() # copy in batch documents of different folder self.assertNotEqual(self.document_txt.folder_id, self.document_gif.folder_id) copied_documents = (self.document_txt | self.document_gif).with_user(self.internal_user).copy() self.assertEqual(copied_documents[0].name, f'{self.document_txt.name} (copy)') self.assertEqual(copied_documents[1].name, f'{self.document_gif.name} (copy)') # Check that copied documents res_model and res_id are pointing to themselves document_txt_copy = self.document_txt.with_user(self.internal_user).copy() copied_documents = (self.document_txt | document_txt_copy).with_user(self.internal_user).copy() self.document_txt.unlink() for copied_document in copied_documents: self.assertFalse(copied_document.res_id) self.assertFalse(copied_document.res_model) self.assertEqual(copied_document.attachment_id.res_id, copied_document.id) self.assertEqual(copied_document.attachment_id.res_model, "documents.document") self.assertTrue(copied_document.exists()) self.document_gif.write({ "res_model": "res.partner", "res_id": self.env.user.partner_id.id, }) with mute_logger('odoo.addons.documents.models.documents_document'): copied_document = self.document_gif.copy() self.assertFalse(copied_document.res_id, copied_document.id) self.assertFalse(copied_document.res_model) self.assertEqual(copied_document.attachment_id.res_id, copied_document.id) self.assertEqual(copied_document.attachment_id.res_model, "documents.document") def test_copy_document_company_to_company(self): """ Manager can copy both the company-root folder and doc to company-root. """ copied_folder = self.company_root_folder.with_user(self.document_manager).copy(default={'user_folder_id': 'COMPANY'}) self.assertTrue(copied_folder) self.assertFalse(copied_folder.owner_id, "Copied folder should not have owner_id set") copied_doc = self.company_root_document.with_user(self.document_manager).copy(default={'user_folder_id': 'COMPANY'}) self.assertTrue(copied_doc) self.assertFalse(copied_doc.owner_id, "Copied document should not have owner_id set") def test_copy_document_company_to_my_drive(self): """ Manager can copy from company-root folder/doc to My drive. Internal user can copy from company-root folder/doc to MY sets owner to that user. """ copied_folder = self.company_root_folder.with_user(self.document_manager).copy(default={'user_folder_id': 'MY'}) self.assertTrue(copied_folder.owner_id, "Copied folder should have owner_id set") self.assertEqual(copied_folder.owner_id.id, self.document_manager.id, "Copied folder owner should be the manager") copied_doc = self.company_root_document.with_user(self.document_manager).copy(default={'user_folder_id': 'MY'}) self.assertTrue(copied_doc.owner_id, "Copied document should have owner_id set") self.assertEqual(copied_doc.owner_id.id, self.document_manager.id, "Copied document owner should be the manager") copied_folder = self.company_root_folder.with_user(self.doc_user).copy(default={'user_folder_id': 'MY'}) self.assertTrue(copied_folder.owner_id, "Copied folder should have owner_id set") self.assertEqual(copied_folder.owner_id.id, self.doc_user.id, "Copied folder owner should be the internal user") copied_doc = self.company_root_document.with_user(self.doc_user).copy(default={'user_folder_id': 'MY'}) self.assertTrue(copied_doc.owner_id, "Copied document should have owner_id set") self.assertEqual(copied_doc.owner_id.id, self.doc_user.id, "Copied document owner should be the internal user") def test_copy_document_to_itself(self): """ Copying a folder into itself or one of its own descendants raises an UserError. """ folder = self.company_root_folder sub_folder = self.company_sub_folder with self.assertRaisesRegex(UserError, "cannot copy a folder into itself"): folder.with_user(self.document_manager).copy(default={'user_folder_id': str(folder.id)}) with self.assertRaisesRegex(UserError, "cannot copy a folder into itself"): sub_folder.with_user(self.document_manager).copy(default={'user_folder_id': str(sub_folder.id)}) with self.assertRaisesRegex(UserError, "cannot copy a folder into itself"): folder.with_user(self.document_manager).copy(default={'user_folder_id': str(sub_folder.id)}) def test_copy_shortcut(self): """Check that copying shortcuts works as intended.""" manager_shortcut = self.document_txt.with_user(self.document_manager).action_create_shortcut() self.assertTrue(manager_shortcut.folder_id) self.assertEqual(manager_shortcut.copy({'user_folder_id': 'MY'}).user_folder_id, 'MY') self.assertEqual(manager_shortcut.copy({'user_folder_id': 'COMPANY'}).user_folder_id, 'COMPANY') self.assertEqual(manager_shortcut.copy({'user_folder_id': str(manager_shortcut.folder_id.id)}).user_folder_id, str(manager_shortcut.folder_id.id)) def test_embedding_actions(self): """Check that embedded actions name is translated.""" self.env['res.lang']._activate_lang('fr_FR') doc = self.env['documents.document'].create({'name': 'A request', 'folder_id': self.folder_a.id}) self.assertFalse(doc.available_embedded_actions_ids) self.server_action.with_context(lang='fr_FR').name = "Blablabla" self.env['documents.document'].action_folder_embed_action(self.folder_a.id, self.server_action.id) doc.invalidate_recordset(['available_embedded_actions_ids']) embedded_action = doc.available_embedded_actions_ids self.assertEqual(embedded_action.display_name, self.server_action.display_name) self.assertEqual(embedded_action.with_context(lang='fr_FR').display_name, "Blablabla") def test_embedding_actions_permission(self): """Test that embedding actions enforces permissions but allows sudo bypass.""" user_no_rights = new_test_user(self.env, login='user_no_doc_rights', groups='base.group_user') folder = self.env['documents.document'].create({ 'name': 'Test Folder', 'type': 'folder' }) action = self.env['ir.actions.server'].create({ 'name': 'Test Server Action', 'model_id': self.env['ir.model']._get('documents.document').id, 'state': 'code', }) self.env['documents.document'].with_user(user_no_rights).sudo().action_folder_embed_action(folder.id, action.id) embedded = self.env['ir.embedded.actions'].search([ ('action_id', '=', action.id), ('parent_res_id', '=', folder.id), ('parent_res_model', '=', 'documents.document'), ]) self.assertTrue(embedded, "The action should have been embedded in sudo mode") with self.assertRaises(AccessError): self.env['documents.document'].with_user(user_no_rights).action_folder_embed_action(folder.id, action.id) self.assertTrue(embedded, "The action should not have been unembedded without rights or sudo") def test_embedding_actions_obsolescence_gc(self): """Actions made children should not be used as embedded actions anymore and are garbage collected.""" doc = self.env['documents.document'].create({ 'name': 'A request', 'folder_id': self.folder_a.id, }) parent_base = self.server_action parent_base.group_ids = [Command.create({'name': 'Test Gr', 'user_ids': [Command.link(self.env.user.id)]})] child_base = parent_base.copy() self.assertTrue(child_base.group_ids) self.env['documents.document'].action_folder_embed_action(self.folder_a.id, parent_base.id) self.env['documents.document'].action_folder_embed_action(self.folder_a.id, child_base.id) self.env['ir.embedded.actions'].with_user(self.env.ref('base.user_root'))._gc_documents_obsolete() embedded_action = self.env["ir.embedded.actions"].search([ ("action_id", "in", (child_base | parent_base).ids) ]).grouped(lambda a: a.action_id.id) def _visible_action_ids(): actions = self.env['documents.document'].get_documents_actions(folder_id=self.folder_a.id) return {a['id'] for a in actions} actions_init = _visible_action_ids() self.assertIn(parent_base.id, actions_init) self.assertIn(child_base.id, actions_init) doc.invalidate_recordset(['available_embedded_actions_ids']) parent_base.write({ 'child_ids': [Command.link(child_base.id)], }) actions_before = _visible_action_ids() self.assertIn(parent_base.id, actions_before) # Child is no longer visible nor embeddable self.assertNotIn(child_base.id, actions_before) # Even if the record still exists, it cannot be used programmatically either self.assertNotIn(child_base.id, doc.available_embedded_actions_ids.action_id.ids) doc_in_context = self.env['documents.document'].with_context( active_model='documents.document', active_id=doc.id) child_embedded = embedded_action[child_base.id].exists() self.assertTrue(child_embedded) with self.assertRaises(UserError): doc_in_context.action_execute_embedded_action(embedded_action[child_base.id].id) # Child action gets gc'ed self.env['ir.embedded.actions'].with_user(self.ref('base.user_root'))._gc_documents_obsolete() self.assertFalse(child_embedded.exists()) # Not the parent doc_in_context.action_execute_embedded_action(embedded_action[parent_base.id].id) def test_document_thumbnail_status(self): for mimetype in ['application/pdf', 'application/pdf;base64']: with self.subTest(mimetype=mimetype): pdf_document = self.env['documents.document'].create({ 'name': 'Test PDF doc', 'mimetype': mimetype, 'datas': "JVBERi0gRmFrZSBQREYgY29udGVudA==", 'folder_id': self.folder_b.id, }) self.assertEqual(pdf_document.thumbnail, False) self.assertEqual(pdf_document.thumbnail_status, 'client_generated') word_document = self.env['documents.document'].create({ 'name': 'Test DOC doc', 'mimetype': 'application/msword', 'folder_id': self.folder_b.id, }) self.assertEqual(word_document.thumbnail, False) self.assertEqual(word_document.thumbnail_status, False) webp_document = self.env['documents.document'].create({ 'name': 'Test WEBP doc', 'mimetype': 'image/webp', 'datas': WEBP, 'folder_id': self.folder_b.id, }) self.assertEqual(webp_document.thumbnail, False) self.assertEqual(webp_document.thumbnail_status, 'client_generated') image_documents = self.env['documents.document'].create([{ 'name': 'Test image doc', 'mimetype': mimetype, 'datas': GIF, 'folder_id': self.folder_b.id, } for mimetype in [ 'image/bmp', 'image/gif', 'image/jpeg', 'image/png', 'image/svg+xml', 'image/tiff', 'image/x-icon' ]]) for image_document in image_documents: with self.subTest(mimetype=image_document.mimetype): self.assertEqual(image_document.thumbnail, GIF) self.assertEqual(image_document.thumbnail_status, 'present') text_documents = self.env['documents.document'].create([{ 'name': 'Test Special Case Text doc', 'mimetype': mimetype, 'datas': TEXT, 'folder_id': self.folder_b.id, } for mimetype in [ 'text/html', 'text/csv', 'text/plain', 'text/javascript', 'text/css', 'text/markdown', 'text/xml', 'application/json', 'application/xml', 'text/calendar', ]]) for text_document in text_documents: with self.subTest(mimetype=text_document.mimetype): self.assertEqual(text_document.thumbnail, False) self.assertEqual(text_document.thumbnail_status, False) def test_document_max_upload_limit(self): Doc = self.env['documents.document'] ICP = self.env['ir.config_parameter'] key_doc = 'document.max_fileupload_size' key_web = 'web.max_file_upload_size' ICP.set_param(key_doc, 20) ICP.set_param(key_web, 10) self.assertEqual(Doc.get_document_max_upload_limit(), 20) ICP.set_param(key_doc, 0) self.assertEqual(Doc.get_document_max_upload_limit(), None) ICP.search([('key', '=', key_doc)]).unlink() self.assertEqual(Doc.get_document_max_upload_limit(), 10) ICP.search([('key', '=', key_web)]).unlink() self.assertEqual( Doc.get_document_max_upload_limit(), http.DEFAULT_MAX_CONTENT_LENGTH ) def test_document_order_by_is_folder(self): # check that the order is "folder first", and then most recent first doc_1 = self.env['documents.document'].create([{'name': 'D1'}]) doc_2 = self.env['documents.document'].create([{'name': 'D2', 'type': 'folder'}]) doc_3 = self.env['documents.document'].create([{'name': 'D3', 'type': 'url'}]) doc_4 = self.env['documents.document'].create([{'name': 'D4'}]) docs = doc_1 | doc_2 | doc_3 | doc_4 result = self.env['documents.document'].search([('id', 'in', docs.ids)], order='is_folder, create_date DESC, id DESC') self.assertEqual(result[0], doc_2) self.assertEqual(result[1], doc_4) self.assertEqual(result[2], doc_3) self.assertEqual(result[3], doc_1) def test_document_order_by_last_access_date(self): documents = self.env['documents.document'].create([{'name': 'D1'}, {'name': 'D2'}]) self.env['documents.access'].create([{ 'document_id': documents[0].id, 'last_access_date': datetime.now() + timedelta(days=1), 'partner_id': self.env.user.partner_id.id, }, { 'document_id': documents[1].id, 'last_access_date': datetime.now() + timedelta(days=2), 'partner_id': self.env.user.partner_id.id, }]) result = self.env['documents.document'].search([('id', 'in', documents.ids)], order='last_access_date_group DESC') self.assertEqual(result[0], documents[1]) self.assertEqual(result[1], documents[0]) result = self.env['documents.document'].search([('id', 'in', documents.ids)], order='last_access_date_group ASC') self.assertEqual(result[0], documents[0]) self.assertEqual(result[1], documents[1]) def test_document_group_by_last_access_date(self): Doc = self.env['documents.document'] documents = Doc.create([{'name': f'D{i}'} for i in range(6)]) self.env['documents.access'].create([{ 'document_id': documents[0].id, 'last_access_date': datetime.now() - timedelta(hours=1), 'partner_id': self.env.user.partner_id.id, }, { 'document_id': documents[1].id, 'last_access_date': datetime.now() - timedelta(days=2), 'partner_id': self.env.user.partner_id.id, }, { 'document_id': documents[2].id, 'last_access_date': datetime.now() - timedelta(days=8), 'partner_id': self.env.user.partner_id.id, }, { 'document_id': documents[3].id, 'last_access_date': datetime.now() - timedelta(days=40), 'partner_id': self.env.user.partner_id.id, }, { 'document_id': documents[4].id, 'last_access_date': datetime.now() - timedelta(minutes=1), 'partner_id': self.env.user.partner_id.id, }, { 'document_id': documents[5].id, 'last_access_date': datetime.now() - timedelta(days=1, hours=5), 'partner_id': self.env.user.partner_id.id, }]) result = Doc.formatted_read_group( [('id', 'in', documents.ids)], groupby=['last_access_date_group'], aggregates=['__count'], order='last_access_date_group DESC') self.assertEqual(len(result), 4) self.assertEqual(result[0]['last_access_date_group'], '3_day') self.assertEqual(result[0]['__count'], 2) result_day = Doc.search(result[0]['__extra_domain']) self.assertEqual(result_day[0], documents[4]) self.assertEqual(result_day[1], documents[0]) self.assertEqual(result_day.mapped('last_access_date_group'), ['3_day'] * 2) self.assertEqual(result[1]['last_access_date_group'], '2_week') self.assertEqual(result[1]['__count'], 2) result_week = Doc.search(result[1]['__extra_domain']) self.assertEqual(result_week[0], documents[5]) self.assertEqual(result_week[1], documents[1]) self.assertEqual(result_week.mapped('last_access_date_group'), ['2_week'] * 2) self.assertEqual(result[2]['last_access_date_group'], '1_month') self.assertEqual(result[2]['__count'], 1) self.assertEqual(Doc.search(result[2]['__extra_domain']), documents[2]) self.assertEqual(documents[2].last_access_date_group, '1_month') self.assertEqual(result[3]['last_access_date_group'], '0_older') self.assertEqual(result[3]['__count'], 1) self.assertEqual(Doc.search(result[3]['__extra_domain']), documents[3]) self.assertEqual(documents[3].last_access_date_group, '0_older') def test_link_constrains(self): folder = self.env['documents.document'].create({'name': 'folder', 'type': 'folder'}) for url in ("wrong URL format", "https:/ example.com", "test https://example.com"): with self.assertRaises(ValidationError): self.env['documents.document'].create({ 'name': 'Test Document', 'folder_id': folder.id, 'url': url, }) def test_document_shortcut_to_my_drive(self): shortcut_1 = self.document_txt.action_create_shortcut(location_user_folder_id=str(self.folder_b.id)) shortcut_2 = shortcut_1.with_user(self.internal_user).action_create_shortcut(location_user_folder_id='MY') self.assertEqual(shortcut_2.folder_id.id, False) self.assertEqual(shortcut_2.with_user(self.internal_user).user_folder_id, 'MY') def test_document_upload_from_chatter(self): folder = self.env['documents.document'].create([ {'type': 'folder', 'name': 'folder', 'access_internal': 'view'} ]) attachment = self.env['ir.attachment'].create({ 'datas': GIF, 'name': 'TestAttachment.gif', 'res_model': 'documents.document', 'res_id': folder.id, }) self.assertNotEqual(attachment.name, folder.name, 'the folder name should not change') def test_document_toggle_lock(self): """ Test unlocking of the documents when the user_permission is set to edit. """ self.document_txt.write({'owner_id': self.document_manager.id}) self.document_txt.access_ids.filtered('role').unlink() self.document_txt.with_user(self.document_manager).toggle_lock() with self.assertRaises(AccessError): self.document_txt.with_user(self.doc_user).toggle_lock() self.assertEqual(self.document_txt.lock_uid.id, self.document_manager.id, 'viewer should not have unlocked') self.document_txt.access_internal = 'edit' self.document_txt.with_user(self.doc_user).toggle_lock() self.assertFalse(self.document_txt.lock_uid, 'editor should have unlocked') def test_res_name_recompute_with_deleted_record(self): partner = self.env['res.partner'].create({'name': 'Test Partner'}) doc = self.env['documents.document'].create({ 'name': 'Test', 'res_id': partner.id, 'res_model': 'res.partner', }) self.assertEqual(doc.res_name, "Test Partner") partner.unlink() self.assertFalse(doc.res_name)