Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate static urls #11571

Open
wants to merge 15 commits into
base: dev/8.0.x
Choose a base branch
from
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,4 @@ pip-wheel-metadata
webpack-stats.json
.DS_STORE
CACHE
.tsconfig-paths.json
.frontend-configuration-settings.json
frontend_configuration
6 changes: 4 additions & 2 deletions arches/app/media/js/utils/create-vue-application.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import Tooltip from 'primevue/tooltip';
import { createApp } from 'vue';
import { createGettext } from "vue3-gettext";

import arches from 'arches';
import { DEFAULT_THEME } from "@/arches/themes/default.ts";
import generateArchesURL from '@/arches/utils/generate-arches-url.ts';


export default async function createVueApplication(vueComponent, themeConfiguration) {
/**
Expand All @@ -27,7 +28,8 @@ export default async function createVueApplication(vueComponent, themeConfigurat
* TODO: cbyrd #10501 - we should add an event listener that will re-fetch i18n data
* and rebuild the app when a specific event is fired from the LanguageSwitcher component.
**/
return fetch(arches.urls.api_get_frontend_i18n_data).then(function(resp) {

return fetch(generateArchesURL("get_frontend_i18n_data")).then(function(resp) {
if (!resp.ok) {
throw new Error(resp.statusText);
}
Expand Down
56 changes: 56 additions & 0 deletions arches/app/src/arches/utils/generate-arches-url.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { describe, it, expect } from "vitest";
import generateArchesURL from "@/arches/utils/generate-arches-url.ts";

// @ts-expect-error ARCHES_URLS is defined globally
global.ARCHES_URLS = {
example_url: "/{language_code}/admin/example/{id}",
another_url: "/admin/another/{id}",
multi_interpolation_url:
"/{language_code}/resource/{resource_id}/edit/{field_id}/version/{version_id}",
};

describe("generateArchesURL", () => {
it("should return a valid URL with specified language code and parameters", () => {
const result = generateArchesURL("example_url", { id: "123" }, "fr");
expect(result).toBe("/fr/admin/example/123");
});

it("should use the <html> lang attribute when no language code is provided", () => {
Object.defineProperty(document.documentElement, "lang", {
value: "de",
configurable: true,
});

const result = generateArchesURL("example_url", { id: "123" });
expect(result).toBe("/de/admin/example/123");
});

it("should throw an error if the URL name is not found", () => {
expect(() =>
generateArchesURL("invalid_url", { id: "123" }, "fr"),
).toThrowError("Key 'invalid_url' not found in JSON object");
});

it("should replace URL parameters correctly", () => {
const result = generateArchesURL("another_url", { id: "456" });
expect(result).toBe("/admin/another/456");
});

it("should handle URLs without language code placeholder", () => {
const result = generateArchesURL("another_url", { id: "789" });
expect(result).toBe("/admin/another/789");
});

it("should handle multiple interpolations in the URL", () => {
const result = generateArchesURL(
"multi_interpolation_url",
{
resource_id: "42",
field_id: "name",
version_id: "7",
},
"es",
);
expect(result).toBe("/es/resource/42/edit/name/version/7");
});
});
27 changes: 27 additions & 0 deletions arches/app/src/arches/utils/generate-arches-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export default function (
urlName: string,
urlParams = {},
languageCode?: string,
) {
// @ts-expect-error ARCHES_URLS is defined globally
let url = ARCHES_URLS[urlName];

if (!url) {
throw new Error(`Key '${urlName}' not found in JSON object`);
}

if (url.includes("{language_code}")) {
if (!languageCode) {
const htmlLang = document.documentElement.lang;
languageCode = htmlLang.split("-")[0];
}

url = url.replace("{language_code}", languageCode);
}

Object.entries(urlParams).forEach(([key, value]) => {
url = url.replace(new RegExp(`{${key}}`, "g"), value);
});

return url;
}
3 changes: 1 addition & 2 deletions arches/app/templates/base-root.htm
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<!DOCTYPE html>
<!--[if IE 8]> <html lang="en" class="ie8"> <![endif]-->
<!--[if IE 9]> <html lang="en" class="ie9"> <![endif]-->
<!--[if !IE]><!--> <html lang="en"> <!--<![endif]-->
<!--[if !IE]><!--> <html lang="{{ app_settings.ACTIVE_LANGUAGE }}"> <!--<![endif]-->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about this more since yesterday, this might be alright after all, given the current pattern of using the i18n pattern routes from Django. On language switch, reload the page, see the updated /lang/... route. Hash out a different pattern if/when we need it.


{% block head %}
<head>
Expand Down Expand Up @@ -77,7 +77,6 @@
{% endblock pre_require_js %}

{% block arches_modules %}
{% include "arches_urls.htm" %}
{% endblock arches_modules %}

{% if main_script %}
Expand Down
10 changes: 1 addition & 9 deletions arches/app/templates/base.htm
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,9 @@
-->
{% extends "base-root.htm" %}

{% load static %}
{% load i18n %}
{% load webpack_static from webpack_loader %}
{% load render_bundle from webpack_loader %}

<!DOCTYPE html>
<!--[if IE 8]> <html lang="en" class="ie8"> <![endif]-->
<!--[if IE 9]> <html lang="en" class="ie9"> <![endif]-->
<!--[if !IE]><!--> <html lang="en"> <!--<![endif]-->
jacobtylerwalls marked this conversation as resolved.
Show resolved Hide resolved

{% if use_livereload %}
<script>document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] + ':{{ livereload_port }}/livereload.js?snipver=1"></' + 'script>')</script>
{% endif %}
Expand Down Expand Up @@ -61,5 +54,4 @@

{% block arches_modules %}
{% include 'javascript.htm' %}
{% endblock arches_modules %}
</html>
{% endblock arches_modules %}
198 changes: 198 additions & 0 deletions arches/app/utils/frontend_configuration_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import json
import os
import re
import site
import sys

from django.conf import settings
from django.urls import get_resolver, URLPattern, URLResolver
from django.urls.resolvers import RegexPattern, RoutePattern, LocalePrefixPattern

from arches.settings_utils import list_arches_app_names, list_arches_app_paths


def generate_frontend_configuration():
try:
_generate_frontend_configuration_directory()
_generate_urls_json()
_generate_webpack_configuration()
_generate_tsconfig_paths()
except Exception as e:
# Ensures error message is shown if error encountered
sys.stderr.write(str(e))
raise e


def _generate_frontend_configuration_directory():
destination_dir = os.path.realpath(
os.path.join(_get_base_path(), "..", "frontend_configuration")
)

os.makedirs(destination_dir, exist_ok=True)


def _generate_urls_json():
def generate_human_readable_urls(patterns, prefix="", namespace="", result={}):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I try to avoid mutable default arguments like [] and {}, although I do see you've wrapped this all in a closure. Just wondering if you'd prefer to make this a little more explicit so someone else isn't surprised that result persists between calls.

def join_paths(*args):
return "/".join(filter(None, (arg.strip("/") for arg in args)))
Comment on lines +36 to +37
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We seem to be losing the trailing slash on some routes. Leaving aside whether it's a good idea to have trailing slashes required in your routes, if projects do have that, I think we should leave it to them (and whatever third-party packages even outside of arches they depend on).

For instance, with status quo, we have on javascript.htm:

reorder_cards="/reorder_cards/"

but now in urls.json:

    "reorder_cards": "/reorder_cards",

giving:

> _arches_utils_generate_arches_url_ts__WEBPACK_IMPORTED_MODULE_11__["default"]('reorder_cards')
'/reorder_cards'

Which will cause a redirect (or not) depending on the value of APPEND_SLASH and yada yada.

Can we avoid a behavior change here?


def interpolate_route(pattern):
if isinstance(pattern, RoutePattern):
return re.sub(r"<(?:[^:]+:)?([^>]+)>", r"{\1}", pattern._route)
elif isinstance(pattern, RegexPattern):
regex = pattern._regex.lstrip("^").rstrip("$")

# Replace named capture groups (e.g., (?P<param>)) with {param}
regex = re.sub(r"\(\?P<(\w+)>[^)]+\)", r"{\1}", regex)

# Remove non-capturing groups (e.g., (?:...))
regex = re.sub(r"\(\?:[^\)]+\)", "", regex)

# Remove character sets (e.g., [0-9])
regex = re.sub(r"\[[^\]]+\]", "", regex)

# Remove backslashes (used to escape special characters in regex)
regex = regex.replace("\\", "")

# Remove regex-specific special characters (^, $, +, *, ?, (), etc.)
regex = re.sub(r"[\^\$\+\*\?\(\)]", "", regex)

return regex.strip("/")

for pattern in patterns:
if isinstance(pattern, URLPattern):
if pattern.name:
result[f"{namespace}{pattern.name}"] = "/" + join_paths(
prefix, interpolate_route(pattern.pattern)
)
elif isinstance(pattern, URLResolver):
current_namespace = namespace + (
f":{pattern.namespace}:" if pattern.namespace else ""
)

if isinstance(
pattern.pattern, LocalePrefixPattern
): # handles i18n_patterns
new_prefix = join_paths(prefix, "{language_code}")
else:
new_prefix = join_paths(prefix, interpolate_route(pattern.pattern))

generate_human_readable_urls(
pattern.url_patterns, new_prefix, current_namespace, result
)
return result

resolver = get_resolver()
human_readable_urls = generate_human_readable_urls(resolver.url_patterns)

# manual additions
human_readable_urls["static_url"] = settings.STATIC_URL
human_readable_urls["media_url"] = settings.MEDIA_URL

destination_path = os.path.realpath(
os.path.join(_get_base_path(), "..", "frontend_configuration", "urls.json")
)

with open(destination_path, "w") as file:
json.dump(
{
"_comment": "This is a generated file. Do not edit directly.",
**{
url_name: human_readable_urls[url_name]
for url_name in sorted(human_readable_urls)
},
},
file,
indent=4,
)


def _generate_webpack_configuration():
app_root_path = os.path.realpath(settings.APP_ROOT)
root_dir_path = os.path.realpath(settings.ROOT_DIR)

arches_app_names = list_arches_app_names()
arches_app_paths = list_arches_app_paths()

destination_path = os.path.realpath(
os.path.join(
_get_base_path(), "..", "frontend_configuration", "webpack-metadata.json"
)
)

with open(destination_path, "w") as file:
json.dump(
{
"_comment": "This is a generated file. Do not edit directly.",
"APP_ROOT": app_root_path,
"ARCHES_APPLICATIONS": arches_app_names,
"ARCHES_APPLICATIONS_PATHS": dict(
zip(arches_app_names, arches_app_paths, strict=True)
),
"SITE_PACKAGES_DIRECTORY": site.getsitepackages()[0],
"PUBLIC_SERVER_ADDRESS": settings.PUBLIC_SERVER_ADDRESS,
"ROOT_DIR": root_dir_path,
"STATIC_URL": settings.STATIC_URL,
"WEBPACK_DEVELOPMENT_SERVER_PORT": settings.WEBPACK_DEVELOPMENT_SERVER_PORT,
},
file,
indent=4,
)


def _generate_tsconfig_paths():
base_path = _get_base_path()
root_dir_path = os.path.realpath(settings.ROOT_DIR)

path_lookup = dict(
zip(list_arches_app_names(), list_arches_app_paths(), strict=True)
)

tsconfig_paths_data = {
"_comment": "This is a generated file. Do not edit directly.",
"compilerOptions": {
"paths": {
"@/arches/*": [
os.path.join(
".",
os.path.relpath(
root_dir_path,
os.path.join(base_path, ".."),
),
"app",
"src",
"arches",
"*",
)
],
**{
os.path.join("@", path_name, "*"): [
os.path.join(
".",
os.path.relpath(path, os.path.join(base_path, "..")),
"src",
path_name,
"*",
)
]
for path_name, path in path_lookup.items()
},
"*": ["./node_modules/*"],
}
},
}

destination_path = os.path.realpath(
os.path.join(base_path, "..", "frontend_configuration", "tsconfig-paths.json")
)

with open(destination_path, "w") as file:
json.dump(tsconfig_paths_data, file, indent=4)


def _get_base_path():
return (
os.path.realpath(settings.ROOT_DIR)
if settings.APP_NAME == "Arches"
else os.path.realpath(settings.APP_ROOT)
)
3 changes: 0 additions & 3 deletions arches/app/utils/index_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
import uuid
import pyprind
import sys
import django

django.setup()

from datetime import datetime
from django.db import connection, connections
Expand Down
4 changes: 3 additions & 1 deletion arches/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
from semantic_version import SimpleSpec, Version

from arches import __version__
from arches.settings_utils import generate_frontend_configuration
from arches.app.utils.frontend_configuration_utils import (
generate_frontend_configuration,
)


class ArchesAppConfig(AppConfig):
Expand Down
3 changes: 1 addition & 2 deletions arches/install/arches-templates/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,4 @@ webpack-stats.json
*.egg-info
.DS_STORE
CACHE
.tsconfig-paths.json
.frontend-configuration-settings.json
frontend_configuration
2 changes: 1 addition & 1 deletion arches/install/arches-templates/project_name/apps.py-tpl
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.apps import AppConfig
from django.conf import settings

from arches.settings_utils import generate_frontend_configuration
from arches.app.utils.frontend_configuration_utils import generate_frontend_configuration


class {{ project_name_title_case }}Config(AppConfig):
Expand Down
2 changes: 1 addition & 1 deletion arches/install/arches-templates/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"extends": "./.tsconfig-paths.json",
"extends": "./frontend_configuration/tsconfig-paths.json",
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
Expand Down
Loading