Add user application
issue5782 review28641002
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
* Add user application
|
||||
* Add sequence_ordered
|
||||
* Remove most country specific code in languages
|
||||
* Add support for derivative translations
|
||||
|
||||
@@ -27,5 +27,6 @@ Introduction to all the key parts of trytond:
|
||||
wizard
|
||||
reports/index
|
||||
rpc
|
||||
user_application
|
||||
modules/index
|
||||
translation
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
.. _topics-user_application:
|
||||
|
||||
================
|
||||
User Application
|
||||
================
|
||||
|
||||
Tryton provides a way to connect URL rules to an callable endpoint using the
|
||||
decorator method `route` of the `trytond.application.app` instance. This allows
|
||||
to define a custom API based on HTTP that can be used to create a specific user
|
||||
application.
|
||||
|
||||
The decorator takes as first parameter a string which follow the `Rule
|
||||
Format`_ of Werkzeug and as second parameter sequence of HTTP methods.
|
||||
|
||||
Example::
|
||||
|
||||
from trytond.application import app
|
||||
|
||||
@app.route('/hello', methods=['GET'])
|
||||
def hello(request):
|
||||
return 'Hello world'
|
||||
|
||||
.. _Rule Format: http://werkzeug.pocoo.org/docs/latest/routing/#rule-format
|
||||
|
||||
Tryton also provides some wrappers in `trytond.procotols.wrappers` to ease the
|
||||
creation of such route.
|
||||
|
||||
- `with_pool`: which takes the first parameter as database name and
|
||||
replace it by the corresponding instance of the :ref:`Pool <ref-pool>`.
|
||||
|
||||
- `with_transaction([readonly])`: which starts a :class:`Transaction` using
|
||||
the :ref:`Pool <ref-pool>` from `with_pool`. If `readonly` is not set,
|
||||
the transaction will not be readonly for `POST`, `PUT`, `DELETE` and
|
||||
`PATCH` methods and readonly for all others.
|
||||
|
||||
- `user_application(name[, json])`: which set the :attr:`Transaction.user`
|
||||
from the `Authorization` header using the type `bearer` and a valid key
|
||||
for the named user application.
|
||||
|
||||
User Application Key
|
||||
====================
|
||||
|
||||
Tryton also provides a easy way to manage access to user application using
|
||||
keys per named application.
|
||||
A key is created with a `POST` request on the `URL`
|
||||
`/<database_name>/user/application/` which returns the key. The request must
|
||||
contain as data a json object with the keys:
|
||||
|
||||
- `user`: the user login
|
||||
|
||||
- `application`: the name of the application
|
||||
|
||||
After the creation, the key must be validated by the user from the preferences
|
||||
of a Tryton client.
|
||||
|
||||
A key can be deleted with a `DELETE` request on the same `URL`. The request
|
||||
must contain as data a json object with the keys:
|
||||
|
||||
- `user`: the user login
|
||||
|
||||
- `key`: the key to delete
|
||||
|
||||
- `application`: the name of the application of the key
|
||||
@@ -3,13 +3,11 @@
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import logging
|
||||
import pydoc
|
||||
from functools import wraps
|
||||
|
||||
from werkzeug.utils import redirect
|
||||
from werkzeug.exceptions import abort
|
||||
from sql import Table
|
||||
|
||||
from trytond.pool import Pool
|
||||
from trytond import security
|
||||
from trytond import backend
|
||||
from trytond.config import config
|
||||
@@ -20,6 +18,7 @@ from trytond.exceptions import (
|
||||
UserError, UserWarning, ConcurrencyException, LoginException)
|
||||
from trytond.tools import is_instance_method
|
||||
from trytond.wsgi import app
|
||||
from .wrappers import with_pool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -29,19 +28,6 @@ ir_module = Table('ir_module')
|
||||
res_user = Table('res_user')
|
||||
|
||||
|
||||
def with_pool(func):
|
||||
@wraps(func)
|
||||
def wrapper(request, database_name, *args, **kwargs):
|
||||
database_list = Pool.database_list()
|
||||
pool = Pool(database_name)
|
||||
if database_name not in database_list:
|
||||
with Transaction().start(
|
||||
database_name, request.user_id, readonly=True):
|
||||
pool.init()
|
||||
return func(request, pool, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
@app.route('/<string:database_name>/', methods=['POST'])
|
||||
def rpc(request, database_name):
|
||||
methods = {
|
||||
@@ -51,8 +37,8 @@ def rpc(request, database_name):
|
||||
'system.methodHelp': help_method,
|
||||
'system.methodSignature': lambda *a: 'signatures not supported',
|
||||
}
|
||||
return methods.get(request.method, _dispatch)(
|
||||
request, database_name, *request.params)
|
||||
return methods.get(request.rpc_method, _dispatch)(
|
||||
request, database_name, *request.rpc_params)
|
||||
|
||||
|
||||
def login(request, database_name, user, parameters, language=None):
|
||||
@@ -92,7 +78,7 @@ def root(request, *args):
|
||||
'common.server.version': lambda *a: __version__,
|
||||
'common.db.list': db_list,
|
||||
}
|
||||
return methods[request.method](request, *request.params)
|
||||
return methods[request.rpc_method](request, *request.rpc_params)
|
||||
|
||||
|
||||
@app.route('/', methods=['GET'])
|
||||
@@ -128,7 +114,7 @@ def list_method(request, pool):
|
||||
|
||||
|
||||
def get_object_method(request, pool):
|
||||
method = request.method
|
||||
method = request.rpc_method
|
||||
type, _ = method.split('.', 1)
|
||||
name = '.'.join(method.split('.')[1:-1])
|
||||
method = method.split('.')[-1]
|
||||
|
||||
@@ -10,7 +10,7 @@ import base64
|
||||
|
||||
from werkzeug.wrappers import Response
|
||||
from werkzeug.utils import cached_property
|
||||
from werkzeug.exceptions import BadRequest
|
||||
from werkzeug.exceptions import BadRequest, InternalServerError
|
||||
|
||||
from trytond.protocols.wrappers import Request
|
||||
from trytond.exceptions import TrytonException
|
||||
@@ -129,11 +129,11 @@ class JSONRequest(Request):
|
||||
raise BadRequest('Not a JSON request')
|
||||
|
||||
@cached_property
|
||||
def method(self):
|
||||
def rpc_method(self):
|
||||
return self.parsed_data['method']
|
||||
|
||||
@cached_property
|
||||
def params(self):
|
||||
def rpc_params(self):
|
||||
return self.parsed_data['params']
|
||||
|
||||
|
||||
@@ -146,16 +146,23 @@ class JSONProtocol:
|
||||
|
||||
@classmethod
|
||||
def response(cls, data, request):
|
||||
if isinstance(request, JSONRequest):
|
||||
response = {'id': request.parsed_data.get('id', 0)}
|
||||
try:
|
||||
parsed_data = request.parsed_data
|
||||
except BadRequest:
|
||||
parsed_data = {}
|
||||
if (isinstance(request, JSONRequest)
|
||||
and set(parsed_data.keys()) == {'id', 'method', 'params'}):
|
||||
response = {'id': parsed_data.get('id', 0)}
|
||||
if isinstance(data, TrytonException):
|
||||
response['error'] = data.args
|
||||
elif isinstance(data, Exception):
|
||||
# report exception back to server
|
||||
response['error'] = (str(data), data.__format_traceback__)
|
||||
else:
|
||||
response['result'] = data
|
||||
else:
|
||||
response = {}
|
||||
if isinstance(data, TrytonException):
|
||||
response['error'] = data.args
|
||||
elif isinstance(data, Exception):
|
||||
# report exception back to server
|
||||
response['error'] = (str(data), data.__format_traceback__)
|
||||
else:
|
||||
response['result'] = data
|
||||
if isinstance(data, Exception):
|
||||
return InternalServerError()
|
||||
response = data
|
||||
return Response(json.dumps(response, cls=JSONEncoder),
|
||||
content_type='application/json')
|
||||
|
||||
@@ -2,15 +2,23 @@
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import base64
|
||||
import gzip
|
||||
import logging
|
||||
from io import BytesIO
|
||||
from functools import wraps
|
||||
|
||||
from werkzeug.wrappers import Request as _Request
|
||||
from werkzeug.wrappers import Request as _Request, Response
|
||||
from werkzeug.utils import cached_property
|
||||
from werkzeug.http import wsgi_to_bytes, bytes_to_wsgi
|
||||
from werkzeug.datastructures import Authorization
|
||||
from werkzeug.exceptions import abort
|
||||
from werkzeug.exceptions import abort, HTTPException
|
||||
|
||||
from trytond import security
|
||||
from trytond import security, backend
|
||||
from trytond.pool import Pool
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.config import config
|
||||
from trytond.cache import Cache
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Request(_Request):
|
||||
@@ -30,11 +38,11 @@ class Request(_Request):
|
||||
return self.data
|
||||
|
||||
@property
|
||||
def method(self):
|
||||
def rpc_method(self):
|
||||
return
|
||||
|
||||
@property
|
||||
def params(self):
|
||||
def rpc_params(self):
|
||||
return
|
||||
|
||||
@cached_property
|
||||
@@ -85,3 +93,91 @@ def parse_authorization_header(value):
|
||||
'userid': userid,
|
||||
'session': bytes_to_wsgi(session),
|
||||
})
|
||||
|
||||
|
||||
def with_pool(func):
|
||||
@wraps(func)
|
||||
def wrapper(request, database_name, *args, **kwargs):
|
||||
database_list = Pool.database_list()
|
||||
pool = Pool(database_name)
|
||||
if database_name not in database_list:
|
||||
with Transaction().start(
|
||||
database_name, request.user_id, readonly=True):
|
||||
pool.init()
|
||||
return func(request, pool, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def with_transaction(readonly=None):
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(request, pool, *args, **kwargs):
|
||||
DatabaseOperationalError = backend.get('DatabaseOperationalError')
|
||||
readonly_ = readonly # can not modify non local
|
||||
if readonly_ is None:
|
||||
if request.method in {'POST', 'PUT', 'DELETE', 'PATCH'}:
|
||||
readonly_ = False
|
||||
else:
|
||||
readonly_ = True
|
||||
for count in range(config.getint('database', 'retry'), -1, -1):
|
||||
with Transaction().start(
|
||||
pool.database_name, 0, readonly=readonly_
|
||||
) as transaction:
|
||||
try:
|
||||
Cache.clean(pool.database_name)
|
||||
result = func(request, pool, *args, **kwargs)
|
||||
return result
|
||||
except DatabaseOperationalError:
|
||||
if count and not readonly_:
|
||||
transaction.rollback()
|
||||
continue
|
||||
logger.error('%s', request, exc_info=True)
|
||||
raise
|
||||
except Exception:
|
||||
logger.error('%s', request, exc_info=True)
|
||||
raise
|
||||
# Need to commit to unlock SQLite database
|
||||
transaction.commit()
|
||||
Cache.resets(pool.database_name)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def user_application(name, json=True):
|
||||
from .jsonrpc import JSONEncoder, json as json_
|
||||
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(request, *args, **kwargs):
|
||||
pool = Pool()
|
||||
UserApplication = pool.get('res.user.application')
|
||||
|
||||
authorization = request.headers['Authorization']
|
||||
try:
|
||||
auth_type, auth_info = authorization.split(None, 1)
|
||||
auth_type = auth_type.lower()
|
||||
except ValueError:
|
||||
abort(401)
|
||||
if auth_type != b'bearer':
|
||||
abort(403)
|
||||
|
||||
application = UserApplication.check(auth_info, name)
|
||||
if not application:
|
||||
abort(403)
|
||||
transaction = Transaction()
|
||||
# TODO language
|
||||
with transaction.set_user(application.user.id), \
|
||||
transaction.set_context(_check_access=True):
|
||||
try:
|
||||
response = func(request, *args, **kwargs)
|
||||
except Exception, e:
|
||||
if isinstance(e, HTTPException):
|
||||
raise
|
||||
logger.error('%s', request, exc_info=True)
|
||||
abort(500)
|
||||
if not isinstance(response, Response) and json:
|
||||
response = Response(json_.dumps(response, cls=JSONEncoder),
|
||||
content_type='application/json')
|
||||
return response
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
@@ -150,11 +150,11 @@ class XMLRequest(Request):
|
||||
raise BadRequest('Not an XML request')
|
||||
|
||||
@property
|
||||
def method(self):
|
||||
def rpc_method(self):
|
||||
return self.parsed_data[1]
|
||||
|
||||
@property
|
||||
def params(self):
|
||||
def rpc_params(self):
|
||||
return self.parsed_data[0]
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
# 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 ..pool import Pool
|
||||
|
||||
from .group import *
|
||||
from .user import *
|
||||
from .ir import *
|
||||
from . import routes
|
||||
|
||||
__all__ = ['register', 'routes']
|
||||
|
||||
|
||||
def register():
|
||||
@@ -15,6 +19,7 @@ def register():
|
||||
UserAction,
|
||||
UserGroup,
|
||||
Warning_,
|
||||
UserApplication,
|
||||
UserConfigStart,
|
||||
UIMenuGroup,
|
||||
ActionGroup,
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import logging
|
||||
import time
|
||||
|
||||
from werkzeug.exceptions import abort
|
||||
|
||||
from trytond.wsgi import app
|
||||
from trytond.protocols.wrappers import with_pool, with_transaction
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@app.route('/<database_name>/user/application/', methods=['POST', 'DELETE'])
|
||||
@with_pool
|
||||
@with_transaction(readonly=False)
|
||||
def user_application(request, pool):
|
||||
User = pool.get('res.user')
|
||||
UserApplication = pool.get('res.user.application')
|
||||
LoginAttempt = pool.get('res.user.login.attempt')
|
||||
data = request.parsed_data
|
||||
login = data.get('user')
|
||||
|
||||
if request.method == 'POST':
|
||||
users = User.search([
|
||||
('login', '=', login),
|
||||
])
|
||||
if not users:
|
||||
logger.info('User Application not found: %s', data.get('user'))
|
||||
return abort(404)
|
||||
user, = users
|
||||
data['user'] = user.id
|
||||
data.pop('key', None)
|
||||
data.pop('state', None)
|
||||
application, = UserApplication.create([data])
|
||||
time.sleep(2 ** UserApplication.count(application))
|
||||
return application.key
|
||||
elif request.method == 'DELETE':
|
||||
time.sleep(2 ** LoginAttempt.count(login) - 1)
|
||||
applications = UserApplication.search([
|
||||
('user.login', '=', login),
|
||||
('key', '=', data.get('key')),
|
||||
('application', '=', data.get('application')),
|
||||
])
|
||||
if applications:
|
||||
UserApplication.delete(applications)
|
||||
LoginAttempt.remove(login)
|
||||
else:
|
||||
LoginAttempt.add(login)
|
||||
+100
-2
@@ -8,6 +8,7 @@ import hashlib
|
||||
import time
|
||||
import datetime
|
||||
import logging
|
||||
import uuid
|
||||
from functools import wraps
|
||||
from itertools import groupby, ifilter
|
||||
from operator import attrgetter
|
||||
@@ -23,7 +24,7 @@ try:
|
||||
except ImportError:
|
||||
bcrypt = None
|
||||
|
||||
from ..model import ModelView, ModelSQL, fields, Unique
|
||||
from ..model import ModelView, ModelSQL, Workflow, fields, Unique
|
||||
from ..wizard import Wizard, StateView, Button, StateTransition
|
||||
from ..tools import grouped_slice
|
||||
from .. import backend
|
||||
@@ -31,12 +32,13 @@ from ..transaction import Transaction
|
||||
from ..cache import Cache
|
||||
from ..pool import Pool
|
||||
from ..config import config
|
||||
from ..pyson import PYSONEncoder
|
||||
from ..pyson import PYSONEncoder, Eval
|
||||
from ..rpc import RPC
|
||||
from ..exceptions import LoginException
|
||||
|
||||
__all__ = [
|
||||
'User', 'LoginAttempt', 'UserAction', 'UserGroup', 'Warning_',
|
||||
'UserApplication',
|
||||
'UserConfigStart', 'UserConfig',
|
||||
]
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -62,6 +64,8 @@ class User(ModelSQL, ModelView):
|
||||
rule_groups = fields.Many2Many('ir.rule.group-res.user',
|
||||
'user', 'rule_group', 'Rules',
|
||||
domain=[('global_p', '!=', True), ('default_p', '!=', True)])
|
||||
applications = fields.One2Many(
|
||||
'res.user.application', 'user', "Applications")
|
||||
language = fields.Many2One('ir.lang', 'Language',
|
||||
domain=['OR',
|
||||
('translatable', '=', True),
|
||||
@@ -100,6 +104,7 @@ class User(ModelSQL, ModelView):
|
||||
'actions',
|
||||
'status_bar',
|
||||
'warnings',
|
||||
'applications',
|
||||
]
|
||||
cls._context_fields = [
|
||||
'language',
|
||||
@@ -698,6 +703,99 @@ class Warning_(ModelSQL, ModelView):
|
||||
return False
|
||||
|
||||
|
||||
class UserApplication(Workflow, ModelSQL, ModelView):
|
||||
"User Application"
|
||||
__name__ = 'res.user.application'
|
||||
_rec_name = 'key'
|
||||
|
||||
key = fields.Char("Key", required=True, select=True)
|
||||
user = fields.Many2One('res.user', "User", required=True, select=True)
|
||||
application = fields.Selection([], "Application")
|
||||
state = fields.Selection([
|
||||
('requested', "Requested"),
|
||||
('validated', "Validated"),
|
||||
('cancelled', "Cancelled"),
|
||||
], "State", readonly=True)
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super(UserApplication, cls).__setup__()
|
||||
cls._transitions |= set((
|
||||
('requested', 'validated'),
|
||||
('requested', 'cancelled'),
|
||||
('validated', 'cancelled'),
|
||||
))
|
||||
cls._buttons.update({
|
||||
'validate_': {
|
||||
'invisible': Eval('state') != 'requested',
|
||||
},
|
||||
'cancel': {
|
||||
'invisible': Eval('state') == 'cancelled',
|
||||
},
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def default_key(cls):
|
||||
return ''.join(uuid.uuid4().hex for _ in xrange(4))
|
||||
|
||||
@classmethod
|
||||
def default_state(cls):
|
||||
return 'requested'
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('validated')
|
||||
def validate_(cls, applications):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('cancelled')
|
||||
def cancel(cls, applications):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def count(cls, application):
|
||||
return cls.search([
|
||||
('user', '=', application.user.id),
|
||||
('state', '=', 'requested'),
|
||||
], count=True)
|
||||
|
||||
@classmethod
|
||||
def check(cls, key, application):
|
||||
records = cls.search([
|
||||
('key', '=', key),
|
||||
('application', '=', application),
|
||||
('state', '=', 'validated'),
|
||||
], limit=1)
|
||||
if not records:
|
||||
return
|
||||
record, = records
|
||||
return record
|
||||
|
||||
@classmethod
|
||||
def create(cls, vlist):
|
||||
pool = Pool()
|
||||
User = pool.get('res.user')
|
||||
applications = super(UserApplication, cls).create(vlist)
|
||||
User._get_preferences_cache.clear()
|
||||
return applications
|
||||
|
||||
@classmethod
|
||||
def write(cls, *args):
|
||||
pool = Pool()
|
||||
User = pool.get('res.user')
|
||||
super(UserApplication, cls).write(*args)
|
||||
User._get_preferences_cache.clear()
|
||||
|
||||
@classmethod
|
||||
def delete(cls, applications):
|
||||
pool = Pool()
|
||||
User = pool.get('res.user')
|
||||
super(UserApplication, cls).delete(applications)
|
||||
User._get_preferences_cache.clear()
|
||||
|
||||
|
||||
class UserConfigStart(ModelView):
|
||||
'User Config Init'
|
||||
__name__ = 'res.user.config.start'
|
||||
|
||||
@@ -106,5 +106,46 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="rule_group" ref="rule_group_user_warning"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="user_application_view_form">
|
||||
<field name="model">res.user.application</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">user_application_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="user_application_view_list">
|
||||
<field name="model">res.user.application</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">user_application_list</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule.group" id="rule_group_user_application">
|
||||
<field name="model"
|
||||
search="[('model', '=', 'res.user.application')]"/>
|
||||
<field name="global_p" eval="False"/>
|
||||
<field name="default_p" eval="True"/>
|
||||
</record>
|
||||
<record model="ir.rule" id="rule_user_application1">
|
||||
<field name="domain"
|
||||
eval="[('user', '=', Eval('user', {}).get('id', -1))]"
|
||||
pyson="1"/>
|
||||
<field name="rule_group" ref="rule_group_user_application"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule.group" id="rule_group_user_application_admin">
|
||||
<field name="model"
|
||||
search="[('model', '=', 'res.user.application')]"/>
|
||||
<field name="global_p" eval="False"/>
|
||||
<field name="default_p" eval="False"/>
|
||||
</record>
|
||||
<record model="ir.rule" id="rule_user_application_admin1">
|
||||
<field name="domain" eval="[]" pyson="1"/>
|
||||
<field name="rule_group" ref="rule_group_user_application_admin"/>
|
||||
</record>
|
||||
<record model="ir.rule.group-res.group"
|
||||
id="rule_user_application_admin_admin">
|
||||
<field name="rule_group" ref="rule_group_user_application_admin"/>
|
||||
<field name="group" ref="group_admin"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</tryton>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form>
|
||||
<label name="user"/>
|
||||
<field name="user"/>
|
||||
<newline/>
|
||||
<label name="key"/>
|
||||
<field name="key"/>
|
||||
<label name="application"/>
|
||||
<field name="application"/>
|
||||
<label name="state"/>
|
||||
<field name="state"/>
|
||||
<group id="buttons" colspan="2" col="2">
|
||||
<button name="cancel" string="Cancel" icon="tryton-cancel"/>
|
||||
<button name="validate_" string="Validate" icon="tryton-ok"/>
|
||||
</group>
|
||||
</form>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tree>
|
||||
<field name="user" expand="1"/>
|
||||
<field name="key" expand="1"/>
|
||||
<field name="application" expand="1"/>
|
||||
<field name="state"/>
|
||||
<button name="cancel" string="Cancel"/>
|
||||
<button name="validate_" string="Validate"/>
|
||||
</tree>
|
||||
@@ -26,6 +26,9 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="groups"/>
|
||||
<field name="rule_groups"/>
|
||||
</page>
|
||||
<page name="applications" col="1">
|
||||
<field name="applications"/>
|
||||
</page>
|
||||
<page string="Preferences" col="2" id="preferences">
|
||||
<label name="language"/>
|
||||
<field name="language" widget="selection"/>
|
||||
|
||||
@@ -21,6 +21,9 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<page string="Group Membership" col="1" id="membership">
|
||||
<field name="groups"/>
|
||||
</page>
|
||||
<page name="applications" col="1">
|
||||
<field name="applications"/>
|
||||
</page>
|
||||
<page string="Preferences" col="2" id="preferences">
|
||||
<label name="language"/>
|
||||
<field name="language" widget="selection"/>
|
||||
|
||||
+4
-2
@@ -6,7 +6,7 @@ import traceback
|
||||
|
||||
from werkzeug.wrappers import Response
|
||||
from werkzeug.routing import Map, Rule
|
||||
from werkzeug.exceptions import abort
|
||||
from werkzeug.exceptions import abort, HTTPException
|
||||
|
||||
import wrapt
|
||||
|
||||
@@ -45,6 +45,8 @@ class TrytondWSGI(object):
|
||||
try:
|
||||
endpoint, request.view_args = adapter.match()
|
||||
return endpoint(request, **request.view_args)
|
||||
except HTTPException, e:
|
||||
return e
|
||||
except Exception, e:
|
||||
tb_s = ''.join(traceback.format_exception(*sys.exc_info()))
|
||||
for path in sys.path:
|
||||
@@ -65,7 +67,7 @@ class TrytondWSGI(object):
|
||||
else:
|
||||
request = Request(environ)
|
||||
data = self.dispatch_request(request)
|
||||
if not isinstance(data, Response):
|
||||
if not isinstance(data, (Response, HTTPException)):
|
||||
for cls in self.protocols:
|
||||
for mimetype in request.accept_mimetypes:
|
||||
if cls.content_type in mimetype:
|
||||
|
||||
Reference in New Issue
Block a user