From e51af723bf8cbeba37c529cd3e434a25f708f9f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Mon, 18 Nov 2024 14:09:29 +0100 Subject: [PATCH] feat: Add license and copyright attribution to project (DEV-4347) (#3429) Co-authored-by: Marcin Procyk --- .../org/knora/webapi/ProjectEraseIT.scala | 2 + .../IntegrationTestAdminJsonProtocol.scala | 21 ++++++++ .../admin/ProjectRestServiceSpec.scala | 15 +++++- .../sharedtestdata/SharedTestDataADM.scala | 20 +++++++ .../knora/webapi/slice/admin/api/Codecs.scala | 12 +++-- .../webapi/slice/admin/api/Examples.scala | 4 ++ ...rojectsEndpointsRequestsAndResponses.scala | 4 ++ .../admin/api/model/ProjectsMessagesADM.scala | 4 ++ .../admin/domain/model/KnoraProject.scala | 21 ++++++++ .../domain/service/KnoraProjectRepo.scala | 2 + .../domain/service/KnoraProjectService.scala | 4 ++ .../admin/domain/service/ProjectService.scala | 4 ++ .../slice/admin/repo/rdf/RdfConversions.scala | 2 + .../repo/service/KnoraProjectRepoLive.scala | 34 ++++++++---- .../slice/common/repo/rdf/Vocabulary.scala | 2 + .../org/knora/webapi/TestDataFactory.scala | 2 + .../service/KnoraProjectServiceSpec.scala | 52 +++++++++++++++++++ .../domain/service/ProjectServiceSpec.scala | 4 ++ .../service/KnoraProjectRepoLiveSpec.scala | 10 +++- 19 files changed, 201 insertions(+), 18 deletions(-) create mode 100644 webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectServiceSpec.scala diff --git a/integration/src/test/scala/org/knora/webapi/ProjectEraseIT.scala b/integration/src/test/scala/org/knora/webapi/ProjectEraseIT.scala index dfd542331a..aaee0bc1fd 100644 --- a/integration/src/test/scala/org/knora/webapi/ProjectEraseIT.scala +++ b/integration/src/test/scala/org/knora/webapi/ProjectEraseIT.scala @@ -83,6 +83,8 @@ object ProjectEraseIT extends E2EZSpec { None, KnoraProject.Status.Active, KnoraProject.SelfJoin.CanJoin, + None, + None, ), ), ).orDie diff --git a/integration/src/test/scala/org/knora/webapi/messages/admin/responder/IntegrationTestAdminJsonProtocol.scala b/integration/src/test/scala/org/knora/webapi/messages/admin/responder/IntegrationTestAdminJsonProtocol.scala index fd36ebe999..d6595ab841 100644 --- a/integration/src/test/scala/org/knora/webapi/messages/admin/responder/IntegrationTestAdminJsonProtocol.scala +++ b/integration/src/test/scala/org/knora/webapi/messages/admin/responder/IntegrationTestAdminJsonProtocol.scala @@ -45,7 +45,10 @@ import org.knora.webapi.slice.admin.api.model.ProjectAdminMembersGetResponseADM import org.knora.webapi.slice.admin.api.model.ProjectMembersGetResponseADM import org.knora.webapi.slice.admin.api.model.ProjectOperationResponseADM import org.knora.webapi.slice.admin.domain.model.Group +import org.knora.webapi.slice.admin.domain.model.KnoraProject.CopyrightAttribution +import org.knora.webapi.slice.admin.domain.model.KnoraProject.License import org.knora.webapi.slice.admin.domain.model.User +import org.knora.webapi.slice.common.Value.StringValue /** * A spray-json protocol for generating Knora API JSON providing data about projects. @@ -189,9 +192,27 @@ object IntegrationTestAdminJsonProtocol extends TriplestoreJsonProtocol { "ontologies", "status", "selfjoin", + "copyrightAttribution", + "license", ), ) + trait StringValueFormat[T <: StringValue] extends JsonFormat[T] { self => + def from: String => Either[String, T] + override def write(v: T): JsValue = JsString(v.value) + override def read(json: JsValue): T = json match + case JsString(str) => self.from(str).fold(err => throw DeserializationException(err), identity) + case _ => throw DeserializationException("Value must be a JSON string.") + } + + implicit object CopyrightAttributionFormat extends StringValueFormat[CopyrightAttribution] { + override val from: String => Either[String, CopyrightAttribution] = CopyrightAttribution.from + } + + implicit object LicenseFormat extends StringValueFormat[License] { + override val from: String => Either[String, License] = License.from + } + implicit val groupFormat: JsonFormat[Group] = jsonFormat6(Group.apply) implicit val projectAdminMembersGetResponseADMFormat: RootJsonFormat[ProjectAdminMembersGetResponseADM] = rootFormat( diff --git a/integration/src/test/scala/org/knora/webapi/responders/admin/ProjectRestServiceSpec.scala b/integration/src/test/scala/org/knora/webapi/responders/admin/ProjectRestServiceSpec.scala index 945c7126b4..24db171d85 100644 --- a/integration/src/test/scala/org/knora/webapi/responders/admin/ProjectRestServiceSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/responders/admin/ProjectRestServiceSpec.scala @@ -162,6 +162,8 @@ class ProjectRestServiceSpec extends CoreSpec with ImplicitSender { logo = Some(Logo.unsafeFrom("/fu/bar/baz.jpg")), status = Status.Active, selfjoin = SelfJoin.CannotJoin, + copyrightAttribution = Some(CopyrightAttribution.unsafeFrom("2024, Example Project")), + license = Some(License.unsafeFrom("CC-BY-4.0")), ), SharedTestDataADM.rootUser, ), @@ -174,6 +176,8 @@ class ProjectRestServiceSpec extends CoreSpec with ImplicitSender { received.project.description should be( Seq(StringLiteralV2.from(value = "project description", language = Some("en"))), ) + received.project.copyrightAttribution should be(Some(CopyrightAttribution.unsafeFrom("2024, Example Project"))) + received.project.license should be(Some(License.unsafeFrom("CC-BY-4.0"))) newProjectIri.set(received.project.id) @@ -257,6 +261,8 @@ class ProjectRestServiceSpec extends CoreSpec with ImplicitSender { logo = Some(Logo.unsafeFrom("/fu/bar/baz.jpg")), status = Status.Active, selfjoin = SelfJoin.CannotJoin, + None, + None, ), SharedTestDataADM.rootUser, ), @@ -269,7 +275,8 @@ class ProjectRestServiceSpec extends CoreSpec with ImplicitSender { received.project.description should be( Seq(StringLiteralV2.from(value = "project description", language = Some("en"))), ) - + received.project.copyrightAttribution should be(None) + received.project.license should be(None) } "CREATE a project that its info has special characters" in { @@ -292,6 +299,8 @@ class ProjectRestServiceSpec extends CoreSpec with ImplicitSender { logo = Some(Logo.unsafeFrom("/fu/bar/baz.jpg")), status = Status.Active, selfjoin = SelfJoin.CannotJoin, + None, + None, ), SharedTestDataADM.rootUser, ), @@ -324,6 +333,8 @@ class ProjectRestServiceSpec extends CoreSpec with ImplicitSender { logo = Some(Logo.unsafeFrom("/fu/bar/baz.jpg")), status = Status.Active, selfjoin = SelfJoin.CannotJoin, + copyrightAttribution = None, + license = None, ), SharedTestDataADM.rootUser, ), @@ -346,6 +357,8 @@ class ProjectRestServiceSpec extends CoreSpec with ImplicitSender { logo = Some(Logo.unsafeFrom("/fu/bar/baz.jpg")), status = Status.Active, selfjoin = SelfJoin.CannotJoin, + copyrightAttribution = None, + license = None, ), SharedTestDataADM.rootUser, ), diff --git a/integration/src/test/scala/org/knora/webapi/sharedtestdata/SharedTestDataADM.scala b/integration/src/test/scala/org/knora/webapi/sharedtestdata/SharedTestDataADM.scala index f40a35403f..b4e11edcc4 100644 --- a/integration/src/test/scala/org/knora/webapi/sharedtestdata/SharedTestDataADM.scala +++ b/integration/src/test/scala/org/knora/webapi/sharedtestdata/SharedTestDataADM.scala @@ -169,6 +169,8 @@ object SharedTestDataADM { ), status = true, selfjoin = false, + copyrightAttribution = None, + license = None, ) /* represents the full ProjectADM of the default shared ontologies project */ @@ -183,6 +185,8 @@ object SharedTestDataADM { ontologies = Seq.empty[IRI], status = true, selfjoin = false, + copyrightAttribution = None, + license = None, ) /** @@ -295,6 +299,8 @@ object SharedTestDataADM { ontologies = Seq(SharedOntologyTestDataADM.IMAGES_ONTOLOGY_IRI), status = true, selfjoin = false, + copyrightAttribution = None, + license = None, ) /* represents the full ProjectADM of the images project in the external format */ @@ -309,6 +315,8 @@ object SharedTestDataADM { ontologies = Seq(SharedOntologyTestDataADM.IMAGES_ONTOLOGY_IRI_LocalHost), status = true, selfjoin = false, + copyrightAttribution = None, + license = None, ) /* represents the full GroupADM of the images ProjectAdmin group */ @@ -472,6 +480,8 @@ object SharedTestDataADM { ontologies = Seq(SharedOntologyTestDataADM.INCUNABULA_ONTOLOGY_IRI), status = true, selfjoin = false, + copyrightAttribution = None, + license = None, ) /* represents the ProjectADM of the incunabula project in the external format*/ @@ -507,6 +517,8 @@ object SharedTestDataADM { ontologies = Seq(SharedOntologyTestDataADM.INCUNABULA_ONTOLOGY_IRI_LocalHost), status = true, selfjoin = false, + copyrightAttribution = None, + license = None, ) /** @@ -619,6 +631,8 @@ object SharedTestDataADM { ontologies = Seq(SharedOntologyTestDataADM.ANYTHING_ONTOLOGY_IRI, SharedOntologyTestDataADM.SomethingOntologyIri), status = true, selfjoin = false, + copyrightAttribution = None, + license = None, ) def anythingProjectExternal: Project = Project( @@ -635,6 +649,8 @@ object SharedTestDataADM { ), status = true, selfjoin = false, + copyrightAttribution = None, + license = None, ) /* represents the full GroupADM of the Thing searcher group */ @@ -672,6 +688,8 @@ object SharedTestDataADM { ), status = true, selfjoin = false, + copyrightAttribution = None, + license = None, ) /* represents the user profile of 'superuser' as found in admin-data.ttl */ @@ -721,5 +739,7 @@ object SharedTestDataADM { ontologies = Seq("http://www.knora.org/ontology/0804/dokubib"), status = false, selfjoin = false, + copyrightAttribution = None, + license = None, ) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Codecs.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Codecs.scala index 7a92b5c308..d0486968d8 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Codecs.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Codecs.scala @@ -102,11 +102,13 @@ object Codecs { implicit val restrictedViewWatermark: StringCodec[RestrictedView.Watermark] = booleanCodec( RestrictedView.Watermark.from, ) - implicit val selfJoin: StringCodec[SelfJoin] = booleanCodec(SelfJoin.from) - implicit val shortcode: StringCodec[Shortcode] = stringCodec(Shortcode.from) - implicit val shortname: StringCodec[Shortname] = stringCodec(Shortname.from) - implicit val sparqlEncodedString: StringCodec[SparqlEncodedString] = stringCodec(SparqlEncodedString.from) - implicit val status: StringCodec[Status] = booleanCodec(Status.from) + implicit val selfJoin: StringCodec[SelfJoin] = booleanCodec(SelfJoin.from) + implicit val shortcode: StringCodec[Shortcode] = stringCodec(Shortcode.from) + implicit val shortname: StringCodec[Shortname] = stringCodec(Shortname.from) + implicit val sparqlEncodedString: StringCodec[SparqlEncodedString] = stringCodec(SparqlEncodedString.from) + implicit val status: StringCodec[Status] = booleanCodec(Status.from) + implicit val copyrightAttribution: StringCodec[CopyrightAttribution] = stringCodec(CopyrightAttribution.from) + implicit val license: StringCodec[License] = stringCodec(License.from) // user implicit val userIri: StringCodec[UserIri] = stringCodec(UserIri.from) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Examples.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Examples.scala index f669607840..c76501dd20 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Examples.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Examples.scala @@ -23,6 +23,8 @@ import org.knora.webapi.slice.admin.domain.model.GroupIri import org.knora.webapi.slice.admin.domain.model.GroupName import org.knora.webapi.slice.admin.domain.model.GroupSelfJoin import org.knora.webapi.slice.admin.domain.model.GroupStatus +import org.knora.webapi.slice.admin.domain.model.KnoraProject.CopyrightAttribution +import org.knora.webapi.slice.admin.domain.model.KnoraProject.License import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.model.User import org.knora.webapi.slice.admin.domain.model.UserIri @@ -93,6 +95,8 @@ object Examples { status = true, ontologies = Seq.empty, selfjoin = false, + copyrightAttribution = Some(CopyrightAttribution.unsafeFrom("2024, Example Project")), + license = Some(License.unsafeFrom("CC-BY-4.0")), ) private val group = Group( diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectsEndpointsRequestsAndResponses.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectsEndpointsRequestsAndResponses.scala index 89b8bce775..109144d967 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectsEndpointsRequestsAndResponses.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectsEndpointsRequestsAndResponses.scala @@ -27,6 +27,8 @@ object ProjectsEndpointsRequestsAndResponses { logo: Option[Logo] = None, status: Status, selfjoin: SelfJoin, + copyrightAttribution: Option[CopyrightAttribution], + license: Option[License], ) object ProjectCreateRequest { implicit val codec: JsonCodec[ProjectCreateRequest] = DeriveJsonCodec.gen[ProjectCreateRequest] @@ -40,6 +42,8 @@ object ProjectsEndpointsRequestsAndResponses { logo: Option[Logo] = None, status: Option[Status] = None, selfjoin: Option[SelfJoin] = None, + copyrightAttribution: Option[CopyrightAttribution] = None, + license: Option[License] = None, ) object ProjectUpdateRequest { implicit val codec: JsonCodec[ProjectUpdateRequest] = DeriveJsonCodec.gen[ProjectUpdateRequest] diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectsMessagesADM.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectsMessagesADM.scala index dcb3b218f2..dcab052e59 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectsMessagesADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectsMessagesADM.scala @@ -11,6 +11,8 @@ import zio.json.JsonCodec import org.knora.webapi.IRI import org.knora.webapi.messages.admin.responder.AdminKnoraResponseADM import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2 +import org.knora.webapi.slice.admin.api.Codecs.ZioJsonCodec.copyrightAttribution +import org.knora.webapi.slice.admin.api.Codecs.ZioJsonCodec.license import org.knora.webapi.slice.admin.domain.model.KnoraProject.* import org.knora.webapi.slice.admin.domain.model.RestrictedView import org.knora.webapi.slice.admin.domain.model.User @@ -40,6 +42,8 @@ case class Project( ontologies: Seq[IRI], status: Boolean, selfjoin: Boolean, + copyrightAttribution: Option[CopyrightAttribution], + license: Option[License], ) extends Ordered[Project] { def projectIri: ProjectIri = ProjectIri.unsafeFrom(id) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/KnoraProject.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/KnoraProject.scala index 42889abb00..67ffcfef1e 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/KnoraProject.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/KnoraProject.scala @@ -34,6 +34,8 @@ case class KnoraProject( status: Status, selfjoin: SelfJoin, restrictedView: RestrictedView, + copyrightAttribution: Option[CopyrightAttribution], + license: Option[License], ) extends EntityWithId[ProjectIri] object KnoraProject { @@ -173,4 +175,23 @@ object KnoraProject { def from(value: Boolean): SelfJoin = if (value) CanJoin else CannotJoin } + + final case class CopyrightAttribution private (override val value: String) extends StringValue + object CopyrightAttribution extends StringValueCompanion[CopyrightAttribution] { + private val maxLength = 1_000 + def from(str: String): Either[String, CopyrightAttribution] = + if (str.isEmpty) Left("Copyright attribution cannot be empty.") + else if (str.contains("\n")) Left("Copyright attribution may not contain line breaks.") + else if (str.length >= maxLength) Left(s"Copyright attribution may only be ${maxLength} characters long.") + else Right(CopyrightAttribution(str)) + } + + final case class License private (override val value: String) extends StringValue + object License extends StringValueCompanion[License] { + private val maxLength = 10_000 + def from(str: String): Either[String, License] = + if (str.isEmpty) Left("License cannot be empty.") + else if (str.length >= maxLength) Left(s"License may only be ${maxLength} characters long.") + else Right(License(str)) + } } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectRepo.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectRepo.scala index 0e231dce98..2d82da3468 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectRepo.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectRepo.scala @@ -47,6 +47,8 @@ object KnoraProjectRepo { KnoraProject.Status.Active, KnoraProject.SelfJoin.CannotJoin, RestrictedView.default, + None, + None, ) val SystemProject: KnoraProject = makeBuiltIn( diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectService.scala index 9c3837ecf5..3af6f33f00 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectService.scala @@ -52,6 +52,8 @@ final case class KnoraProjectService(knoraProjectRepo: KnoraProjectRepo, ontolog req.status, req.selfjoin, RestrictedView.default, + req.copyrightAttribution, + req.license, ) project <- knoraProjectRepo.save(project) } yield project @@ -97,6 +99,8 @@ final case class KnoraProjectService(knoraProjectRepo: KnoraProjectRepo, ontolog logo = updateReq.logo.orElse(project.logo), status = updateReq.status.getOrElse(project.status), selfjoin = updateReq.selfjoin.getOrElse(project.selfjoin), + copyrightAttribution = updateReq.copyrightAttribution.orElse(project.copyrightAttribution), + license = updateReq.license.orElse(project.license), ), ) } yield updated diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/ProjectService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/ProjectService.scala index 82a2db205a..e4af95ef9c 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/ProjectService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/ProjectService.scala @@ -56,6 +56,8 @@ final case class ProjectService( ontologies, knoraProject.status.value, knoraProject.selfjoin.value, + knoraProject.copyrightAttribution, + knoraProject.license, ), ) @@ -73,6 +75,8 @@ final case class ProjectService( status = Status.from(project.status), selfjoin = SelfJoin.from(project.selfjoin), restrictedView, + project.copyrightAttribution, + project.license, ) def setProjectRestrictedView(project: Project, settings: RestrictedView): Task[RestrictedView] = diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/rdf/RdfConversions.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/rdf/RdfConversions.scala index 1b7c9264ab..f01da22943 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/rdf/RdfConversions.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/rdf/RdfConversions.scala @@ -42,6 +42,8 @@ object RdfConversions { implicit val selfjoinConverter: Boolean => Either[String, SelfJoin] = value => Right(SelfJoin.from(value)) implicit val descriptionConverter: LangString => Either[String, Description] = langString => Description.from(StringLiteralV2.from(langString.value, langString.lang)) + implicit val copyrightAttributionConverter: String => Either[String, CopyrightAttribution] = CopyrightAttribution.from + implicit val licenseConverter: String => Either[String, License] = License.from // User properties implicit val usernameConverter: String => Either[String, Username] = Username.from diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala index 045cc486a4..9268f786c1 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala @@ -14,6 +14,8 @@ import zio.* import org.knora.webapi.messages.OntologyConstants.KnoraAdmin import org.knora.webapi.messages.OntologyConstants.KnoraAdmin.* +import org.knora.webapi.messages.OntologyConstants.KnoraBase.HasCopyrightAttribution +import org.knora.webapi.messages.OntologyConstants.KnoraBase.HasLicense import org.knora.webapi.slice.admin.domain.model.KnoraProject import org.knora.webapi.slice.admin.domain.model.KnoraProject.* import org.knora.webapi.slice.admin.domain.model.RestrictedView @@ -48,6 +50,8 @@ final case class KnoraProjectRepoLive( Vocabulary.KnoraAdmin.projectLongname, Vocabulary.KnoraAdmin.projectRestrictedViewSize, Vocabulary.KnoraAdmin.projectRestrictedViewWatermark, + Vocabulary.KnoraBase.hasCopyrightAttribution, + Vocabulary.KnoraBase.hasLicense, ), ) @@ -91,16 +95,18 @@ object KnoraProjectRepoLive { } yield size.orElse(watermark).getOrElse(RestrictedView.default) for { - iri <- resource.getSubjectIri - shortcode <- resource.getStringLiteralOrFail[Shortcode](ProjectShortcode) - shortname <- resource.getStringLiteralOrFail[Shortname](ProjectShortname) - longname <- resource.getStringLiteral[Longname](ProjectLongname) - description <- resource.getLangStringLiteralsOrFail[Description](ProjectDescription) - keywords <- resource.getStringLiterals[Keyword](ProjectKeyword) - logo <- resource.getStringLiteral[Logo](ProjectLogo) - status <- resource.getBooleanLiteralOrFail[Status](StatusProp) - selfjoin <- resource.getBooleanLiteralOrFail[SelfJoin](HasSelfJoinEnabled) - restrictedView <- getRestrictedView + iri <- resource.getSubjectIri + shortcode <- resource.getStringLiteralOrFail[Shortcode](ProjectShortcode) + shortname <- resource.getStringLiteralOrFail[Shortname](ProjectShortname) + longname <- resource.getStringLiteral[Longname](ProjectLongname) + description <- resource.getLangStringLiteralsOrFail[Description](ProjectDescription) + keywords <- resource.getStringLiterals[Keyword](ProjectKeyword) + logo <- resource.getStringLiteral[Logo](ProjectLogo) + status <- resource.getBooleanLiteralOrFail[Status](StatusProp) + selfjoin <- resource.getBooleanLiteralOrFail[SelfJoin](HasSelfJoinEnabled) + copyrightAttribution <- resource.getStringLiteral[CopyrightAttribution](HasCopyrightAttribution) + license <- resource.getStringLiteral[License](HasLicense) + restrictedView <- getRestrictedView } yield KnoraProject( id = ProjectIri.unsafeFrom(iri.value), shortcode = shortcode, @@ -112,6 +118,8 @@ object KnoraProjectRepoLive { status = status, selfjoin = selfjoin, restrictedView = restrictedView, + copyrightAttribution = copyrightAttribution, + license = license, ) } @@ -136,6 +144,12 @@ object KnoraProjectRepoLive { case RestrictedView.Watermark(watermark) => pattern.andHas(Vocabulary.KnoraAdmin.projectRestrictedViewWatermark, watermark) } + + project.copyrightAttribution.foreach(attr => + pattern.andHas(Vocabulary.KnoraBase.hasCopyrightAttribution, attr.value), + ) + project.license.foreach(license => pattern.andHas(Vocabulary.KnoraBase.hasLicense, license.value)) + pattern } } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/repo/rdf/Vocabulary.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/repo/rdf/Vocabulary.scala index 0e6d2097bc..471e2710a0 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/repo/rdf/Vocabulary.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/repo/rdf/Vocabulary.scala @@ -108,6 +108,8 @@ object Vocabulary { val valueHasMapping: Iri = iri(kb + "valueHasMapping") val valueHasMaxStandoffStartIndex: Iri = iri(kb + "valueHasMaxStandoffStartIndex") val valueHasStandoff: Iri = iri(kb + "valueHasStandoff") + val hasCopyrightAttribution: Iri = iri(kb + "hasCopyrightAttribution") + val hasLicense: Iri = iri(kb + "hasLicense") val internalFilename: Iri = iri(kb + "internalFilename") val internalMimeType: Iri = iri(kb + "internalMimeType") diff --git a/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala b/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala index aa361d10d1..982ff13950 100644 --- a/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala +++ b/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala @@ -99,5 +99,7 @@ object TestDataFactory { Status.Active, SelfJoin.CannotJoin, RestrictedView.default, + Some(CopyrightAttribution.unsafeFrom("2024, Example Project")), + Some(License.unsafeFrom("CC-BY-4.0")), ) } diff --git a/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectServiceSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectServiceSpec.scala new file mode 100644 index 0000000000..030da89ce6 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectServiceSpec.scala @@ -0,0 +1,52 @@ +/* + * Copyright © 2021 - 2024 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.admin.domain.service + +import zio.ZIO +import zio.test.* +import zio.test.Assertion.* + +import org.knora.webapi.TestDataFactory +import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequestsAndResponses.ProjectUpdateRequest +import org.knora.webapi.slice.admin.domain.model.KnoraProject.CopyrightAttribution +import org.knora.webapi.slice.admin.domain.model.KnoraProject.License +import org.knora.webapi.slice.admin.domain.repo.KnoraProjectRepoInMemory +import org.knora.webapi.slice.ontology.repo.service.OntologyRepoInMemory +import org.knora.webapi.slice.resourceinfo.domain.IriConverter + +object KnoraProjectServiceSpec extends ZIOSpecDefault { + + private val projectService = ZIO.serviceWithZIO[KnoraProjectService] + private val repo = ZIO.serviceWithZIO[KnoraProjectRepoInMemory] + + private val updateSuite = suite("updateProject")( + test("should update the license and copyright attribution") { + val project = TestDataFactory.someProject + for { + _ <- repo(_.save(project)) + updateRequest = ProjectUpdateRequest( + copyrightAttribution = Some(CopyrightAttribution.unsafeFrom("Foo")), + license = Some(License.unsafeFrom("bar")), + ) + actual <- projectService(_.updateProject(project, updateRequest)) + } yield assertTrue( + actual == project.copy( + copyrightAttribution = Some(CopyrightAttribution.unsafeFrom("Foo")), + license = Some(License.unsafeFrom("bar")), + ), + ) + }, + ) + val spec = suite("KnoraProjectService")(updateSuite) + .provide( + KnoraProjectService.layer, + KnoraProjectRepoInMemory.layer, + OntologyRepoInMemory.emptyLayer, + IriConverter.layer, + StringFormatter.test, + ) +} diff --git a/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/ProjectServiceSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/ProjectServiceSpec.scala index 659c4433b8..e6f842faa6 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/ProjectServiceSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/ProjectServiceSpec.scala @@ -35,6 +35,8 @@ object ProjectServiceSpec extends ZIOSpecDefault { ontologies = List.empty, status = true, selfjoin = true, + copyrightAttribution = None, + license = None, ) assertTrue( ProjectService.projectDataNamedGraphV2(p).value == s"http://www.knora.org/data/$shortcode/$shortname", @@ -55,6 +57,8 @@ object ProjectServiceSpec extends ZIOSpecDefault { status = Status.Active, selfjoin = SelfJoin.CanJoin, restrictedView = RestrictedView.default, + copyrightAttribution = None, + license = None, ) assertTrue( ProjectService diff --git a/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLiveSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLiveSpec.scala index 0a4977a8fb..1fd1ed296c 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLiveSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLiveSpec.scala @@ -18,8 +18,10 @@ import zio.test.check import org.knora.webapi.messages.StringFormatter import org.knora.webapi.slice.admin.AdminConstants import org.knora.webapi.slice.admin.domain.model.KnoraProject +import org.knora.webapi.slice.admin.domain.model.KnoraProject.CopyrightAttribution import org.knora.webapi.slice.admin.domain.model.KnoraProject.Description import org.knora.webapi.slice.admin.domain.model.KnoraProject.Keyword +import org.knora.webapi.slice.admin.domain.model.KnoraProject.License import org.knora.webapi.slice.admin.domain.model.KnoraProject.Logo import org.knora.webapi.slice.admin.domain.model.KnoraProject.Longname import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri @@ -45,6 +47,8 @@ object KnoraProjectRepoLiveSpec extends ZIOSpecDefault { Status.Active, SelfJoin.CannotJoin, RestrictedView.default, + Some(CopyrightAttribution.unsafeFrom("2024, Example Project")), + Some(License.unsafeFrom("Apache-2.0")), ) private val someProjectTrig = @@ -62,7 +66,9 @@ object KnoraProjectRepoLiveSpec extends ZIOSpecDefault { | knora-admin:projectLogo "logo.png" ; | knora-admin:status true ; | knora-admin:hasSelfJoinEnabled false ; - | knora-admin:projectRestrictedViewSize "!128,128" . + | knora-admin:projectRestrictedViewSize "!128,128" ; + | knora-base:hasCopyrightAttribution "2024, Example Project" ; + | knora-base:hasLicense "Apache-2.0" . |} |""".stripMargin @@ -106,7 +112,7 @@ object KnoraProjectRepoLiveSpec extends ZIOSpecDefault { for { _ <- TriplestoreServiceInMemory.setDataSetFromTriG(someProjectTrig) project <- KnoraProjectRepo(_.findById(ProjectIri.unsafeFrom("http://rdfh.ch/projects/1234"))) - } yield assertTrue(project.contains(someProject)) + } yield assertTrue(project == Some(someProject)) }, test("return None if project does not exist") { for {