Skip to content

Commit

Permalink
feat: The restricted view must be either restricted with a watermark …
Browse files Browse the repository at this point in the history
…or by a particular size (DEV-3356) (#3080)
  • Loading branch information
seakayone authored Mar 4, 2024
1 parent 2715aa1 commit 75f5363
Show file tree
Hide file tree
Showing 11 changed files with 138 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -787,7 +787,7 @@ class ProjectsADME2ESpec extends E2ESpec with ProjectsADMJsonProtocol {
)
val response = singleAwaitingRequest(request)
assert(response.status === StatusCodes.OK)
assert(responseToString(response) === """{"size":"pct:1","watermark":false}""")
assert(responseToString(response) === """{"size":"pct:1"}""")
}

"return the `BadRequest` if the size value is invalid" in {
Expand Down Expand Up @@ -822,12 +822,11 @@ class ProjectsADME2ESpec extends E2ESpec with ProjectsADMJsonProtocol {
}

"used to set RestrictedViewSize by project Shortcode" should {
"return requested value to be set with 200 Response Status" in {
"when setting watermark to false return default size with 200 Response Status" in {
val shortcode = SharedTestDataADM.imagesProject.shortcode
val payload = """
|{
| "size":"!512,512",
| "watermark":true
| "watermark": false
|}""".stripMargin
val request =
Post(
Expand All @@ -838,7 +837,7 @@ class ProjectsADME2ESpec extends E2ESpec with ProjectsADMJsonProtocol {
)
val response: HttpResponse = singleAwaitingRequest(request)
assert(response.status === StatusCodes.OK)
assert(responseToString(response) === """{"size":"!512,512","watermark":true}""")
assert(responseToString(response) === """{"size":"!128,128"}""")
}

"return the `BadRequest` if the size value is invalid" in {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequestsAndRespon
import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequestsAndResponses.ProjectUpdateRequest
import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri
import org.knora.webapi.slice.admin.domain.model.RestrictedView
import org.knora.webapi.slice.admin.domain.model.RestrictedViewSize
import org.knora.webapi.slice.admin.domain.model.User
import org.knora.webapi.slice.admin.domain.model.UserIri
import org.knora.webapi.slice.admin.domain.service.ProjectADMService
Expand Down Expand Up @@ -745,10 +744,7 @@ final case class ProjectsResponderADMLive(
)
// create permissions for admins and members of the new group
_ <- createPermissionsForAdminsAndMembersOfNewProject(newProjectIRI)
_ <- projectService.setProjectRestrictedView(
newProjectADM,
RestrictedView(RestrictedViewSize.default, watermark = false)
)
_ <- projectService.setProjectRestrictedView(newProjectADM, RestrictedView.default)

} yield ProjectOperationResponseADM(project = newProjectADM.unescape)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ import org.knora.webapi.slice.admin.domain.model.ListProperties.Labels
import org.knora.webapi.slice.admin.domain.model.ListProperties.ListIri
import org.knora.webapi.slice.admin.domain.model.ListProperties.ListName
import org.knora.webapi.slice.admin.domain.model.ListProperties.Position
import org.knora.webapi.slice.admin.domain.model.Password
import org.knora.webapi.slice.admin.domain.model.RestrictedView
import org.knora.webapi.slice.admin.domain.model.SystemAdmin
import org.knora.webapi.slice.admin.domain.model.UserIri
import org.knora.webapi.slice.admin.domain.model.UserStatus
import org.knora.webapi.slice.admin.domain.model.Username
import org.knora.webapi.slice.admin.domain.model.*
import org.knora.webapi.slice.common.Value.BooleanValue
import org.knora.webapi.slice.common.Value.IntValue
Expand All @@ -42,7 +48,6 @@ object Codecs {
implicit val logo: StringCodec[Logo] = stringCodec(Logo.from)
implicit val longname: StringCodec[Longname] = stringCodec(Longname.from)
implicit val projectIri: StringCodec[ProjectIri] = stringCodec(ProjectIri.from)
implicit val restrictedViewSize: StringCodec[RestrictedViewSize] = stringCodec(RestrictedViewSize.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)
Expand Down Expand Up @@ -89,11 +94,14 @@ object Codecs {
implicit val assetId: StringCodec[AssetId] = stringCodec(AssetId.from, _.value)

// project
implicit val keyword: StringCodec[Keyword] = stringCodec(Keyword.from)
implicit val logo: StringCodec[Logo] = stringCodec(Logo.from)
implicit val longname: StringCodec[Longname] = stringCodec(Longname.from)
implicit val projectIri: StringCodec[ProjectIri] = stringCodec(ProjectIri.from)
implicit val restrictedViewSize: StringCodec[RestrictedViewSize] = stringCodec(RestrictedViewSize.from)
implicit val keyword: StringCodec[Keyword] = stringCodec(Keyword.from)
implicit val logo: StringCodec[Logo] = stringCodec(Logo.from)
implicit val longname: StringCodec[Longname] = stringCodec(Longname.from)
implicit val projectIri: StringCodec[ProjectIri] = stringCodec(ProjectIri.from)
implicit val restrictedViewSize: StringCodec[RestrictedView.Size] = stringCodec(RestrictedView.Size.from)
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequestsAndRespon
import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequestsAndResponses.ProjectUpdateRequest
import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequestsAndResponses.RestrictedViewResponse
import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequestsAndResponses.SetRestrictedViewRequest
import org.knora.webapi.slice.admin.domain.model.RestrictedViewSize
import org.knora.webapi.slice.admin.domain.model.RestrictedView
import org.knora.webapi.slice.common.api.BaseEndpoints

final case class ProjectsEndpoints(
Expand Down Expand Up @@ -94,17 +94,19 @@ final case class ProjectsEndpoints(
object Secured {
private val bodyProjectSetRestrictedViewSizeRequest =
zioJsonBody[SetRestrictedViewRequest]
.default(SetRestrictedViewRequest(RestrictedViewSize.default, watermark = Some(false)))
.description(
"The restricted view settings to be set.\n" +
"The image restrictions support two of the (IIIF size)[https://iiif.io/api/image/3.0/#42-size] forms:\n" +
"Set how all still image resources of a projects should be displayed when viewed as restricted.\n" +
"This can be either a size restriction or a watermark.\n" +
"For that, we support two of the (IIIF size)[https://iiif.io/api/image/3.0/#42-size] forms:\n" +
"* `!d,d` The returned image is scaled so that the width and height of the returned image are not " +
"greater than d, while maintaining the aspect ratio.\n" +
"* `pct:n` The width and height of the returned image is scaled to n percent of the width and height " +
"of the extracted region. 1<= n <= 100.\n\n" +
"If the watermark is set to `true`, the returned image will be watermarked."
"If the watermark is set to `true`, the returned image will be watermarked, " +
"otherwise the default size " + RestrictedView.Size.default.value + " is set.\n\n" +
"It is only possible to set either the size or the watermark, not both at the same time."
)
.example(SetRestrictedViewRequest(RestrictedViewSize.default, watermark = Some(false)))
.example(SetRestrictedViewRequest(Some(RestrictedView.Size.default), None))

val postAdminProjectsByProjectIriRestrictedViewSettings = baseEndpoints.securedEndpoint.post
.in(projectsByIri / restrictedViewSettings)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@

package org.knora.webapi.slice.admin.api.model

import zio.IO
import zio.ZIO
import zio.json.DeriveJsonCodec
import zio.json.JsonCodec

import dsp.errors.BadRequestException
import org.knora.webapi.slice.admin.api.Codecs.ZioJsonCodec.*
import org.knora.webapi.slice.admin.domain.model.KnoraProject.*
import org.knora.webapi.slice.admin.domain.model.RestrictedViewSize
import org.knora.webapi.slice.admin.domain.model.RestrictedView

object ProjectsEndpointsRequestsAndResponses {

Expand Down Expand Up @@ -42,13 +45,34 @@ object ProjectsEndpointsRequestsAndResponses {
implicit val codec: JsonCodec[ProjectUpdateRequest] = DeriveJsonCodec.gen[ProjectUpdateRequest]
}

final case class SetRestrictedViewRequest(size: RestrictedViewSize, watermark: Option[Boolean])
final case class SetRestrictedViewRequest(
size: Option[RestrictedView.Size],
watermark: Option[RestrictedView.Watermark]
) {
def toRestrictedView: IO[BadRequestException, RestrictedView] =
(size, watermark) match {
case (Some(size), None) => ZIO.succeed(size)
case (None, Some(watermark)) => ZIO.succeed(RestrictedView.Watermark.from(watermark.value))
case _ =>
ZIO.fail(BadRequestException("Specify either the size or the watermark; if none was provided, include one."))
}
}

object SetRestrictedViewRequest {
implicit val codec: JsonCodec[SetRestrictedViewRequest] = DeriveJsonCodec.gen[SetRestrictedViewRequest]
}

final case class RestrictedViewResponse(size: RestrictedViewSize, watermark: Boolean)
final case class RestrictedViewResponse(
size: Option[RestrictedView.Size],
watermark: Option[RestrictedView.Watermark]
)
object RestrictedViewResponse {
implicit val codec: JsonCodec[RestrictedViewResponse] = DeriveJsonCodec.gen[RestrictedViewResponse]

def from(restrictedView: RestrictedView): RestrictedViewResponse =
restrictedView match {
case size: RestrictedView.Size => RestrictedViewResponse(Some(size), None)
case watermark: RestrictedView.Watermark => RestrictedViewResponse(None, Some(watermark))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequestsAndRespon
import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri
import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode
import org.knora.webapi.slice.admin.domain.model.KnoraProject.Status
import org.knora.webapi.slice.admin.domain.model.RestrictedView
import org.knora.webapi.slice.admin.domain.model.User
import org.knora.webapi.slice.admin.domain.service.KnoraProjectRepo
import org.knora.webapi.slice.admin.domain.service.ProjectADMService
Expand Down Expand Up @@ -274,19 +273,19 @@ final case class ProjectsADMRestServiceLive(
* @param id The project's id represented by iri, shortcode or shortname.
* @param user Requesting user.
* @param req Contains the values to be set.
* @return [[ProjectRestrictedViewSizeResponseADM]].
* @return [[RestrictedViewResponse]].
*/
override def updateProjectRestrictedViewSettings(
id: ProjectIdentifierADM,
user: User,
req: SetRestrictedViewRequest
): Task[RestrictedViewResponse] =
for {
project <- projectRepo.findById(id).someOrFail(NotFoundException(s"Project '${getId(id)}' not found."))
_ <- permissionService.ensureSystemAdminOrProjectAdmin(user, project)
watermarkBool = req.watermark.getOrElse(false)
_ <- projectService.setProjectRestrictedView(project, RestrictedView(req.size, watermarkBool))
} yield RestrictedViewResponse(req.size, watermarkBool)
project <- projectRepo.findById(id).someOrFail(NotFoundException(s"Project '${getId(id)}' not found."))
_ <- permissionService.ensureSystemAdminOrProjectAdmin(user, project)
restrictedView <- req.toRestrictedView
newSettings <- projectService.setProjectRestrictedView(project, restrictedView)
} yield RestrictedViewResponse.from(newSettings)

override def exportProject(shortcodeStr: String, user: User): Task[Unit] =
convertStringToShortcodeId(shortcodeStr).flatMap(exportProject(_, user))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,42 @@ package org.knora.webapi.slice.admin.domain.model
import scala.util.matching.Regex

import org.knora.webapi.slice.common.StringValueCompanion
import org.knora.webapi.slice.common.Value.BooleanValue
import org.knora.webapi.slice.common.Value.StringValue

final case class RestrictedView(size: RestrictedViewSize, watermark: Boolean)
sealed trait RestrictedView

final case class RestrictedViewSize private (value: String) extends AnyVal with StringValue
object RestrictedView {

object RestrictedViewSize extends StringValueCompanion[RestrictedViewSize] {
val default: RestrictedView = Size.default

// matches strings "pct:n" with n between 1 and 100
private val percentagePattern: Regex = "pct:(?:100|[1-9][0-9]?)$".r
final case class Watermark private (value: Boolean) extends RestrictedView with BooleanValue
object Watermark {

// matches strings "!x,x" where x is a positive integer and represents the dimensions of the restricted view
private val dimensionsPattern: Regex = "!(\\d+),(\\1)$".r
val On: Watermark = Watermark(true)
val Off: Watermark = Watermark(false)

val default: RestrictedViewSize = RestrictedViewSize.unsafeFrom("!512,512")
def from(value: Boolean): Watermark = if (value) On else Off
}

def from(value: String): Either[String, RestrictedViewSize] =
value match {
case _ if value.isEmpty => Left("RestrictedViewSize cannot be empty.")
case _ if percentagePattern.matches(value) => Right(RestrictedViewSize(value))
case _ if dimensionsPattern.matches(value) => Right(RestrictedViewSize(value))
case _ => Left(s"Invalid RestrictedViewSize: $value")
}
final case class Size private (value: String) extends RestrictedView with StringValue

object Size extends StringValueCompanion[Size] {

// matches strings "pct:n" with n between 1 and 100
private val percentagePattern: Regex = "pct:(?:100|[1-9][0-9]?)$".r

// matches strings "!x,x" where x is a positive integer and represents the dimensions of the restricted view
private val dimensionsPattern: Regex = "!(\\d+),(\\1)$".r

val default: Size = Size.unsafeFrom("!128,128")

def from(value: String): Either[String, Size] =
value match {
case _ if value.isEmpty => Left("RestrictedViewSize cannot be empty.")
case _ if percentagePattern.matches(value) => Right(Size(value))
case _ if dimensionsPattern.matches(value) => Right(Size(value))
case _ => Left(s"Invalid RestrictedViewSize: $value")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,15 @@ final case class ProjectADMService(
.map(_ :+ projectGraph)
}

def setProjectRestrictedView(project: KnoraProject, settings: RestrictedView): Task[Unit] =
projectRepo.setProjectRestrictedView(project, settings)
def setProjectRestrictedView(project: KnoraProject, settings: RestrictedView): Task[RestrictedView] = {
val newSettings = settings match {
case RestrictedView.Watermark(false) => RestrictedView.default
case s => s
}
projectRepo.setProjectRestrictedView(project, newSettings).as(newSettings)
}

def setProjectRestrictedView(project: ProjectADM, settings: RestrictedView): Task[Unit] =
def setProjectRestrictedView(project: ProjectADM, settings: RestrictedView): Task[RestrictedView] =
setProjectRestrictedView(toKnoraProject(project), settings)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ object Vocabulary {
val belongsToProject: Iri = Rdf.iri(KnoraAdminPrefixExpansion, "belongsToProject")
val groupName: Iri = Rdf.iri(KnoraAdminPrefixExpansion, "groupName")
val groupDescriptions: Iri = Rdf.iri(KnoraAdminPrefixExpansion, "groupDescriptions")

// project properties
val projectRestrictedViewSize: Iri = Rdf.iri(KnoraAdminPrefixExpansion, "projectRestrictedViewSize")
val projectRestrictedViewWatermark: Iri = Rdf.iri(KnoraAdminPrefixExpansion, "projectRestrictedViewWatermark")
}

object KnoraBase {
Expand Down
Loading

0 comments on commit 75f5363

Please sign in to comment.