Permalink
Browse files

[MERGE] Merged branch adding an avatar to users. This avatar will rep…

…lace the use of gravatars in various addons. For more info, please refer to the included documentation.
  • Loading branch information...
2 parents 00f5267 + d0691c9 commit 4885a3b32458f93ee34173eacea8783c4dadb6a6 @tde-banana-odoo tde-banana-odoo committed Mar 30, 2012
@@ -0,0 +1,9 @@
+User avatar
+===========
+
+This revision adds an avatar for users. This replaces the use of gravatar to emulate avatars, used in views like the tasks kanban view. Two fields have been added to the res.users model:
+ - avatar_big, a binary field holding the image. It is base-64 encoded, and PIL-supported. Images stored are resized to 540x450 px, to limitate the binary field size.
+ - avatar, a function binary field holding an automatically resized version of the avatar_big field. It is also base-64 encoded, and PIL-supported. Dimensions of the resized avatar are 180x150. This field is used as an inteface to get and set the user avatar.
+When changing the avatar through the avatar function field, the new image is automatically resized to 540x450, and stored in the avatar_big field. This triggers the function field, that will compute a 180x150 resized version of the image.
+
+An avatar field has been added to the users form view, as well as in Preferences. When creating a new user, a default avatar is chosen among 6 possible default images.
View
@@ -6,3 +6,11 @@ OpenERP Server
:maxdepth: 1
test-framework
+
+New feature merges
+++++++++++++++++++
+
+.. toctree::
+ :maxdepth: 1
+
+ api/user_img_specs
@@ -84,12 +84,18 @@
<form string="Users">
<field name="name" readonly="1"/>
<newline/>
- <group colspan="2" col="2">
- <separator string="Preferences" colspan="2"/>
- <field name="view" readonly="0"/>
- <field name="context_lang" readonly="0"/>
- <field name="context_tz" readonly="0"/>
- <field name="menu_tips" readonly="0" groups="base.group_no_one"/>
+ <group colspan="2" col="3">
+ <group col="2" colspan="2">
+ <separator string="Preferences" colspan="2"/>
+ <field name="view" readonly="0"/>
+ <field name="context_lang" readonly="0"/>
+ <field name="context_tz" readonly="0"/>
+ <field name="menu_tips" readonly="0" groups="base.group_no_one"/>
+ </group>
+ <group col="2" colspan="1">
+ <separator string="Avatar" colspan="2"/>
+ <field name="avatar" widget='image' nolabel="1" colspan="2" on_change="onchange_avatar(avatar)"/>
+ </group>
</group>
<group name="default_filters" colspan="2" col="2">
<separator string="Default Filters" colspan="2"/>
@@ -118,32 +124,36 @@
</group>
<notebook colspan="4">
<page string="User">
- <group colspan="4" col="6">
- <!-- Second nested group to avoid misalignment with email prefs groups
- in simplified view -->
- <group colspan="6" col="6">
- <group col="2" colspan="2">
- <separator string="Preferences" colspan="2"/>
- <field name="context_lang"/>
- <field name="context_tz"/>
- <field name="menu_tips"/>
+ <group colspan="4" col="7">
+ <!-- Second nested group to avoid misalignment with email prefs groups
+ in simplified view -->
+ <group colspan="7" col="7">
+ <group col="2" colspan="1">
+ <separator string="Avatar" colspan="2"/>
+ <field name="avatar" widget='image' nolabel="1" colspan="2" on_change="onchange_avatar(avatar)"/>
+ </group>
+ <group col="3" colspan="2">
+ <separator string="Preferences" colspan="3"/>
+ <field name="context_lang"/>
+ <field name="context_tz"/>
+ <field name="menu_tips"/>
+ </group>
+ <group name="default_filters" colspan="2" col="2">
+ <separator string="Default Filters" colspan="2"/>
+ <field name="company_id" required="1" context="{'user_preference': 0}" groups="base.group_multi_company"/>
+ </group>
+ <group colspan="2" col="2" groups="base.group_extended">
+ <separator string="Action" colspan="2"/>
+ <field name="action_id"/>
+ <field domain="[('usage','=','menu')]" name="menu_id" required="True"/>
+ </group>
</group>
- <group name="default_filters" colspan="2" col="2">
- <separator string="Default Filters" colspan="2"/>
- <field name="company_id" required="1" context="{'user_preference': 0}" groups="base.group_multi_company"/>
+ <group colspan="7" col="2">
+ <separator string="Email Preferences" colspan="2"/>
+ <field name="user_email" widget="email"/>
+ <field name="signature"/>
</group>
- <group colspan="2" col="2" groups="base.group_extended">
- <separator string="Action" colspan="2"/>
- <field name="action_id"/>
- <field domain="[('usage','=','menu')]" name="menu_id" required="True"/>
- </group>
- </group>
- <group colspan="6" col="2">
- <separator string="Email Preferences" colspan="2"/>
- <field name="user_email" widget="email"/>
- <field name="signature"/>
</group>
- </group>
</page>
<page string="Access Rights">
<field nolabel="1" name="groups_id"/>
@@ -25,17 +25,20 @@
import pytz
+import io, StringIO
+from lxml import etree
+from lxml.builder import E
import netsvc
-import pooler
-import tools
+import openerp
+import openerp.exceptions
from osv import fields,osv
from osv.orm import browse_record
+from PIL import Image
+import pooler
+import random
from service import security
+import tools
from tools.translate import _
-import openerp
-import openerp.exceptions
-from lxml import etree
-from lxml.builder import E
_logger = logging.getLogger(__name__)
@@ -203,7 +206,6 @@ def _set_interface_type(self, cr, uid, ids, name, value, arg, context=None):
self.write(cr, uid, ids, {'groups_id': [(4, extended_group_id)]}, context=context)
return True
-
def _get_interface_type(self, cr, uid, ids, name, args, context=None):
"""Implementation of 'view' function field getter, returns the type of interface of the users.
@param field_name: Name of the field
@@ -215,6 +217,33 @@ def _get_interface_type(self, cr, uid, ids, name, args, context=None):
extended_users = group_obj.read(cr, uid, extended_group_id, ['users'], context=context)['users']
return dict(zip(ids, ['extended' if user in extended_users else 'simple' for user in ids]))
+ def onchange_avatar(self, cr, uid, ids, value, context=None):
+ if not value:
+ return {'value': {'avatar_big': value, 'avatar': value} }
+ return {'value': {'avatar_big': self._avatar_resize(cr, uid, value, 540, 450, context=context), 'avatar': self._avatar_resize(cr, uid, value, context=context)} }
+
+ def _set_avatar(self, cr, uid, id, name, value, args, context=None):
+ if not value:
+ vals = {'avatar_big': value}
+ else:
+ vals = {'avatar_big': self._avatar_resize(cr, uid, value, 540, 450, context=context)}
+ return self.write(cr, uid, [id], vals, context=context)
+
+ def _avatar_resize(self, cr, uid, avatar, height=180, width=150, context=None):
+ image_stream = io.BytesIO(avatar.decode('base64'))
+ img = Image.open(image_stream)
+ img.thumbnail((height, width), Image.ANTIALIAS)
+ img_stream = StringIO.StringIO()
+ img.save(img_stream, "PNG")
+ return img_stream.getvalue().encode('base64')
+
+ def _get_avatar(self, cr, uid, ids, name, args, context=None):
+ result = dict.fromkeys(ids, False)
+ for user in self.browse(cr, uid, ids, context=context):
+ if user.avatar_big:
+ result[user.id] = self._avatar_resize(cr, uid, user.avatar_big, context=context)
+ return result
+
def _set_new_password(self, cr, uid, id, name, value, args, context=None):
if value is False:
# Do not update the password if no value is provided, ignore silently.
@@ -244,6 +273,11 @@ def _get_password(self, cr, uid, ids, arg, karg, context=None):
"otherwise leave empty. After a change of password, the user has to login again."),
'user_email': fields.char('Email', size=64),
'signature': fields.text('Signature', size=64),
+ 'avatar_big': fields.binary('Big-sized avatar', help="This field holds the image used as avatar for the user. The avatar field is used as an interface to access this field. The image is base64 encoded, and PIL-supported. It is stored as a 540x450 px image, in case a bigger image must be used."),
+ 'avatar': fields.function(_get_avatar, fnct_inv=_set_avatar, string='Avatar', type="binary",
+ store = {
+ 'res.users': (lambda self, cr, uid, ids, c={}: ids, ['avatar_big'], 10),
+ }, help="Image used as avatar for the user. It is automatically resized as a 180x150 px image. This field serves as an interface to the avatar_big field."),
'active': fields.boolean('Active'),
'action_id': fields.many2one('ir.actions.actions', 'Home Action', help="If specified, this action will be opened at logon for this user, in addition to the standard menu."),
'menu_id': fields.many2one('ir.actions.actions', 'Menu Action', help="If specified, the action will replace the standard menu for this user."),
@@ -355,9 +389,15 @@ def _get_group(self,cr, uid, context=None):
pass
return result
+ def _get_avatar(self, cr, uid, context=None):
+ # default avatar file name: avatar0 -> avatar6.png, choose randomly
+ avatar_path = openerp.modules.get_module_resource('base', 'static/src/img', 'avatar%d.png' % random.randint(0, 6))
+ return self._avatar_resize(cr, uid, open(avatar_path, 'rb').read().encode('base64'), context=context)
+
_defaults = {
'password' : '',
'context_lang': 'en_US',
+ 'avatar': _get_avatar,
'active' : True,
'menu_id': _get_menu,
'company_id': _get_company,
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 4885a3b

Please sign in to comment.