[MERGE] Sync with trunk-website-al
bzr revid: rim@openerp.com-20140131075451-2g14hxx3h5ycf5pe
This commit is contained in:
@@ -129,19 +129,21 @@ Administrator</field>
|
||||
<field name="path">/dev/null</field>
|
||||
<field name="mode">all</field>
|
||||
</record>
|
||||
|
||||
<record id="public_partner" model="res.partner">
|
||||
<field name="name">Public user</field>
|
||||
<field name="active" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="public_user" model="res.users">
|
||||
<field name="name">Public user</field>
|
||||
<field name="login">public</field>
|
||||
<field name="password"></field>
|
||||
<!-- Avoid auto-including this demo user in any default group -->
|
||||
<field name="groups_id" eval="[(5,)]"/>
|
||||
<field name="groups_id" eval="[(6,0,[ref('base.group_public')])]"/>
|
||||
<field name="image" type="base64" file="base/static/img/public_user-image.png"/>
|
||||
<field name="partner_id" ref="public_partner"/>
|
||||
<field name="active" eval="False"/>
|
||||
<!-- TODO make res.partner active false too -->
|
||||
</record>
|
||||
<record id="base.group_public" model="res.groups">
|
||||
<field name="users" eval="[(4, ref('base.public_user'))]"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
|
||||
@@ -62,7 +62,6 @@ class ir_http(osv.AbstractModel):
|
||||
raise http.SessionExpiredException("Session expired")
|
||||
|
||||
def _auth_method_none(self):
|
||||
request.disable_db = True
|
||||
request.uid = None
|
||||
|
||||
def _auth_method_public(self):
|
||||
@@ -136,7 +135,7 @@ class ir_http(osv.AbstractModel):
|
||||
m = request.registry.get('ir.module.module')
|
||||
ids = m.search(cr, openerp.SUPERUSER_ID, [('state', '=', 'installed'), ('name', '!=', 'web')], context=request.context)
|
||||
installed = set(x['name'] for x in m.read(cr, 1, ids, ['name'], context=request.context))
|
||||
mods = ['', "web"] + sorted(installed)
|
||||
mods = [''] + openerp.conf.server_wide_modules + sorted(installed)
|
||||
self._routing_map = http.routing_map(mods, False, converters=self._get_converters())
|
||||
|
||||
return self._routing_map
|
||||
|
||||
@@ -24,18 +24,26 @@ _logger = logging.getLogger(__name__)
|
||||
#--------------------------------------------------------------------
|
||||
# QWeb template engine
|
||||
#--------------------------------------------------------------------
|
||||
|
||||
class QWebException(Exception):
|
||||
def __init__(self, message, template=None, node=None, expression=None, inner=None):
|
||||
def __init__(self, message, **kw):
|
||||
Exception.__init__(self, message)
|
||||
self.template = template
|
||||
self.node = node
|
||||
self.expression = expression
|
||||
self.inner = inner
|
||||
self.qweb = dict(kw)
|
||||
|
||||
class QWebTemplateNotFound(QWebException):
|
||||
pass
|
||||
|
||||
def convert_to_qweb_exception(etype=None, **kw):
|
||||
if etype is None:
|
||||
etype = QWebException
|
||||
orig_type, original, tb = sys.exc_info()
|
||||
try:
|
||||
raise etype, original, tb
|
||||
except etype, e:
|
||||
for k, v in kw.items():
|
||||
e.qweb[k] = v
|
||||
e.qweb['inner'] = original
|
||||
return e
|
||||
|
||||
class QWebContext(dict):
|
||||
def __init__(self, cr, uid, data, loader=None, templates=None, context=None):
|
||||
self.cr = cr
|
||||
@@ -156,21 +164,21 @@ class QWeb(orm.AbstractModel):
|
||||
if qwebcontext.loader and name not in qwebcontext.templates:
|
||||
try:
|
||||
xml_doc = qwebcontext.loader(name)
|
||||
except ValueError, e:
|
||||
raise QWebTemplateNotFound("Loader could not find template %r" % name, template=origin_template, inner=e)
|
||||
except ValueError:
|
||||
raise convert_to_qweb_exception(QWebTemplateNotFound, message="Loader could not find template %r" % name, template=origin_template)
|
||||
self.load_document(xml_doc, qwebcontext=qwebcontext)
|
||||
|
||||
if name in qwebcontext.templates:
|
||||
return qwebcontext.templates[name]
|
||||
|
||||
raise QWebTemplateNotFound("Template %r not found" % name, template=origin_template)
|
||||
raise convert_to_qweb_exception(QWebTemplateNotFound, message="Template %r not found" % name, template=origin_template)
|
||||
|
||||
def eval(self, expr, qwebcontext):
|
||||
try:
|
||||
return qwebcontext.safe_eval(expr)
|
||||
except Exception, e:
|
||||
except Exception:
|
||||
template = qwebcontext.get('__template__')
|
||||
raise QWebException("Could not evaluate expression %r" % expr, expression=expr, template=template, inner=e)
|
||||
raise convert_to_qweb_exception(message="Could not evaluate expression %r" % expr, expression=expr, template=template)
|
||||
|
||||
def eval_object(self, expr, qwebcontext):
|
||||
return self.eval(expr, qwebcontext)
|
||||
@@ -196,9 +204,9 @@ class QWeb(orm.AbstractModel):
|
||||
|
||||
try:
|
||||
return str(expr % qwebcontext)
|
||||
except Exception, e:
|
||||
except Exception:
|
||||
template = qwebcontext.get('__template__')
|
||||
raise QWebException("Format error for expression %r" % expr, expression=expr, template=template, inner=e)
|
||||
raise convert_to_qweb_exception(message="Format error for expression %r" % expr, expression=expr, template=template)
|
||||
|
||||
def eval_bool(self, expr, qwebcontext):
|
||||
return int(bool(self.eval(expr, qwebcontext)))
|
||||
@@ -281,9 +289,9 @@ class QWeb(orm.AbstractModel):
|
||||
g_inner.append(self.render_node(current_node, qwebcontext))
|
||||
except QWebException:
|
||||
raise
|
||||
except Exception, e:
|
||||
except Exception:
|
||||
template = qwebcontext.get('__template__')
|
||||
raise QWebException("Could not render element %r -- %s" % (element.nodeName, e.message), node=element, template=template, inner=e), None, sys.exc_info()[2]
|
||||
raise convert_to_qweb_exception(message="Could not render element %r" % element.nodeName, node=element, template=template)
|
||||
name = str(element.nodeName)
|
||||
inner = "".join(g_inner)
|
||||
trim = template_attributes.get("trim", 0)
|
||||
@@ -634,8 +642,8 @@ class ManyToOneConverter(osv.AbstractModel):
|
||||
|
||||
def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
|
||||
[read] = record.read([field_name])
|
||||
if not read[field_name]: return ''
|
||||
_, value = read[field_name]
|
||||
|
||||
return nl2br(value, options=options)
|
||||
|
||||
class HTMLConverter(osv.AbstractModel):
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
domain="[('comments', 'like', 'openerp-web')]"/>
|
||||
<field name="name" operator="="/>
|
||||
<field name="lang"/>
|
||||
<field name="source"/>
|
||||
<field name="src"/>
|
||||
<field name="value"/>
|
||||
</search>
|
||||
</field>
|
||||
|
||||
@@ -24,10 +24,8 @@ import logging
|
||||
from lxml import etree
|
||||
from operator import itemgetter
|
||||
import os
|
||||
import time
|
||||
|
||||
import HTMLParser
|
||||
from lxml import etree
|
||||
|
||||
import openerp
|
||||
from openerp import tools
|
||||
@@ -36,7 +34,7 @@ from openerp.tools import graph, SKIPPED_ELEMENT_TYPES
|
||||
from openerp.tools.safe_eval import safe_eval as eval
|
||||
from openerp.tools.view_validation import valid_view
|
||||
from openerp.tools import misc
|
||||
from openerp.osv.orm import browse_record, browse_record_list
|
||||
from openerp.tools.translate import _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -89,7 +87,7 @@ class view(osv.osv):
|
||||
help="ID of the view defined in xml file"),
|
||||
'groups_id': fields.many2many('res.groups', 'ir_ui_view_group_rel', 'view_id', 'group_id',
|
||||
string='Groups', help="If this field is empty, the view applies to all users. Otherwise, the view applies to the users of those groups only."),
|
||||
'model_ids': fields.one2many('ir.model.data', 'res_id', auto_join=True),
|
||||
'model_ids': fields.one2many('ir.model.data', 'res_id', domain=[('model','=','ir.ui.view')], auto_join=True),
|
||||
}
|
||||
_defaults = {
|
||||
'priority': 16,
|
||||
@@ -114,20 +112,19 @@ class view(osv.osv):
|
||||
def _check_xml(self, cr, uid, ids, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
context['check_view_ids'] = ids
|
||||
context = dict(context, check_view_ids=ids)
|
||||
|
||||
# Sanity checks: the view should not break anything upon rendering!
|
||||
# Any exception raised below will cause a transaction rollback.
|
||||
for view in self.browse(cr, uid, ids, context):
|
||||
# Sanity check: the view should not break anything upon rendering!
|
||||
try:
|
||||
fvg = self.read_combined(cr, uid, view.id, None, context=context)
|
||||
view_arch_utf8 = fvg['arch']
|
||||
except Exception, e:
|
||||
_logger.exception(e)
|
||||
return False
|
||||
view_def = self.read_combined(cr, uid, view.id, None, context=context)
|
||||
view_arch_utf8 = view_def['arch']
|
||||
if view.type != 'qweb':
|
||||
view_doc = etree.fromstring(view_arch_utf8)
|
||||
# verify that all fields used are valid, etc.
|
||||
self.postprocess_and_fields(cr, uid, view.model, view_doc, view.id, context=context)
|
||||
# RNG-based validation is not possible anymore with 7.0 forms
|
||||
# TODO 7.0: provide alternative assertion-based validation of view_arch_utf8
|
||||
view_docs = [etree.fromstring(view_arch_utf8)]
|
||||
view_docs = [view_doc]
|
||||
if view_docs[0].tag == 'data':
|
||||
# A <data> element is a wrapper for multiple root nodes
|
||||
view_docs = view_docs[0]
|
||||
@@ -142,7 +139,7 @@ class view(osv.osv):
|
||||
return True
|
||||
|
||||
_constraints = [
|
||||
(_check_xml, 'Invalid XML for View Architecture!', ['arch'])
|
||||
(_check_xml, 'Invalid view definition', ['arch'])
|
||||
]
|
||||
|
||||
def _auto_init(self, cr, context=None):
|
||||
@@ -251,8 +248,21 @@ class view(osv.osv):
|
||||
if not (view.groups_id and user_groups.isdisjoint(view.groups_id))]
|
||||
|
||||
def raise_view_error(self, cr, uid, message, view_id, context=None):
|
||||
view = self.browse(cr, uid, [view_id], context)[0]
|
||||
message = "Inherit error: %s view_id: %s, xml_id: %s, model: %s, parent_view: %s" % (message, view_id, view.xml_id, view.model, view.inherit_id)
|
||||
view = self.browse(cr, uid, view_id, context)
|
||||
not_avail = _('n/a')
|
||||
message = ("%(msg)s\n\n" +
|
||||
_("Error context:\nView `%(view_name)s`") +
|
||||
"\n[view_id: %(viewid)s, xml_id: %(xmlid)s, "
|
||||
"model: %(model)s, parent_id: %(parent)s]") % \
|
||||
{
|
||||
'view_name': view.name or not_avail,
|
||||
'viewid': view_id or not_avail,
|
||||
'xmlid': view.xml_id or not_avail,
|
||||
'model': view.model or not_avail,
|
||||
'parent': view.inherit_id.id or not_avail,
|
||||
'msg': message,
|
||||
}
|
||||
_logger.error(message)
|
||||
raise AttributeError(message)
|
||||
|
||||
def locate_node(self, arch, spec):
|
||||
@@ -360,7 +370,7 @@ class view(osv.osv):
|
||||
elif pos == 'before':
|
||||
node.addprevious(child)
|
||||
else:
|
||||
self.raise_view_error(cr, uid, "Invalid position value: '%s'" % pos, inherit_id, context=context)
|
||||
self.raise_view_error(cr, uid, _("Invalid position attribute: '%s'") % pos, inherit_id, context=context)
|
||||
else:
|
||||
attrs = ''.join([
|
||||
' %s="%s"' % (attr, spec.get(attr))
|
||||
@@ -368,7 +378,7 @@ class view(osv.osv):
|
||||
if attr != 'position'
|
||||
])
|
||||
tag = "<%s%s>" % (spec.tag, attrs)
|
||||
self.raise_view_error(cr, uid, "Element '%s' not found in parent view " % tag, inherit_id, context=context)
|
||||
self.raise_view_error(cr, uid, _("Element '%s' cannot be located in parent view") % tag, inherit_id, context=context)
|
||||
|
||||
return source
|
||||
|
||||
@@ -458,6 +468,9 @@ class view(osv.osv):
|
||||
|
||||
modifiers = {}
|
||||
Model = self.pool.get(model)
|
||||
if not Model:
|
||||
self.raise_view_error(cr, user, _('Model not found: %(model)s') % dict(model=model),
|
||||
view_id, context)
|
||||
|
||||
def encode(s):
|
||||
if isinstance(s, unicode):
|
||||
@@ -474,7 +487,7 @@ class view(osv.osv):
|
||||
|
||||
:return: True if field should be included in the result of fields_view_get
|
||||
"""
|
||||
if Model and node.tag == 'field' and node.get('name') in Model._all_columns:
|
||||
if node.tag == 'field' and node.get('name') in Model._all_columns:
|
||||
column = Model._all_columns[node.get('name')].column
|
||||
if column.groups and not self.user_has_groups(
|
||||
cr, user, groups=column.groups, context=context):
|
||||
@@ -523,8 +536,6 @@ class view(osv.osv):
|
||||
column = False
|
||||
|
||||
if column:
|
||||
relation = self.pool[column._obj] if column._obj else None
|
||||
|
||||
children = False
|
||||
views = {}
|
||||
for f in node:
|
||||
@@ -563,7 +574,7 @@ class view(osv.osv):
|
||||
# Get the attrs before they are (possibly) deleted by check_group below
|
||||
orm.transfer_node_to_modifiers(node, modifiers, context, in_tree_view)
|
||||
|
||||
# TODO remove attrs couterpart in modifiers when invisible is true ?
|
||||
# TODO remove attrs counterpart in modifiers when invisible is true ?
|
||||
|
||||
# translate view
|
||||
if 'lang' in context:
|
||||
@@ -638,6 +649,8 @@ class view(osv.osv):
|
||||
"""
|
||||
fields = {}
|
||||
Model = self.pool.get(model)
|
||||
if not Model:
|
||||
self.raise_view_error(cr, user, _('Model not found: %(model)s') % dict(model=model), view_id, context)
|
||||
|
||||
if node.tag == 'diagram':
|
||||
if node.getchildren()[0].tag == 'node':
|
||||
@@ -649,7 +662,7 @@ class view(osv.osv):
|
||||
if node.getchildren()[1].tag == 'arrow':
|
||||
arrow_fields = self.pool[node.getchildren()[1].get('object')].fields_get(cr, user, None, context)
|
||||
fields.update(arrow_fields)
|
||||
elif Model:
|
||||
else:
|
||||
fields = Model.fields_get(cr, user, None, context)
|
||||
|
||||
fields_def = self.postprocess(cr, user, model, node, view_id, False, fields, context=context)
|
||||
@@ -669,14 +682,9 @@ class view(osv.osv):
|
||||
elif field in fields:
|
||||
fields[field].update(fields_def[field])
|
||||
else:
|
||||
cr.execute('select name, model from ir_ui_view where (id=%s or inherit_id=%s) and arch like %s', (view_id, view_id, '%%%s%%' % field))
|
||||
res = cr.fetchall()[:]
|
||||
model = res[0][1]
|
||||
res.insert(0, ("Can't find field '%s' in the following view parts composing the view of object model '%s':" % (field, model), None))
|
||||
msg = "\n * ".join([r[0] for r in res])
|
||||
msg += "\n\nEither you wrongly customized this view, or some modules bringing those views are not compatible with your current data model"
|
||||
_logger.error(msg)
|
||||
raise orm.except_orm('View error', msg)
|
||||
message = _("Field `%(field_name)s` does not exist") % \
|
||||
dict(field_name=field)
|
||||
self.raise_view_error(cr, user, message, view_id, context)
|
||||
return arch, fields
|
||||
|
||||
#------------------------------------------------------
|
||||
@@ -687,9 +695,7 @@ class view(osv.osv):
|
||||
if '.' not in xml_id:
|
||||
raise ValueError('Invalid template id: %r' % (xml_id,))
|
||||
|
||||
m, n = xml_id.split('.', 1)
|
||||
_, view_id = self.pool['ir.model.data'].get_object_reference(cr, uid, m, n)
|
||||
|
||||
view_id = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, xml_id, raise_if_not_found=True)
|
||||
arch = self.read_combined(cr, uid, view_id, fields=['arch'], context=context)['arch']
|
||||
arch_tree = etree.fromstring(arch)
|
||||
|
||||
|
||||
@@ -23,14 +23,12 @@ import datetime
|
||||
from lxml import etree
|
||||
import math
|
||||
import pytz
|
||||
import re
|
||||
|
||||
import openerp
|
||||
from openerp import SUPERUSER_ID
|
||||
from openerp import tools
|
||||
from openerp.osv import osv, fields
|
||||
from openerp.tools.translate import _
|
||||
from openerp.tools.yaml_import import is_comment
|
||||
|
||||
class format_address(object):
|
||||
def fields_view_get_address(self, cr, uid, arch, context={}):
|
||||
@@ -218,12 +216,12 @@ class res_partner(osv.osv, format_address):
|
||||
_display_name = lambda self, *args, **kwargs: self._display_name_compute(*args, **kwargs)
|
||||
|
||||
_commercial_partner_store_triggers = {
|
||||
'res.partner': (lambda self,cr,uid,ids,context=None: self.search(cr, uid, [('id','child_of',ids)]),
|
||||
['parent_id', 'is_company'], 10)
|
||||
'res.partner': (lambda self,cr,uid,ids,context=None: self.search(cr, uid, [('id','child_of',ids)], context=dict(active_test=False)),
|
||||
['parent_id', 'is_company'], 10)
|
||||
}
|
||||
_display_name_store_triggers = {
|
||||
'res.partner': (lambda self,cr,uid,ids,context=None: self.search(cr, uid, [('id','child_of',ids)]),
|
||||
['parent_id', 'is_company', 'name'], 10)
|
||||
'res.partner': (lambda self,cr,uid,ids,context=None: self.search(cr, uid, [('id','child_of',ids)], context=dict(active_test=False)),
|
||||
['parent_id', 'is_company', 'name'], 10)
|
||||
}
|
||||
|
||||
_order = "display_name"
|
||||
@@ -605,7 +603,8 @@ class res_partner(osv.osv, format_address):
|
||||
""" Override search() to always show inactive children when searching via ``child_of`` operator. The ORM will
|
||||
always call search() with a simple domain of the form [('parent_id', 'in', [ids])]. """
|
||||
# a special ``domain`` is set on the ``child_ids`` o2m to bypass this logic, as it uses similar domain expressions
|
||||
if len(args) == 1 and len(args[0]) == 3 and args[0][:2] == ('parent_id','in'):
|
||||
if len(args) == 1 and len(args[0]) == 3 and args[0][:2] == ('parent_id','in') \
|
||||
and args[0][2] != [False]:
|
||||
context = dict(context or {}, active_test=False)
|
||||
return super(res_partner, self)._search(cr, user, args, offset=offset, limit=limit, order=order, context=context,
|
||||
count=count, access_rights_uid=access_rights_uid)
|
||||
@@ -614,26 +613,36 @@ class res_partner(osv.osv, format_address):
|
||||
if not args:
|
||||
args = []
|
||||
if name and operator in ('=', 'ilike', '=ilike', 'like', '=like'):
|
||||
|
||||
self.check_access_rights(cr, uid, 'read')
|
||||
where_query = self._where_calc(cr, uid, args, context=context)
|
||||
self._apply_ir_rules(cr, uid, where_query, 'read', context=context)
|
||||
from_clause, where_clause, where_clause_params = where_query.get_sql()
|
||||
where_str = where_clause and (" WHERE %s AND " % where_clause) or ' WHERE '
|
||||
|
||||
# search on the name of the contacts and of its company
|
||||
search_name = name
|
||||
if operator in ('ilike', 'like'):
|
||||
search_name = '%%%s%%' % name
|
||||
if operator in ('=ilike', '=like'):
|
||||
operator = operator[1:]
|
||||
query_args = {'name': search_name}
|
||||
query = ('''SELECT id FROM res_partner
|
||||
WHERE email ''' + operator + ''' %(name)s
|
||||
OR display_name ''' + operator + ''' %(name)s
|
||||
ORDER BY display_name
|
||||
''')
|
||||
|
||||
query = ('SELECT id FROM res_partner ' +
|
||||
where_str + '(email ' + operator + ''' %s
|
||||
OR display_name ''' + operator + ''' %s)
|
||||
ORDER BY display_name''')
|
||||
|
||||
where_clause_params += [search_name, search_name]
|
||||
if limit:
|
||||
query += ' limit %(limit)s'
|
||||
query_args['limit'] = limit
|
||||
cr.execute(query, query_args)
|
||||
query += ' limit %s'
|
||||
where_clause_params.append(limit)
|
||||
cr.execute(query, where_clause_params)
|
||||
ids = map(lambda x: x[0], cr.fetchall())
|
||||
ids = self.search(cr, uid, [('id', 'in', ids)] + args, limit=limit, context=context)
|
||||
|
||||
if ids:
|
||||
return self.name_get(cr, uid, ids, context)
|
||||
else:
|
||||
return []
|
||||
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):
|
||||
|
||||
@@ -39,6 +39,20 @@ class test_base(common.TransactionCase):
|
||||
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')
|
||||
|
||||
def test_15_res_partner_name_search(self):
|
||||
cr,uid = self.cr, self.uid
|
||||
for name, active in [
|
||||
('"A Raoul Grosbedon" <raoul@chirurgiens-dentistes.fr>', False),
|
||||
('B Raoul chirurgiens-dentistes.fr', True),
|
||||
("C Raoul O'hara <!@historicalsociety.museum>", True),
|
||||
('ryu+giga-Sushi@aizubange.fukushima.jp', True),
|
||||
]:
|
||||
partner_id, dummy = self.res_partner.name_create(cr, uid, name, context={'default_active': active})
|
||||
partners = self.res_partner.name_search(cr, uid, 'Raoul')
|
||||
self.assertEqual(len(partners), 2, 'Incorrect search number result for name_search')
|
||||
partners = self.res_partner.name_search(cr, uid, 'Raoul', limit=1)
|
||||
self.assertEqual(len(partners), 1, 'Incorrect search number result for name_search with a limit')
|
||||
self.assertEqual(partners[0][1], 'B Raoul chirurgiens-dentistes.fr', 'Incorrect partner returned, should be the first active')
|
||||
|
||||
def test_20_res_partner_address_sync(self):
|
||||
cr, uid = self.cr, self.uid
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
from functools import partial
|
||||
import unittest2
|
||||
|
||||
from lxml import etree as ET
|
||||
from lxml.builder import E
|
||||
|
||||
from openerp.tests import common
|
||||
from openerp.tools import mute_logger
|
||||
|
||||
Field = E.field
|
||||
|
||||
|
||||
class TestNodeLocator(common.BaseCase):
|
||||
"""
|
||||
The node locator returns None when it can not find a node, and the first
|
||||
@@ -143,6 +140,7 @@ class TestViewInheritance(common.TransactionCase):
|
||||
'name': name,
|
||||
'arch': arch or self.arch_for(name, parent=parent),
|
||||
'inherit_id': parent,
|
||||
'priority': 5, # higher than default views
|
||||
})
|
||||
self.ids[name] = view_id
|
||||
return view_id
|
||||
@@ -150,7 +148,7 @@ class TestViewInheritance(common.TransactionCase):
|
||||
def setUp(self):
|
||||
super(TestViewInheritance, self).setUp()
|
||||
|
||||
self.model = 'dummy'
|
||||
self.model = 'ir.ui.view.custom'
|
||||
self.View = self.registry('ir.ui.view')
|
||||
self._init = self.View.pool._init
|
||||
self.View.pool._init = False
|
||||
@@ -368,21 +366,23 @@ class TestNoModel(common.TransactionCase):
|
||||
View = self.registry('ir.ui.view')
|
||||
view_id = View.create(self.cr, self.uid, {
|
||||
'name': 'dummy',
|
||||
'arch': '<form string=""/>',
|
||||
'inherit_id': False
|
||||
'arch': '<template name="foo"/>',
|
||||
'inherit_id': False,
|
||||
'type': 'qweb',
|
||||
})
|
||||
fields = ['name', 'arch', 'type', 'priority', 'inherit_id', 'model']
|
||||
[view] = View.read(self.cr, self.uid, [view_id], fields)
|
||||
self.assertEqual(view, {
|
||||
'id': view_id,
|
||||
'name': 'dummy',
|
||||
'arch': '<form string=""/>',
|
||||
'type': 'form',
|
||||
'arch': '<template name="foo"/>',
|
||||
'type': 'qweb',
|
||||
'priority': 16,
|
||||
'inherit_id': False,
|
||||
'model': False,
|
||||
})
|
||||
|
||||
text_para = E.p("", {'class': 'legalese'})
|
||||
arch = E.body(
|
||||
E.div(
|
||||
E.h1("Title"),
|
||||
@@ -390,43 +390,32 @@ class TestNoModel(common.TransactionCase):
|
||||
E.p("Welcome!"),
|
||||
E.div(
|
||||
E.hr(),
|
||||
E.p("Copyright copyrighter", {'class': 'legalese'}),
|
||||
text_para,
|
||||
id="footer"),
|
||||
{'class': "index"},)
|
||||
def test_fields_mess(self):
|
||||
"""
|
||||
Try to call __view_look_dom_arch without a model provided, will need
|
||||
to be altered once it's broken up into sane components
|
||||
"""
|
||||
View = self.registry('ir.ui.view')
|
||||
|
||||
sarch, fields = View.postprocess_and_fields(
|
||||
self.cr, self.uid, None, self.arch, None)
|
||||
|
||||
self.assertEqual(sarch, ET.tostring(self.arch, encoding='utf-8'))
|
||||
self.assertEqual(fields, {})
|
||||
|
||||
def test_mess_translation(self):
|
||||
def test_qweb_translation(self):
|
||||
"""
|
||||
Test if translations work correctly without a model
|
||||
"""
|
||||
View = self.registry('ir.ui.view')
|
||||
self.registry('res.lang').load_lang(self.cr, self.uid, 'fr_FR')
|
||||
orig_text = "Copyright copyrighter"
|
||||
translated_text = u"Copyrighter, tous droits réservés"
|
||||
self.text_para.text = orig_text
|
||||
self.registry('ir.translation').create(self.cr, self.uid, {
|
||||
'name': '',
|
||||
'name': 'website',
|
||||
'type': 'view',
|
||||
'lang': 'fr_FR',
|
||||
'src': 'Copyright copyrighter',
|
||||
'value': u"Copyrighter, tous droits réservés",
|
||||
'src': orig_text,
|
||||
'value': translated_text,
|
||||
})
|
||||
sarch, fields = View.postprocess_and_fields(
|
||||
self.cr, self.uid, None, self.arch, None, {'lang': 'fr_FR'})
|
||||
sarch = View.translate_qweb(self.cr, self.uid, None, self.arch, 'fr_FR')
|
||||
|
||||
self.text_para.text = translated_text
|
||||
self.assertEqual(
|
||||
sarch,
|
||||
ET.tostring(self.arch, encoding='utf-8')
|
||||
.replace('Copyright copyrighter',
|
||||
'Copyrighter, tous droits réservés'))
|
||||
self.assertEqual(fields, {})
|
||||
ET.tostring(sarch, encoding='utf-8'),
|
||||
ET.tostring(self.arch, encoding='utf-8'))
|
||||
|
||||
|
||||
class test_views(common.TransactionCase):
|
||||
|
||||
+24
-32
@@ -72,29 +72,6 @@ def redirect_with_hash(url, code=303):
|
||||
return werkzeug.utils.redirect(url, code)
|
||||
return "<html><head><script>window.location = '%s' + location.hash;</script></head></html>" % url
|
||||
|
||||
def ensure_db(with_registry=False, redirect='/web/database/selector'):
|
||||
db = request.params.get('db')
|
||||
# if db not provided, use the session one
|
||||
if not db:
|
||||
db = request.session.db
|
||||
|
||||
# if no database provided and no database in session, use monodb
|
||||
if not db:
|
||||
db = db_monodb(request.httprequest)
|
||||
|
||||
# if no db can be found til here, send to the database selector
|
||||
# the database selector will redirect to database manager if needed
|
||||
if not db:
|
||||
werkzeug.exceptions.abort(werkzeug.utils.redirect(redirect, 303))
|
||||
|
||||
# always switch the session to the computed db
|
||||
if db != request.session.db:
|
||||
request.session.logout()
|
||||
request.session.db = db
|
||||
|
||||
if with_registry:
|
||||
request.disable_db = False
|
||||
|
||||
class WebRequest(object):
|
||||
""" Parent class for all OpenERP Web request types, mostly deals with
|
||||
initialization and setup of the request object (the dispatching itself has
|
||||
@@ -227,7 +204,7 @@ class WebRequest(object):
|
||||
kwargs.update(self.func_arguments)
|
||||
|
||||
# Backward for 7.0
|
||||
if getattr(self.func, '_first_arg_is_req', False):
|
||||
if getattr(self.func.method, '_first_arg_is_req', False):
|
||||
args = (request,) + args
|
||||
# Correct exception handling and concurency retry
|
||||
@service_model.check
|
||||
@@ -271,6 +248,7 @@ def route(route=None, **kw):
|
||||
authentication modules. There request code will not have any facilities to access the database nor have any
|
||||
configuration indicating the current database nor the current user.
|
||||
:param methods: A sequence of http methods this route applies to. If not specified, all methods are allowed.
|
||||
:param cors: The Access-Control-Allow-Origin cors directive value.
|
||||
"""
|
||||
routing = kw.copy()
|
||||
assert not 'type' in routing or routing['type'] in ("http", "json")
|
||||
@@ -931,7 +909,7 @@ class Root(object):
|
||||
self.load_addons()
|
||||
|
||||
_logger.info("Generating nondb routing")
|
||||
self.nodb_routing_map = routing_map(['', "web"], True)
|
||||
self.nodb_routing_map = routing_map([''] + openerp.conf.server_wide_modules, True)
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
""" Handle a WSGI request
|
||||
@@ -979,8 +957,13 @@ class Root(object):
|
||||
return explicit_session
|
||||
|
||||
def setup_db(self, httprequest):
|
||||
if not httprequest.session.db:
|
||||
# allow "admin" routes to works without being logged in when in monodb.
|
||||
db = httprequest.session.db
|
||||
# Check if session.db is legit
|
||||
if db and db not in db_filter([db], httprequest=httprequest):
|
||||
httprequest.session.logout()
|
||||
db = None
|
||||
|
||||
if not db:
|
||||
httprequest.session.db = db_monodb(httprequest)
|
||||
|
||||
def setup_lang(self, httprequest):
|
||||
@@ -1003,8 +986,6 @@ class Root(object):
|
||||
try:
|
||||
result.process()
|
||||
except(Exception), e:
|
||||
# In case of auth="none" we re-activate db getter for exception handling
|
||||
request.disable_db = False
|
||||
if request.db:
|
||||
result = request.registry['ir.http']._handle_exception(e)
|
||||
else:
|
||||
@@ -1026,6 +1007,16 @@ class Root(object):
|
||||
if not explicit_session and hasattr(response, 'set_cookie'):
|
||||
response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
|
||||
|
||||
# Support for Cross-Origin Resource Sharing
|
||||
if request.func and 'cors' in request.func.routing:
|
||||
response.headers.set('Access-Control-Allow-Origin', request.func.routing['cors'])
|
||||
methods = 'GET, POST'
|
||||
if request.func_request_type == 'json':
|
||||
methods = 'POST'
|
||||
elif request.func.routing.get('methods'):
|
||||
methods = ', '.join(request.func.routing['methods'])
|
||||
response.headers.set('Access-Control-Allow-Methods', methods)
|
||||
|
||||
return response
|
||||
|
||||
def dispatch(self, environ, start_response):
|
||||
@@ -1079,8 +1070,11 @@ class Root(object):
|
||||
return request.registry['ir.http'].routing_map()
|
||||
|
||||
def db_list(force=False, httprequest=None):
|
||||
httprequest = httprequest or request.httprequest
|
||||
dbs = openerp.netsvc.dispatch_rpc("db", "list", [force])
|
||||
return db_filter(dbs, httprequest=httprequest)
|
||||
|
||||
def db_filter(dbs, httprequest=None):
|
||||
httprequest = httprequest or request.httprequest
|
||||
h = httprequest.environ['HTTP_HOST'].split(':')[0]
|
||||
d = h.split('.')[0]
|
||||
r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
|
||||
@@ -1099,8 +1093,6 @@ def db_monodb(httprequest=None):
|
||||
Returns ``None`` if the magic is not magic enough.
|
||||
"""
|
||||
httprequest = httprequest or request.httprequest
|
||||
db = None
|
||||
redirect = None
|
||||
|
||||
dbs = db_list(True, httprequest)
|
||||
|
||||
|
||||
@@ -70,6 +70,9 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=
|
||||
cr.commit()
|
||||
else:
|
||||
cr.rollback()
|
||||
# avoid keeping stale xml_id, etc. in cache
|
||||
openerp.modules.registry.RegistryManager.clear_caches(cr.dbname)
|
||||
|
||||
|
||||
def _get_files_of_kind(kind):
|
||||
if kind == 'demo':
|
||||
|
||||
@@ -68,8 +68,8 @@ class Registry(Mapping):
|
||||
# must be reloaded.
|
||||
# The `base_cache_signaling sequence` indicates all caches must be
|
||||
# invalidated (i.e. cleared).
|
||||
self.base_registry_signaling_sequence = 1
|
||||
self.base_cache_signaling_sequence = 1
|
||||
self.base_registry_signaling_sequence = None
|
||||
self.base_cache_signaling_sequence = None
|
||||
|
||||
# Flag indicating if at least one model cache has been cleared.
|
||||
# Useful only in a multi-process context.
|
||||
@@ -159,7 +159,7 @@ class Registry(Mapping):
|
||||
@classmethod
|
||||
def setup_multi_process_signaling(cls, cr):
|
||||
if not openerp.multi_process:
|
||||
return
|
||||
return None, None
|
||||
|
||||
# Inter-process signaling:
|
||||
# The `base_registry_signaling` sequence indicates the whole registry
|
||||
@@ -172,6 +172,16 @@ class Registry(Mapping):
|
||||
cr.execute("""SELECT nextval('base_registry_signaling')""")
|
||||
cr.execute("""CREATE SEQUENCE base_cache_signaling INCREMENT BY 1 START WITH 1""")
|
||||
cr.execute("""SELECT nextval('base_cache_signaling')""")
|
||||
|
||||
cr.execute("""
|
||||
SELECT base_registry_signaling.last_value,
|
||||
base_cache_signaling.last_value
|
||||
FROM base_registry_signaling, base_cache_signaling""")
|
||||
r, c = cr.fetchone()
|
||||
_logger.debug("Multiprocess load registry signaling: [Registry: # %s] "\
|
||||
"[Cache: # %s]",
|
||||
r, c)
|
||||
return r, c
|
||||
|
||||
@contextmanager
|
||||
def cursor(self, auto_commit=True):
|
||||
@@ -229,6 +239,10 @@ class RegistryManager(object):
|
||||
cls.delete(db_name)
|
||||
cls.registries[db_name] = registry
|
||||
try:
|
||||
with registry.cursor() as cr:
|
||||
seq_registry, seq_cache = Registry.setup_multi_process_signaling(cr)
|
||||
registry.base_registry_signaling_sequence = seq_registry
|
||||
registry.base_cache_signaling_sequence = seq_cache
|
||||
# This should be a method on Registry
|
||||
openerp.modules.load_modules(registry.db, force_demo, status, update_module)
|
||||
except Exception:
|
||||
@@ -242,7 +256,6 @@ class RegistryManager(object):
|
||||
|
||||
cr = registry.db.cursor()
|
||||
try:
|
||||
Registry.setup_multi_process_signaling(cr)
|
||||
registry.do_parent_store(cr)
|
||||
cr.commit()
|
||||
finally:
|
||||
@@ -304,16 +317,20 @@ class RegistryManager(object):
|
||||
base_cache_signaling.last_value
|
||||
FROM base_registry_signaling, base_cache_signaling""")
|
||||
r, c = cr.fetchone()
|
||||
_logger.debug("Multiprocess signaling check: [Registry - old# %s new# %s] "\
|
||||
"[Cache - old# %s new# %s]",
|
||||
registry.base_registry_signaling_sequence, r,
|
||||
registry.base_cache_signaling_sequence, c)
|
||||
# Check if the model registry must be reloaded (e.g. after the
|
||||
# database has been updated by another process).
|
||||
if registry.base_registry_signaling_sequence > 1 and registry.base_registry_signaling_sequence != r:
|
||||
if registry.base_registry_signaling_sequence is not None and registry.base_registry_signaling_sequence != r:
|
||||
changed = True
|
||||
_logger.info("Reloading the model registry after database signaling.")
|
||||
registry = cls.new(db_name)
|
||||
# Check if the model caches must be invalidated (e.g. after a write
|
||||
# occured on another process). Don't clear right after a registry
|
||||
# has been reload.
|
||||
elif registry.base_cache_signaling_sequence > 1 and registry.base_cache_signaling_sequence != c:
|
||||
elif registry.base_cache_signaling_sequence is not None and registry.base_cache_signaling_sequence != c:
|
||||
changed = True
|
||||
_logger.info("Invalidating all model caches after database signaling.")
|
||||
registry.clear_caches()
|
||||
@@ -352,6 +369,7 @@ class RegistryManager(object):
|
||||
@classmethod
|
||||
def signal_registry_change(cls, db_name):
|
||||
if openerp.multi_process and db_name in cls.registries:
|
||||
_logger.info("Registry changed, signaling through the database")
|
||||
registry = cls.get(db_name)
|
||||
cr = registry.db.cursor()
|
||||
r = 1
|
||||
|
||||
@@ -1018,7 +1018,7 @@ class expression(object):
|
||||
right += ' 23:59:59'
|
||||
push(create_substitution_leaf(leaf, (left, operator, right), working_model))
|
||||
|
||||
elif field.translate:
|
||||
elif field.translate and right:
|
||||
need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike')
|
||||
sql_operator = {'=like': 'like', '=ilike': 'ilike'}.get(operator, operator)
|
||||
if need_wildcard:
|
||||
|
||||
+23
-13
@@ -515,7 +515,7 @@ class browse_record(object):
|
||||
return self._id
|
||||
|
||||
def __str__(self):
|
||||
return "browse_record(%s, %d)" % (self._table_name, self._id)
|
||||
return "browse_record(%s, %s)" % (self._table_name, self._id)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, browse_record):
|
||||
@@ -899,11 +899,6 @@ class BaseModel(object):
|
||||
for c in new.keys():
|
||||
if new[c].manual:
|
||||
del new[c]
|
||||
# Duplicate float fields because they have a .digits
|
||||
# cache (which must be per-registry, not server-wide).
|
||||
for c in new.keys():
|
||||
if new[c]._type == 'float':
|
||||
new[c] = copy.copy(new[c])
|
||||
if hasattr(new, 'update'):
|
||||
new.update(cls.__dict__.get(s, {}))
|
||||
elif s=='_constraints':
|
||||
@@ -939,6 +934,13 @@ class BaseModel(object):
|
||||
if not getattr(cls, '_original_module', None):
|
||||
cls._original_module = cls._module
|
||||
obj = object.__new__(cls)
|
||||
|
||||
if hasattr(obj, '_columns'):
|
||||
# float fields are registry-dependent (digit attribute). Duplicate them to avoid issues.
|
||||
for c, f in obj._columns.items():
|
||||
if f._type == 'float':
|
||||
obj._columns[c] = copy.copy(f)
|
||||
|
||||
obj.__init__(pool, cr)
|
||||
return obj
|
||||
|
||||
@@ -1541,9 +1543,16 @@ class BaseModel(object):
|
||||
error_msgs = []
|
||||
for constraint in self._constraints:
|
||||
fun, msg, fields = constraint
|
||||
# We don't pass around the context here: validation code
|
||||
# must always yield the same results.
|
||||
if not fun(self, cr, uid, ids):
|
||||
try:
|
||||
# We don't pass around the context here: validation code
|
||||
# must always yield the same results.
|
||||
valid = fun(self, cr, uid, ids)
|
||||
extra_error = None
|
||||
except Exception, e:
|
||||
_logger.debug('Exception while validating constraint', exc_info=True)
|
||||
valid = False
|
||||
extra_error = tools.ustr(e)
|
||||
if not valid:
|
||||
# Check presence of __call__ directly instead of using
|
||||
# callable() because it will be deprecated as of Python 3.0
|
||||
if hasattr(msg, '__call__'):
|
||||
@@ -1555,6 +1564,8 @@ class BaseModel(object):
|
||||
translated_msg = tmp_msg
|
||||
else:
|
||||
translated_msg = trans._get_source(cr, uid, self._name, 'constraint', lng, msg)
|
||||
if extra_error:
|
||||
translated_msg += "\n\n%s\n%s" % (_('Error details:'), extra_error)
|
||||
error_msgs.append(
|
||||
_("The field(s) `%s` failed against a constraint: %s") % (', '.join(fields), translated_msg)
|
||||
)
|
||||
@@ -1837,7 +1848,7 @@ class BaseModel(object):
|
||||
# otherwise try to find the lowest priority matching ir.ui.view
|
||||
view_id = View.default_view(cr, uid, self._name, view_type, context=context)
|
||||
|
||||
# context for postproscessing might be overriden
|
||||
# context for post-processing might be overriden
|
||||
ctx = context
|
||||
if view_id:
|
||||
# read the view with inherited views applied
|
||||
@@ -1862,7 +1873,7 @@ class BaseModel(object):
|
||||
raise except_orm(_('Invalid Architecture!'), _("No default view of type '%s' could be found !") % view_type)
|
||||
|
||||
# Apply post processing, groups and modifiers etc...
|
||||
xarch, xfields = View.postprocess_and_fields( cr, uid, self._name, etree.fromstring(result['arch']), view_id, context=ctx)
|
||||
xarch, xfields = View.postprocess_and_fields(cr, uid, self._name, etree.fromstring(result['arch']), view_id, context=ctx)
|
||||
result['arch'] = xarch
|
||||
result['fields'] = xfields
|
||||
|
||||
@@ -1872,8 +1883,7 @@ class BaseModel(object):
|
||||
def clean(x):
|
||||
x = x[2]
|
||||
for key in toclean:
|
||||
if key in x:
|
||||
del x[key]
|
||||
x.pop(key, None)
|
||||
return x
|
||||
ir_values_obj = self.pool.get('ir.values')
|
||||
resprint = ir_values_obj.get(cr, uid, 'action', 'client_print_multi', [(self._name, False)], False, context)
|
||||
|
||||
@@ -255,9 +255,7 @@ class rml_parse(object):
|
||||
d = DEFAULT_DIGITS = 2
|
||||
if dp:
|
||||
decimal_precision_obj = self.pool['decimal.precision']
|
||||
ids = decimal_precision_obj.search(self.cr, self.uid, [('name', '=', dp)])
|
||||
if ids:
|
||||
d = decimal_precision_obj.browse(self.cr, self.uid, ids)[0].digits
|
||||
d = decimal_precision_obj.precision_get(self.cr, self.uid, dp)
|
||||
elif obj and f:
|
||||
res_digits = getattr(obj._columns[f], 'digits', lambda x: ((16, DEFAULT_DIGITS)))
|
||||
if isinstance(res_digits, tuple):
|
||||
|
||||
@@ -145,7 +145,7 @@ def exp_get_progress(id):
|
||||
raise Exception, e
|
||||
|
||||
def exp_drop(db_name):
|
||||
if not exp_db_exist(db_name):
|
||||
if db_name not in exp_list(True):
|
||||
return False
|
||||
openerp.modules.registry.RegistryManager.delete(db_name)
|
||||
openerp.sql_db.close_db(db_name)
|
||||
|
||||
@@ -19,7 +19,6 @@ import test_misc
|
||||
import test_orm
|
||||
import test_osv
|
||||
import test_translate
|
||||
import test_uninstall
|
||||
import test_view_validation
|
||||
import test_qweb
|
||||
import test_func
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# This assumes an existing but uninitialized database.
|
||||
import unittest2
|
||||
|
||||
import openerp
|
||||
from openerp import SUPERUSER_ID
|
||||
import common
|
||||
|
||||
DB = common.DB
|
||||
ADMIN_USER_ID = common.ADMIN_USER_ID
|
||||
|
||||
def registry(model):
|
||||
return openerp.modules.registry.RegistryManager.get(DB)[model]
|
||||
|
||||
def cursor():
|
||||
return openerp.modules.registry.RegistryManager.get(DB).db.cursor()
|
||||
|
||||
def model_exists(model_name):
|
||||
registry = openerp.modules.registry.RegistryManager.get(DB)
|
||||
return model_name in registry
|
||||
|
||||
def reload_registry():
|
||||
openerp.modules.registry.RegistryManager.new(
|
||||
DB, update_module=True)
|
||||
|
||||
def search_registry(model_name, domain):
|
||||
cr = cursor()
|
||||
model = registry(model_name)
|
||||
record_ids = model.search(cr, SUPERUSER_ID, domain, {})
|
||||
cr.close()
|
||||
return record_ids
|
||||
|
||||
def install_module(module_name):
|
||||
ir_module_module = registry('ir.module.module')
|
||||
cr = cursor()
|
||||
module_ids = ir_module_module.search(cr, SUPERUSER_ID,
|
||||
[('name', '=', module_name)], {})
|
||||
assert len(module_ids) == 1
|
||||
ir_module_module.button_install(cr, SUPERUSER_ID, module_ids, {})
|
||||
cr.commit()
|
||||
cr.close()
|
||||
reload_registry()
|
||||
|
||||
def uninstall_module(module_name):
|
||||
ir_module_module = registry('ir.module.module')
|
||||
cr = cursor()
|
||||
module_ids = ir_module_module.search(cr, SUPERUSER_ID,
|
||||
[('name', '=', module_name)], {})
|
||||
assert len(module_ids) == 1
|
||||
ir_module_module.button_uninstall(cr, SUPERUSER_ID, module_ids, {})
|
||||
cr.commit()
|
||||
cr.close()
|
||||
reload_registry()
|
||||
|
||||
class test_uninstall(unittest2.TestCase):
|
||||
"""
|
||||
Test the install/uninstall of a test module. The module is available in
|
||||
`openerp.tests` which should be present in the addons-path.
|
||||
"""
|
||||
|
||||
def test_01_install(self):
|
||||
""" Check a few things showing the module is installed. """
|
||||
install_module('test_uninstall')
|
||||
assert model_exists('test_uninstall.model')
|
||||
|
||||
assert search_registry('ir.model.data',
|
||||
[('module', '=', 'test_uninstall')])
|
||||
|
||||
assert search_registry('ir.model.fields',
|
||||
[('model', '=', 'test_uninstall.model')])
|
||||
|
||||
def test_02_uninstall(self):
|
||||
""" Check a few things showing the module is uninstalled. """
|
||||
uninstall_module('test_uninstall')
|
||||
assert not model_exists('test_uninstall.model')
|
||||
|
||||
assert not search_registry('ir.model.data',
|
||||
[('module', '=', 'test_uninstall')])
|
||||
|
||||
assert not search_registry('ir.model.fields',
|
||||
[('model', '=', 'test_uninstall.model')])
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest2.main()
|
||||
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
Reference in New Issue
Block a user