diff --git a/article-api/src/main/scala/no/ndla/articleapi/db/HtmlMigration.scala b/article-api/src/main/scala/no/ndla/articleapi/db/HtmlMigration.scala index 527cf16112..163fcfc37b 100644 --- a/article-api/src/main/scala/no/ndla/articleapi/db/HtmlMigration.scala +++ b/article-api/src/main/scala/no/ndla/articleapi/db/HtmlMigration.scala @@ -72,10 +72,10 @@ abstract class HtmlMigration extends DocumentMigration { val newArticle = oldArticle.copy( title = convertedTitle, - introduction = convertedIntroduction, content = convertedContent, - metaDescription = convertedMetaDescription, visualElement = convertedVisualElement, + introduction = convertedIntroduction, + metaDescription = convertedMetaDescription, ) newArticle.asJson.noSpaces } diff --git a/article-api/src/main/scala/no/ndla/articleapi/db/migration/V69__SetPublishedCount.scala b/article-api/src/main/scala/no/ndla/articleapi/db/migration/V69__SetPublishedCount.scala new file mode 100644 index 0000000000..c0b983c772 --- /dev/null +++ b/article-api/src/main/scala/no/ndla/articleapi/db/migration/V69__SetPublishedCount.scala @@ -0,0 +1,49 @@ +/* + * Part of NDLA article-api + * Copyright (C) 2026 NDLA + * + * See LICENSE + * + */ + +package no.ndla.articleapi.db.migration + +import io.circe.{Json, parser} +import no.ndla.database.TableMigration +import org.postgresql.util.PGobject +import scalikejdbc.{DBSession, SQLSyntax, WrappedResultSet} +import scalikejdbc.interpolation.Implicits.scalikejdbcSQLInterpolationImplicitDef + +case class DocumentRow(id: Long, revision: Int, articleId: Long, document: String) + +class V69__SetPublishedCount extends TableMigration[DocumentRow] { + val columnName: String = "document" + override val tableName: String = "contentdata" + + private lazy val columnNameSQL: SQLSyntax = SQLSyntax.createUnsafely(columnName) + override lazy val whereClause: SQLSyntax = sqls"$columnNameSQL is not null" + + private def countOtherVersions(revision: Int, articleId: Long)(implicit session: DBSession): Long = { + sql"select count(*) from $tableNameSQL where revision < $revision and article_id = $articleId" + .map(rs => rs.long("count")) + .single() + .getOrElse(0L) + } + + override def extractRowData(rs: WrappedResultSet): DocumentRow = + DocumentRow(rs.long("id"), rs.int("revision"), rs.long("article_id"), rs.string(columnName)) + + override def updateRow(rowData: DocumentRow)(implicit session: DBSession): Int = { + val other = countOtherVersions(rowData.revision, rowData.articleId) + val oldDocument = parser.parse(rowData.document).toTry.get + val newDoc = oldDocument.mapObject(_.add("publishedCount", Json.fromLong(other + 1))).noSpaces + + val dataObject = new PGobject() + dataObject.setType("jsonb") + dataObject.setValue(newDoc) + sql"""update $tableNameSQL + set $columnNameSQL = $dataObject + where id = ${rowData.id} + """.update() + } +} diff --git a/article-api/src/main/scala/no/ndla/articleapi/model/api/ArticleV2DTO.scala b/article-api/src/main/scala/no/ndla/articleapi/model/api/ArticleV2DTO.scala index 3f9163b830..4d887cd789 100644 --- a/article-api/src/main/scala/no/ndla/articleapi/model/api/ArticleV2DTO.scala +++ b/article-api/src/main/scala/no/ndla/articleapi/model/api/ArticleV2DTO.scala @@ -71,6 +71,8 @@ case class ArticleV2DTO( disclaimer: Option[DisclaimerDTO], @description("Traits extracted from the article content") traits: List[ArticleTrait], + @description("Number of times the article have been published") + publishedCount: Int, ) object ArticleV2DTO { 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..6db6d6c87e 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 @@ -189,14 +189,14 @@ class ConverterService(using props: Props) extends StrictLogging { val newPublishedDate = partialArticle.published.getOrElse(existingArticle.published) existingArticle.copy( - availability = newAvailability, - grepCodes = newGrepCodes, copyright = existingArticle.copyright.copy(license = newLicense), + tags = newTags, metaDescription = newMeta, + published = newPublishedDate, + grepCodes = newGrepCodes, + availability = newAvailability, relatedContent = newRelatedContent, - tags = newTags, revisionDate = newRevisionDate, - published = newPublishedDate, ) } @@ -289,6 +289,7 @@ class ConverterService(using props: Props) extends StrictLogging { slug = article.slug, disclaimer = disclaimer, traits = article.traits, + publishedCount = article.publishedCount.getOrElse(1), ) ) } else { diff --git a/article-api/src/test/scala/no/ndla/articleapi/TestData.scala b/article-api/src/test/scala/no/ndla/articleapi/TestData.scala index 2e523bbc99..b714c794d3 100644 --- a/article-api/src/test/scala/no/ndla/articleapi/TestData.scala +++ b/article-api/src/test/scala/no/ndla/articleapi/TestData.scala @@ -83,6 +83,7 @@ class TestData { slug = None, disclaimer = None, traits = List.empty, + publishedCount = 1, ) val apiArticleV2: api.ArticleV2DTO = api.ArticleV2DTO( @@ -129,6 +130,7 @@ class TestData { slug = None, disclaimer = None, traits = List.empty, + publishedCount = 1, ) val sampleArticleWithPublicDomain: Article = Article( @@ -161,6 +163,7 @@ class TestData { slug = None, disclaimer = OptLanguageFields.empty, traits = List.empty, + publishedCount = Some(1), ) val sampleDomainArticle: Article = Article( @@ -188,6 +191,7 @@ class TestData { slug = None, disclaimer = OptLanguageFields.empty, traits = List.empty, + publishedCount = Some(1), ) val sampleDomainArticle2: Article = Article( @@ -215,6 +219,7 @@ class TestData { slug = None, disclaimer = OptLanguageFields.empty, traits = List.empty, + publishedCount = Some(1), ) val sampleArticleWithByNcSa: Article = sampleArticleWithPublicDomain.copy(copyright = byNcSaCopyright) @@ -254,6 +259,7 @@ class TestData { slug = None, disclaimer = OptLanguageFields.empty, traits = List.empty, + publishedCount = Some(1), ) val apiArticleWithHtmlFaultV2: api.ArticleV2DTO = api.ArticleV2DTO( @@ -301,6 +307,7 @@ class TestData { slug = None, disclaimer = None, traits = List.empty, + publishedCount = 1, ) val (nodeId, nodeId2) = ("1234", "4321") @@ -337,6 +344,7 @@ class TestData { slug = None, disclaimer = OptLanguageFields.empty, traits = List.empty, + publishedCount = Some(1), ) } 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..58bbc357bc 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 @@ -203,12 +203,12 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { val existingArticle = TestData .sampleDomainArticle .copy( - availability = Availability.everyone, - grepCodes = Seq("old", "code"), copyright = Copyright("CC-BY-4.0", Some("origin"), Seq(), Seq(), Seq(), None, None, false), + tags = Seq(Tag(Seq("gammel", "Tag"), "nb")), metaDescription = Seq(Description("gammelDesc", "nb")), + grepCodes = Seq("old", "code"), + availability = Availability.everyone, relatedContent = Seq(Left(RelatedContentLink("title1", "url1")), Right(12L)), - tags = Seq(Tag(Seq("gammel", "Tag"), "nb")), ) val revisionDate = NDLADate.now() @@ -232,18 +232,18 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { val updatedArticle = TestData .sampleDomainArticle .copy( - availability = Availability.teacher, - grepCodes = Seq("New", "grep", "codes"), copyright = Copyright("newLicense", Some("origin"), Seq(), Seq(), Seq(), None, None, false), + tags = Seq(Tag(Seq("nye", "Tags"), "nb")), metaDescription = Seq(Description("nyDesc", "nb")), + published = revisionDate, + grepCodes = Seq("New", "grep", "codes"), + availability = Availability.teacher, relatedContent = Seq( Left(RelatedContentLink("New Title", "New Url")), Left(RelatedContentLink("Newer Title", "Newer Url")), Right(42L), ), - tags = Seq(Tag(Seq("nye", "Tags"), "nb")), revisionDate = Some(revisionDate), - published = revisionDate, ) service.updateArticleFields(existingArticle, partialArticle) should be(updatedArticle) @@ -254,13 +254,13 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { val existingArticle = TestData .sampleDomainArticle .copy( - availability = Availability.everyone, - grepCodes = Seq("old", "code"), copyright = Copyright("CC-BY-4.0", Some("origin"), Seq(), Seq(), Seq(), None, None, false), + tags = Seq(Tag(Seq("Gluten", "Tag"), "de")), metaDescription = Seq(Description("oldDesc", "de")), + grepCodes = Seq("old", "code"), + availability = Availability.everyone, relatedContent = Seq(Left(RelatedContentLink("title1", "url1")), Left(RelatedContentLink("old title", "old url"))), - tags = Seq(Tag(Seq("Gluten", "Tag"), "de")), ) val revisionDate = NDLADate.now() @@ -289,14 +289,14 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { val updatedArticle = TestData .sampleDomainArticle .copy( - availability = Availability.teacher, - grepCodes = Seq("New", "grep", "codes"), copyright = Copyright("newLicense", Some("origin"), Seq(), Seq(), Seq(), None, None, false), + tags = Seq(Tag(Seq("Guten", "Tag"), "de")), metaDescription = Seq(Description("neuDesc", "de")), + published = revisionDate, + grepCodes = Seq("New", "grep", "codes"), + availability = Availability.teacher, relatedContent = Seq(Right(42L), Right(420L), Right(4200L)), - tags = Seq(Tag(Seq("Guten", "Tag"), "de")), revisionDate = Some(revisionDate), - published = revisionDate, ) service.updateArticleFields(existingArticle, partialArticle) should be(updatedArticle) diff --git a/article-api/src/test/scala/no/ndla/articleapi/service/ReadServiceTest.scala b/article-api/src/test/scala/no/ndla/articleapi/service/ReadServiceTest.scala index b13874a6e6..8ed3a6f34d 100644 --- a/article-api/src/test/scala/no/ndla/articleapi/service/ReadServiceTest.scala +++ b/article-api/src/test/scala/no/ndla/articleapi/service/ReadServiceTest.scala @@ -293,8 +293,8 @@ class ReadServiceTest extends UnitSuite with TestEnvironment { title = Seq(Title("Parent title", "nb")), metaDescription = Seq(Description("Parent description", "nb")), metaImage = Seq(ArticleMetaImage("1000", "alt", "nb")), - slug = Some("some-slug"), published = date, + slug = Some("some-slug"), ) val article1 = TestData @@ -304,8 +304,8 @@ class ReadServiceTest extends UnitSuite with TestEnvironment { title = Seq(Title("Article1 title", "nb")), metaDescription = Seq(Description("Article1 description", "nb")), metaImage = Seq(ArticleMetaImage("1000", "alt", "nb")), - slug = Some("slug-one"), published = date, + slug = Some("slug-one"), ) val article2 = TestData @@ -315,8 +315,8 @@ class ReadServiceTest extends UnitSuite with TestEnvironment { title = Seq(Title("Article2 title", "nb")), metaDescription = Seq(Description("Article2 description", "nb")), metaImage = Seq(), - slug = Some("slug-two"), published = date, + slug = Some("slug-two"), ) val frontPage = FrontPageDTO( diff --git a/article-api/src/test/scala/no/ndla/articleapi/service/search/ArticleSearchServiceTest.scala b/article-api/src/test/scala/no/ndla/articleapi/service/search/ArticleSearchServiceTest.scala index 1d1c973a76..602bf7f7ca 100644 --- a/article-api/src/test/scala/no/ndla/articleapi/service/search/ArticleSearchServiceTest.scala +++ b/article-api/src/test/scala/no/ndla/articleapi/service/search/ArticleSearchServiceTest.scala @@ -72,14 +72,14 @@ class ArticleSearchServiceTest extends ElasticsearchIntegrationSuite with UnitSu .copy( id = Option(1), title = List(Title("Batmen er på vift med en bil", "nb")), - introduction = List(Introduction("Batmen", "nb")), content = List( ArticleContent("Bilde av en bil flaggermusmann som vifter med vingene bil.", "nb") ), tags = List(Tag(List("fugl"), "nb")), + introduction = List(Introduction("Batmen", "nb")), + metaImage = List(ArticleMetaImage("5555", "Alt text is here friend", "nb")), created = today.minusDays(4), updated = today.minusDays(3), - metaImage = List(ArticleMetaImage("5555", "Alt text is here friend", "nb")), grepCodes = Seq("KV123", "KV456"), ) @@ -88,9 +88,9 @@ class ArticleSearchServiceTest extends ElasticsearchIntegrationSuite with UnitSu .copy( id = Option(2), title = List(Title("Pingvinen er ute og går", "nb")), - introduction = List(Introduction("Pingvinen", "nb")), content = List(ArticleContent("
Bilde av en
en pingvin som vagger borover en gate
", "nb")), tags = List(Tag(List("fugl"), "nb")), + introduction = List(Introduction("Pingvinen", "nb")), created = today.minusDays(4), updated = today.minusDays(2), grepCodes = Seq("KV123", "KV456"), @@ -101,9 +101,9 @@ class ArticleSearchServiceTest extends ElasticsearchIntegrationSuite with UnitSu .copy( id = Option(3), title = List(Title("Donald Duck kjører bil", "nb")), - introduction = List(Introduction("Donald Duck", "nb")), content = List(ArticleContent("Bilde av en en and
som kjører en rød bil.
", "nb")), tags = List(Tag(List("and"), "nb")), + introduction = List(Introduction("Donald Duck", "nb")), created = today.minusDays(4), updated = today.minusDays(1), grepCodes = Seq("KV456"), @@ -114,10 +114,10 @@ class ArticleSearchServiceTest extends ElasticsearchIntegrationSuite with UnitSu .copy( id = Option(4), title = List(Title("Superman er ute og flyr", "nb")), - introduction = List(Introduction("Superman", "nb")), content = List(ArticleContent("Bilde av en flygende mann
som har superkrefter.
", "nb")), tags = List(Tag(List("supermann"), "nb")), + introduction = List(Introduction("Superman", "nb")), created = today.minusDays(4), updated = today, ) @@ -127,9 +127,9 @@ class ArticleSearchServiceTest extends ElasticsearchIntegrationSuite with UnitSu .copy( id = Option(5), title = List(Title("Hulken løfter biler", "nb")), - introduction = List(Introduction("Hulken", "nb")), content = List(ArticleContent("Bilde av hulk
som løfter en rød bil.
", "nb")), tags = List(Tag(List("hulk"), "nb")), + introduction = List(Introduction("Hulken", "nb")), created = today.minusDays(40), updated = today.minusDays(35), ) @@ -139,7 +139,6 @@ class ArticleSearchServiceTest extends ElasticsearchIntegrationSuite with UnitSu .copy( id = Option(6), title = List(Title("Loke og Tor prøver å fange midgaardsormen", "nb")), - introduction = List(Introduction("Loke og Tor", "nb")), content = List( ArticleContent( "Bilde av Loke og Tor
som fisker fra Naglfar.
", @@ -147,6 +146,7 @@ class ArticleSearchServiceTest extends ElasticsearchIntegrationSuite with UnitSu ) ), tags = List(Tag(List("Loke", "Tor", "Naglfar"), "nb")), + introduction = List(Introduction("Loke og Tor", "nb")), created = today.minusDays(30), updated = today.minusDays(25), ) @@ -156,9 +156,9 @@ class ArticleSearchServiceTest extends ElasticsearchIntegrationSuite with UnitSu .copy( id = Option(7), title = List(Title("Yggdrasil livets tre", "nb")), - introduction = List(Introduction("Yggdrasil", "nb")), content = List(ArticleContent("Bilde av Yggdrasil livets tre med alle dyrene som bor i det.", "nb")), tags = List(Tag(List("yggdrasil"), "nb")), + introduction = List(Introduction("Yggdrasil", "nb")), created = today.minusDays(20), updated = today.minusDays(15), ) @@ -168,9 +168,9 @@ class ArticleSearchServiceTest extends ElasticsearchIntegrationSuite with UnitSu .copy( id = Option(8), title = List(Title("Baldur har mareritt", "nb")), - introduction = List(Introduction("Baldur", "nb")), content = List(ArticleContent("
Bilde av Baldurs mareritt om Ragnarok.", "nb")), tags = List(Tag(List("baldur"), "nb")), + introduction = List(Introduction("Baldur", "nb")), created = today.minusDays(10), updated = today.minusDays(5), articleType = ArticleType.TopicArticle, @@ -181,9 +181,9 @@ class ArticleSearchServiceTest extends ElasticsearchIntegrationSuite with UnitSu .copy( id = Option(9), title = List(Title("En Baldur har mareritt om Ragnarok", "nb")), - introduction = List(Introduction("Baldur", "nb")), content = List(ArticleContent("
Bilde av Baldurs som har mareritt.", "nb")), tags = List(Tag(List("baldur"), "nb")), + introduction = List(Introduction("Baldur", "nb")), created = today.minusDays(10), updated = today.minusDays(5), articleType = ArticleType.TopicArticle, @@ -194,9 +194,9 @@ class ArticleSearchServiceTest extends ElasticsearchIntegrationSuite with UnitSu .copy( id = Option(10), title = List(Title("This article is in english", "en")), - introduction = List(Introduction("Engulsk", "en")), content = List(ArticleContent("
Something something english What about", "en")), tags = List(Tag(List("englando"), "en")), + introduction = List(Introduction("Engulsk", "en")), created = today.minusDays(10), updated = today.minusDays(5), articleType = ArticleType.TopicArticle, @@ -207,15 +207,15 @@ class ArticleSearchServiceTest extends ElasticsearchIntegrationSuite with UnitSu .copy( id = Option(11), title = List(Title("Katter", "nb"), Title("Cats", "en"), Title("Baloi", "biz")), + content = + List(ArticleContent("
Noe om en katt
", "nb"), ArticleContent("Something about a cat
", "en")), + tags = List(Tag(List("ikkehund"), "nb"), Tag(List("notdog"), "en")), introduction = List( Introduction("Katter er store", "nb"), Introduction("Cats are big", "en"), Introduction("Cats are baloi", "biz"), ), metaDescription = List(Description("hurr durr ima sheep", "en")), - content = - List(ArticleContent("Noe om en katt
", "nb"), ArticleContent("Something about a cat
", "en")), - tags = List(Tag(List("ikkehund"), "nb"), Tag(List("notdog"), "en")), created = today.minusDays(10), updated = today.minusDays(5), articleType = ArticleType.TopicArticle, @@ -226,10 +226,10 @@ class ArticleSearchServiceTest extends ElasticsearchIntegrationSuite with UnitSu .copy( id = Option(12), title = List(Title("availability - Hemmelig lærer artikkel", "nb")), - introduction = List(Introduction("Lærer", "nb")), - metaDescription = List(Description("lærer", "nb")), content = List(ArticleContent("Lærer
", "nb")), tags = List(Tag(List("lærer"), "nb")), + introduction = List(Introduction("Lærer", "nb")), + metaDescription = List(Description("lærer", "nb")), created = today.minusDays(10), updated = today.minusDays(5), articleType = ArticleType.Standard, @@ -241,10 +241,10 @@ class ArticleSearchServiceTest extends ElasticsearchIntegrationSuite with UnitSu .copy( id = Option(13), title = List(Title("availability - Hemmelig student artikkel", "nb")), - introduction = List(Introduction("Student", "nb")), - metaDescription = List(Description("student", "nb")), content = List(ArticleContent("Student
", "nb")), tags = List(Tag(List("student"), "nb")), + introduction = List(Introduction("Student", "nb")), + metaDescription = List(Description("student", "nb")), created = today.minusDays(10), updated = today.minusDays(5), articleType = ArticleType.Standard, diff --git a/common/src/main/scala/no/ndla/common/model/api/search/MultiSearchSummaryDTO.scala b/common/src/main/scala/no/ndla/common/model/api/search/MultiSearchSummaryDTO.scala index be0fa57491..739be813d8 100644 --- a/common/src/main/scala/no/ndla/common/model/api/search/MultiSearchSummaryDTO.scala +++ b/common/src/main/scala/no/ndla/common/model/api/search/MultiSearchSummaryDTO.scala @@ -138,6 +138,8 @@ case class MultiSearchSummaryDTO( resultType: SearchType, @description("List of codes the resource is tagged with") grepCodes: Seq[String], + @description("Number of times resource is published") + publishedCount: Option[Int], ) extends MultiSummaryBaseDTO object MultiSearchSummaryDTO extends SchemaImplicits { diff --git a/common/src/main/scala/no/ndla/common/model/domain/article/Article.scala b/common/src/main/scala/no/ndla/common/model/domain/article/Article.scala index ff2a5e40b6..2dfe63a21a 100644 --- a/common/src/main/scala/no/ndla/common/model/domain/article/Article.scala +++ b/common/src/main/scala/no/ndla/common/model/domain/article/Article.scala @@ -41,6 +41,7 @@ case class Article( slug: Option[String], disclaimer: OptLanguageFields[String], traits: List[ArticleTrait], + publishedCount: Option[Int], ) extends Content object Article { diff --git a/common/src/main/scala/no/ndla/common/model/domain/draft/Draft.scala b/common/src/main/scala/no/ndla/common/model/domain/draft/Draft.scala index a9bf0c2a14..39b4a70065 100644 --- a/common/src/main/scala/no/ndla/common/model/domain/draft/Draft.scala +++ b/common/src/main/scala/no/ndla/common/model/domain/draft/Draft.scala @@ -52,6 +52,7 @@ case class Draft( qualityEvaluation: Option[QualityEvaluation], disclaimer: OptLanguageFields[String], traits: List[ArticleTrait], + publishedCount: Int, ) extends Content { def supportedLanguages: Seq[String] = getSupportedLanguages( diff --git a/common/src/test/scala/no/ndla/common/model/api/search/MultiSearchSummaryDTOTest.scala b/common/src/test/scala/no/ndla/common/model/api/search/MultiSearchSummaryDTOTest.scala index fe848a8db5..733d8f2b28 100644 --- a/common/src/test/scala/no/ndla/common/model/api/search/MultiSearchSummaryDTOTest.scala +++ b/common/src/test/scala/no/ndla/common/model/api/search/MultiSearchSummaryDTOTest.scala @@ -53,6 +53,8 @@ class MultiSearchSummaryDTOTest extends UnitTestSuiteBase { paths = List(), lastUpdated = now, license = None, + revision = None, + started = false, revisions = Seq(), responsible = None, comments = None, @@ -64,8 +66,7 @@ class MultiSearchSummaryDTOTest extends UnitTestSuiteBase { favorited = None, resultType = SearchType.Articles, grepCodes = Seq.empty, - revision = None, - started = false, + publishedCount = Some(1), ) import io.circe.syntax.* diff --git a/draft-api/src/main/scala/no/ndla/draftapi/db/migration/V83__SetPublishedCount.scala b/draft-api/src/main/scala/no/ndla/draftapi/db/migration/V83__SetPublishedCount.scala new file mode 100644 index 0000000000..deb1c44748 --- /dev/null +++ b/draft-api/src/main/scala/no/ndla/draftapi/db/migration/V83__SetPublishedCount.scala @@ -0,0 +1,53 @@ +/* + * Part of NDLA draft-api + * Copyright (C) 2026 NDLA + * + * See LICENSE + * + */ + +package no.ndla.draftapi.db.migration + +import io.circe.{Json, parser} +import no.ndla.database.TableMigration +import org.postgresql.util.PGobject +import scalikejdbc.{DBSession, SQLSyntax, WrappedResultSet} +import scalikejdbc.interpolation.Implicits.scalikejdbcSQLInterpolationImplicitDef + +case class DocumentRow(id: Long, revision: Int, articleId: Long, document: String) + +class V83__SetPublishedCount extends TableMigration[DocumentRow] { + val columnName: String = "document" + override val tableName: String = "articledata" + + private lazy val columnNameSQL: SQLSyntax = SQLSyntax.createUnsafely(columnName) + override lazy val whereClause: SQLSyntax = sqls"$columnNameSQL is not null" + + private def countOtherVersions(revision: Int, articleId: Long)(implicit session: DBSession): Long = { + sql"select count(*) from $tableNameSQL where revision < $revision and article_id = $articleId and $columnNameSQL -> 'status' ->> 'current' = 'PUBLISHED'" + .map(rs => rs.long("count")) + .single() + .getOrElse(0L) + } + + override def extractRowData(rs: WrappedResultSet): DocumentRow = + DocumentRow(rs.long("id"), rs.int("revision"), rs.long("article_id"), rs.string(columnName)) + + override def updateRow(rowData: DocumentRow)(implicit session: DBSession): Int = { + var other = countOtherVersions(rowData.revision, rowData.articleId) + val oldDocument = parser.parse(rowData.document).toTry.get + val published = oldDocument.hcursor.downField("status").downField("current").as[String].toTry.getOrElse("") + if (published == "PUBLISHED") { + other = other + 1 + } + val newDoc = oldDocument.mapObject(_.add("publishedCount", Json.fromLong(other))).noSpaces + + val dataObject = new PGobject() + dataObject.setType("jsonb") + dataObject.setValue(newDoc) + sql"""update $tableNameSQL + set $columnNameSQL = $dataObject + where id = ${rowData.id} + """.update() + } +} diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleDTO.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleDTO.scala index 5530cdcd00..281557d380 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleDTO.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleDTO.scala @@ -97,6 +97,8 @@ case class ArticleDTO( disclaimer: Option[DisclaimerDTO], @description("Traits extracted from the article content") traits: List[ArticleTrait], + @description("Number of times the article is published") + publishedCount: Int, ) object ArticleDTO { 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 10d3188c1d..5cc9547a83 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 @@ -129,6 +129,7 @@ class ConverterService(using qualityEvaluation = qualityEvaluationToDomain(newArticle.qualityEvaluation), disclaimer = domainDisclaimer, traits = traits, + publishedCount = 0, ) ) } @@ -334,6 +335,7 @@ class ConverterService(using qualityEvaluation = toApiQualityEvaluation(article.qualityEvaluation), disclaimer = disclaimer, traits = article.traits, + publishedCount = article.publishedCount, ) ) } else { @@ -521,6 +523,7 @@ class ConverterService(using slug = draft.slug, disclaimer = draft.disclaimer, traits = draft.traits, + publishedCount = Some(draft.publishedCount), ) ) } @@ -739,6 +742,7 @@ class ConverterService(using qualityEvaluation = qualityEvaluationToDomain(article.qualityEvaluation), disclaimer = updatedDisclaimer, traits = traitUtil.getArticleTraits(updatedContents), + publishedCount = toMergeInto.publishedCount, ) Success(converted) diff --git a/draft-api/src/main/scala/no/ndla/draftapi/service/StateTransitionRules.scala b/draft-api/src/main/scala/no/ndla/draftapi/service/StateTransitionRules.scala index efba21ba44..cba33bdded 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/service/StateTransitionRules.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/service/StateTransitionRules.scala @@ -78,13 +78,14 @@ class StateTransitionRules(using (article, user) => article.id match { case Some(id) => for { - externalIds <- dbUtility.readOnly(implicit session => draftRepository.getExternalIdsFromId(id)) - h5pPaths = converterService.getEmbeddedH5PPaths(article) - _ = h5pApiClient.publishH5Ps(h5pPaths, user) - taxonomyT = taxonomyApiClient.updateTaxonomyIfExists(id, article, user) - articleUpdateT = articleApiClient.updateArticle(id, article, externalIds, useSoftValidation, user) - _ <- taxonomyT - articleUpdate <- articleUpdateT + externalIds <- dbUtility.readOnly(implicit session => draftRepository.getExternalIdsFromId(id)) + h5pPaths = converterService.getEmbeddedH5PPaths(article) + _ = h5pApiClient.publishH5Ps(h5pPaths, user) + withPublishCount = article.copy(publishedCount = article.publishedCount + 1) + taxonomyT = taxonomyApiClient.updateTaxonomyIfExists(id, article, user) + articleUpdateT = articleApiClient.updateArticle(id, withPublishCount, externalIds, useSoftValidation, user) + _ <- taxonomyT + articleUpdate <- articleUpdateT } yield articleUpdate case None => Failure(NotFoundException("This is a bug, article to publish has no id.")) }, @@ -262,7 +263,7 @@ class StateTransitionRules(using } } - def debugLog(x: Any): Unit = { + private def debugLog(x: Any): Unit = { if (scala.util.Properties.propOrEmpty("DEBUG_FLAKE") == "true") { println(x) } diff --git a/draft-api/src/main/scala/no/ndla/draftapi/service/WriteService.scala b/draft-api/src/main/scala/no/ndla/draftapi/service/WriteService.scala index 62be524893..2ccd3e7466 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/service/WriteService.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/service/WriteService.scala @@ -203,7 +203,9 @@ class WriteService(using } } - def updateArticleAndStoreAsNewIfPublished(article: Draft, statusWasUpdated: Boolean)(using DBSession): Try[Draft] = { + private def updateArticleAndStoreAsNewIfPublished(article: Draft, statusWasUpdated: Boolean)(using + DBSession + ): Try[Draft] = { val storeAsNewVersion = statusWasUpdated && article.status.current == PUBLISHED draftRepository.updateArticle(article) match { case Success(updated) if storeAsNewVersion => draftRepository.storeArticleAsNewVersion(updated, None) diff --git a/draft-api/src/main/scala/no/ndla/draftapi/validation/ContentValidator.scala b/draft-api/src/main/scala/no/ndla/draftapi/validation/ContentValidator.scala index 5abcfe8995..36fdbdbd58 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/validation/ContentValidator.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/validation/ContentValidator.scala @@ -73,12 +73,12 @@ class ContentValidator(using private def getArticleOnLanguage(article: Draft, language: String): Draft = { article.copy( - content = article.content.filter(_.language == language), - introduction = article.introduction.filter(_.language == language), - metaDescription = article.metaDescription.filter(_.language == language), title = article.title.filter(_.language == language), + content = article.content.filter(_.language == language), tags = article.tags.filter(_.language == language), visualElement = article.visualElement.filter(_.language == language), + introduction = article.introduction.filter(_.language == language), + metaDescription = article.metaDescription.filter(_.language == language), metaImage = article.metaImage.filter(_.language == language), ) } @@ -130,12 +130,12 @@ class ContentValidator(using .withSortedLanguageFields(article) .copy( revision = None, + updated = NDLADate.MIN, + updatedBy = "", notes = Seq.empty, editorLabels = Seq.empty, - comments = List.empty, - updated = NDLADate.MIN, revisionMeta = Seq.empty, - updatedBy = "", + comments = List.empty, ) withComparableValues(oldArticle) == withComparableValues(changedArticle) diff --git a/draft-api/src/test/scala/no/ndla/draftapi/TestData.scala b/draft-api/src/test/scala/no/ndla/draftapi/TestData.scala index f45896e6da..db118e368e 100644 --- a/draft-api/src/test/scala/no/ndla/draftapi/TestData.scala +++ b/draft-api/src/test/scala/no/ndla/draftapi/TestData.scala @@ -121,6 +121,7 @@ object TestData { qualityEvaluation = None, disclaimer = None, traits = List.empty, + publishedCount = 1, ) val blankUpdatedArticle: UpdatedArticleDTO = api.UpdatedArticleDTO( @@ -240,6 +241,7 @@ object TestData { qualityEvaluation = None, disclaimer = None, traits = List.empty, + publishedCount = 1, ) val apiArticleUserTest: api.ArticleDTO = api.ArticleDTO( @@ -296,6 +298,7 @@ object TestData { qualityEvaluation = None, disclaimer = None, traits = List.empty, + publishedCount = 1, ) val sampleTopicArticle: Draft = Draft( @@ -332,6 +335,7 @@ object TestData { qualityEvaluation = None, disclaimer = OptLanguageFields.empty, traits = List.empty, + publishedCount = 0, ) val sampleArticleWithPublicDomain: Draft = Draft( @@ -368,6 +372,7 @@ object TestData { qualityEvaluation = None, disclaimer = OptLanguageFields.empty, traits = List.empty, + publishedCount = 0, ) val sampleDomainArticle: Draft = Draft( @@ -406,6 +411,7 @@ object TestData { qualityEvaluation = None, disclaimer = OptLanguageFields.empty, traits = List.empty, + publishedCount = 1, ) val newArticle: NewArticleDTO = api.NewArticleDTO( @@ -460,9 +466,9 @@ object TestData { content = Seq( common.ArticleContent( """avsnitt
<$EmbedTagName data-resource=\"concept\" data-content-id=\"123\" data-title=\"Forklaring\" data-type=\"block\">$EmbedTagName>", @@ -531,6 +533,7 @@ object TestData { updated = today.minusDays(5), published = today.minusDays(5), articleType = ArticleType.FrontpageArticle, + slug = Some("forsideartikkel"), ) val articlesToIndex: Seq[Article] = List( @@ -575,6 +578,7 @@ object TestData { slug = None, disclaimer = OptLanguageFields.empty, traits = List.empty, + publishedCount = None, ) val emptyDomainDraft: Draft = Draft( @@ -611,6 +615,7 @@ object TestData { qualityEvaluation = None, disclaimer = OptLanguageFields.empty, traits = List.empty, + publishedCount = 0, ) val draftStatus: Status = Status(DraftStatus.PLANNED, Set.empty) @@ -675,6 +680,7 @@ object TestData { qualityEvaluation = None, disclaimer = OptLanguageFields.empty, traits = List.empty, + publishedCount = 0, ) val sampleDraftWithByNcSa: Draft = sampleDraftWithPublicDomain.copy(copyright = Some(draftByNcSaCopyright)) @@ -685,16 +691,16 @@ object TestData { .copy( id = Option(1), title = List(Title("Batmen er på vift med en bil", "nb")), - introduction = List(Introduction("Batmen", "nb")), - metaDescription = List.empty, - visualElement = List.empty, content = List( ArticleContent("Bilde av en bil flaggermusmann som vifter med vingene bil.", "nb") ), + copyright = Some(draftByNcSaCopyright.copy(creators = List(Author(ContributorType.Writer, "Kjekspolitiet")))), tags = List(Tag(List("fugl"), "nb")), + visualElement = List.empty, + introduction = List(Introduction("Batmen", "nb")), + metaDescription = List.empty, created = today.minusDays(4), updated = today.minusDays(3), - copyright = Some(draftByNcSaCopyright.copy(creators = List(Author(ContributorType.Writer, "Kjekspolitiet")))), grepCodes = Seq("K123", "K456"), ) @@ -703,19 +709,19 @@ object TestData { .copy( id = Option(2), title = List(Title("Pingvinen er ute og går", "nb")), - introduction = List(Introduction("Pingvinen", "nb")), - metaDescription = List.empty, - visualElement = List.empty, content = List(ArticleContent("Bilde av en
en pingvin som vagger borover en gate
", "nb")), - tags = List(Tag(List("fugl"), "nb")), - created = today.minusDays(4), - updated = today.minusDays(2), copyright = Some( draftPublicDomainCopyright.copy( creators = List(Author(ContributorType.Writer, "Pjolter")), processors = List(Author(ContributorType.Editorial, "Svims")), ) ), + tags = List(Tag(List("fugl"), "nb")), + visualElement = List.empty, + introduction = List(Introduction("Pingvinen", "nb")), + metaDescription = List.empty, + created = today.minusDays(4), + updated = today.minusDays(2), grepCodes = Seq("K456", "K123"), ) @@ -724,11 +730,11 @@ object TestData { .copy( id = Option(3), title = List(Title("Donald Duck kjører bil", "nb")), - introduction = List(Introduction("Donald Duck", "nb")), - metaDescription = List.empty, - visualElement = List.empty, content = List(ArticleContent("Bilde av en en and
som kjører en rød bil.
", "nb")), tags = List(Tag(List("and"), "nb")), + visualElement = List.empty, + introduction = List(Introduction("Donald Duck", "nb")), + metaDescription = List.empty, created = today.minusDays(4), updated = today.minusDays(1), grepCodes = Seq("K123"), @@ -739,12 +745,12 @@ object TestData { .copy( id = Option(4), title = List(Title("Superman er ute og flyr", "nb")), - introduction = List(Introduction("Superman", "nb")), - metaDescription = List.empty, - visualElement = List.empty, content = List(ArticleContent("Bilde av en flygende mann
som har superkrefter.
", "nb")), tags = List(Tag(List("supermann"), "nb")), + visualElement = List.empty, + introduction = List(Introduction("Superman", "nb")), + metaDescription = List.empty, created = today.minusDays(4), updated = today, ) @@ -754,11 +760,11 @@ object TestData { .copy( id = Option(5), title = List(Title("Hulken løfter biler", "nb")), - introduction = List(Introduction("Hulken", "nb")), - metaDescription = List.empty, - visualElement = List.empty, content = List(ArticleContent("Bilde av hulk
som løfter en rød bil.
", "nb")), tags = List(Tag(List("hulk"), "nb")), + visualElement = List.empty, + introduction = List(Introduction("Hulken", "nb")), + metaDescription = List.empty, created = today.minusDays(40), updated = today.minusDays(35), notes = @@ -774,9 +780,6 @@ object TestData { .copy( id = Option(6), title = List(Title("Loke og Tor prøver å fange midgaardsormen", "nb")), - introduction = List(Introduction("Loke og Tor", "nb")), - metaDescription = List.empty, - visualElement = List.empty, content = List( ArticleContent( "Bilde av Loke og Tor
som fisker fra Naglfar.
", @@ -784,6 +787,9 @@ object TestData { ) ), tags = List(Tag(List("Loke", "Tor", "Naglfar"), "nb")), + visualElement = List.empty, + introduction = List(Introduction("Loke og Tor", "nb")), + metaDescription = List.empty, created = today.minusDays(30), updated = today.minusDays(25), ) @@ -793,11 +799,11 @@ object TestData { .copy( id = Option(7), title = List(Title("Yggdrasil livets tre", "nb")), - introduction = List(Introduction("Yggdrasil", "nb")), - metaDescription = List.empty, - visualElement = List.empty, content = List(ArticleContent("Bilde av Yggdrasil livets tre med alle dyrene som bor i det.", "nb")), tags = List(Tag(List("yggdrasil"), "nb")), + visualElement = List.empty, + introduction = List(Introduction("Yggdrasil", "nb")), + metaDescription = List.empty, created = today.minusDays(20), updated = today.minusDays(15), ) @@ -807,11 +813,11 @@ object TestData { .copy( id = Option(8), title = List(Title("Baldur har mareritt", "nb")), - introduction = List(Introduction("Baldur", "nb")), - metaDescription = List.empty, - visualElement = List.empty, content = List(ArticleContent("
Bilde av Baldurs mareritt om Ragnarok.", "nb")), tags = List(Tag(List("baldur"), "nb")), + visualElement = List.empty, + introduction = List(Introduction("Baldur", "nb")), + metaDescription = List.empty, created = today.minusDays(10), updated = today.minusDays(5), articleType = ArticleType.TopicArticle, @@ -822,11 +828,11 @@ object TestData { .copy( id = Option(9), title = List(Title("Baldur har mareritt om Ragnarok", "nb")), - introduction = List(Introduction("Baldur", "nb")), - metaDescription = List.empty, - visualElement = List.empty, content = List(ArticleContent("
Bilde av Baldurs som har mareritt.", "nb")), tags = List(Tag(List("baldur"), "nb")), + visualElement = List.empty, + introduction = List(Introduction("Baldur", "nb")), + metaDescription = List.empty, created = today.minusDays(10), updated = today.minusDays(5), articleType = ArticleType.TopicArticle, @@ -838,11 +844,11 @@ object TestData { id = Option(10), status = Status(DraftStatus.IN_PROGRESS, Set.empty), title = List(Title("This article is in english", "en")), - introduction = List(Introduction("Engulsk", "en")), - metaDescription = List.empty, - visualElement = List.empty, content = List(ArticleContent("
Something something english What", "en")), tags = List(Tag(List("englando"), "en")), + visualElement = List.empty, + introduction = List(Introduction("Engulsk", "en")), + metaDescription = List.empty, metaImage = List(ArticleMetaImage("123", "alt", "en")), created = today.minusDays(10), updated = today.minusDays(5), @@ -855,12 +861,12 @@ object TestData { id = Option(11), status = Status(DraftStatus.IN_PROGRESS, Set.empty), title = List(Title("Katter", "nb"), Title("Cats", "en")), - introduction = List(Introduction("Katter er store", "nb"), Introduction("Cats are big", "en")), content = List(ArticleContent("
Noe om en katt
", "nb"), ArticleContent("Something about a cat
", "en")), tags = List(Tag(List("katt"), "nb"), Tag(List("cat"), "en")), - metaDescription = List(common.Description("hurr dirr ima sheep", "en")), visualElement = List.empty, + introduction = List(Introduction("Katter er store", "nb"), Introduction("Cats are big", "en")), + metaDescription = List(common.Description("hurr dirr ima sheep", "en")), created = today.minusDays(10), updated = today.minusDays(5), articleType = ArticleType.TopicArticle, @@ -872,8 +878,6 @@ object TestData { id = Option(12), status = importedDraftStatus, title = List(Title("Ekstrastoff", "nb")), - introduction = List(Introduction("Ekstra", "nb")), - metaDescription = List(common.Description("", "nb")), content = List( ArticleContent( s"""artikkeltekst med fire deler
<$EmbedTagName data-resource=\"concept\" data-resource_id=\"222\">$EmbedTagName> @@ -890,8 +894,10 @@ object TestData { "nb", ) ), - visualElement = List(VisualElement(s"<$EmbedTagName data-resource_id=\"333\">", "nb")), tags = List(Tag(List(""), "nb")), + visualElement = List(VisualElement(s"<$EmbedTagName data-resource_id=\"333\">", "nb")), + introduction = List(Introduction("Ekstra", "nb")), + metaDescription = List(common.Description("", "nb")), created = today.minusDays(10), updated = today.minusDays(5), ) @@ -901,8 +907,6 @@ object TestData { .copy( id = Option(13), title = List(Title("Luringen", "nb"), Title("English title", "en"), Title("Chhattisgarhi title", "hne")), - introduction = List(Introduction("Luringen", "nb")), - metaDescription = List(common.Description("", "nb")), content = List( ArticleContent( s"Helsesøster
Søkeord: delt?streng delt!streng delt&streng
<$EmbedTagName data-resource=\"content-link\" data-content-type=\"article\" data-content-id=\"666\">$EmbedTagName>Vanlig i gamle testamentet
delt-streng
Christianity!
", "en"), ), - visualElement = List.empty, tags = List(Tag(List("engel"), "nb")), + visualElement = List.empty, + introduction = List(Introduction("Religion", "nb")), + metaDescription = List(common.Description("metareligion", "nb")), created = today.minusDays(10), updated = today.minusDays(5), articleType = ArticleType.TopicArticle, @@ -963,15 +969,15 @@ object TestData { .copy( id = Option(16), title = List(Title("Engler og demoner", "nb")), - slug = Some("engler-og-demoner"), - introduction = List(Introduction("Religion", "nb")), - metaDescription = List(common.Description("metareligion", "nb")), content = List(ArticleContent("Vanlig i gamle testamentet
Vanlig i gamle testamentet