details:   https://code.tryton.org/tryton/commit/508d2755ddc5
branch:    default
user:      Cédric Krier <[email protected]>
date:      Tue Sep 08 09:58:42 2026 +0200
description:
        Add routes for sao custom in base route

        Closes #13927
diffstat:

 trytond/CHANGELOG            |   1 +
 trytond/trytond/ir/routes.py |  99 +++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 99 insertions(+), 1 deletions(-)

diffs (147 lines):

diff -r 027042dcaa8d -r 508d2755ddc5 trytond/CHANGELOG
--- a/trytond/CHANGELOG Thu Aug 06 14:53:51 2026 +0200
+++ b/trytond/CHANGELOG Tue Sep 08 09:58:42 2026 +0200
@@ -1,3 +1,4 @@
+* Add routes for sao custom in base router
 * Replace the administration group by a flag on the user
 * Make DBTestCase public
 * Add function to import XML files
diff -r 027042dcaa8d -r 508d2755ddc5 trytond/trytond/ir/routes.py
--- a/trytond/trytond/ir/routes.py      Thu Aug 06 14:53:51 2026 +0200
+++ b/trytond/trytond/ir/routes.py      Tue Sep 08 09:58:42 2026 +0200
@@ -2,8 +2,10 @@
 # this repository contains the full copyright notices and license terms.
 import csv
 import datetime as dt
+import hashlib
 import io
 import json
+import os.path
 from numbers import Number
 
 import trytond.config as config
@@ -12,7 +14,7 @@
 from trytond.protocols.wrappers import (
     HTTPStatus, Response, abort, redirect, with_pool, with_transaction)
 from trytond.routing import Route, Router, Rule
-from trytond.tools import slugify
+from trytond.tools import file_open, find_path, slugify
 from trytond.transaction import Transaction
 from trytond.wsgi import app
 
@@ -115,6 +117,49 @@
 </html>'''
 
 
+def etag_for_files(paths, chunk_size=1024 * 1024):
+    digest = hashlib.sha256()
+    content_length = 0
+    for path in paths:
+        content_length += os.path.getsize(find_path(path))
+        with file_open(path) as f:
+            while chunk := f.read(chunk_size):
+                digest.update(chunk)
+    return digest.hexdigest(), content_length
+
+
+class ChainedFiles:
+    def __init__(self, paths, chunk_size=64 * 1014):
+        self.paths = paths
+        self.chunk_size = chunk_size
+        self._files = []
+        self._closed = False
+
+    def __iter__(self):
+        try:
+            for path in self.paths:
+                f = file_open(path)
+                self._files.append(f)
+                try:
+                    while True:
+                        chunk = f.read(self.chunk_size)
+                        if not chunk:
+                            break
+                        yield chunk
+                finally:
+                    self._files.remove(f)
+                    f.close()
+        finally:
+            self.close()
+
+    def close(self):
+        if not self._closed:
+            self._closed = True
+            for f in self._files:
+                f.close()
+            self._files.clear()
+
+
 class Base(Router):
     __name__ = 'base'
 
@@ -122,6 +167,8 @@
     def __setup__(cls):
         super().__setup__()
         cls.__routes__.update({
+                'custom_js': Route(Rule('/custom.js', methods={'GET'})),
+                'custom_css': Route(Rule('/custom.css', methods={'GET'})),
                 'html_editor': Route(
                     Rule(
                         'html/<model>/<int:record>/<field>',
@@ -153,6 +200,56 @@
                 })
 
     @classmethod
+    def custom_js(cls, request):
+        cache_timeout = config.getint('web', 'cache_timeout')
+        paths = cls.custom_js_files()
+        etag, content_length = etag_for_files(paths)
+        if request.if_none_match and request.if_none_match.contains(etag):
+            response = Response(status=HTTPStatus.NOT_MODIFIED)
+        else:
+            response = Response(
+                ChainedFiles(paths),
+                HTTPStatus.OK,
+                content_type='text/javascript',
+                direct_passthrough=True)
+            response.content_length = content_length
+            response.cache_control.public = True
+            response.cache_control.max_age = cache_timeout
+        response.set_etag(etag)
+        return response
+
+    @classmethod
+    def custom_js_files(cls):
+        """Returns the list of file paths relative to modules
+        to include in custom.js"""
+        return []
+
+    @classmethod
+    def custom_css(cls, request):
+        cache_timeout = config.getint('web', 'cache_timeout')
+        paths = cls.custom_css_files()
+        etag, content_length = etag_for_files(paths)
+        if request.if_none_match and request.if_none_match.contains(etag):
+            response = Response(status=HTTPStatus.NOT_MODIFIED)
+        else:
+            response = Response(
+                ChainedFiles(paths),
+                HTTPStatus.OK,
+                content_type='text/css',
+                direct_passthrough=True)
+            response.content_length = content_length
+            response.cache_control.public = True
+            response.cache_control.max_age = cache_timeout
+        response.set_etag(etag)
+        return response
+
+    @classmethod
+    def custom_css_files(cls):
+        """Returns the list of file paths relative to modules
+        to include in custom.css"""
+        return []
+
+    @classmethod
     def html_editor(cls, request, pool, model, record, field):
         Field = pool.get('ir.model.field')
         field, = Field.search([

Reply via email to