From 731ed4d5bd6d8e28ed21e01f8052dc6ce8539c45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Gil=20Sorribes?= Date: Mon, 25 Feb 2019 11:34:23 +0100 Subject: [PATCH] [11.0][IMP] base_tier_validation fixup and extend tests [ADD] systray icon for pending reviews [FIX] Remove python safe_eval [ADD] base_tier_validation_formula and migration scripts [ADD] widget domain and python expression to define reviewer in tier definition [ADD] auto updating of systray icon counter [ADD] validation date field [ADD] review widget dropdown menu --- base_tier_validation/__manifest__.py | 10 +- .../migrations/11.0.1.2.0/post-migrate.py | 16 + base_tier_validation/models/__init__.py | 1 + base_tier_validation/models/res_users.py | 38 ++ .../models/tier_definition.py | 31 +- base_tier_validation/models/tier_review.py | 2 + .../models/tier_validation.py | 34 +- base_tier_validation/readme/CONTRIBUTORS.rst | 1 + .../static/src/js/review_widget.js | 58 +++ base_tier_validation/static/src/js/systray.js | 135 ++++++ .../static/src/less/review.less | 4 + .../static/src/less/systray.less | 113 +++++ .../static/src/xml/systray.xml | 41 ++ .../static/src/xml/tier_review_template.xml | 63 +++ base_tier_validation/tests/common.py | 6 + .../tests/test_tier_validation.py | 84 +++- .../tests/tier_validation_tester.py | 21 + base_tier_validation/views/assets_backend.xml | 13 + .../views/tier_definition_view.xml | 31 +- .../views/tier_review_view.xml | 4 +- base_tier_validation_formula/README.rst | 81 ++++ base_tier_validation_formula/__init__.py | 1 + base_tier_validation_formula/__manifest__.py | 20 + .../models/__init__.py | 3 + .../models/tier_definition.py | 37 ++ .../models/tier_review.py | 47 ++ .../models/tier_validation.py | 25 ++ .../readme/CONTRIBUTORS.rst | 2 + .../readme/DESCRIPTION.rst | 2 + base_tier_validation_formula/readme/USAGE.rst | 2 + .../static/description/icon.png | Bin 0 -> 9455 bytes .../static/description/index.html | 405 ++++++++++++++++++ .../tests/__init__.py | 4 + base_tier_validation_formula/tests/common.py | 19 + .../tests/test_tier_validation.py | 112 +++++ .../tests/tier_validation_tester.py | 23 + .../views/tier_definition_view.xml | 25 ++ 37 files changed, 1464 insertions(+), 50 deletions(-) create mode 100644 base_tier_validation/migrations/11.0.1.2.0/post-migrate.py create mode 100644 base_tier_validation/models/res_users.py create mode 100644 base_tier_validation/static/src/js/review_widget.js create mode 100644 base_tier_validation/static/src/js/systray.js create mode 100644 base_tier_validation/static/src/less/review.less create mode 100644 base_tier_validation/static/src/less/systray.less create mode 100644 base_tier_validation/static/src/xml/systray.xml create mode 100644 base_tier_validation/static/src/xml/tier_review_template.xml create mode 100644 base_tier_validation/views/assets_backend.xml create mode 100644 base_tier_validation_formula/README.rst create mode 100644 base_tier_validation_formula/__init__.py create mode 100644 base_tier_validation_formula/__manifest__.py create mode 100644 base_tier_validation_formula/models/__init__.py create mode 100644 base_tier_validation_formula/models/tier_definition.py create mode 100644 base_tier_validation_formula/models/tier_review.py create mode 100644 base_tier_validation_formula/models/tier_validation.py create mode 100644 base_tier_validation_formula/readme/CONTRIBUTORS.rst create mode 100644 base_tier_validation_formula/readme/DESCRIPTION.rst create mode 100644 base_tier_validation_formula/readme/USAGE.rst create mode 100644 base_tier_validation_formula/static/description/icon.png create mode 100644 base_tier_validation_formula/static/description/index.html create mode 100644 base_tier_validation_formula/tests/__init__.py create mode 100644 base_tier_validation_formula/tests/common.py create mode 100644 base_tier_validation_formula/tests/test_tier_validation.py create mode 100644 base_tier_validation_formula/tests/tier_validation_tester.py create mode 100644 base_tier_validation_formula/views/tier_definition_view.xml diff --git a/base_tier_validation/__manifest__.py b/base_tier_validation/__manifest__.py index a1b7c11..1f40cc9 100644 --- a/base_tier_validation/__manifest__.py +++ b/base_tier_validation/__manifest__.py @@ -3,7 +3,7 @@ { "name": "Base Tier Validation", "summary": "Implement a validation process based on tiers.", - "version": "12.0.1.0.0", + "version": "12.0.2.0.0", "development_status": "Mature", "maintainers": ['lreficent'], "category": "Tools", @@ -13,11 +13,17 @@ "application": False, "installable": True, "depends": [ - "base", + "web", + "bus", ], "data": [ "security/ir.model.access.csv", "views/tier_definition_view.xml", "views/tier_review_view.xml", + "views/assets_backend.xml", + ], + 'qweb': [ + 'static/src/xml/systray.xml', + 'static/src/xml/tier_review_template.xml', ], } diff --git a/base_tier_validation/migrations/11.0.1.2.0/post-migrate.py b/base_tier_validation/migrations/11.0.1.2.0/post-migrate.py new file mode 100644 index 0000000..32d5222 --- /dev/null +++ b/base_tier_validation/migrations/11.0.1.2.0/post-migrate.py @@ -0,0 +1,16 @@ +# Copyright 2019 Creu Blanca +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from openupgradelib.openupgrade import migrate + + +@migrate() +def migrate(env, version): + module_ids = env['ir.module.module'].search([ + ('name', '=', 'base_tier_validation_formula'), + ('state', '=', 'uninstalled') + ]) + if module_ids: + module_ids.sudo().button_install() + cr = env.cr + cr.execute("UPDATE tier_definition SET definition_type = 'formula'") diff --git a/base_tier_validation/models/__init__.py b/base_tier_validation/models/__init__.py index 2c43513..4bfc7ac 100644 --- a/base_tier_validation/models/__init__.py +++ b/base_tier_validation/models/__init__.py @@ -3,3 +3,4 @@ from . import tier_definition from . import tier_review from . import tier_validation +from . import res_users diff --git a/base_tier_validation/models/res_users.py b/base_tier_validation/models/res_users.py new file mode 100644 index 0000000..930845d --- /dev/null +++ b/base_tier_validation/models/res_users.py @@ -0,0 +1,38 @@ +# Copyright 2019 Eficent Business and IT Consulting Services S.L. +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models, modules + + +class Users(models.Model): + _inherit = 'res.users' + + review_ids = fields.Many2many( + string="Reviews", comodel_name="tier.review" + ) + + @api.model + def review_user_count(self): + user_reviews = {} + to_review_docs = {} + for review in self.env.user.review_ids.filtered( + lambda r: r.status == 'pending'): + record = review.env[review.model].browse(review.res_id) + if not user_reviews.get(review['model']): + user_reviews[review.model] = { + 'name': record._description, + 'model': review.model, + 'icon': modules.module.get_module_icon( + self.env[review.model]._original_module), + 'pending_count': 0 + } + docs = to_review_docs.get(review.model) + if (docs and record not in docs) or not docs: + user_reviews[review.model]['pending_count'] += 1 + to_review_docs.setdefault(review.model, []).append(record) + return list(user_reviews.values()) + + @api.model + def get_reviews(self, data): + return self.env['tier.review'].search_read( + [('id', 'in', data.get('res_ids'))]) diff --git a/base_tier_validation/models/tier_definition.py b/base_tier_validation/models/tier_definition.py index 2dcc27f..d49cacb 100644 --- a/base_tier_validation/models/tier_definition.py +++ b/base_tier_validation/models/tier_definition.py @@ -7,13 +7,18 @@ from odoo import api, fields, models class TierDefinition(models.Model): _name = "tier.definition" _description = "Tier Definition" - _rec_name = "model_id" + + @api.model + def _get_default_name(self): + return "New Tier Validation" @api.model def _get_tier_validation_model_names(self): res = [] return res + name = fields.Char( + 'Description', required=True, default=_get_default_name) model_id = fields.Many2one( comodel_name="ir.model", string="Referenced Model", @@ -23,8 +28,10 @@ class TierDefinition(models.Model): ) review_type = fields.Selection( string="Validated by", default="individual", - selection=[("individual", "Specific user"), - ("group", "Any user in a specific group.")] + selection=[ + ("individual", "Specific user"), + ("group", "Any user in a specific group."), + ] ) reviewer_id = fields.Many2one( comodel_name="res.users", string="Reviewer", @@ -32,13 +39,14 @@ class TierDefinition(models.Model): reviewer_group_id = fields.Many2one( comodel_name="res.groups", string="Reviewer group", ) - python_code = fields.Text( - string='Tier Definition Expression', - help="Write Python code that defines when this tier confirmation " - "will be needed. The result of executing the expresion must be " - "a boolean.", - default="""# Available locals:\n# - rec: current record""", + definition_type = fields.Selection( + string="Definition", + selection=[ + ('domain', 'Domain'), + ], + default='domain', ) + definition_domain = fields.Char() active = fields.Boolean(default=True) sequence = fields.Integer(default=30) company_id = fields.Many2one( @@ -52,3 +60,8 @@ class TierDefinition(models.Model): return {'domain': { 'model_id': [ ('model', 'in', self._get_tier_validation_model_names())]}} + + @api.onchange('review_type') + def onchange_review_type(self): + self.reviewer_id = None + self.reviewer_group_id = None diff --git a/base_tier_validation/models/tier_review.py b/base_tier_validation/models/tier_review.py index 7e0b3a3..f86749b 100644 --- a/base_tier_validation/models/tier_review.py +++ b/base_tier_validation/models/tier_review.py @@ -8,6 +8,7 @@ class TierReview(models.Model): _name = "tier.review" _description = "Tier Review" + name = fields.Char(related="definition_id.name", readonly=True) status = fields.Selection( selection=[("pending", "Pending"), ("rejected", "Rejected"), @@ -39,6 +40,7 @@ class TierReview(models.Model): requested_by = fields.Many2one( comodel_name="res.users", ) + reviewed_date = fields.Datetime(string='Validation Date') @api.multi @api.depends('reviewer_id', 'reviewer_group_id', 'reviewer_group_id.users') diff --git a/base_tier_validation/models/tier_validation.py b/base_tier_validation/models/tier_validation.py index 27c6aeb..e2c84af 100644 --- a/base_tier_validation/models/tier_validation.py +++ b/base_tier_validation/models/tier_validation.py @@ -2,8 +2,8 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import api, fields, models, _ -from odoo.exceptions import ValidationError, UserError -from odoo.tools.safe_eval import safe_eval +from odoo.exceptions import ValidationError +from ast import literal_eval class TierValidation(models.AbstractModel): @@ -23,6 +23,10 @@ class TierValidation(models.AbstractModel): domain=lambda self: [('model', '=', self._name)], auto_join=True, ) + review_ids_dropdown = fields.One2many( + related='review_ids', + help="Field needed to display the dropdown menu correctly" + ) validated = fields.Boolean( compute="_compute_validated_rejected", search="_search_validated", @@ -94,12 +98,10 @@ class TierValidation(models.AbstractModel): @api.multi def evaluate_tier(self, tier): - try: - res = safe_eval(tier.python_code, globals_dict={'rec': self}) - except Exception as error: - raise UserError(_( - "Error evaluating tier validation conditions.\n %s") % error) - return res + domain = [] + if tier.definition_domain: + domain = literal_eval(tier.definition_domain) + return self.search([('id', '=', self.id)] + domain) @api.model def _get_under_validation_exceptions(self): @@ -147,12 +149,13 @@ class TierValidation(models.AbstractModel): tier_reviews = tiers or self.review_ids user_reviews = tier_reviews.filtered( lambda r: r.status in ('pending', 'rejected') and - (r.reviewer_id == self.env.user or - r.reviewer_group_id in self.env.user.groups_id)) + (self.env.user in r.reviewer_ids)) user_reviews.write({ 'status': 'approved', 'done_by': self.env.user.id, + 'reviewed_date': fields.Datetime.now(), }) + # TODO: add message_post @api.multi def validate_tier(self): @@ -169,7 +172,9 @@ class TierValidation(models.AbstractModel): user_reviews.write({ 'status': 'rejected', 'done_by': self.env.user.id, + 'reviewed_date': fields.Datetime.now(), }) + # TODO: Add Message_post @api.multi def request_validation(self): @@ -191,7 +196,7 @@ class TierValidation(models.AbstractModel): 'sequence': sequence, 'requested_by': self.env.uid, }) - # TODO: notify? post some msg in chatter? + self._update_counter() return created_trs @api.multi @@ -199,3 +204,10 @@ class TierValidation(models.AbstractModel): for rec in self: if getattr(rec, self._state_field) in self._state_from: rec.mapped('review_ids').unlink() + self._update_counter() + + def _update_counter(self): + notifications = [] + channel = 'base.tier.validation' + notifications.append([channel, {}]) + self.env['bus.bus'].sendmany(notifications) diff --git a/base_tier_validation/readme/CONTRIBUTORS.rst b/base_tier_validation/readme/CONTRIBUTORS.rst index c228335..98446a0 100644 --- a/base_tier_validation/readme/CONTRIBUTORS.rst +++ b/base_tier_validation/readme/CONTRIBUTORS.rst @@ -1,2 +1,3 @@ * Lois Rilo * Naglis Jonaitis +* Adrià Gil Sorribes diff --git a/base_tier_validation/static/src/js/review_widget.js b/base_tier_validation/static/src/js/review_widget.js new file mode 100644 index 0000000..cdc454e --- /dev/null +++ b/base_tier_validation/static/src/js/review_widget.js @@ -0,0 +1,58 @@ +odoo.define('base_tier_validation.ReviewField', function (require) { + "use strict"; + + var AbstractField = require('web.AbstractField'); + var core = require('web.core'); + var session = require('web.session'); + var field_registry = require('web.field_registry'); + var Widget = require('web.Widget'); + + var _t = core._t; + var QWeb = core.qweb; + + var ReviewField = AbstractField.extend({ + template: 'tier.review.ReviewPopUp', + events: { + 'click .o_info_btn': '_onButtonClicked', + }, + start: function () { + var self = this; + console.log(self) + + }, + /** + * Make RPC and get current user's activity details + * @private + */ + _getReviewData: function(res_ids){ + var self = this; + + return self._rpc({ + model: 'res.users', + method: 'get_reviews', + args: [res_ids], + }).then(function (data) { + self.reviews = data; + }); + }, + _renderDropdown: function () { + var self = this; + return this._getReviewData(self.value).then(function (){ + self.$('.o_review').html(QWeb.render("tier.review.ReviewDropDown", { + reviews : self.reviews + })); + }); + }, + _onButtonClicked: function (event) { + event.preventDefault(); + if (!this.$el.hasClass('open')) { + this._renderDropdown(); + } + }, + }); + + field_registry.add('review_popup', ReviewField); + + return ReviewField; + + }); \ No newline at end of file diff --git a/base_tier_validation/static/src/js/systray.js b/base_tier_validation/static/src/js/systray.js new file mode 100644 index 0000000..9b658e0 --- /dev/null +++ b/base_tier_validation/static/src/js/systray.js @@ -0,0 +1,135 @@ +odoo.define('tier_validation.systray', function (require) { + "use strict"; + + var config = require('web.config'); + var core = require('web.core'); + var session = require('web.session'); + var SystrayMenu = require('web.SystrayMenu'); + var Widget = require('web.Widget'); + var bus = require('bus.bus').bus; + + var chat_manager = require('mail.chat_manager'); + + var QWeb = core.qweb; + + var ReviewMenu = Widget.extend({ + template:'tier.validation.ReviewMenu', + events: { + "click": "_onReviewMenuClick", + "click .o_mail_channel_preview": "_onReviewFilterClick", + }, + start: function () { + this.$reviews_preview = this.$('.o_mail_navbar_dropdown_channels'); + this._updateReviewPreview(); + var channel = 'base.tier.validation'; + bus.add_channel(channel); + bus.on('notification', this, this._updateReviewPreview); + return this._super(); + }, + + // Private + + /** + * Make RPC and get current user's activity details + * @private + */ + _getReviewData: function(){ + var self = this; + + return self._rpc({ + model: 'res.users', + method: 'review_user_count', + kwargs: { + context: session.user_context, + }, + }).then(function (data) { + self.reviews = data; + self.reviewCounter = _.reduce(data, function(total_count, p_data){ return total_count + p_data.pending_count; }, 0); + self.$('.o_notification_counter').text(self.reviewCounter); + self.$el.toggleClass('o_no_notification', !self.reviewCounter); + }); + }, + /** + * Check wether activity systray dropdown is open or not + * @private + * @returns {boolean} + */ + _isOpen: function () { + return this.$el.hasClass('open'); + }, + /** + * Update(render) activity system tray view on activity updation. + * @private + */ + _updateReviewPreview: function () { + var self = this; + self._getReviewData().then(function (){ + self.$reviews_preview.html(QWeb.render('tier.validation.ReviewMenuPreview', { + reviews : self.reviews + })); + }); + }, + /** + * update counter based on activity status(created or Done) + * @private + * @param {Object} [data] key, value to decide activity created or deleted + * @param {String} [data.type] notification type + * @param {Boolean} [data.activity_deleted] when activity deleted + * @param {Boolean} [data.activity_created] when activity created + */ + _updateCounter: function (data) { + if (data) { + if (data.review_created) { + this.reviewCounter ++; + } + if (data.review_deleted && this.reviewCounter > 0) { + this.reviewCounter --; + } + this.$('.o_notification_counter').text(this.reviewCounter); + this.$el.toggleClass('o_no_notification', !this.reviewCounter); + } + }, + + + // Handlers + + /** + * Redirect to particular model view + * @private + * @param {MouseEvent} event + */ + _onReviewFilterClick: function (event) { + // fetch the data from the button otherwise fetch the ones from the parent (.o_tier_channel_preview). + var data = _.extend({}, $(event.currentTarget).data(), $(event.target).data()); + var context = {}; + this.do_action({ + type: 'ir.actions.act_window', + name: data.model_name, + res_model: data.res_model, + views: [[false, 'list'], [false, 'form']], + search_view_id: [false], + domain: [['review_ids.reviewer_ids', '=', session.uid], + ['review_ids.status', '=', 'pending']], + context:context, + }); + }, + /** + * When menu clicked update activity preview if counter updated + * @private + * @param {MouseEvent} event + */ + _onReviewMenuClick: function () { + if (!this._isOpen()) { + this._updateReviewPreview(); + } + }, + + }); + + SystrayMenu.Items.push(ReviewMenu); + + // to test activity menu in qunit test cases we need it + return { + ReviewMenu: ReviewMenu, + }; +}); \ No newline at end of file diff --git a/base_tier_validation/static/src/less/review.less b/base_tier_validation/static/src/less/review.less new file mode 100644 index 0000000..e47e821 --- /dev/null +++ b/base_tier_validation/static/src/less/review.less @@ -0,0 +1,4 @@ +ul.o_review { + min-width: 600px; + max-width: 800px +} \ No newline at end of file diff --git a/base_tier_validation/static/src/less/systray.less b/base_tier_validation/static/src/less/systray.less new file mode 100644 index 0000000..24a43f5 --- /dev/null +++ b/base_tier_validation/static/src/less/systray.less @@ -0,0 +1,113 @@ +// Navbar icon and dropdown +.o_tier_navbar_item { + > a { + opacity: 1; + > i { + font-size: larger; + } + } + &.o_no_notification > a { + opacity: 0.5; + > i { + .o-transform(translateY(0px)); + } + .o_notification_counter { + display: none; + } + } + &.open .o_tier_navbar_dropdown { + .o-flex-display(); + .o-flex-flow(column, nowrap); + } + .o_notification_counter { + .o-position-absolute(@top: 20%, @right: 1px); + background: @odoo-brand-optional; + color: white; + padding: 0em 0.3em; + font-size: 0.7em; + } + .o_tier_navbar_dropdown { + width: 350px; + padding: 0; + + .o_spinner { + .o-flex-display(); + .o-align-items(center); + .o-justify-content(center); + color: @odoo-main-text-color; + height: 50px; + } + + .o_tier_navbar_dropdown_channels { + .o-flex(0, 1, auto); + max-height: 400px; + min-height: 50px; + overflow-y: auto; + + @media (min-width: @screen-sm-min) { + .o_tier_channel_preview { + height: 50px; + padding: 5px; + .o_tier_channel_image { + width: 40px; + } + .o_channel_info { + margin-left: 10px; + .o_channel_title { + .o_last_message_date { + padding-top: 2px; + font-size: x-small; + margin-left: 10px; + } + } + } + } + } + } + .o_no_review { + cursor: initial; + .o-align-items(center); + color: grey; + opacity: 0.5; + padding: 3px; + } + } +} + +.o_no_chat_window .o_tier_navbar_dropdown .o_new_message { + display: none; // hide 'new message' button if chat windows are disabled +} + +// Mobile rules +// Goal: mock the design of Discuss in mobile +@media (max-width: @screen-xs-max) { + .o_tier_navbar_item { + .o_notification_counter { + top: 10%; + } + .o_tier_navbar_dropdown { + position: relative; + .o_tier_navbar_dropdown_top { + padding: 5px; + } + .o_tier_navbar_mobile_header { + padding: 5px; + height: 44px; + border-bottom: 1px solid #ebebeb; + box-shadow: 0 0 2px @gray-lighter-darker; + } + .o_tier_navbar_dropdown_channels { + max-height: none; + padding-bottom: 52px; // leave space for tabs + } + .o_tier_mobile_tabs { + position: fixed; + bottom: 0px; + left: 0px; + right: 0px; + background-color: white; + color: @odoo-main-text-color; + } + } + } +} diff --git a/base_tier_validation/static/src/xml/systray.xml b/base_tier_validation/static/src/xml/systray.xml new file mode 100644 index 0000000..55962d1 --- /dev/null +++ b/base_tier_validation/static/src/xml/systray.xml @@ -0,0 +1,41 @@ + + + + + +
  • + No reviews to do. +
  • +
    + +
    +
    + +
    +
    +
    + + + +
    +
    + + 0 Pending +
    +
    +
    +
    +
    + + +
  • + + +
  • +
    + +
    diff --git a/base_tier_validation/static/src/xml/tier_review_template.xml b/base_tier_validation/static/src/xml/tier_review_template.xml new file mode 100644 index 0000000..0154a8e --- /dev/null +++ b/base_tier_validation/static/src/xml/tier_review_template.xml @@ -0,0 +1,63 @@ + + + + +