diff --git a/rero_ils/config.py b/rero_ils/config.py
index 2a544598a0..829e7c2a94 100644
--- a/rero_ils/config.py
+++ b/rero_ils/config.py
@@ -275,9 +275,9 @@ def _(x):
THEME_SETTINGS_TEMPLATE = SETTINGS_TEMPLATE
#: Template for error pages.
THEME_ERROR_TEMPLATE = "rero_ils/page_error.html"
-# External CSS for each organisation customization
-# For production: change "test" with "prod" on url
-RERO_ILS_THEME_ORGANISATION_CSS_ENDPOINT = "https://resources.rero.ch/bib/test/css/"
+#: Local path for organisation-specific CSS customization files.
+#: CSS files are served from static assets (e.g., aoste.css, global.css).
+RERO_ILS_THEME_ORGANISATION_CSS_ENDPOINT = "/static/themes/css/"
#: Template for including a tracking code for web analytics.
THEME_TRACKINGCODE_TEMPLATE = "rero_ils/trackingcode.html"
THEME_JAVASCRIPT_TEMPLATE = "rero_ils/javascript.html"
diff --git a/rero_ils/theme/static/themes/css/aoste.css b/rero_ils/theme/static/themes/css/aoste.css
new file mode 100644
index 0000000000..150932407d
--- /dev/null
+++ b/rero_ils/theme/static/themes/css/aoste.css
@@ -0,0 +1,37 @@
+@layer bootstrap {
+ body#view-aoste nav.rero-ils-header {
+ background-color: #ffd7d7 !important;
+ }
+
+ body#view-aoste nav.rero-ils-header img.rero-ils-logo {
+ display: none;
+ }
+
+ div#aoste-logo {
+ content: url('https://resources.rero.ch/ils/test/images/logo-aoste.svg');
+ max-height: 44px;
+ }
+
+ #view-global .rero-slogan {
+ margin-bottom: 4em;
+ }
+
+
+ @media (max-width: 576px) {
+ .h1, h1 {
+ font-size: 1.5rem;
+ }
+
+ .h2, h2 {
+ font-size: 1.2rem;
+ }
+
+ #view-global .rero-ils-frontpage {
+ height: 20vh;
+ }
+
+ #view-global .rero-slogan {
+ margin-bottom: 1em;
+ }
+ }
+}
\ No newline at end of file
diff --git a/rero_ils/theme/static/themes/css/fictive.css b/rero_ils/theme/static/themes/css/fictive.css
new file mode 100644
index 0000000000..1a1a9e7d7a
--- /dev/null
+++ b/rero_ils/theme/static/themes/css/fictive.css
@@ -0,0 +1,37 @@
+@layer bootstrap {
+ body#view-fictive nav.rero-ils-header {
+ background-color: #4f009e !important;
+ }
+
+ body#view-fictive nav.rero-ils-header img.rero-ils-logo {
+ display: none ;
+ }
+
+ div#fictive-logo {
+ content: url('https://resources.rero.ch/ils/test/images/logo-fictive.svg');
+ max-height: 44px;
+ }
+
+ #view-global .rero-slogan {
+ margin-bottom: 4em ;
+ }
+
+
+ @media (max-width: 576px) {
+ .h1, h1 {
+ font-size: 1.5rem ;
+ }
+
+ .h2, h2 {
+ font-size: 1.2rem ;
+ }
+
+ #view-global .rero-ils-frontpage {
+ height: 20vh ;
+ }
+
+ #view-global .rero-slogan {
+ margin-bottom: 1em ;
+ }
+ }
+}
diff --git a/rero_ils/theme/static/themes/css/global.css b/rero_ils/theme/static/themes/css/global.css
new file mode 100644
index 0000000000..eae9b6d24a
--- /dev/null
+++ b/rero_ils/theme/static/themes/css/global.css
@@ -0,0 +1,38 @@
+@layer bootstrap {
+ body#view-global nav.rero-ils-header {
+ background-color: #1b4464 !important;
+ }
+
+ body#view-global nav.rero-ils-header img.rero-ils-logo {
+ display: none;
+ }
+
+ div#global-logo {
+ content: url('https://resources.rero.ch/bib/test/images/logo-global.svg');
+ min-height: 44px;
+ max-height: 44px;
+ min-width: 58px;
+ }
+
+ #view-global .rero-slogan {
+ margin-bottom: 4em;
+ }
+
+ @media (max-width: 576px) {
+ .h1, h1 {
+ font-size: 1.5rem;
+ }
+
+ .h2, h2 {
+ font-size: 1.2rem;
+ }
+
+ #view-global .rero-ils-frontpage {
+ height: 20vh;
+ }
+
+ #view-global .rero-slogan {
+ margin-bottom: 1em;
+ }
+ }
+}
\ No newline at end of file
diff --git a/rero_ils/theme/static/themes/css/highlands.css b/rero_ils/theme/static/themes/css/highlands.css
new file mode 100644
index 0000000000..3edeb9c6fa
--- /dev/null
+++ b/rero_ils/theme/static/themes/css/highlands.css
@@ -0,0 +1,37 @@
+@layer bootstrap {
+ body#view-highlands nav.rero-ils-header {
+ background-color: #333333 !important;
+ }
+
+ body#view-highlands nav.rero-ils-header img.rero-ils-logo {
+ display: none;
+ }
+
+ div#highlands-logo {
+ content: url('https://resources.rero.ch/ils/test/images/logo-highlands.svg');
+ max-height: 44px;
+ }
+
+ #view-global .rero-slogan {
+ margin-bottom: 4em;
+ }
+
+
+ @media (max-width: 576px) {
+ .h1, h1 {
+ font-size: 1.5rem;
+ }
+
+ .h2, h2 {
+ font-size: 1.2rem;
+ }
+
+ #view-global .rero-ils-frontpage {
+ height: 20vh;
+ }
+
+ #view-global .rero-slogan {
+ margin-bottom: 1em;
+ }
+ }
+}
\ No newline at end of file
diff --git a/rero_ils/theme/static/themes/images/logo-aoste.svg b/rero_ils/theme/static/themes/images/logo-aoste.svg
new file mode 100644
index 0000000000..1daf9be0f8
--- /dev/null
+++ b/rero_ils/theme/static/themes/images/logo-aoste.svg
@@ -0,0 +1,266 @@
+
+
+
\ No newline at end of file
diff --git a/rero_ils/theme/static/themes/images/logo-fictive.svg b/rero_ils/theme/static/themes/images/logo-fictive.svg
new file mode 100644
index 0000000000..64f7710fff
--- /dev/null
+++ b/rero_ils/theme/static/themes/images/logo-fictive.svg
@@ -0,0 +1,210 @@
+
+
+
\ No newline at end of file
diff --git a/rero_ils/theme/static/themes/images/logo-global.svg b/rero_ils/theme/static/themes/images/logo-global.svg
new file mode 100644
index 0000000000..50b754dd7f
--- /dev/null
+++ b/rero_ils/theme/static/themes/images/logo-global.svg
@@ -0,0 +1,308 @@
+
+
+
+
diff --git a/rero_ils/theme/static/themes/images/logo-highlands.svg b/rero_ils/theme/static/themes/images/logo-highlands.svg
new file mode 100644
index 0000000000..fc4c0612da
--- /dev/null
+++ b/rero_ils/theme/static/themes/images/logo-highlands.svg
@@ -0,0 +1,196 @@
+
+
+
\ No newline at end of file
diff --git a/rero_ils/theme/templates/rero_ils/header.html b/rero_ils/theme/templates/rero_ils/header.html
index f1b7c9118f..ec06e90150 100644
--- a/rero_ils/theme/templates/rero_ils/header.html
+++ b/rero_ils/theme/templates/rero_ils/header.html
@@ -24,7 +24,7 @@
{%- block navbar_brand %}
{%- if config.THEME_LOGO %}
-
+
diff --git a/rero_ils/theme/templates/rero_ils/page_cover.html b/rero_ils/theme/templates/rero_ils/page_cover.html
index 02eabf1c07..820ca2907a 100644
--- a/rero_ils/theme/templates/rero_ils/page_cover.html
+++ b/rero_ils/theme/templates/rero_ils/page_cover.html
@@ -35,7 +35,7 @@
{%- block brand %}
{%- if config.THEME_LOGO %}
-
+
diff --git a/scripts/convert_marc21xml.py b/scripts/convert_marc21xml.py
index ab6e9bb18c..2e4b6feb13 100644
--- a/scripts/convert_marc21xml.py
+++ b/scripts/convert_marc21xml.py
@@ -38,15 +38,15 @@ def convert(cls, data):
if identifier.get("source") == "RERO":
_id = identifier["value"]
if _id is None:
- logs = dict(
- warning=["Fixtures conversions"],
- error=["Unable to extract the main identifier"],
- )
+ logs = {
+ "warning": ["Fixtures conversions"],
+ "error": ["Unable to extract the main identifier"],
+ }
return None, {}, "error", logs
- logs = dict(
- info=["Conversion ok"],
- warning=["Fixtures conversions"],
- )
+ logs = {
+ "info": ["Conversion ok"],
+ "warning": ["Fixtures conversions"],
+ }
return _id, converted, "complete", logs
@classmethod
diff --git a/scripts/load_testing.py b/scripts/load_testing.py
index 97f8db4fd0..708428c619 100644
--- a/scripts/load_testing.py
+++ b/scripts/load_testing.py
@@ -37,12 +37,13 @@
class UpdateThread(threading.Thread):
"""Thread updating records."""
+
pids = []
calls = 50
locks = {}
token = None
- index = 'documents'
- host = 'localhost:5000'
+ index = "documents"
+ host = "localhost:5000"
def __init__(self, thread_number):
"""Init.
@@ -52,13 +53,13 @@ def __init__(self, thread_number):
threading.Thread.__init__(self)
self.thread_number = thread_number
self.headers = {
- 'Authorization': f'Bearer {UpdateThread.token}',
- 'Content-type': 'application/json'
+ "Authorization": f"Bearer {UpdateThread.token}",
+ "Content-type": "application/json"
}
def run(self):
"""Execute command."""
- for index in range(0, UpdateThread.calls):
+ for index in range(UpdateThread.calls):
pid = random.choice(UpdateThread.pids)
# Create lock if not exists
@@ -68,7 +69,7 @@ def run(self):
# Lock for PID
UpdateThread.locks[pid].acquire()
- url = f'https://{UpdateThread.host}/api/{UpdateThread.index}/{pid}'
+ url = f"https://{UpdateThread.host}/api/{UpdateThread.index}/{pid}"
try:
response = requests.get(url,
@@ -76,7 +77,7 @@ def run(self):
headers=self.headers)
if response.status_code != 200:
- raise Exception(f'GET error {response.status_code}')
+ raise Exception(f"GET error {response.status_code}")
def remove_calculated_properties(data):
"""Remove calculated properties to avoid a 400 error.
@@ -84,7 +85,7 @@ def remove_calculated_properties(data):
:param dict data: Data to process.
"""
for key in list(data):
- if key.startswith('_'):
+ if key.startswith("_"):
del data[key]
else:
if isinstance(data[key], list):
@@ -101,57 +102,57 @@ def remove_calculated_properties(data):
response = requests.put(url,
verify=False,
headers=self.headers,
- data=json.dumps(data['metadata']))
+ data=json.dumps(data["metadata"]))
if response.status_code != 200:
- raise Exception(f'PUT error {response.status_code}')
+ raise Exception(f"PUT error {response.status_code}")
print(
- f'Updated {url} in execution {index+1} of thread ' \
- f'{(self.thread_number+1)}: Status {response.status_code}, ' \
- f'Time {response.elapsed.total_seconds()}'
+ f"Updated {url} in execution {index+1} of thread "
+ f"{(self.thread_number+1)}: Status {response.status_code}, "
+ f"Time {response.elapsed.total_seconds()}"
)
except Exception as exception:
print(
- f'Error during processing of {url} in execution {index} ' \
- f'of thread {(self.thread_number+1)}: {str(exception)}'
+ f"Error during processing of {url} in execution {index} "
+ f"of thread {(self.thread_number+1)}: {exception!s}"
)
# Unlock
UpdateThread.locks[pid].release()
-def get_records_pids(index='documents', size=1000):
+def get_records_pids(index="documents", size=1000):
"""Get a list of records PIDs for the given index.
:param str index: Index to search for records.
:param int size: Number of records to return.
"""
results = Search(using=Elasticsearch(),
- index=index)[0:size].source(includes=['pid']).execute()
+ index=index)[0:size].source(includes=["pid"]).execute()
- return [item['pid'] for item in results]
+ return [item["pid"] for item in results]
if __name__ == "__main__":
- INDEX = sys.argv[1] if len(sys.argv) > 1 else 'documents'
+ INDEX = sys.argv[1] if len(sys.argv) > 1 else "documents"
NUMBER_OF_THREADS = int(sys.argv[2]) if len(sys.argv) > 2 else 10
CALLS_PER_THREAD = int(sys.argv[3]) if len(sys.argv) > 3 else 50
RECORDS_SIZE = int(sys.argv[4]) if len(sys.argv) > 4 else 1000
TOKEN = sys.argv[5] if len(sys.argv) > 5 else None
- HOST = sys.argv[6] if len(sys.argv) > 6 else 'localhost:5000'
+ HOST = sys.argv[6] if len(sys.argv) > 6 else "localhost:5000"
print()
- print(f'Index: {INDEX}')
- print(f'Number of threads: {NUMBER_OF_THREADS}')
- print(f'Calls per thread: {CALLS_PER_THREAD}')
- print(f'Records size: {RECORDS_SIZE}')
- print(f'Authentication token: {TOKEN}')
- print(f'Host: {HOST}')
+ print(f"Index: {INDEX}")
+ print(f"Number of threads: {NUMBER_OF_THREADS}")
+ print(f"Calls per thread: {CALLS_PER_THREAD}")
+ print(f"Records size: {RECORDS_SIZE}")
+ print(f"Authentication token: {TOKEN}")
+ print(f"Host: {HOST}")
- RESPONSE = input('\nIs that correct? [y/N]: ')
+ RESPONSE = input("\nIs that correct? [y/N]: ")
- if RESPONSE.lower() != 'y':
+ if RESPONSE.lower() != "y":
exit(0)
print()
@@ -162,6 +163,6 @@ def get_records_pids(index='documents', size=1000):
UpdateThread.index = INDEX
UpdateThread.host = HOST
- for i in range(0, NUMBER_OF_THREADS):
+ for i in range(NUMBER_OF_THREADS):
thread = UpdateThread(i)
thread.start()
diff --git a/tests/ui/test_views.py b/tests/ui/test_views.py
index 7c9d3bcbfd..9470028033 100644
--- a/tests/ui/test_views.py
+++ b/tests/ui/test_views.py
@@ -81,11 +81,21 @@ def test_view_parameter_notfound(client):
def test_external_endpoint_on_institution_homepage(client, org_martigny, app):
- """Test external endpoint on institution homepage."""
+ """Test organisation CSS endpoint configuration.
+
+ Verify that:
+ - The CSS endpoint is configured to use local static files
+ - The endpoint path appears in the rendered organisation homepage
+ """
result = client.get(url_for("rero_ils.index_with_view_code", viewcode="org1"))
+ assert result.status_code == 200
endpoint = app.config["RERO_ILS_THEME_ORGANISATION_CSS_ENDPOINT"]
- assert endpoint == "https://resources.rero.ch/bib/test/css/"
- assert str(result.data).find(endpoint) > 1
+
+ # Verify endpoint is configured for local static files
+ assert endpoint == "/static/themes/css/", f"Expected local static CSS endpoint, got: {endpoint}"
+
+ # Verify endpoint appears in the organisation homepage response
+ assert endpoint in result.text, f"CSS endpoint '{endpoint}' not found in homepage response"
def test_help(client):
diff --git a/uv.lock b/uv.lock
index ea987cfba0..30d926dcac 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1179,7 +1179,7 @@ name = "exceptiongroup"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "typing-extensions", marker = "python_full_version < '3.12'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [