[IMP][9.0] Change Requests and workflow improvements on documents (#155)

* [IMP] Refactor document_page_approval to always use states, and a few code improvements
* [IMP] Add QWeb report to print document pages
* Categories don't save content
* FIX Last Contributor (uid and date). Use related fields instead of computed where possible.
Fix search views, store some fields to make them searchable, added filters
* Add api.depends on computed diff
This commit is contained in:
Iván Todorovich 2018-04-12 19:27:32 -03:00 committed by Maxime Chambreuil
parent 17d757fd95
commit 96256b9872
34 changed files with 1136 additions and 777 deletions

View File

@ -21,7 +21,7 @@
{
'name': 'Document Page',
'version': '9.0.1.0.1',
'version': '9.0.2.0.0',
'category': 'Knowledge Management',
'author': 'OpenERP SA, Odoo Community Association (OCA)',
'images': ['images/category_list.png', 'images/create_category.png',
@ -37,6 +37,10 @@
'wizard/document_page_create_menu.xml',
'wizard/document_page_show_diff.xml',
'views/document_page.xml',
'views/document_page_category.xml',
'views/document_page_history.xml',
'views/document_page_assets.xml',
'views/report_document_page.xml',
'security/document_page_security.xml',
'security/ir.model.access.csv',
],

View File

@ -1,41 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data noupdate="1">
<record id="wiki_wiki_main" model="document.page">
<field name="name">The Odoo wiki</field>
<field name="tags">help, quick start, wiki, formatting</field>
<field name="minor_edit">0</field>
<field name="index">1</field>
<field name="summary">Initial Page</field>
<field name="content">== The Odoo wiki ==
[[File:https://www.odoo.com/openerp_website/static/src/img/logo_transparent_198px.png Odoo]]
The Odoo wiki allows you to manage your enterprise's contents using wiki
restructured texts. This module provides a collaborative way to manage internal
FAQs, quality manuals, technical references, etc.
==Keypoints==
* Same formating style than MediaWiki,
* Any number of wiki group for different purposes,
* Detailed history on all pages,
* Integrated with the document management system.
==Why you should use the OpenERP integrated wiki than a separate wiki system ?==
* Allows links to any document of the system,
* Uses the access controls of OpenERP for uniq access rights management,
* Use it to describe projects, tasks, products,
* Integrated with customer portal to provide restricted external accesses,
* Linked to users processes for quality manuals.
==To get more information==
* [[Basic Wiki Editing]]
* [[Wiki Documentation]]
* [http://openerp.com The OpenERP website]
</field>
<field name="parent_id" ref="wiki_groups_wikiformatting0"/>
</record>
</data>
</odoo>

View File

@ -8,7 +8,7 @@
<record id="demo_category1" model="document.page">
<field name="name">OpenERP Features</field>
<field name="type">category</field>
<field name="content">
<field name="template">
Summary of the feature
Long explanation

View File

@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Ivan Todorovich <ivan.todorovich@gmail.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
def migrate(cr, version): # pragma: no cover
# Set all pre-existing categories template to its content
cr.execute("""
UPDATE document_page
SET template = content
WHERE type = 'category'
""")

View File

@ -18,10 +18,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import logging
from openerp import models, fields, api
_logger = logging.getLogger(__name__)
from openerp import models, fields, api
class DocumentPage(models.Model):
@ -54,18 +52,47 @@ class DocumentPage(models.Model):
)
content = fields.Text(
"Content"
"Content",
compute='_compute_content',
inverse='_inverse_content',
search='_search_content',
required=True,
)
# no-op computed field
summary = fields.Char(
help='Describe the changes made',
compute=lambda x: x,
inverse=lambda x: x,
)
template = fields.Html(
"Template",
help="Template that will be used as a content template "
"for all new page of this category.",
)
# deprecated - should be removed on 10.0
# left here because some modules might still need it
display_content = fields.Text(
string='Displayed Content',
compute='_get_display_content'
compute='_compute_display_content'
)
history_head = fields.Many2one(
'document.page.history',
'HEAD',
compute='_compute_history_head',
store=True,
auto_join=True,
)
history_ids = fields.One2many(
'document.page.history',
'page_id',
'History'
'History',
order='create_date DESC',
readonly=True,
)
menu_id = fields.Many2one(
@ -74,85 +101,88 @@ class DocumentPage(models.Model):
readonly=True
)
create_date = fields.Datetime(
"Created on",
content_date = fields.Datetime(
'Last Contribution Date',
related='history_head.create_date',
store=True,
index=True,
readonly=True
readonly=True,
)
create_uid = fields.Many2one(
content_uid = fields.Many2one(
'res.users',
'Author',
'Last Contributor',
related='history_head.create_uid',
store=True,
index=True,
readonly=True
readonly=True,
)
write_date = fields.Datetime(
"Modification Date",
index=True,
readonly=True)
write_uid = fields.Many2one(
'res.users',
"Last Contributor",
index=True,
readonly=True
)
def _get_page_index(self, page, link=True):
@api.multi
def _get_page_index(self, link=True):
"""Return the index of a document."""
self.ensure_one()
index = []
for subpage in page.child_ids:
index += ["<li>" + self._get_page_index(subpage) +
"</li>"]
for subpage in self.child_ids:
index += ["<li>" + subpage._get_page_index() + "</li>"]
r = ''
if link:
r = '<a href="#id=%s">%s</a>' % (page.id, page.name)
r = '<a href="#id=%s">%s</a>' % (self.id, self.name)
if index:
r += "<ul>" + "".join(index) + "</ul>"
return r
def _get_display_content(self):
"""Return the content of a document."""
for page in self:
if page.type == "category":
display_content = self._get_page_index(page, link=False)
else:
display_content = page.content
page.display_content = display_content
@api.onchange("parent_id")
def do_set_content(self):
"""We Set it the right content to the new parent."""
if self.parent_id and not self.content:
if self.parent_id.type == "category":
self.content = self.parent_id.content
def create_history(self, page_id, content):
"""Create the first history of a newly created document."""
history = self.env['document.page.history']
return history.create({
"content": content,
"page_id": page_id
})
@api.multi
@api.depends('content')
def _compute_display_content(self):
# @deprecated, simply use content
for rec in self:
rec.display_content = rec.content
@api.multi
def write(self, vals):
"""Write the content and set the history."""
result = super(DocumentPage, self).write(vals)
content = vals.get('content')
if content:
for page in self:
self.create_history(page.id, content)
return result
@api.depends('history_head', 'history_ids')
def _compute_content(self):
for rec in self:
if rec.type == 'category':
rec.content = rec._get_page_index(link=False)
else:
if rec.history_head:
rec.content = rec.history_head.content
else:
# html widget's default, so it doesn't trigger ghost save
rec.content = '<p><br></p>'
@api.model
@api.returns('self', lambda value: value.id)
def create(self, vals):
"""Create the first history of a document."""
page_id = super(DocumentPage, self).create(vals)
content = vals.get('content')
if content:
self.create_history(page_id.id, content)
return page_id
@api.multi
def _inverse_content(self):
for rec in self:
if rec.type == 'content':
rec._create_history({
'content': rec.content,
'summary': rec.summary,
})
@api.multi
def _search_content(self, operator, value):
return [('history_head.content', operator, value)]
@api.multi
@api.depends('history_ids')
def _compute_history_head(self):
for rec in self:
if rec.history_ids:
rec.history_head = rec.history_ids[0]
@api.multi
def _create_history(self, vals):
self.ensure_one()
history = self.env['document.page.history']
vals['page_id'] = self.id
return history.create(vals)
@api.onchange("parent_id")
def _onchange_parent_id(self):
"""We Set it the right content to the new parent."""
if not self.content or self.content == '<p><br></p>':
if self.parent_id and self.parent_id.type == "category":
self.content = self.parent_id.template

View File

@ -18,11 +18,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import logging
import difflib
from openerp import models, fields, _
_logger = logging.getLogger(__name__)
from openerp import models, fields, api, _
class DocumentPageHistory(models.Model):
@ -31,24 +28,40 @@ class DocumentPageHistory(models.Model):
_name = "document.page.history"
_description = "Document Page History"
_order = 'id DESC'
_rec_name = "create_date"
page_id = fields.Many2one('document.page', 'Page')
page_id = fields.Many2one('document.page', 'Page', ondelete='cascade')
summary = fields.Char('Summary', index=True)
content = fields.Text("Content")
create_date = fields.Datetime("Date")
create_uid = fields.Many2one('res.users', "Modified By")
diff = fields.Text(compute='_compute_diff')
@api.multi
@api.depends('content', 'page_id.history_ids')
def _compute_diff(self):
"""Shows a diff between this version and the previous version"""
history = self.env['document.page.history']
for rec in self:
prev = history.search([
('page_id', '=', rec.page_id.id),
('create_date', '<', rec.create_date)],
limit=1,
order='create_date DESC')
if prev:
rec.diff = self.getDiff(prev.id, rec.id)
else:
rec.diff = self.getDiff(False, rec.id)
@api.model
def getDiff(self, v1, v2):
"""Return the difference between two version of document version."""
text1 = self.browse(v1).content
text2 = self.browse(v2).content
line1 = line2 = ''
if text1:
line1 = text1.splitlines(1)
if text2:
line2 = text2.splitlines(1)
if (not line1 and not line2) or (line1 == line2):
text1 = v1 and self.browse(v1).content or ''
text2 = v2 and self.browse(v2).content or ''
# Include line breaks to make it more readable
# TODO: consider using a beautify library directly on the content
text1 = text1.replace('</p><p>', '</p>\r\n<p>')
text2 = text2.replace('</p><p>', '</p>\r\n<p>')
line1 = text1.splitlines(1)
line2 = text2.splitlines(1)
if line1 == line2:
return _('There are no changes in revisions.')
else:
diff = difflib.HtmlDiff()
@ -58,3 +71,11 @@ class DocumentPageHistory(models.Model):
"Revision-{}".format(v2),
context=True
)
@api.multi
def name_get(self):
result = []
for rec in self:
name = "%s #%i" % (rec.page_id.name, rec.id)
result.append((rec.id, name))
return result

View File

@ -1,8 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="0">
<!-- Defined in Knowledge. Change it's name -->
<record id="base.group_document_user" model="res.groups">
<field name="name">User (Read only)</field>
</record>
<record id="group_document_editor" model="res.groups">
<field name="name">Editor</field>
<field name="category_id" ref="knowledge.module_category_knowledge_management"/>
<field name="implied_ids" eval="[(4, ref('base.group_document_user'))]"/>
</record>
<record id="group_document_manager" model="res.groups">
<field name="name">Manager</field>
<field name="category_id" ref="knowledge.module_category_knowledge_management"/>
<field name="implied_ids" eval="[(4, ref('group_document_editor'))]"/>
<field name="users" eval="[(4, ref('base.user_root'))]"/>
</record>
</data>
</odoo>

View File

@ -1,4 +1,7 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
document_page_all,document.page,model_document_page,,1,0,0,0
document_page,document.page,model_document_page,base.group_user,1,1,1,1
document_page_history,document.page.history,model_document_page_history,base.group_user,1,0,1,0
document_page_user,document.page user,model_document_page,base.group_document_user,1,0,0,0
document_page_history_user,document.page.history user,model_document_page_history,base.group_document_user,1,0,0,0
document_page_editor,document.page editor,model_document_page,group_document_editor,1,1,1,0
document_page_history_editor,document.page.history editor,model_document_page_history,group_document_editor,1,1,1,0
document_page_manager,document.page manager,model_document_page,group_document_manager,1,1,1,1
document_page_history_manager,document.page.history manager,model_document_page_history,group_document_manager,1,1,1,0

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 document_page_all document_page_user document.page document.page user model_document_page base.group_document_user 1 0 0 0
3 document_page document_page_history_user document.page document.page.history user model_document_page model_document_page_history base.group_user base.group_document_user 1 1 0 1 0 1 0
4 document_page_history document_page_editor document.page.history document.page editor model_document_page_history model_document_page base.group_user group_document_editor 1 0 1 1 0
5 document_page_history_editor document.page.history editor model_document_page_history group_document_editor 1 1 1 0
6 document_page_manager document.page manager model_document_page group_document_manager 1 1 1 1
7 document_page_history_manager document.page.history manager model_document_page_history group_document_manager 1 1 1 0

View File

@ -1,12 +1,29 @@
.oe_form_editable .oe_document_page {
display: none;
table.diff {
font-family: Courier;
border: medium;
}
table.diff {font-family:Courier; border:medium;}
.diff_header {background-color:#e0e0e0}
td.diff_header {text-align:right}
.diff_next {background-color:#c0c0c0}
.diff_add {background-color:#aaffaa}
.diff_chg {background-color:#ffff77}
.diff_sub {background-color:#ffaaaa}
table.diff .diff_header {
background-color: #e0e0e0
}
table.diff td.diff_header {
text-align: right
}
table.diff .diff_next {
background-color:#c0c0c0
}
table.diff .diff_add {
background-color:#aaffaa
}
table.diff .diff_chg {
background-color:#ffff77
}
table.diff .diff_sub {
background-color:#ffaaaa
}

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from . import test_document_page, test_document_page_history
from . import test_document_page
from . import test_document_page_create_menu
from . import test_document_page_show_diff

View File

@ -4,34 +4,37 @@ from openerp.tests import common
class TestDocumentPage(common.TransactionCase):
"""document_page test class."""
def setUp(self):
super(TestDocumentPage, self).setUp()
self.page_obj = self.env['document.page']
self.history_obj = self.env['document.page.history']
self.category1 = self.env.ref('document_page.demo_category1')
self.page1 = self.env.ref('document_page.demo_page1')
def test_page_creation(self):
"""Test page creation."""
parent_page = self.env.ref('document_page.demo_category1')
self.assertEqual(parent_page.name, 'OpenERP Features')
record = self.env['document.page'].create({
'name': 'Test Page1',
'parent_id': parent_page.id,
page = self.page_obj.create({
'name': 'Test Page 1',
'parent_id': self.category1.id,
'content': 'Test content'
})
self.assertEqual(record.name, 'Test Page1')
self.assertEqual(page.content, 'Test content')
self.assertEqual(len(page.history_ids), 1)
page.content = 'New content for Demo Page'
self.assertEqual(len(page.history_ids), 2)
def test_category_display_content(self):
"""Test category display content."""
page = self.env.ref('document_page.demo_category1')
self.assertTrue(page.display_content.find('Demo') > 1)
def test_category_template(self):
page = self.page_obj.create({
'name': 'Test Page 2',
'parent_id': self.category1.id,
})
page._onchange_parent_id()
self.assertEqual(page.content, self.category1.template)
def test_page_display_content(self):
"""Test page display content."""
page = self.env.ref('document_page.demo_page1')
self.assertTrue(page.display_content.find('Demo') > 1)
def test_page_do_set_content(self):
"""Test page set content."""
page = self.env.ref('document_page.demo_page1')
page.content = None
page.do_set_content()
self.assertTrue(page.display_content.find('Summary') == 1)
def test_page_history_diff(self):
page = self.page_obj.create({
'name': 'Test Page 3',
'content': 'Test content'
})
page.content = 'New content'
self.assertIsNotNone(page.history_ids[0].diff)

View File

@ -1,21 +0,0 @@
# -*- coding: utf-8 -*-
from openerp.tests import common
class TestDocumentPageHistory(common.TransactionCase):
"""document_page_history test class."""
def test_page_history_demo_page1(self):
"""Test page history demo page1."""
page = self.env.ref('document_page.demo_page1')
page.content = 'Test content updated'
history_document = self.env['document.page.history']
history_pages = history_document.search([('page_id', '=', page.id)])
active_ids = [i.id for i in history_pages]
result = history_document.getDiff(active_ids[0], active_ids[0])
self.assertEqual(result, 'There are no changes in revisions.')
result = history_document.getDiff(active_ids[0], active_ids[1])
self.assertNotEqual(result, 'There are no changes in revisions.')

View File

@ -19,7 +19,7 @@ class TestDocumentPageShowDiff(common.TransactionCase):
self.assertTrue(
show_diff_object.with_context(
active_ids=[i.id for i in history_pages]
).get_diff()
)._get_diff()
)
page.write({'content': 'Text content updated'})
@ -30,7 +30,7 @@ class TestDocumentPageShowDiff(common.TransactionCase):
with self.assertRaises(Exception) as context:
show_diff_object.with_context(
active_ids=[i.id for i in history_pages]
).get_diff()
)._get_diff()
self.assertTrue(_("Select one or maximum two history revisions!")
in context.exception)

View File

@ -1,10 +1,7 @@
<?xml version="1.0"?>
<odoo>
<data>
<menuitem name="Pages"
id="menu_wiki"
parent="knowledge.menu_document"
sequence="20" />
<!-- wiki tree view -->
<record id="view_wiki_tree_children" model="ir.ui.view">
@ -15,8 +12,8 @@
<field name="arch" type="xml">
<tree string="Document Page">
<field name="name"/>
<field name="write_uid"/>
<field name="write_date"/>
<field name="content_uid"/>
<field name="content_date"/>
</tree>
</field>
</record>
@ -30,8 +27,8 @@
<field name="name"/>
<field name="parent_id"/>
<field name="create_uid" invisible="1"/>
<field name="write_uid"/>
<field name="write_date"/>
<field name="content_uid"/>
<field name="content_date"/>
</tree>
</field>
</record>
@ -47,31 +44,32 @@
<h1><field name="name" placeholder="Name"/></h1>
<group>
<group>
<field name="parent_id"
string="Category"
context="{'default_type':'category'}"/>
<field name="parent_id" string="Category" context="{'default_type':'category'}"/>
</group>
<group>
<field name="write_uid"
groups="base.group_no_one"/>
<field name="write_date"
groups="base.group_no_one"/>
<field name="menu_id"
groups="base.group_no_one"/>
<field name="content_uid"/>
<field name="content_date"/>
<field name="menu_id" readonly="1" attrs="{'invisible': [('menu_id','=',False)]}"/>
</group>
</group>
<separator string="Content" class="oe_edit_only" />
<field name="content"
placeholder="e.g. Once upon a time..."
class="oe_edit_only"
widget="html" />
<separator string="Last Content" />
<div class="oe_document_page">
<field name="display_content"
widget="html"
class="oe_view_only"
options='{"safe": True}' />
</div>
<notebook>
<page name="content" string="Content">
<label for="summary" class="oe_edit_only" />
<field name="summary" placeholder="eg: Changed ... for ..." class="oe_edit_only" />
<label for="content" class="oe_edit_only"/>
<field name="content" widget="html" placeholder="e.g. Once upon a time..." required="1" options="{'safe': True}"/>
</page>
<page name="history" string="History">
<field name="history_ids">
<tree>
<field name="id"/>
<field name="create_date"/>
<field name="summary"/>
<field name="create_uid"/>
</tree>
</field>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
@ -88,12 +86,7 @@
<form string="Document Page">
<field name="type" invisible="1"/>
<h1><field name="name" placeholder="Name"/></h1>
<div class="oe_document_page">
<field name="display_content"
widget="html"
class="oe_view_only"
options='{"safe": True}' />
</div>
<field name="content" class="oe_view_only" required="1" options='{"safe": True}'/>
</form>
</field>
</record>
@ -104,90 +97,19 @@
<field name="model">document.page</field>
<field name="arch" type="xml">
<search string="Document Page">
<field name="name"
string="Content"
filter_domain="['|', ('name','ilike',self), ('content','ilike',self)]"/>
<field name="write_uid"/>
<field name="name" string="Content" filter_domain="['|', ('name','ilike',self), ('content','ilike',self)]"/>
<field name="parent_id"/>
<field name="create_uid"/>
<field name="content_uid"/>
<group expand="0" string="Group By...">
<filter string="Document Type"
domain="[]"
context="{'group_by':'parent_id'}" />
<filter string="Author"
domain="[]"
context="{'group_by':'create_uid'}" />
<filter string="Last Contributor"
domain="[]"
context="{'group_by':'write_uid'}" />
<filter string="Category" context="{'group_by':'parent_id'}" />
<filter string="Author" context="{'group_by':'create_uid'}" />
<filter string="Last Contributor" context="{'group_by':'content_uid'}" />
</group>
</search>
</field>
</record>
<!-- Category Views -->
<record id="view_category_form" model="ir.ui.view">
<field name="name">document.page.category.form</field>
<field name="model">document.page</field>
<field name="arch" type="xml">
<form string="Category">
<sheet>
<field name="type" invisible="1"/>
<h1><field name="name" placeholder="Name"/></h1>
<group>
<group>
<field name="parent_id" string="Category"
context="{'default_type':'category'}"/>
</group>
<group>
<field name="write_uid"
groups="base.group_no_one"/>
<field name="write_date"
groups="base.group_no_one"/>
<field name="menu_id"
groups="base.group_no_one"/>
</group>
</group>
<notebook>
<page string="Template" name="template">
<div>
<label for="content"
string="Template that will be used as a content template for all new page of this category."/>
</div>
<field name="content"
placeholder="e.g. Once upon a time..."
widget="html" />
</page>
<page string="Documents" name="documents">
<div class="oe_document_page">
<field name="display_content"
widget="html"
class="oe_view_only"
options='{"safe": True}' />
</div>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
<record id="view_category_tree" model="ir.ui.view">
<field name="name">document.page.category.tree</field>
<field name="model">document.page</field>
<field name="arch" type="xml">
<tree string="Categories">
<field name="name"/>
<field name="parent_id"/>
<field name="create_uid" invisible="1"/>
<field name="write_uid"/>
<field name="write_date"/>
</tree>
</field>
</record>
<!-- page action -->
<record id="action_page" model="ir.actions.act_window">
@ -205,103 +127,35 @@
</p>
</field>
</record>
<record id="action_page_view_tree" model="ir.actions.act_window.view">
<field name="sequence" eval="0" />
<field name="view_mode">tree</field>
<field name="view_id" ref="view_wiki_tree"/>
<field name="act_window_id" ref="action_page"/>
</record>
<record id="action_page_view_form" model="ir.actions.act_window.view">
<field name="sequence" eval="5" />
<field name="view_mode">form</field>
<field name="view_id" ref="view_wiki_form"/>
<field name="act_window_id" ref="action_page"/>
</record>
<menuitem id="menu_wiki"
name="Pages"
parent="knowledge.menu_document"
sequence="20" />
<menuitem id="menu_page"
parent="menu_wiki"
name="Pages"
action="action_page"
sequence="10" />
name="Pages"
parent="menu_wiki"
action="action_page"
sequence="10" />
<record id="action_category" model="ir.actions.act_window">
<field name="name">Category</field>
<field name="res_model">document.page</field>
<field name="domain">[('type','=','category')]</field>
<field name="context">{'default_type': 'category'}</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_category_tree"/>
<field name="search_view_id" ref="view_wiki_filter"/>
</record>
<record id="action_category_view_tree" model="ir.actions.act_window.view">
<field name="sequence" eval="0" />
<field name="view_mode">tree</field>
<field name="view_id" ref="view_category_tree"/>
<field name="act_window_id" ref="action_category"/>
</record>
<record id="action_category_view_form" model="ir.actions.act_window.view">
<field name="sequence" eval="5" />
<field name="view_mode">form</field>
<field name="view_id" ref="view_category_form"/>
<field name="act_window_id" ref="action_category"/>
</record>
<menuitem id="menu_category"
parent="menu_wiki"
name="Categories"
action="action_category"
sequence="20"/>
<!-- History Tree view -->
<record model="ir.ui.view" id="view_wiki_history_tree">
<field name="name">document.page.history.tree</field>
<field name="model">document.page.history</field>
<field name="arch" type="xml">
<tree string="Document History">
<field name="create_date"/>
<field name="create_uid"/>
<field name="page_id"/>
</tree>
</field>
</record>
<!-- History Form view -->
<record model="ir.ui.view" id="wiki_history_form">
<field name="name">document.page.history.form</field>
<field name="model">document.page.history</field>
<field name="arch" type="xml">
<form string="Document Page History" version="7.0">
<label for="page_id" class="oe_edit_only"/>
<h1><field name="page_id" select="1" /></h1>
<label for="create_date" class="oe_edit_only"/>
<field name="create_date" readonly="1"/>
<label for="content" class="oe_edit_only"/>
<field name="content" colspan="4"
widget="html" options='{"safe": True}'/>
</form>
</field>
</record>
<!-- History Action -->
<record model="ir.actions.act_window" id="action_history">
<field name="name">Page history</field>
<field name="res_model">document.page.history</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="menu_page_history"
parent="menu_wiki"
name="Pages history"
action="action_history"
sequence="30"
groups="base.group_no_one" />
<act_window
id="action_related_page_history"
context="{'search_default_page_id': [active_id], 'default_page_id': active_id}"
domain="[('page_id','=',active_id)]"
name="Page History"
res_model="document.page.history"
src_model="document.page"/>
<act_window
id="action_related_page_create_menu"

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data>
<template id="assets_backend" name="document_page assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link rel="stylesheet" href="/document_page/static/src/css/document_page.css"/>
</xpath>
</template>
</data>
</odoo>

View File

@ -0,0 +1,90 @@
<?xml version="1.0"?>
<odoo>
<data>
<!-- Category Views -->
<record id="view_category_form" model="ir.ui.view">
<field name="name">document.page.category.form</field>
<field name="model">document.page</field>
<field name="arch" type="xml">
<form string="Category">
<sheet>
<field name="type" invisible="1"/>
<h1><field name="name" placeholder="Name"/></h1>
<group>
<group>
<field name="parent_id" string="Category" context="{'default_type':'category'}"/>
</group>
<group>
<field name="write_uid" groups="base.group_no_one"/>
<field name="write_date" groups="base.group_no_one"/>
<field name="menu_id" groups="base.group_no_one"/>
</group>
</group>
<notebook>
<page string="Template" name="template">
<field name="template" placeholder="e.g. Once upon a time..." />
</page>
<page string="Documents" name="documents">
<field name="content" widget="html" class="oe_view_only" options='{"safe": True}' />
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
<record id="view_category_tree" model="ir.ui.view">
<field name="name">document.page.category.tree</field>
<field name="model">document.page</field>
<field name="arch" type="xml">
<tree string="Categories">
<field name="name"/>
<field name="parent_id"/>
<field name="create_uid" invisible="1"/>
<field name="write_uid"/>
<field name="write_date"/>
</tree>
</field>
</record>
<!-- category Action -->
<record id="action_category" model="ir.actions.act_window">
<field name="name">Category</field>
<field name="res_model">document.page</field>
<field name="domain">[('type','=','category')]</field>
<field name="context">{'default_type': 'category'}</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_category_tree"/>
<field name="search_view_id" ref="view_wiki_filter"/>
</record>
<record id="action_category_view_tree" model="ir.actions.act_window.view">
<field name="sequence" eval="0" />
<field name="view_mode">tree</field>
<field name="view_id" ref="view_category_tree"/>
<field name="act_window_id" ref="action_category"/>
</record>
<record id="action_category_view_form" model="ir.actions.act_window.view">
<field name="sequence" eval="5" />
<field name="view_mode">form</field>
<field name="view_id" ref="view_category_form"/>
<field name="act_window_id" ref="action_category"/>
</record>
<menuitem id="menu_category"
parent="menu_wiki"
name="Categories"
action="action_category"
sequence="20"/>
</data>
</odoo>

View File

@ -0,0 +1,90 @@
<?xml version="1.0"?>
<odoo>
<data>
<!-- History Tree view -->
<record model="ir.ui.view" id="view_wiki_history_tree">
<field name="name">document.page.history.tree</field>
<field name="model">document.page.history</field>
<field name="arch" type="xml">
<tree string="Document History">
<field name="id"/>
<field name="page_id"/>
<field name="summary"/>
<field name="create_uid"/>
<field name="create_date"/>
</tree>
</field>
</record>
<!-- History Search view -->
<record id="view_wiki_history_filter" model="ir.ui.view">
<field name="name">document.page.history.search</field>
<field name="model">document.page.history</field>
<field name="arch" type="xml">
<search string="Document Page History">
<field name="page_id"/>
<field name="content"/>
<field name="create_uid"/>
<group expand="0" string="Group By...">
<filter name="group_author" string="Author" context="{'group_by':'create_uid'}" />
</group>
</search>
</field>
</record>
<!-- History Form view -->
<record model="ir.ui.view" id="wiki_history_form">
<field name="name">document.page.history.form</field>
<field name="model">document.page.history</field>
<field name="arch" type="xml">
<form string="Document Page History">
<sheet>
<h1><field name="page_id"/></h1>
<group>
<group>
<field name="create_uid" readonly="1"/>
<field name="create_date" readonly="1"/>
</group>
</group>
<notebook>
<page name="content" string="Content">
<label for="summary"/>
<field name="summary" placeholder="eg: Changed ... for ..."/>
<label for="content"/>
<field name="content" widget="html" placeholder="e.g. Once upon a time..." options="{'safe': True}"/>
</page>
<page name="diff" string="Changes">
<field name="diff" widget="html" style="overflow-x: scroll" />
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- History Action -->
<record model="ir.actions.act_window" id="action_history">
<field name="name">Page history</field>
<field name="res_model">document.page.history</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="menu_page_history"
parent="menu_wiki"
name="Pages history"
action="action_history"
sequence="30"
groups="base.group_no_one" />
<act_window id="action_related_page_history"
context="{'search_default_page_id': [active_id], 'default_page_id': active_id}"
domain="[('page_id','=',active_id)]"
name="Page History"
res_model="document.page.history"
src_model="document.page"/>
</data>
</odoo>

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<template id="report_documentpage_doc">
<t t-call="report.external_layout">
<div class="page">
<h1 t-field="doc.display_name" />
<div t-raw="doc.display_content" />
</div>
</t>
</template>
<template id="report_documentpage">
<t t-call="report.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="document_page.report_documentpage_doc" />
</t>
</t>
</template>
<report
id="report_document_page"
string="Document Page"
model="document.page"
report_type="qweb-pdf"
file="document_page.report_documentpage"
name="document_page.report_documentpage"
/>
</data>
</openerp>

View File

@ -19,7 +19,7 @@
#
##############################################################################
from openerp import models, fields, _
from openerp import exceptions
from openerp.exceptions import UserError
class DocumentPageShowDiff(models.TransientModel):
@ -27,33 +27,24 @@ class DocumentPageShowDiff(models.TransientModel):
_name = 'wizard.document.page.history.show_diff'
def get_diff(self):
def _get_diff(self):
"""Return the Difference between two document."""
history = self.env["document.page.history"]
ids = self.env.context.get('active_ids', [])
diff = ""
diff = False
if len(ids) == 2:
if ids[0] > ids[1]:
diff = history.getDiff(ids[1], ids[0])
else:
diff = history.getDiff(ids[0], ids[1])
elif len(ids) == 1:
old = history.browse(ids[0])
nids = history.search(
[('page_id', '=', old.page_id.id)],
order='id DESC',
limit=1
)
diff = history.getDiff(ids[0], nids.id)
diff = history.browse(ids[0]).diff
else:
raise exceptions.Warning(
_("Select one or maximum two history revisions!")
)
raise UserError(
_("Select one or maximum two history revisions!"))
return diff
diff = fields.Text(
'Diff',
readonly=True,
default=get_diff
default=_get_diff,
)

View File

@ -8,13 +8,9 @@
<field name="model">wizard.document.page.history.show_diff</field>
<field name="arch" type="xml">
<form string="Difference" version="7.0">
<field name="diff"
widget="html"
options='{"safe": True}' />
<field name="diff" widget="html" class="overflow-x: scroll" />
<footer>
<button string="Close"
class="oe_link"
special="cancel" />
<button string="Close" class="oe_link" special="cancel" />
</footer>
</form>
</field>

View File

@ -19,3 +19,4 @@
#
##############################################################################
from . import models
from .hooks import post_init_hook, uninstall_hook

View File

@ -21,13 +21,12 @@
{
'name': 'Document Page Approval',
'version': '9.0.1.0.0',
'version': '9.0.2.0.0',
"author": "Savoir-faire Linux,Odoo Community Association (OCA)",
"website": "http://www.savoirfairelinux.com",
"license": "AGPL-3",
'category': 'Knowledge Management',
'depends': [
'knowledge',
'document_page',
'mail',
],
@ -40,6 +39,8 @@
],
'installable': True,
'auto_install': False,
'post_init_hook': 'post_init_hook',
'uninstall_hook': 'uninstall_hook',
'images': [
'images/category.png',
'images/page_history_list.png',

View File

@ -1,15 +1,13 @@
<?xml version="1.0"?>
<odoo>
<!-- Allow user to make upgrade-proof customizations to email template -->
<data noupdate="1">
<!-- If user wants to make upgrade-proof customizations to email templates, he should edit ir.model.data and check noupdate himself -->
<data>
<record id="email_template_new_draft_need_approval" model="mail.template">
<field name="name">Automated new draft need approval Notification Mail</field>
<field name="email_from">${object.create_uid.company_id.email or 'noreply@localhost.com'}</field>
<field name="subject">New version of "${object.page_id.name}" to approve</field>
<field name="email_to">${object.get_approvers_email}</field>
<field name="subject">New version of ${object.display_name} needs your approval</field>
<field name="model_id" ref="model_document_page_history"/>
<field name="auto_delete" eval="True"/>
<field name="lang">${object.create_uid.partner_id.lang}</field>
@ -17,13 +15,29 @@
<![CDATA[
<p>Hello,</p>
<p>The page "${object.page_id.name}" has been modified and need your approval.</p>
<p>${object.create_uid.name} submited a new Change Request for <b>${object.page_id.name}</b> and it needs your approval.</p>
<p>You can review the new version here : <a href="${object.get_page_url}">${object.get_page_url}</a></p>
<h1><a href="${object.page_url}">${object.display_name}</a></h1>
<p>
<b>Modified by:</b> ${object.create_uid.name}<br/>
<b>Date:</b> ${object.create_date}<br>
</p>
% if object.summary:
<h3>Summary</h3>
<p>${object.summary}</p>
% endif
<h3>Diff</h3>
<div style="overflow-x:scroll; font-size:0.85em; margin-bottom:2em;">
${object.diff|safe}
</div>
<p>Have a great day.</p>
<p>Have a great day.<br/>
--<br/>
Odoo</p>]]>
<p>Odoo</p>
]]>
</field>
</record>

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Ivan Todorovich (<ivan.todorovich@gmail.com>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
def post_init_hook(cr, registry): # pragma: no cover
# Set all pre-existing pages history to approved
cr.execute("""
UPDATE document_page_history
SET state='approved',
approved_uid=create_uid,
approved_date=create_date
WHERE state IS NULL
""")
def uninstall_hook(cr, registry): # pragma: no cover
# Remove unapproved pages
cr.execute(
"DELETE FROM document_page_history "
"WHERE state != 'approved'"
)

View File

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Ivan Todorovich <ivan.todorovich@gmail.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
def migrate(cr, version): # pragma: no cover
# Set all pre-existing pages history to approved
cr.execute("""
UPDATE document_page_history
SET state='approved',
approved_uid=create_uid,
approved_date=create_date
WHERE state IS NULL
""")

View File

@ -20,6 +20,7 @@
##############################################################################
from openerp import models, fields, api
from ast import literal_eval
class DocumentPageApproval(models.Model):
@ -27,104 +28,126 @@ class DocumentPageApproval(models.Model):
_inherit = 'document.page'
@api.multi
def _get_display_content(self):
"""Display the content of document."""
for page in self:
content = ""
if page.type == "category":
content = self._get_page_index(page, link=False)
else:
history = self.env['document.page.history']
if self.is_approval_required(page):
history_ids = history.search(
[
('page_id', '=', page.id),
('state', '=', 'approved')
],
limit=1,
order='create_date DESC'
)
content = history_ids.content
else:
content = page.content
page.display_content = content
@api.multi
def _get_approved_date(self):
"""Return the approved date of a document."""
for page in self:
approved_date = False
if self.is_approval_required(page):
history = self.env['document.page.history']
history_ids = history.search(
[
('page_id', '=', page.id),
('state', '=', 'approved')
],
limit=1,
order='create_date DESC'
)
approved_date = history_ids.approved_date
page.approved_date = approved_date
@api.multi
def _get_approved_uid(self):
"""Return the user's id of the approved user."""
for page in self:
approved_uid = False
if self.is_approval_required(page):
history = self.env['document.page.history']
history_ids = history.search(
[
('page_id', '=', page.id),
('state', '=', 'approved')
],
limit=1,
order='create_date DESC'
)
approved_uid = history_ids.approved_uid.id
page.approved_uid = approved_uid
@api.multi
def _is_parent_approval_required(self):
"""Check if the document required approval base on his parrent."""
for page in self:
page.is_parent_approval_required = self.is_approval_required(page)
def is_approval_required(self, page):
"""Check if a document required approval."""
if page:
res = page.approval_required
res = res or self.is_approval_required(page.parent_id)
else:
res = False
return res
display_content = fields.Text(
compute=_get_display_content,
string='Displayed Content'
history_ids = fields.One2many(
order='approved_date DESC',
domain=[('state', '=', 'approved')],
)
approved_date = fields.Datetime(
compute=_get_approved_date,
string="Approved Date"
'Approved Date',
related='history_head.approved_date',
store=True,
index=True,
readonly=True,
)
approved_uid = fields.Many2one(
'res.users',
compute=_get_approved_uid,
string="Approved By",
'Approved by',
related='history_head.approved_uid',
store=True,
index=True,
readonly=True,
)
approval_required = fields.Boolean("Require approval")
is_parent_approval_required = fields.Boolean(
compute=_is_parent_approval_required,
string="parent approval"
approval_required = fields.Boolean(
'Require approval',
help='Require approval for changes on this page or its child pages.',
)
approver_gid = fields.Many2one(
"res.groups",
"Approver group"
"Approver group",
help='Users must also belong to the Approvers group',
)
is_approval_required = fields.Boolean(
'Approval required',
help='If true, changes of this page require approval',
compute='_compute_is_approval_required',
)
am_i_approver = fields.Boolean(
compute='_compute_am_i_approver'
)
approver_group_ids = fields.Many2many(
'res.groups',
string='Approver groups',
help='Groups that can approve changes to this document',
compute='_compute_approver_group_ids',
)
has_changes_pending_approval = fields.Boolean(
compute='_compute_has_changes_pending_approval',
string='Has changes pending approval'
)
@api.multi
@api.depends('approval_required', 'parent_id.is_approval_required')
def _compute_is_approval_required(self):
"""Check if the document required approval based on his parents."""
for page in self:
res = page.approval_required
if page.parent_id:
res = res or page.parent_id.is_approval_required
page.is_approval_required = res
@api.multi
@api.depends('approver_gid', 'parent_id.approver_group_ids')
def _compute_approver_group_ids(self):
"""Compute the approver groups based on his parents."""
for page in self:
res = page.approver_gid
if page.parent_id:
res = res | page.parent_id.approver_group_ids
page.approver_group_ids = res
@api.multi
@api.depends('is_approval_required', 'approver_group_ids')
def _compute_am_i_approver(self):
"""Check if the current user can approve changes to this page."""
for rec in self:
rec.am_i_approver = rec.can_user_approve_this_page(self.env.user)
@api.multi
def can_user_approve_this_page(self, user):
"""Check if a user can approve this page."""
self.ensure_one()
# if it's not required, anyone can approve
if not self.is_approval_required:
return True
# to approve, you must have approver rights
approver_group_id = self.env.ref(
'document_page_approval.group_document_approver_user')
if approver_group_id not in user.groups_id:
return False
# and belong to at least one of the approver_groups (if any is set)
if not self.approver_group_ids:
return True
return len(user.groups_id & self.approver_group_ids) > 0
@api.multi
def _compute_has_changes_pending_approval(self):
history = self.env['document.page.history']
for rec in self:
changes = history.search_count([
('page_id', '=', rec.id),
('state', '=', 'to approve')])
rec.has_changes_pending_approval = (changes > 0)
@api.multi
def _create_history(self, vals):
res = super(DocumentPageApproval, self)._create_history(vals)
res.signal_workflow('document_page_auto_confirm')
@api.multi
def action_changes_pending_approval(self):
self.ensure_one()
action = self.env.ref('document_page_approval.action_change_requests')
action = action.read()[0]
context = literal_eval(action['context'])
context['search_default_page_id'] = self.id
context['default_page_id'] = self.id
action['context'] = context
return action

View File

@ -28,117 +28,122 @@ from openerp import models, fields, api
class DocumentPageHistoryWorkflow(models.Model):
"""Useful to manage edition's workflow on a document."""
_inherit = 'document.page.history'
_name = 'document.page.history'
_inherit = ['document.page.history', 'mail.thread']
state = fields.Selection([
('draft', 'Draft'),
('to approve', 'Pending Approval'),
('approved', 'Approved'),
('cancelled', 'Cancelled')],
'Status',
readonly=True,
)
approved_date = fields.Datetime(
'Approved Date',
)
approved_uid = fields.Many2one(
'res.users',
'Approved by',
)
is_approval_required = fields.Boolean(
related='page_id.is_approval_required',
string="Approval required",
)
am_i_owner = fields.Boolean(
compute='_compute_am_i_owner'
)
am_i_approver = fields.Boolean(
related='page_id.am_i_approver'
)
page_url = fields.Text(
compute='_compute_page_url',
string="URL",
)
@api.multi
def page_approval_draft(self):
"""Set a document state as draft and notified the reviewers."""
"""Set a change request as draft"""
self.write({'state': 'draft'})
@api.multi
def page_approval_to_approve(self):
"""Set a change request as to approve"""
self.write({'state': 'to approve'})
template = self.env.ref(
'document_page_approval.email_template_new_draft_need_approval')
for page in self:
if page.is_parent_approval_required:
template.send_mail(page.id, force_send=True)
return True
approver_gid = self.env.ref(
'document_page_approval.group_document_approver_user')
for rec in self:
if rec.is_approval_required:
guids = [g.id for g in rec.page_id.approver_group_ids]
users = self.env['res.users'].search([
('groups_id', 'in', guids),
('groups_id', 'in', approver_gid.id)])
rec.message_subscribe_users([u.id for u in users])
rec.message_post_with_template(template.id)
@api.multi
def page_approval_approved(self):
"""Set a document state as approve."""
message_obj = self.env['mail.message']
"""Set a change request as approved."""
self.write({
'state': 'approved',
'approved_date': datetime.now().strftime(
DEFAULT_SERVER_DATETIME_FORMAT),
'approved_uid': self.env.uid
})
# Notify followers a new version is available
for page_history in self:
subtype = self.env.ref('mail.mt_comment')
message_obj.create(
{'res_id': page_history.page_id.id,
'model': 'document.page',
'subtype_id': subtype.id,
'body': _('New version of the document %s'
' approved.') % page_history.page_id.name
}
for rec in self:
# Trigger computed field update
rec.page_id._compute_history_head()
# Notify state change
rec.message_post(
subtype='mt_comment',
body=_(
'Change request has been approved by %s.'
) % (self.env.user.name)
)
return True
@api.multi
def _can_user_approve_page(self):
"""Check if a user cas approve the page."""
user = self.env.user
for page in self:
page.can_user_approve_page = page.can_user_approve_this_page(
page.page_id,
user
# Notify followers a new version is available
rec.page_id.message_post(
subtype='mt_comment',
body=_(
'New version of the document %s approved.'
) % (rec.page_id.name)
)
def can_user_approve_this_page(self, page, user):
"""Check if a user can approved the page."""
if page:
res = page.approver_gid in user.groups_id
res = res or self.can_user_approve_this_page(page.parent_id, user)
else:
res = False
return res
@api.multi
def page_approval_cancelled(self):
"""Set a change request as cancelled."""
self.write({'state': 'cancelled'})
for rec in self:
rec.message_post(
subtype='mt_comment',
body=_(
'Change request <b>%s</b> has been cancelled by %s.'
) % (rec.display_name, self.env.user.name)
)
@api.multi
def get_approvers_guids(self):
"""Return the approvers group."""
res = {}
for page in self:
res[page.id] = self.get_approvers_guids_for_page(page.page_id)
return res
def get_approvers_guids_for_page(self, page):
"""Return the approvers group for a page."""
if page:
if page.approver_gid:
res = [page.approver_gid.id]
else:
res = []
res.extend(self.get_approvers_guids_for_page(page.parent_id))
else:
res = []
return res
def _compute_am_i_owner(self):
"""Check if current user is the owner"""
for rec in self:
rec.am_i_owner = (rec.create_uid == self.env.user)
@api.multi
def _get_approvers_email(self):
"""Get the approvers email."""
for page in self:
emails = ''
guids = self.get_approvers_guids()
uids = [i.id for i in self.env['res.users'].search([
('groups_id', 'in', guids[page.id])
])]
users = self.env['res.users'].browse(uids)
for user in users:
if user.email:
emails += user.email
emails += ','
else:
empl = self.env['hr.employee'].search([
('login', '=', user.login)
])
if empl.work_email:
emails += empl.work_email
emails += ','
page.get_approvers_email = emails[:-1]
@api.multi
def _get_page_url(self):
"""Get the page url."""
def _compute_page_url(self):
"""Compute the page url."""
for page in self:
base_url = self.env['ir.config_parameter'].get_param(
'web.base.url',
default='http://localhost:8069'
)
page.get_page_url = (
page.page_url = (
'{}/web#db={}&id={}&view_type=form&'
'model=document.page.history').format(
base_url,
@ -146,37 +151,18 @@ class DocumentPageHistoryWorkflow(models.Model):
page.id
)
state = fields.Selection(
[('draft', 'Draft'), ('approved', 'Approved')],
'Status',
readonly=True
)
approved_date = fields.Datetime("Approved Date")
approved_uid = fields.Many2one(
'res.users',
"Approved By"
)
is_parent_approval_required = fields.Boolean(
related='page_id.is_parent_approval_required',
string="parent approval",
store=False
)
can_user_approve_page = fields.Boolean(
compute=_can_user_approve_page,
string="can user approve this page",
store=False
)
get_approvers_email = fields.Text(
compute=_get_approvers_email,
string="get all approvers email",
store=False
)
get_page_url = fields.Text(
compute=_get_page_url,
string="URL",
store=False
)
@api.multi
def _compute_diff(self):
"""Shows a diff between this version and the previous version"""
history = self.env['document.page.history']
for rec in self:
domain = [
('page_id', '=', rec.page_id.id),
('state', '=', 'approved')]
if rec.approved_date:
domain.append(('approved_date', '<', rec.approved_date))
prev = history.search(domain, limit=1, order='approved_date DESC')
if prev:
rec.diff = self.getDiff(prev.id, rec.id)
else:
rec.diff = self.getDiff(False, rec.id)

View File

@ -1,9 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="0">
<record id="group_document_approver_user" model="res.groups">
<field name="name">Document approver</field>
<field name="users" eval="[(4, ref('base.user_root'))]"/>
<field name="name">Approver</field>
<field name="category_id" ref="knowledge.module_category_knowledge_management"/>
<field name="implied_ids" eval="[(4, ref('document_page.group_document_editor'))]"/>
</record>
<record id="document_page.group_document_manager" model="res.groups">
<field name="implied_ids" eval="[(4, ref('group_document_approver_user'))]"/>
</record>
</data>
</odoo>

View File

@ -1,2 +1 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
document_page_history,document.page.history,model_document_page_history,group_document_approver_user,1,1,1,0

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
document_page_history document.page.history model_document_page_history group_document_approver_user 1 1 1 0

View File

@ -1,2 +1,2 @@
# -*- coding: utf-8 -*-
from . import test_document_page_approval, test_document_page_history_workflow
from . import test_document_page_approval

View File

@ -3,35 +3,123 @@ from openerp.tests import common
class TestDocumentPageApproval(common.TransactionCase):
"""Test document page approval model."""
def test_get_display_content(self):
"""Test page display content."""
# Check content of a category
category = self.env['document.page'].search([
('name', '=', 'OpenERP Features')
])
def setUp(self):
super(TestDocumentPageApproval, self).setUp()
self.page_obj = self.env['document.page']
self.history_obj = self.env['document.page.history']
# demo
self.category1 = self.env.ref('document_page.demo_category1')
self.page1 = self.env.ref('document_page.demo_page1')
self.approver_gid = self.env.ref(
'document_page_approval.group_document_approver_user')
# demo_approval
self.category2 = self.page_obj.create({
'name': 'This category requires approval',
'type': 'category',
'approval_required': True,
'approver_gid': self.approver_gid.id,
})
self.page2 = self.page_obj.create({
'name': 'This page requires approval',
'parent_id': self.category2.id,
'content': 'This content will require approval'
})
self.assertIsNotNone(category.display_content, 'a category')
def test_approval_required(self):
page = self.page2
self.assertTrue(page.is_approval_required)
self.assertTrue(page.has_changes_pending_approval)
self.assertEqual(len(page.history_ids), 0)
# Check content of a page
pages = self.env['document.page'].search([
('parent_id', '=', category.id)
])
def test_change_request_approve(self):
page = self.page2
chreq = self.history_obj.search([
('page_id', '=', page.id),
('state', '!=', 'approved')
])[0]
# It should automatically be in 'to approve' state
self.assertEqual(chreq.state, 'to approve')
self.assertNotEqual(chreq.content, page.content)
# who_am_i
self.assertTrue(chreq.am_i_owner)
self.assertTrue(chreq.am_i_approver)
# approve
chreq.signal_workflow('page_approval_approve')
self.assertEqual(chreq.state, 'approved')
self.assertEqual(chreq.content, page.content)
# new changes should create change requests
page.write({'content': 'New content'})
self.assertNotEqual(page.content, 'New content')
chreq = self.history_obj.search([
('page_id', '=', page.id),
('state', '!=', 'approved')
])[0]
chreq.signal_workflow('page_approval_approve')
self.assertEqual(page.content, 'New content')
def test_change_request_auto_approve(self):
page = self.page1
self.assertFalse(page.is_approval_required)
page.write({'content': 'New content'})
self.assertEqual(page.content, 'New content')
def test_change_request_from_scratch(self):
page = self.page2
# aprove everything
self.history_obj.search([
('page_id', '=', page.id),
('state', '!=', 'approved')
]).signal_workflow('page_approval_approve')
# new change request from scrath
chreq = self.history_obj.create({
'page_id': page.id,
'summary': 'Changed something',
'content': 'New content',
})
self.assertEqual(chreq.state, 'draft')
self.assertNotEqual(page.content, chreq.content)
self.assertNotEqual(page.approved_date, chreq.approved_date)
self.assertNotEqual(page.approved_uid, chreq.approved_uid)
chreq.signal_workflow('page_approval_to_approve')
self.assertEqual(chreq.state, 'to approve')
self.assertNotEqual(page.content, chreq.content)
self.assertNotEqual(page.approved_date, chreq.approved_date)
self.assertNotEqual(page.approved_uid, chreq.approved_uid)
chreq.signal_workflow('page_approval_cancel')
self.assertEqual(chreq.state, 'cancelled')
self.assertNotEqual(page.content, chreq.content)
self.assertNotEqual(page.approved_date, chreq.approved_date)
self.assertNotEqual(page.approved_uid, chreq.approved_uid)
chreq.signal_workflow('page_approval_reopen')
self.assertEqual(chreq.state, 'draft')
self.assertNotEqual(page.content, chreq.content)
self.assertNotEqual(page.approved_date, chreq.approved_date)
self.assertNotEqual(page.approved_uid, chreq.approved_uid)
chreq.signal_workflow('page_approval_approve')
self.assertEqual(chreq.state, 'approved')
self.assertEqual(page.content, chreq.content)
self.assertEqual(page.approved_date, chreq.approved_date)
self.assertEqual(page.approved_uid, chreq.approved_uid)
def test_get_approvers_guids(self):
"""Get approver guids."""
page = self.page2
self.assertTrue(len(page.approver_group_ids) > 0)
def test_get_page_url(self):
"""Test if page url exist."""
pages = self.env['document.page.history'].search([])
page = pages[0]
self.assertIsNotNone(page.display_content, 'Page content')
# Check if approval is required
self.assertTrue(page.is_approval_required(page) ==
category.approval_required)
# Check content of an approval page
page.approval_required = True
self.assertIsNotNone(page.display_content, 'Page content')
# Check if approval is required
self.assertTrue(page.is_approval_required(page))
# Check if parent approval is required
self.assertTrue(page.is_parent_approval_required)
self.assertIsNotNone(page.page_url)

View File

@ -1,67 +0,0 @@
# -*- coding: utf-8 -*-
from openerp.tests import common
# Import logger
import logging
# Get the logger
_logger = logging.getLogger(__name__)
class TestDocumentPageHistoryWorkflow(common.TransactionCase):
"""Test document page history workflow."""
def test_can_user_approve_this_page(self):
"""Test if a user can approve this page."""
category = self.env.ref('document_page.demo_category1')
category.approval_required = True
category.approver_gid = self.env.ref(
'document_page_approval.group_document_approver_user')
page = self.env['document.page'].create({
'name': 'Test Page10',
'content': 'A difficult test',
'parent_id': category.id
})
history = self.env['document.page.history'].search(
[
('page_id', '=', page.id)
],
limit=1,
order='create_date DESC'
)
self.assertTrue(history.can_user_approve_page)
def test_get_approvers_guids(self):
"""Get approver guids."""
category = self.env.ref('document_page.demo_category1')
category.approval_required = True
pages = self.env['document.page.history'].search([
('page_id', '=', category.id)
])
page = pages[0]
approvers_guid = page.get_approvers_guids()
self.assertTrue(len(approvers_guid) > 0)
def test_get_approvers_email(self):
"""Get approver email."""
category = self.env.ref('document_page.demo_category1')
category.approval_required = True
pages = self.env['document.page.history'].search([
('page_id', '=', category.id)
])
page = pages[0]
_logger.info("Email: " + str(page.get_approvers_email))
self.assertIsNotNone(page.get_approvers_email)
def test_get_page_url(self):
"""Test if page url exist."""
category = self.env.ref('document_page.demo_category1')
category.approval_required = True
pages = self.env['document.page.history'].search([
('page_id', '=', category.id)
])
page = pages[0]
_logger.info("Page: " + str(page.get_page_url))
self.assertIsNotNone(page.get_page_url)

View File

@ -1,58 +1,110 @@
<?xml version="1.0"?>
<odoo>
<!-- History Form View -->
<record id="wiki_history_form_inherit" model="ir.ui.view">
<field name="name">document.page.history.form</field>
<field name="model">document.page.history</field>
<field name="inherit_id"
ref="document_page.wiki_history_form"/>
<field name="inherit_id" ref="document_page.wiki_history_form"/>
<field name="arch" type="xml">
<xpath expr="//form/label[@for='page_id']"
position="before">
<header attrs="{'invisible':[('is_parent_approval_required','=',False)]}">
<span attrs="{'invisible':[('can_user_approve_page','=',False)]}">
<button name="page_approval_approve"
string="Approve"
states="draft" />
</span>
<field name="state"
widget="statusbar"
statusbar_visible="draft,approved"/>
<field name="is_parent_approval_required"
invisible="1" />
<field name="can_user_approve_page"
invisible="1" />
<sheet position="before">
<header>
<!-- draft -> to approve -->
<button name="page_approval_to_approve" string="Send to Review" class="oe_highlight"
attrs="{'invisible':['|','|',('is_approval_required','=',False),('am_i_owner','=',False),('state', 'not in', ['draft'])]}"/>
<!-- approve if i am approver -->
<button name="page_approval_approve" string="Approve" class="oe_highlight"
attrs="{'invisible':['|','|',('is_approval_required','=',False),('am_i_approver','=',False),('state','not in',['draft','to approve'])]}"/>
<!-- approve if it's not required and i am owner -->
<button name="page_approval_approve" string="Approve" class="oe_highlight"
attrs="{'invisible':['|','|',('is_approval_required','=',True),('am_i_owner','=',False),('state','not in',['draft', 'to approve'])]}"/>
<!-- cancel if i am owner or approver -->
<button name="page_approval_cancel" string="Cancel"
attrs="{'invisible':['|','&amp;',('am_i_owner','=',False),('am_i_approver','=',False),('state','not in',['draft','to approve'])]}"/>
<!-- reopen if i am owner or approver -->
<button name="page_approval_reopen" string="Back to draft"
attrs="{'invisible':['|','&amp;',('am_i_owner','=',False),('am_i_approver','=',False),('state','not in',['cancelled'])]}"/>
<field name="am_i_owner" invisible="1"/>
<field name="am_i_approver" invisible="1"/>
<field name="is_approval_required" invisible="1"/>
<field name="state" widget="statusbar" statusbar_visible="draft,approved" />
</header>
</sheet>
<xpath expr="//field[@name='create_uid']/parent::group" position="after">
<group>
<field name="approved_uid" readonly="1" attrs="{'invisible':[('state','not in',['approved'])]}"/>
<field name="approved_date" readonly="1" attrs="{'invisible':[('state','not in',['approved'])]}"/>
</group>
</xpath>
<field name="content" position="attributes">
<attribute name="attrs">{'readonly': [('state', 'not in', ['draft'])]}</attribute>
</field>
<sheet position="after">
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</sheet>
</field>
</record>
<!-- Page Form View -->
<record id="wiki_form_inherit" model="ir.ui.view">
<field name="name">document.page.form</field>
<field name="model">document.page</field>
<field name="inherit_id"
ref="document_page.view_wiki_form" />
<field name="inherit_id" ref="document_page.view_wiki_form" />
<field name="arch" type="xml">
<field name="display_content"
position="before">
<group class="oe_read_only">
<field name="approved_date" />
<field name="approved_uid" />
</group>
<sheet position="before">
<div class="alert alert-info" role="alert" style="margin-bottom:0px;"
attrs="{'invisible': [('has_changes_pending_approval','=',False)]}">
This document has <b>Changes Pending Approval</b>. You are viewing the last approved content.
</div>
<div class="alert alert-warning oe_edit_only" role="alert" style="margin-bottom:0px;"
attrs="{'invisible': [('is_approval_required','=',False)]}">
This document requires approval. If edited, you will create a new <b>Change Request</b>.
</div>
<field name="is_approval_required" invisible="1"/>
<field name="has_changes_pending_approval" invisible="1"/>
</sheet>
<field name="type" position="before">
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" name="action_changes_pending_approval"
string="Change Requests" type="object"
attrs="{'invisible':[('has_changes_pending_approval','=',False)]}" icon="fa-edit"/>
</div>
</field>
<field name="content_uid" position="after">
<field name="approved_uid"/>
</field>
<field name="content_date" position="replace">
<field name="approved_date"/>
</field>
<field name="history_ids" position="inside">
<tree>
<field name="id"/>
<field name="approved_date"/>
<field name="summary"/>
<field name="create_uid"/>
<field name="approved_uid"/>
</tree>
</field>
</field>
</record>
<!-- Page Menu Form View -->
<record id="view_wiki_menu_form_inherit" model="ir.ui.view">
<field name="name">document.page.form</field>
<field name="model">document.page</field>
<field name="inherit_id"
ref="document_page.view_wiki_menu_form" />
<field name="inherit_id" ref="document_page.view_wiki_menu_form" />
<field name="arch" type="xml">
<field name="display_content"
position="before">
<group class="oe_read_only"
attrs="{'invisible':[('type','!=','content')]}">
<field name="content" position="before">
<group class="oe_read_only" attrs="{'invisible':[('type','!=','content')]}">
<field name="approved_date" />
<field name="approved_uid" />
</group>
@ -60,11 +112,11 @@
</field>
</record>
<!-- Catgory Form View -->
<record id="view_category_form_inherit" model="ir.ui.view">
<field name="name">document.page.category.form</field>
<field name="model">document.page</field>
<field name="inherit_id"
ref="document_page.view_category_form" />
<field name="inherit_id" ref="document_page.view_category_form" />
<field name="arch" type="xml">
<field name="parent_id" position="after">
<field name="approval_required"/>
@ -74,18 +126,63 @@
</field>
</record>
<!-- History Tree view -->
<record id="view_wiki_history_tree_inherit" model="ir.ui.view">
<field name="name">document.page.history.tree</field>
<field name="model">document.page.history</field>
<field name="inherit_id"
ref="document_page.view_wiki_history_tree" />
<field name="inherit_id" ref="document_page.view_wiki_history_tree"/>
<field name="arch" type="xml">
<field name="page_id" position="after">
<field name="state"
attrs="{'invisible':[('is_parent_approval_required','=',False)]}"/>
<field name="is_parent_approval_required"
invisible="1" />
</field>
<tree position="attributes">
<attribute name="decoration-info">state=='draft'</attribute>
<attribute name="decoration-primary">state=='to approve'</attribute>
<attribute name="decoration-muted">state=='cancelled'</attribute>
</tree>
<tree position="inside">
<field name="state"/>
<field name="approved_uid"/>
<field name="approved_date"/>
</tree>
</field>
</record>
<!-- History Search view -->
<record id="view_wiki_history_filter" model="ir.ui.view">
<field name="name">document.page.history.search</field>
<field name="model">document.page.history</field>
<field name="inherit_id" ref="document_page.view_wiki_history_filter"/>
<field name="arch" type="xml">
<field name="page_id" position="before">
<field name="state"/>
</field>
<field name="create_uid" position="after">
<filter name="draft" string="Draft" domain="[('state','=','draft')]"/>
<filter name="pending" string="Pending Approval" domain="[('state','=','to approve')]"/>
<filter name="approved" string="Approved" domain="[('state','=','approved')]"/>
<filter name="cancelled" string="Cancelled" domain="[('state','=','cancelled')]"/>
</field>
<filter name="group_author" position="before">
<filter name="group_state" string="State" context="{'group_by':'state'}" />
</filter>
<filter name="group_author" position="after">
<filter name="group_approver" string="Approver" context="{'group_by':'approved_uid'}" />
</filter>
</field>
</record>
<!-- Change Requests Action -->
<record model="ir.actions.act_window" id="action_change_requests">
<field name="name">Change Requests</field>
<field name="res_model">document.page.history</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="context">{'search_default_state':'to approve'}</field>
</record>
<menuitem id="menu_page_change_requests"
name="Change Requests"
parent="document_page.menu_wiki"
action="action_change_requests"
sequence="25"
groups="document_page.group_document_editor" />
</odoo>

View File

@ -6,33 +6,98 @@
<field name="on_create">True</field>
</record>
<record model="workflow.activity" id="act_approved">
<field name="wkf_id"
ref="wkf_document_page_history_aproval" />
<field name="name">approved</field>
<field name="kind">function</field>
<field name="action">page_approval_approved()</field>
<field name="flow_stop">True</field>
</record>
<record model="workflow.activity" id="act_draft">
<field name="wkf_id"
ref="wkf_document_page_history_aproval" />
<field name="wkf_id" ref="wkf_document_page_history_aproval" />
<field name="flow_start">True</field>
<field name="name">draft</field>
<field name="kind">function</field>
<field name="action">page_approval_draft()</field>
</record>
<record model="workflow.activity" id="act_to_approve">
<field name="wkf_id" ref="wkf_document_page_history_aproval" />
<field name="name">to approve</field>
<field name="kind">function</field>
<field name="action">page_approval_to_approve()</field>
</record>
<record model="workflow.activity" id="act_approved">
<field name="wkf_id" ref="wkf_document_page_history_aproval" />
<field name="name">approved</field>
<field name="kind">function</field>
<field name="action">page_approval_approved()</field>
<field name="flow_stop">True</field>
</record>
<record model="workflow.activity" id="act_cancelled">
<field name="wkf_id" ref="wkf_document_page_history_aproval" />
<field name="name">cancelled</field>
<field name="kind">function</field>
<field name="action">page_approval_cancelled()</field>
</record>
<!-- Transitions -->
<record model="workflow.transition" id="tdr">
<field name="act_from" ref="act_draft"/>
<field name="act_to" ref="act_to_approve"/>
<field name="condition">am_i_owner</field>
<field name="signal">page_approval_to_approve</field>
</record>
<record model="workflow.transition" id="tda">
<field name="act_from" ref="act_draft" />
<field name="act_to" ref="act_approved" />
<field name="act_from" ref="act_draft"/>
<field name="act_to" ref="act_approved"/>
<field name="condition">am_i_approver</field>
<field name="signal">page_approval_approve</field>
</record>
<record model="workflow.transition" id="tra">
<field name="act_from" ref="act_to_approve"/>
<field name="act_to" ref="act_approved"/>
<field name="condition">am_i_approver</field>
<field name="signal">page_approval_approve</field>
</record>
<record model="workflow.transition" id="tad">
<field name="act_from" ref="act_approved" />
<field name="act_to" ref="act_draft" />
<field name="act_from" ref="act_approved"/>
<field name="act_to" ref="act_draft"/>
<field name="condition">am_i_approver</field>
<field name="signal">edit</field>
</record>
<record model="workflow.transition" id="tdc">
<field name="act_from" ref="act_draft"/>
<field name="act_to" ref="act_cancelled"/>
<field name="condition">am_i_owner</field>
<field name="signal">page_approval_cancel</field>
</record>
<record model="workflow.transition" id="trc">
<field name="act_from" ref="act_to_approve"/>
<field name="act_to" ref="act_cancelled"/>
<field name="condition">am_i_owner or am_i_approver</field>
<field name="signal">page_approval_cancel</field>
</record>
<record model="workflow.transition" id="tcd">
<field name="act_from" ref="act_cancelled"/>
<field name="act_to" ref="act_draft"/>
<field name="condition">am_i_owner or am_i_approver</field>
<field name="signal">page_approval_reopen</field>
</record>
<!-- Automatic Transitions for change requests created directly from documents -->
<record model="workflow.transition" id="tda_auto">
<field name="act_from" ref="act_draft"/>
<field name="act_to" ref="act_approved"/>
<field name="condition">not is_approval_required</field>
<field name="signal">document_page_auto_confirm</field>
</record>
<record model="workflow.transition" id="tdr_auto">
<field name="act_from" ref="act_draft"/>
<field name="act_to" ref="act_to_approve"/>
<field name="condition">is_approval_required</field>
<field name="signal">document_page_auto_confirm</field>
</record>
</odoo>