diff --git a/document_page/__openerp__.py b/document_page/__openerp__.py index 8c129f3e..a358f00a 100644 --- a/document_page/__openerp__.py +++ b/document_page/__openerp__.py @@ -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', ], diff --git a/document_page/data/document_page.xml b/document_page/data/document_page.xml deleted file mode 100644 index 9cd3c0dd..00000000 --- a/document_page/data/document_page.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - The Odoo wiki - help, quick start, wiki, formatting - 0 - 1 - Initial Page - == 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] - - - - - - - diff --git a/document_page/demo/document_page.xml b/document_page/demo/document_page.xml index 8e6d2eca..c9e0a042 100644 --- a/document_page/demo/document_page.xml +++ b/document_page/demo/document_page.xml @@ -8,7 +8,7 @@ OpenERP Features category - + Summary of the feature Long explanation diff --git a/document_page/migrations/9.0.2.0.0/post-migration.py b/document_page/migrations/9.0.2.0.0/post-migration.py new file mode 100644 index 00000000..f590bbaa --- /dev/null +++ b/document_page/migrations/9.0.2.0.0/post-migration.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Ivan Todorovich +# 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' + """) diff --git a/document_page/models/document_page.py b/document_page/models/document_page.py index ed7fc35c..1d8d63fe 100644 --- a/document_page/models/document_page.py +++ b/document_page/models/document_page.py @@ -18,10 +18,8 @@ # along with this program. If not, see . # ############################################################################## -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 += ["
  • " + self._get_page_index(subpage) + - "
  • "] + for subpage in self.child_ids: + index += ["
  • " + subpage._get_page_index() + "
  • "] r = '' if link: - r = '%s' % (page.id, page.name) + r = '%s' % (self.id, self.name) if index: r += "
      " + "".join(index) + "
    " 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 = '


    ' - @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 == '


    ': + if self.parent_id and self.parent_id.type == "category": + self.content = self.parent_id.template diff --git a/document_page/models/document_page_history.py b/document_page/models/document_page_history.py index f296c094..71535701 100644 --- a/document_page/models/document_page_history.py +++ b/document_page/models/document_page_history.py @@ -18,11 +18,8 @@ # along with this program. If not, see . # ############################################################################## -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('

    ', '

    \r\n

    ') + text2 = text2.replace('

    ', '

    \r\n

    ') + 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 diff --git a/document_page/security/document_page_security.xml b/document_page/security/document_page_security.xml index 6834b69b..07716365 100644 --- a/document_page/security/document_page_security.xml +++ b/document_page/security/document_page_security.xml @@ -1,8 +1,24 @@ + + + User (Read only) + + + + Editor + + + + + + Manager + + + diff --git a/document_page/security/ir.model.access.csv b/document_page/security/ir.model.access.csv index 13f0479f..61f5869d 100644 --- a/document_page/security/ir.model.access.csv +++ b/document_page/security/ir.model.access.csv @@ -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 diff --git a/document_page/static/src/css/document_page.css b/document_page/static/src/css/document_page.css index ddbbcffd..00a6336c 100644 --- a/document_page/static/src/css/document_page.css +++ b/document_page/static/src/css/document_page.css @@ -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 +} diff --git a/document_page/tests/__init__.py b/document_page/tests/__init__.py index 4c507a03..14e2fa0b 100644 --- a/document_page/tests/__init__.py +++ b/document_page/tests/__init__.py @@ -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 diff --git a/document_page/tests/test_document_page.py b/document_page/tests/test_document_page.py index 9c3b4d72..0b627cba 100644 --- a/document_page/tests/test_document_page.py +++ b/document_page/tests/test_document_page.py @@ -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) diff --git a/document_page/tests/test_document_page_history.py b/document_page/tests/test_document_page_history.py deleted file mode 100644 index ced210c1..00000000 --- a/document_page/tests/test_document_page_history.py +++ /dev/null @@ -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.') diff --git a/document_page/tests/test_document_page_show_diff.py b/document_page/tests/test_document_page_show_diff.py index a43f7bc1..9498a951 100644 --- a/document_page/tests/test_document_page_show_diff.py +++ b/document_page/tests/test_document_page_show_diff.py @@ -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) diff --git a/document_page/views/document_page.xml b/document_page/views/document_page.xml index 06e4f19a..deb2810e 100644 --- a/document_page/views/document_page.xml +++ b/document_page/views/document_page.xml @@ -1,10 +1,7 @@ - + @@ -15,8 +12,8 @@ - - + + @@ -30,8 +27,8 @@ - - + + @@ -47,31 +44,32 @@

    - + - - - + + + - - - -
    - -
    + + + + + + + + + + + + + +
    @@ -88,12 +86,7 @@

    -
    - -
    + @@ -104,90 +97,19 @@ document.page - - + + + - - - + + + - - - document.page.category.form - document.page - -
    - - -

    - - - - - - - - - - - - -
    -
    - -
    - -
    - -
    -
    -
    -
    -
    - - -
    -
    -
    -
    - - - document.page.category.tree - document.page - - - - - - - - - - @@ -205,103 +127,35 @@

    + tree + form + + + + + + + name="Pages" + parent="menu_wiki" + action="action_page" + sequence="10" /> - - Category - document.page - [('type','=','category')] - {'default_type': 'category'} - form - tree,form - - - - - - tree - - - - - - form - - - - - - - - document.page.history.tree - document.page.history - - - - - - - - - - - document.page.history.form - document.page.history - -
    -
    - - - - Page history - document.page.history - form - tree,form - - - - - + + + + + diff --git a/document_page/views/document_page_category.xml b/document_page/views/document_page_category.xml new file mode 100755 index 00000000..c861d5b6 --- /dev/null +++ b/document_page/views/document_page_category.xml @@ -0,0 +1,90 @@ + + + + + + + document.page.category.form + document.page + +
    + + +

    + + + + + + + + + + + + + + + + + + +
    +
    + + +
    +
    +
    +
    + + + document.page.category.tree + document.page + + + + + + + + + + + + + + + Category + document.page + [('type','=','category')] + {'default_type': 'category'} + form + tree,form + + + + + + + tree + + + + + + + form + + + + + + + +
    +
    diff --git a/document_page/views/document_page_history.xml b/document_page/views/document_page_history.xml new file mode 100755 index 00000000..1112e103 --- /dev/null +++ b/document_page/views/document_page_history.xml @@ -0,0 +1,90 @@ + + + + + + + document.page.history.tree + document.page.history + + + + + + + + + + + + + + document.page.history.search + document.page.history + + + + + + + + + + + + + + + document.page.history.form + document.page.history + +
    + +

    + + + + + + + + + + + + + +
    +
    +
    +
    + + + + Page history + document.page.history + form + tree,form + + + + + + + +
    +
    diff --git a/document_page/views/report_document_page.xml b/document_page/views/report_document_page.xml new file mode 100755 index 00000000..6882e7de --- /dev/null +++ b/document_page/views/report_document_page.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + diff --git a/document_page/wizard/document_page_show_diff.py b/document_page/wizard/document_page_show_diff.py index db42214d..84900e96 100644 --- a/document_page/wizard/document_page_show_diff.py +++ b/document_page/wizard/document_page_show_diff.py @@ -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, ) diff --git a/document_page/wizard/document_page_show_diff.xml b/document_page/wizard/document_page_show_diff.xml index 4499769f..8214be66 100644 --- a/document_page/wizard/document_page_show_diff.xml +++ b/document_page/wizard/document_page_show_diff.xml @@ -8,13 +8,9 @@ wizard.document.page.history.show_diff
    - +
    -
    diff --git a/document_page_approval/__init__.py b/document_page_approval/__init__.py index 7f626589..de09c2f0 100644 --- a/document_page_approval/__init__.py +++ b/document_page_approval/__init__.py @@ -19,3 +19,4 @@ # ############################################################################## from . import models +from .hooks import post_init_hook, uninstall_hook diff --git a/document_page_approval/__openerp__.py b/document_page_approval/__openerp__.py index 1e95feb3..114a9fd5 100644 --- a/document_page_approval/__openerp__.py +++ b/document_page_approval/__openerp__.py @@ -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', diff --git a/document_page_approval/data/email_template.xml b/document_page_approval/data/email_template.xml index e31c8bcb..08bb782e 100644 --- a/document_page_approval/data/email_template.xml +++ b/document_page_approval/data/email_template.xml @@ -1,15 +1,13 @@ - - - + + Automated new draft need approval Notification Mail ${object.create_uid.company_id.email or 'noreply@localhost.com'} - New version of "${object.page_id.name}" to approve - ${object.get_approvers_email} + New version of ${object.display_name} needs your approval ${object.create_uid.partner_id.lang} @@ -17,13 +15,29 @@ Hello,

    -

    The page "${object.page_id.name}" has been modified and need your approval.

    +

    ${object.create_uid.name} submited a new Change Request for ${object.page_id.name} and it needs your approval.

    -

    You can review the new version here : ${object.get_page_url}

    +

    ${object.display_name}

    +

    + Modified by: ${object.create_uid.name}
    + Date: ${object.create_date}
    +

    + +% if object.summary: +

    Summary

    +

    ${object.summary}

    +% endif + +

    Diff

    +
    +${object.diff|safe} +
    + +

    Have a great day.

    -

    Have a great day.
    --
    -Odoo

    ]]> +

    Odoo

    + ]]>
    diff --git a/document_page_approval/hooks.py b/document_page_approval/hooks.py new file mode 100644 index 00000000..7cd62e97 --- /dev/null +++ b/document_page_approval/hooks.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Ivan Todorovich () +# 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'" + ) diff --git a/document_page_approval/migrations/9.0.2.0.0/post-migration.py b/document_page_approval/migrations/9.0.2.0.0/post-migration.py new file mode 100644 index 00000000..9c04167c --- /dev/null +++ b/document_page_approval/migrations/9.0.2.0.0/post-migration.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Ivan Todorovich +# 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 + """) diff --git a/document_page_approval/models/document_page_approval.py b/document_page_approval/models/document_page_approval.py index 278f5227..6e00fe3d 100644 --- a/document_page_approval/models/document_page_approval.py +++ b/document_page_approval/models/document_page_approval.py @@ -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 diff --git a/document_page_approval/models/document_page_history_workflow.py b/document_page_approval/models/document_page_history_workflow.py index 2eeb7e91..e66e05e1 100644 --- a/document_page_approval/models/document_page_history_workflow.py +++ b/document_page_approval/models/document_page_history_workflow.py @@ -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 %s 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) diff --git a/document_page_approval/security/document_page_security.xml b/document_page_approval/security/document_page_security.xml index d41d96f8..3618a677 100644 --- a/document_page_approval/security/document_page_security.xml +++ b/document_page_approval/security/document_page_security.xml @@ -1,9 +1,16 @@ + - Document approver - + Approver + + + + + + + diff --git a/document_page_approval/security/ir.model.access.csv b/document_page_approval/security/ir.model.access.csv index f8e75f15..97dd8b91 100644 --- a/document_page_approval/security/ir.model.access.csv +++ b/document_page_approval/security/ir.model.access.csv @@ -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 diff --git a/document_page_approval/tests/__init__.py b/document_page_approval/tests/__init__.py index 2413da81..30782b8b 100644 --- a/document_page_approval/tests/__init__.py +++ b/document_page_approval/tests/__init__.py @@ -1,2 +1,2 @@ # -*- coding: utf-8 -*- -from . import test_document_page_approval, test_document_page_history_workflow +from . import test_document_page_approval diff --git a/document_page_approval/tests/test_document_page_approval.py b/document_page_approval/tests/test_document_page_approval.py index 4c0e8457..f418fd5d 100644 --- a/document_page_approval/tests/test_document_page_approval.py +++ b/document_page_approval/tests/test_document_page_approval.py @@ -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) diff --git a/document_page_approval/tests/test_document_page_history_workflow.py b/document_page_approval/tests/test_document_page_history_workflow.py deleted file mode 100644 index 508aff80..00000000 --- a/document_page_approval/tests/test_document_page_history_workflow.py +++ /dev/null @@ -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) diff --git a/document_page_approval/views/document_page_approval.xml b/document_page_approval/views/document_page_approval.xml index 6b0e5c91..60a8176e 100644 --- a/document_page_approval/views/document_page_approval.xml +++ b/document_page_approval/views/document_page_approval.xml @@ -1,58 +1,110 @@ + + document.page.history.form document.page.history - + - -
    - -
    + + + + + + + + {'readonly': [('state', 'not in', ['draft'])]} + + +
    + + +
    +
    + document.page.form document.page - + - - - - - + + + + + + + + + +
    +
    + + + + + + + + + + + + + + + + + + +
    + document.page.form document.page - + - - + + @@ -60,11 +112,11 @@ + document.page.category.form document.page - + @@ -74,18 +126,63 @@ + document.page.history.tree document.page.history - + - - - - + + state=='draft' + state=='to approve' + state=='cancelled' + + + + + + + + + + + document.page.history.search + document.page.history + + + + + + + + + + + + + + + + + + + + + + + Change Requests + document.page.history + form + tree,form + {'search_default_state':'to approve'} + + +
    diff --git a/document_page_approval/workflows/document_page_approval.xml b/document_page_approval/workflows/document_page_approval.xml index ea8a254b..81a5ba85 100644 --- a/document_page_approval/workflows/document_page_approval.xml +++ b/document_page_approval/workflows/document_page_approval.xml @@ -6,33 +6,98 @@ True - - - approved - function - page_approval_approved() - True - - - + True draft function page_approval_draft() + + + to approve + function + page_approval_to_approve() + + + + + approved + function + page_approval_approved() + True + + + + + cancelled + function + page_approval_cancelled() + + + + + + + am_i_owner + page_approval_to_approve + + - - + + + am_i_approver + page_approval_approve + + + + + + am_i_approver page_approval_approve - - + + + am_i_approver edit + + + + + am_i_owner + page_approval_cancel + + + + + + am_i_owner or am_i_approver + page_approval_cancel + + + + + + am_i_owner or am_i_approver + page_approval_reopen + + + + + + + not is_approval_required + document_page_auto_confirm + + + + + + is_approval_required + document_page_auto_confirm +