Skip to content

Commit 50aa52e

Browse files
authored
Merge pull request #106 from clowder-framework/extractor-catalog-labels
Extractor Catalog: Extractor Labels features
2 parents bf87df0 + 9ab65a8 commit 50aa52e

20 files changed

+752
-73
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/)
55
and this project adheres to [Semantic Versioning](http://semver.org/).
66

7+
## Unreleased
8+
9+
### Fixed
10+
- Fixed permissions checks on search results for search interfaces that would cause misleading counts. [#60](https://github.com/clowder-framework/clowder/issues/60)
11+
712
## 1.12.0 - 2020-10-19
813
**_Warning:_**
914
- This update modifies the MongoDB schema. Make sure to start the application with `-DMONGOUPDATE=1`.
@@ -24,7 +29,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
2429
- Ignore the `update` field when posting to `/api/extractors`. [#89](https://github.com/clowder-framework/clowder/issues/89)
2530
- Search results were hardcoded to be in batches of 2.
2631

27-
# 1.11.2 - 2020-10-13
32+
## 1.11.2 - 2020-10-13
2833

2934
### Fixed
3035
- Clowder healthcheck was not correct, resulting in docker-compose never thinking it was healthy. This could also result

app/api/Extractions.scala

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ package api
33
import java.io.{FileInputStream, InputStream}
44
import java.net.URL
55
import java.util.Calendar
6-
import javax.inject.Inject
76

7+
import javax.inject.Inject
88
import controllers.Utils
99
import fileutils.FilesUtils
1010
import models._
@@ -475,7 +475,32 @@ class Extractions @Inject()(
475475
},
476476
info => {
477477
extractors.updateExtractorInfo(info) match {
478-
case Some(u) => Ok(Json.obj("status" -> "OK", "message" -> ("Extractor info updated. ID = " + u.id)))
478+
case Some(u) => {
479+
// Create/assign any default labels for this extractor
480+
u.defaultLabels.foreach(labelStr => {
481+
val segments = labelStr.split("/")
482+
val (labelName, labelCategory) = if (segments.length > 1) {
483+
(segments(1), segments(0))
484+
} else {
485+
(segments(0), "Other")
486+
}
487+
extractors.getExtractorsLabel(labelName) match {
488+
case None => {
489+
// Label does not exist - create and assign it
490+
val createdLabel = extractors.createExtractorsLabel(labelName, Some(labelCategory), List[String](u.name))
491+
}
492+
case Some(lbl) => {
493+
// Label already exists, assign it
494+
if (!lbl.extractors.contains(u.name)) {
495+
val label = ExtractorsLabel(lbl.id, lbl.name, lbl.category, lbl.extractors ++ List[String](u.name))
496+
val updatedLabel = extractors.updateExtractorsLabel(label)
497+
}
498+
}
499+
}
500+
})
501+
502+
Ok(Json.obj("status" -> "OK", "message" -> ("Extractor info updated. ID = " + u.id)))
503+
}
479504
case None => BadRequest(Json.obj("status" -> "KO", "message" -> "Error updating extractor info"))
480505
}
481506
}
@@ -674,4 +699,71 @@ class Extractions @Inject()(
674699
Ok(toJson("added new event"))
675700
}
676701

702+
def createExtractorsLabel() = ServerAdminAction(parse.json) { implicit request =>
703+
// Fetch parameters from request body
704+
val (name, category, assignedExtractors) = parseExtractorsLabel(request)
705+
706+
// Validate that name is not empty
707+
if (name.isEmpty) {
708+
BadRequest("Label Name cannot be empty")
709+
} else {
710+
// Validate that name is unique
711+
extractors.getExtractorsLabel(name) match {
712+
case Some(lbl) => Conflict("Label name is already in use: " + lbl.name)
713+
case None => {
714+
// Create the new label
715+
val label = extractors.createExtractorsLabel(name, category, assignedExtractors)
716+
Ok(Json.toJson(label))
717+
}
718+
}
719+
}
720+
}
721+
722+
def updateExtractorsLabel(id: UUID) = ServerAdminAction(parse.json) { implicit request =>
723+
// Fetch parameters from request body
724+
val (name, category, assignedExtractors) = parseExtractorsLabel(request)
725+
726+
// Validate that name is not empty
727+
if (name.isEmpty) {
728+
BadRequest("Label Name cannot be empty")
729+
} else {
730+
// Validate that name is still unique
731+
extractors.getExtractorsLabel(name) match {
732+
case Some(lbl) => {
733+
// Exclude current id (in case name hasn't changed)
734+
if (lbl.id != id) {
735+
Conflict("Label name is already in use: " + lbl.name)
736+
} else {
737+
// Update the label
738+
val updatedLabel = extractors.updateExtractorsLabel(ExtractorsLabel(id, name, category, assignedExtractors))
739+
Ok(Json.toJson(updatedLabel))
740+
}
741+
}
742+
case None => {
743+
// Update the label
744+
val updatedLabel = extractors.updateExtractorsLabel(ExtractorsLabel(id, name, category, assignedExtractors))
745+
Ok(Json.toJson(updatedLabel))
746+
}
747+
}
748+
}
749+
}
750+
751+
def deleteExtractorsLabel(id: UUID) = ServerAdminAction { implicit request =>
752+
// Fetch existing label
753+
extractors.getExtractorsLabel(id) match {
754+
case Some(lbl) => {
755+
val deleted = extractors.deleteExtractorsLabel(lbl)
756+
Ok(Json.toJson(deleted))
757+
}
758+
case None => BadRequest("Failed to delete label: " + id)
759+
}
760+
}
761+
762+
def parseExtractorsLabel(request: UserRequest[JsValue]): (String, Option[String], List[String]) = {
763+
val name = (request.body \ "name").as[String]
764+
val category = (request.body \ "category").asOpt[String]
765+
val assignedExtractors = (request.body \ "extractors").as[List[String]]
766+
767+
(name, category, assignedExtractors)
768+
}
677769
}

app/api/Search.scala

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
package api
22

33
import api.Permission._
4-
import services.{RdfSPARQLService, DatasetService, FileService, CollectionService, PreviewService, SpaceService,
5-
MultimediaQueryService, ElasticsearchPlugin}
4+
import services.{RdfSPARQLService, PreviewService, SpaceService, MultimediaQueryService, ElasticsearchPlugin}
65
import play.Logger
76
import scala.collection.mutable.{ListBuffer, HashMap}
87
import util.{SearchUtils, SearchResult}
@@ -15,9 +14,6 @@ import models._
1514

1615
@Singleton
1716
class Search @Inject() (
18-
files: FileService,
19-
datasets: DatasetService,
20-
collections: CollectionService,
2117
previews: PreviewService,
2218
queries: MultimediaQueryService,
2319
spaces: SpaceService,
@@ -49,10 +45,16 @@ class Search @Inject() (
4945
(tag match {case Some(x) => s"&tag=$x" case None => ""})
5046

5147
// Add space filter to search here as a simple permissions check
52-
val permitted = spaces.listAccess(0, Set[Permission](Permission.ViewSpace), request.user, true, true, false, false).map(sp => sp.id)
48+
val superAdmin = request.user match {
49+
case Some(u) => u.superAdminMode
50+
case None => false
51+
}
52+
val permitted = if (superAdmin)
53+
List[UUID]()
54+
else
55+
spaces.listAccess(0, Set[Permission](Permission.ViewSpace), request.user, true, true, false, false).map(sp => sp.id)
5356

5457
val response = plugin.search(query, resource_type, datasetid, collectionid, spaceid, folderid, field, tag, from_index, size, permitted, request.user)
55-
5658
val result = SearchUtils.prepareSearchResponse(response, source_url, request.user)
5759
Ok(toJson(result))
5860
}
@@ -70,8 +72,19 @@ class Search @Inject() (
7072

7173
current.plugin[ElasticsearchPlugin] match {
7274
case Some(plugin) => {
75+
// Add space filter to search here as a simple permissions check
76+
val superAdmin = request.user match {
77+
case Some(u) => u.superAdminMode
78+
case None => false
79+
}
80+
val permitted = if (superAdmin)
81+
List[UUID]()
82+
else
83+
spaces.listAccess(0, Set[Permission](Permission.ViewSpace), request.user, true, true, false, false).map(sp => sp.id)
84+
85+
7386
val queryList = Json.parse(query).as[List[JsValue]]
74-
val response = plugin.search(queryList, grouping, from, size, user)
87+
val response = plugin.search(queryList, grouping, from, size, permitted, user)
7588

7689
// TODO: Better way to build a URL?
7790
val source_url = s"/api/search?query=$query&grouping=$grouping"

app/controllers/Application.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,9 @@ class Application @Inject() (files: FileService, collections: CollectionService,
496496
api.routes.javascript.Extractions.addExtractorInfo,
497497
api.routes.javascript.Extractions.getExtractorInfo,
498498
api.routes.javascript.Extractions.deleteExtractor,
499+
api.routes.javascript.Extractions.createExtractorsLabel,
500+
api.routes.javascript.Extractions.updateExtractorsLabel,
501+
api.routes.javascript.Extractions.deleteExtractorsLabel,
499502
api.routes.javascript.Folders.createFolder,
500503
api.routes.javascript.Folders.deleteFolder,
501504
api.routes.javascript.Folders.updateFolderName,
@@ -523,6 +526,7 @@ class Application @Inject() (files: FileService, collections: CollectionService,
523526
controllers.routes.javascript.Collections.newCollectionWithParent,
524527
controllers.routes.javascript.Spaces.stagingArea,
525528
controllers.routes.javascript.Extractors.selectExtractors,
529+
controllers.routes.javascript.Extractors.manageLabels,
526530
controllers.routes.javascript.Extractors.showJobHistory,
527531
controllers.routes.javascript.CurationObjects.submit,
528532
controllers.routes.javascript.CurationObjects.getCurationObject,

app/controllers/Extractors.scala

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class Extractors @Inject() (extractions: ExtractionService,
5555
implicit val user = request.user
5656
// Filter extractors by the trigger type if necessary
5757
var runningExtractors: List[ExtractorInfo] = extractorService.listExtractorsInfo(List.empty)
58+
5859
request.getQueryString("processTriggerSearchFilter") match {
5960
case Some("file/*") => runningExtractors = runningExtractors.filter(re => re.process.file.length > 0)
6061
case Some("dataset/*") => runningExtractors = runningExtractors.filter(re => re.process.dataset.length > 0)
@@ -63,7 +64,26 @@ class Extractors @Inject() (extractions: ExtractionService,
6364
}
6465
val selectedExtractors: List[String] = extractorService.getEnabledExtractors()
6566
val groups = extractions.groupByType(extractions.findAll())
66-
Ok(views.html.updateExtractors(runningExtractors, selectedExtractors, groups))
67+
val allLabels = extractorService.listExtractorsLabels()
68+
val categorizedLabels = allLabels.groupBy(_.category.getOrElse("Other"))
69+
request.getQueryString("labelFilter") match {
70+
case None => {}
71+
case Some(lblName) => allLabels.find(lbl => lblName == lbl.name) match {
72+
case None => {}
73+
case Some(label) => runningExtractors = runningExtractors.filter(re => label.extractors.contains(re.name))
74+
}
75+
}
76+
77+
Ok(views.html.updateExtractors(runningExtractors, selectedExtractors, groups, categorizedLabels))
78+
}
79+
80+
def manageLabels = ServerAdminAction { implicit request =>
81+
implicit val user = request.user
82+
val categories = List[String]("EXTRACT")
83+
val extractors = extractorService.listExtractorsInfo(categories)
84+
val labels = extractorService.listExtractorsLabels()
85+
86+
Ok(views.html.extractorLabels(labels, extractors))
6787
}
6888

6989
/**
@@ -107,7 +127,10 @@ class Extractors @Inject() (extractions: ExtractionService,
107127
implicit val user = request.user
108128
val targetExtractor = extractorService.listExtractorsInfo(List.empty).find(p => p.name == extractorName)
109129
targetExtractor match {
110-
case Some(extractor) => Ok(views.html.extractorDetails(extractor))
130+
case Some(extractor) => {
131+
val labels = extractorService.getLabelsForExtractor(extractor.name)
132+
Ok(views.html.extractorDetails(extractor, labels))
133+
}
111134
case None => InternalServerError("Extractor not found: " + extractorName)
112135
}
113136
}

app/models/Extraction.scala

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ case class ExtractorDetail(
7272
* @param external_services external services used by the extractor
7373
* @param libraries libraries on which the code depends
7474
* @param bibtex bibtext formatted citation of relevant papers
75+
* @param maturity indicates whether this extractor is ready for public consumption
76+
* * For example: "Development" (default), "Staging", or "Production"
77+
* @param defaultLabels the categorization label names that were imported by default
78+
* * For example: "Image", "Video", "Audio", etc
7579
* @param process events that should trigger this extractor to process
7680
* @param categories list of categories that apply to the extractor
7781
* @param parameters JSON schema representing allowed parameters
@@ -94,6 +98,7 @@ case class ExtractorInfo(
9498
libraries: List[String],
9599
bibtex: List[String],
96100
maturity: String = "Development",
101+
defaultLabels: List[String] = List[String](),
97102
process: ExtractorProcessTriggers = new ExtractorProcessTriggers(),
98103
categories: List[String] = List[String](ExtractorCategory.EXTRACT.toString),
99104
parameters: JsValue = JsObject(Seq())
@@ -146,6 +151,7 @@ object ExtractorInfo {
146151
(JsPath \ "libraries").read[List[String]].orElse(Reads.pure(List.empty)) and
147152
(JsPath \ "bibtex").read[List[String]].orElse(Reads.pure(List.empty)) and
148153
(JsPath \ "maturity").read[String].orElse(Reads.pure("Development")) and
154+
(JsPath \ "labels").read[List[String]].orElse(Reads.pure(List.empty)) and
149155
(JsPath \ "process").read[ExtractorProcessTriggers].orElse(Reads.pure(new ExtractorProcessTriggers())) and
150156
(JsPath \ "categories").read[List[String]].orElse(Reads.pure(List[String](ExtractorCategory.EXTRACT.toString))) and
151157
(JsPath \ "parameters").read[JsValue].orElse(Reads.pure(JsObject(Seq())))
@@ -185,3 +191,27 @@ case class ExtractionGroup(
185191
latestMsg: String,
186192
allMsgs: Map[UUID, List[Extraction]]
187193
)
194+
195+
case class ExtractorsLabel(
196+
id: UUID,
197+
name: String,
198+
category: Option[String],
199+
extractors: List[String]
200+
)
201+
202+
object ExtractorsLabel {
203+
implicit val writes = new Writes[ExtractorsLabel] {
204+
def writes(label: ExtractorsLabel) = Json.obj(
205+
"id" -> label.id,
206+
"name" -> label.name,
207+
"category" -> label.category,
208+
"extractors" -> label.extractors
209+
)
210+
}
211+
implicit val reads = (
212+
(JsPath \ "id").read[UUID] and
213+
(JsPath \ "name").read[String] and
214+
(JsPath \ "category").readNullable[String] and
215+
(JsPath \ "extractors").read[List[String]].orElse(Reads.pure(List.empty))
216+
)(ExtractorsLabel.apply _)
217+
}

0 commit comments

Comments
 (0)