Skip to content

Commit

Permalink
Merge pull request #614 from NDLANO/author-type-enum
Browse files Browse the repository at this point in the history
Author type enum
  • Loading branch information
gunnarvelle authored Feb 28, 2025
2 parents edeb6a8 + 1f468c3 commit 4fcd1f9
Show file tree
Hide file tree
Showing 79 changed files with 2,509 additions and 375 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,42 +45,6 @@ class ArticleApiProperties extends BaseProps with DatabaseProps {
def RedisHost: String = propOrElse("REDIS_HOST", "redis")
def RedisPort: Int = propOrElse("REDIS_PORT", "6379").toInt

def oldCreatorTypes: List[String] = List(
"opphavsmann",
"fotograf",
"kunstner",
"forfatter",
"manusforfatter",
"innleser",
"oversetter",
"regissør",
"illustratør",
"medforfatter",
"komponist"
)

def creatorTypes: List[String] = List(
"originator",
"photographer",
"artist",
"writer",
"scriptwriter",
"reader",
"translator",
"director",
"illustrator",
"cowriter",
"composer"
)

def oldProcessorTypes: List[String] =
List("bearbeider", "tilrettelegger", "redaksjonelt", "språklig", "ide", "sammenstiller", "korrektur")
def processorTypes: List[String] =
List("processor", "facilitator", "editorial", "linguistic", "idea", "compiler", "correction")

def oldRightsholderTypes: List[String] = List("rettighetshaver", "forlag", "distributør", "leverandør")
def rightsholderTypes: List[String] = List("rightsholder", "publisher", "distributor", "supplier")

// When converting a content node, the converter may run several times over the content to make sure
// everything is converted. This value defines a maximum number of times the converter runs on a node
def maxConvertionRounds = 5
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Part of NDLA article-api
* Copyright (C) 2025 NDLA
*
* See LICENSE
*
*/

package no.ndla.articleapi.db.migration

import io.circe.{ACursor, parser}
import io.circe.generic.auto.*
import io.circe.syntax.EncoderOps
import no.ndla.common.model.domain.{Author, ContributorType}
import no.ndla.database.DocumentMigration

class V58__FixContributorTypes extends DocumentMigration {
override val tableName: String = "contentdata"
override val columnName: String = "document"

override def convertColumn(value: String): String = {
val oldDocument = parser.parse(value).toTry.get
val copyright = oldDocument.hcursor.downField("copyright")
val creators = convertList(copyright, "creators")
val processors = convertList(copyright, "processors")
val rightsholders = convertList(copyright, "rightsholders")

val newDocument = oldDocument.hcursor
.downField("copyright")
.withFocus(_.mapObject(_.remove("creators").add("creators", creators.asJson)))
.withFocus(_.mapObject(_.remove("processors").add("processors", processors.asJson)))
.withFocus(_.mapObject(_.remove("rightsholders").add("rightsholders", rightsholders.asJson)))
newDocument.top.get.noSpaces
}

private def convertList(cursor: ACursor, fieldName: String): List[Author] = {
val field = cursor.downField(fieldName)
if (field.succeeded) {
field.as[Option[List[OldAuthor]]].toTry.get match {
case None => List.empty
case Some(authors) => convertAuthors(authors)
}
} else List.empty
}

private def convertAuthors(authors: List[OldAuthor]): List[Author] = {
authors.map(author => {
ContributorType.valueOf(author.`type`.toLowerCase) match {
case None => Author(ContributorType.mapping(author.`type`.toLowerCase), author.name)
case Some(a) => Author(a, author.name)
}
})
}
}

case class OldAuthor(`type`: String, name: String)
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ trait ContentValidator {
val field = s"title.language"
TextValidator.validate(field, title.value, inlineHtmlTags).toList ++
validateLanguage("title.language", title.language) ++
validateLength("title", title.value, 256)
validateLength("title", title.value, 0, 256)
}) ++ validateNonEmpty("title", titles)
}

Expand All @@ -171,9 +171,11 @@ trait ContentValidator {
val allAuthors = copyright.creators ++ copyright.processors ++ copyright.rightsholders
val licenseCorrelationMessage = validateAuthorLicenseCorrelation(copyright.license, allAuthors)
val contributorsMessages =
copyright.creators.flatMap(a => validateAuthor(a, "copyright.creators", props.creatorTypes)) ++
copyright.processors.flatMap(a => validateAuthor(a, "copyright.processors", props.processorTypes)) ++
copyright.rightsholders.flatMap(a => validateAuthor(a, "copyright.rightsholders", props.rightsholderTypes))
copyright.creators.flatMap(a => validateAuthor(a, "copyright.creators", ContributorType.creators)) ++
copyright.processors.flatMap(a => validateAuthor(a, "copyright.processors", ContributorType.processors)) ++
copyright.rightsholders.flatMap(a =>
validateAuthor(a, "copyright.rightsholders", ContributorType.rightsholders)
)
val originMessage =
copyright.origin
.map(origin => TextValidator.validate("copyright.origin", origin, Set.empty))
Expand All @@ -195,18 +197,22 @@ trait ContentValidator {
if (license == "N/A" || authors.nonEmpty) Seq() else Seq(errorMessage(license))
}

private def validateAuthor(author: Author, fieldPath: String, allowedTypes: Seq[String]): Seq[ValidationMessage] = {
TextValidator.validate(s"$fieldPath.type", author.`type`, Set.empty).toList ++
TextValidator.validate(s"$fieldPath.name", author.name, Set.empty).toList ++
validateAuthorType(s"$fieldPath.type", author.`type`, allowedTypes).toList
private def validateAuthor(
author: Author,
fieldPath: String,
allowedTypes: Seq[ContributorType]
): Seq[ValidationMessage] = {
TextValidator.validate(s"$fieldPath.name", author.name, Set.empty).toList ++
validateAuthorType(s"$fieldPath.type", author.`type`, allowedTypes).toList ++
validateLength(s"$fieldPath.name", author.name, 1, 256)
}

private def validateAuthorType(
fieldPath: String,
`type`: String,
allowedTypes: Seq[String]
`type`: ContributorType,
allowedTypes: Seq[ContributorType]
): Option[ValidationMessage] = {
if (allowedTypes.contains(`type`.toLowerCase)) {
if (allowedTypes.contains(`type`)) {
None
} else {
Some(ValidationMessage(fieldPath, s"Author is of illegal type. Must be one of ${allowedTypes.mkString(", ")}"))
Expand Down Expand Up @@ -273,9 +279,16 @@ trait ContentValidator {
}
}

private def validateLength(fieldPath: String, content: String, maxLength: Int): Option[ValidationMessage] = {
private def validateLength(fieldPath: String, content: String, minLength: Int, maxLength: Int) = {
if (content.length > maxLength)
Some(ValidationMessage(fieldPath, s"This field exceeds the maximum permitted length of $maxLength characters"))
else if (content.length < minLength)
Some(
ValidationMessage(
fieldPath,
s"This field is shorter than the minimum permitted length of $minLength characters"
)
)
else
None
}
Expand Down
6 changes: 3 additions & 3 deletions article-api/src/test/scala/no/ndla/articleapi/TestData.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ trait TestData {
Copyright(
License.CC_BY_NC_SA.toString,
Some("Gotham City"),
List(Author("Writer", "DC Comics")),
List(Author(ContributorType.Writer, "DC Comics")),
List(),
List(),
None,
Expand All @@ -39,7 +39,7 @@ trait TestData {
Copyright(
License.Copyrighted.toString,
Some("New York"),
List(Author("Writer", "Clark Kent")),
List(Author(ContributorType.Writer, "Clark Kent")),
List(),
List(),
None,
Expand All @@ -59,7 +59,7 @@ trait TestData {
copyright = model.api.CopyrightDTO(
model.api.LicenseDTO("licence", None, None),
Some("origin"),
Seq(model.api.AuthorDTO("developer", "Per")),
Seq(model.api.AuthorDTO(ContributorType.Editorial, "Per")),
List(),
List(),
None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ package no.ndla.articleapi.controller

import no.ndla.articleapi.{TestEnvironment, UnitSuite}
import no.ndla.common.model.domain.article.Article
import no.ndla.common.model.domain.Author
import no.ndla.common.model.domain.{Author, ContributorType}
import no.ndla.tapirtesting.TapirControllerTest
import org.mockito.ArgumentMatchers.{any, eq as eqTo}
import org.mockito.Mockito.{doReturn, never, reset, times, verify, verifyNoMoreInteractions, when}
Expand All @@ -20,7 +20,7 @@ import sttp.client3.quick.*
import scala.util.{Failure, Success}

class InternControllerTest extends UnitSuite with TestEnvironment with TapirControllerTest {
val author: Author = Author("forfatter", "Henrik")
val author: Author = Author(ContributorType.Writer, "Henrik")

val controller: TapirController = new InternController

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* Part of NDLA article-api
* Copyright (C) 2024 NDLA
*
* See LICENSE
*
*/

package no.ndla.articleapi.db.migration

import io.circe.parser
import no.ndla.articleapi.{TestEnvironment, UnitSuite}

class V58__FixContributorTypesTest extends UnitSuite with TestEnvironment {
test("That copyright has correct contributor types") {
val migration = new V58__FixContributorTypes
val oldDocument =
"""
|{
| "id": 1,
| "tags": [],
| "title": [
| {
| "title": "Title",
| "language": "nb"
| }
| ],
| "content": [
| {
| "content": "<section>Content</section>",
| "language": "nb"
| }
| ],
| "created": "2017-05-29T09:43:41.000Z",
| "updated": "2017-07-18T10:21:08.000Z",
| "revision": 1,
| "copyright": {
| "origin": "",
| "license": "CC-BY-SA-4.0",
| "validTo": null,
| "processed": false,
| "validFrom": null,
| "creators": [
| {
| "type": "Forfatter",
| "name": "Sissel Paaske"
| }
| ],
| "processors": [],
| "rightsholders": [
| {
| "type": "Supplier",
| "name": "Cerpus AS"
| }
| ]
| },
| "grepCodes": [],
| "metaImage": [],
| "published": "2017-07-18T10:21:08.000Z",
| "updatedBy": "r0gHb9Xg3li4yyXv0QSGQczV3bviakrT",
| "conceptIds": [],
| "disclaimer": {},
| "articleType": "standard",
| "availability": "everyone",
| "introduction": [
| {
| "language": "nb",
| "introduction": "Introduction."
| }
| ],
| "revisionDate": "2030-01-01T00:00:00.000Z",
| "visualElement": [],
| "relatedContent": [],
| "metaDescription": [
| {
| "content": "Metabeskrivelse",
| "language": "nb"
| }
| ],
| "requiredLibraries": []
|}
|""".stripMargin
val newDocument =
"""
|{
| "id": 1,
| "tags": [],
| "title": [
| {
| "title": "Title",
| "language": "nb"
| }
| ],
| "content": [
| {
| "content": "<section>Content</section>",
| "language": "nb"
| }
| ],
| "created": "2017-05-29T09:43:41.000Z",
| "updated": "2017-07-18T10:21:08.000Z",
| "revision": 1,
| "copyright": {
| "origin": "",
| "license": "CC-BY-SA-4.0",
| "validTo": null,
| "processed": false,
| "validFrom": null,
| "creators": [
| {
| "type": "writer",
| "name": "Sissel Paaske"
| }
| ],
| "processors": [],
| "rightsholders": [
| {
| "type": "supplier",
| "name": "Cerpus AS"
| }
| ]
| },
| "grepCodes": [],
| "metaImage": [],
| "published": "2017-07-18T10:21:08.000Z",
| "updatedBy": "r0gHb9Xg3li4yyXv0QSGQczV3bviakrT",
| "conceptIds": [],
| "disclaimer": {},
| "articleType": "standard",
| "availability": "everyone",
| "introduction": [
| {
| "language": "nb",
| "introduction": "Introduction."
| }
| ],
| "revisionDate": "2030-01-01T00:00:00.000Z",
| "visualElement": [],
| "relatedContent": [],
| "metaDescription": [
| {
| "content": "Metabeskrivelse",
| "language": "nb"
| }
| ],
| "requiredLibraries": []
|}
|""".stripMargin

val expected = parser.parse(newDocument).toTry.get
migration.convertColumn(oldDocument) should be(expected.noSpaces)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import no.ndla.common.model.api.{LicenseDTO, UpdateWith}
import no.ndla.common.model.domain.{
Author,
Availability,
ContributorType,
Description,
Introduction,
RequiredLibrary,
Expand All @@ -39,7 +40,7 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment {

val service = new ConverterService
val contentTitle: Title = Title("", "und")
val author: Author = Author("forfatter", "Henrik")
val author: Author = Author(ContributorType.Writer, "Henrik")
val tag: Tag = Tag(List("asdf"), "nb")
val requiredLibrary: RequiredLibrary = RequiredLibrary("", "", "")
val nodeId = "1234"
Expand Down
Loading

0 comments on commit 4fcd1f9

Please sign in to comment.