Skip to content
This repository was archived by the owner on Apr 24, 2026. It is now read-only.
Merged
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
18 changes: 18 additions & 0 deletions src/main/java/no/ndla/taxonomy/domain/Node.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;
import no.ndla.taxonomy.config.Constants;
import no.ndla.taxonomy.domain.exceptions.ChildNotFoundException;
import no.ndla.taxonomy.domain.exceptions.DuplicateIdException;
import no.ndla.taxonomy.util.PrettyUrlUtil;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.UpdateTimestamp;
Expand Down Expand Up @@ -746,4 +748,20 @@ public List<JsonTranslation> getTranslations() {
public void setTranslations(List<JsonTranslation> translations) {
this.translations = translations;
}

public Set<String> translatedPrettyNames() {
var pretties = this.translations.stream()
.map(JsonTranslation::getName)
.filter(Objects::nonNull)
.map(PrettyUrlUtil::prettyName)
.collect(Collectors.toSet());
getPrettyName().ifPresent(pretties::add);
return pretties;
}

public Optional<String> getPrettyName() {
var defaultTranslation = this.getTranslatedName(Constants.DefaultLanguage);
var name = Optional.ofNullable(defaultTranslation);
return name.map(PrettyUrlUtil::prettyName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"rank",
"connectionId",
"url",
"defaultUrl",
"parents",
"parentContextIds",
"relevanceId",
Expand Down Expand Up @@ -99,5 +100,8 @@ public record TaxonomyContextDTO(
@JsonProperty @Schema(description = "Pretty-url of this particular context")
String url,

@JsonProperty @Schema(description = "Pretty-url of this particular context in the default language")
String defaultUrl,

@JsonProperty @Schema(description = "List of all parents to this context. Empty if node is fetched as child")
List<TaxonomyCrumbDTO> parents) {}
25 changes: 18 additions & 7 deletions src/main/java/no/ndla/taxonomy/service/NodeService.java
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,22 @@ public List<TaxonomyContextDTO> nodesToContexts(List<Node> nodes, boolean filter
}
})
.toList();
var url = PrettyUrlUtil.createPrettyUrl(
Optional.of(context.rootName()),
LanguageField.fromNode(node),
language,
context.contextId(),
node.getNodeType())
.orElse(context.path());

var defaultUrl = PrettyUrlUtil.createPrettyUrl(
Optional.of(context.rootName()),
LanguageField.fromNode(node),
Constants.DefaultLanguage,
context.contextId(),
node.getNodeType())
.orElse(context.path());

return new TaxonomyContextDTO(
node.getPublicId(),
node.getPublicId(),
Expand All @@ -378,13 +394,8 @@ public List<TaxonomyContextDTO> nodesToContexts(List<Node> nodes, boolean filter
context.contextId(),
context.rank(),
context.connectionId(),
PrettyUrlUtil.createPrettyUrl(
Optional.of(context.rootName()),
LanguageField.fromNode(node),
language,
context.contextId(),
node.getNodeType())
.orElse(context.path()),
url,
defaultUrl,
parents);
});
})
Expand Down
41 changes: 34 additions & 7 deletions src/main/java/no/ndla/taxonomy/service/dtos/NodeDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ public class NodeDTO {
@Schema(description = "A pretty url based on name and context. Empty if no context.")
private Optional<String> url = Optional.empty();

@JsonProperty
@Schema(description = "A pretty url based on the name and context in the default language.")
private Optional<String> defaultUrl = Optional.empty();

@JsonProperty
@Schema(description = "A list of all contexts this node is part of")
private List<TaxonomyContextDTO> contexts = new ArrayList<>();
Expand Down Expand Up @@ -118,6 +122,14 @@ public class NodeDTO {
@Schema(description = "The technical evaluation of the node.")
private Optional<TechnicalEvaluationDTO> technicalEvaluation = Optional.empty();

@JsonProperty
@Schema(description = "Url safe names for the node.")
private Set<String> urlName = new TreeSet<>();

@JsonProperty
@Schema(description = "Url safe name for the node in the default language.")
private String defaultUrlName;
Comment thread
gunnarvelle marked this conversation as resolved.

public NodeDTO() {}

public NodeDTO(
Expand Down Expand Up @@ -154,6 +166,9 @@ public NodeDTO(
this.supportedLanguages =
this.translations.stream().map(t -> t.language).collect(Collectors.toCollection(TreeSet::new));

this.urlName = entity.translatedPrettyNames();
this.defaultUrlName = entity.getPrettyName().orElse(null);
Comment thread
gunnarvelle marked this conversation as resolved.

this.language = languageCode; // TODO: Must handle programmes and subject better
// this.language = supportedLanguages.contains(languageCode)
// ? languageCode
Expand Down Expand Up @@ -189,6 +204,7 @@ public NodeDTO(
this.relevanceId = Optional.of(contextDto.relevanceId());
this.contextId = Optional.of(contextDto.contextId());
this.url = Optional.of(contextDto.url());
this.defaultUrl = Optional.of(contextDto.defaultUrl());
this.context = Optional.of(contextDto);
});

Expand Down Expand Up @@ -227,6 +243,22 @@ private TaxonomyContextDTO getTaxonomyContextDTO(
.filter(Objects::nonNull)
.toList();
var relevance = Relevance.unsafeGetRelevance(URI.create(ctx.relevanceId()));
var url = PrettyUrlUtil.createPrettyUrl(
Optional.of(ctx.rootName()),
LanguageField.fromNode(entity),
this.language,
ctx.contextId(),
entity.getNodeType())
.orElse(ctx.path());

var defaultUrl = PrettyUrlUtil.createPrettyUrl(
Optional.of(ctx.rootName()),
LanguageField.fromNode(entity),
Constants.DefaultLanguage,
ctx.contextId(),
entity.getNodeType())
.orElse(ctx.path());

return new TaxonomyContextDTO(
entity.getPublicId(),
entity.getPublicId(),
Expand Down Expand Up @@ -254,13 +286,8 @@ private TaxonomyContextDTO getTaxonomyContextDTO(
ctx.contextId(),
ctx.rank(),
ctx.connectionId(),
PrettyUrlUtil.createPrettyUrl(
Optional.of(ctx.rootName()),
LanguageField.fromNode(entity),
this.language,
ctx.contextId(),
entity.getNodeType())
.orElse(ctx.path()),
url,
defaultUrl,
parents);
}

Expand Down
6 changes: 6 additions & 0 deletions src/main/java/no/ndla/taxonomy/util/PrettyUrlUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ public static Optional<String> createPrettyUrl(
return Optional.of(builder.toString());
}

public static String prettyName(String name) {
var builder = new StringBuilder();
buildUrlFragment(builder, cleanString(name));
return builder.toString();
}

private static String nodeTypeMapping(NodeType nodeType) {
return switch (nodeType) {
case SUBJECT -> "/f";
Expand Down
2 changes: 0 additions & 2 deletions src/test/java/no/ndla/taxonomy/domain/Builder.java
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ public Node node(String key, NodeType nodeType, Consumer<NodeBuilder> consumer)
if (null != consumer) consumer.accept(node);

entityManager.persist(node.node);

contextUpdaterService.updateContexts(node.node);

return node.node;
Expand Down Expand Up @@ -377,7 +376,6 @@ public NodeBuilder child(String key, NodeType nodeType, Consumer<NodeBuilder> co

public NodeBuilder child(Node child) {
entityManager.persist(NodeConnection.create(node, child, Relevance.CORE));

contextUpdaterService.updateContexts(child);

return this;
Expand Down
25 changes: 25 additions & 0 deletions src/test/java/no/ndla/taxonomy/domain/NodeUrlNameTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Part of NDLA taxonomy-api
* Copyright (C) 2026 NDLA
*
* See LICENSE
*/

package no.ndla.taxonomy.domain;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.Set;
import org.junit.jupiter.api.Test;

class NodeUrlNameTest {

@Test
void translatedPrettyNamesSkipsNullTranslationNames() {
var node = new Node(NodeType.TOPIC);
node.setName("Default Name");
node.addTranslation(new JsonTranslation(null, "nn"));

assertEquals(Set.of("default-name"), node.translatedPrettyNames());
}
}
41 changes: 41 additions & 0 deletions src/test/java/no/ndla/taxonomy/service/dtos/NodeDTOTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Part of NDLA taxonomy-api
* Copyright (C) 2026 NDLA
*
* See LICENSE
*/

package no.ndla.taxonomy.service.dtos;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.springframework.test.util.ReflectionTestUtils.getField;

import java.util.Optional;
import no.ndla.taxonomy.domain.Node;
import no.ndla.taxonomy.domain.NodeConnectionType;
import no.ndla.taxonomy.domain.NodeType;
import org.junit.jupiter.api.Test;

class NodeDTOTest {

@Test
void defaultUrlNameUsesDefaultLanguageTranslation() {
var node = new Node(NodeType.TOPIC);
node.setName("English base name");
node.addTranslation("Bokmal default name", "nb");

var dto = new NodeDTO(
Optional.empty(),
Optional.empty(),
node,
NodeConnectionType.BRANCH,
"en",
Optional.empty(),
false,
false,
true,
false);

assertEquals("bokmal-default-name", getField(dto, "defaultUrlName"));
}
}
Loading