@@ -1,10 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data noupdate="1">
|
||||
<record id="partner_demo" model="res.partner">
|
||||
<field name="name">Demo User</field>
|
||||
<field name="company_id" ref="main_company"/>
|
||||
<field name="customer" eval="False"/>
|
||||
</record>
|
||||
<record id="user_demo" model="res.users">
|
||||
<field name="partner_id" ref="base.partner_demo"/>
|
||||
<field name="login">demo</field>
|
||||
<field name="password">demo</field>
|
||||
<field name="name">Demo User</field>
|
||||
<field name="signature">Mr Demo</field>
|
||||
<field name="company_id" ref="main_company"/>
|
||||
<field name="groups_id" eval="[(6,0,[ref('base.group_user')])]"/>
|
||||
|
||||
@@ -556,29 +556,6 @@
|
||||
|
||||
<menuitem action="ir_action_wizard" id="menu_ir_action_wizard" parent="base.next_id_6"/>
|
||||
|
||||
<!-- Needaction mechanism -->
|
||||
<record model="ir.ui.view" id="view_ir_needaction_users_rel_tree">
|
||||
<field name="name">ir.needaction_users_rel.tree</field>
|
||||
<field name="model">ir.needaction_users_rel</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Subscription">
|
||||
<field name="user_id"/>
|
||||
<field name="res_model"/>
|
||||
<field name="res_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_view_needaction_users_rel" model="ir.actions.act_window">
|
||||
<field name="name">Need action relationships</field>
|
||||
<field name="res_model">ir.needaction_users_rel</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_needaction_users_rel" name="Need actions" parent="base.menu_users" sequence="20" action="action_view_needaction_users_rel" groups="base.group_no_one"/>
|
||||
|
||||
|
||||
<!-- View -->
|
||||
<record id="view_view_form" model="ir.ui.view">
|
||||
<field name="name">ir.ui.view</field>
|
||||
|
||||
@@ -44,7 +44,7 @@ class actions(osv.osv):
|
||||
_table = 'ir_actions'
|
||||
_order = 'name'
|
||||
_columns = {
|
||||
'name': fields.char('Action Name', required=True, size=64),
|
||||
'name': fields.char('Name', size=64, required=True),
|
||||
'type': fields.char('Action Type', required=True, size=32,readonly=True),
|
||||
'usage': fields.char('Action Usage', size=32),
|
||||
}
|
||||
@@ -861,11 +861,10 @@ class act_client(osv.osv):
|
||||
_order = 'name'
|
||||
|
||||
def _get_params(self, cr, uid, ids, field_name, arg, context):
|
||||
return dict([
|
||||
((record.id, ast.literal_eval(record.params_store))
|
||||
if record.params_store else (record.id, False))
|
||||
for record in self.browse(cr, uid, ids, context=context)
|
||||
])
|
||||
result = {}
|
||||
for record in self.browse(cr, uid, ids, context=context):
|
||||
result[record.id] = record.params_store and eval(record.params_store, {'uid': uid}) or False
|
||||
return result
|
||||
|
||||
def _set_params(self, cr, uid, id, field_name, field_value, arg, context):
|
||||
if isinstance(field_value, dict):
|
||||
@@ -874,10 +873,15 @@ class act_client(osv.osv):
|
||||
self.write(cr, uid, id, {'params_store': field_value}, context=context)
|
||||
|
||||
_columns = {
|
||||
'name': fields.char('Action Name', required=True, size=64, translate=True),
|
||||
'tag': fields.char('Client action tag', size=64, required=True,
|
||||
help="An arbitrary string, interpreted by the client"
|
||||
" according to its own needs and wishes. There "
|
||||
"is no central tag repository across clients."),
|
||||
'res_model': fields.char('Destination Model', size=64,
|
||||
help="Optional model, mostly used for needactions."),
|
||||
'context': fields.char('Context Value', size=250, required=True,
|
||||
help="Context dictionary as Python expression, empty by default (Default: {})"),
|
||||
'params': fields.function(_get_params, fnct_inv=_set_params,
|
||||
type='binary',
|
||||
string="Supplementary arguments",
|
||||
@@ -887,6 +891,7 @@ class act_client(osv.osv):
|
||||
}
|
||||
_defaults = {
|
||||
'type': 'ir.actions.client',
|
||||
'context': '{}',
|
||||
|
||||
}
|
||||
act_client()
|
||||
|
||||
@@ -19,180 +19,45 @@
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import openerp.pooler as pooler
|
||||
from operator import itemgetter
|
||||
from osv import osv, fields
|
||||
from tools.translate import _
|
||||
from osv import osv
|
||||
|
||||
class ir_needaction_users_rel(osv.Model):
|
||||
''' ir_needaction_users_rel holds data related to the needaction
|
||||
mechanism inside OpenERP. A row in this model is characterized
|
||||
by:
|
||||
- res_model: model of the record requiring an action
|
||||
- res_id: ID of the record requiring an action
|
||||
- user_id: foreign key to the res.users table, to the user that has to
|
||||
perform the action
|
||||
This model can be seen as a many2many, linking (res_model, res_id) to users
|
||||
(those whose attention is required on the record). '''
|
||||
|
||||
_name = 'ir.needaction_users_rel'
|
||||
_description = 'Needaction relationship table'
|
||||
_rec_name = 'id'
|
||||
_order = 'id desc'
|
||||
_columns = {
|
||||
'res_model': fields.char('Related Document Model', size=128,
|
||||
select=1, required=True),
|
||||
'res_id': fields.integer('Related Document ID',
|
||||
select=1, required=True),
|
||||
'user_id': fields.many2one('res.users', string='Related User',
|
||||
ondelete='cascade', select=1, required=True),
|
||||
}
|
||||
|
||||
def _get_users(self, cr, uid, res_ids, res_model, context=None):
|
||||
"""Given res_ids of res_model, get user_ids present in table"""
|
||||
rel_ids = self.search(cr, uid, [('res_model', '=', res_model), ('res_id', 'in', res_ids)], context=context)
|
||||
return list(set(map(itemgetter('user_id'), self.read(cr, uid, rel_ids, ['user_id'], context=context))))
|
||||
|
||||
def create_users(self, cr, uid, res_ids, res_model, user_ids, context=None):
|
||||
"""Given res_ids of res_model, add user_ids to the relationship table"""
|
||||
for res_id in res_ids:
|
||||
for user_id in user_ids:
|
||||
self.create(cr, uid, {'res_model': res_model, 'res_id': res_id, 'user_id': user_id}, context=context)
|
||||
return True
|
||||
|
||||
def unlink_users(self, cr, uid, res_ids, res_model, context=None):
|
||||
"""Given res_ids of res_model, delete all entries in the relationship table"""
|
||||
to_del_ids = self.search(cr, uid, [('res_model', '=', res_model), ('res_id', 'in', res_ids)], context=context)
|
||||
return self.unlink(cr, uid, to_del_ids, context=context)
|
||||
|
||||
def update_users(self, cr, uid, res_ids, res_model, user_ids, context=None):
|
||||
"""Given res_ids of res_model, update their entries in the relationship table to user_ids"""
|
||||
# read current records
|
||||
cur_users = self._get_users(cr, uid, res_ids, res_model, context=context)
|
||||
if len(cur_users) == len(user_ids) and all(cur_user in user_ids for cur_user in cur_users):
|
||||
return True
|
||||
# unlink old records
|
||||
self.unlink_users(cr, uid, res_ids, res_model, context=context)
|
||||
# link new records
|
||||
self.create_users(cr, uid, res_ids, res_model, user_ids, context=context)
|
||||
return True
|
||||
|
||||
|
||||
class ir_needaction_mixin(osv.Model):
|
||||
class ir_needaction_mixin(osv.AbstractModel):
|
||||
'''Mixin class for objects using the need action feature.
|
||||
|
||||
Need action feature can be used by objects having to be able to
|
||||
signal that an action is required on a particular record. If in
|
||||
the business logic an action must be performed by somebody, for
|
||||
instance validation by a manager, this mechanism allows to set a
|
||||
|
||||
Need action feature can be used by models that have to be able to
|
||||
signal that an action is required on a particular record. If in
|
||||
the business logic an action must be performed by somebody, for
|
||||
instance validation by a manager, this mechanism allows to set a
|
||||
list of users asked to perform an action.
|
||||
|
||||
This class wraps a class (ir.ir_needaction_users_rel) that
|
||||
behaves like a many2many field. This class handles the low-level
|
||||
considerations of updating relationships. Every change made on
|
||||
the record calls a method that updates the relationships.
|
||||
|
||||
Objects using the 'need_action' feature should override the
|
||||
``get_needaction_user_ids`` method. This methods returns a
|
||||
dictionary whose keys are record ids, and values a list of user
|
||||
ids, like in a many2many relationship. Therefore by defining
|
||||
only one method, you can specify if an action is required by
|
||||
defining the users that have to do it, in every possible
|
||||
situation.
|
||||
|
||||
This class also offers several global services:
|
||||
- ``needaction_get_record_ids``: for the current model and uid, get
|
||||
all record ids that ask this user to perform an action. This
|
||||
mechanism is used for instance to display the number of pending
|
||||
actions in menus, such as Leads (12)
|
||||
- ``needaction_get_action_count``: as ``needaction_get_record_ids``
|
||||
but returns only the number of action, not the ids (performs a
|
||||
search with count=True)
|
||||
|
||||
The ``ir_needaction_mixin`` class adds a calculated field
|
||||
``needaction_pending``. This function field allows to state
|
||||
whether a given record has a needaction for the current user.
|
||||
This is usefull if you want to customize views according to the
|
||||
needaction feature. For example, you may want to set records in
|
||||
bold in a list view if the current user has an action to perform
|
||||
on the record. '''
|
||||
|
||||
Models using the 'need_action' feature should override the
|
||||
``_needaction_domain_get`` method. This method returns a
|
||||
domain to filter records requiring an action for a specific user.
|
||||
|
||||
This class also offers several global services:
|
||||
- ``_needaction_count``: returns the number of actions uid has to perform
|
||||
'''
|
||||
|
||||
_name = 'ir.needaction_mixin'
|
||||
_description = 'Need action mixin'
|
||||
|
||||
def get_needaction_pending(self, cr, uid, ids, name, arg, context=None):
|
||||
res = {}
|
||||
needaction_user_ids = self.get_needaction_user_ids(cr, uid, ids, context=context)
|
||||
for id in ids:
|
||||
res[id] = uid in needaction_user_ids[id]
|
||||
return res
|
||||
_needaction = True
|
||||
|
||||
def search_needaction_pending(self, cr, uid, self_again, field_name, criterion, context=None):
|
||||
ids = self.needaction_get_record_ids(
|
||||
cr, uid, uid, limit=1024, context=context)
|
||||
return [('id', 'in', ids)]
|
||||
|
||||
_columns = {
|
||||
'needaction_pending': fields.function(
|
||||
get_needaction_pending, type='boolean',
|
||||
fnct_search=search_needaction_pending,
|
||||
string='Need action pending',
|
||||
help="If True, this field states that users have to perform an " \
|
||||
"action This field comes from the ir.needaction_mixin class."),
|
||||
}
|
||||
|
||||
#------------------------------------------------------
|
||||
# Addon API
|
||||
# Addons API
|
||||
#------------------------------------------------------
|
||||
|
||||
def get_needaction_user_ids(self, cr, uid, ids, context=None):
|
||||
""" Returns the user_ids that have to perform an action
|
||||
:return: dict { record_id: [user_ids], }
|
||||
|
||||
def _needaction_domain_get(self, cr, uid, context=None):
|
||||
""" Returns the domain to filter records that require an action
|
||||
:return: domain or False is no action
|
||||
"""
|
||||
return dict((id,list()) for id in ids)
|
||||
|
||||
def create(self, cr, uid, values, context=None):
|
||||
rel_obj = self.pool.get('ir.needaction_users_rel')
|
||||
# perform create
|
||||
obj_id = super(ir_needaction_mixin, self).create(cr, uid, values, context=context)
|
||||
# link user_ids
|
||||
needaction_user_ids = self.get_needaction_user_ids(cr, uid, [obj_id], context=context)
|
||||
rel_obj.create_users(cr, uid, [obj_id], self._name, needaction_user_ids[obj_id], context=context)
|
||||
return obj_id
|
||||
|
||||
def write(self, cr, uid, ids, values, context=None):
|
||||
rel_obj = self.pool.get('ir.needaction_users_rel')
|
||||
# perform write
|
||||
write_res = super(ir_needaction_mixin, self).write(cr, uid, ids, values, context=context)
|
||||
# get and update user_ids
|
||||
needaction_user_ids = self.get_needaction_user_ids(cr, uid, ids, context=context)
|
||||
for id in ids:
|
||||
rel_obj.update_users(cr, uid, [id], self._name, needaction_user_ids[id], context=context)
|
||||
return write_res
|
||||
|
||||
def unlink(self, cr, uid, ids, context=None):
|
||||
# unlink user_ids
|
||||
rel_obj = self.pool.get('ir.needaction_users_rel')
|
||||
rel_obj.unlink_users(cr, uid, ids, self._name, context=context)
|
||||
# perform unlink
|
||||
return super(ir_needaction_mixin, self).unlink(cr, uid, ids, context=context)
|
||||
|
||||
return False
|
||||
|
||||
#------------------------------------------------------
|
||||
# "Need action" API
|
||||
#------------------------------------------------------
|
||||
|
||||
def needaction_get_record_ids(self, cr, uid, user_id, limit=80, context=None):
|
||||
"""Given the current model and a user_id
|
||||
return the record ids that require the user to perform an
|
||||
action"""
|
||||
rel_obj = self.pool.get('ir.needaction_users_rel')
|
||||
rel_ids = rel_obj.search(cr, uid, [('res_model', '=', self._name), ('user_id', '=', user_id)], limit=limit, context=context)
|
||||
return map(itemgetter('res_id'), rel_obj.read(cr, uid, rel_ids, ['res_id'], context=context))
|
||||
|
||||
def needaction_get_action_count(self, cr, uid, user_id, limit=80, context=None):
|
||||
"""Given the current model and a user_id
|
||||
get the number of actions it has to perform"""
|
||||
rel_obj = self.pool.get('ir.needaction_users_rel')
|
||||
return rel_obj.search(cr, uid, [('res_model', '=', self._name), ('user_id', '=', user_id)], limit=limit, count=True, context=context)
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
def _needaction_count(self, cr, uid, domain=[], context=None):
|
||||
""" Get the number of actions uid has to perform. """
|
||||
dom = self._needaction_domain_get(cr, uid, context=context)
|
||||
if not dom:
|
||||
return 0
|
||||
return self.search(cr, uid, (domain or []) +dom, context=context, count=True)
|
||||
|
||||
@@ -210,7 +210,7 @@ class ir_sequence(openerp.osv.osv.osv):
|
||||
|
||||
def next_by_id(self, cr, uid, sequence_id, context=None):
|
||||
""" Draw an interpolated string using the specified sequence."""
|
||||
self.check_read(cr, uid)
|
||||
self.check_access_rights(cr, uid, 'read')
|
||||
company_ids = self.pool.get('res.company').search(cr, uid, [], order='company_id', context=context) + [False]
|
||||
ids = self.search(cr, uid, ['&',('id','=', sequence_id),('company_id','in',company_ids)])
|
||||
return self._next(cr, uid, ids, context)
|
||||
@@ -227,7 +227,7 @@ class ir_sequence(openerp.osv.osv.osv):
|
||||
sequence selection. A matching sequence for that
|
||||
specific company will get higher priority.
|
||||
"""
|
||||
self.check_read(cr, uid)
|
||||
self.check_access_rights(cr, uid, 'read')
|
||||
company_ids = self.pool.get('res.company').search(cr, uid, [], order='company_id', context=context) + [False]
|
||||
ids = self.search(cr, uid, ['&',('code','=', sequence_code),('company_id','in',company_ids)])
|
||||
return self._next(cr, uid, ids, context)
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
import base64
|
||||
import re
|
||||
import threading
|
||||
|
||||
from tools.safe_eval import safe_eval as eval
|
||||
import tools
|
||||
import openerp.modules
|
||||
from osv import fields, osv
|
||||
@@ -256,23 +256,24 @@ class ir_ui_menu(osv.osv):
|
||||
|
||||
return res
|
||||
|
||||
def _get_needaction_info(self, cr, uid, id, domain=[], context={}):
|
||||
return [False, 0]
|
||||
|
||||
def _get_needaction(self, cr, uid, ids, field_names, args, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
res = {}
|
||||
for menu in self.browse(cr, uid, ids, context=context):
|
||||
res[menu.id] = {}
|
||||
if menu.action and menu.action.type == 'ir.actions.act_window' and menu.action.res_model:
|
||||
menu_needaction_res = self.pool.get(menu.action.res_model)._get_needaction_info(cr, uid, uid, domain=menu.action.domain, context=context)
|
||||
else:
|
||||
menu_needaction_res = [False, 0]
|
||||
res[menu.id]['needaction_enabled'] = menu_needaction_res[0]
|
||||
res[menu.id]['needaction_counter'] = menu_needaction_res[1]
|
||||
res[menu.id] = {
|
||||
'needaction_enabled': False,
|
||||
'needaction_counter': False,
|
||||
}
|
||||
if menu.action and menu.action.type in ('ir.actions.act_window','ir.actions.client') and menu.action.res_model:
|
||||
obj = self.pool.get(menu.action.res_model)
|
||||
if obj._needaction:
|
||||
if menu.action.type=='ir.actions.act_window':
|
||||
dom = menu.action.domain and eval(menu.action.domain, {'uid': uid}) or []
|
||||
else:
|
||||
dom = eval(menu.action.params_store or '{}', {'uid': uid}).get('domain')
|
||||
res[menu.id]['needaction_enabled'] = obj._needaction
|
||||
res[menu.id]['needaction_counter'] = obj._needaction_count(cr, uid, dom, context=context)
|
||||
return res
|
||||
|
||||
|
||||
_columns = {
|
||||
'name': fields.char('Menu', size=64, required=True, translate=True),
|
||||
'sequence': fields.integer('Sequence'),
|
||||
|
||||
@@ -215,6 +215,7 @@ class res_partner(osv.osv):
|
||||
"Use this field anywhere a small image is required."),
|
||||
'company_id': fields.many2one('res.company', 'Company', select=1),
|
||||
'color': fields.integer('Color Index'),
|
||||
'user_ids': fields.one2many('res.users', 'partner_id', 'Users'),
|
||||
'contact_address': fields.function(_address_display, type='char', string='Complete Address'),
|
||||
}
|
||||
|
||||
@@ -357,35 +358,33 @@ class res_partner(osv.osv):
|
||||
res.append((record.id, name))
|
||||
return res
|
||||
|
||||
def _parse_partner_name(self, text, context=None):
|
||||
""" Supported syntax:
|
||||
- 'Raoul <raoul@grosbedon.fr>': will find name and email address
|
||||
- otherwise: default, everything is set as the name """
|
||||
match = re.search(r'([^\s,<@]+@[^>\s,]+)', text)
|
||||
if match:
|
||||
email = match.group(1)
|
||||
name = text[:text.index(email)].replace('"','').replace('<','').strip()
|
||||
else:
|
||||
name, email = text, ''
|
||||
return name, email
|
||||
|
||||
def name_create(self, cr, uid, name, context=None):
|
||||
""" Override of orm's name_create method for partners. The purpose is
|
||||
to handle some basic formats to create partners using the
|
||||
name_create.
|
||||
Supported syntax:
|
||||
- 'raoul@grosbedon.fr': create a partner with name raoul@grosbedon.fr
|
||||
and sets its email to raoul@grosbedon.fr
|
||||
- 'Raoul Grosbedon <raoul@grosbedon.fr>': create a partner with name
|
||||
Raoul Grosbedon, and set its email to raoul@grosbedon.fr
|
||||
- anything else: fall back on the default name_create
|
||||
Regex :
|
||||
- ([a-zA-Z0-9._%-]+@[a-zA-Z0-9_-]+\.[a-zA-Z0-9._]{1,8}): raoul@grosbedon.fr
|
||||
- ([\w\s.\\-]+)[\<]([a-zA-Z0-9._%-]+@[a-zA-Z0-9_-]+\.[a-zA-Z0-9._]{1,8})[\>]:
|
||||
Raoul Grosbedon, raoul@grosbedon.fr
|
||||
"""
|
||||
contact_regex = re.compile('([\w\s.\\-]+)[\<]([a-zA-Z0-9._%-]+@[a-zA-Z0-9_-]+\.[a-zA-Z0-9._]{1,8})[\>]')
|
||||
email_regex = re.compile('([a-zA-Z0-9._%-]+@[a-zA-Z0-9_-]+\.[a-zA-Z0-9._]{1,8})')
|
||||
contact_regex_res = contact_regex.findall(name)
|
||||
email_regex_res = email_regex.findall(name)
|
||||
email = False
|
||||
if contact_regex_res:
|
||||
name = contact_regex_res[0][0].rstrip(' ') # remove extra spaces on the right
|
||||
email = contact_regex_res[0][1]
|
||||
elif email_regex_res:
|
||||
email = '%s' % (email_regex_res[0])
|
||||
else:
|
||||
pass
|
||||
data = {self._rec_name: name, 'email': email}
|
||||
rec_id = self.create(cr, uid, data, context);
|
||||
If only an email address is received and that the regex cannot find
|
||||
a name, the name will have the email value.
|
||||
If 'force_email' key in context: must find the email address. """
|
||||
if context is None:
|
||||
context = {}
|
||||
name, email = self._parse_partner_name(name, context=context)
|
||||
if context.get('force_email') and not email:
|
||||
raise osv.except_osv(_('Warning'), _("Couldn't create contact without email address !"))
|
||||
if not name and email:
|
||||
name = email
|
||||
rec_id = self.create(cr, uid, {self._rec_name: name or email, 'email': email or False}, context=context)
|
||||
return self.name_get(cr, uid, [rec_id], context)[0]
|
||||
|
||||
def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100):
|
||||
@@ -410,6 +409,21 @@ class res_partner(osv.osv):
|
||||
return self.name_get(cr, uid, ids, context)
|
||||
return super(res_partner,self).name_search(cr, uid, name, args, operator=operator, context=context, limit=limit)
|
||||
|
||||
def find_or_create(self, cr, uid, email, context=None):
|
||||
""" Find a partner with the given ``email`` or use :py:method:`~.name_create`
|
||||
to create one
|
||||
|
||||
:param str email: email-like string, which should contain at least one email,
|
||||
e.g. ``"Raoul Grosbedon <r.g@grosbedon.fr>"``"""
|
||||
assert email, 'an email is required for find_or_create to work'
|
||||
emails = tools.email_split(email)
|
||||
if emails:
|
||||
email = emails[0]
|
||||
ids = self.search(cr, uid, [('email','ilike',email)], context=context)
|
||||
if not ids:
|
||||
return self.name_create(cr, uid, email, context=context)[0]
|
||||
return ids[0]
|
||||
|
||||
def _email_send(self, cr, uid, ids, email_from, subject, body, on_error=None):
|
||||
partners = self.browse(cr, uid, ids)
|
||||
for partner in partners:
|
||||
|
||||
@@ -186,13 +186,10 @@
|
||||
<td class="oe_kanban_title1" align="left" valign="middle">
|
||||
<h4><a type="open"><field name="name"/></a></h4>
|
||||
<i><div t-if="record.function.raw_value">
|
||||
<field name="function"/><br/></div></i>
|
||||
</td>
|
||||
<td valign="top" align="right">
|
||||
<a t-if="record.email.raw_value" title="Mail" t-att-href="'mailto:'+record.email.value" style="text-decoration: none;" >
|
||||
<img src="/web/static/src/img/icons/terp-mail-message-new.png" border="0" width="16" height="16"/>
|
||||
</a>
|
||||
|
||||
<field name="function"/></div></i>
|
||||
<div><a t-if="record.email.raw_value" title="Mail" t-att-href="'mailto:'+record.email.value">
|
||||
<field name="email"/>
|
||||
</a></div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -120,6 +120,5 @@
|
||||
"access_ir_config_parameter","ir_config_parameter","model_ir_config_parameter",,1,0,0,0
|
||||
"access_ir_mail_server_all","ir_mail_server","model_ir_mail_server",,1,0,0,0
|
||||
"access_ir_actions_client","ir_actions_client all","model_ir_actions_client",,1,0,0,0
|
||||
"access_ir_needaction_users_rel","ir_needaction_users_rel","model_ir_needaction_users_rel",,1,1,1,1
|
||||
"access_ir_needaction_mixin","ir_needaction_mixin","model_ir_needaction_mixin",,1,1,1,1
|
||||
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import test_ir_values
|
||||
import test_ir_values, test_base
|
||||
|
||||
checks = [
|
||||
test_ir_values,
|
||||
test_ir_values, test_base
|
||||
]
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import unittest2
|
||||
|
||||
import openerp.tests.common as common
|
||||
|
||||
class test_base(common.TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super(test_base,self).setUp()
|
||||
self.res_partner = self.registry('res.partner')
|
||||
|
||||
# samples use effective TLDs from the Mozilla public suffix
|
||||
# list at http://publicsuffix.org
|
||||
self.samples = [
|
||||
('"Raoul Grosbedon" <raoul@chirurgiens-dentistes.fr> ', 'Raoul Grosbedon', 'raoul@chirurgiens-dentistes.fr'),
|
||||
('ryu+giga-Sushi@aizubange.fukushima.jp', '', 'ryu+giga-Sushi@aizubange.fukushima.jp'),
|
||||
('Raoul chirurgiens-dentistes.fr', 'Raoul chirurgiens-dentistes.fr', ''),
|
||||
(" Raoul O'hara <!@historicalsociety.museum>", "Raoul O'hara", '!@historicalsociety.museum')
|
||||
]
|
||||
|
||||
def test_00_res_partner_name_create(self):
|
||||
cr, uid = self.cr, self.uid
|
||||
parse = self.res_partner._parse_partner_name
|
||||
for text, name, mail in self.samples:
|
||||
self.assertEqual((name,mail), parse(text), 'Partner name parsing failed')
|
||||
partner_id, dummy = self.res_partner.name_create(cr, uid, text)
|
||||
partner = self.res_partner.browse(cr, uid, partner_id)
|
||||
self.assertEqual(name or mail, partner.name, 'Partner name incorrect')
|
||||
self.assertEqual(mail or False, partner.email, 'Partner email incorrect')
|
||||
|
||||
def test_10_res_partner_find_or_create(self):
|
||||
cr,uid = self.cr, self.uid
|
||||
email = self.samples[0][0]
|
||||
partner_id, dummy = self.res_partner.name_create(cr, uid, email)
|
||||
found_id = self.res_partner.find_or_create(cr, uid, email)
|
||||
self.assertEqual(partner_id, found_id, 'find_or_create failed')
|
||||
new_id = self.res_partner.find_or_create(cr, uid, self.samples[1][0])
|
||||
self.assertTrue(new_id > partner_id, 'find_or_create failed - should have created new one')
|
||||
new_id2 = self.res_partner.find_or_create(cr, uid, self.samples[2][0])
|
||||
self.assertTrue(new_id2 > new_id, 'find_or_create failed - should have created new one again')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest2.main()
|
||||
@@ -515,7 +515,8 @@ class one2many(_column):
|
||||
for id in ids:
|
||||
res[id] = []
|
||||
|
||||
ids2 = obj.pool.get(self._obj).search(cr, user, self._domain + [(self._fields_id, 'in', ids)], limit=self._limit, context=context)
|
||||
domain = self._domain(obj) if callable(self._domain) else self._domain
|
||||
ids2 = obj.pool.get(self._obj).search(cr, user, domain + [(self._fields_id, 'in', ids)], limit=self._limit, context=context)
|
||||
for r in obj.pool.get(self._obj)._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
|
||||
if r[self._fields_id] in res:
|
||||
res[r[self._fields_id]].append(r['id'])
|
||||
@@ -557,7 +558,8 @@ class one2many(_column):
|
||||
reverse_rel = obj._all_columns.get(self._fields_id)
|
||||
assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
|
||||
# if the o2m has a static domain we must respect it when unlinking
|
||||
extra_domain = self._domain if isinstance(getattr(self, '_domain', None), list) else []
|
||||
domain = self._domain(obj) if callable(self._domain) else self._domain
|
||||
extra_domain = domain or []
|
||||
ids_to_unlink = obj.search(cr, user, [(self._fields_id,'=',id)] + extra_domain, context=context)
|
||||
# If the model has cascade deletion, we delete the rows because it is the intended behavior,
|
||||
# otherwise we only nullify the reverse foreign key column.
|
||||
@@ -575,7 +577,8 @@ class one2many(_column):
|
||||
return result
|
||||
|
||||
def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
|
||||
return obj.pool.get(self._obj).name_search(cr, uid, value, self._domain, operator, context=context,limit=limit)
|
||||
domain = self._domain(obj) if callable(self._domain) else self._domain
|
||||
return obj.pool.get(self._obj).name_search(cr, uid, value, domain, operator, context=context,limit=limit)
|
||||
|
||||
|
||||
@classmethod
|
||||
@@ -1540,9 +1543,7 @@ def field_to_dict(model, cr, user, field, context=None):
|
||||
"""
|
||||
|
||||
res = {'type': field._type}
|
||||
# This additional attributes for M2M and function field is added
|
||||
# because we need to display tooltip with this additional information
|
||||
# when client is started in debug mode.
|
||||
# some attributes for m2m/function field are added as debug info only
|
||||
if isinstance(field, function):
|
||||
res['function'] = field._fnct and field._fnct.func_name or False
|
||||
res['store'] = field.store
|
||||
@@ -1577,7 +1578,7 @@ def field_to_dict(model, cr, user, field, context=None):
|
||||
res['selection'] = field.selection(model, cr, user, context)
|
||||
if res['type'] in ('one2many', 'many2many', 'many2one'):
|
||||
res['relation'] = field._obj
|
||||
res['domain'] = field._domain
|
||||
res['domain'] = field._domain(model) if callable(field._domain) else field._domain
|
||||
res['context'] = field._context
|
||||
|
||||
if isinstance(field, one2many):
|
||||
|
||||
+20
-60
@@ -654,6 +654,7 @@ class BaseModel(object):
|
||||
may be set to False.
|
||||
"""
|
||||
__metaclass__ = MetaModel
|
||||
_auto = True # create database backend
|
||||
_register = False # Set to false if the model shouldn't be automatically discovered.
|
||||
_name = None
|
||||
_columns = {}
|
||||
@@ -667,6 +668,7 @@ class BaseModel(object):
|
||||
_order = 'id'
|
||||
_sequence = None
|
||||
_description = None
|
||||
_needaction = False
|
||||
|
||||
# dict of {field:method}, with method returning the name_get of records
|
||||
# to include in the _read_group, if grouped on this field
|
||||
@@ -1823,12 +1825,11 @@ class BaseModel(object):
|
||||
fields = {}
|
||||
if node.tag == 'diagram':
|
||||
if node.getchildren()[0].tag == 'node':
|
||||
node_fields = self.pool.get(node.getchildren()[0].get('object')).fields_get(cr, user, None, context)
|
||||
node_model = self.pool.get(node.getchildren()[0].get('object'))
|
||||
node_fields = node_model.fields_get(cr, user, None, context)
|
||||
fields.update(node_fields)
|
||||
if not node.get("create"):
|
||||
fn = getattr(self.pool.get(node.getchildren()[1].get('object')), 'check_create')
|
||||
if not fn(cr, user, raise_exception=False):
|
||||
node.set("create", 'false')
|
||||
if not node.get("create") and not node_model.check_access_rights(cr, user, 'create', raise_exception=False):
|
||||
node.set("create", 'false')
|
||||
if node.getchildren()[1].tag == 'arrow':
|
||||
arrow_fields = self.pool.get(node.getchildren()[1].get('object')).fields_get(cr, user, None, context)
|
||||
fields.update(arrow_fields)
|
||||
@@ -1837,8 +1838,8 @@ class BaseModel(object):
|
||||
fields_def = self.__view_look_dom(cr, user, node, view_id, False, fields, context=context)
|
||||
node = self._disable_workflow_buttons(cr, user, node)
|
||||
if node.tag in ('kanban', 'tree', 'form', 'gantt'):
|
||||
for action, fn in (('create', 'check_create'), ('delete', 'check_unlink'), ('edit', 'check_write')):
|
||||
if not node.get(action) and not getattr(self, fn)(cr, user, raise_exception=False):
|
||||
for action, operation in (('create', 'create'), ('delete', 'unlink'), ('edit', 'write')):
|
||||
if not node.get(action) and not self.check_access_rights(cr, user, operation, raise_exception=False):
|
||||
node.set(action, 'false')
|
||||
arch = etree.tostring(node, encoding="utf-8").replace('\t', '')
|
||||
for k in fields.keys():
|
||||
@@ -2354,7 +2355,7 @@ class BaseModel(object):
|
||||
def read_string(self, cr, uid, id, langs, fields=None, context=None):
|
||||
res = {}
|
||||
res2 = {}
|
||||
self.pool.get('ir.translation').check_read(cr, uid)
|
||||
self.pool.get('ir.translation').check_access_rights(cr, uid, 'read')
|
||||
if not fields:
|
||||
fields = self._columns.keys() + self._inherit_fields.keys()
|
||||
#FIXME: collect all calls to _get_source into one SQL call.
|
||||
@@ -2378,7 +2379,7 @@ class BaseModel(object):
|
||||
return res
|
||||
|
||||
def write_string(self, cr, uid, id, langs, vals, context=None):
|
||||
self.pool.get('ir.translation').check_write(cr, uid)
|
||||
self.pool.get('ir.translation').check_access_rights(cr, uid, 'write')
|
||||
#FIXME: try to only call the translation in one SQL
|
||||
for lang in langs:
|
||||
for field in vals:
|
||||
@@ -2526,7 +2527,7 @@ class BaseModel(object):
|
||||
|
||||
"""
|
||||
context = context or {}
|
||||
self.check_read(cr, uid)
|
||||
self.check_access_rights(cr, uid, 'read')
|
||||
if not fields:
|
||||
fields = self._columns.keys()
|
||||
|
||||
@@ -3407,8 +3408,7 @@ class BaseModel(object):
|
||||
if context is None:
|
||||
context = {}
|
||||
|
||||
write_access = self.check_write(cr, user, False) or \
|
||||
self.check_create(cr, user, False)
|
||||
write_access = self.check_access_rights(cr, user, 'write') or self.check_access_rights(cr, user, 'create')
|
||||
|
||||
res = {}
|
||||
|
||||
@@ -3472,7 +3472,7 @@ class BaseModel(object):
|
||||
|
||||
if not context:
|
||||
context = {}
|
||||
self.check_read(cr, user)
|
||||
self.check_access_rights(cr, user, 'read')
|
||||
if not fields:
|
||||
fields = list(set(self._columns.keys() + self._inherit_fields.keys()))
|
||||
if isinstance(ids, (int, long)):
|
||||
@@ -3746,18 +3746,6 @@ class BaseModel(object):
|
||||
according to the access rights."""
|
||||
return self.pool.get('ir.model.access').check(cr, uid, self._name, operation, raise_exception)
|
||||
|
||||
def check_create(self, cr, uid, raise_exception=True):
|
||||
return self.check_access_rights(cr, uid, 'create', raise_exception)
|
||||
|
||||
def check_read(self, cr, uid, raise_exception=True):
|
||||
return self.check_access_rights(cr, uid, 'read', raise_exception)
|
||||
|
||||
def check_unlink(self, cr, uid, raise_exception=True):
|
||||
return self.check_access_rights(cr, uid, 'unlink', raise_exception)
|
||||
|
||||
def check_write(self, cr, uid, raise_exception=True):
|
||||
return self.check_access_rights(cr, uid, 'write', raise_exception)
|
||||
|
||||
def check_access_rule(self, cr, uid, ids, operation, context=None):
|
||||
"""Verifies that the operation given by ``operation`` is allowed for the user
|
||||
according to ir.rules.
|
||||
@@ -3821,7 +3809,7 @@ class BaseModel(object):
|
||||
|
||||
self._check_concurrency(cr, ids, context)
|
||||
|
||||
self.check_unlink(cr, uid)
|
||||
self.check_access_rights(cr, uid, 'unlink')
|
||||
|
||||
ir_property = self.pool.get('ir.property')
|
||||
|
||||
@@ -3957,7 +3945,7 @@ class BaseModel(object):
|
||||
ids = [ids]
|
||||
|
||||
self._check_concurrency(cr, ids, context)
|
||||
self.check_write(cr, user)
|
||||
self.check_access_rights(cr, user, 'write')
|
||||
|
||||
result = self._store_get_values(cr, user, ids, vals.keys(), context) or []
|
||||
|
||||
@@ -4177,7 +4165,7 @@ class BaseModel(object):
|
||||
if self.is_transient():
|
||||
self._transient_vacuum(cr, user)
|
||||
|
||||
self.check_create(cr, user)
|
||||
self.check_access_rights(cr, user, 'create')
|
||||
|
||||
if self._log_access:
|
||||
for f in LOG_ACCESS_COLUMNS:
|
||||
@@ -4669,7 +4657,7 @@ class BaseModel(object):
|
||||
"""
|
||||
if context is None:
|
||||
context = {}
|
||||
self.check_read(cr, access_rights_uid or user)
|
||||
self.check_access_rights(cr, access_rights_uid or user, 'read')
|
||||
|
||||
# For transient models, restrict acces to the current user, except for the super-user
|
||||
if self.is_transient() and self._log_access and user != SUPERUSER_ID:
|
||||
@@ -4969,37 +4957,7 @@ class BaseModel(object):
|
||||
# backwards compatibility
|
||||
get_xml_id = get_external_id
|
||||
_get_xml_ids = _get_external_ids
|
||||
|
||||
def _get_needaction_info(self, cr, uid, user_id, limit=None, order=None, domain=False, context=None):
|
||||
"""Base method for needaction mechanism
|
||||
- see ir.needaction for actual implementation
|
||||
- if the model uses the need action mechanism
|
||||
(hasattr(model_obj, 'needaction_get_record_ids')):
|
||||
- get the record ids on which the user has actions to perform
|
||||
- evaluate the menu domain
|
||||
- compose a new domain: menu domain, limited to ids of
|
||||
records requesting an action
|
||||
- count the number of records maching that domain, that
|
||||
is the number of actions the user has to perform
|
||||
- this method returns default values
|
||||
:param: model_name: the name of the model (ex: hr.holidays)
|
||||
:param: user_id: the id of user
|
||||
:return: [uses_needaction=True/False, needaction_uid_ctr=%d]
|
||||
"""
|
||||
if hasattr(self, 'needaction_get_record_ids'):
|
||||
# Arbitrary limit, but still much lower thant infinity, to avoid
|
||||
# getting too much data.
|
||||
ids = self.needaction_get_record_ids(cr, uid, user_id, limit=8192, context=context)
|
||||
if not ids:
|
||||
return [True, 0]
|
||||
if domain:
|
||||
new_domain = eval(domain, locals_dict={'uid': user_id}) + [('id', 'in', ids)]
|
||||
else:
|
||||
new_domain = [('id', 'in', ids)]
|
||||
return [True, self.search(cr, uid, new_domain, limit=limit, order=order, count=True, context=context)]
|
||||
else:
|
||||
return [False, 0]
|
||||
|
||||
|
||||
# Transience
|
||||
def is_transient(self):
|
||||
""" Return whether the model is transient.
|
||||
@@ -5126,6 +5084,7 @@ class Model(BaseModel):
|
||||
The system will later instantiate the class once per database (on
|
||||
which the class' module is installed).
|
||||
"""
|
||||
_auto = True
|
||||
_register = False # not visible in ORM registry, meant to be python-inherited only
|
||||
_transient = False # True in a TransientModel
|
||||
|
||||
@@ -5138,6 +5097,7 @@ class TransientModel(BaseModel):
|
||||
records they created. The super-user has unrestricted access
|
||||
to all TransientModel records.
|
||||
"""
|
||||
_auto = True
|
||||
_register = False # not visible in ORM registry, meant to be python-inherited only
|
||||
_transient = True
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ Tests can be explicitely added to the `fast_suite` or `checks` lists or not.
|
||||
See the :ref:`test-framework` section in the :ref:`features` list.
|
||||
"""
|
||||
|
||||
from . import test_expression, test_ir_sequence, test_orm,\
|
||||
test_view_validation, test_uninstall
|
||||
from . import test_expression, test_html_sanitize, test_ir_sequence, test_orm,\
|
||||
test_view_validation, test_uninstall, test_misc
|
||||
|
||||
fast_suite = [
|
||||
test_ir_sequence,
|
||||
@@ -17,8 +17,10 @@ fast_suite = [
|
||||
|
||||
checks = [
|
||||
test_expression,
|
||||
test_html_sanitize,
|
||||
test_orm,
|
||||
test_view_validation,
|
||||
test_misc,
|
||||
]
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# This test can be run stand-alone with something like:
|
||||
# > PYTHONPATH=. python2 openerp/tests/test_misc.py
|
||||
|
||||
import unittest2
|
||||
|
||||
class test_misc(unittest2.TestCase):
|
||||
""" Test some of our generic utility functions """
|
||||
|
||||
def test_append_to_html(self):
|
||||
from openerp.tools import append_content_to_html
|
||||
test_samples = [
|
||||
('<!DOCTYPE...><HTML encoding="blah">some <b>content</b></HtMl>', '--\nYours truly', True,
|
||||
'<!DOCTYPE...><html encoding="blah">some <b>content</b>\n<pre>--\nYours truly</pre>\n</html>'),
|
||||
('<html><body>some <b>content</b></body></html>', '<!DOCTYPE...>\n<html><body>\n<p>--</p>\n<p>Yours truly</p>\n</body>\n</html>', False,
|
||||
'<html><body>some <b>content</b>\n\n\n<p>--</p>\n<p>Yours truly</p>\n\n\n</body></html>'),
|
||||
]
|
||||
for html, content, flag, expected in test_samples:
|
||||
self.assertEqual(append_content_to_html(html,content,flag), expected, 'append_content_to_html is broken')
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest2.main()
|
||||
@@ -1,67 +1,92 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Business Applications
|
||||
# Copyright (C) 2012 OpenERP S.A. (<http://openerp.com>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import lxml.html
|
||||
import operator
|
||||
import re
|
||||
|
||||
def html_sanitize(x):
|
||||
if not x:
|
||||
return x
|
||||
if type(x) == str:
|
||||
x = unicode(x, "utf8", "replace")
|
||||
root = lxml.html.fromstring("<div>%s</div>" % x)
|
||||
result = handle_element(root)
|
||||
res = ""
|
||||
for el in children(result[0]):
|
||||
if type(el) == str or type(el) == unicode:
|
||||
res += el
|
||||
else:
|
||||
el.tail = ""
|
||||
res += lxml.html.tostring(el)
|
||||
return res
|
||||
from openerp.loglevels import ustr
|
||||
|
||||
def html_sanitize(src):
|
||||
if not src:
|
||||
return src
|
||||
src = ustr(src, errors='replace')
|
||||
root = lxml.html.fromstring(u"<div>%s</div>" % src)
|
||||
result = handle_element(root)
|
||||
res = []
|
||||
for element in children(result[0]):
|
||||
if isinstance(element, basestring):
|
||||
res.append(element)
|
||||
else:
|
||||
element.tail = ""
|
||||
res.append(lxml.html.tostring(element))
|
||||
return ''.join(res)
|
||||
|
||||
# FIXME: shouldn't this be a whitelist rather than a blacklist?!
|
||||
to_remove = set(["script", "head", "meta", "title", "link", "img"])
|
||||
to_unwrap = set(["html", "body"])
|
||||
|
||||
javascript_regex = re.compile("""^\s*javascript\s*\:.*$""")
|
||||
javascript_regex = re.compile(r"^\s*javascript\s*:.*$", re.IGNORECASE)
|
||||
|
||||
def handle_a(el, new):
|
||||
href = el.get("href", "#")
|
||||
if javascript_regex.search(href):
|
||||
href = "#"
|
||||
new.set("href", href)
|
||||
|
||||
special = {
|
||||
"a": handle_a,
|
||||
}
|
||||
|
||||
def handle_element(el):
|
||||
if type(el) == str or type(el) == unicode:
|
||||
return [el]
|
||||
if el.tag in to_remove:
|
||||
def handle_element(element):
|
||||
if isinstance(element, basestring):
|
||||
return [element]
|
||||
if element.tag in to_remove:
|
||||
return []
|
||||
if el.tag in to_unwrap:
|
||||
return reduce(lambda x,y: x+y, [handle_element(x) for x in children(el)])
|
||||
new = lxml.html.fromstring("<%s />" % el.tag)
|
||||
for i in children(el):
|
||||
append_to(handle_element(i), new)
|
||||
if el.tag in special:
|
||||
special[el.tag](el, new)
|
||||
return [new]
|
||||
|
||||
def children(el):
|
||||
if element.tag in to_unwrap:
|
||||
return reduce(operator.add, [handle_element(x) for x in children(element)])
|
||||
result = lxml.html.fromstring("<%s />" % element.tag)
|
||||
for c in children(element):
|
||||
append_to(handle_element(c), result)
|
||||
if element.tag in special:
|
||||
special[element.tag](element, result)
|
||||
return [result]
|
||||
|
||||
def children(node):
|
||||
res = []
|
||||
if el.text is not None:
|
||||
res.append(el.text)
|
||||
for i in el.getchildren():
|
||||
res.append(i)
|
||||
if i.tail is not None:
|
||||
res.append(i.tail)
|
||||
if node.text is not None:
|
||||
res.append(node.text)
|
||||
for child_node in node.getchildren():
|
||||
res.append(child_node)
|
||||
if child_node.tail is not None:
|
||||
res.append(child_node.tail)
|
||||
return res
|
||||
|
||||
def append_to(new_ones, el):
|
||||
for i in new_ones:
|
||||
if type(i) == str or type(i) == unicode:
|
||||
children = el.getchildren()
|
||||
def append_to(elements, dest_node):
|
||||
for element in elements:
|
||||
if isinstance(element, basestring):
|
||||
children = dest_node.getchildren()
|
||||
if len(children) == 0:
|
||||
el.text = i
|
||||
dest_node.text = element
|
||||
else:
|
||||
children[-1].tail = i
|
||||
children[-1].tail = element
|
||||
else:
|
||||
el.append(i)
|
||||
dest_node.append(element)
|
||||
|
||||
@@ -406,6 +406,42 @@ def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=Non
|
||||
cr.close()
|
||||
return res
|
||||
|
||||
def email_split(text):
|
||||
""" Return a list of the email addresses found in ``text`` """
|
||||
if not text: return []
|
||||
return re.findall(r'([^ ,<@]+@[^> ,]+)', text)
|
||||
|
||||
def append_content_to_html(html, content, plaintext=True):
|
||||
"""Append extra content at the end of an HTML snippet, trying
|
||||
to locate the end of the HTML document (</body>, </html>, or
|
||||
EOF), and wrapping the provided content in a <pre/> block
|
||||
unless ``plaintext`` is False. A side-effect of this
|
||||
method is to coerce all HTML tags to lowercase in ``html``,
|
||||
and strip enclosing <html> or <body> tags in content if
|
||||
``plaintext`` is False.
|
||||
|
||||
:param str html: html tagsoup (doesn't have to be XHTML)
|
||||
:param str content: extra content to append
|
||||
:param bool plaintext: whether content is plaintext and should
|
||||
be wrapped in a <pre/> tag.
|
||||
"""
|
||||
html = ustr(html)
|
||||
if plaintext:
|
||||
content = u'\n<pre>%s</pre>\n' % ustr(content)
|
||||
else:
|
||||
content = re.sub(r'(?i)(</?html.*>|</?body.*>|<!\W*DOCTYPE.*>)', '', content)
|
||||
content = u'\n%s\n'% ustr(content)
|
||||
# Force all tags to lowercase
|
||||
html = re.sub(r'(</?)\W*(\w+)([ >])',
|
||||
lambda m: '%s%s%s' % (m.group(1),m.group(2).lower(),m.group(3)), html)
|
||||
insert_location = html.find('</body>')
|
||||
if insert_location == -1:
|
||||
insert_location = html.find('</html>')
|
||||
if insert_location == -1:
|
||||
return '%s%s' % (html, content)
|
||||
return '%s%s%s' % (html[:insert_location], content, html[insert_location:])
|
||||
|
||||
|
||||
#----------------------------------------------------------
|
||||
# SMS
|
||||
#----------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user