Files
tradon/ir/routes.py
2025-12-26 13:11:43 +00:00

298 lines
10 KiB
Python
Executable File

# 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 csv
import datetime as dt
import io
import json
from numbers import Number
from trytond.config import config
from trytond.i18n import gettext
from trytond.protocols.jsonrpc import JSONDecoder
from trytond.protocols.wrappers import (
HTTPStatus, Response, abort, redirect, with_pool, with_transaction)
from trytond.tools import slugify
from trytond.transaction import Transaction
from trytond.wsgi import app
SOURCE = config.get(
'html', 'src', default='https://cloud.tinymce.com/stable/tinymce.min.js')
AVATAR_TIMEOUT = config.getint(
'web', 'avatar_timeout', default=7 * 24 * 60 * 60)
def get_token(record):
return str((record.write_date or record.create_date).timestamp())
def get_config(names, section='html', default=None):
names = names[:]
while names:
value = config.get(section, '-'.join(names))
if value is not None:
return value
names = names[:-1]
return default
@app.route('/<database_name>/ir/html/<model>/<int:record>/<field>',
methods={'GET', 'POST'})
@app.auth_required
@with_pool
@with_transaction(
user='request', context=dict(_check_access=True, fuzzy_translation=True))
def html_editor(request, pool, model, record, field):
Field = pool.get('ir.model.field')
field, = Field.search([
('name', '=', field),
('model.model', '=', model),
])
transaction = Transaction()
language = request.args.get('language', transaction.language)
with transaction.set_context(language=language):
Model = pool.get(model)
record = Model(record)
status = HTTPStatus.OK
error = ''
if request.method == 'POST':
setattr(record, field.name, request.form['text'])
if request.form['_csrf_token'] == get_token(record):
record.save()
return redirect(request.url)
else:
status = HTTPStatus.BAD_REQUEST
error = gettext('ir.msg_html_editor_save_fail')
csrf_token = get_token(record)
text = getattr(record, field.name) or ''
if isinstance(text, bytes):
try:
text = text.decode('utf-8')
except UnicodeDecodeError as e:
error = str(e).replace("'", "\\'")
text = ''
elif not isinstance(text, str):
abort(HTTPStatus.BAD_REQUEST)
title = '%(model)s "%(name)s" %(field)s - %(title)s' % {
'model': field.model_ref.name,
'name': record.rec_name,
'field': field.field_description,
'title': request.args.get('title', "Tryton"),
}
return Response(TEMPLATE % {
'source': SOURCE,
'plugins': get_config(
['plugins', model, field.name], default=''),
'css': get_config(
['css', model, field.name], default='[]'),
'class': get_config(
['class', model, field.name], default="''"),
'language': transaction.language,
'title': title,
'text': text,
'csrf_token': csrf_token,
'error': error,
}, status, content_type='text/html')
TEMPLATE = '''<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<script src="%(source)s"></script>
<script>
tinymce.init({
selector: '#text',
language: '%(language)s',
plugins: 'fullscreen autosave code %(plugins)s',
removed_menuitems: 'newdocument',
toolbar: 'save | undo redo | styleselect | bold italic | ' +
'alignleft aligncenter alignright alignjustify | ' +
'bullist numlist outdent indent | link image | close',
extended_valid_elements:
'py:if[test],' +
'py:choose[test],py:when[test],py:otherwise,' +
'py:for[each],' +
'py:def[function],' +
'py:match[path],' +
'py:with[vars],' +
'py:replace[value]',
custom_elements:
'py:if,' +
'py:choose,py:when,py:otherwise,' +
'py:for,' +
'py:def,' +
'py:match,' +
'py:with,' +
'py:replace',
content_css: %(css)s,
body_class: %(class)s,
setup: function(editor) {
editor.addMenuItem('save', {
text: 'Save',
icon: 'save',
context: 'file',
cmd: 'save',
});
editor.addButton('save', {
title: 'Save',
icon: 'save',
cmd: 'save',
});
editor.addShortcut('ctrl+s', 'save', 'save');
editor.addCommand('save', function() {
document.form.submit();
});
editor.addButton('close', {
title: 'Close',
icon: 'remove',
onclick: function() {
window.location =
window.location.protocol + '//_@' +
window.location.host +
window.location.pathname;
window.close();
},
});
},
init_instance_callback: function(editor) {
editor.execCommand('mceFullScreen');
var error = '%(error)s';
if (error) {
editor.notificationManager.open({
text: error,
type: 'error',
});
}
},
});
</script>
<title>%(title)s</title>
</head>
<body>
<form name="form" method="post" style="display: block; text-align: center">
<textarea id="text" name="text">%(text)s</textarea>
<input name="_csrf_token" type="hidden" value="%(csrf_token)s">
</form>
</body>
</html>'''
@app.route('/<database_name>/data/<model>', methods={'GET'})
@app.auth_required
@with_pool
@with_transaction(user='request', context=dict(_check_access=True))
def data(request, pool, model):
User = pool.get('res.user')
Lang = pool.get('ir.lang')
try:
Model = pool.get(model)
except KeyError:
abort(HTTPStatus.NOT_FOUND)
transaction = Transaction()
context = User(transaction.user).get_preferences(context_only=True)
language = request.args.get('l')
if language:
context['language'] = language
try:
domain = json.loads(
request.args.get('d', '[]'), object_hook=JSONDecoder())
except json.JSONDecodeError:
abort(HTTPStatus.BAD_REQUEST)
try:
ctx = json.loads(
request.args.get('c', '{}'), object_hook=JSONDecoder())
except json.JSONDecoder:
abort(HTTPStatus.BAD_REQUEST)
for key in list(ctx.keys()):
if key.startswith('_') and key != '_datetime':
del ctx[key]
context.update(ctx)
limit = None
offset = 0
if 's' in request.args:
try:
limit = int(request.args.get('s'))
if 'p' in request.args:
offset = int(request.args.get('p')) * limit
except ValueError:
abort(HTTPStatus.BAD_REQUEST)
if 'o' in request.args:
order = [(o.split(',', 1) + [''])[:2]
for o in request.args.getlist('o')]
else:
order = None
fields_names = request.args.getlist('f')
encoding = request.args.get('enc', 'UTF-8')
delimiter = request.args.get('dl', ',')
quotechar = request.args.get('qc', '"')
try:
header = bool(int(request.args.get('h', True)))
locale_format = bool(int(request.args.get('loc', False)))
except ValueError:
abort(HTTPStatus.BAD_REQUEST)
with transaction.set_context(**context):
lang = Lang.get(transaction.language)
def format_(row):
for i, value in enumerate(row):
if locale_format:
if isinstance(value, Number):
value = lang.format('%.12g', value)
elif isinstance(value, (dt.date, dt.datetime)):
value = lang.strftime(value)
elif isinstance(value, bool):
value = int(value)
row[i] = value
return row
try:
if domain and isinstance(domain[0], (int, float)):
rows = Model.export_data(
Model.browse(domain), fields_names, header)
else:
rows = Model.export_data_domain(
domain, fields_names,
limit=limit, offset=offset, order=order, header=header)
except (ValueError, KeyError):
abort(HTTPStatus.BAD_REQUEST)
data = io.StringIO(newline='')
writer = csv.writer(data, delimiter=delimiter, quotechar=quotechar)
for row in rows:
writer.writerow(format_(row))
data = data.getvalue().encode(encoding)
filename = slugify(Model.__names__()['model']) + '.csv'
filename = filename.encode('latin-1', 'ignore')
response = Response(data, mimetype='text/csv; charset=' + encoding)
response.headers.add(
'Content-Disposition', 'attachment', filename=filename)
response.headers.add('Content-Length', len(data))
return response
@app.route('/avatar/<base64:database_name>/<uuid>', methods={'GET'})
@with_pool
@with_transaction()
def avatar(request, pool, uuid):
Avatar = pool.get('ir.avatar')
try:
avatar, = Avatar.search([
('uuid', '=', uuid),
])
except ValueError:
abort(HTTPStatus.NOT_FOUND)
try:
size = int(request.args.get('s', 64))
except ValueError:
abort(HTTPStatus.BAD_REQUEST)
response = Response(avatar.get(size), mimetype='image/jpeg')
response.headers['Cache-Control'] = (
'max-age=%s, public' % AVATAR_TIMEOUT)
response.add_etag()
return response