Add model field access
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
* Add model field access
|
||||
* Add loading attribute on fields
|
||||
* Remove priority attribute on fields
|
||||
* Model doesn't convert anymore ids for inherited methods
|
||||
|
||||
+141
-24
@@ -235,38 +235,36 @@ class ModelAccess(ModelSQL, ModelView):
|
||||
|
||||
@Cache('ir_model_access.check')
|
||||
def check(self, model_name, mode='read', raise_exception=True):
|
||||
'''
|
||||
Check access for model_name
|
||||
|
||||
:param model_name: the model name
|
||||
:param mode: 'read', 'write', 'create' or 'delete'
|
||||
:param raise_exception: raise an exception if the test failed
|
||||
|
||||
:return: a boolean
|
||||
'''
|
||||
assert mode in ['read', 'write', 'create', 'delete'], \
|
||||
'Invalid access mode for security'
|
||||
model_obj = self.pool.get(model_name)
|
||||
if hasattr(model_obj, 'table_query') \
|
||||
and model_obj.table_query():
|
||||
return False
|
||||
if Transaction().user == 0:
|
||||
return True
|
||||
|
||||
ir_model_obj = self.pool.get('ir.model')
|
||||
user_group_obj = self.pool.get('res.user-res.group')
|
||||
cursor = Transaction().cursor
|
||||
cursor.execute('SELECT MAX(CASE WHEN a.perm_'+mode+' THEN 1 else 0 END) '
|
||||
'FROM ir_model_access a '
|
||||
'JOIN "' + ir_model_obj._table + '" m '
|
||||
'ON (a.model = m.id) '
|
||||
'JOIN "' + user_group_obj._table + '" gu '
|
||||
'ON (gu.gid = a."group") '
|
||||
'WHERE m.model = %s AND gu.uid = %s',
|
||||
(model_name, Transaction().user,))
|
||||
row = cursor.fetchall()
|
||||
if row[0][0] is None:
|
||||
cursor.execute('SELECT ' \
|
||||
'MAX(CASE WHEN perm_' + mode + ' THEN 1 else 0 END) ' \
|
||||
'FROM ir_model_access a ' \
|
||||
'JOIN ir_model m ' \
|
||||
'ON (a.model = m.id) ' \
|
||||
'WHERE a."group" IS NULL AND m.model = %s', (model_name,))
|
||||
row = cursor.fetchall()
|
||||
if row[0][0] is None:
|
||||
return True
|
||||
|
||||
if not row[0][0]:
|
||||
cursor.execute('SELECT MAX(CASE WHEN a.perm_%s THEN 1 ELSE 0 END) '
|
||||
'FROM "%s" AS a '
|
||||
'JOIN "%s" AS m '
|
||||
'ON (a.model = m.id) '
|
||||
'LEFT JOIN "%s" AS gu '
|
||||
'ON (gu.gid = a."group") '
|
||||
'WHERE m.model = %%s AND (gu.uid = %%s OR a."group" IS NULL)'
|
||||
% (mode, self._table, ir_model_obj._table,
|
||||
user_group_obj._table),
|
||||
(model_name, Transaction().user))
|
||||
access, = cursor.fetchone()
|
||||
if not access and access is not None:
|
||||
if raise_exception:
|
||||
self.raise_user_error(mode, model_name)
|
||||
else:
|
||||
@@ -309,6 +307,125 @@ class ModelAccess(ModelSQL, ModelView):
|
||||
ModelAccess()
|
||||
|
||||
|
||||
class ModelFieldAccess(ModelSQL, ModelView):
|
||||
"Model Field Access"
|
||||
_name = 'ir.model.field.access'
|
||||
_description = __doc__
|
||||
_rec_name = 'field'
|
||||
field = fields.Many2One('ir.model.field', 'Field', required=True,
|
||||
ondelete='CASCADE')
|
||||
group = fields.Many2One('res.group', 'Group', ondelete='CASCADE')
|
||||
perm_read = fields.Boolean('Read Access')
|
||||
perm_write = fields.Boolean('Write Access')
|
||||
description = fields.Text('Description')
|
||||
|
||||
def __init__(self):
|
||||
super(ModelFieldAccess, self).__init__()
|
||||
self._sql_constraints += [
|
||||
('field_group_uniq', 'UNIQUE("field", "group")',
|
||||
'Only one record by field and group is allowed!'),
|
||||
]
|
||||
self._error_messages.update({
|
||||
'read': 'You can not read the field! (%s.%s)',
|
||||
'write': 'You can not write on the field! (%s.%s)',
|
||||
})
|
||||
|
||||
def check_xml_record(self, ids, values):
|
||||
return True
|
||||
|
||||
def default_perm_read(self):
|
||||
return False
|
||||
|
||||
def default_perm_write(self):
|
||||
return False
|
||||
|
||||
@Cache('ir_model_field_access.check')
|
||||
def check(self, model_name, fields, mode='read', raise_exception=True,
|
||||
access=False):
|
||||
'''
|
||||
Check access for fields on model_name.
|
||||
|
||||
:param model_name: the model name
|
||||
:param fields: a list of fields
|
||||
:param mode: 'read' or 'write'
|
||||
:param raise_exception: raise an exception if the test failed
|
||||
:param access: return a dictionary with access right instead of boolean
|
||||
|
||||
:return: a boolean
|
||||
'''
|
||||
assert mode in ('read', 'write'), 'Invalid access mode'
|
||||
if Transaction().user == 0:
|
||||
if access:
|
||||
return dict((x, True) for x in fields)
|
||||
return True
|
||||
|
||||
ir_model_obj = self.pool.get('ir.model')
|
||||
ir_model_field_obj = self.pool.get('ir.model.field')
|
||||
user_group_obj = self.pool.get('res.user-res.group')
|
||||
|
||||
cursor = Transaction().cursor
|
||||
|
||||
cursor.execute('SELECT f.name, '
|
||||
'MAX(CASE WHEN a.perm_%s THEN 1 ELSE 0 END) '
|
||||
'FROM "%s" AS a '
|
||||
'JOIN "%s" AS f '
|
||||
'ON (a.field = f.id) '
|
||||
'JOIN "%s" AS m '
|
||||
'ON (f.model = m.id) '
|
||||
'LEFT JOIN "%s" AS gu '
|
||||
'ON (gu.gid = a."group") '
|
||||
'WHERE m.model = %%s AND (gu.uid = %%s OR a."group" IS NULL) '
|
||||
'GROUP BY f.name'
|
||||
% (mode, self._table, ir_model_field_obj._table,
|
||||
ir_model_obj._table, user_group_obj._table),
|
||||
(model_name, Transaction().user))
|
||||
accesses = dict(cursor.fetchall())
|
||||
if access:
|
||||
return accesses
|
||||
for field in fields:
|
||||
if not accesses.get(field, True):
|
||||
if raise_exception:
|
||||
self.raise_user_error(mode, (model_name, field))
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
def write(self, ids, vals):
|
||||
res = super(ModelFieldAccess, self).write(ids, vals)
|
||||
# Restart the cache
|
||||
self.check.reset()
|
||||
for _, model in self.pool.iterobject():
|
||||
try:
|
||||
model.fields_view_get.reset()
|
||||
except Exception:
|
||||
pass
|
||||
return res
|
||||
|
||||
def create(self, vals):
|
||||
res = super(ModelFieldAccess, self).create(vals)
|
||||
# Restart the cache
|
||||
self.check.reset()
|
||||
for _, model in self.pool.iterobject():
|
||||
try:
|
||||
model.fields_view_get.reset()
|
||||
except Exception:
|
||||
pass
|
||||
return res
|
||||
|
||||
def delete(self, ids):
|
||||
res = super(ModelFieldAccess, self).delete(ids)
|
||||
# Restart the cache
|
||||
self.check.reset()
|
||||
for _, model in self.pool.iterobject():
|
||||
try:
|
||||
model.fields_view_get.reset()
|
||||
except Exception:
|
||||
pass
|
||||
return res
|
||||
|
||||
ModelFieldAccess()
|
||||
|
||||
|
||||
class ModelData(ModelSQL, ModelView):
|
||||
"Model data"
|
||||
_name = 'ir.model.data'
|
||||
|
||||
@@ -190,6 +190,63 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<menuitem parent="ir.menu_models"
|
||||
action="act_model_access_form" id="menu_model_access_form"/>
|
||||
|
||||
<record model="ir.ui.view" id="model_field_access_view_tree">
|
||||
<field name="model">ir.model.field.access</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="arch" type="xml">
|
||||
<![CDATA[
|
||||
<tree string="Field Access">
|
||||
<field name="perm_read" select="2"/>
|
||||
<field name="perm_write" select="2"/>
|
||||
<field name="field" select="1"/>
|
||||
<field name="group" select="1"/>
|
||||
</tree>
|
||||
]]>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="model_field_access_view_form">
|
||||
<field name="model">ir.model.field.access</field>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<![CDATA[
|
||||
<form string="Field Access">
|
||||
<label name="field"/>
|
||||
<field name="field"/>
|
||||
<label name="group"/>
|
||||
<field name="group"/>
|
||||
<newline/>
|
||||
<label name="perm_read"/>
|
||||
<field name="perm_read"/>
|
||||
<label name="perm_write"/>
|
||||
<field name="perm_write"/>
|
||||
<separator name="description" colspan="4"/>
|
||||
<field name="description" colspan="4"/>
|
||||
</form>
|
||||
]]>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.act_window" id="act_model_field_access_form">
|
||||
<field name="name">Fields Access</field>
|
||||
<field name="res_model">ir.model.field.access</field>
|
||||
<field name="view_type">form</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view"
|
||||
id="act_model_field_access_form_view1">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="model_field_access_view_tree"/>
|
||||
<field name="act_window" ref="act_model_field_access_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view"
|
||||
id="act_model_field_access_form_view2">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="view" ref="model_field_access_view_form"/>
|
||||
<field name="act_window" ref="act_model_field_access_form"/>
|
||||
</record>
|
||||
<menuitem parent="menu_model_access_form"
|
||||
action="act_model_field_access_form" id="menu_model_field_access_form"/>
|
||||
|
||||
<record model="ir.action.report" id="report_model_graph">
|
||||
<field name="name">Graph</field>
|
||||
<field name="model">ir.model</field>
|
||||
|
||||
+15
-2
@@ -90,13 +90,26 @@ class BrowseRecord(object):
|
||||
% (name, self._model._name))
|
||||
|
||||
if col.loading == 'eager':
|
||||
field_access_obj = self._model.pool.get('ir.model.field.access')
|
||||
fread_accesses = {}
|
||||
for inherit_name in self._model._inherits:
|
||||
inherit_obj = self._model.pool.get(inherit_name)
|
||||
fread_accesses.update(field_access_obj.check(inherit_name,
|
||||
inherit_obj._columns.keys(), 'read', access=True))
|
||||
fread_accesses.update(field_access_obj.check(self._model._name,
|
||||
self._model._columns.keys(), 'read', access=True))
|
||||
to_remove = set(x for x, y in fread_accesses.iteritems()
|
||||
if not y and x != name)
|
||||
|
||||
ffields = dict((fname, field) for fname, (_, _, field)
|
||||
in self._model._inherit_fields.iteritems()
|
||||
if field.loading == 'eager'
|
||||
and fname not in self._model._columns)
|
||||
and fname not in self._model._columns
|
||||
and fname not in to_remove)
|
||||
ffields.update(dict((fname, field) for fname, field
|
||||
in self._model._columns.iteritems()
|
||||
if field.loading == 'eager'))
|
||||
if field.loading == 'eager'
|
||||
and fname not in to_remove))
|
||||
else:
|
||||
ffields = {name: col}
|
||||
|
||||
|
||||
@@ -619,6 +619,8 @@ class Model(object):
|
||||
res = {}
|
||||
translation_obj = self.pool.get('ir.translation')
|
||||
model_access_obj = self.pool.get('ir.model.access')
|
||||
field_access_obj = self.pool.get('ir.model.field.access')
|
||||
|
||||
for parent in self._inherits:
|
||||
res.update(self.pool.get(parent).fields_get(fields_names))
|
||||
write_access = model_access_obj.check(self._name, 'write',
|
||||
@@ -648,6 +650,8 @@ class Model(object):
|
||||
|
||||
encoder = PYSONEncoder()
|
||||
|
||||
fwrite_accesses = field_access_obj.check(self._name, fields_names or
|
||||
self._columns.keys(), 'write', access=True)
|
||||
for field in (x for x in self._columns.keys()
|
||||
if ((not fields_names) or x in fields_names)):
|
||||
res[field] = {'type': self._columns[field]._type}
|
||||
@@ -671,7 +675,7 @@ class Model(object):
|
||||
if getattr(self._columns[field], arg, None) != None:
|
||||
res[field][arg] = copy.copy(getattr(self._columns[field],
|
||||
arg))
|
||||
if not write_access:
|
||||
if not write_access or not fwrite_accesses.get(field, True):
|
||||
res[field]['readonly'] = True
|
||||
if res[field].get('states') and \
|
||||
'readonly' in res[field]['states']:
|
||||
|
||||
@@ -82,7 +82,11 @@ class ModelStorage(Model):
|
||||
:return: the id of the created record
|
||||
'''
|
||||
model_access_obj = self.pool.get('ir.model.access')
|
||||
model_field_access_obj = self.pool.get('ir.model.field.access')
|
||||
|
||||
model_access_obj.check(self._name, 'create')
|
||||
model_field_access_obj.check(self._name,
|
||||
[x for x in values if x in self._columns], 'write')
|
||||
self.__clean_xxx2many_cache()
|
||||
return False
|
||||
|
||||
@@ -113,8 +117,11 @@ class ModelStorage(Model):
|
||||
and fields value as value. The list will not be in the same order.
|
||||
'''
|
||||
model_access_obj = self.pool.get('ir.model.access')
|
||||
model_field_access_obj = self.pool.get('ir.model.field.access')
|
||||
|
||||
model_access_obj.check(self._name, 'read')
|
||||
model_field_access_obj.check(self._name,
|
||||
fields_names or self._columns.keys(), 'read')
|
||||
if isinstance(ids, (int, long)):
|
||||
return {}
|
||||
return []
|
||||
@@ -129,8 +136,11 @@ class ModelStorage(Model):
|
||||
:return: True if succeed
|
||||
'''
|
||||
model_access_obj = self.pool.get('ir.model.access')
|
||||
model_field_access_obj = self.pool.get('ir.model.field.access')
|
||||
|
||||
model_access_obj.check(self._name, 'write')
|
||||
model_field_access_obj.check(self._name,
|
||||
[x for x in values if x in self._columns], 'write')
|
||||
if not self.check_xml_record(ids, values):
|
||||
self.raise_user_error('write_xml_record',
|
||||
error_description='xml_record_desc')
|
||||
|
||||
@@ -256,9 +256,45 @@ class ModelView(Model):
|
||||
return value
|
||||
|
||||
def _view_look_dom_arch(self, tree, type):
|
||||
field_access_obj = self.pool.get('ir.model.field.access')
|
||||
|
||||
fields_width = {}
|
||||
tree_root = tree.getroottree().getroot()
|
||||
|
||||
# Find field without read access
|
||||
fread_accesses = field_access_obj.check(self._name,
|
||||
self._columns.keys(), 'read', access=True)
|
||||
fields_to_remove = list(x for x, y in fread_accesses.iteritems()
|
||||
if not y)
|
||||
for name, field in self._columns.iteritems():
|
||||
for field_to_remove in fields_to_remove:
|
||||
if field_to_remove in field.depends:
|
||||
fields_to_remove.append(name)
|
||||
|
||||
# Find field inherited without read access
|
||||
for inherit_name in self._inherits:
|
||||
inherit_obj = self.pool.get(inherit_name)
|
||||
fread_accesses = field_access_obj.check(inherit_obj._name,
|
||||
inherit_obj._columns.keys(), 'read', access=True)
|
||||
fields_to_remove += list(x for x, y in fread_accesses.iteritems()
|
||||
if not y and x not in self._columns.keys())
|
||||
for name, field in inherit_obj._columns.iteritems():
|
||||
for field_to_remove in fields_to_remove:
|
||||
if field_to_remove in field.depends:
|
||||
fields_to_remove.append(name)
|
||||
|
||||
# Remove field without read access
|
||||
for field in fields_to_remove:
|
||||
for element in tree.xpath(
|
||||
'//field[@name="%s"] | //label[@name="%s"]'
|
||||
% (field, field)):
|
||||
if type == 'form':
|
||||
element.tag = 'label'
|
||||
element.attrib.clear()
|
||||
elif type == 'tree':
|
||||
parent = element.getparent()
|
||||
parent.remove(element)
|
||||
|
||||
if type == 'tree':
|
||||
viewtreewidth_obj = self.pool.get('ir.ui.view_tree_width')
|
||||
viewtreewidth_ids = viewtreewidth_obj.search([
|
||||
|
||||
@@ -7,3 +7,4 @@ from mptt import *
|
||||
from import_data import *
|
||||
from export_data import *
|
||||
from trigger import *
|
||||
from access import *
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
#This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
#this repository contains the full copyright notices and license terms.
|
||||
from trytond.model import ModelSQL, fields
|
||||
|
||||
|
||||
class TestAccess(ModelSQL):
|
||||
'Test Access'
|
||||
_name = 'test.access'
|
||||
_description = __doc__
|
||||
|
||||
field1 = fields.Char('Field 1')
|
||||
field2 = fields.Char('Field 2')
|
||||
|
||||
TestAccess()
|
||||
@@ -0,0 +1,794 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
#this repository contains the full copyright notices and license terms.
|
||||
from __future__ import with_statement
|
||||
import unittest
|
||||
from trytond.tests.test_tryton import POOL, DB_NAME, USER, CONTEXT, \
|
||||
install_module
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
|
||||
class ModelAccessTestCase(unittest.TestCase):
|
||||
'''
|
||||
Test Model Access
|
||||
'''
|
||||
|
||||
def setUp(self):
|
||||
install_module('test')
|
||||
self.model_access = POOL.get('ir.model.access')
|
||||
self.test_access = POOL.get('test.access')
|
||||
self.model = POOL.get('ir.model')
|
||||
self.group = POOL.get('res.group')
|
||||
|
||||
def test0010perm_read(self):
|
||||
'''
|
||||
Test Read Access
|
||||
'''
|
||||
with Transaction().start(DB_NAME, USER, CONTEXT) as transaction:
|
||||
model_id, = self.model.search([('model', '=', 'test.access')])
|
||||
|
||||
test_id = self.test_access.create({})
|
||||
|
||||
# Without model access
|
||||
self.test_access.read(test_id)
|
||||
|
||||
# With model access
|
||||
|
||||
# One access allowed for any group
|
||||
model_access_wo_group_id = self.model_access.create({
|
||||
'model': model_id,
|
||||
'group': False,
|
||||
'perm_read': True,
|
||||
})
|
||||
self.test_access.read(test_id)
|
||||
|
||||
# One access disallowed for any group
|
||||
self.model_access.write(model_access_wo_group_id, {
|
||||
'perm_read': False,
|
||||
})
|
||||
self.failUnlessRaises(Exception, self.test_access.read,
|
||||
test_id)
|
||||
|
||||
# Two access rules with one group allowed
|
||||
group_id = self.group.search([('users', '=', USER)])[0]
|
||||
model_access_w_group_id = self.model_access.create({
|
||||
'model': model_id,
|
||||
'group': group_id,
|
||||
'perm_read': True,
|
||||
})
|
||||
|
||||
self.test_access.read(test_id)
|
||||
|
||||
# Two access rules with both allowed
|
||||
self.model_access.write(model_access_wo_group_id, {
|
||||
'perm_read': True,
|
||||
})
|
||||
self.test_access.read(test_id)
|
||||
|
||||
# Two access rules with any group allowed
|
||||
self.model_access.write(model_access_w_group_id, {
|
||||
'perm_read': False,
|
||||
})
|
||||
self.test_access.read(test_id)
|
||||
|
||||
# Two access rules with both disallowed
|
||||
self.model_access.write(model_access_wo_group_id, {
|
||||
'perm_read': False,
|
||||
})
|
||||
self.failUnlessRaises(Exception, self.test_access.read,
|
||||
test_id)
|
||||
|
||||
# One access disallowed for one group
|
||||
self.model_access.delete(model_access_wo_group_id)
|
||||
self.failUnlessRaises(Exception, self.test_access.read,
|
||||
test_id)
|
||||
|
||||
# One access allowed for one group
|
||||
self.model_access.write(model_access_w_group_id, {
|
||||
'perm_read': True,
|
||||
})
|
||||
self.test_access.read(test_id)
|
||||
|
||||
# One access allowed for one other group
|
||||
group_id = self.group.create({'name': 'Test'})
|
||||
self.model_access.write(model_access_w_group_id, {
|
||||
'group': group_id,
|
||||
})
|
||||
self.test_access.read(test_id)
|
||||
|
||||
# One access disallowed for one other group
|
||||
self.model_access.write(model_access_w_group_id, {
|
||||
'perm_read': False,
|
||||
})
|
||||
self.test_access.read(test_id)
|
||||
|
||||
transaction.cursor.rollback()
|
||||
|
||||
def test0020perm_write(self):
|
||||
'''
|
||||
Test Write Access
|
||||
'''
|
||||
with Transaction().start(DB_NAME, USER, CONTEXT) as transaction:
|
||||
model_id, = self.model.search([('model', '=', 'test.access')])
|
||||
|
||||
test_id = self.test_access.create({})
|
||||
|
||||
# Without model access
|
||||
self.test_access.write(test_id, {})
|
||||
|
||||
# With model access
|
||||
|
||||
# One access allowed for any group
|
||||
model_access_wo_group_id = self.model_access.create({
|
||||
'model': model_id,
|
||||
'group': False,
|
||||
'perm_write': True,
|
||||
})
|
||||
self.test_access.write(test_id, {})
|
||||
|
||||
# One access disallowed for any group
|
||||
self.model_access.write(model_access_wo_group_id, {
|
||||
'perm_write': False,
|
||||
})
|
||||
self.failUnlessRaises(Exception, self.test_access.write,
|
||||
test_id, {})
|
||||
|
||||
# Two access rules with one group allowed
|
||||
group_id = self.group.search([('users', '=', USER)])[0]
|
||||
model_access_w_group_id = self.model_access.create({
|
||||
'model': model_id,
|
||||
'group': group_id,
|
||||
'perm_write': True,
|
||||
})
|
||||
|
||||
self.test_access.write(test_id, {})
|
||||
|
||||
# Two access rules with both allowed
|
||||
self.model_access.write(model_access_wo_group_id, {
|
||||
'perm_write': True,
|
||||
})
|
||||
self.test_access.write(test_id, {})
|
||||
|
||||
# Two access rules with any group allowed
|
||||
self.model_access.write(model_access_w_group_id, {
|
||||
'perm_write': False,
|
||||
})
|
||||
self.test_access.write(test_id, {})
|
||||
|
||||
# Two access rules with both disallowed
|
||||
self.model_access.write(model_access_wo_group_id, {
|
||||
'perm_write': False,
|
||||
})
|
||||
self.failUnlessRaises(Exception, self.test_access.write,
|
||||
test_id, {})
|
||||
|
||||
# One access disallowed for one group
|
||||
self.model_access.delete(model_access_wo_group_id)
|
||||
self.failUnlessRaises(Exception, self.test_access.write,
|
||||
test_id, {})
|
||||
|
||||
# One access allowed for one group
|
||||
self.model_access.write(model_access_w_group_id, {
|
||||
'perm_write': True,
|
||||
})
|
||||
self.test_access.write(test_id, {})
|
||||
|
||||
# One access allowed for one other group
|
||||
group_id = self.group.create({'name': 'Test'})
|
||||
self.model_access.write(model_access_w_group_id, {
|
||||
'group': group_id,
|
||||
})
|
||||
self.test_access.write(test_id, {})
|
||||
|
||||
# One access disallowed for one other group
|
||||
self.model_access.write(model_access_w_group_id, {
|
||||
'perm_write': False,
|
||||
})
|
||||
self.test_access.write(test_id, {})
|
||||
|
||||
transaction.cursor.rollback()
|
||||
|
||||
def test0030perm_create(self):
|
||||
'''
|
||||
Test Create Access
|
||||
'''
|
||||
with Transaction().start(DB_NAME, USER, CONTEXT) as transaction:
|
||||
model_id, = self.model.search([('model', '=', 'test.access')])
|
||||
|
||||
# Without model access
|
||||
test_id = self.test_access.create({})
|
||||
|
||||
# With model access
|
||||
|
||||
# One access allowed for any group
|
||||
model_access_wo_group_id = self.model_access.create({
|
||||
'model': model_id,
|
||||
'group': False,
|
||||
'perm_create': True,
|
||||
})
|
||||
self.test_access.create({})
|
||||
|
||||
# One access disallowed for any group
|
||||
self.model_access.write(model_access_wo_group_id, {
|
||||
'perm_create': False,
|
||||
})
|
||||
self.failUnlessRaises(Exception, self.test_access.create, {})
|
||||
|
||||
# Two access rules with one group allowed
|
||||
group_id = self.group.search([('users', '=', USER)])[0]
|
||||
model_access_w_group_id = self.model_access.create({
|
||||
'model': model_id,
|
||||
'group': group_id,
|
||||
'perm_create': True,
|
||||
})
|
||||
|
||||
self.test_access.create({})
|
||||
|
||||
# Two access rules with both allowed
|
||||
self.model_access.write(model_access_wo_group_id, {
|
||||
'perm_create': True,
|
||||
})
|
||||
self.test_access.create({})
|
||||
|
||||
# Two access rules with any group allowed
|
||||
self.model_access.write(model_access_w_group_id, {
|
||||
'perm_create': False,
|
||||
})
|
||||
self.test_access.create({})
|
||||
|
||||
# Two access rules with both disallowed
|
||||
self.model_access.write(model_access_wo_group_id, {
|
||||
'perm_create': False,
|
||||
})
|
||||
self.failUnlessRaises(Exception, self.test_access.create, {})
|
||||
|
||||
# One access disallowed for one group
|
||||
self.model_access.delete(model_access_wo_group_id)
|
||||
self.failUnlessRaises(Exception, self.test_access.create, {})
|
||||
|
||||
# One access allowed for one group
|
||||
self.model_access.write(model_access_w_group_id, {
|
||||
'perm_create': True,
|
||||
})
|
||||
self.test_access.create({})
|
||||
|
||||
# One access allowed for one other group
|
||||
group_id = self.group.create({'name': 'Test'})
|
||||
self.model_access.write(model_access_w_group_id, {
|
||||
'group': group_id,
|
||||
})
|
||||
self.test_access.create({})
|
||||
|
||||
# One access disallowed for one other group
|
||||
self.model_access.write(model_access_w_group_id, {
|
||||
'perm_create': False,
|
||||
})
|
||||
self.test_access.create({})
|
||||
|
||||
transaction.cursor.rollback()
|
||||
|
||||
def test0040perm_delete(self):
|
||||
'''
|
||||
Test Delete Access
|
||||
'''
|
||||
with Transaction().start(DB_NAME, USER, CONTEXT) as transaction:
|
||||
model_id, = self.model.search([('model', '=', 'test.access')])
|
||||
|
||||
test_ids = [self.test_access.create({}) for x in range(11)]
|
||||
|
||||
# Without model access
|
||||
test_id = self.test_access.delete(test_ids.pop())
|
||||
|
||||
# With model access
|
||||
|
||||
# One access allowed for any group
|
||||
model_access_wo_group_id = self.model_access.create({
|
||||
'model': model_id,
|
||||
'group': False,
|
||||
'perm_delete': True,
|
||||
})
|
||||
self.test_access.delete(test_ids.pop())
|
||||
|
||||
# One access disallowed for any group
|
||||
self.model_access.write(model_access_wo_group_id, {
|
||||
'perm_delete': False,
|
||||
})
|
||||
self.failUnlessRaises(Exception, self.test_access.delete,
|
||||
test_ids.pop())
|
||||
|
||||
# Two access rules with one group allowed
|
||||
group_id = self.group.search([('users', '=', USER)])[0]
|
||||
model_access_w_group_id = self.model_access.create({
|
||||
'model': model_id,
|
||||
'group': group_id,
|
||||
'perm_delete': True,
|
||||
})
|
||||
|
||||
self.test_access.delete(test_ids.pop())
|
||||
|
||||
# Two access rules with both allowed
|
||||
self.model_access.write(model_access_wo_group_id, {
|
||||
'perm_delete': True,
|
||||
})
|
||||
self.test_access.delete(test_ids.pop())
|
||||
|
||||
# Two access rules with any group allowed
|
||||
self.model_access.write(model_access_w_group_id, {
|
||||
'perm_delete': False,
|
||||
})
|
||||
self.test_access.delete(test_ids.pop())
|
||||
|
||||
# Two access rules with both disallowed
|
||||
self.model_access.write(model_access_wo_group_id, {
|
||||
'perm_delete': False,
|
||||
})
|
||||
self.failUnlessRaises(Exception, self.test_access.delete,
|
||||
test_ids.pop())
|
||||
|
||||
# One access disallowed for one group
|
||||
self.model_access.delete(model_access_wo_group_id)
|
||||
self.failUnlessRaises(Exception, self.test_access.delete,
|
||||
test_ids.pop())
|
||||
|
||||
# One access allowed for one group
|
||||
self.model_access.write(model_access_w_group_id, {
|
||||
'perm_delete': True,
|
||||
})
|
||||
self.test_access.delete(test_ids.pop())
|
||||
|
||||
# One access allowed for one other group
|
||||
group_id = self.group.create({'name': 'Test'})
|
||||
self.model_access.write(model_access_w_group_id, {
|
||||
'group': group_id,
|
||||
})
|
||||
self.test_access.delete(test_ids.pop())
|
||||
|
||||
# One access disallowed for one other group
|
||||
self.model_access.write(model_access_w_group_id, {
|
||||
'perm_delete': False,
|
||||
})
|
||||
self.test_access.delete(test_ids.pop())
|
||||
|
||||
transaction.cursor.rollback()
|
||||
|
||||
|
||||
class ModelFieldAccessTestCase(unittest.TestCase):
|
||||
'''
|
||||
Test Model Field Access
|
||||
'''
|
||||
|
||||
def setUp(self):
|
||||
install_module('test')
|
||||
self.field_access = POOL.get('ir.model.field.access')
|
||||
self.test_access = POOL.get('test.access')
|
||||
self.field = POOL.get('ir.model.field')
|
||||
self.group = POOL.get('res.group')
|
||||
|
||||
def test0010perm_read(self):
|
||||
'''
|
||||
Test Read Access
|
||||
'''
|
||||
with Transaction().start(DB_NAME, USER, CONTEXT) as transaction:
|
||||
field1_id, = self.field.search([
|
||||
('model.model', '=', 'test.access'),
|
||||
('name', '=', 'field1'),
|
||||
])
|
||||
field2_id, = self.field.search([
|
||||
('model.model', '=', 'test.access'),
|
||||
('name', '=', 'field2'),
|
||||
])
|
||||
|
||||
test_id = self.test_access.create({
|
||||
'field1': 'ham',
|
||||
'field2': 'spam',
|
||||
})
|
||||
|
||||
# Without field access
|
||||
self.test_access.read(test_id, ['field1'])
|
||||
self.test_access.read(test_id, ['field2'])
|
||||
self.test_access.read(test_id)
|
||||
test_access = self.test_access.browse(test_id)
|
||||
test_access.field1
|
||||
test_access.field2
|
||||
transaction.cursor.cache.clear()
|
||||
|
||||
# With field access
|
||||
|
||||
# One access allowed for any group
|
||||
field_access_wo_group_id = self.field_access.create({
|
||||
'field': field1_id,
|
||||
'group': False,
|
||||
'perm_read': True,
|
||||
})
|
||||
self.test_access.read(test_id, ['field1'])
|
||||
self.test_access.read(test_id, ['field2'])
|
||||
self.test_access.read(test_id)
|
||||
test_access = self.test_access.browse(test_id)
|
||||
test_access.field1
|
||||
test_access.field2
|
||||
transaction.cursor.cache.clear()
|
||||
|
||||
# One access disallowed for any group
|
||||
self.field_access.write(field_access_wo_group_id, {
|
||||
'perm_read': False,
|
||||
})
|
||||
|
||||
self.failUnlessRaises(Exception, self.test_access.read, test_id,
|
||||
['field1'])
|
||||
self.test_access.read(test_id, ['field2'])
|
||||
self.failUnlessRaises(Exception, self.test_access.read, test_id)
|
||||
test_access = self.test_access.browse(test_id)
|
||||
self.failUnlessRaises(Exception, getattr, test_access, 'field1')
|
||||
test_access.field2
|
||||
transaction.cursor.cache.clear()
|
||||
|
||||
# Two access rules with one group allowed
|
||||
group_id = self.group.search([('users', '=', USER)])[0]
|
||||
field_access_w_group_id = self.field_access.create({
|
||||
'field': field1_id,
|
||||
'group': group_id,
|
||||
'perm_read': True,
|
||||
})
|
||||
|
||||
self.test_access.read(test_id, ['field1'])
|
||||
self.test_access.read(test_id, ['field2'])
|
||||
self.test_access.read(test_id)
|
||||
test_access = self.test_access.browse(test_id)
|
||||
test_access.field1
|
||||
test_access.field2
|
||||
transaction.cursor.cache.clear()
|
||||
|
||||
# Two access rules with both allowed
|
||||
self.field_access.write(field_access_wo_group_id, {
|
||||
'perm_read': True,
|
||||
})
|
||||
self.test_access.read(test_id, ['field1'])
|
||||
self.test_access.read(test_id, ['field2'])
|
||||
self.test_access.read(test_id)
|
||||
test_access = self.test_access.browse(test_id)
|
||||
test_access.field1
|
||||
test_access.field2
|
||||
transaction.cursor.cache.clear()
|
||||
|
||||
# Two access rules with any group allowed
|
||||
self.field_access.write(field_access_w_group_id, {
|
||||
'perm_read': False,
|
||||
})
|
||||
self.test_access.read(test_id, ['field1'])
|
||||
self.test_access.read(test_id, ['field2'])
|
||||
self.test_access.read(test_id)
|
||||
test_access = self.test_access.browse(test_id)
|
||||
test_access.field1
|
||||
test_access.field2
|
||||
transaction.cursor.cache.clear()
|
||||
|
||||
# Two access rules with both disallowed
|
||||
self.field_access.write(field_access_wo_group_id, {
|
||||
'perm_read': False,
|
||||
})
|
||||
self.failUnlessRaises(Exception, self.test_access.read, test_id,
|
||||
['field1'])
|
||||
self.test_access.read(test_id, ['field2'])
|
||||
self.failUnlessRaises(Exception, self.test_access.read, test_id)
|
||||
test_access = self.test_access.browse(test_id)
|
||||
self.failUnlessRaises(Exception, getattr, test_access, 'field1')
|
||||
test_access.field2
|
||||
transaction.cursor.cache.clear()
|
||||
|
||||
# One access disallowed for one group
|
||||
self.field_access.delete(field_access_wo_group_id)
|
||||
self.failUnlessRaises(Exception, self.test_access.read, test_id,
|
||||
['field1'])
|
||||
self.test_access.read(test_id, ['field2'])
|
||||
self.failUnlessRaises(Exception, self.test_access.read, test_id)
|
||||
test_access = self.test_access.browse(test_id)
|
||||
self.failUnlessRaises(Exception, getattr, test_access, 'field1')
|
||||
test_access.field2
|
||||
transaction.cursor.cache.clear()
|
||||
|
||||
# One access allowed for one group
|
||||
self.field_access.write(field_access_w_group_id, {
|
||||
'perm_read': True,
|
||||
})
|
||||
self.test_access.read(test_id, ['field1'])
|
||||
self.test_access.read(test_id, ['field2'])
|
||||
self.test_access.read(test_id)
|
||||
test_access = self.test_access.browse(test_id)
|
||||
test_access.field1
|
||||
test_access.field2
|
||||
transaction.cursor.cache.clear()
|
||||
|
||||
# One access allowed for one other group
|
||||
group_id = self.group.create({'name': 'Test'})
|
||||
self.field_access.write(field_access_w_group_id, {
|
||||
'group': group_id,
|
||||
})
|
||||
self.test_access.read(test_id, ['field1'])
|
||||
self.test_access.read(test_id, ['field2'])
|
||||
self.test_access.read(test_id)
|
||||
test_access = self.test_access.browse(test_id)
|
||||
test_access.field1
|
||||
test_access.field2
|
||||
transaction.cursor.cache.clear()
|
||||
|
||||
# One access disallowed for one other group
|
||||
self.field_access.write(field_access_w_group_id, {
|
||||
'perm_read': False,
|
||||
})
|
||||
self.test_access.read(test_id, ['field1'])
|
||||
self.test_access.read(test_id, ['field2'])
|
||||
self.test_access.read(test_id)
|
||||
test_access = self.test_access.browse(test_id)
|
||||
test_access.field1
|
||||
test_access.field2
|
||||
transaction.cursor.cache.clear()
|
||||
|
||||
# Two access rules on both fields allowed
|
||||
self.field_access.delete(field_access_w_group_id)
|
||||
|
||||
field_access1 = self.field_access.create({
|
||||
'field': field1_id,
|
||||
'group': False,
|
||||
'perm_read': True,
|
||||
})
|
||||
field_access2 = self.field_access.create({
|
||||
'field': field2_id,
|
||||
'group': False,
|
||||
'perm_read': True,
|
||||
})
|
||||
|
||||
self.test_access.read(test_id, ['field1'])
|
||||
self.test_access.read(test_id, ['field2'])
|
||||
self.test_access.read(test_id)
|
||||
test_access = self.test_access.browse(test_id)
|
||||
test_access.field1
|
||||
test_access.field2
|
||||
transaction.cursor.cache.clear()
|
||||
|
||||
# Two access rules on both fields one allowed and one disallowed
|
||||
self.field_access.write(field_access2, {
|
||||
'perm_read': False,
|
||||
})
|
||||
self.test_access.read(test_id, ['field1'])
|
||||
self.failUnlessRaises(Exception, self.test_access.read, test_id,
|
||||
['field2'])
|
||||
self.failUnlessRaises(Exception, self.test_access.read, test_id)
|
||||
test_access = self.test_access.browse(test_id)
|
||||
test_access.field1
|
||||
self.failUnlessRaises(Exception, getattr, test_access, 'field2')
|
||||
transaction.cursor.cache.clear()
|
||||
|
||||
# Two access rules on both fields disallowed
|
||||
self.field_access.write(field_access1, {
|
||||
'perm_read': False,
|
||||
})
|
||||
self.failUnlessRaises(Exception, self.test_access.read, test_id,
|
||||
['field1'])
|
||||
self.failUnlessRaises(Exception, self.test_access.read, test_id,
|
||||
['field2'])
|
||||
self.failUnlessRaises(Exception, self.test_access.read, test_id)
|
||||
test_access = self.test_access.browse(test_id)
|
||||
self.failUnlessRaises(Exception, getattr, test_access, 'field1')
|
||||
self.failUnlessRaises(Exception, getattr, test_access, 'field2')
|
||||
transaction.cursor.cache.clear()
|
||||
|
||||
transaction.cursor.rollback()
|
||||
|
||||
def test0010perm_write(self):
|
||||
'''
|
||||
Test Write Access
|
||||
'''
|
||||
with Transaction().start(DB_NAME, USER, CONTEXT) as transaction:
|
||||
field1_id, = self.field.search([
|
||||
('model.model', '=', 'test.access'),
|
||||
('name', '=', 'field1'),
|
||||
])
|
||||
field2_id, = self.field.search([
|
||||
('model.model', '=', 'test.access'),
|
||||
('name', '=', 'field2'),
|
||||
])
|
||||
|
||||
test_id = self.test_access.create({
|
||||
'field1': 'ham',
|
||||
'field2': 'spam',
|
||||
})
|
||||
|
||||
# Without field access
|
||||
self.test_access.write(test_id, {})
|
||||
self.test_access.write(test_id, {'field1': 'ham'})
|
||||
self.test_access.write(test_id, {'field2': 'spam'})
|
||||
|
||||
# With field access
|
||||
|
||||
# One access allowed for any group
|
||||
field_access_wo_group_id = self.field_access.create({
|
||||
'field': field1_id,
|
||||
'group': False,
|
||||
'perm_write': True,
|
||||
})
|
||||
self.test_access.write(test_id, {})
|
||||
self.test_access.write(test_id, {'field1': 'ham'})
|
||||
self.test_access.write(test_id, {'field2': 'spam'})
|
||||
self.test_access.write(test_id, {
|
||||
'field1': 'ham',
|
||||
'field2': 'spam',
|
||||
})
|
||||
|
||||
# One access disallowed for any group
|
||||
self.field_access.write(field_access_wo_group_id, {
|
||||
'perm_write': False,
|
||||
})
|
||||
|
||||
self.test_access.write(test_id, {})
|
||||
self.failUnlessRaises(Exception, self.test_access.write, test_id,
|
||||
{'field1': 'ham'})
|
||||
self.test_access.write(test_id, {'field2': 'spam'})
|
||||
self.failUnlessRaises(Exception, self.test_access.write, test_id, {
|
||||
'field1': 'ham',
|
||||
'field2': 'spam',
|
||||
})
|
||||
|
||||
# Two access rules with one group allowed
|
||||
group_id = self.group.search([('users', '=', USER)])[0]
|
||||
field_access_w_group_id = self.field_access.create({
|
||||
'field': field1_id,
|
||||
'group': group_id,
|
||||
'perm_write': True,
|
||||
})
|
||||
|
||||
self.test_access.write(test_id, {})
|
||||
self.test_access.write(test_id, {'field1': 'ham'})
|
||||
self.test_access.write(test_id, {'field2': 'spam'})
|
||||
self.test_access.write(test_id, {
|
||||
'field1': 'ham',
|
||||
'field2': 'spam',
|
||||
})
|
||||
|
||||
# Two access rules with both allowed
|
||||
self.field_access.write(field_access_wo_group_id, {
|
||||
'perm_write': True,
|
||||
})
|
||||
self.test_access.write(test_id, {})
|
||||
self.test_access.write(test_id, {'field1': 'ham'})
|
||||
self.test_access.write(test_id, {'field2': 'spam'})
|
||||
self.test_access.write(test_id, {
|
||||
'field1': 'ham',
|
||||
'field2': 'spam',
|
||||
})
|
||||
|
||||
# Two access rules with any group allowed
|
||||
self.field_access.write(field_access_w_group_id, {
|
||||
'perm_write': False,
|
||||
})
|
||||
self.test_access.write(test_id, {})
|
||||
self.test_access.write(test_id, {'field1': 'ham'})
|
||||
self.test_access.write(test_id, {'field2': 'spam'})
|
||||
self.test_access.write(test_id, {
|
||||
'field1': 'ham',
|
||||
'field2': 'spam',
|
||||
})
|
||||
|
||||
# Two access rules with both disallowed
|
||||
self.field_access.write(field_access_wo_group_id, {
|
||||
'perm_write': False,
|
||||
})
|
||||
self.test_access.write(test_id, {})
|
||||
self.failUnlessRaises(Exception, self.test_access.write, test_id,
|
||||
{'field1': 'ham'})
|
||||
self.test_access.write(test_id, {'field2': 'spam'})
|
||||
self.failUnlessRaises(Exception, self.test_access.write, test_id, {
|
||||
'field1': 'ham',
|
||||
'field2': 'spam',
|
||||
})
|
||||
|
||||
# One access disallowed for one group
|
||||
self.field_access.delete(field_access_wo_group_id)
|
||||
self.test_access.write(test_id, {})
|
||||
self.failUnlessRaises(Exception, self.test_access.write, test_id,
|
||||
{'field1': 'ham'})
|
||||
self.test_access.write(test_id, {'field2': 'ham'})
|
||||
self.failUnlessRaises(Exception, self.test_access.write, test_id, {
|
||||
'field1': 'ham',
|
||||
'field2': 'spam',
|
||||
})
|
||||
|
||||
# One access allowed for one group
|
||||
self.field_access.write(field_access_w_group_id, {
|
||||
'perm_write': True,
|
||||
})
|
||||
self.test_access.write(test_id, {})
|
||||
self.test_access.write(test_id, {'field1': 'ham'})
|
||||
self.test_access.write(test_id, {'field2': 'spam'})
|
||||
self.test_access.write(test_id, {
|
||||
'field1': 'ham',
|
||||
'field2': 'spam',
|
||||
})
|
||||
|
||||
# One access allowed for one other group
|
||||
group_id = self.group.create({'name': 'Test'})
|
||||
self.field_access.write(field_access_w_group_id, {
|
||||
'group': group_id,
|
||||
})
|
||||
self.test_access.write(test_id, {})
|
||||
self.test_access.write(test_id, {'field1': 'ham'})
|
||||
self.test_access.write(test_id, {'field2': 'spam'})
|
||||
self.test_access.write(test_id, {
|
||||
'field1': 'ham',
|
||||
'field2': 'spam',
|
||||
})
|
||||
|
||||
# One access disallowed for one other group
|
||||
self.field_access.write(field_access_w_group_id, {
|
||||
'perm_write': False,
|
||||
})
|
||||
self.test_access.write(test_id, {})
|
||||
self.test_access.write(test_id, {'field1': 'ham'})
|
||||
self.test_access.write(test_id, {'field2': 'spam'})
|
||||
self.test_access.write(test_id, {
|
||||
'field1': 'ham',
|
||||
'field2': 'spam',
|
||||
})
|
||||
|
||||
# Two access rules on both fields allowed
|
||||
self.field_access.delete(field_access_w_group_id)
|
||||
|
||||
field_access1 = self.field_access.create({
|
||||
'field': field1_id,
|
||||
'group': False,
|
||||
'perm_write': True,
|
||||
})
|
||||
field_access2 = self.field_access.create({
|
||||
'field': field2_id,
|
||||
'group': False,
|
||||
'perm_write': True,
|
||||
})
|
||||
|
||||
self.test_access.write(test_id, {})
|
||||
self.test_access.write(test_id, {'field1': 'ham'})
|
||||
self.test_access.write(test_id, {'field2': 'spam'})
|
||||
self.test_access.write(test_id, {
|
||||
'field1': 'ham',
|
||||
'field2': 'spam',
|
||||
})
|
||||
|
||||
# Two access rules on both fields one allowed and one disallowed
|
||||
self.field_access.write(field_access2, {
|
||||
'perm_write': False,
|
||||
})
|
||||
self.test_access.write(test_id, {})
|
||||
self.test_access.write(test_id, {'field1': 'ham'})
|
||||
self.failUnlessRaises(Exception, self.test_access.write, test_id,
|
||||
{'field2': 'spam'})
|
||||
self.failUnlessRaises(Exception, self.test_access.write, test_id, {
|
||||
'field1': 'ham',
|
||||
'field2': 'spam',
|
||||
})
|
||||
|
||||
# Two access rules on both fields disallowed
|
||||
self.field_access.write(field_access1, {
|
||||
'perm_write': False,
|
||||
})
|
||||
self.test_access.write(test_id, {})
|
||||
self.failUnlessRaises(Exception, self.test_access.write, test_id,
|
||||
{'field1': 'ham'})
|
||||
self.failUnlessRaises(Exception, self.test_access.write, test_id,
|
||||
{'field2': 'spam'})
|
||||
self.failUnlessRaises(Exception, self.test_access.write, test_id, {
|
||||
'field1': 'ham',
|
||||
'field2': 'spam',
|
||||
})
|
||||
|
||||
transaction.cursor.rollback()
|
||||
|
||||
def suite():
|
||||
suite_ = unittest.TestSuite()
|
||||
suite_.addTests(unittest.TestLoader(
|
||||
).loadTestsFromTestCase(ModelAccessTestCase))
|
||||
suite_.addTests(unittest.TestLoader(
|
||||
).loadTestsFromTestCase(ModelFieldAccessTestCase))
|
||||
return suite_
|
||||
|
||||
if __name__ == '__main__':
|
||||
suite = suite()
|
||||
unittest.TextTestRunner(verbosity=2).run(suite)
|
||||
@@ -194,6 +194,8 @@ def all_suite():
|
||||
suite_.addTests(test_protocols_datatype.suite())
|
||||
import trytond.tests.test_sequence as test_sequence
|
||||
suite_.addTests(test_sequence.suite())
|
||||
import trytond.tests.test_access as test_access
|
||||
suite_.addTests(test_access.suite())
|
||||
return suite_
|
||||
|
||||
def modules_suite():
|
||||
|
||||
Reference in New Issue
Block a user