[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) <yti@odoo.com>
Co-authored-by: jdoutreloux <jud@odoo.com>
Co-authored-by: Yannick Tivisse <yti@odoo.com>
This commit is contained in:
Xavier Bol (xbo)
2020-01-21 09:22:46 +00:00
committed by Yannick Tivisse
co-authored by jdoutreloux Yannick Tivisse
parent be01db2f71
commit 51b36c3dbb
16 changed files with 84 additions and 25 deletions
@@ -59,7 +59,8 @@
<div t-attf-class="oe_kanban_card oe_kanban_global_click">
<div>
<strong><span><field name="analytic_id"/></span></strong>
<img t-att-src="kanban_image('res.users', 'image_128', record.user_id.raw_value)" t-att-title="record.user_id.value" t-att-alt="record.user_id.value" class="oe_kanban_avatar o_image_24_cover float-right"/>
<t t-set="unassigned"><t t-esc="_t('Unassigned')"/></t>
<img t-att-src="kanban_image('res.users', 'image_128', record.user_id.raw_value)" t-att-title="record.user_id.value || unassigned" t-att-alt="record.user_id.value" class="oe_kanban_avatar o_image_24_cover float-right"/>
</div>
<div t-if="record.date_start.value"><i class="fa fa-calendar"></i> From <field name="date_start"/> <t t-if="record.date_stop.value">to <field name="date_stop"/></t></div>
<div t-if="record.product_id.value"><strong>Product</strong> <field name="product_id"/> </div>
+4 -2
View File
@@ -377,7 +377,8 @@
</div>
</div>
<div class="oe_kanban_bottom_right">
<img t-att-src="kanban_image('res.users', 'image_128', record.user_id.raw_value)" t-att-title="record.user_id.value" t-att-alt="record.user_id.value" class="oe_kanban_avatar float-right"/>
<t t-set="unassigned"><t t-esc="_t('Unassigned')"/></t>
<img t-att-src="kanban_image('res.users', 'image_128', record.user_id.raw_value)" t-att-title="record.user_id.value || unassigned" t-att-alt="record.user_id.value" class="oe_kanban_avatar float-right"/>
</div>
</div>
</div>
@@ -508,7 +509,8 @@
<field name="activity_ids" widget="kanban_activity"/>
</div>
<div class="oe_kanban_bottom_right">
<img t-att-src="kanban_image('res.users', 'image_128', record.user_id.raw_value)" t-att-title="record.user_id.value" t-att-alt="record.user_id.value" class="oe_kanban_avatar"/>
<t t-set="unassigned"><t t-esc="_t('Unassigned')"/></t>
<img t-att-src="kanban_image('res.users', 'image_128', record.user_id.raw_value)" t-att-title="record.user_id.value || unassigned" t-att-alt="record.user_id.value" class="oe_kanban_avatar"/>
</div>
</div>
</div>
@@ -329,7 +329,8 @@
<field name="legend_blocked" invisible="1"/>
<field name="legend_done" invisible="1"/>
</div>
<img t-att-src="kanban_image('res.users', 'image_128', record.user_id.raw_value)" t-att-title="record.user_id.value" t-att-alt="record.user_id.value" class="oe_kanban_avatar"/>
<t t-set="unassigned"><t t-esc="_t('Unassigned')"/></t>
<img t-att-src="kanban_image('res.users', 'image_128', record.user_id.raw_value)" t-att-title="record.user_id.value || unassigned" t-att-alt="record.user_id.value" class="oe_kanban_avatar"/>
</div>
</div>
@@ -164,7 +164,8 @@
</div>
<div class="oe_kanban_bottom_right">
<field name="kanban_state" widget="state_selection"/>
<img t-att-src="kanban_image('res.users', 'image_128', record.user_id.raw_value)" t-att-title="record.user_id.value" t-att-alt="record.user_id.value" class="oe_kanban_avatar float-right"/>
<t t-set="unassigned"><t t-esc="_t('Unassigned')"/></t>
<img t-att-src="kanban_image('res.users', 'image_128', record.user_id.raw_value)" t-att-title="record.user_id.value || unassigned" t-att-alt="record.user_id.value" class="oe_kanban_avatar float-right"/>
</div>
</div>
</div>
@@ -351,7 +351,8 @@
<field name='mailing_model_id' attrs="{'invisible': [('mailing_model_name','=','mailing.list')]}"/>
<span attrs="{'invisible': [('mailing_model_name','!=','mailing.list')]}">Mailing Contact</span>
</span>
<img t-att-src="kanban_image('res.users', 'image_128', record.user_id.raw_value)" t-att-title="record.user_id.value" t-att-alt="record.user_id.value" class="o_image_24_cover oe_kanban_avatar float-right"/>
<t t-set="unassigned"><t t-esc="_t('Unassigned')"/></t>
<img t-att-src="kanban_image('res.users', 'image_128', record.user_id.raw_value)" t-att-title="record.user_id.value || unassigned" t-att-alt="record.user_id.value" class="o_image_24_cover oe_kanban_avatar float-right"/>
</div>
</div>
</t>
+4 -2
View File
@@ -256,7 +256,8 @@
</div>
<div class="col-4">
<div class="oe_kanban_bottom_right">
<img t-att-src="kanban_image('res.users', 'image_128', record.user_id.raw_value)" t-att-title="record.user_id.value" t-att-alt="record.user_id.value" class="oe_kanban_avatar o_image_24_cover float-right"/>
<t t-set="unassigned"><t t-esc="_t('Unassigned')"/></t>
<img t-att-src="kanban_image('res.users', 'image_128', record.user_id.raw_value)" t-att-title="record.user_id.value || unassigned" t-att-alt="record.user_id.value" class="oe_kanban_avatar o_image_24_cover float-right"/>
</div>
</div>
</div>
@@ -660,7 +661,8 @@
</div>
<div class="oe_kanban_bottom_right" t-if="!selection_mode">
<field name="kanban_state" widget="state_selection" groups="base.group_user" invisible="context.get('fsm_mode', False)"/>
<img t-att-src="kanban_image('res.users', 'image_128', record.user_id.raw_value)" t-att-title="record.user_id.value" t-att-alt="record.user_id.value" class="oe_kanban_avatar"/>
<t t-set="unassigned"><t t-esc="_t('Unassigned')"/></t>
<img t-att-src="kanban_image('res.users', 'image_128', record.user_id.raw_value)" t-att-title="record.user_id.value || unassigned" t-att-alt="record.user_id.value" class="oe_kanban_avatar"/>
</div>
</div>
</div>
@@ -244,7 +244,8 @@
<field name="vendor_id"/>
</div>
<div class="oe_kanban_bottom_right">
<img t-att-src="kanban_image('res.users', 'image_128', record.user_id.raw_value)" t-att-title="record.user_id.value" t-att-alt="record.user_id.value" class="oe_kanban_avatar float-right"/>
<t t-set="unassigned"><t t-esc="_t('Unassigned')"/></t>
<img t-att-src="kanban_image('res.users', 'image_128', record.user_id.raw_value)" t-att-title="record.user_id.value || unassigned" t-att-alt="record.user_id.value" class="oe_kanban_avatar float-right"/>
</div>
</div>
</div>
@@ -78,7 +78,8 @@
<div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_left"/>
<div class="oe_kanban_bottom_right">
<img t-att-src="kanban_image('res.users', 'image_128', record.user_id.raw_value)" t-att-title="record.user_id.value" t-att-alt="record.user_id.value" class="oe_kanban_avatar"/>
<t t-set="unassigned"><t t-esc="_t('Unassigned')"/></t>
<img t-att-src="kanban_image('res.users', 'image_128', record.user_id.raw_value)" t-att-title="record.user_id.value || unassigned" t-att-alt="record.user_id.value" class="oe_kanban_avatar"/>
</div>
</div>
</div>
+21 -8
View File
@@ -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)
@@ -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();
@@ -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");
@@ -211,7 +211,8 @@
<t t-else=""><i class="fa fa-file-pdf-o mr-2" aria-label="Document" role="img" title="Document"/></t>
<field name="slide_type"/>
</span>
<img t-att-src="kanban_image('res.users', 'image_128', record.user_id.raw_value)" t-att-title="record.user_id.value" t-att-alt="record.user_id.value" class="oe_kanban_avatar o_image_24_cover"/>
<t t-set="unassigned"><t t-esc="_t('Unassigned')"/></t>
<img t-att-src="kanban_image('res.users', 'image_128', record.user_id.raw_value)" t-att-title="record.user_id.value || unassigned" t-att-alt="record.user_id.value" class="oe_kanban_avatar o_image_24_cover"/>
</div>
</div>
</div>
+2 -1
View File
@@ -19,6 +19,7 @@ System</span>]]></field>
<field name="company_id" ref="main_company"/>
<field name="company_ids" eval="[(4, ref('main_company'))]"/>
<field name="groups_id" eval="[(6,0,[])]"/>
<field name="image_1920" type="base64" file="base/static/img/avatar_grey.png"/>
<field name="signature"><![CDATA[<span>-- <br/>
Administrator</span>]]></field>
</record>
@@ -29,7 +30,7 @@ Administrator</span>]]></field>
<field name="login">default</field>
<field name="active" eval="False"/>
</record>
<record id="public_user" model="res.users">
<field name="name">Public user</field>
<field name="login">public</field>
+22 -1
View File
@@ -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
#
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

+7
View File
@@ -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)