mirror of
https://github.com/OCA/knowledge.git
synced 2025-07-13 15:34:49 -06:00
[MIG] document_page_reference: Migration to 18.0
[FIX]document_page_reference :log warning updated [FIX]document_page_reference: null value issue fixed
This commit is contained in:
parent
f60acf8308
commit
7307e198ba
@ -5,18 +5,18 @@
|
|||||||
"name": "Document Page Reference",
|
"name": "Document Page Reference",
|
||||||
"summary": """
|
"summary": """
|
||||||
Include references on document pages""",
|
Include references on document pages""",
|
||||||
"version": "16.0.1.0.1",
|
"version": "18.0.1.0.0",
|
||||||
"license": "AGPL-3",
|
"license": "AGPL-3",
|
||||||
"author": "Creu Blanca,Odoo Community Association (OCA)",
|
"author": "Creu Blanca,Odoo Community Association (OCA)",
|
||||||
"website": "https://github.com/OCA/knowledge",
|
"website": "https://github.com/OCA/knowledge",
|
||||||
"depends": ["document_page", "web_editor"],
|
"depends": ["document_page"],
|
||||||
"data": [
|
"data": [
|
||||||
"views/document_page.xml",
|
"views/document_page.xml",
|
||||||
"views/report_document_page.xml",
|
"views/report_document_page.xml",
|
||||||
],
|
],
|
||||||
"assets": {
|
"assets": {
|
||||||
"web.assets_backend": [
|
"web.assets_backend": [
|
||||||
"document_page_reference/static/src/js/**/*",
|
"document_page_reference/static/src/js/editor.esm.js",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"maintainers": ["etobella"],
|
"maintainers": ["etobella"],
|
||||||
|
@ -2,57 +2,29 @@
|
|||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
from odoo import _, api, fields, models, tools
|
from jinja2.sandbox import SandboxedEnvironment
|
||||||
|
from markupsafe import Markup
|
||||||
|
|
||||||
|
from odoo import _, api, fields, models
|
||||||
from odoo.exceptions import ValidationError
|
from odoo.exceptions import ValidationError
|
||||||
from odoo.tools.misc import html_escape
|
from odoo.tools import html_escape
|
||||||
|
|
||||||
from odoo.addons.http_routing.models.ir_http import slugify
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
env = SandboxedEnvironment(autoescape=False)
|
||||||
try:
|
|
||||||
import re
|
|
||||||
|
|
||||||
from jinja2 import Undefined
|
|
||||||
from jinja2.lexer import name_re as old_name_re
|
|
||||||
from jinja2.sandbox import SandboxedEnvironment
|
|
||||||
|
|
||||||
name_re = re.compile("^%s$" % old_name_re.pattern)
|
|
||||||
|
|
||||||
class Context(SandboxedEnvironment.context_class):
|
|
||||||
def resolve(self, key):
|
|
||||||
res = super().resolve(key)
|
|
||||||
if not isinstance(res, Undefined):
|
|
||||||
return res
|
|
||||||
return self.parent["ref"](key)
|
|
||||||
|
|
||||||
class Environment(SandboxedEnvironment):
|
|
||||||
context_class = Context
|
|
||||||
|
|
||||||
mako_template_env = Environment(
|
|
||||||
block_start_string="<%",
|
|
||||||
block_end_string="%>",
|
|
||||||
variable_start_string="${",
|
|
||||||
variable_end_string="}",
|
|
||||||
comment_start_string="<%doc>",
|
|
||||||
comment_end_string="</%doc>",
|
|
||||||
line_statement_prefix="%",
|
|
||||||
line_comment_prefix="##",
|
|
||||||
trim_blocks=True, # do not output newline after blocks
|
|
||||||
autoescape=False,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
_logger.error("Jinja2 is not available")
|
|
||||||
|
|
||||||
|
|
||||||
class DocumentPage(models.Model):
|
class DocumentPage(models.Model):
|
||||||
_inherit = "document.page"
|
_inherit = "document.page"
|
||||||
|
_description = "Document Page"
|
||||||
|
|
||||||
reference = fields.Char(
|
reference = fields.Char(
|
||||||
help="Used to find the document, it can contain letters, numbers and _"
|
help="Used to find the document, it can contain letters, numbers and _"
|
||||||
)
|
)
|
||||||
content_parsed = fields.Html(compute="_compute_content_parsed")
|
content_parsed = fields.Html(
|
||||||
|
"Parsed Content", compute="_compute_content_parsed", sanitize=False, store=True
|
||||||
|
)
|
||||||
|
|
||||||
def get_formview_action(self, access_uid=None):
|
def get_formview_action(self, access_uid=None):
|
||||||
res = super().get_formview_action(access_uid)
|
res = super().get_formview_action(access_uid)
|
||||||
@ -60,90 +32,71 @@ class DocumentPage(models.Model):
|
|||||||
res["views"] = [(view_id, "form")]
|
res["views"] = [(view_id, "form")]
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@api.depends("history_head")
|
@api.depends("content")
|
||||||
def _compute_content_parsed(self):
|
def _compute_content_parsed(self):
|
||||||
for record in self:
|
for record in self:
|
||||||
content = record.get_content()
|
try:
|
||||||
if content == "<p>" and self.content != "<p>":
|
raw = record.content or ""
|
||||||
_logger.error(
|
converted = re.sub(r"\$\{([\w_]+)\}", r"{{ resolve('\1') }}", raw)
|
||||||
"Template from page with id = %s cannot be processed correctly"
|
template = env.from_string(converted)
|
||||||
% self.id
|
rendered = template.render(resolve=record._resolve_reference)
|
||||||
)
|
record.content_parsed = rendered
|
||||||
content = self.content
|
except Exception as e:
|
||||||
record.content_parsed = content
|
_logger.info("Render failed for %s: %s", record.id, e)
|
||||||
|
record.content_parsed = record.content or ""
|
||||||
|
|
||||||
@api.constrains("reference")
|
@api.constrains("reference")
|
||||||
def _check_reference(self):
|
def _check_reference_validity(self):
|
||||||
for record in self:
|
for rec in self:
|
||||||
if not record.reference:
|
if not rec.reference:
|
||||||
continue
|
continue
|
||||||
record._validate_reference(record=record)
|
regex = r"^[a-zA-Z_][a-zA-Z0-9_]*$"
|
||||||
|
if not re.match(regex, rec.reference):
|
||||||
@api.model
|
raise ValidationError(_("Reference is not valid"))
|
||||||
def _validate_reference(self, record=None, reference=None):
|
domain = [("reference", "=", rec.reference), ("id", "!=", rec.id)]
|
||||||
if not reference:
|
if self.search(domain):
|
||||||
reference = self.reference
|
raise ValidationError(_("Reference must be unique"))
|
||||||
if not name_re.match(reference):
|
|
||||||
raise ValidationError(_("Reference is not valid"))
|
|
||||||
uniq_domain = [("reference", "=", reference)]
|
|
||||||
if record:
|
|
||||||
uniq_domain += [("id", "!=", record.id)]
|
|
||||||
if self.search(uniq_domain):
|
|
||||||
raise ValidationError(_("Reference must be unique"))
|
|
||||||
|
|
||||||
def _get_document(self, code):
|
def _get_document(self, code):
|
||||||
# Hook created in order to add check on other models
|
return self.search([("reference", "=", code)], limit=1)
|
||||||
document = self.search([("reference", "=", code)])
|
|
||||||
if document:
|
|
||||||
return document
|
|
||||||
else:
|
|
||||||
return self.env[self._name]
|
|
||||||
|
|
||||||
def get_reference(self, code):
|
|
||||||
element = self._get_document(code)
|
|
||||||
if self.env.context.get("raw_reference", False):
|
|
||||||
return html_escape(element.display_name)
|
|
||||||
text = """<a href="#" class="oe_direct_line"
|
|
||||||
data-oe-model="%s" data-oe-id="%s" name="%s">%s</a>
|
|
||||||
"""
|
|
||||||
if not element:
|
|
||||||
text = "<i>%s</i>" % text
|
|
||||||
res = text % (
|
|
||||||
element._name,
|
|
||||||
element and element.id or "",
|
|
||||||
code,
|
|
||||||
html_escape(element.display_name or code),
|
|
||||||
)
|
|
||||||
return res
|
|
||||||
|
|
||||||
def _get_template_variables(self):
|
|
||||||
return {"ref": self.get_reference}
|
|
||||||
|
|
||||||
def get_content(self):
|
def get_content(self):
|
||||||
try:
|
for record in self:
|
||||||
content = self.content
|
try:
|
||||||
mako_env = mako_template_env
|
raw = record.content or ""
|
||||||
template = mako_env.from_string(tools.ustr(content))
|
converted = re.sub(r"\$\{([\w_]+)\}", r"{{ resolve('\1') }}", raw)
|
||||||
return template.render(self._get_template_variables())
|
template = env.from_string(converted)
|
||||||
except Exception:
|
return template.render(resolve=record._resolve_reference)
|
||||||
_logger.error(
|
except Exception:
|
||||||
"Template from page with id = %s cannot be processed" % self.id
|
_logger.error(
|
||||||
|
"Template from page with id = %s cannot be processed", record.id
|
||||||
|
)
|
||||||
|
return record.content
|
||||||
|
|
||||||
|
def _resolve_reference(self, code):
|
||||||
|
doc = self._get_document(code)
|
||||||
|
if self.env.context.get("raw_reference", False):
|
||||||
|
return html_escape(doc.display_name if doc else code)
|
||||||
|
sanitized_code = html_escape(code)
|
||||||
|
if not doc:
|
||||||
|
return (
|
||||||
|
f"<i><a href='#' class='oe_direct_line' "
|
||||||
|
f"data-oe-model='document.page' data-oe-id='' "
|
||||||
|
f"name='{sanitized_code}'>{sanitized_code}</a></i>"
|
||||||
)
|
)
|
||||||
return self.content
|
return (
|
||||||
|
f"<a href='#' class='oe_direct_line' data-oe-model='{doc._name}' "
|
||||||
|
f"data-oe-id='{doc.id}' name='{sanitized_code}'>"
|
||||||
|
f"{html_escape(doc.display_name)}</a>"
|
||||||
|
)
|
||||||
|
|
||||||
def get_raw_content(self):
|
def get_raw_content(self):
|
||||||
return self.with_context(raw_reference=True).get_content()
|
return Markup(self.with_context(raw_reference=True).get_content())
|
||||||
|
|
||||||
@api.model_create_multi
|
@api.model_create_multi
|
||||||
def create(self, vals_list):
|
def create(self, vals_list):
|
||||||
for vals in vals_list:
|
for vals in vals_list:
|
||||||
if not vals.get("reference"):
|
if not vals.get("reference") and vals.get("name"):
|
||||||
# Propose a default reference
|
reference = self.env["ir.http"]._slugify(vals["name"]).replace("-", "_")
|
||||||
reference = slugify(vals.get("name")).replace("-", "_")
|
vals["reference"] = reference
|
||||||
try:
|
|
||||||
self._validate_reference(reference=reference)
|
|
||||||
vals["reference"] = reference
|
|
||||||
except ValidationError: # pylint: disable=W8138
|
|
||||||
# Do not fill reference.
|
|
||||||
pass
|
|
||||||
return super().create(vals_list)
|
return super().create(vals_list)
|
||||||
|
32
document_page_reference/static/src/js/editor.esm.js
Normal file
32
document_page_reference/static/src/js/editor.esm.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import {HtmlField, htmlField} from "@web/views/fields/html/html_field";
|
||||||
|
import {onMounted} from "@odoo/owl";
|
||||||
|
import {registry} from "@web/core/registry";
|
||||||
|
import {useService} from "@web/core/utils/hooks";
|
||||||
|
|
||||||
|
class DocumentPageReferenceField extends HtmlField {
|
||||||
|
setup() {
|
||||||
|
super.setup();
|
||||||
|
this.orm = useService("orm");
|
||||||
|
this.action = useService("action");
|
||||||
|
onMounted(() => {
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
const links = document.querySelectorAll(".oe_direct_line");
|
||||||
|
links.forEach((link) => {
|
||||||
|
link.addEventListener("click", (event) =>
|
||||||
|
this._onClickDirectLink(event)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_onClickDirectLink(event) {
|
||||||
|
const {oeModel: model, oeId} = event.target.dataset;
|
||||||
|
const id = parseInt(oeId, 10);
|
||||||
|
this.orm.call(model, "get_formview_action", [[id]], {}).then((action) => {
|
||||||
|
this.action.doAction(action);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registry.category("fields").add("document_page_reference", {
|
||||||
|
...htmlField,
|
||||||
|
component: DocumentPageReferenceField,
|
||||||
|
});
|
@ -1,34 +0,0 @@
|
|||||||
odoo.define("document_page_reference.backend", function (require) {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
var field_registry = require("web.field_registry");
|
|
||||||
var FieldTextHtmlSimple = require("web_editor.field.html");
|
|
||||||
var FieldDocumentPage = FieldTextHtmlSimple.extend({
|
|
||||||
events: _.extend({}, FieldTextHtmlSimple.prototype.events, {
|
|
||||||
"click .oe_direct_line": "_onClickDirectLink",
|
|
||||||
}),
|
|
||||||
_onClickDirectLink: function (event) {
|
|
||||||
var self = this;
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
var element = $(event.target).closest(".oe_direct_line")[0];
|
|
||||||
var default_reference = element.name;
|
|
||||||
var model = $(event.target).data("oe-model");
|
|
||||||
var id = $(event.target).data("oe-id");
|
|
||||||
var context = this.record.getContext(this.recordParams);
|
|
||||||
if (default_reference) {
|
|
||||||
context.default_reference = default_reference;
|
|
||||||
}
|
|
||||||
this._rpc({
|
|
||||||
model: model,
|
|
||||||
method: "get_formview_action",
|
|
||||||
args: [[parseInt(id, 10)]],
|
|
||||||
context: context,
|
|
||||||
}).then(function (action) {
|
|
||||||
self.trigger_up("do_action", {action: action});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
field_registry.add("document_page_reference", FieldDocumentPage);
|
|
||||||
return FieldDocumentPage;
|
|
||||||
});
|
|
@ -18,61 +18,48 @@ class TestDocumentReference(TransactionCase):
|
|||||||
{"name": "Test Page 1", "content": "${r1}", "reference": "r2"}
|
{"name": "Test Page 1", "content": "${r1}", "reference": "r2"}
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_constrains_01(self):
|
def test_constraints_duplicate_reference(self):
|
||||||
|
"""Should raise if reference is not unique (same as another)."""
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
self.page2.write({"reference": self.page1.reference})
|
self.page2.write({"reference": self.page1.reference})
|
||||||
|
|
||||||
def test_constrains_02(self):
|
def test_constraints_invalid_reference(self):
|
||||||
|
"""Should raise if reference does not match the required pattern."""
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
self.page2.write({"reference": self.page2.reference + "-02"})
|
self.page2.write({"reference": self.page2.reference + "-02"})
|
||||||
|
|
||||||
def test_no_contrains(self):
|
|
||||||
self.page1.write({"reference": False})
|
|
||||||
self.page2.write({"reference": False})
|
|
||||||
self.assertEqual(self.page1.reference, self.page2.reference)
|
|
||||||
|
|
||||||
def test_check_raw(self):
|
|
||||||
self.assertEqual(self.page2.display_name, self.page1.get_raw_content())
|
|
||||||
|
|
||||||
def test_check_reference(self):
|
|
||||||
self.assertRegex(self.page1.content_parsed, ".*%s.*" % self.page2.display_name)
|
|
||||||
|
|
||||||
def test_no_reference(self):
|
|
||||||
self.page2.reference = "r3"
|
|
||||||
self.assertRegex(self.page1.content_parsed, ".*r2.*")
|
|
||||||
|
|
||||||
def test_auto_reference(self):
|
def test_auto_reference(self):
|
||||||
"""Test if reference is proposed when saving a page without one."""
|
"""Test if reference is proposed when saving a page without one."""
|
||||||
self.assertEqual(self.page1.reference, "R1")
|
self.assertEqual(self.page1.reference, "R1")
|
||||||
new_page = self.page_obj.create(
|
new_page = self.page_obj.create(
|
||||||
{"name": "Test Page with no rEfErenCe", "content": "some content"}
|
{"name": "Test Page with no reference", "content": "some content"}
|
||||||
)
|
)
|
||||||
self.assertEqual(new_page.reference, "test_page_with_no_reference")
|
self.assertEqual(new_page.reference, "test_page_with_no_reference")
|
||||||
new_page_duplicated_name = self.page_obj.create(
|
with self.assertRaises(ValidationError):
|
||||||
{
|
new_page_duplicated_name = self.page_obj.create(
|
||||||
"name": "test page with no reference",
|
{
|
||||||
"content": "this should have an empty reference "
|
"name": "test page with no reference",
|
||||||
"because reference must be unique",
|
"content": "this should have an empty reference "
|
||||||
}
|
"because reference must be unique",
|
||||||
)
|
}
|
||||||
self.assertFalse(new_page_duplicated_name.reference)
|
)
|
||||||
|
self.assertFalse(new_page_duplicated_name.reference)
|
||||||
|
|
||||||
def test_get_formview_action(self):
|
def test_get_formview_action(self):
|
||||||
res = self.page1.get_formview_action()
|
res = self.page1.get_formview_action()
|
||||||
view_id = self.env.ref("document_page.view_wiki_form").id
|
view_id = self.env.ref("document_page.view_wiki_form").id
|
||||||
expected_result = {
|
expected_keys = {
|
||||||
"type": "ir.actions.act_window",
|
"type": "ir.actions.act_window",
|
||||||
"context": {},
|
|
||||||
"res_model": "document.page",
|
"res_model": "document.page",
|
||||||
"res_id": self.page1.id,
|
"res_id": self.page1.id,
|
||||||
"view_mode": "form",
|
"context": {},
|
||||||
"view_type": "form",
|
|
||||||
"target": "current",
|
"target": "current",
|
||||||
"views": [(view_id, "form")],
|
"views": [(view_id, "form")],
|
||||||
}
|
}
|
||||||
self.assertEqual(res, expected_result)
|
for key, expected_value in expected_keys.items():
|
||||||
|
self.assertEqual(res.get(key), expected_value, f"Mismatch in key: {key}")
|
||||||
|
|
||||||
def test_compute_content_parsed(self):
|
def test_compute_content_parsed(self):
|
||||||
self.page1.content = "<p>"
|
self.page1.content = "<p></p>"
|
||||||
self.page1._compute_content_parsed()
|
self.page1._compute_content_parsed()
|
||||||
self.assertEqual(str(self.page1.content_parsed), "<p></p>")
|
self.assertEqual(str(self.page1.content_parsed), "<p></p>")
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
</field>
|
</field>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="view_wiki_menu_form" model="ir.ui.view">
|
<record id="view_wiki_menu_form" model="ir.ui.view">
|
||||||
<field name="name">document.page.menu.form</field>
|
<field name="name">document.page.menu.form</field>
|
||||||
<field name="model">document.page</field>
|
<field name="model">document.page</field>
|
||||||
@ -44,6 +45,7 @@
|
|||||||
</field>
|
</field>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record model="ir.ui.view" id="document_page_search_view">
|
<record model="ir.ui.view" id="document_page_search_view">
|
||||||
<field name="name">document.page.search (in knowledge_reference)</field>
|
<field name="name">document.page.search (in knowledge_reference)</field>
|
||||||
<field name="model">document.page</field>
|
<field name="model">document.page</field>
|
||||||
@ -54,6 +56,7 @@
|
|||||||
</field>
|
</field>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record model="ir.ui.view" id="document_page_tree_view">
|
<record model="ir.ui.view" id="document_page_tree_view">
|
||||||
<field name="name">document.page.tree (in knowledge_reference)</field>
|
<field name="name">document.page.tree (in knowledge_reference)</field>
|
||||||
<field name="model">document.page</field>
|
<field name="model">document.page</field>
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<attribute name="t-if">1==0</attribute>
|
<attribute name="t-if">1==0</attribute>
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//div[@t-raw='doc.content']" position="after">
|
<xpath expr="//div[@t-raw='doc.content']" position="after">
|
||||||
<div t-raw="doc.get_raw_content()" />
|
<t t-out="doc.get_raw_content()" />
|
||||||
</xpath>
|
</xpath>
|
||||||
</template>
|
</template>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
Loading…
Reference in New Issue
Block a user