diff --git a/doc/changelog.rst b/doc/changelog.rst index eadfe81b5e8..46f84364c76 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -6,6 +6,8 @@ Changelog `trunk` ------- +- Added support of custom group_by format and display format when using group_by + on a datetime field, using datetime_format context key - Improved ``html_email_clean`` in tools: better quote and signature finding, added shortening. - Cleaned and slightly refactored ``ir.actions.server``. The ``loop``, ``sms`` diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py index 999431788f8..e13602d6a77 100644 --- a/openerp/addons/base/ir/ir_actions.py +++ b/openerp/addons/base/ir/ir_actions.py @@ -110,7 +110,7 @@ class report_xml(osv.osv): kwargs = {} new_report = report_sxw('report.'+r['report_name'], r['model'], opj('addons',r['report_rml'] or '/'), header=r['header'], register=False, **kwargs) - elif r['report_xsl']: + elif r['report_xsl'] and r['report_xml']: new_report = report_rml('report.'+r['report_name'], r['model'], opj('addons',r['report_xml']), r['report_xsl'] and opj('addons',r['report_xsl']), register=False) diff --git a/openerp/addons/base/ir/ir_mail_server.py b/openerp/addons/base/ir/ir_mail_server.py index 79154a35f3b..1b5f5d5d6a4 100644 --- a/openerp/addons/base/ir/ir_mail_server.py +++ b/openerp/addons/base/ir/ir_mail_server.py @@ -404,8 +404,10 @@ class ir_mail_server(osv.osv): # The email's "Envelope From" (Return-Path), and all recipient addresses must only contain ASCII characters. from_rfc2822 = extract_rfc2822_addresses(smtp_from) - assert len(from_rfc2822) == 1, "Malformed 'Return-Path' or 'From' address - it may only contain plain ASCII characters" - smtp_from = from_rfc2822[0] + assert from_rfc2822, ("Malformed 'Return-Path' or 'From' address: %r - " + "It should contain one valid plain ASCII email") % smtp_from + # use last extracted email, to support rarities like 'Support@MyComp ' + smtp_from = from_rfc2822[-1] email_to = message['To'] email_cc = message['Cc'] email_bcc = message['Bcc'] diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 9699545ddf5..f46ac13af42 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -21,6 +21,7 @@ import logging from lxml import etree +from operator import itemgetter import os from openerp import tools @@ -127,10 +128,14 @@ class view(osv.osv): try: fvg = self.pool[view.model].fields_view_get(cr, uid, view_id=view.id, view_type=view.type, context=context) return fvg['arch'] - except: + except Exception: return False def _check_xml(self, cr, uid, ids, context=None): + if context is None: + context = {} + context['check_view_ids'] = ids + for view in self.browse(cr, uid, ids, context): # Sanity check: the view should not break anything upon rendering! view_arch_utf8 = self._check_render_view(cr, uid, view, context=context) @@ -183,13 +188,15 @@ class view(osv.osv): :rtype: list of tuples :return: [(view_arch,view_id), ...] """ + user_groups = frozenset(self.pool.get('res.users').browse(cr, 1, uid, context).groups_id) if self.pool._init: # Module init currently in progress, only consider views from modules whose code was already loaded + check_view_ids = context and context.get('check_view_ids') or (0,) query = """SELECT v.id FROM ir_ui_view v LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id) - WHERE v.inherit_id=%s AND v.model=%s AND (md.module IS NULL or md.module in %s) + WHERE v.inherit_id=%s AND v.model=%s AND (md.module in %s OR v.id in %s) ORDER BY priority""" - query_params = (view_id, model, tuple(self.pool._init_modules)) + query_params = (view_id, model, tuple(self.pool._init_modules), tuple(check_view_ids)) else: # Modules fully loaded, consider all views query = """SELECT v.id FROM ir_ui_view v @@ -282,6 +289,22 @@ class view(osv.osv): 'blank_nodes': blank_nodes, 'node_parent_field': _Model_Field,} + def _validate_custom_views(self, cr, uid, model): + """Validate architecture of custom views (= without xml id) for a given model. + This method is called at the end of registry update. + """ + cr.execute("""SELECT max(v.id) + FROM ir_ui_view v + LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id) + WHERE md.module IS NULL + AND v.model = %s + GROUP BY coalesce(v.inherit_id, v.id) + """, (model,)) + + ids = map(itemgetter(0), cr.fetchall()) + return self._check_xml(cr, uid, ids) + + class view_sc(osv.osv): _name = 'ir.ui.view_sc' _columns = { diff --git a/openerp/addons/base/tests/__init__.py b/openerp/addons/base/tests/__init__.py index 4c6a1e444f5..0306839b5d3 100644 --- a/openerp/addons/base/tests/__init__.py +++ b/openerp/addons/base/tests/__init__.py @@ -7,6 +7,7 @@ import test_menu import test_res_config import test_res_lang import test_search +import test_views checks = [ test_base, @@ -18,4 +19,5 @@ checks = [ test_res_config, test_res_lang, test_search, + test_views, ] diff --git a/openerp/addons/base/tests/test_views.py b/openerp/addons/base/tests/test_views.py new file mode 100644 index 00000000000..6203e4f001e --- /dev/null +++ b/openerp/addons/base/tests/test_views.py @@ -0,0 +1,105 @@ +from functools import partial +import unittest2 + +import openerp.tests.common as common +from openerp.osv.orm import except_orm +from openerp.tools import mute_logger + +class test_views(common.TransactionCase): + + @mute_logger('openerp.osv.orm', 'openerp.addons.base.ir.ir_ui_view') + def test_00_init_check_views(self): + Views = self.registry('ir.ui.view') + + self.assertTrue(Views.pool._init) + + error_msg = "The model name does not exist or the view architecture cannot be rendered" + # test arch check is call for views without xmlid during registry initialization + with self.assertRaisesRegexp(except_orm, error_msg): + Views.create(self.cr, self.uid, { + 'name': 'Test View #1', + 'model': 'ir.ui.view', + 'arch': """ + + + + """, + }) + + # same for inherited views + with self.assertRaisesRegexp(except_orm, error_msg): + # Views.pudb = True + Views.create(self.cr, self.uid, { + 'name': 'Test View #2', + 'model': 'ir.ui.view', + 'inherit_id': self.browse_ref('base.view_view_tree').id, + 'arch': """ + + + + """, + }) + + def _insert_view(self, **kw): + """Insert view into database via a query to passtrough validation""" + kw.pop('id', None) + + keys = sorted(kw.keys()) + fields = ','.join('"%s"' % (k.replace('"', r'\"'),) for k in keys) + params = ','.join('%%(%s)s' % (k,) for k in keys) + + query = 'INSERT INTO ir_ui_view(%s) VALUES(%s) RETURNING id' % (fields, params) + self.cr.execute(query, kw) + return self.cr.fetchone()[0] + + def test_10_validate_custom_views(self): + Views = self.registry('ir.ui.view') + model = 'ir.actions.act_url' + + validate = partial(Views._validate_custom_views, self.cr, self.uid, model) + + # validation of a single view + vid = self._insert_view(**{ + 'name': 'base view', + 'model': model, + 'priority': 1, + 'arch': """ + + + + """, + }) + self.assertTrue(validate()) # single view + + # validation of a inherited view + self._insert_view(**{ + 'name': 'inherited view', + 'model': model, + 'priority': 1, + 'inherit_id': vid, + 'arch': """ + + + + """, + }) + self.assertTrue(validate()) # inherited view + + # validation of a bad inherited view + self._insert_view(**{ + 'name': 'bad inherited view', + 'model': model, + 'priority': 2, + 'inherit_id': vid, + 'arch': """ + + + + """, + }) + with mute_logger('openerp.osv.orm', 'openerp.addons.base.ir.ir_ui_view'): + self.assertFalse(validate()) # bad inherited view + + +if __name__ == '__main__': + unittest2.main() diff --git a/openerp/modules/loading.py b/openerp/modules/loading.py index 814c1dac5d9..390ac77b2ed 100644 --- a/openerp/modules/loading.py +++ b/openerp/modules/loading.py @@ -400,12 +400,22 @@ def load_modules(db, force_demo=False, status=None, update_module=False): _logger.info('Reloading registry once more after uninstalling modules') return openerp.modules.registry.RegistryManager.new(cr.dbname, force_demo, status, update_module) + # STEP 7: verify custom views on every model + if update_module: + Views = registry['ir.ui.view'] + custom_view_test = True + for model in registry.models.keys(): + if not Views._validate_custom_views(cr, SUPERUSER_ID, model): + custom_view_test = False + _logger.error('invalid custom view(s) for model %s', model) + report.record_result(custom_view_test) + if report.failures: _logger.error('At least one test failed when loading the modules.') else: _logger.info('Modules loaded.') - # STEP 7: call _register_hook on every model + # STEP 8: call _register_hook on every model for model in registry.models.values(): model._register_hook(cr) diff --git a/openerp/modules/migration.py b/openerp/modules/migration.py index 09595336eaa..f199771ba61 100644 --- a/openerp/modules/migration.py +++ b/openerp/modules/migration.py @@ -89,7 +89,7 @@ class MigrationManager(object): 'post': '[%s>]', } - if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'): + if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade') or pkg.installed_version is None: return def convert_version(version): diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 51b0c2b56b9..83346cae8b3 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -2191,7 +2191,7 @@ class BaseModel(object): are applied """ - sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, user, inherit_id, self._name) + sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, user, inherit_id, self._name, context=context) for (view_arch, view_id) in sql_inherit: source = apply_inheritance_specs(source, view_arch, view_id) source = apply_view_inheritance(cr, user, source, view_id) @@ -2601,7 +2601,18 @@ class BaseModel(object): :param list groupby: fields by which the records will be grouped :param int offset: optional number of records to skip :param int limit: optional max number of records to return - :param dict context: context arguments, like lang, time zone + :param dict context: context arguments, like lang, time zone. A special + context key exist for datetime fields : ``datetime_format``. + context[``datetime_format``] = { + 'field_name': { + groupby_format: format for to_char (default: yyyy-mm) + display_format: format for displaying the value + in the result (default: MMM yyyy) + interval: day, month or year; used for begin + and end date of group_by intervals + computation (default: month) + } + } :param list orderby: optional ``order by`` specification, for overriding the natural sort ordering of the groups, see also :py:meth:`~osv.osv.osv.search` @@ -2641,11 +2652,26 @@ class BaseModel(object): fget = self.fields_get(cr, uid, fields) flist = '' group_count = group_by = groupby + group_by_params = {} if groupby: if fget.get(groupby): groupby_type = fget[groupby]['type'] if groupby_type in ('date', 'datetime'): - qualified_groupby_field = "to_char(%s,'yyyy-mm')" % qualified_groupby_field + if context.get('datetime_format') and isinstance(context['datetime_format'], dict) \ + and context['datetime_format'].get(groupby) and isinstance(context['datetime_format'][groupby], dict): + groupby_format = context['datetime_format'][groupby].get('groupby_format', 'yyyy-mm') + display_format = context['datetime_format'][groupby].get('display_format', 'MMMM yyyy') + interval = context['datetime_format'][groupby].get('interval', 'month') + else: + groupby_format = 'yyyy-mm' + display_format = 'MMMM yyyy' + interval = 'month' + group_by_params = { + 'groupby_format': groupby_format, + 'display_format': display_format, + 'interval': interval, + } + qualified_groupby_field = "to_char(%s,%%s)" % qualified_groupby_field flist = "%s as %s " % (qualified_groupby_field, groupby) elif groupby_type == 'boolean': qualified_groupby_field = "coalesce(%s,false)" % qualified_groupby_field @@ -2672,6 +2698,8 @@ class BaseModel(object): gb = groupby and (' GROUP BY ' + qualified_groupby_field) or '' from_clause, where_clause, where_clause_params = query.get_sql() + if group_by_params and group_by_params.get('groupby_format'): + where_clause_params = [group_by_params['groupby_format']] + where_clause_params + [group_by_params['groupby_format']] where_clause = where_clause and ' WHERE ' + where_clause limit_str = limit and ' limit %d' % limit or '' offset_str = offset and ' offset %d' % offset or '' @@ -2708,14 +2736,20 @@ class BaseModel(object): d['__context'] = {'group_by': groupby_list[1:]} if groupby and groupby in fget: if d[groupby] and fget[groupby]['type'] in ('date', 'datetime'): - dt = datetime.datetime.strptime(alldata[d['id']][groupby][:7], '%Y-%m') - days = calendar.monthrange(dt.year, dt.month)[1] - - date_value = datetime.datetime.strptime(d[groupby][:10], '%Y-%m-%d') + groupby_datetime = datetime.datetime.strptime(alldata[d['id']][groupby], '%Y-%m-%d') d[groupby] = babel.dates.format_date( - date_value, format='MMMM yyyy', locale=context.get('lang', 'en_US')) - d['__domain'] = [(groupby, '>=', alldata[d['id']][groupby] and datetime.datetime.strptime(alldata[d['id']][groupby][:7] + '-01', '%Y-%m-%d').strftime('%Y-%m-%d') or False),\ - (groupby, '<=', alldata[d['id']][groupby] and datetime.datetime.strptime(alldata[d['id']][groupby][:7] + '-' + str(days), '%Y-%m-%d').strftime('%Y-%m-%d') or False)] + domain + groupby_datetime, format=group_by_params.get('display_format', 'MMMM yyyy'), locale=context.get('lang', 'en_US')) + if group_by_params.get('interval') == 'month': + days = calendar.monthrange(groupby_datetime.year, groupby_datetime.month)[1] + domain_dt_begin = groupby_datetime.replace(day=1) + domain_dt_end = groupby_datetime.replace(day=days) + elif group_by_params.get('interval') == 'day': + domain_dt_begin = groupby_datetime.replace(hour=0, minute=0) + domain_dt_end = groupby_datetime.replace(hour=23, minute=59, second=59) + else: + domain_dt_begin = groupby_datetime.replace(month=1, day=1) + domain_dt_end = groupby_datetime.replace(month=12, day=31) + d['__domain'] = [(groupby, '>=', domain_dt_begin.strftime('%Y-%m-%d')), (groupby, '<=', domain_dt_end.strftime('%Y-%m-%d'))] + domain del alldata[d['id']][groupby] d.update(alldata[d['id']]) del d['id'] diff --git a/openerp/tools/mail.py b/openerp/tools/mail.py index 6c4776bf0e4..98f8869f85c 100644 --- a/openerp/tools/mail.py +++ b/openerp/tools/mail.py @@ -448,6 +448,11 @@ command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE) # group(1) = the record ID ; group(2) = the model (if any) ; group(3) = the domain reference_re = re.compile("<.*-open(?:object|erp)-(\\d+)(?:-([\w.]+))?.*@(.*)>", re.UNICODE) +# Bounce regex +# Typical form of bounce is bounce-128-crm.lead-34@domain +# group(1) = the mail ID; group(2) = the model (if any); group(3) = the record ID +bounce_re = re.compile("[\w]+-(\d+)-?([\w.]+)?-?(\d+)?", re.UNICODE) + def generate_tracking_message_id(res_id): """Returns a string that can be used in the Message-ID RFC822 header field diff --git a/openerp/tools/translate.py b/openerp/tools/translate.py index 3c51fa5f3b5..4a055950c46 100644 --- a/openerp/tools/translate.py +++ b/openerp/tools/translate.py @@ -832,8 +832,11 @@ def trans_generate(lang, modules, cr): if module: src_file = open(fabsolutepath, 'r') try: - for lineno, message, comments in extract.extract(extract_method, src_file, - keywords=extract_keywords): + for extracted in extract.extract(extract_method, src_file, + keywords=extract_keywords): + # Babel 0.9.6 yields lineno, message, comments + # Babel 1.3 yields lineno, message, comments, context + lineno, message, comments = extracted[:3] push_translation(module, trans_type, display_path, lineno, encode(message), comments + extra_comments) except Exception: