From d725549d9ee257e519093bf8d4b3895e0c51472c Mon Sep 17 00:00:00 2001 From: Jonas Carlsen Date: Tue, 27 Jan 2026 09:11:32 +0100 Subject: [PATCH] feat: copyright URLs per locale --- .github/workflows/mapping_ci.yml | 2 + .../articleapi/service/ConverterService.scala | 14 ++-- .../service/ConverterServiceTest.scala | 4 +- .../audioapi/service/ConverterService.scala | 16 +++-- .../service/ConverterServiceTest.scala | 8 +-- .../audioapi/service/WriteServiceTest.scala | 12 ++-- .../conceptapi/service/ConverterService.scala | 27 ++++--- .../search/SearchConverterService.scala | 2 +- .../draftapi/controller/DraftController.scala | 14 +++- .../draftapi/service/ConverterService.scala | 16 +++-- .../controller/DraftControllerTest.scala | 21 +++++- .../service/ConverterServiceTest.scala | 4 +- .../imageapi/service/ConverterService.scala | 18 +++-- .../controller/InternControllerTest.scala | 2 +- .../controller/LearningpathControllerV2.scala | 14 +++- .../service/ConverterService.scala | 26 ++++--- mapping/package.mill | 7 +- .../main/scala/no/ndla/mapping/License.scala | 72 ++++++++++++++++--- .../scala/no/ndla/mapping/LicenseTest.scala | 6 +- .../search/SearchConverterService.scala | 7 +- 20 files changed, 206 insertions(+), 86 deletions(-) diff --git a/.github/workflows/mapping_ci.yml b/.github/workflows/mapping_ci.yml index e8682ee4d9..73dfc03560 100644 --- a/.github/workflows/mapping_ci.yml +++ b/.github/workflows/mapping_ci.yml @@ -7,11 +7,13 @@ on: push: paths: - mapping/** + - language/** - modules/** - build.mill pull_request: paths: - mapping/** + - language/** - modules/** - build.mill jobs: diff --git a/article-api/src/main/scala/no/ndla/articleapi/service/ConverterService.scala b/article-api/src/main/scala/no/ndla/articleapi/service/ConverterService.scala index 9a27b2ddb9..847ff1427b 100644 --- a/article-api/src/main/scala/no/ndla/articleapi/service/ConverterService.scala +++ b/article-api/src/main/scala/no/ndla/articleapi/service/ConverterService.scala @@ -257,7 +257,7 @@ class ConverterService(using props: Props) extends StrictLogging { .map(toApiArticleContentV2) .getOrElse(api.ArticleContentV2DTO("", UnknownLanguage.toString)) val metaImage = findByLanguageOrBestEffort(article.metaImage, language).map(toApiArticleMetaImage) - val copyright = toApiCopyright(article.copyright) + val copyright = toApiCopyright(article.copyright, language) val disclaimer = article.disclaimer.findByLanguageOrBestEffort(language).map(DisclaimerDTO.fromLanguageValue) Success( @@ -317,9 +317,9 @@ class ConverterService(using props: Props) extends StrictLogging { } - private def toApiCopyright(copyright: Copyright): commonApi.CopyrightDTO = { + private def toApiCopyright(copyright: Copyright, language: String): commonApi.CopyrightDTO = { commonApi.CopyrightDTO( - toApiLicense(copyright.license), + toApiLicense(copyright.license, language), copyright.origin, copyright.creators.map(_.toApi), copyright.processors.map(_.toApi), @@ -330,10 +330,12 @@ class ConverterService(using props: Props) extends StrictLogging { ) } - def toApiLicense(shortLicense: String): LicenseDTO = { + def toApiLicense(shortLicense: String, language: String): LicenseDTO = { getLicense(shortLicense) match { - case Some(l) => model.api.LicenseDTO(l.license.toString, Option(l.description), l.url) - case None => model.api.LicenseDTO("unknown", None, None) + case Some(l) => model + .api + .LicenseDTO(l.license.toString, Option(l.description), findByLanguageOrBestEffort(l.url, language).map(_.url)) + case None => model.api.LicenseDTO("unknown", None, None) } } diff --git a/article-api/src/test/scala/no/ndla/articleapi/service/ConverterServiceTest.scala b/article-api/src/test/scala/no/ndla/articleapi/service/ConverterServiceTest.scala index b54be330d3..266af976d8 100644 --- a/article-api/src/test/scala/no/ndla/articleapi/service/ConverterServiceTest.scala +++ b/article-api/src/test/scala/no/ndla/articleapi/service/ConverterServiceTest.scala @@ -45,11 +45,11 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { val sampleAlt = "Fotografi" test("toApiLicense defaults to unknown if the license was not found") { - service.toApiLicense("invalid") should equal(LicenseDTO("unknown", None, None)) + service.toApiLicense("invalid", "nb") should equal(LicenseDTO("unknown", None, None)) } test("toApiLicense converts a short license string to a license object with description and url") { - service.toApiLicense("CC-BY-4.0") should equal( + service.toApiLicense("CC-BY-4.0", "nb") should equal( model .api .LicenseDTO( diff --git a/audio-api/src/main/scala/no/ndla/audioapi/service/ConverterService.scala b/audio-api/src/main/scala/no/ndla/audioapi/service/ConverterService.scala index a5ff73dc70..1e857548d9 100644 --- a/audio-api/src/main/scala/no/ndla/audioapi/service/ConverterService.scala +++ b/audio-api/src/main/scala/no/ndla/audioapi/service/ConverterService.scala @@ -98,7 +98,7 @@ class ConverterService(using clock: Clock, props: Props) extends StrictLogging { revision = audioMeta.revision.get, title = maybeToApiTitle(findByLanguageOrBestEffort(audioMeta.titles, language)), audioFile = toApiAudio(findByLanguageOrBestEffort(audioMeta.filePaths, language)), - copyright = toApiCopyright(audioMeta.copyright), + copyright = toApiCopyright(audioMeta.copyright, language.getOrElse(props.DefaultLanguage)), tags = toApiTags(findByLanguageOrBestEffort(audioMeta.tags, language)), supportedLanguages = audioMeta.supportedLanguages, audioType = audioMeta.audioType.toString, @@ -137,18 +137,22 @@ class ConverterService(using clock: Clock, props: Props) extends StrictLogging { } } - def toApiLicence(licenseAbbrevation: String): commonApi.LicenseDTO = { + def toApiLicence(licenseAbbrevation: String, language: String): commonApi.LicenseDTO = { getLicense(licenseAbbrevation) match { - case Some(license) => commonApi.LicenseDTO(license.license.toString, Option(license.description), license.url) - case None => + case Some(license) => commonApi.LicenseDTO( + license.license.toString, + Option(license.description), + findByLanguageOrBestEffort(license.url, language).map(_.url), + ) + case None => logger.warn("Could not retrieve license information for {}", licenseAbbrevation) commonApi.LicenseDTO("unknown", None, None) } } - def toApiCopyright(copyright: Copyright): commonApi.CopyrightDTO = { + def toApiCopyright(copyright: Copyright, language: String): commonApi.CopyrightDTO = { commonApi.CopyrightDTO( - toApiLicence(copyright.license), + toApiLicence(copyright.license, language), copyright.origin, copyright.creators.map(_.toApi), copyright.processors.map(_.toApi), diff --git a/audio-api/src/test/scala/no/ndla/audioapi/service/ConverterServiceTest.scala b/audio-api/src/test/scala/no/ndla/audioapi/service/ConverterServiceTest.scala index 75f60198aa..b18948c9d6 100644 --- a/audio-api/src/test/scala/no/ndla/audioapi/service/ConverterServiceTest.scala +++ b/audio-api/src/test/scala/no/ndla/audioapi/service/ConverterServiceTest.scala @@ -60,7 +60,7 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { audioMeta.revision.get, api.TitleDTO("Batmen er på vift med en bil", "nb"), service.toApiAudio(audioMeta.filePaths.headOption), - service.toApiCopyright(audioMeta.copyright), + service.toApiCopyright(audioMeta.copyright, "nb"), api.TagDTO(Seq("fisk"), "nb"), Seq("nb"), "standard", @@ -80,7 +80,7 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { audioMeta.revision.get, api.TitleDTO("Batmen er på vift med en bil", "nb"), service.toApiAudio(audioMeta.filePaths.headOption), - service.toApiCopyright(audioMeta.copyright), + service.toApiCopyright(audioMeta.copyright, "nb"), api.TagDTO(Seq("fisk"), "nb"), Seq("nb"), "standard", @@ -108,13 +108,13 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { Some("https://creativecommons.org/licenses/by-sa/4.0/"), ) - service.toApiLicence(licenseAbbr) should equal(license) + service.toApiLicence(licenseAbbr, "nb") should equal(license) } test("That toApiLicense returns unknown if the license is invalid") { val licenseAbbr = "garbage" - service.toApiLicence(licenseAbbr) should equal(commonApi.LicenseDTO("unknown", None, None)) + service.toApiLicence(licenseAbbr, "nb") should equal(commonApi.LicenseDTO("unknown", None, None)) } test("That mergeLanguageField merges language fields as expected") { diff --git a/audio-api/src/test/scala/no/ndla/audioapi/service/WriteServiceTest.scala b/audio-api/src/test/scala/no/ndla/audioapi/service/WriteServiceTest.scala index 39cf966710..926ec1b568 100644 --- a/audio-api/src/test/scala/no/ndla/audioapi/service/WriteServiceTest.scala +++ b/audio-api/src/test/scala/no/ndla/audioapi/service/WriteServiceTest.scala @@ -295,7 +295,7 @@ class WriteServiceTest extends UnitSuite with TestEnvironment { 1, "A new english title", "en", - converterService.toApiCopyright(domainAudioMeta.copyright), + converterService.toApiCopyright(domainAudioMeta.copyright, "en"), Seq(), None, None, @@ -312,7 +312,7 @@ class WriteServiceTest extends UnitSuite with TestEnvironment { 1, "En ny norsk tittel", "nb", - converterService.toApiCopyright(domainAudioMeta.copyright), + converterService.toApiCopyright(domainAudioMeta.copyright, "nb"), Seq(), None, None, @@ -330,7 +330,7 @@ class WriteServiceTest extends UnitSuite with TestEnvironment { 1, "A new english title", "en", - converterService.toApiCopyright(domainAudioMeta.copyright), + converterService.toApiCopyright(domainAudioMeta.copyright, "en"), Seq(), None, None, @@ -350,7 +350,7 @@ class WriteServiceTest extends UnitSuite with TestEnvironment { 1, "A new english title", "en", - converterService.toApiCopyright(domainAudioMeta.copyright), + converterService.toApiCopyright(domainAudioMeta.copyright, "en"), Seq(), None, None, @@ -372,7 +372,7 @@ class WriteServiceTest extends UnitSuite with TestEnvironment { 1, "En ny norsk tittel", "nb", - converterService.toApiCopyright(domainAudioMeta.copyright), + converterService.toApiCopyright(domainAudioMeta.copyright, "nb"), Seq(), None, None, @@ -837,7 +837,7 @@ class WriteServiceTest extends UnitSuite with TestEnvironment { 1, "A new english title", "en", - converterService.toApiCopyright(domainAudioMeta.copyright), + converterService.toApiCopyright(domainAudioMeta.copyright, "en"), Seq("abc", "123", "abc", "def"), None, None, diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/service/ConverterService.scala b/concept-api/src/main/scala/no/ndla/conceptapi/service/ConverterService.scala index d8a254f553..0d8a2b61aa 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/service/ConverterService.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/service/ConverterService.scala @@ -75,7 +75,7 @@ class ConverterService(using clock: Clock, props: Props) extends StrictLogging { revision = concept.revision.getOrElse(-1), title = title, content = Some(content), - copyright = concept.copyright.map(toApiCopyright), + copyright = concept.copyright.map(lic => toApiCopyright(lic, language)), source = concept.copyright.flatMap(_.origin), tags = tags, created = concept.created, @@ -136,9 +136,12 @@ class ConverterService(using clock: Clock, props: Props) extends StrictLogging { api.ConceptTagsDTO(tags.tags, tags.language) } - private def toApiCopyright(copyright: commonDomain.draft.DraftCopyright): commonApi.DraftCopyrightDTO = { + private def toApiCopyright( + copyright: commonDomain.draft.DraftCopyright, + language: String, + ): commonApi.DraftCopyrightDTO = { commonApi.DraftCopyrightDTO( - copyright.license.flatMap(toMaybeApiLicense), + copyright.license.flatMap(lic => toMaybeApiLicense(lic, language)), copyright.origin, copyright.creators.map(_.toApi), copyright.processors.map(_.toApi), @@ -149,16 +152,22 @@ class ConverterService(using clock: Clock, props: Props) extends StrictLogging { ) } - private def toMaybeApiLicense(shortLicense: String): Option[commonApi.LicenseDTO] = { - getLicense(shortLicense).map(l => commonApi.LicenseDTO(l.license.toString, Option(l.description), l.url)) + private def toMaybeApiLicense(shortLicense: String, language: String): Option[commonApi.LicenseDTO] = { + getLicense(shortLicense).map(l => + commonApi.LicenseDTO( + l.license.toString, + Option(l.description), + findByLanguageOrBestEffort(l.url, language).map(_.url), + ) + ) } - def toApiLicense(maybeShortLicense: Option[String]): commonApi.LicenseDTO = maybeShortLicense - .flatMap(toMaybeApiLicense) + def toApiLicense(maybeShortLicense: Option[String], language: String): commonApi.LicenseDTO = maybeShortLicense + .flatMap(lic => toMaybeApiLicense(lic, language)) .getOrElse(commonApi.LicenseDTO("unknown", None, None)) - def toApiLicense(shortLicense: String): commonApi.LicenseDTO = - toMaybeApiLicense(shortLicense).getOrElse(commonApi.LicenseDTO("unknown", None, None)) + def toApiLicense(shortLicense: String, language: String): commonApi.LicenseDTO = + toMaybeApiLicense(shortLicense, language).getOrElse(commonApi.LicenseDTO("unknown", None, None)) def toApiConceptTitle(title: Title): api.ConceptTitleDTO = api.ConceptTitleDTO(Jsoup.parseBodyFragment(title.title).body().text(), title.title, title.language) diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/service/search/SearchConverterService.scala b/concept-api/src/main/scala/no/ndla/conceptapi/service/search/SearchConverterService.scala index db463dc89d..06e068b417 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/service/search/SearchConverterService.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/service/search/SearchConverterService.scala @@ -113,7 +113,7 @@ class SearchConverterService(using converterService: ConverterService) extends S .getOrElse(api.ConceptContent("", "", UnknownLanguage.toString())) val tag = findByLanguageOrBestEffort(tags, language).map(converterService.toApiTags) val visualElement = findByLanguageOrBestEffort(visualElements, language).map(converterService.toApiVisualElement) - val license = converterService.toApiLicense(searchableConcept.license) + val license = converterService.toApiLicense(searchableConcept.license, language) val copyright = searchableConcept .copyright .map(c => { diff --git a/draft-api/src/main/scala/no/ndla/draftapi/controller/DraftController.scala b/draft-api/src/main/scala/no/ndla/draftapi/controller/DraftController.scala index 95a78ee78b..f50b3b1cfe 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/controller/DraftController.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/controller/DraftController.scala @@ -32,6 +32,7 @@ import sttp.tapir.* import sttp.tapir.server.ServerEndpoint import scala.util.{Failure, Success, Try} +import no.ndla.language.Language.findByLanguageOrBestEffort class DraftController(using readService: ReadService, @@ -433,8 +434,9 @@ class DraftController(using .errorOut(errorOutputsFor(401, 403)) .out(jsonBody[Seq[LicenseDTO]]) .in(filterNot) + .in(language) .in(filter) - .serverLogicPure { case (filterNot, filter) => + .serverLogicPure { case (filterNot, language, filter) => val licenses: Seq[LicenseDefinition] = mapping .License .getLicenses @@ -447,7 +449,15 @@ class DraftController(using case _ => false } - licenses.map(x => LicenseDTO(x.license.toString, Option(x.description), x.url)).asRight + licenses + .map(x => + LicenseDTO( + x.license.toString, + Option(x.description), + findByLanguageOrBestEffort(x.url, language.code).map(_.url), + ) + ) + .asRight } def newArticle: ServerEndpoint[Any, Eff] = endpoint diff --git a/draft-api/src/main/scala/no/ndla/draftapi/service/ConverterService.scala b/draft-api/src/main/scala/no/ndla/draftapi/service/ConverterService.scala index 12711c1878..ec38bf5116 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/service/ConverterService.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/service/ConverterService.scala @@ -306,7 +306,7 @@ class ConverterService(using status = toApiStatus(article.status), title = title, content = articleContent, - copyright = article.copyright.map(toApiCopyright), + copyright = article.copyright.map(lic => toApiCopyright(lic, language)), tags = tags, requiredLibraries = article.requiredLibraries.map(toApiRequiredLibrary), visualElement = visualElement, @@ -377,11 +377,11 @@ class ConverterService(using ) } - private def toApiCopyright(copyright: common.draft.DraftCopyright): DraftCopyrightDTO = { + private def toApiCopyright(copyright: common.draft.DraftCopyright, language: String): DraftCopyrightDTO = { model .api .DraftCopyrightDTO( - copyright.license.map(toApiLicense), + copyright.license.map(lic => toApiLicense(lic, language)), copyright.origin, copyright.creators.map(_.toApi), copyright.processors.map(_.toApi), @@ -392,9 +392,15 @@ class ConverterService(using ) } - def toApiLicense(shortLicense: String): commonApi.LicenseDTO = { + def toApiLicense(shortLicense: String, language: String): commonApi.LicenseDTO = { getLicense(shortLicense) - .map(l => commonApi.LicenseDTO(l.license.toString, Option(l.description), l.url)) + .map(l => + commonApi.LicenseDTO( + l.license.toString, + Option(l.description), + findByLanguageOrBestEffort(l.url, language).map(_.url), + ) + ) .getOrElse(commonApi.LicenseDTO("unknown", None, None)) } diff --git a/draft-api/src/test/scala/no/ndla/draftapi/controller/DraftControllerTest.scala b/draft-api/src/test/scala/no/ndla/draftapi/controller/DraftControllerTest.scala index 4b23a9c743..7fc188dc4c 100644 --- a/draft-api/src/test/scala/no/ndla/draftapi/controller/DraftControllerTest.scala +++ b/draft-api/src/test/scala/no/ndla/draftapi/controller/DraftControllerTest.scala @@ -27,6 +27,7 @@ import org.mockito.Mockito.{reset, times, verify, when} import sttp.client3.quick.* import scala.util.{Failure, Success} +import no.ndla.language.Language.findByLanguageOrBestEffort class DraftControllerTest extends UnitSuite with TestEnvironment with TapirControllerTest { override val controller: DraftController = new DraftController @@ -78,7 +79,13 @@ class DraftControllerTest extends UnitSuite with TestEnvironment with TapirContr test("That GET /licenses/ with filter sat to by only returns creative common licenses") { val creativeCommonlicenses = getLicenses .filter(_.license.toString.startsWith("by")) - .map(l => commonApi.LicenseDTO(l.license.toString, Option(l.description), l.url)) + .map(l => + commonApi.LicenseDTO( + l.license.toString, + Option(l.description), + findByLanguageOrBestEffort(l.url, "en").map(_.url), + ) + ) .toSet val resp = @@ -89,8 +96,16 @@ class DraftControllerTest extends UnitSuite with TestEnvironment with TapirContr } test("That GET /licenses/ with filter not specified returns all licenses") { - val allLicenses = getLicenses.map(l => commonApi.LicenseDTO(l.license.toString, Option(l.description), l.url)).toSet - val resp = simpleHttpClient.send(quickRequest.get(uri"http://localhost:$serverPort/draft-api/v1/drafts/licenses")) + val allLicenses = getLicenses + .map(l => + commonApi.LicenseDTO( + l.license.toString, + Option(l.description), + findByLanguageOrBestEffort(l.url, "en").map(_.url), + ) + ) + .toSet + val resp = simpleHttpClient.send(quickRequest.get(uri"http://localhost:$serverPort/draft-api/v1/drafts/licenses")) resp.code.code should be(200) val convertedBody = CirceUtil.unsafeParseAs[Set[commonApi.LicenseDTO]](resp.body) convertedBody should equal(allLicenses) diff --git a/draft-api/src/test/scala/no/ndla/draftapi/service/ConverterServiceTest.scala b/draft-api/src/test/scala/no/ndla/draftapi/service/ConverterServiceTest.scala index d30244326d..f4d77ecc25 100644 --- a/draft-api/src/test/scala/no/ndla/draftapi/service/ConverterServiceTest.scala +++ b/draft-api/src/test/scala/no/ndla/draftapi/service/ConverterServiceTest.scala @@ -38,11 +38,11 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { val service: ConverterService = new ConverterService test("toApiLicense defaults to unknown if the license was not found") { - service.toApiLicense("invalid") should equal(commonApi.LicenseDTO("unknown", None, None)) + service.toApiLicense("invalid", "en") should equal(commonApi.LicenseDTO("unknown", None, None)) } test("toApiLicense converts a short license string to a license object with description and url") { - service.toApiLicense(CC_BY.toString) should equal( + service.toApiLicense(CC_BY.toString, "en") should equal( commonApi.LicenseDTO( CC_BY.toString, Some("Creative Commons Attribution 4.0 International"), diff --git a/image-api/src/main/scala/no/ndla/imageapi/service/ConverterService.scala b/image-api/src/main/scala/no/ndla/imageapi/service/ConverterService.scala index 90b1c81beb..aa14183f11 100644 --- a/image-api/src/main/scala/no/ndla/imageapi/service/ConverterService.scala +++ b/image-api/src/main/scala/no/ndla/imageapi/service/ConverterService.scala @@ -37,9 +37,9 @@ class ConverterService(using clock: Clock, props: Props) extends StrictLogging { commonApi.AuthorDTO(domainAuthor.`type`, domainAuthor.name) } - def asApiCopyright(domainCopyright: commonDomain.article.Copyright): commonApi.CopyrightDTO = { + def asApiCopyright(domainCopyright: commonDomain.article.Copyright, language: String): commonApi.CopyrightDTO = { commonApi.CopyrightDTO( - asApiLicense(domainCopyright.license), + asApiLicense(domainCopyright.license, language), domainCopyright.origin, domainCopyright.creators.map(asApiAuthor), domainCopyright.processors.map(asApiAuthor), @@ -112,7 +112,7 @@ class ConverterService(using clock: Clock, props: Props) extends StrictLogging { metaUrl = metaUrl, title = title, alttext = alttext, - copyright = asApiCopyright(imageMeta.copyright), + copyright = asApiCopyright(imageMeta.copyright, language.getOrElse(props.DefaultLanguage)), tags = tags, caption = caption, supportedLanguages = supportedLanguages, @@ -199,7 +199,7 @@ class ConverterService(using clock: Clock, props: Props) extends StrictLogging { imageUrl = apiUrl, size = image.size, contentType = image.contentType, - copyright = asApiCopyright(imageMeta.copyright), + copyright = asApiCopyright(imageMeta.copyright, language.getOrElse(props.DefaultLanguage)), tags = tags, caption = caption, supportedLanguages = supportedLanguages, @@ -224,9 +224,15 @@ class ConverterService(using clock: Clock, props: Props) extends StrictLogging { api.ImageTitleDTO(domainImageTitle.title, domainImageTitle.language) } - def asApiLicense(license: String): commonApi.LicenseDTO = { + def asApiLicense(license: String, language: String): commonApi.LicenseDTO = { getLicense(license) - .map(l => commonApi.LicenseDTO(l.license.toString, Some(l.description), l.url)) + .map(l => + commonApi.LicenseDTO( + l.license.toString, + Some(l.description), + findByLanguageOrBestEffort(l.url, language).map(_.url), + ) + ) .getOrElse(commonApi.LicenseDTO("unknown", None, None)) } diff --git a/image-api/src/test/scala/no/ndla/imageapi/controller/InternControllerTest.scala b/image-api/src/test/scala/no/ndla/imageapi/controller/InternControllerTest.scala index 4109bd73d2..198ed1cfac 100644 --- a/image-api/src/test/scala/no/ndla/imageapi/controller/InternControllerTest.scala +++ b/image-api/src/test/scala/no/ndla/imageapi/controller/InternControllerTest.scala @@ -47,7 +47,7 @@ class InternControllerTest extends UnitSuite with TestEnvironment with TapirCont 0, "", commonApi.CopyrightDTO( - commonApi.LicenseDTO(BySa.license.toString, Some(BySa.description), BySa.url), + commonApi.LicenseDTO(BySa.license.toString, Some(BySa.description), BySa.url.headOption.map(_.url)), None, List(), List(), diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/LearningpathControllerV2.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/LearningpathControllerV2.scala index cc9fa5d247..8f4544d4e0 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/LearningpathControllerV2.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/LearningpathControllerV2.scala @@ -32,6 +32,7 @@ import sttp.tapir.generic.auto.* import sttp.tapir.server.ServerEndpoint import scala.util.{Failure, Success, Try} +import no.ndla.language.Language.findByLanguageOrBestEffort class LearningpathControllerV2(using readService: ReadService, @@ -406,14 +407,23 @@ class LearningpathControllerV2(using .description("Shows all valid licenses") .in("licenses") .in(licenseFilter) + .in(language) .out(jsonBody[Seq[LicenseDTO]]) .errorOut(errorOutputsFor(401, 403, 404)) - .serverLogicPure { license => + .serverLogicPure { case (license, language) => val licenses: Seq[LicenseDefinition] = license match { case None => mapping.License.getLicenses case Some(filter) => mapping.License.getLicenses.filter(_.license.toString.contains(filter)) } - licenses.map(x => LicenseDTO(x.license.toString, Option(x.description), x.url)).asRight + licenses + .map(x => + LicenseDTO( + x.license.toString, + Option(x.description), + findByLanguageOrBestEffort(x.url, language.code).map(_.url), + ) + ) + .asRight } private def addLearningpath(): ServerEndpoint[Any, Eff] = endpoint diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/service/ConverterService.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/service/ConverterService.scala index 5076c9bb99..4b6598ad28 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/service/ConverterService.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/service/ConverterService.scala @@ -73,13 +73,17 @@ class ConverterService(using api.LearningPathTagsDTO(tags.tags, tags.language) } - def asApiCopyright(copyright: learningpath.LearningpathCopyright): api.CopyrightDTO = { - api.CopyrightDTO(asApiLicense(copyright.license), copyright.contributors.map(_.toApi)) + def asApiCopyright(copyright: learningpath.LearningpathCopyright, language: String): api.CopyrightDTO = { + api.CopyrightDTO(asApiLicense(copyright.license, language), copyright.contributors.map(_.toApi)) } - def asApiLicense(license: String): commonApi.LicenseDTO = getLicense(license) match { - case Some(l) => commonApi.LicenseDTO(l.license.toString, Option(l.description), l.url) - case None => commonApi.LicenseDTO(license, Some("Invalid license"), None) + def asApiLicense(license: String, language: String): commonApi.LicenseDTO = getLicense(license) match { + case Some(l) => commonApi.LicenseDTO( + l.license.toString, + Option(l.description), + findByLanguageOrBestEffort(l.url, language).map(_.url), + ) + case None => commonApi.LicenseDTO(license, Some("Invalid license"), None) } def asAuthor(user: domain.NdlaUserName): commonApi.AuthorDTO = { @@ -164,7 +168,7 @@ class ConverterService(using created = lp.created, lastUpdated = lp.lastUpdated, tags = tags, - copyright = asApiCopyright(lp.copyright), + copyright = asApiCopyright(lp.copyright, language), canEdit = lp.canEditPath(userInfo), supportedLanguages = supportedLanguages, ownerId = owner, @@ -530,7 +534,7 @@ class ConverterService(using val description = newLearningPath.description.map(Description(_, newLearningPath.language)).toSeq val introduction = newLearningPath.introduction.map(Introduction(_, newLearningPath.language)).toSeq - val copyright = newLearningPath.copyright.getOrElse(newDefaultCopyright(user)) + val copyright = newLearningPath.copyright.getOrElse(newDefaultCopyright(user, newLearningPath.language)) val priority = newLearningPath.priority.getOrElse(common.Priority.Unspecified) @@ -579,13 +583,13 @@ class ConverterService(using } } - private def newDefaultCopyright(user: CombinedUser): CopyrightDTO = { + private def newDefaultCopyright(user: CombinedUser, language: String): CopyrightDTO = { val contributors = user .myndlaUser .map(_.displayName) .map(name => Seq(commonApi.AuthorDTO(ContributorType.Writer, name))) .getOrElse(Seq.empty) - CopyrightDTO(asApiLicense(License.CC_BY.toString), contributors) + CopyrightDTO(asApiLicense(License.CC_BY.toString, language), contributors) } def getApiIntroduction(learningSteps: Seq[LearningStep]): Seq[api.IntroductionDTO] = { @@ -634,7 +638,7 @@ class ConverterService(using learningpath.created, learningpath.lastUpdated, tags, - asApiCopyright(learningpath.copyright), + asApiCopyright(learningpath.copyright, AllLanguages), supportedLanguages, learningpath.isBasedOn, message, @@ -666,7 +670,7 @@ class ConverterService(using val description = findByLanguageOrBestEffort(ls.description, language).map(asApiDescription) val embedUrl = findByLanguageOrBestEffort(ls.embedUrl, language).map(asApiEmbedUrlV2).map(createEmbedUrl) - val copyright = ls.copyright.map(asApiCopyright) + val copyright = ls.copyright.map(copyright => asApiCopyright(copyright, language)) Success( api.LearningStepV2DTO( diff --git a/mapping/package.mill b/mapping/package.mill index 29d28a03a3..f8529a3d3d 100644 --- a/mapping/package.mill +++ b/mapping/package.mill @@ -4,8 +4,9 @@ import mill._, mill.scalalib._ import build.modules.{BaseModule, SharedDependencies} object `package` extends BaseModule { - override def moduleName = "mapping" - override def isComponent: Boolean = false - def mvnDeps: T[Seq[Dep]] = SharedDependencies.logging + override def moduleName = "mapping" + override def isComponent: Boolean = false + override def moduleDeps: Seq[JavaModule] = Seq(build.language) + def mvnDeps: T[Seq[Dep]] = SharedDependencies.logging object test extends TestBase } diff --git a/mapping/src/main/scala/no/ndla/mapping/License.scala b/mapping/src/main/scala/no/ndla/mapping/License.scala index 18396574ad..9de2e70bb4 100644 --- a/mapping/src/main/scala/no/ndla/mapping/License.scala +++ b/mapping/src/main/scala/no/ndla/mapping/License.scala @@ -7,6 +7,7 @@ */ package no.ndla.mapping +import no.ndla.language.model.LanguageField object License extends Enumeration { val CC0: Value = Value("CC0-1.0") @@ -24,41 +25,85 @@ object License extends Enumeration { LicenseDefinition( CC0, "Creative Commons Zero", - Some("https://creativecommons.org/publicdomain/zero/1.0/legalcode"), + Seq( + LicenseUrl("https://creativecommons.org/publicdomain/zero/1.0", "en"), + LicenseUrl("https://creativecommons.org/publicdomain/zero/1.0/deed.no", "nb"), + LicenseUrl("https://creativecommons.org/publicdomain/zero/1.0/deed.no", "nn"), + ), + ), + LicenseDefinition( + PublicDomain, + "Public Domain Mark", + Seq( + LicenseUrl("https://creativecommons.org/publicdomain/mark/1.0", "en"), + LicenseUrl("https://creativecommons.org/publicdomain/mark/1.0/deed.no", "nb"), + LicenseUrl("https://creativecommons.org/publicdomain/mark/1.0/deed.no", "nn"), + ), + ), + LicenseDefinition( + Copyrighted, + "Copyrighted", + Seq( + LicenseUrl("https://ndla.no/en/article/opphavsrett", "en"), + LicenseUrl("https://ndla.no/nb/article/opphavsrett", "nb"), + LicenseUrl("https://ndla.no/nn/article/opphavsrett", "nn"), + ), ), - LicenseDefinition(PublicDomain, "Public Domain Mark", Some("https://creativecommons.org/about/pdm")), - LicenseDefinition(Copyrighted, "Copyrighted", None), LicenseDefinition( CC_BY, "Creative Commons Attribution 4.0 International", - Some("https://creativecommons.org/licenses/by/4.0/"), + Seq( + LicenseUrl("https://creativecommons.org/licenses/by/4.0", "en"), + LicenseUrl("https://creativecommons.org/licenses/by/4.0/deed.no", "nb"), + LicenseUrl("https://creativecommons.org/licenses/by/4.0/deed.no", "nn"), + ), ), LicenseDefinition( CC_BY_SA, "Creative Commons Attribution-ShareAlike 4.0 International", - Some("https://creativecommons.org/licenses/by-sa/4.0/"), + Seq( + LicenseUrl("https://creativecommons.org/licenses/by-sa/4.0", "en"), + LicenseUrl("https://creativecommons.org/licenses/by-sa/4.0/deed.no", "nb"), + LicenseUrl("https://creativecommons.org/licenses/by-sa/4.0/deed.no", "nn"), + ), ), LicenseDefinition( CC_BY_NC, "Creative Commons Attribution-NonCommercial 4.0 International", - Some("https://creativecommons.org/licenses/by-nc/4.0/"), + Seq( + LicenseUrl("https://creativecommons.org/licenses/by-nc/4.0", "en"), + LicenseUrl("https://creativecommons.org/licenses/by-nc/4.0/deed.no", "nb"), + LicenseUrl("https://creativecommons.org/licenses/by-nc/4.0/deed.no", "nn"), + ), ), LicenseDefinition( CC_BY_ND, "Creative Commons Attribution-NoDerivs 4.0 International", - Some("https://creativecommons.org/licenses/by-nd/4.0/"), + Seq( + LicenseUrl("https://creativecommons.org/licenses/by-nd/4.0", "en"), + LicenseUrl("https://creativecommons.org/licenses/by-nd/4.0/deed.no", "nb"), + LicenseUrl("https://creativecommons.org/licenses/by-nd/4.0/deed.no", "nn"), + ), ), LicenseDefinition( CC_BY_NC_SA, "Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International", - Some("https://creativecommons.org/licenses/by-nc-sa/4.0/"), + Seq( + LicenseUrl("https://creativecommons.org/licenses/by-nc-sa/4.0", "en"), + LicenseUrl("https://creativecommons.org/licenses/by-nc-sa/4.0/deed.no", "nb"), + LicenseUrl("https://creativecommons.org/licenses/by-nc-sa/4.0/deed.no", "nn"), + ), ), LicenseDefinition( CC_BY_NC_ND, "Creative Commons Attribution-NonCommercial-NoDerivs 4.0 International", - Some("https://creativecommons.org/licenses/by-nc-nd/4.0/"), + Seq( + LicenseUrl("https://creativecommons.org/licenses/by-nc-nd/4.0", "en"), + LicenseUrl("https://creativecommons.org/licenses/by-nc-nd/4.0/deed.no", "nb"), + LicenseUrl("https://creativecommons.org/licenses/by-nc-nd/4.0/deed.no", "nn"), + ), ), - LicenseDefinition(NA, "Not Applicable", None), + LicenseDefinition(NA, "Not Applicable", Seq.empty), ) private val licenseToLicenseDefinitionsMap = licenseToLicenseDefinitionsSeq.map(x => x.license.toString -> x).toMap @@ -68,4 +113,9 @@ object License extends Enumeration { def getLicenses: Seq[LicenseDefinition] = licenseToLicenseDefinitionsSeq } -case class LicenseDefinition(license: License.Value, description: String, url: Option[String]) +case class LicenseDefinition(license: License.Value, description: String, url: Seq[LicenseUrl]) + +case class LicenseUrl(url: String, language: String) extends LanguageField[String] { + override def value: String = url + override def isEmpty: Boolean = url.isEmpty +} diff --git a/mapping/src/test/scala/no/ndla/mapping/LicenseTest.scala b/mapping/src/test/scala/no/ndla/mapping/LicenseTest.scala index 04373ca40c..0012a71d5e 100644 --- a/mapping/src/test/scala/no/ndla/mapping/LicenseTest.scala +++ b/mapping/src/test/scala/no/ndla/mapping/LicenseTest.scala @@ -14,7 +14,7 @@ class LicenseTest extends UnitSuite { LicenseDefinition( License.CC_BY, "Creative Commons Attribution 4.0 International", - Some("https://creativecommons.org/licenses/by/4.0/"), + Seq(LicenseUrl("https://creativecommons.org/licenses/by/4.0/", "en")), ) ) License.getLicense("CC-BY-4.0") should equal(expectedResult) @@ -28,7 +28,7 @@ class LicenseTest extends UnitSuite { val byLicense = LicenseDefinition( License.CC_BY, "Creative Commons Attribution 4.0 International", - Some("https://creativecommons.org/licenses/by/4.0/"), + Seq(LicenseUrl("https://creativecommons.org/licenses/by/4.0/", "en")), ) License.getLicenses.size should equal(10) @@ -36,7 +36,7 @@ class LicenseTest extends UnitSuite { } test("getLicense returns a NA license") { - val expectedResult = Some(LicenseDefinition(License.NA, "Not Applicable", None)) + val expectedResult = Some(LicenseDefinition(License.NA, "Not Applicable", Seq.empty)) License.getLicense("N/A") should equal(expectedResult) } } diff --git a/search-api/src/main/scala/no/ndla/searchapi/service/search/SearchConverterService.scala b/search-api/src/main/scala/no/ndla/searchapi/service/search/SearchConverterService.scala index b90ad1fb82..5e3056659f 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/service/search/SearchConverterService.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/service/search/SearchConverterService.scala @@ -589,10 +589,11 @@ class SearchConverterService(using } } - private def asLearningPathApiLicense(license: String): LicenseDTO = { + private def asLearningPathApiLicense(license: String, language: String): LicenseDTO = { getLicense(license) match { - case Some(l) => LicenseDTO(l.license.toString, Option(l.description), l.url) - case None => LicenseDTO(license, Some("Invalid license"), None) + case Some(l) => + LicenseDTO(l.license.toString, Option(l.description), findByLanguageOrBestEffort(l.url, language).map(_.url)) + case None => LicenseDTO(license, Some("Invalid license"), None) } }