From 51b36c3dbbce04ba6d459c8890f82ac3fdbd91bd Mon Sep 17 00:00:00 2001 From: "Xavier Bol (xbo)" Date: Wed, 20 Nov 2019 12:54:35 +0000 Subject: [PATCH] [IMP] base: add default image for users + placeholder if unassigned 1/ When the new database is created without demo data, the admin has a 'silhouette' as a default picture. When a new user is created without picture given by the current user, the new user will have a 'silhouette' as a default profile picture. 2/ web: image for fa-user-slash. This image will be used when a record is unassigned. 3/ web, *: Change placeholder by default when record is unassigned. We want to have a fa-user-slash icon when a record is unassigned instead of 'placeholder.png'. A method is created in the BaseModel to have a generic method to change easily the placeholder for other models. 4/ Adapt kanban test to keep the same behaviour. Attention the behaviour is a bit different. Because, now the default image is given by the server to change easily the default image when a record doesn't have an image. Thus, we don't say if it's the default placeholder, but we can say it's not the same image of the record (in this test, the record, it's the partner). 5/ misc: display 'Unassigned' in the hover on kanban cards if record is unassigned closes odoo/odoo#41356 Taskid: 2060206 Related: odoo/enterprise#7758 Signed-off-by: Yannick Tivisse (yti) Co-authored-by: jdoutreloux Co-authored-by: Yannick Tivisse --- .../views/account_analytic_default_view.xml | 3 +- addons/crm/views/crm_lead_views.xml | 6 ++-- .../views/hr_recruitment_views.xml | 3 +- .../maintenance/views/maintenance_views.xml | 3 +- .../views/mailing_mailing_views.xml | 3 +- addons/project/views/project_views.xml | 6 ++-- .../views/purchase_requisition_views.xml | 3 +- .../views/stock_picking_batch_views.xml | 3 +- addons/web/controllers/main.py | 29 +++++++++++++----- .../src/js/views/kanban/kanban_record.js | 5 ++- addons/web/static/tests/views/kanban_tests.js | 9 +++++- .../views/slide_slide_views.xml | 3 +- odoo/addons/base/data/res_users_data.xml | 3 +- odoo/addons/base/models/res_users.py | 23 +++++++++++++- odoo/addons/base/static/img/user-slash.png | Bin 0 -> 4703 bytes odoo/models.py | 7 +++++ 16 files changed, 84 insertions(+), 25 deletions(-) create mode 100644 odoo/addons/base/static/img/user-slash.png diff --git a/addons/account_analytic_default/views/account_analytic_default_view.xml b/addons/account_analytic_default/views/account_analytic_default_view.xml index 805ec5250ca..bddeeaf80eb 100644 --- a/addons/account_analytic_default/views/account_analytic_default_view.xml +++ b/addons/account_analytic_default/views/account_analytic_default_view.xml @@ -59,7 +59,8 @@
- + +
From to
Product
diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index aad23b2abb9..469661290ee 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -377,7 +377,8 @@
- + +
@@ -508,7 +509,8 @@
- + +
diff --git a/addons/hr_recruitment/views/hr_recruitment_views.xml b/addons/hr_recruitment/views/hr_recruitment_views.xml index 15f9c723a90..ea11d7f5e0e 100644 --- a/addons/hr_recruitment/views/hr_recruitment_views.xml +++ b/addons/hr_recruitment/views/hr_recruitment_views.xml @@ -329,7 +329,8 @@ - + + diff --git a/addons/maintenance/views/maintenance_views.xml b/addons/maintenance/views/maintenance_views.xml index 17976dea62f..86209c2d090 100644 --- a/addons/maintenance/views/maintenance_views.xml +++ b/addons/maintenance/views/maintenance_views.xml @@ -164,7 +164,8 @@
- + +
diff --git a/addons/mass_mailing/views/mailing_mailing_views.xml b/addons/mass_mailing/views/mailing_mailing_views.xml index 5e8cfdb390e..c6fc0a3dc29 100644 --- a/addons/mass_mailing/views/mailing_mailing_views.xml +++ b/addons/mass_mailing/views/mailing_mailing_views.xml @@ -351,7 +351,8 @@ Mailing Contact - + + diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index a42c64e6c05..47573ae7024 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -256,7 +256,8 @@
- + +
@@ -660,7 +661,8 @@
- + +
diff --git a/addons/purchase_requisition/views/purchase_requisition_views.xml b/addons/purchase_requisition/views/purchase_requisition_views.xml index 265a747b23f..3a18208b13e 100644 --- a/addons/purchase_requisition/views/purchase_requisition_views.xml +++ b/addons/purchase_requisition/views/purchase_requisition_views.xml @@ -244,7 +244,8 @@
- + +
diff --git a/addons/stock_picking_batch/views/stock_picking_batch_views.xml b/addons/stock_picking_batch/views/stock_picking_batch_views.xml index a729315f9bd..24474a6e0b3 100644 --- a/addons/stock_picking_batch/views/stock_picking_batch_views.xml +++ b/addons/stock_picking_batch/views/stock_picking_batch_views.xml @@ -78,7 +78,8 @@
- + +
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index cdc2fa3b950..3041058826b 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -1367,7 +1367,8 @@ class Binary(http.Controller): @staticmethod def placeholder(image='placeholder.png'): - with tools.file_open(get_resource_path('web', 'static/src/img', image), 'rb') as fd: + image_path = image.lstrip('/').split('/') if '/' in image else ['web', 'static', 'src', 'img', image] + with tools.file_open(get_resource_path(*image_path), 'rb') as fd: return fd.read() @http.route(['/web/content', @@ -1437,23 +1438,35 @@ class Binary(http.Controller): def _content_image(self, xmlid=None, model='ir.attachment', id=None, field='datas', filename_field='name', unique=None, filename=None, mimetype=None, download=None, width=0, height=0, crop=False, quality=0, access_token=None, - placeholder='placeholder.png', **kwargs): + placeholder=None, **kwargs): status, headers, image_base64 = request.env['ir.http'].binary_content( xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename, filename_field=filename_field, download=download, mimetype=mimetype, default_mimetype='image/png', access_token=access_token) - return Binary._content_image_get_response(status, headers, image_base64, field=field, download=download, - width=width, height=height, crop=crop, quality=quality, - placeholder=placeholder) + return Binary._content_image_get_response( + status, headers, image_base64, model=model, id=id, field=field, download=download, + width=width, height=height, crop=crop, quality=quality, + placeholder=placeholder) @staticmethod - def _content_image_get_response(status, headers, image_base64, field='datas', download=None, - width=0, height=0, crop=False, quality=0, placeholder='placeholder.png'): + def _content_image_get_response( + status, headers, image_base64, model='ir.attachment', id=None, + field='datas', download=None, width=0, height=0, crop=False, + quality=0, placeholder='placeholder.png'): if status in [301, 304] or (status != 200 and download): return request.env['ir.http']._response_by_status(status, headers, image_base64) if not image_base64: - image_base64 = base64.b64encode(Binary.placeholder(image=placeholder)) + if placeholder is None and model in request.env: + # Try to browse the record in case a specific placeholder + # is supposed to be used. (eg: Unassigned users on a task) + record = request.env[model].browse(int(id)) if id else request.env[model] + placeholder_filename = record._get_placeholder_filename(field=field) + placeholder_content = Binary.placeholder(image=placeholder_filename) + else: + placeholder_content = Binary.placeholder() + image_base64 = base64.b64encode(placeholder_content) + if not (width or height): width, height = odoo.tools.image_guess_size_from_field_name(field) diff --git a/addons/web/static/src/js/views/kanban/kanban_record.js b/addons/web/static/src/js/views/kanban/kanban_record.js index ab19e73d2eb..f199a286db6 100644 --- a/addons/web/static/src/js/views/kanban/kanban_record.js +++ b/addons/web/static/src/js/views/kanban/kanban_record.js @@ -184,14 +184,13 @@ var KanbanRecord = Widget.extend({ * @returns {string} the url of the image */ _getImageURL: function (model, field, id, placeholder) { - id = (_.isArray(id) ? id[0] : id) || false; - placeholder = placeholder || "/web/static/src/img/placeholder.png"; + id = (_.isArray(id) ? id[0] : id) || null; var isCurrentRecord = this.modelName === model && this.recordData.id === id; var url; if (isCurrentRecord && this.record[field] && this.record[field].raw_value && !utils.is_bin_size(this.record[field].raw_value)) { // Use magic-word technique for detecting image type url = 'data:image/' + this.file_type_magic_word[this.record[field].raw_value[0]] + ';base64,' + this.record[field].raw_value; - } else if (!model || !field || !id || (isCurrentRecord && this.record[field] && !this.record[field].raw_value)) { + } else if (placeholder && (!model || !field || !id || (isCurrentRecord && this.record[field] && !this.record[field].raw_value))) { url = placeholder; } else { var session = this.getSession(); diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js index 0b8334472ad..c0a07c699fa 100644 --- a/addons/web/static/tests/views/kanban_tests.js +++ b/addons/web/static/tests/views/kanban_tests.js @@ -5264,8 +5264,15 @@ QUnit.module('Views', { return this._super.apply(this, arguments); }, }); + var images = kanban.el.querySelectorAll('img'); + var placeholders = []; + for (var [index, img] of images.entries()) { + if (img.dataset.src.indexOf(this.data.partner.records[index].image) === -1) { + // Then we display a placeholder + placeholders.push(img); + } + } - var placeholders = kanban.$('img[data-src$="/web/static/src/img/placeholder.png"]'); assert.strictEqual(placeholders.length, this.data.partner.records.length - 1, "partner with no image should display the placeholder"); diff --git a/addons/website_slides/views/slide_slide_views.xml b/addons/website_slides/views/slide_slide_views.xml index 0301710e345..1f98a26f89d 100644 --- a/addons/website_slides/views/slide_slide_views.xml +++ b/addons/website_slides/views/slide_slide_views.xml @@ -211,7 +211,8 @@ - + + diff --git a/odoo/addons/base/data/res_users_data.xml b/odoo/addons/base/data/res_users_data.xml index 6439180c2a7..dd1b504ff32 100644 --- a/odoo/addons/base/data/res_users_data.xml +++ b/odoo/addons/base/data/res_users_data.xml @@ -19,6 +19,7 @@ System]]> + --
Administrator]]>
@@ -29,7 +30,7 @@ Administrator]]>
default - + Public user public diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py index 718a62c12aa..e428fb68b9a 100644 --- a/odoo/addons/base/models/res_users.py +++ b/odoo/addons/base/models/res_users.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import contextlib +import base64 import pytz import datetime import ipaddress @@ -21,7 +22,8 @@ from odoo.exceptions import AccessDenied, AccessError, UserError, ValidationErro from odoo.http import request from odoo.osv import expression from odoo.service.db import check_super -from odoo.tools import partition, collections, frozendict, lazy_property +from odoo.tools import partition, collections, frozendict, lazy_property, image_process +from odoo.modules.module import get_module_resource _logger = logging.getLogger(__name__) @@ -214,6 +216,17 @@ class Users(models.Model): def _companies_count(self): return self.env['res.company'].sudo().search_count([]) + @api.model + def _get_default_image(self): + """ Get a default image when the user is created without image + + Inspired to _get_default_image method in + https://github.com/odoo/odoo/blob/11.0/odoo/addons/base/res/res_partner.py + """ + image_path = get_module_resource('base', 'static/img', 'avatar.png') + image = base64.b64encode(open(image_path, 'rb').read()) + return image_process(image, colorize=True) + partner_id = fields.Many2one('res.partner', required=True, ondelete='restrict', auto_join=True, string='Related Partner', help='Partner-related data of the user') login = fields.Char(required=True, help="Used to log into the system") @@ -258,6 +271,7 @@ class Users(models.Model): compute='_compute_accesses_count') groups_count = fields.Integer('# Groups', help='Number of groups that apply to the current user', compute='_compute_accesses_count') + image_1920 = fields.Image(related='partner_id.image_1920', inherited=True, readonly=False, default=_get_default_image) _sql_constraints = [ ('login_key', 'UNIQUE (login)', 'You can not have two users with the same login !') @@ -913,6 +927,13 @@ class Users(models.Model): def _register_hook(self): if hasattr(self, 'check_credentials'): _logger.warning("The check_credentials method of res.users has been renamed _check_credentials. One of your installed modules defines one, but it will not be called anymore.") + + def _get_placeholder_filename(self, field=None): + image_fields = ['image_%s' % size for size in [1920, 1024, 512, 256, 128]] + if field in image_fields and not self: + return 'base/static/img/user-slash.png' + return super()._get_placeholder_filename(field=field) + # # Implied groups # diff --git a/odoo/addons/base/static/img/user-slash.png b/odoo/addons/base/static/img/user-slash.png new file mode 100644 index 0000000000000000000000000000000000000000..626ddfcb8d2b7f0636a02e121f15f55a0118200b GIT binary patch literal 4703 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 literal 0 HcmV?d00001 diff --git a/odoo/models.py b/odoo/models.py index bf8d8169666..37cd8386229 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -6152,6 +6152,13 @@ Record ids: %(records)s return result + def _get_placeholder_filename(self, field=None): + """ Returns the filename of the placeholder to use, + set on web/static/src/img by default, or the + complete path to access it (eg: module/path/to/image.png). + """ + return 'placeholder.png' + collections.Set.register(BaseModel) # not exactly true as BaseModel doesn't have __reversed__, index or count collections.Sequence.register(BaseModel)