Skip to content

Commit

Permalink
feat: Add route that sets projectRestrictedViewSetting size (DEV-2304) (
Browse files Browse the repository at this point in the history
#2794)

Signed-off-by: Marcin Procyk <[email protected]>
Co-authored-by: Christian Kleinbölting <[email protected]>
  • Loading branch information
mpro7 and seakayone authored Sep 29, 2023
1 parent 8123dbd commit 738ab1c
Show file tree
Hide file tree
Showing 18 changed files with 388 additions and 26 deletions.
52 changes: 48 additions & 4 deletions docs/03-endpoints/api-admin/projects.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# Projects Endpoint

| Scope | Route | Operations | Explanation |
| --------------- | -------------------------------------------------------------- | ---------- | ----------------------------------------------------------------------- |
| --------------- | -------------------------------------------------------------- |------------|-------------------------------------------------------------------------|
| projects | `/admin/projects` | `GET` | [get all projects](#get-all-projects) |
| projects | `/admin/projects` | `POST` | [create a project](#create-a-new-project) |
| projects | `/admin/projects/shortname/{shortname}` | `GET` | [get a single project](#get-project-by-id) |
Expand All @@ -26,7 +26,8 @@
| view settings | `/admin/projects/shortname/{shortname}/RestrictedViewSettings` | `GET` | [get restricted view settings for a project](#restricted-view-settings) |
| view settings | `/admin/projects/shortcode/{shortcode}/RestrictedViewSettings` | `GET` | [get restricted view settings for a project](#restricted-view-settings) |
| view settings | `/admin/projects/iri/{iri}/RestrictedViewSettings` | `GET` | [get restricted view settings for a project](#restricted-view-settings) |

| view settings | `/admin/projects/iri/{iri}/RestrictedViewSettings` | `POST` | [set restricted view settings for a project](#restricted-view-settings) |
| view settings | `/admin/projects/shortcode/{shortcode}/RestrictedViewSettings` | `POST` | [set restricted view settings for a project](#restricted-view-settings) |

## Project Operations

Expand Down Expand Up @@ -775,7 +776,7 @@ Example response:

```

### Restricted View Settings
### Get Restricted View Settings

Permissions: ProjectAdmin

Expand All @@ -799,7 +800,7 @@ curl --request GET 'http://0.0.0.0:3333/admin/projects/shortname/anything/Restri
```

```bash
curl --request GET 'http://0.0.0.0:3333/admin/projects/iri/http%3A%2F%2Frdfh.ch%2Fprojects%2F0001/RestrictedViewSettings'
curl --request GET 'http://0.0.0.0:3333/admin/projects/iri/http%3A%2F%2Frdfh.ch%2Fprojects%2F0001/RestrictedViewSettings' \
--header 'Authorization: Basic cm9vdEBleGFtcGxlLmNvbTp0ZXN0'
```

Expand All @@ -814,6 +815,49 @@ Example response:
}
```

### Set Restricted View Settings

Both routes take String parameter which sets restricted view size in one of two formats: as an image dimensions or a
percentage. The dimensions pattern looks like: `!X,X`, where X is the number representing scaled image dimensions in
a square, so that the width and height of the returned image are not greater than the requested value.
Example: `!512,512` means the image's bigger side will be set to 512 pixels, setting the other side respectively to
image aspect ratio. The percentage pattern looks like: `pct:X`, where X is the number between 1-100 representing the
percentage the image will be scaled to. Example: `pct:1` means the image will be scaled to 1% of the original image
size.

Permissions: ProjectAdmin/SystemAdmin

Request definition:
- `POST /admin/projects/iri/{iri}/RestrictedViewSettings`
- `POST /admin/projects/shortcode/{shortcode}/RestrictedViewSettings`

Description: Set the project's restricted view

Required payload:
- `size`

Example request:

```bash
curl --request POST 'http://0.0.0.0:5555/admin/projects/iri/http%3A%2F%2Frdfh.ch%2Fprojects%2F0001/RestrictedViewSettings' \
--header 'Authorization: Basic cm9vdEBleGFtcGxlLmNvbTp0ZXN0' \
--data '{"size": "!512,512"}
```
```bash
curl --request POST 'http://0.0.0.0:5555/admin/projects/shortcode/0001/RestrictedViewSettings' \
--header 'Authorization: Basic cm9vdEBleGFtcGxlLmNvbTp0ZXN0' \
--data '{"size": "!512,512"}
```

Example response:

```json
{
"size": "!512,512"
}
```

Operates on the following properties:
- `knora-admin:projectRestrictedViewSize`: the IIIF size value
- `knora-admin:projectRestrictedViewWatermark`: the path to the watermark image. **Currently not used!**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import org.knora.webapi.messages.admin.responder.projectsmessages._
import org.knora.webapi.messages.admin.responder.usersmessages.UserADM
import org.knora.webapi.messages.admin.responder.usersmessages.UsersADMJsonProtocol._
import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject
import org.knora.webapi.messages.store.triplestoremessages.TriplestoreJsonProtocol
import org.knora.webapi.messages.util.rdf.RdfModel
import org.knora.webapi.sharedtestdata.SharedTestDataADM
import org.knora.webapi.util.AkkaHttpUtils
Expand All @@ -39,7 +38,7 @@ import pekko.util.Timeout
/**
* End-to-End (E2E) test specification for testing groups endpoint.
*/
class ProjectsADME2EZioHttpSpec extends E2ESpec with ProjectsADMJsonProtocol with TriplestoreJsonProtocol {
class ProjectsADME2EZioHttpSpec extends E2ESpec with ProjectsADMJsonProtocol {

private val rootEmail = SharedTestDataADM.rootUser.email
private val testPass = SharedTestDataADM.testPass
Expand Down Expand Up @@ -776,5 +775,103 @@ class ProjectsADME2EZioHttpSpec extends E2ESpec with ProjectsADMJsonProtocol wit
)
}
}

if (baseApiUrl.contains("5555")) "used to set RestrictedViewSize by project IRI" should {
"return requested value to be set with 200 Response Status" in {
val encodedIri = URLEncoder.encode(SharedTestDataADM.imagesProject.id, "utf-8")
val payload = """{"size":"pct:1"}"""
val request =
Post(
baseApiUrl + s"/admin/projects/iri/$encodedIri/RestrictedViewSettings",
HttpEntity(ContentTypes.`application/json`, payload)
) ~> addCredentials(
BasicHttpCredentials(rootEmail, testPass)
)
val response: HttpResponse = singleAwaitingRequest(request)
val result: String = responseToString(response)
assert(response.status === StatusCodes.OK)
assert(payload === result)
}

"return the `BadRequest` if the size value is invalid" in {
val encodedIri = URLEncoder.encode(SharedTestDataADM.imagesProject.id, "utf-8")
val payload = """{"size":"pct:0"}"""
val request =
Post(
baseApiUrl + s"/admin/projects/iri/$encodedIri/RestrictedViewSettings",
HttpEntity(ContentTypes.`application/json`, payload)
) ~> addCredentials(
BasicHttpCredentials(rootEmail, testPass)
)
val response: HttpResponse = singleAwaitingRequest(request)
val result: String = responseToString(response)
assert(response.status === StatusCodes.BadRequest)
assert(result.contains("Invalid RestrictedViewSize: pct:0"))
}

"return `Forbidden` for the user who is not a system nor project admin" in {
val encodedIri = URLEncoder.encode(SharedTestDataADM.imagesProject.id, "utf-8")
val payload = """{"size":"pct:1"}"""
val request =
Post(
baseApiUrl + s"/admin/projects/iri/$encodedIri/RestrictedViewSettings",
HttpEntity(ContentTypes.`application/json`, payload)
) ~> addCredentials(
BasicHttpCredentials(SharedTestDataADM.imagesUser02.email, testPass)
)
val response: HttpResponse = singleAwaitingRequest(request)
assert(response.status === StatusCodes.Forbidden)
}
}
else "used to set RestrictedViewSize by project IRI" ignore ()

if (baseApiUrl.contains("5555")) "used to set RestrictedViewSize by project Shortcode" should {
"return requested value to be set with 200 Response Status" in {
val shortcode = SharedTestDataADM.imagesProject.shortcode
val payload = """{"size":"pct:1"}"""
val request =
Post(
baseApiUrl + s"/admin/projects/shortcode/$shortcode/RestrictedViewSettings",
HttpEntity(ContentTypes.`application/json`, payload)
) ~> addCredentials(
BasicHttpCredentials(rootEmail, testPass)
)
val response: HttpResponse = singleAwaitingRequest(request)
val result: String = responseToString(response)
assert(response.status === StatusCodes.OK)
assert(payload === result)
}

"return the `BadRequest` if the size value is invalid" in {
val shortcode = SharedTestDataADM.imagesProject.shortcode
val payload = """{"size":"pct:0"}"""
val request =
Post(
baseApiUrl + s"/admin/projects/shortcode/$shortcode/RestrictedViewSettings",
HttpEntity(ContentTypes.`application/json`, payload)
) ~> addCredentials(
BasicHttpCredentials(rootEmail, testPass)
)
val response: HttpResponse = singleAwaitingRequest(request)
val result: String = responseToString(response)
assert(response.status === StatusCodes.BadRequest)
assert(result.contains("Invalid RestrictedViewSize: pct:0"))
}

"return `Forbidden` for the user who is not a system nor project admin" in {
val shortcode = SharedTestDataADM.imagesProject.shortcode
val payload = """{"size":"pct:1"}"""
val request =
Post(
baseApiUrl + s"/admin/projects/shortcode/$shortcode/RestrictedViewSettings",
HttpEntity(ContentTypes.`application/json`, payload)
) ~> addCredentials(
BasicHttpCredentials(SharedTestDataADM.imagesUser02.email, testPass)
)
val response: HttpResponse = singleAwaitingRequest(request)
assert(response.status === StatusCodes.Forbidden)
}
}
else "used to set RestrictedViewSize by project Shortcode" ignore ()
}
}
45 changes: 45 additions & 0 deletions webapi/src/main/scala/dsp/valueobjects/RestrictedViewSize.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright © 2021 - 2023 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors.
* SPDX-License-Identifier: Apache-2.0
*/

package dsp.valueobjects

import zio.json.JsonCodec

import scala.util.matching.Regex

/**
* RestrictedViewSize value object.
*/
sealed abstract case class RestrictedViewSize private (value: String)

object RestrictedViewSize {
def make(value: String): Either[String, RestrictedViewSize] = {
val trimmed: String = value.trim
// matches strings "pct:1-100"
val percentagePattern: Regex = "pct:[1-9][0-9]?0?$".r
// matches strings "!x,x" where x is a number of pixels
val dimensionsPattern: Regex = "!\\d+,\\d+$".r
def isSquare: Boolean = {
val substr = trimmed.substring(1).split(",").toSeq
substr.head == substr.last
}

if (value.isEmpty) Left(ErrorMessages.RestrictedViewSizeMissing)
else if (percentagePattern.matches(trimmed)) Right(new RestrictedViewSize(trimmed) {})
else if (dimensionsPattern.matches(trimmed) && isSquare) Right(new RestrictedViewSize(trimmed) {})
else Left(ErrorMessages.RestrictedViewSizeInvalid(value))
}

def unsafeFrom(value: String): RestrictedViewSize =
make(value).fold(s => throw new IllegalArgumentException(s), identity)

implicit val codec: JsonCodec[RestrictedViewSize] =
JsonCodec[String].transformOrFail(RestrictedViewSize.make, _.value)
}

object ErrorMessages {
val RestrictedViewSizeMissing = "RestrictedViewSize cannot be empty."
val RestrictedViewSizeInvalid = (v: String) => s"Invalid RestrictedViewSize: $v"
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import spray.json.DefaultJsonProtocol
import spray.json.JsValue
import spray.json.JsonFormat
import spray.json.RootJsonFormat
import zio.json.DeriveJsonCodec
import zio.json.JsonCodec
import zio.prelude.Validation

import java.util.UUID
Expand All @@ -21,6 +23,7 @@ import dsp.errors.ValidationException
import dsp.valueobjects.Iri
import dsp.valueobjects.Iri.ProjectIri
import dsp.valueobjects.Project._
import dsp.valueobjects.RestrictedViewSize
import dsp.valueobjects.V2
import org.knora.webapi.IRI
import org.knora.webapi.core.RelayedMessage
Expand Down Expand Up @@ -346,6 +349,12 @@ case class ProjectRestrictedViewSettingsGetResponseADM(settings: ProjectRestrict
def toJsValue: JsValue = projectRestrictedViewGetResponseADMFormat.write(this)
}

case class ProjectRestrictedViewSizeResponseADM(size: RestrictedViewSize)
object ProjectRestrictedViewSizeResponseADM {
implicit val codec: JsonCodec[ProjectRestrictedViewSizeResponseADM] =
DeriveJsonCodec.gen[ProjectRestrictedViewSizeResponseADM]
}

/**
* Represents an answer to a project creating/modifying operation.
*
Expand Down Expand Up @@ -606,9 +615,7 @@ trait ProjectsADMJsonProtocol extends SprayJsonSupport with DefaultJsonProtocol
jsonFormat(ProjectKeywordsGetResponseADM, "keywords")
implicit val projectRestrictedViewGetResponseADMFormat: RootJsonFormat[ProjectRestrictedViewSettingsGetResponseADM] =
jsonFormat(ProjectRestrictedViewSettingsGetResponseADM, "settings")

implicit val projectOperationResponseADMFormat: RootJsonFormat[ProjectOperationResponseADM] = rootFormat(
lazyFormat(jsonFormat(ProjectOperationResponseADM, "project"))
)

}
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,10 @@ object ProjectUpdatePayloadADM {
)
}
}

final case class ProjectSetRestrictedViewSizePayload(size: String)

object ProjectSetRestrictedViewSizePayload {
implicit val codec: JsonCodec[ProjectSetRestrictedViewSizePayload] =
DeriveJsonCodec.gen[ProjectSetRestrictedViewSizePayload]
}
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,15 @@ trait ProjectsResponderADM {
/**
* Get project's restricted view settings.
*
* @param id the project's identifier (IRI / shortcode / shortname / UUID)
* @param id the project's identifier (IRI / shortcode / shortname)
* @return [[ProjectRestrictedViewSettingsADM]]
*/
def projectRestrictedViewSettingsGetADM(id: ProjectIdentifierADM): Task[Option[ProjectRestrictedViewSettingsADM]]

/**
* Get project's restricted view settings.
*
* @param id the project's identifier (IRI / shortcode / shortname / UUID)
* @param id the project's identifier (IRI / shortcode / shortname)
* @return [[ProjectRestrictedViewSettingsGetResponseADM]]
*/
def projectRestrictedViewSettingsGetRequestADM(
Expand Down Expand Up @@ -646,7 +646,6 @@ final case class ProjectsResponderADMLive(
)
)
}

}

/**
Expand Down Expand Up @@ -816,6 +815,9 @@ final case class ProjectsResponderADMLive(
)
// create permissions for admins and members of the new group
_ <- createPermissionsForAdminsAndMembersOfNewProject(newProjectIRI)
// TODO: DEV-2626 add default value here
// defaultSize = ""
// _ <- setProjectRestrictedViewSettings(id.value, requestingUser, defaultSize)

} yield ProjectOperationResponseADM(project = newProjectADM.unescape)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
package org.knora.webapi.routing

import org.apache.pekko
import org.apache.pekko.http.scaladsl.model.StatusCodes.{MethodNotAllowed, NotFound}
import org.apache.pekko.http.scaladsl.model.{HttpResponse, StatusCodes}
import org.apache.pekko.http.scaladsl.model.HttpResponse
import org.apache.pekko.http.scaladsl.model.StatusCodes.MethodNotAllowed
import org.apache.pekko.http.scaladsl.model.StatusCodes.NotFound

import org.knora.webapi.instrumentation.InstrumentationSupport

import pekko.http.scaladsl.server.Directive0
import pekko.http.scaladsl.server.Directives._

Expand Down
Loading

0 comments on commit 738ab1c

Please sign in to comment.