diff --git a/common/src/main/scala/no/ndla/common/model/domain/frontpage/MovieTheme.scala b/common/src/main/scala/no/ndla/common/model/domain/frontpage/MovieTheme.scala index a4927e485..d7e2103e9 100644 --- a/common/src/main/scala/no/ndla/common/model/domain/frontpage/MovieTheme.scala +++ b/common/src/main/scala/no/ndla/common/model/domain/frontpage/MovieTheme.scala @@ -8,5 +8,10 @@ package no.ndla.common.model.domain.frontpage +import no.ndla.language.model.LanguageField + case class MovieTheme(name: Seq[MovieThemeName], movies: Seq[String]) -case class MovieThemeName(name: String, language: String) +case class MovieThemeName(name: String, language: String) extends LanguageField[String] { + override def value: String = name + override def isEmpty: Boolean = name.isEmpty +} diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/controller/FilmPageController.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/controller/FilmPageController.scala index 1fe3b6df1..f381c6921 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/controller/FilmPageController.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/controller/FilmPageController.scala @@ -27,6 +27,7 @@ trait FilmPageController { class FilmPageController extends TapirController { override val serviceName: String = "filmfrontpage" override val prefix: EndpointInput[Unit] = "frontpage-api" / "v1" / serviceName + private val pathLanguage = path[String]("language").description("The ISO 639-1 language code describing language.") override val endpoints: List[ServerEndpoint[Any, Eff]] = List( endpoint.get .summary("Get data to display on the film front page") @@ -49,6 +50,16 @@ trait FilmPageController { writeService.updateFilmFrontPage(filmFrontPage).partialOverride { case ex: ValidationException => ErrorHelpers.unprocessableEntity(ex.getMessage) } + }, + endpoint.delete + .in("language" / pathLanguage) + .summary("Delete language from film front page") + .description("Delete language from film front page") + .out(jsonBody[FilmFrontPageDTO]) + .errorOut(errorOutputsFor(400, 401, 403, 404)) + .requirePermission(FRONTPAGE_API_WRITE) + .serverLogicPure { _ => language => + writeService.deleteFilmFrontPageLanguage(language) } ) } diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/controller/SubjectPageController.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/controller/SubjectPageController.scala index 347669192..e69694db1 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/controller/SubjectPageController.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/controller/SubjectPageController.scala @@ -30,6 +30,8 @@ trait SubjectPageController { class SubjectPageController extends TapirController { override val serviceName: String = "subjectpage" override val prefix: EndpointInput[Unit] = "frontpage-api" / "v1" / serviceName + private val pathSubjectPageId = path[Long]("subjectpage-id").description("The subjectpage id") + private val pathLanguage = path[String]("language").description("The ISO 639-1 language code describing language.") def getAllSubjectPages: ServerEndpoint[Any, Eff] = endpoint.get .summary("Fetch all subjectpages") @@ -45,7 +47,7 @@ trait SubjectPageController { def getSingleSubjectPage: ServerEndpoint[Any, Eff] = endpoint.get .summary("Get data to display on a subject page") - .in(path[Long]("subjectpage-id").description("The subjectpage id")) + .in(pathSubjectPageId) .in(query[String]("language").default(props.DefaultLanguage)) .in(query[Boolean]("fallback").default(false)) .out(jsonBody[SubjectPageDTO]) @@ -89,7 +91,7 @@ trait SubjectPageController { def updateSubjectPage: ServerEndpoint[Any, Eff] = endpoint.patch .summary("Update subject page") .in(jsonBody[UpdatedSubjectPageDTO]) - .in(path[Long]("subjectpage-id").description("The subjectpage id")) + .in(pathSubjectPageId) .in(query[String]("language").default(props.DefaultLanguage)) .in(query[Boolean]("fallback").default(false)) .out(jsonBody[SubjectPageDTO]) @@ -105,12 +107,26 @@ trait SubjectPageController { } } + def deleteLanguage: ServerEndpoint[Any, Eff] = endpoint.delete + .in(pathSubjectPageId / "language" / pathLanguage) + .summary("Delete language from subject page") + .description("Delete language from subject page") + .out(jsonBody[SubjectPageDTO]) + .errorOut(errorOutputsFor(400, 401, 403, 404)) + .requirePermission(FRONTPAGE_API_WRITE) + .serverLogicPure { _ => + { case (articleId, language) => + writeService.deleteSubjectPageLanguage(articleId, language) + } + } + override val endpoints: List[ServerEndpoint[Any, Eff]] = List( getAllSubjectPages, getSubjectPagesByIds, getSingleSubjectPage, createNewSubjectPage, - updateSubjectPage + updateSubjectPage, + deleteLanguage ) } } diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/FilmFrontPageDTO.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/FilmFrontPageDTO.scala index 9e90226fe..7e594004e 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/FilmFrontPageDTO.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/FilmFrontPageDTO.scala @@ -13,5 +13,6 @@ case class FilmFrontPageDTO( about: Seq[AboutFilmSubjectDTO], movieThemes: Seq[MovieThemeDTO], slideShow: Seq[String], - article: Option[String] + article: Option[String], + supportedLanguages: Seq[String] ) diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/FilmFrontPage.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/FilmFrontPage.scala index 6692bd5db..2aedcd6c6 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/FilmFrontPage.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/FilmFrontPage.scala @@ -15,6 +15,7 @@ import io.circe.parser.* import io.circe.{Decoder, Encoder} import no.ndla.common.model.domain.frontpage.{AboutSubject, MovieTheme} import no.ndla.frontpageapi.Props +import no.ndla.language.Language.getSupportedLanguages import scalikejdbc.{WrappedResultSet, *} import scala.util.Try @@ -25,7 +26,10 @@ case class FilmFrontPage( movieThemes: Seq[MovieTheme], slideShow: Seq[String], article: Option[String] -) +) { + + def supportedLanguages: Seq[String] = getSupportedLanguages(about, movieThemes.flatMap(_.name)) +} object FilmFrontPage { implicit val decoder: Decoder[FilmFrontPage] = deriveDecoder diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/repository/FilmFrontPageRepository.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/repository/FilmFrontPageRepository.scala index 9a7dab705..c8d837dfb 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/repository/FilmFrontPageRepository.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/repository/FilmFrontPageRepository.scala @@ -63,6 +63,16 @@ trait FilmFrontPageRepository { } + def update(page: FilmFrontPage)(implicit session: DBSession = AutoSession): Try[FilmFrontPage] = { + val dataObject = new PGobject() + dataObject.setType("jsonb") + dataObject.setValue(page.asJson.noSpacesDropNull) + + Try( + sql"update ${DBFilmFrontPageData.table} set document=$dataObject".update() + ).map(_ => page) + } + } } diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/service/ConverterService.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/service/ConverterService.scala index a0a1256fa..30e019d45 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/service/ConverterService.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/service/ConverterService.scala @@ -90,7 +90,8 @@ trait ConverterService { toApiAboutFilmSubject(page.about, language), toApiMovieThemes(page.movieThemes, language), page.slideShow, - page.article + page.article, + page.supportedLanguages ) } diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/service/WriteService.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/service/WriteService.scala index 592e4d1cc..37746921c 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/service/WriteService.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/service/WriteService.scala @@ -8,13 +8,14 @@ package no.ndla.frontpageapi.service -import no.ndla.common.errors.ValidationException +import no.ndla.common.errors.{NotFoundException, ValidationException} import no.ndla.common.model.api.FrontPageDTO import no.ndla.common.model.api.frontpage.SubjectPageDTO import no.ndla.frontpageapi.Props import no.ndla.frontpageapi.model.api -import no.ndla.frontpageapi.model.domain.Errors.SubjectPageNotFoundException +import no.ndla.frontpageapi.model.domain.Errors.{OperationNotAllowedException, SubjectPageNotFoundException} import no.ndla.frontpageapi.repository.{FilmFrontPageRepository, FrontPageRepository, SubjectPageRepository} +import no.ndla.language.Language import scala.util.{Failure, Success, Try} @@ -109,6 +110,41 @@ trait WriteService { filmFrontPage <- filmFrontPageRepository.newFilmFrontPage(domainFilmFrontPage) } yield ConverterService.toApiFilmFrontPage(filmFrontPage, None) } + + def deleteSubjectPageLanguage(id: Long, language: String): Try[SubjectPageDTO] = { + subjectPageRepository.withId(id) match { + case Success(Some(subjectPage)) => + subjectPage.supportedLanguages.size match { + case 1 => Failure(OperationNotAllowedException("Only one language left")) + case _ => + val about = subjectPage.about.filter(_.language != language) + val metaDescription = subjectPage.metaDescription.filter(_.language != language) + subjectPageRepository + .updateSubjectPage(subjectPage.copy(about = about, metaDescription = metaDescription)) + .flatMap(ConverterService.toApiSubjectPage(_, Language.NoLanguage, fallback = true)) + } + case Success(None) => Failure(SubjectPageNotFoundException(id)) + case Failure(ex) => Failure(ex) + } + } + + def deleteFilmFrontPageLanguage(language: String): Try[api.FilmFrontPageDTO] = { + filmFrontPageRepository.get match { + case Some(page) => + page.supportedLanguages.size match { + case 1 => Failure(OperationNotAllowedException("Only one language left")) + case _ => + val about = page.about.filter(_.language != language) + val movieThemes = page.movieThemes.map(movieTheme => + movieTheme.copy(name = movieTheme.name.filter(_.language != language)) + ) + filmFrontPageRepository + .update(page.copy(about = about, movieThemes = movieThemes)) + .map(ConverterService.toApiFilmFrontPage(_, None)) + } + case None => Failure(NotFoundException("The film front page was not found")) + } + } } } diff --git a/frontpage-api/src/test/scala/no/ndla/frontpageapi/TestData.scala b/frontpage-api/src/test/scala/no/ndla/frontpageapi/TestData.scala index 84201014d..c6b777269 100644 --- a/frontpage-api/src/test/scala/no/ndla/frontpageapi/TestData.scala +++ b/frontpage-api/src/test/scala/no/ndla/frontpageapi/TestData.scala @@ -163,5 +163,5 @@ object TestData { None ) - val apiFilmFrontPage: api.FilmFrontPageDTO = api.FilmFrontPageDTO("", Seq(), Seq(), Seq(), None) + val apiFilmFrontPage: api.FilmFrontPageDTO = api.FilmFrontPageDTO("", Seq(), Seq(), Seq(), None, Seq()) } diff --git a/frontpage-api/src/test/scala/no/ndla/frontpageapi/service/WriteServiceTest.scala b/frontpage-api/src/test/scala/no/ndla/frontpageapi/service/WriteServiceTest.scala new file mode 100644 index 000000000..ea5b8dc05 --- /dev/null +++ b/frontpage-api/src/test/scala/no/ndla/frontpageapi/service/WriteServiceTest.scala @@ -0,0 +1,78 @@ +/* + * Part of NDLA frontpage-api + * Copyright (C) 2025 NDLA + * + * See LICENSE + * + */ + +package no.ndla.frontpageapi.service + +import no.ndla.common.model.domain.frontpage.VisualElementType.Image +import no.ndla.common.model.domain.frontpage.{AboutSubject, MetaDescription, MovieThemeName, VisualElement} +import no.ndla.frontpageapi.{TestData, TestEnvironment, UnitSuite} +import no.ndla.language.Language +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.when + +import scala.util.Success + +class WriteServiceTest extends UnitSuite with TestEnvironment { + override val writeService: WriteService = new WriteService + + test("That language is deleted for subject page") { + val subjectPage = TestData.domainSubjectPage.copy( + about = + TestData.domainSubjectPage.about ++ Seq(AboutSubject("Foo", "Bar", "nn", VisualElement(Image, "123", None))), + metaDescription = TestData.domainSubjectPage.metaDescription ++ Seq(MetaDescription("Description", "nn")) + ) + when(subjectPageRepository.withId(any)).thenReturn(Success(Some(subjectPage))) + when(subjectPageRepository.updateSubjectPage(any)(any)).thenAnswer(i => Success(i.getArgument(0))) + + val result = writeService.deleteSubjectPageLanguage(subjectPage.id.get, "nn") + result should be( + Success( + ConverterService + .toApiSubjectPage(TestData.domainSubjectPage, Language.NoLanguage, fallback = true) + .failIfFailure + ) + ) + } + + test("That deleting last language for subject page throws exception") { + when(subjectPageRepository.withId(any)).thenReturn(Success(Some(TestData.domainSubjectPage))) + when(subjectPageRepository.updateSubjectPage(any)(any)).thenAnswer(i => Success(i.getArgument(0))) + + val result = writeService.deleteSubjectPageLanguage(TestData.domainSubjectPage.id.get, "nb") + result.isFailure should be(true) + } + + test("That language is deleted for film front page") { + val filmFrontPage = TestData.domainFilmFrontPage.copy( + about = + TestData.domainFilmFrontPage.about ++ Seq(AboutSubject("Foo", "Bar", "nn", VisualElement(Image, "123", None))), + movieThemes = TestData.domainFilmFrontPage.movieThemes.map(movieTheme => + movieTheme.copy(name = movieTheme.name ++ Seq(MovieThemeName("FooBar", "nn"))) + ) + ) + when(filmFrontPageRepository.get(any)).thenReturn(Some(filmFrontPage)) + when(filmFrontPageRepository.update(any)(any)).thenAnswer(i => Success(i.getArgument(0))) + + val result = writeService.deleteFilmFrontPageLanguage("nn") + result should be(Success(ConverterService.toApiFilmFrontPage(TestData.domainFilmFrontPage, None))) + } + + test("That deleting last language for film front page throws exception") { + val filmFrontPage = TestData.domainFilmFrontPage.copy( + about = TestData.domainFilmFrontPage.about.filter(_.language == "nb"), + movieThemes = TestData.domainFilmFrontPage.movieThemes.map(movieTheme => + movieTheme.copy(name = movieTheme.name.filter(_.language == "nb")) + ) + ) + when(filmFrontPageRepository.get(any)).thenReturn(Some(filmFrontPage)) + when(filmFrontPageRepository.update(any)(any)).thenAnswer(i => Success(i.getArgument(0))) + + val result = writeService.deleteFilmFrontPageLanguage("nb") + result.isFailure should be(true) + } +}