944 lines
46 KiB
Python
944 lines
46 KiB
Python
# -*- 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)
|