changeset c2494167ee23 in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset&node=c2494167ee23
description:
Add support for avatar
issue10085
review343561002
diffstat:
CHANGELOG | 1 +
doc/ref/models/models.rst | 35 +++++
doc/topics/configuration.rst | 32 ++++
setup.py | 5 +-
trytond/ir/__init__.py | 3 +
trytond/ir/avatar.py | 193 +++++++++++++++++++++++++++++
trytond/ir/fonts/LICENSE | 93 +++++++++++++
trytond/ir/fonts/karla.ttf | Bin
trytond/ir/message.xml | 15 ++
trytond/ir/routes.py | 25 +++
trytond/model/__init__.py | 4 +-
trytond/model/avatar.py | 80 ++++++++++++
trytond/res/user.py | 13 +-
trytond/res/view/user_form.xml | 27 ++-
trytond/res/view/user_form_preferences.xml | 17 +-
trytond/res/view/user_list.xml | 4 +-
trytond/tests/test_res.py | 16 ++-
17 files changed, 540 insertions(+), 23 deletions(-)
diffs (777 lines):
diff -r 4d7ab7a969d3 -r c2494167ee23 CHANGELOG
--- a/CHANGELOG Sat Mar 27 21:00:44 2021 +0100
+++ b/CHANGELOG Sun Mar 28 19:20:37 2021 +0200
@@ -1,3 +1,4 @@
+* Add support for avatars
* Add status command
* Add document widget on form view
* Add full text and similarity search and FullText field
diff -r 4d7ab7a969d3 -r c2494167ee23 doc/ref/models/models.rst
--- a/doc/ref/models/models.rst Sat Mar 27 21:00:44 2021 +0100
+++ b/doc/ref/models/models.rst Sun Mar 28 19:20:37 2021 +0200
@@ -914,3 +914,38 @@
Helper method that checks if there is no recursion in the tree defined by
:meth:`tree`.
+
+============
+avatar_mixin
+============
+
+.. method:: avatar_mixin([size[, default]])
+
+Returns a mixin_ :class:`AvatarMixin`. ``size`` defines the size of the avatar
+image and its default value is ``64``. ``default`` indicates the name of the
+field to use for generating a default avatar, if it's not set then no default
+avatar is generated.
+
+.. class:: AvatarMixin
+
+.. attribute:: AvatarMixin.avatars
+
+ The :class:`trytond.model.fields.One2Many` field used to store the
+ ``ir.avatar`` records.
+
+.. attribute:: AvatarMixin.avatar
+
+ The :class:`trytond.model.fields.Binary` field that contains the avatar.
+
+.. attribute:: AvatarMixin.avatar_url
+
+ The :class:`trytond.model.fields.Char` field that containts the URL for the
+ avatar.
+
+.. attribute:: AvatarMixin.has_avatar
+
+ Indicates whether the record has an avatar.
+
+.. classmethod:: AvatarMixin.generate_avatar(records, field)
+
+ Generate a default avatar for each record using the field.
diff -r 4d7ab7a969d3 -r c2494167ee23 doc/topics/configuration.rst
--- a/doc/topics/configuration.rst Sat Mar 27 21:00:44 2021 +0100
+++ b/doc/topics/configuration.rst Sun Mar 28 19:20:37 2021 +0200
@@ -78,6 +78,23 @@
The list (one per line) of origins allowed for `Cross-Origin Resource sharing
<https://en.wikipedia.org/wiki/Cross-origin_resource_sharing>`_.
+avatar_base
+~~~~~~~~~~~
+
+The base URL without a path for avatar URL.
+
+Default: ``''``
+
+.. note:: It can be used to setup a CDN.
+
+
+avatar_timeout
+~~~~~~~~~~~~~~
+
+The time in seconds that the avatar can be stored in cache.
+
+Default: 7 days
+
database
--------
@@ -139,6 +156,21 @@
Default: ``en``
+avatar_filestore
+~~~~~~~~~~~~~~~~
+
+This configuration value indicates whether the avatars should be stored in the
+:py:mod:`trytond.filestore` (``True``) or the database (``False``).
+
+Default: ``False``
+
+avatar_prefix
+~~~~~~~~~~~~~
+
+The prefix to use with the :ref:`FileStore <ref-filestore>` to store avatars.
+
+Default: ``None``
+
default_name
~~~~~~~~~~~~
diff -r 4d7ab7a969d3 -r c2494167ee23 setup.py
--- a/setup.py Sat Mar 27 21:00:44 2021 +0100
+++ b/setup.py Sun Mar 28 19:20:37 2021 +0200
@@ -76,6 +76,7 @@
pg_require = ['psycopg2cffi >= 2.5.4']
else:
pg_require = ['psycopg2 >= 2.5.4']
+tests_require = ['pillow']
setup(name=name,
version=version,
@@ -95,7 +96,7 @@
packages=find_packages(exclude=['*.modules.*', 'modules.*', 'modules',
'*.proteus.*', 'proteus.*', 'proteus']),
package_data={
- 'trytond': ['ir/ui/icons/*.svg', '*.rnc', '*.rng'],
+ 'trytond': ['ir/ui/icons/*.svg', '*.rnc', '*.rng', 'ir/fonts/*.ttf'],
'trytond.backend.postgresql': ['init.sql'],
'trytond.backend.sqlite': ['init.sql'],
'trytond.ir': ['tryton.cfg', '*.xml', 'view/*.xml', 'locale/*.po'],
@@ -172,11 +173,13 @@
'html2text': ['html2text'],
'weasyprint': ['weasyprint'],
'coroutine': ['gevent>=1.1'],
+ 'image': ['pillow'],
},
dependency_links=dependency_links,
zip_safe=False,
test_suite='trytond.tests',
test_loader='trytond.test_loader:Loader',
+ tests_require=tests_require,
cmdclass={
'update_rng': rnc2rng,
},
diff -r 4d7ab7a969d3 -r c2494167ee23 trytond/ir/__init__.py
--- a/trytond/ir/__init__.py Sat Mar 27 21:00:44 2021 +0100
+++ b/trytond/ir/__init__.py Sun Mar 28 19:20:37 2021 +0200
@@ -9,6 +9,7 @@
from . import model
from . import attachment
from . import note
+from . import avatar
from . import cron
from . import lang
from . import export
@@ -70,6 +71,8 @@
attachment.Attachment,
note.Note,
note.NoteRead,
+ avatar.Avatar,
+ avatar.AvatarCache,
cron.Cron,
lang.Lang,
lang.LangConfigStart,
diff -r 4d7ab7a969d3 -r c2494167ee23 trytond/ir/avatar.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/trytond/ir/avatar.py Sun Mar 28 19:20:37 2021 +0200
@@ -0,0 +1,193 @@
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+import io
+import math
+import os
+import uuid
+from random import Random
+from urllib.parse import urljoin, quote
+
+try:
+ import PIL
+ from PIL import Image, ImageDraw, ImageFont
+except ImportError:
+ PIL = None
+
+from trytond.config import config
+from trytond.model import ModelSQL, fields, Unique
+from trytond.pool import Pool
+from trytond.transaction import Transaction
+from trytond.wsgi import Base64Converter
+
+from .resource import ResourceMixin
+
+if config.getboolean('database', 'avatar_filestore', default=False):
+ file_id = 'image_id'
+ store_prefix = config.get('database', 'avatar_prefix', default=None)
+else:
+ file_id = None
+ store_prefix = None
+URL_BASE = config.get('web', 'avatar_base', default='')
+FONT = os.path.join(os.path.dirname(__file__), 'fonts', 'karla.ttf')
+
+
+class ImageMixin:
+ image = fields.Binary(
+ "Image", file_id=file_id, store_prefix=store_prefix)
+ image_id = fields.Char("Image ID", readonly=True)
+
+
+class Avatar(ImageMixin, ResourceMixin, ModelSQL):
+ "Avatar"
+ __name__ = 'ir.avatar'
+
+ uuid = fields.Char("UUID", required=True)
+ cache = fields.One2Many('ir.avatar.cache', 'avatar', "Cache")
+
+ @classmethod
+ def __setup__(cls):
+ super().__setup__()
+ t = cls.__table__()
+ cls._sql_constraints = [
+ ('resource_unique', Unique(t, t.resource),
+ 'ir.msg_avatar_resource_unique'),
+ ]
+
+ @classmethod
+ def default_uuid(cls):
+ return uuid.uuid4().hex
+
+ @classmethod
+ def create(cls, vlist):
+ vlist = [v.copy() for v in vlist]
+ for values in vlist:
+ values.setdefault('uuid', cls.default_uuid())
+ return super().create(vlist)
+
+ @classmethod
+ def write(cls, *args):
+ avatars = sum(args[0:None:2], [])
+ super().write(*args)
+ cls.clear_cache(avatars)
+
+ @property
+ def url(self):
+ if self.image_id or self.image:
+ return urljoin(
+ URL_BASE, quote('/avatar/%(database)s/%(uuid)s' % {
+ 'database': Base64Converter(None).to_url(
+ Transaction().database.name),
+ 'uuid': self.uuid,
+ }))
+
+ def get(self, size=64):
+ size = min((
+ 2 ** math.ceil(math.log2(size)),
+ 10 * math.ceil(size / 10) if size <= 100
+ else 50 * math.ceil(size / 50)))
+ if not (0 < size < 2048):
+ raise ValueError("Invalid size")
+ for avatar in self.cache:
+ if avatar.size == size:
+ return avatar.image
+ if not self.image:
+ return None
+ if PIL:
+ with Transaction().new_transaction():
+ cache = self._store_cache(size, self._resize(size))
+ cache.save()
+ return cache.image
+ else:
+ return self.image
+
+ @classmethod
+ def convert(cls, image, **_params):
+ if not PIL or not image:
+ return image
+ data = io.BytesIO()
+ img = Image.open(io.BytesIO(image))
+ width, height = img.size
+ size = min(width, height)
+ img.crop((
+ (width - size) // 2,
+ (height - size) // 2,
+ (width + size) // 2,
+ (height + size) // 2))
+ if size > 2048:
+ img.resize((2048, 2048))
+ img.save(data, format='jpeg', optimize=True, **_params)
+ return data.getvalue()
+
+ def _resize(self, size=64, **_params):
+ if not PIL:
+ return self.image
+ data = io.BytesIO()
+ img = Image.open(io.BytesIO(self.image))
+ img = img.resize((size, size))
+ img.save(data, format='jpeg', optimize=True, **_params)
+ return data.getvalue()
+
+ def _store_cache(self, size, image):
+ pool = Pool()
+ Cache = pool.get('ir.avatar.cache')
+ return Cache(
+ avatar=self,
+ image=image,
+ size=size)
+
+ @classmethod
+ def clear_cache(cls, avatars):
+ pool = Pool()
+ Cache = pool.get('ir.avatar.cache')
+ caches = [c for a in avatars for c in a.cache]
+ Cache.delete(caches)
+
+
+class AvatarCache(ImageMixin, ModelSQL):
+ "Avatar Cache"
+ __name__ = 'ir.avatar.cache'
+
+ avatar = fields.Many2One(
+ 'ir.avatar', "Avatar", required=True, ondelete='CASCADE')
+ size = fields.Integer(
+ "Size", required=True,
+ domain=[
+ ('size', '>', 0),
+ ('size', '<', 2048),
+ ])
+
+ @classmethod
+ def __setup__(cls):
+ super().__setup__()
+ t = cls.__table__()
+ cls._sql_constraints = [
+ ('size_unique', Unique(t, t.avatar, t.size),
+ 'ir.msg_avatar_size_unique'),
+ ]
+ cls._order.append(('size', 'ASC'))
+
+
+def generate(size, string):
+ if not PIL:
+ return
+
+ def background_color(string):
+ random = Random(string)
+ r = v = b = 255
+ # Skip too bright color
+ while r + v + b > 255 * 2:
+ r = random.randint(0, 255)
+ v = random.randint(0, 255)
+ b = random.randint(0, 255)
+ return r, v, b
+
+ white = (255, 255, 255)
+ image = Image.new('RGB', (size, size), background_color(string))
+ draw = ImageDraw.Draw(image)
+ font = ImageFont.truetype(FONT, size=int(0.65 * size))
+ letter = string[0].upper() if string else ''
+ draw.text(
+ (size / 2, size / 2), letter, fill=white, font=font, anchor='mm')
+ data = io.BytesIO()
+ image.save(data, format='jpeg', optimize=True)
+ return data.getvalue()
diff -r 4d7ab7a969d3 -r c2494167ee23 trytond/ir/fonts/LICENSE
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/trytond/ir/fonts/LICENSE Sun Mar 28 19:20:37 2021 +0200
@@ -0,0 +1,93 @@
+Copyright 2019 The Karla Project Authors (https://github.com/googlefonts/karla)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+https://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff -r 4d7ab7a969d3 -r c2494167ee23 trytond/ir/fonts/karla.ttf
Binary file trytond/ir/fonts/karla.ttf has changed
diff -r 4d7ab7a969d3 -r c2494167ee23 trytond/ir/message.xml
--- a/trytond/ir/message.xml Sat Mar 27 21:00:44 2021 +0100
+++ b/trytond/ir/message.xml Sun Mar 28 19:20:37 2021 +0200
@@ -351,5 +351,20 @@
<record model="ir.message"
id="msg_email_template_invalid_field_pyson_type">
<field name="text">The PYSON %(field)s in e-mail template
"%(template)s" must generate a list.</field>
</record>
+ <record model="ir.message" id="msg_avatar_resource_unique">
+ <field name="text">Only one avatar is allowed per resource.</field>
+ </record>
+ <record model="ir.message" id="msg_avatar_size_unique">
+ <field name="text">The size of an avatar must be unique.</field>
+ </record>
+ <record model="ir.message" id="msg_avatar">
+ <field name="text">Avatar</field>
+ </record>
+ <record model="ir.message" id="msg_avatars">
+ <field name="text">Avatars</field>
+ </record>
+ <record model="ir.message" id="msg_avatar_url">
+ <field name="text">Avatar URL</field>
+ </record>
</data>
</tryton>
diff -r 4d7ab7a969d3 -r c2494167ee23 trytond/ir/routes.py
--- a/trytond/ir/routes.py Sat Mar 27 21:00:44 2021 +0100
+++ b/trytond/ir/routes.py Sun Mar 28 19:20:37 2021 +0200
@@ -24,6 +24,8 @@
SOURCE = config.get(
'html', 'src', default='https://cloud.tinymce.com/stable/tinymce.min.js')
+AVATAR_TIMEOUT = config.getint(
+ 'web', 'avatar_timeout', default=7 * 24 * 60 * 60)
def get_token(record):
@@ -278,3 +280,26 @@
'Content-Disposition', 'attachment', filename=filename)
response.headers.add('Content-Length', len(data))
return response
+
+
[email protected]('/avatar/<base64:database_name>/<uuid>', methods={'GET'})
+@with_pool
+@with_transaction()
+def avatar(request, pool, uuid):
+ Avatar = pool.get('ir.avatar')
+
+ try:
+ avatar, = Avatar.search([
+ ('uuid', '=', uuid),
+ ])
+ except ValueError:
+ abort(HTTPStatus.NOT_FOUND)
+ try:
+ size = int(request.args.get('s', 64))
+ except ValueError:
+ abort(HTTPStatus.BAD_REQUEST)
+ response = Response(avatar.get(size), mimetype='image/jpeg')
+ response.headers['Cache-Control'] = (
+ 'max-age=%s, public' % AVATAR_TIMEOUT)
+ response.add_etag()
+ return response
diff -r 4d7ab7a969d3 -r c2494167ee23 trytond/model/__init__.py
--- a/trytond/model/__init__.py Sat Mar 27 21:00:44 2021 +0100
+++ b/trytond/model/__init__.py Sun Mar 28 19:20:37 2021 +0200
@@ -15,9 +15,11 @@
from .order import sequence_ordered
from .active import DeactivableMixin
from .tree import tree
+from .avatar import avatar_mixin
__all__ = ['Model', 'ModelView', 'ModelStorage', 'ModelSingleton', 'ModelSQL',
'Check', 'Unique', 'Exclude',
'Workflow', 'DictSchemaMixin', 'MatchMixin', 'UnionMixin', 'dualmethod',
'MultiValueMixin', 'ValueMixin', 'SymbolMixin',
- 'EvalEnvironment', 'sequence_ordered', 'DeactivableMixin', 'tree']
+ 'EvalEnvironment', 'sequence_ordered', 'DeactivableMixin', 'tree',
+ 'avatar_mixin']
diff -r 4d7ab7a969d3 -r c2494167ee23 trytond/model/avatar.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/trytond/model/avatar.py Sun Mar 28 19:20:37 2021 +0200
@@ -0,0 +1,80 @@
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+
+from trytond.i18n import lazy_gettext
+from trytond.model import fields
+from trytond.pool import Pool
+
+
+def avatar_mixin(size=64, default=None):
+ class AvatarMixin:
+
+ avatars = fields.One2Many(
+ 'ir.avatar', 'resource', lazy_gettext('ir.msg_avatars'), size=1)
+ avatar = fields.Function(
+ fields.Binary(lazy_gettext('ir.msg_avatar')),
+ '_get_avatar', setter='_set_avatar')
+ avatar_url = fields.Function(
+ fields.Char(lazy_gettext('ir.msg_avatar_url')), '_get_avatar_url')
+
+ @property
+ def has_avatar(self):
+ if self.avatars:
+ avatar, = self.avatars
+ return bool(avatar.image_id or avatar.image)
+ return False
+
+ def _get_avatar(self, name):
+ if self.avatars:
+ avatar, = self.avatars
+ return avatar.get(size=size)
+ return None
+
+ @classmethod
+ def _set_avatar(cls, records, name, value):
+ pool = Pool()
+ Avatar = pool.get('ir.avatar')
+ avatars = []
+ image = Avatar.convert(value)
+ for record in records:
+ if record.avatars:
+ avatar, = record.avatars
+ else:
+ avatar = Avatar(resource=record)
+ avatars.append(avatar)
+ Avatar.save(avatars)
+ # Use write the image to store only once in filestore
+ Avatar.write(avatars, {
+ 'image': image,
+ })
+
+ def _get_avatar_url(self, name):
+ if self.avatars:
+ avatar, = self.avatars
+ return avatar.url
+
+ @classmethod
+ def generate_avatar(cls, records, field='rec_name'):
+ from trytond.ir.avatar import generate
+ records = [r for r in records if not r.has_avatar]
+ if not records:
+ return
+ for record in records:
+ record.avatar = generate(size, getattr(record, field))
+ cls.save(records)
+
+ if default:
+
+ @classmethod
+ def create(cls, vlist):
+ records = super().create(vlist)
+ cls.generate_avatar(records, field=default)
+ return records
+
+ @classmethod
+ def write(cls, *args):
+ records = sum(args[0:None:2], [])
+ super().write(*args)
+ cls.generate_avatar(records, field=default)
+
+ return AvatarMixin
diff -r 4d7ab7a969d3 -r c2494167ee23 trytond/res/user.py
--- a/trytond/res/user.py Sat Mar 27 21:00:44 2021 +0100
+++ b/trytond/res/user.py Sun Mar 28 19:20:37 2021 +0200
@@ -43,7 +43,8 @@
from trytond.exceptions import UserError
from trytond.i18n import gettext
from trytond.model import (
- ModelView, ModelSQL, Workflow, DeactivableMixin, fields, Unique)
+ ModelView, ModelSQL, Workflow, DeactivableMixin, fields, Unique,
+ avatar_mixin)
from trytond.pool import Pool
from trytond.pyson import PYSONEncoder, Eval, Bool
from trytond.report import Report, get_email
@@ -99,7 +100,7 @@
pass
-class User(DeactivableMixin, ModelSQL, ModelView):
+class User(avatar_mixin(100, 'login'), DeactivableMixin, ModelSQL, ModelView):
"User"
__name__ = "res.user"
name = fields.Char('Name', select=True)
@@ -141,6 +142,8 @@
'get_language_direction')
email = fields.Char('Email')
status_bar = fields.Function(fields.Char('Status Bar'), 'get_status_bar')
+ avatar_badge_url = fields.Function(
+ fields.Char("Avatar Badge URL"), 'get_avatar_badge_url')
warnings = fields.One2Many('res.user.warning', 'user', 'Warnings')
sessions = fields.Function(fields.Integer('Sessions'),
'get_sessions')
@@ -176,6 +179,9 @@
'pyson_menu',
'actions',
'status_bar',
+ 'avatar',
+ 'avatar_url',
+ 'avatar_badge_url',
'warnings',
'applications',
]
@@ -242,6 +248,9 @@
def get_status_bar(self, name):
return self.name
+ def get_avatar_badge_url(self, name):
+ pass
+
def get_password(self, name):
return 'x' * 10
diff -r 4d7ab7a969d3 -r c2494167ee23 trytond/res/view/user_form.xml
--- a/trytond/res/view/user_form.xml Sat Mar 27 21:00:44 2021 +0100
+++ b/trytond/res/view/user_form.xml Sun Mar 28 19:20:37 2021 +0200
@@ -2,19 +2,24 @@
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<form>
- <label name="name"/>
- <field name="name"/>
- <label name="active"/>
- <field name="active"/>
+ <group col="4" colspan="3" yalign="0" id="header">
+ <label name="name"/>
+ <field name="name"/>
+ <label name="active"/>
+ <field name="active"/>
+
+ <label name="login"/>
+ <field name="login"/>
+ <label name="email"/>
+ <field name="email" widget="email"/>
+
+ <label name="password"/>
+ <field name="password" widget="password"/>
+ <button name="reset_password" colspan="2" />
+ </group>
+ <field name="avatar" widget="image" height="100" width="100" xexpand="0"/>
<notebook colspan="4">
<page string="User" id="user">
- <label name="login"/>
- <field name="login"/>
- <label name="email"/>
- <field name="email" widget="email"/>
- <label name="password"/>
- <field name="password" widget="password"/>
- <button name="reset_password" colspan="2" />
<separator name="signature" colspan="4"/>
<field name="signature" colspan="4" widget="richtext"/>
</page>
diff -r 4d7ab7a969d3 -r c2494167ee23 trytond/res/view/user_form_preferences.xml
--- a/trytond/res/view/user_form_preferences.xml Sat Mar 27 21:00:44
2021 +0100
+++ b/trytond/res/view/user_form_preferences.xml Sun Mar 28 19:20:37
2021 +0200
@@ -2,14 +2,19 @@
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<form>
- <label name="name"/>
- <field name="name"/>
+ <group col="4" colspan="3" yalign="0" id="header">
+ <label name="name"/>
+ <field name="name"/>
+ <newline/>
+
+ <label name="email"/>
+ <field name="email" widget="email"/>
+ <label name="password"/>
+ <field name="password" widget="password"/>
+ </group>
+ <field name="avatar" widget="image" height="100" width="100" xexpand="0"/>
<notebook colspan="4">
<page string="User" id="user">
- <label name="email"/>
- <field name="email" widget="email"/>
- <label name="password"/>
- <field name="password" widget="password"/>
<separator name="signature" colspan="4"/>
<field name="signature" colspan="4" widget="richtext"/>
</page>
diff -r 4d7ab7a969d3 -r c2494167ee23 trytond/res/view/user_list.xml
--- a/trytond/res/view/user_list.xml Sat Mar 27 21:00:44 2021 +0100
+++ b/trytond/res/view/user_list.xml Sun Mar 28 19:20:37 2021 +0200
@@ -2,7 +2,9 @@
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tree>
- <field name="name" expand="2"/>
+ <field name="name" expand="2">
+ <prefix id="avatar" icon="avatar_url" icon_type="url" url_size="s"/>
+ </field>
<field name="login" expand="1"/>
<field name="sessions"/>
</tree>
diff -r 4d7ab7a969d3 -r c2494167ee23 trytond/tests/test_res.py
--- a/trytond/tests/test_res.py Sat Mar 27 21:00:44 2021 +0100
+++ b/trytond/tests/test_res.py Sun Mar 28 19:20:37 2021 +0200
@@ -2,13 +2,27 @@
# this repository contains the full copyright notices and license terms.
import unittest
-from .test_tryton import ModuleTestCase
+from trytond.pool import Pool
+
+from .test_tryton import ModuleTestCase, with_transaction
class ResTestCase(ModuleTestCase):
'Test res module'
module = 'res'
+ @with_transaction()
+ def test_user_avatar(self):
+ pool = Pool()
+ User = pool.get('res.user')
+
+ user = User(login="avatar")
+ user.save()
+
+ self.assertEqual(len(user.avatars), 1)
+ self.assertIsNotNone(user.avatar)
+ self.assertRegex(user.avatar_url, r'/avatar/.*/([0-9a-fA-F]{12})')
+
def suite():
return unittest.TestLoader().loadTestsFromTestCase(ResTestCase)