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 20 commits into
base: dev/8.0.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]-->
chrabyrd marked this conversation as resolved.
Show resolved Hide resolved

{% 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 %}
219 changes: 219 additions & 0 deletions arches/app/utils/frontend_configuration_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
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=""):
def join_paths(*args):
components = []

for index, segment in enumerate(args):
if index == 0: # Only strip trailing slash for the first segment
segment = segment.rstrip("/")
elif (
index == len(args) - 1
): # Only strip leading slash for the last segment
segment = segment.lstrip("/")
else: # Strip both slashes for middle segments
segment = segment.strip("/")

components.append(segment)

return "/".join(filter(None, components))

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
regex = re.sub(r"[\^\$\+\*\?\(\)]", "", regex)

return regex

result = {}

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

elif isinstance(pattern, URLResolver):
current_namespace = (
f"{namespace}{pattern.namespace}:"
if pattern.namespace
else namespace
)

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))

sub_result = generate_human_readable_urls(
pattern.url_patterns, new_prefix, current_namespace
)
result.update(sub_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_RELATIVE_PATH": os.path.relpath(app_root_path),
"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)
)
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
Loading