From 51e043980e3445179cfec7648b735728e6f519ec Mon Sep 17 00:00:00 2001 From: tcnichol Date: Wed, 10 Feb 2021 13:48:56 -0600 Subject: [PATCH 01/38] adding yml for geoserver and extractors --- docker-compose.geoserver-extractors.yml | 90 +++++++++++++++++++++++++ docker-compose.geoserver.yml | 37 ++++++++++ 2 files changed, 127 insertions(+) create mode 100644 docker-compose.geoserver-extractors.yml create mode 100644 docker-compose.geoserver.yml diff --git a/docker-compose.geoserver-extractors.yml b/docker-compose.geoserver-extractors.yml new file mode 100644 index 000000000..bd0b22112 --- /dev/null +++ b/docker-compose.geoserver-extractors.yml @@ -0,0 +1,90 @@ +version: "3.2" + +services: + + ncsa_geo_shp: + image: clowder/extractors-geoshp-preview:${VERSION:-latest} + networks: + - clowder + environment: + - RABBITMQ_URI=${RABBITMQ_URI:-amqp://guest:guest@rabbitmq/%2F} + - RABBITMQ_EXCHANGE=${RABBITMQ_EXCHANGE:-clowder} + - REGISTRATION_ENDPOINTS=${REGISTRATION_ENDPOINTS} + - PROXY_ON=${PROXY_ON} + - PROXY_HOST=${PROXY_HOST} + - PROXY_URL=${PROXY_URL} + - GEOSERVER_USERNAME=${GEOSERVER_USERNAME:-admin} + - GEOSERVER_PASSWORD=${GEOSERVER_PASSWORD:-geoserver} + - GEOSERVER_WORKSPACE=${GEOSERVER_WORKSPACE} + - GEOSERVER_USER=${GEOSERVER_USER:-admin} + - GEOSERVER_URL=${GEOSERVER_URL} + depends_on: + - geoserver + restart: unless-stopped + deploy: + mode: replicated + replicas: 1 + placement: + constraints: + - node.role == worker + restart_policy: + condition: any + + extractor-geotiff-preview: + image: clowder/extractors-geotiff-preview + networks: + - clowder + environment: + - RABBITMQ_URI=${RABBITMQ_URI:-amqp://guest:guest@rabbitmq/%2F} + - RABBITMQ_EXCHANGE=${RABBITMQ_EXCHANGE:-clowder} + - REGISTRATION_ENDPOINTS=${REGISTRATION_ENDPOINTS} + - PROXY_ON=${PROXY_ON} + - PROXY_HOST=${PROXY_HOST} + - PROXY_URL=${PROXY_URL} + - GEOSERVER_USERNAME=${GEOSERVER_USERNAME:-admin} + - GEOSERVER_PASSWORD=${GEOSERVER_PASSWORD:-geoserver} + - GEOSERVER_WORKSPACE=${GEOSERVER_WORKSPACE} + - GEOSERVER_USER=${GEOSERVER_USER:-admin} + - GEOSERVER_URL=${GEOSERVER_URL} + depends_on: + - geoserver + restart: unless-stopped + deploy: + mode: replicated + replicas: 1 + placement: + constraints: + - node.role == worker + restart_policy: + condition: any + + extractor-geotiff-metadata: + image: clowder/extractors-geotiff-metadata + networks: + - clowder + environment: + - RABBITMQ_URI=${RABBITMQ_URI:-amqp://guest:guest@rabbitmq/%2F} + - RABBITMQ_EXCHANGE=${RABBITMQ_EXCHANGE:-clowder} + - REGISTRATION_ENDPOINTS=${REGISTRATION_ENDPOINTS} + - PROXY_ON=${PROXY_ON} + - PROXY_HOST=${PROXY_HOST} + - PROXY_URL=${PROXY_URL} + - GEOSERVER_USERNAME=${GEOSERVER_USERNAME:-admin} + - GEOSERVER_PASSWORD=${GEOSERVER_PASSWORD:-geoserver} + - GEOSERVER_WORKSPACE=${GEOSERVER_WORKSPACE} + - GEOSERVER_USER=${GEOSERVER_USER:-admin} + - GEOSERVER_URL=${GEOSERVER_URL} + depends_on: + - geoserver + restart: unless-stopped + deploy: + mode: replicated + replicas: 1 + placement: + constraints: + - node.role == worker + restart_policy: + condition: any + +networks: + clowder: \ No newline at end of file diff --git a/docker-compose.geoserver.yml b/docker-compose.geoserver.yml new file mode 100644 index 000000000..322f58b60 --- /dev/null +++ b/docker-compose.geoserver.yml @@ -0,0 +1,37 @@ +version: "3.2" + +services: + + geoserver: + image: clowder/geoserver:${VERSION:-latest} + networks: + - clowder +# user: "55242:17027" + environment: + - RABBITMQ_URI=${RABBITMQ_URI:-amqp://guest:guest@rabbitmq/%2F} + - RABBITMQ_EXCHANGE=${RABBITMQ_EXCHANGE:-clowder} + - REGISTRATION_ENDPOINTS=${REGISTRATION_ENDPOINTS} + - PROXY_ON=${PROXY_ON} + - PROXY_HOST=${PROXY_HOST} + - PROXY_URL=${PROXY_URL} + - GEOSERVER_USERNAME=${GEOSERVER_USERNAME:-admin} + - GEOSERVER_PASSWORD=${GEOSERVER_PASSWORD:-geoserver} + - GEOSERVER_WORKSPACE=${GEOSERVER_WORKSPACE} + - GEOSERVER_USER=${GEOSERVER_USER:-admin} + - GEOSERVER_URL=${GEOSERVER_URL} + ports: + - 8080:8080 + volumes: + - /generated/data/mounting:/data_dir + restart: unless-stopped + deploy: + mode: replicated + replicas: 1 + placement: + constraints: + - node.role == manager + restart_policy: + condition: any + +networks: + clowder: \ No newline at end of file From dcd1e3a46ff3478e7bb2b02105d32a155559b826 Mon Sep 17 00:00:00 2001 From: tcnichol Date: Fri, 2 Apr 2021 13:22:07 -0500 Subject: [PATCH 02/38] adding notes about what fields need to be replaced for extractors --- docker-compose.geoserver-extractors.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docker-compose.geoserver-extractors.yml b/docker-compose.geoserver-extractors.yml index bd0b22112..e185c4286 100644 --- a/docker-compose.geoserver-extractors.yml +++ b/docker-compose.geoserver-extractors.yml @@ -1,7 +1,13 @@ +# +#note - for each extractor, replace the following fields: +# - GEOSERVER_USERNAME +# - GEOSERVER_PASSWORD +# - GEOSERVER_USER +# - replace 'GEOSERVER_URL' with the actual ip or url for the geoserver (not 'localhost') + version: "3.2" services: - ncsa_geo_shp: image: clowder/extractors-geoshp-preview:${VERSION:-latest} networks: From ef0d8ad1867890228cd7953eb7c10dc7158ff870 Mon Sep 17 00:00:00 2001 From: tcnichol Date: Fri, 9 Apr 2021 14:50:53 -0500 Subject: [PATCH 03/38] using TRAEFIK HOST for proxy replacing ip with 'geoserver' --- docker-compose.geoserver-extractors.yml | 36 +++++-------------------- docker-compose.geoserver.yml | 10 +------ 2 files changed, 7 insertions(+), 39 deletions(-) diff --git a/docker-compose.geoserver-extractors.yml b/docker-compose.geoserver-extractors.yml index e185c4286..7806a7484 100644 --- a/docker-compose.geoserver-extractors.yml +++ b/docker-compose.geoserver-extractors.yml @@ -17,24 +17,16 @@ services: - RABBITMQ_EXCHANGE=${RABBITMQ_EXCHANGE:-clowder} - REGISTRATION_ENDPOINTS=${REGISTRATION_ENDPOINTS} - PROXY_ON=${PROXY_ON} - - PROXY_HOST=${PROXY_HOST} + - PROXY_HOST=${TRAEFIK_HOST} - PROXY_URL=${PROXY_URL} - GEOSERVER_USERNAME=${GEOSERVER_USERNAME:-admin} - GEOSERVER_PASSWORD=${GEOSERVER_PASSWORD:-geoserver} - GEOSERVER_WORKSPACE=${GEOSERVER_WORKSPACE} - GEOSERVER_USER=${GEOSERVER_USER:-admin} - - GEOSERVER_URL=${GEOSERVER_URL} + - GEOSERVER_URL=${GEOSERVER_URL:-http://geoserver:8080} depends_on: - geoserver restart: unless-stopped - deploy: - mode: replicated - replicas: 1 - placement: - constraints: - - node.role == worker - restart_policy: - condition: any extractor-geotiff-preview: image: clowder/extractors-geotiff-preview @@ -45,24 +37,16 @@ services: - RABBITMQ_EXCHANGE=${RABBITMQ_EXCHANGE:-clowder} - REGISTRATION_ENDPOINTS=${REGISTRATION_ENDPOINTS} - PROXY_ON=${PROXY_ON} - - PROXY_HOST=${PROXY_HOST} + - PROXY_HOST=${TRAEFIK_HOST} - PROXY_URL=${PROXY_URL} - GEOSERVER_USERNAME=${GEOSERVER_USERNAME:-admin} - GEOSERVER_PASSWORD=${GEOSERVER_PASSWORD:-geoserver} - GEOSERVER_WORKSPACE=${GEOSERVER_WORKSPACE} - GEOSERVER_USER=${GEOSERVER_USER:-admin} - - GEOSERVER_URL=${GEOSERVER_URL} + - GEOSERVER_URL=${GEOSERVER_URL:-http://geoserver:8080} depends_on: - geoserver restart: unless-stopped - deploy: - mode: replicated - replicas: 1 - placement: - constraints: - - node.role == worker - restart_policy: - condition: any extractor-geotiff-metadata: image: clowder/extractors-geotiff-metadata @@ -73,24 +57,16 @@ services: - RABBITMQ_EXCHANGE=${RABBITMQ_EXCHANGE:-clowder} - REGISTRATION_ENDPOINTS=${REGISTRATION_ENDPOINTS} - PROXY_ON=${PROXY_ON} - - PROXY_HOST=${PROXY_HOST} + - PROXY_HOST=${TRAEFIK_HOST} - PROXY_URL=${PROXY_URL} - GEOSERVER_USERNAME=${GEOSERVER_USERNAME:-admin} - GEOSERVER_PASSWORD=${GEOSERVER_PASSWORD:-geoserver} - GEOSERVER_WORKSPACE=${GEOSERVER_WORKSPACE} - GEOSERVER_USER=${GEOSERVER_USER:-admin} - - GEOSERVER_URL=${GEOSERVER_URL} + - GEOSERVER_URL=${GEOSERVER_URL:-http://geoserver:8080} depends_on: - geoserver restart: unless-stopped - deploy: - mode: replicated - replicas: 1 - placement: - constraints: - - node.role == worker - restart_policy: - condition: any networks: clowder: \ No newline at end of file diff --git a/docker-compose.geoserver.yml b/docker-compose.geoserver.yml index 322f58b60..5a89ddcc0 100644 --- a/docker-compose.geoserver.yml +++ b/docker-compose.geoserver.yml @@ -12,7 +12,7 @@ services: - RABBITMQ_EXCHANGE=${RABBITMQ_EXCHANGE:-clowder} - REGISTRATION_ENDPOINTS=${REGISTRATION_ENDPOINTS} - PROXY_ON=${PROXY_ON} - - PROXY_HOST=${PROXY_HOST} + - PROXY_HOST=${TRAEFIK_HOST} - PROXY_URL=${PROXY_URL} - GEOSERVER_USERNAME=${GEOSERVER_USERNAME:-admin} - GEOSERVER_PASSWORD=${GEOSERVER_PASSWORD:-geoserver} @@ -24,14 +24,6 @@ services: volumes: - /generated/data/mounting:/data_dir restart: unless-stopped - deploy: - mode: replicated - replicas: 1 - placement: - constraints: - - node.role == manager - restart_policy: - condition: any networks: clowder: \ No newline at end of file From 82663f06da3f7e51fcc90e664d26aedb8264e3f7 Mon Sep 17 00:00:00 2001 From: tcnichol Date: Fri, 7 May 2021 11:06:10 -0500 Subject: [PATCH 04/38] all geoserver in one yml --- docker-compose.geoserver-extractors.yml | 72 ------------------------- docker-compose.geoserver.yml | 60 +++++++++++++++++++++ 2 files changed, 60 insertions(+), 72 deletions(-) delete mode 100644 docker-compose.geoserver-extractors.yml diff --git a/docker-compose.geoserver-extractors.yml b/docker-compose.geoserver-extractors.yml deleted file mode 100644 index 7806a7484..000000000 --- a/docker-compose.geoserver-extractors.yml +++ /dev/null @@ -1,72 +0,0 @@ -# -#note - for each extractor, replace the following fields: -# - GEOSERVER_USERNAME -# - GEOSERVER_PASSWORD -# - GEOSERVER_USER -# - replace 'GEOSERVER_URL' with the actual ip or url for the geoserver (not 'localhost') - -version: "3.2" - -services: - ncsa_geo_shp: - image: clowder/extractors-geoshp-preview:${VERSION:-latest} - networks: - - clowder - environment: - - RABBITMQ_URI=${RABBITMQ_URI:-amqp://guest:guest@rabbitmq/%2F} - - RABBITMQ_EXCHANGE=${RABBITMQ_EXCHANGE:-clowder} - - REGISTRATION_ENDPOINTS=${REGISTRATION_ENDPOINTS} - - PROXY_ON=${PROXY_ON} - - PROXY_HOST=${TRAEFIK_HOST} - - PROXY_URL=${PROXY_URL} - - GEOSERVER_USERNAME=${GEOSERVER_USERNAME:-admin} - - GEOSERVER_PASSWORD=${GEOSERVER_PASSWORD:-geoserver} - - GEOSERVER_WORKSPACE=${GEOSERVER_WORKSPACE} - - GEOSERVER_USER=${GEOSERVER_USER:-admin} - - GEOSERVER_URL=${GEOSERVER_URL:-http://geoserver:8080} - depends_on: - - geoserver - restart: unless-stopped - - extractor-geotiff-preview: - image: clowder/extractors-geotiff-preview - networks: - - clowder - environment: - - RABBITMQ_URI=${RABBITMQ_URI:-amqp://guest:guest@rabbitmq/%2F} - - RABBITMQ_EXCHANGE=${RABBITMQ_EXCHANGE:-clowder} - - REGISTRATION_ENDPOINTS=${REGISTRATION_ENDPOINTS} - - PROXY_ON=${PROXY_ON} - - PROXY_HOST=${TRAEFIK_HOST} - - PROXY_URL=${PROXY_URL} - - GEOSERVER_USERNAME=${GEOSERVER_USERNAME:-admin} - - GEOSERVER_PASSWORD=${GEOSERVER_PASSWORD:-geoserver} - - GEOSERVER_WORKSPACE=${GEOSERVER_WORKSPACE} - - GEOSERVER_USER=${GEOSERVER_USER:-admin} - - GEOSERVER_URL=${GEOSERVER_URL:-http://geoserver:8080} - depends_on: - - geoserver - restart: unless-stopped - - extractor-geotiff-metadata: - image: clowder/extractors-geotiff-metadata - networks: - - clowder - environment: - - RABBITMQ_URI=${RABBITMQ_URI:-amqp://guest:guest@rabbitmq/%2F} - - RABBITMQ_EXCHANGE=${RABBITMQ_EXCHANGE:-clowder} - - REGISTRATION_ENDPOINTS=${REGISTRATION_ENDPOINTS} - - PROXY_ON=${PROXY_ON} - - PROXY_HOST=${TRAEFIK_HOST} - - PROXY_URL=${PROXY_URL} - - GEOSERVER_USERNAME=${GEOSERVER_USERNAME:-admin} - - GEOSERVER_PASSWORD=${GEOSERVER_PASSWORD:-geoserver} - - GEOSERVER_WORKSPACE=${GEOSERVER_WORKSPACE} - - GEOSERVER_USER=${GEOSERVER_USER:-admin} - - GEOSERVER_URL=${GEOSERVER_URL:-http://geoserver:8080} - depends_on: - - geoserver - restart: unless-stopped - -networks: - clowder: \ No newline at end of file diff --git a/docker-compose.geoserver.yml b/docker-compose.geoserver.yml index 5a89ddcc0..2448460fc 100644 --- a/docker-compose.geoserver.yml +++ b/docker-compose.geoserver.yml @@ -25,5 +25,65 @@ services: - /generated/data/mounting:/data_dir restart: unless-stopped + ncsa_geo_shp: + image: clowder/extractors-geoshp-preview:${VERSION:-latest} + networks: + - clowder + environment: + - RABBITMQ_URI=${RABBITMQ_URI:-amqp://guest:guest@rabbitmq/%2F} + - RABBITMQ_EXCHANGE=${RABBITMQ_EXCHANGE:-clowder} + - REGISTRATION_ENDPOINTS=${REGISTRATION_ENDPOINTS} + - PROXY_ON=${PROXY_ON} + - PROXY_HOST=${TRAEFIK_HOST} + - PROXY_URL=${PROXY_URL} + - GEOSERVER_USERNAME=${GEOSERVER_USERNAME:-admin} + - GEOSERVER_PASSWORD=${GEOSERVER_PASSWORD:-geoserver} + - GEOSERVER_WORKSPACE=${GEOSERVER_WORKSPACE} + - GEOSERVER_USER=${GEOSERVER_USER:-admin} + - GEOSERVER_URL=${GEOSERVER_URL:-http://geoserver:8080} + depends_on: + - geoserver + restart: unless-stopped + + extractor-geotiff-preview: + image: clowder/extractors-geotiff-preview + networks: + - clowder + environment: + - RABBITMQ_URI=${RABBITMQ_URI:-amqp://guest:guest@rabbitmq/%2F} + - RABBITMQ_EXCHANGE=${RABBITMQ_EXCHANGE:-clowder} + - REGISTRATION_ENDPOINTS=${REGISTRATION_ENDPOINTS} + - PROXY_ON=${PROXY_ON} + - PROXY_HOST=${TRAEFIK_HOST} + - PROXY_URL=${PROXY_URL} + - GEOSERVER_USERNAME=${GEOSERVER_USERNAME:-admin} + - GEOSERVER_PASSWORD=${GEOSERVER_PASSWORD:-geoserver} + - GEOSERVER_WORKSPACE=${GEOSERVER_WORKSPACE} + - GEOSERVER_USER=${GEOSERVER_USER:-admin} + - GEOSERVER_URL=${GEOSERVER_URL:-http://geoserver:8080} + depends_on: + - geoserver + restart: unless-stopped + + extractor-geotiff-metadata: + image: clowder/extractors-geotiff-metadata + networks: + - clowder + environment: + - RABBITMQ_URI=${RABBITMQ_URI:-amqp://guest:guest@rabbitmq/%2F} + - RABBITMQ_EXCHANGE=${RABBITMQ_EXCHANGE:-clowder} + - REGISTRATION_ENDPOINTS=${REGISTRATION_ENDPOINTS} + - PROXY_ON=${PROXY_ON} + - PROXY_HOST=${TRAEFIK_HOST} + - PROXY_URL=${PROXY_URL} + - GEOSERVER_USERNAME=${GEOSERVER_USERNAME:-admin} + - GEOSERVER_PASSWORD=${GEOSERVER_PASSWORD:-geoserver} + - GEOSERVER_WORKSPACE=${GEOSERVER_WORKSPACE} + - GEOSERVER_USER=${GEOSERVER_USER:-admin} + - GEOSERVER_URL=${GEOSERVER_URL:-http://geoserver:8080} + depends_on: + - geoserver + restart: unless-stopped + networks: clowder: \ No newline at end of file From d9611f9a375a1204488a79b85f98bf1d422cb36f Mon Sep 17 00:00:00 2001 From: tcnichol Date: Fri, 21 May 2021 14:58:12 -0500 Subject: [PATCH 05/38] adding new parameter option string folder id --- app/api/Files.scala | 45 +++++++++++++++++++++++++++++++++++++++------ conf/routes | 4 ++-- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/app/api/Files.scala b/app/api/Files.scala index 1c8406e0d..5235e82ea 100644 --- a/app/api/Files.scala +++ b/app/api/Files.scala @@ -523,14 +523,47 @@ class Files @Inject()( /** * Upload a file to a specific dataset */ - def uploadToDataset(dataset_id: UUID, showPreviews: String = "DatasetLevel", originalZipFile: String = "", flagsFromPrevious: String = "", extract: Boolean = true) = PermissionAction(Permission.AddResourceToDataset, Some(ResourceRef(ResourceRef.dataset, dataset_id)))(parse.multipartFormData) { implicit request => + def uploadToDataset(dataset_id: UUID, showPreviews: String = "DatasetLevel", originalZipFile: String = "", flagsFromPrevious: String = "", extract: Boolean = true, folder_id: Option[String] = None) = PermissionAction(Permission.AddResourceToDataset, Some(ResourceRef(ResourceRef.dataset, dataset_id)))(parse.multipartFormData) { implicit request => datasets.get(dataset_id) match { case Some(dataset) => { - val uploadedFiles = FileUtils.uploadFilesMultipart(request, Some(dataset), showPreviews = showPreviews, originalZipFile = originalZipFile, flagsFromPrevious = flagsFromPrevious, runExtractors = extract, apiKey = request.apiKey) - uploadedFiles.length match { - case 0 => BadRequest("No files uploaded") - case 1 => Ok(Json.obj("id" -> uploadedFiles.head.id)) - case _ => Ok(Json.obj("ids" -> uploadedFiles.toList)) + folder_id match { + case Some(id) => { + if (UUID.isValid(id)){ + folders.get(UUID(id)) match { + case Some(folder) => { + val uploadedFiles = FileUtils.uploadFilesMultipart(request, Some(dataset), Some(folder), showPreviews = showPreviews, originalZipFile = originalZipFile, flagsFromPrevious = flagsFromPrevious, runExtractors = extract, apiKey = request.apiKey) + uploadedFiles.length match { + case 0 => BadRequest("No files uploaded") + case 1 => Ok(Json.obj("id" -> uploadedFiles.head.id)) + case _ => Ok(Json.obj("ids" -> uploadedFiles.toList)) + } + } + case None => { + val uploadedFiles = FileUtils.uploadFilesMultipart(request, Some(dataset), showPreviews = showPreviews, originalZipFile = originalZipFile, flagsFromPrevious = flagsFromPrevious, runExtractors = extract, apiKey = request.apiKey) + uploadedFiles.length match { + case 0 => BadRequest("No files uploaded") + case 1 => Ok(Json.obj("id" -> uploadedFiles.head.id)) + case _ => Ok(Json.obj("ids" -> uploadedFiles.toList)) + } + } + } + } else { + val uploadedFiles = FileUtils.uploadFilesMultipart(request, Some(dataset), showPreviews = showPreviews, originalZipFile = originalZipFile, flagsFromPrevious = flagsFromPrevious, runExtractors = extract, apiKey = request.apiKey) + uploadedFiles.length match { + case 0 => BadRequest("No files uploaded") + case 1 => Ok(Json.obj("id" -> uploadedFiles.head.id)) + case _ => Ok(Json.obj("ids" -> uploadedFiles.toList)) + } + } + } + case None => { + val uploadedFiles = FileUtils.uploadFilesMultipart(request, Some(dataset), showPreviews = showPreviews, originalZipFile = originalZipFile, flagsFromPrevious = flagsFromPrevious, runExtractors = extract, apiKey = request.apiKey) + uploadedFiles.length match { + case 0 => BadRequest("No files uploaded") + case 1 => Ok(Json.obj("id" -> uploadedFiles.head.id)) + case _ => Ok(Json.obj("ids" -> uploadedFiles.toList)) + } + } } } case None => { diff --git a/conf/routes b/conf/routes index 21e6fe83c..e970917bc 100644 --- a/conf/routes +++ b/conf/routes @@ -377,8 +377,8 @@ POST /api/files POST /api/files/withFlags/:flags @api.Files.upload(showPreviews: String ?= "DatasetLevel", originalZipFile: String ?= "", flags: String) POST /api/files/searchusermetadata @api.Files.searchFilesUserMetadata POST /api/files/searchmetadata @api.Files.searchFilesGeneralMetadata -POST /api/uploadToDataset/withFlags/:id/:flags @api.Files.uploadToDataset(id: UUID, showPreviews: String ?= "DatasetLevel", originalZipFile: String ?= "", flags: String, extract: Boolean ?= true) -POST /api/uploadToDataset/:id @api.Files.uploadToDataset(id: UUID, showPreviews: String ?= "DatasetLevel", originalZipFile: String ?= "", flags: String ?= "", extract: Boolean ?= true) +POST /api/uploadToDataset/withFlags/:id/:flags @api.Files.uploadToDataset(id: UUID, showPreviews: String ?= "DatasetLevel", originalZipFile: String ?= "", flags: String, extract: Boolean ?= true, folder_id: Option[String] ?= None) +POST /api/uploadToDataset/:id @api.Files.uploadToDataset(id: UUID, showPreviews: String ?= "DatasetLevel", originalZipFile: String ?= "", flags: String ?= "", extract: Boolean ?= true, folder_id: Option[String] ?= None) PUT /api/files/:id/updateDescription @api.Files.updateDescription(id: UUID) POST /api/files/uploadIntermediate/:idAndFlags @api.Files.uploadIntermediate(idAndFlags) POST /api/files/sendJob/:fileId/:fileType @api.Files.sendJob(fileId: UUID,fileType) From 88c7d77f8f600ba882b5e233297a39bea84f9235 Mon Sep 17 00:00:00 2001 From: tcnichol Date: Fri, 21 May 2021 15:05:32 -0500 Subject: [PATCH 06/38] updating swagger.yml --- public/swagger.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/swagger.yml b/public/swagger.yml index b4c6cadce..9c708eeac 100644 --- a/public/swagger.yml +++ b/public/swagger.yml @@ -418,6 +418,10 @@ paths: in: query schema: type: boolean + - name: folder_id + in: query + schema: + type: string requestBody: content: multipart/form-data: From 004b96fc63a595f6b1560815ae08f6cb462ec31b Mon Sep 17 00:00:00 2001 From: tcnichol Date: Mon, 24 May 2021 13:32:53 -0500 Subject: [PATCH 07/38] using opion[uuid] instead of option[string] --- app/api/Files.scala | 39 +++++++++++++++------------------------ conf/routes | 2 +- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/app/api/Files.scala b/app/api/Files.scala index 5235e82ea..6e7089951 100644 --- a/app/api/Files.scala +++ b/app/api/Files.scala @@ -523,36 +523,27 @@ class Files @Inject()( /** * Upload a file to a specific dataset */ - def uploadToDataset(dataset_id: UUID, showPreviews: String = "DatasetLevel", originalZipFile: String = "", flagsFromPrevious: String = "", extract: Boolean = true, folder_id: Option[String] = None) = PermissionAction(Permission.AddResourceToDataset, Some(ResourceRef(ResourceRef.dataset, dataset_id)))(parse.multipartFormData) { implicit request => + def uploadToDataset(dataset_id: UUID, showPreviews: String = "DatasetLevel", originalZipFile: String = "", flagsFromPrevious: String = "", extract: Boolean = true, folder_id : Option[UUID]) = PermissionAction(Permission.AddResourceToDataset, Some(ResourceRef(ResourceRef.dataset, dataset_id)))(parse.multipartFormData) { implicit request => datasets.get(dataset_id) match { case Some(dataset) => { folder_id match { case Some(id) => { - if (UUID.isValid(id)){ - folders.get(UUID(id)) match { - case Some(folder) => { - val uploadedFiles = FileUtils.uploadFilesMultipart(request, Some(dataset), Some(folder), showPreviews = showPreviews, originalZipFile = originalZipFile, flagsFromPrevious = flagsFromPrevious, runExtractors = extract, apiKey = request.apiKey) - uploadedFiles.length match { - case 0 => BadRequest("No files uploaded") - case 1 => Ok(Json.obj("id" -> uploadedFiles.head.id)) - case _ => Ok(Json.obj("ids" -> uploadedFiles.toList)) - } - } - case None => { - val uploadedFiles = FileUtils.uploadFilesMultipart(request, Some(dataset), showPreviews = showPreviews, originalZipFile = originalZipFile, flagsFromPrevious = flagsFromPrevious, runExtractors = extract, apiKey = request.apiKey) - uploadedFiles.length match { - case 0 => BadRequest("No files uploaded") - case 1 => Ok(Json.obj("id" -> uploadedFiles.head.id)) - case _ => Ok(Json.obj("ids" -> uploadedFiles.toList)) - } + folders.get(id) match { + case Some(folder) => { + val uploadedFiles = FileUtils.uploadFilesMultipart(request, Some(dataset),Some(folder), showPreviews = showPreviews, originalZipFile = originalZipFile, flagsFromPrevious = flagsFromPrevious, runExtractors = extract, apiKey = request.apiKey) + uploadedFiles.length match { + case 0 => BadRequest("No files uploaded") + case 1 => Ok(Json.obj("id" -> uploadedFiles.head.id)) + case _ => Ok(Json.obj("ids" -> uploadedFiles.toList)) } } - } else { - val uploadedFiles = FileUtils.uploadFilesMultipart(request, Some(dataset), showPreviews = showPreviews, originalZipFile = originalZipFile, flagsFromPrevious = flagsFromPrevious, runExtractors = extract, apiKey = request.apiKey) - uploadedFiles.length match { - case 0 => BadRequest("No files uploaded") - case 1 => Ok(Json.obj("id" -> uploadedFiles.head.id)) - case _ => Ok(Json.obj("ids" -> uploadedFiles.toList)) + case None => { + val uploadedFiles = FileUtils.uploadFilesMultipart(request, Some(dataset), showPreviews = showPreviews, originalZipFile = originalZipFile, flagsFromPrevious = flagsFromPrevious, runExtractors = extract, apiKey = request.apiKey) + uploadedFiles.length match { + case 0 => BadRequest("No files uploaded") + case 1 => Ok(Json.obj("id" -> uploadedFiles.head.id)) + case _ => Ok(Json.obj("ids" -> uploadedFiles.toList)) + } } } } diff --git a/conf/routes b/conf/routes index e970917bc..c3e9b5758 100644 --- a/conf/routes +++ b/conf/routes @@ -377,7 +377,7 @@ POST /api/files POST /api/files/withFlags/:flags @api.Files.upload(showPreviews: String ?= "DatasetLevel", originalZipFile: String ?= "", flags: String) POST /api/files/searchusermetadata @api.Files.searchFilesUserMetadata POST /api/files/searchmetadata @api.Files.searchFilesGeneralMetadata -POST /api/uploadToDataset/withFlags/:id/:flags @api.Files.uploadToDataset(id: UUID, showPreviews: String ?= "DatasetLevel", originalZipFile: String ?= "", flags: String, extract: Boolean ?= true, folder_id: Option[String] ?= None) +POST /api/uploadToDataset/withFlags/:id/:flags @api.Files.uploadToDataset(id: UUID, showPreviews: String ?= "DatasetLevel", originalZipFile: String ?= "", flags: String, extract: Boolean ?= true, folder_id: Option[UUID] ?= None) POST /api/uploadToDataset/:id @api.Files.uploadToDataset(id: UUID, showPreviews: String ?= "DatasetLevel", originalZipFile: String ?= "", flags: String ?= "", extract: Boolean ?= true, folder_id: Option[String] ?= None) PUT /api/files/:id/updateDescription @api.Files.updateDescription(id: UUID) POST /api/files/uploadIntermediate/:idAndFlags @api.Files.uploadIntermediate(idAndFlags) From 4cde26ac3baa5ec2df83f26b8bf97a490847271c Mon Sep 17 00:00:00 2001 From: tcnichol Date: Mon, 24 May 2021 14:41:03 -0500 Subject: [PATCH 08/38] using opion[uuid] instead of option[string] --- app/api/Files.scala | 37 +++++++------------------------------ conf/routes | 2 +- 2 files changed, 8 insertions(+), 31 deletions(-) diff --git a/app/api/Files.scala b/app/api/Files.scala index 6e7089951..28eb28554 100644 --- a/app/api/Files.scala +++ b/app/api/Files.scala @@ -523,38 +523,15 @@ class Files @Inject()( /** * Upload a file to a specific dataset */ - def uploadToDataset(dataset_id: UUID, showPreviews: String = "DatasetLevel", originalZipFile: String = "", flagsFromPrevious: String = "", extract: Boolean = true, folder_id : Option[UUID]) = PermissionAction(Permission.AddResourceToDataset, Some(ResourceRef(ResourceRef.dataset, dataset_id)))(parse.multipartFormData) { implicit request => + def uploadToDataset(dataset_id: UUID, showPreviews: String = "DatasetLevel", originalZipFile: String = "", flagsFromPrevious: String = "", extract: Boolean = true, folder_id: Option[UUID]) = PermissionAction(Permission.AddResourceToDataset, Some(ResourceRef(ResourceRef.dataset, dataset_id)))(parse.multipartFormData) { implicit request => datasets.get(dataset_id) match { case Some(dataset) => { - folder_id match { - case Some(id) => { - folders.get(id) match { - case Some(folder) => { - val uploadedFiles = FileUtils.uploadFilesMultipart(request, Some(dataset),Some(folder), showPreviews = showPreviews, originalZipFile = originalZipFile, flagsFromPrevious = flagsFromPrevious, runExtractors = extract, apiKey = request.apiKey) - uploadedFiles.length match { - case 0 => BadRequest("No files uploaded") - case 1 => Ok(Json.obj("id" -> uploadedFiles.head.id)) - case _ => Ok(Json.obj("ids" -> uploadedFiles.toList)) - } - } - case None => { - val uploadedFiles = FileUtils.uploadFilesMultipart(request, Some(dataset), showPreviews = showPreviews, originalZipFile = originalZipFile, flagsFromPrevious = flagsFromPrevious, runExtractors = extract, apiKey = request.apiKey) - uploadedFiles.length match { - case 0 => BadRequest("No files uploaded") - case 1 => Ok(Json.obj("id" -> uploadedFiles.head.id)) - case _ => Ok(Json.obj("ids" -> uploadedFiles.toList)) - } - } - } - } - case None => { - val uploadedFiles = FileUtils.uploadFilesMultipart(request, Some(dataset), showPreviews = showPreviews, originalZipFile = originalZipFile, flagsFromPrevious = flagsFromPrevious, runExtractors = extract, apiKey = request.apiKey) - uploadedFiles.length match { - case 0 => BadRequest("No files uploaded") - case 1 => Ok(Json.obj("id" -> uploadedFiles.head.id)) - case _ => Ok(Json.obj("ids" -> uploadedFiles.toList)) - } - } + val folder = folder_id.flatMap(x => folders.get(x)) + val uploadedFiles = FileUtils.uploadFilesMultipart(request, Some(dataset), folder, showPreviews = showPreviews, originalZipFile = originalZipFile, flagsFromPrevious = flagsFromPrevious, runExtractors = extract, apiKey = request.apiKey) + uploadedFiles.length match { + case 0 => BadRequest("No files uploaded") + case 1 => Ok(Json.obj("id" -> uploadedFiles.head.id)) + case _ => Ok(Json.obj("ids" -> uploadedFiles.toList)) } } case None => { diff --git a/conf/routes b/conf/routes index c3e9b5758..b8013b7f2 100644 --- a/conf/routes +++ b/conf/routes @@ -378,7 +378,7 @@ POST /api/files/withFlags/:flags POST /api/files/searchusermetadata @api.Files.searchFilesUserMetadata POST /api/files/searchmetadata @api.Files.searchFilesGeneralMetadata POST /api/uploadToDataset/withFlags/:id/:flags @api.Files.uploadToDataset(id: UUID, showPreviews: String ?= "DatasetLevel", originalZipFile: String ?= "", flags: String, extract: Boolean ?= true, folder_id: Option[UUID] ?= None) -POST /api/uploadToDataset/:id @api.Files.uploadToDataset(id: UUID, showPreviews: String ?= "DatasetLevel", originalZipFile: String ?= "", flags: String ?= "", extract: Boolean ?= true, folder_id: Option[String] ?= None) +POST /api/uploadToDataset/:id @api.Files.uploadToDataset(id: UUID, showPreviews: String ?= "DatasetLevel", originalZipFile: String ?= "", flags: String ?= "", extract: Boolean ?= true, folder_id: Option[UUID] ?= None) PUT /api/files/:id/updateDescription @api.Files.updateDescription(id: UUID) POST /api/files/uploadIntermediate/:idAndFlags @api.Files.uploadIntermediate(idAndFlags) POST /api/files/sendJob/:fileId/:fileType @api.Files.sendJob(fileId: UUID,fileType) From 488f6a8417599c85be863194f47a2c8c4095d56a Mon Sep 17 00:00:00 2001 From: tcnichol Date: Mon, 24 May 2021 15:01:23 -0500 Subject: [PATCH 09/38] changing option[uuid] to option[string] --- app/api/Files.scala | 4 ++-- conf/routes | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/api/Files.scala b/app/api/Files.scala index 28eb28554..2e0b15b8a 100644 --- a/app/api/Files.scala +++ b/app/api/Files.scala @@ -523,10 +523,10 @@ class Files @Inject()( /** * Upload a file to a specific dataset */ - def uploadToDataset(dataset_id: UUID, showPreviews: String = "DatasetLevel", originalZipFile: String = "", flagsFromPrevious: String = "", extract: Boolean = true, folder_id: Option[UUID]) = PermissionAction(Permission.AddResourceToDataset, Some(ResourceRef(ResourceRef.dataset, dataset_id)))(parse.multipartFormData) { implicit request => + def uploadToDataset(dataset_id: UUID, showPreviews: String = "DatasetLevel", originalZipFile: String = "", flagsFromPrevious: String = "", extract: Boolean = true, folder_id: Option[String]) = PermissionAction(Permission.AddResourceToDataset, Some(ResourceRef(ResourceRef.dataset, dataset_id)))(parse.multipartFormData) { implicit request => datasets.get(dataset_id) match { case Some(dataset) => { - val folder = folder_id.flatMap(x => folders.get(x)) + val folder = folder_id.flatMap(x => folders.get(UUID(x))) val uploadedFiles = FileUtils.uploadFilesMultipart(request, Some(dataset), folder, showPreviews = showPreviews, originalZipFile = originalZipFile, flagsFromPrevious = flagsFromPrevious, runExtractors = extract, apiKey = request.apiKey) uploadedFiles.length match { case 0 => BadRequest("No files uploaded") diff --git a/conf/routes b/conf/routes index b8013b7f2..e970917bc 100644 --- a/conf/routes +++ b/conf/routes @@ -377,8 +377,8 @@ POST /api/files POST /api/files/withFlags/:flags @api.Files.upload(showPreviews: String ?= "DatasetLevel", originalZipFile: String ?= "", flags: String) POST /api/files/searchusermetadata @api.Files.searchFilesUserMetadata POST /api/files/searchmetadata @api.Files.searchFilesGeneralMetadata -POST /api/uploadToDataset/withFlags/:id/:flags @api.Files.uploadToDataset(id: UUID, showPreviews: String ?= "DatasetLevel", originalZipFile: String ?= "", flags: String, extract: Boolean ?= true, folder_id: Option[UUID] ?= None) -POST /api/uploadToDataset/:id @api.Files.uploadToDataset(id: UUID, showPreviews: String ?= "DatasetLevel", originalZipFile: String ?= "", flags: String ?= "", extract: Boolean ?= true, folder_id: Option[UUID] ?= None) +POST /api/uploadToDataset/withFlags/:id/:flags @api.Files.uploadToDataset(id: UUID, showPreviews: String ?= "DatasetLevel", originalZipFile: String ?= "", flags: String, extract: Boolean ?= true, folder_id: Option[String] ?= None) +POST /api/uploadToDataset/:id @api.Files.uploadToDataset(id: UUID, showPreviews: String ?= "DatasetLevel", originalZipFile: String ?= "", flags: String ?= "", extract: Boolean ?= true, folder_id: Option[String] ?= None) PUT /api/files/:id/updateDescription @api.Files.updateDescription(id: UUID) POST /api/files/uploadIntermediate/:idAndFlags @api.Files.uploadIntermediate(idAndFlags) POST /api/files/sendJob/:fileId/:fileType @api.Files.sendJob(fileId: UUID,fileType) From 017438782b7193991fe9c2d876cbf71bfedb82a3 Mon Sep 17 00:00:00 2001 From: tcnichol Date: Mon, 24 May 2021 15:30:10 -0500 Subject: [PATCH 10/38] breaks if x is not a proper UUID --- app/api/Files.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/api/Files.scala b/app/api/Files.scala index 2e0b15b8a..bd7330c95 100644 --- a/app/api/Files.scala +++ b/app/api/Files.scala @@ -526,7 +526,9 @@ class Files @Inject()( def uploadToDataset(dataset_id: UUID, showPreviews: String = "DatasetLevel", originalZipFile: String = "", flagsFromPrevious: String = "", extract: Boolean = true, folder_id: Option[String]) = PermissionAction(Permission.AddResourceToDataset, Some(ResourceRef(ResourceRef.dataset, dataset_id)))(parse.multipartFormData) { implicit request => datasets.get(dataset_id) match { case Some(dataset) => { + val folder = folder_id.flatMap(x => folders.get(UUID(x))) + val uploadedFiles = FileUtils.uploadFilesMultipart(request, Some(dataset), folder, showPreviews = showPreviews, originalZipFile = originalZipFile, flagsFromPrevious = flagsFromPrevious, runExtractors = extract, apiKey = request.apiKey) uploadedFiles.length match { case 0 => BadRequest("No files uploaded") From 725473e882aef32619db0857e74b6c54bbde49f7 Mon Sep 17 00:00:00 2001 From: tcnichol Date: Mon, 24 May 2021 16:15:48 -0500 Subject: [PATCH 11/38] fix that handles case of folder_id string is present but not uuid --- app/api/Files.scala | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/api/Files.scala b/app/api/Files.scala index bd7330c95..87a171e35 100644 --- a/app/api/Files.scala +++ b/app/api/Files.scala @@ -527,9 +527,14 @@ class Files @Inject()( datasets.get(dataset_id) match { case Some(dataset) => { - val folder = folder_id.flatMap(x => folders.get(UUID(x))) - val uploadedFiles = FileUtils.uploadFilesMultipart(request, Some(dataset), folder, showPreviews = showPreviews, originalZipFile = originalZipFile, flagsFromPrevious = flagsFromPrevious, runExtractors = extract, apiKey = request.apiKey) + val current_folder = if (UUID.isValid(folder_id.get)){ + folders.get(UUID(folder_id.get)) + } else { + None + } + + val uploadedFiles = FileUtils.uploadFilesMultipart(request, Some(dataset), current_folder, showPreviews = showPreviews, originalZipFile = originalZipFile, flagsFromPrevious = flagsFromPrevious, runExtractors = extract, apiKey = request.apiKey) uploadedFiles.length match { case 0 => BadRequest("No files uploaded") case 1 => Ok(Json.obj("id" -> uploadedFiles.head.id)) From 8a8df19baaa383543c963324b5bc1bfa8e92a0a0 Mon Sep 17 00:00:00 2001 From: tcnichol Date: Mon, 24 May 2021 18:00:26 -0500 Subject: [PATCH 12/38] getting rid of blank lines --- app/api/Files.scala | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/api/Files.scala b/app/api/Files.scala index 87a171e35..944dc580e 100644 --- a/app/api/Files.scala +++ b/app/api/Files.scala @@ -526,14 +526,11 @@ class Files @Inject()( def uploadToDataset(dataset_id: UUID, showPreviews: String = "DatasetLevel", originalZipFile: String = "", flagsFromPrevious: String = "", extract: Boolean = true, folder_id: Option[String]) = PermissionAction(Permission.AddResourceToDataset, Some(ResourceRef(ResourceRef.dataset, dataset_id)))(parse.multipartFormData) { implicit request => datasets.get(dataset_id) match { case Some(dataset) => { - - val current_folder = if (UUID.isValid(folder_id.get)){ folders.get(UUID(folder_id.get)) } else { None } - val uploadedFiles = FileUtils.uploadFilesMultipart(request, Some(dataset), current_folder, showPreviews = showPreviews, originalZipFile = originalZipFile, flagsFromPrevious = flagsFromPrevious, runExtractors = extract, apiKey = request.apiKey) uploadedFiles.length match { case 0 => BadRequest("No files uploaded") From cb941eaa0df39b1c7f683f51d8e6adc6fbc4802f Mon Sep 17 00:00:00 2001 From: tcnichol Date: Sun, 11 Jul 2021 15:44:55 -0500 Subject: [PATCH 13/38] fixed endpoint, added changelog entry --- CHANGELOG.md | 3 +++ app/api/Files.scala | 9 +++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c428d6a5b..d7d7b8251 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - Added index for comments, will speed up index creation +### Added +- Endpoint '/api/files/uploadToDataset' now allows folder_id for uploading file to folder. [#232](https://github.com/clowder-framework/clowder/issues/232) + ## 1.17.0 - 2021-04-29 ### Fixed diff --git a/app/api/Files.scala b/app/api/Files.scala index 944dc580e..bdb67299b 100644 --- a/app/api/Files.scala +++ b/app/api/Files.scala @@ -526,10 +526,11 @@ class Files @Inject()( def uploadToDataset(dataset_id: UUID, showPreviews: String = "DatasetLevel", originalZipFile: String = "", flagsFromPrevious: String = "", extract: Boolean = true, folder_id: Option[String]) = PermissionAction(Permission.AddResourceToDataset, Some(ResourceRef(ResourceRef.dataset, dataset_id)))(parse.multipartFormData) { implicit request => datasets.get(dataset_id) match { case Some(dataset) => { - val current_folder = if (UUID.isValid(folder_id.get)){ - folders.get(UUID(folder_id.get)) - } else { - None + var current_folder : Option[Folder] = None + if (folder_id != None) { + if (UUID.isValid(folder_id.get)){ + current_folder = folders.get(UUID(folder_id.get)) + } } val uploadedFiles = FileUtils.uploadFilesMultipart(request, Some(dataset), current_folder, showPreviews = showPreviews, originalZipFile = originalZipFile, flagsFromPrevious = flagsFromPrevious, runExtractors = extract, apiKey = request.apiKey) uploadedFiles.length match { From 5ace6fc1cee998ff1e7e9566eff863fb2bc6e27d Mon Sep 17 00:00:00 2001 From: toddn Date: Fri, 23 Jul 2021 13:28:55 -0500 Subject: [PATCH 14/38] adding indexing for new collections created by the api route --- app/api/Collections.scala | 49 ++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/app/api/Collections.scala b/app/api/Collections.scala index 09d399014..3f8f47bfe 100644 --- a/app/api/Collections.scala +++ b/app/api/Collections.scala @@ -1,32 +1,28 @@ package api -import java.io.{ByteArrayInputStream, InputStream, ByteArrayOutputStream} -import java.security.{DigestInputStream, MessageDigest} -import java.text.SimpleDateFormat -import java.util.zip.{ZipEntry, ZipOutputStream, Deflater} - import Iterators.RootCollectionIterator -import _root_.util.JSONLD +import util.SearchUtils import api.Permission.Permission -import org.apache.commons.codec.binary.Hex +import controllers.Utils +import models._ import play.api.Logger import play.api.Play.current -import models._ +import play.api.libs.concurrent.Execution.Implicits._ import play.api.libs.iteratee.Enumerator -import services._ -import play.api.libs.json._ -import play.api.libs.json.{JsObject, JsValue} import play.api.libs.json.Json.toJson -import javax.inject.{ Singleton, Inject} -import scala.collection.mutable.ListBuffer -import scala.concurrent.{Future, ExecutionContext} -import play.api.libs.concurrent.Execution.Implicits._ -import scala.util.parsing.json.JSONArray -import scala.util.{Try, Success, Failure} -import java.util.{Calendar, Date} -import controllers.Utils +import play.api.libs.json.{JsObject, JsValue, _} +import services._ + +import java.io.ByteArrayOutputStream +import java.security.MessageDigest +import java.util.zip.{Deflater, ZipOutputStream} +import java.util.{Calendar, Date} +import javax.inject.{Inject, Singleton} import scala.collection.immutable.List +import scala.collection.mutable.ListBuffer +import scala.concurrent.{ExecutionContext, Future} +import scala.util.{Failure, Success, Try} /** @@ -72,6 +68,13 @@ class Collections @Inject() (datasets: DatasetService, collections.addToRootSpaces(c.id, s.id) events.addSourceEvent(request.user, c.id, c.name, s.id, s.name, EventType.ADD_COLLECTION_SPACE.toString) }) + // index collection + current.plugin[ElasticsearchPlugin].foreach{ + _.index(SearchUtils.getElasticsearchObject(c)) + } + //Add to Events Table + val option_user = userService.findByIdentity(identity) + events.addObjectEvent(option_user, c.id, c.name, EventType.CREATE_COLLECTION.toString) Ok(toJson(Map("id" -> id))) } case None => Ok(toJson(Map("status" -> "error"))) @@ -572,6 +575,14 @@ class Collections @Inject() (datasets: DatasetService, events.addSourceEvent(request.user, c.id, c.name, s.id, s.name, EventType.ADD_COLLECTION_SPACE.toString) } + // index collection + current.plugin[ElasticsearchPlugin].foreach{ + _.index(SearchUtils.getElasticsearchObject(c)) + } + //Add to Events Table + val option_user = userService.findByIdentity(identity) + events.addObjectEvent(option_user, c.id, c.name, EventType.CREATE_COLLECTION.toString) + //do stuff with parent here (request.body \"parentId").asOpt[String] match { case Some(parentId) => { From 8c343c422d3e050d5d6a4353db500eee794f817a Mon Sep 17 00:00:00 2001 From: toddn Date: Sat, 24 Jul 2021 13:17:37 -0500 Subject: [PATCH 15/38] updating CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e734fe22..66a7cb930 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## unreleased + +### Fixed +- Collections created using api route are now indexed upon creation. [#257](https://github.com/clowder-framework/clowder/issues/257) + ## 1.18.0 - 2021-07-08 ### Added From 1c6a289ebaa8e69b7c8058127e914d62b64e5268 Mon Sep 17 00:00:00 2001 From: toddn Date: Tue, 3 Aug 2021 13:08:53 -0500 Subject: [PATCH 16/38] fixing indents --- docker-compose.geoserver.yml | 46 ++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/docker-compose.geoserver.yml b/docker-compose.geoserver.yml index 2448460fc..882319891 100644 --- a/docker-compose.geoserver.yml +++ b/docker-compose.geoserver.yml @@ -25,28 +25,32 @@ services: - /generated/data/mounting:/data_dir restart: unless-stopped - ncsa_geo_shp: - image: clowder/extractors-geoshp-preview:${VERSION:-latest} - networks: - - clowder - environment: - - RABBITMQ_URI=${RABBITMQ_URI:-amqp://guest:guest@rabbitmq/%2F} - - RABBITMQ_EXCHANGE=${RABBITMQ_EXCHANGE:-clowder} - - REGISTRATION_ENDPOINTS=${REGISTRATION_ENDPOINTS} - - PROXY_ON=${PROXY_ON} - - PROXY_HOST=${TRAEFIK_HOST} - - PROXY_URL=${PROXY_URL} - - GEOSERVER_USERNAME=${GEOSERVER_USERNAME:-admin} - - GEOSERVER_PASSWORD=${GEOSERVER_PASSWORD:-geoserver} - - GEOSERVER_WORKSPACE=${GEOSERVER_WORKSPACE} - - GEOSERVER_USER=${GEOSERVER_USER:-admin} - - GEOSERVER_URL=${GEOSERVER_URL:-http://geoserver:8080} - depends_on: - - geoserver - restart: unless-stopped + + + ncsa_geo_shp: + image: clowder/extractors-geoshp-preview:latest + networks: + - clowder + environment: + - RABBITMQ_URI=${RABBITMQ_URI:-amqp://guest:guest@rabbitmq/%2F} + - RABBITMQ_EXCHANGE=${RABBITMQ_EXCHANGE:-clowder} + - REGISTRATION_ENDPOINTS=${REGISTRATION_ENDPOINTS} + - PROXY_ON=${PROXY_ON} + - PROXY_HOST=${TRAEFIK_HOST} + - PROXY_URL=${PROXY_URL} + - GEOSERVER_USERNAME=${GEOSERVER_USERNAME:-admin} + - GEOSERVER_PASSWORD=${GEOSERVER_PASSWORD:-geoserver} + - GEOSERVER_WORKSPACE=${GEOSERVER_WORKSPACE} + - GEOSERVER_USER=${GEOSERVER_USER:-admin} + - GEOSERVER_URL=${GEOSERVER_URL:-http://geoserver:8080} + depends_on: + - geoserver + - rabbitmq + - clowder + restart: unless-stopped extractor-geotiff-preview: - image: clowder/extractors-geotiff-preview + image: clowder/extractors-geotiff-previews:${VERSION:-latest} networks: - clowder environment: @@ -66,7 +70,7 @@ services: restart: unless-stopped extractor-geotiff-metadata: - image: clowder/extractors-geotiff-metadata + image: clowder/extractors-geotiff-metadata:${VERSION:-latest} networks: - clowder environment: From 40664db67ac078be8499829055bafec242c106dc Mon Sep 17 00:00:00 2001 From: toddn Date: Tue, 3 Aug 2021 13:12:18 -0500 Subject: [PATCH 17/38] fixed indents --- docker-compose.geoserver.yml | 76 ++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/docker-compose.geoserver.yml b/docker-compose.geoserver.yml index 882319891..60638952f 100644 --- a/docker-compose.geoserver.yml +++ b/docker-compose.geoserver.yml @@ -49,45 +49,45 @@ services: - clowder restart: unless-stopped - extractor-geotiff-preview: - image: clowder/extractors-geotiff-previews:${VERSION:-latest} - networks: - - clowder - environment: - - RABBITMQ_URI=${RABBITMQ_URI:-amqp://guest:guest@rabbitmq/%2F} - - RABBITMQ_EXCHANGE=${RABBITMQ_EXCHANGE:-clowder} - - REGISTRATION_ENDPOINTS=${REGISTRATION_ENDPOINTS} - - PROXY_ON=${PROXY_ON} - - PROXY_HOST=${TRAEFIK_HOST} - - PROXY_URL=${PROXY_URL} - - GEOSERVER_USERNAME=${GEOSERVER_USERNAME:-admin} - - GEOSERVER_PASSWORD=${GEOSERVER_PASSWORD:-geoserver} - - GEOSERVER_WORKSPACE=${GEOSERVER_WORKSPACE} - - GEOSERVER_USER=${GEOSERVER_USER:-admin} - - GEOSERVER_URL=${GEOSERVER_URL:-http://geoserver:8080} - depends_on: - - geoserver - restart: unless-stopped + extractor-geotiff-preview: + image: clowder/extractors-geotiff-previews:${VERSION:-latest} + networks: + - clowder + environment: + - RABBITMQ_URI=${RABBITMQ_URI:-amqp://guest:guest@rabbitmq/%2F} + - RABBITMQ_EXCHANGE=${RABBITMQ_EXCHANGE:-clowder} + - REGISTRATION_ENDPOINTS=${REGISTRATION_ENDPOINTS} + - PROXY_ON=${PROXY_ON} + - PROXY_HOST=${TRAEFIK_HOST} + - PROXY_URL=${PROXY_URL} + - GEOSERVER_USERNAME=${GEOSERVER_USERNAME:-admin} + - GEOSERVER_PASSWORD=${GEOSERVER_PASSWORD:-geoserver} + - GEOSERVER_WORKSPACE=${GEOSERVER_WORKSPACE} + - GEOSERVER_USER=${GEOSERVER_USER:-admin} + - GEOSERVER_URL=${GEOSERVER_URL:-http://geoserver:8080} + depends_on: + - geoserver + restart: unless-stopped - extractor-geotiff-metadata: - image: clowder/extractors-geotiff-metadata:${VERSION:-latest} - networks: - - clowder - environment: - - RABBITMQ_URI=${RABBITMQ_URI:-amqp://guest:guest@rabbitmq/%2F} - - RABBITMQ_EXCHANGE=${RABBITMQ_EXCHANGE:-clowder} - - REGISTRATION_ENDPOINTS=${REGISTRATION_ENDPOINTS} - - PROXY_ON=${PROXY_ON} - - PROXY_HOST=${TRAEFIK_HOST} - - PROXY_URL=${PROXY_URL} - - GEOSERVER_USERNAME=${GEOSERVER_USERNAME:-admin} - - GEOSERVER_PASSWORD=${GEOSERVER_PASSWORD:-geoserver} - - GEOSERVER_WORKSPACE=${GEOSERVER_WORKSPACE} - - GEOSERVER_USER=${GEOSERVER_USER:-admin} - - GEOSERVER_URL=${GEOSERVER_URL:-http://geoserver:8080} - depends_on: - - geoserver - restart: unless-stopped + extractor-geotiff-metadata: + image: clowder/extractors-geotiff-metadata:${VERSION:-latest} + networks: + - clowder + environment: + - RABBITMQ_URI=${RABBITMQ_URI:-amqp://guest:guest@rabbitmq/%2F} + - RABBITMQ_EXCHANGE=${RABBITMQ_EXCHANGE:-clowder} + - REGISTRATION_ENDPOINTS=${REGISTRATION_ENDPOINTS} + - PROXY_ON=${PROXY_ON} + - PROXY_HOST=${TRAEFIK_HOST} + - PROXY_URL=${PROXY_URL} + - GEOSERVER_USERNAME=${GEOSERVER_USERNAME:-admin} + - GEOSERVER_PASSWORD=${GEOSERVER_PASSWORD:-geoserver} + - GEOSERVER_WORKSPACE=${GEOSERVER_WORKSPACE} + - GEOSERVER_USER=${GEOSERVER_USER:-admin} + - GEOSERVER_URL=${GEOSERVER_URL:-http://geoserver:8080} + depends_on: + - geoserver + restart: unless-stopped networks: clowder: \ No newline at end of file From a306fb1ee367d69c1db1c5d4a20e28f819549d19 Mon Sep 17 00:00:00 2001 From: toddn Date: Tue, 3 Aug 2021 13:13:30 -0500 Subject: [PATCH 18/38] fixing latest tag --- docker-compose.geoserver.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.geoserver.yml b/docker-compose.geoserver.yml index 60638952f..6b1b382e9 100644 --- a/docker-compose.geoserver.yml +++ b/docker-compose.geoserver.yml @@ -50,7 +50,7 @@ services: restart: unless-stopped extractor-geotiff-preview: - image: clowder/extractors-geotiff-previews:${VERSION:-latest} + image: clowder/extractors-geotiff-previews:latest networks: - clowder environment: @@ -70,7 +70,7 @@ services: restart: unless-stopped extractor-geotiff-metadata: - image: clowder/extractors-geotiff-metadata:${VERSION:-latest} + image: clowder/extractors-geotiff-metadata:latest networks: - clowder environment: From 018a16fcd694af09f7b8cc2f4505f7b7268861cf Mon Sep 17 00:00:00 2001 From: toddn Date: Tue, 3 Aug 2021 13:18:39 -0500 Subject: [PATCH 19/38] fixing name - geotiff preview not previews --- docker-compose.geoserver.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.geoserver.yml b/docker-compose.geoserver.yml index 6b1b382e9..e43fe2564 100644 --- a/docker-compose.geoserver.yml +++ b/docker-compose.geoserver.yml @@ -50,7 +50,7 @@ services: restart: unless-stopped extractor-geotiff-preview: - image: clowder/extractors-geotiff-previews:latest + image: clowder/extractors-geotiff-preview:latest networks: - clowder environment: From e2051a220f4ae9100c630724cad6e79d672a11a6 Mon Sep 17 00:00:00 2001 From: toddn Date: Tue, 10 Aug 2021 12:53:21 -0500 Subject: [PATCH 20/38] adding in env example for geoserver --- env.example | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/env.example b/env.example index ecdce0e5a..a5b1e66ce 100644 --- a/env.example +++ b/env.example @@ -101,3 +101,18 @@ # in case of external rabbitmq, the url to clowder #RABBITMQ_CLOWDERURL=https://clowder-docker.ncsa.illinois.edu/clowder/ + +# ---------------------------------------------------------------------- +# GEOSERVER CONFIGURATION +# ---------------------------------------------------------------------- + +#PROXY_ON=false +#PROXY_URL=http://localhost:9000 + +#GEOSERVER_USERNAME=admin +#GEOSERVER_PASSWORD=geoserver + +#GEOSERVER_WORKSPACE=workspace + +#GEOSERVER_USER=admin +#GEOSERVER_URL=http://localhost:8080 From c6b319091d557c2f219f990e38028a31e5db86d0 Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Wed, 18 Aug 2021 10:32:13 -0500 Subject: [PATCH 21/38] add when parameter to query after/before a date --- app/api/Datasets.scala | 35 ++++++++++++++++++++++++----------- conf/routes | 4 ++-- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/app/api/Datasets.scala b/app/api/Datasets.scala index 710fb8f3e..95701e64a 100644 --- a/app/api/Datasets.scala +++ b/app/api/Datasets.scala @@ -65,19 +65,19 @@ class Datasets @Inject()( } } - def list(title: Option[String], date: Option[String], limit: Int, exact: Boolean) = PrivateServerAction { implicit request => - Ok(toJson(listDatasets(title, date, limit, Set[Permission](Permission.ViewDataset), request.user, request.user.fold(false)(_.superAdminMode), exact))) + def list(when: Option[String], title: Option[String], date: Option[String], limit: Int, exact: Boolean) = PrivateServerAction { implicit request => + Ok(toJson(listDatasets(when, title, date, limit, Set[Permission](Permission.ViewDataset), request.user, request.user.fold(false)(_.superAdminMode), exact))) } - def listCanEdit(title: Option[String], date: Option[String], limit: Int, exact: Boolean) = PrivateServerAction { implicit request => - Ok(toJson(listDatasets(title, date, limit, Set[Permission](Permission.AddResourceToDataset, Permission.EditDataset), request.user, request.user.fold(false)(_.superAdminMode), exact))) + def listCanEdit(when: Option[String], title: Option[String], date: Option[String], limit: Int, exact: Boolean) = PrivateServerAction { implicit request => + Ok(toJson(listDatasets(when, title, date, limit, Set[Permission](Permission.AddResourceToDataset, Permission.EditDataset), request.user, request.user.fold(false)(_.superAdminMode), exact))) } def listMoveFileToDataset(file_id: UUID, title: Option[String], limit: Int, exact: Boolean) = PrivateServerAction { implicit request => if (play.Play.application().configuration().getBoolean("datasetFileWithinSpace")) { Ok(toJson(listDatasetsInSpace(file_id, title, limit, Set[Permission](Permission.AddResourceToDataset, Permission.EditDataset), request.user, request.user.fold(false)(_.superAdminMode), exact))) } else { - Ok(toJson(listDatasets(title, None, limit, Set[Permission](Permission.AddResourceToDataset, Permission.EditDataset), request.user, request.user.fold(false)(_.superAdminMode), exact))) + Ok(toJson(listDatasets(None, title, None, limit, Set[Permission](Permission.AddResourceToDataset, Permission.EditDataset), request.user, request.user.fold(false)(_.superAdminMode), exact))) } } @@ -152,18 +152,31 @@ class Datasets @Inject()( /** * Returns list of datasets based on parameters and permissions. */ - private def listDatasets(title: Option[String], date: Option[String], limit: Int, permission: Set[Permission], user: Option[User], superAdmin: Boolean, exact: Boolean) : List[Dataset] = { - (title, date) match { - case (Some(t), Some(d)) => { + private def listDatasets(when: Option[String], title: Option[String], date: Option[String], limit: Int, permission: Set[Permission], user: Option[User], superAdmin: Boolean, exact: Boolean) : List[Dataset] = { + (when, title, date) match { + case (Some(when), Some(t), Some(d)) => { + datasets.listAccess(d, nextPage=(when=="a"), limit, t, permission, user, superAdmin, true,false, exact) + } + case (Some(when), Some(t), None) => { + datasets.listAccess(limit, t, permission, user, superAdmin, true,false, exact) + } + case (Some(when), None, Some(d)) => { + datasets.listAccess(d, nextPage=(when=="a"), limit, permission, user, superAdmin, true,false) + } + case (Some(when), None, None) => { + datasets.listAccess(limit, permission, user, superAdmin, true,false) + } + // default when to be "after" if not present in parameters. i.e. nextPage=true + case (None, Some(t), Some(d)) => { datasets.listAccess(d, true, limit, t, permission, user, superAdmin, true,false, exact) } - case (Some(t), None) => { + case (None, Some(t), None) => { datasets.listAccess(limit, t, permission, user, superAdmin, true,false, exact) } - case (None, Some(d)) => { + case (None, None, Some(d)) => { datasets.listAccess(d, true, limit, permission, user, superAdmin, true,false) } - case (None, None) => { + case (None, None, None) => { datasets.listAccess(limit, permission, user, superAdmin, true,false) } } diff --git a/conf/routes b/conf/routes index ccf856250..1f360aca4 100644 --- a/conf/routes +++ b/conf/routes @@ -555,8 +555,8 @@ GET /api/collections/:coll_id/removeFromSpaceAllowed/:space_id # ---------------------------------------------------------------------- # DATASETS ENDPOINT # ---------------------------------------------------------------------- -GET /api/datasets @api.Datasets.list(title: Option[String] ?= None, date: Option[String] ?= None, limit: Int ?= 12, exact: Boolean ?= false) -GET /api/datasets/canEdit @api.Datasets.listCanEdit(title: Option[String] ?= None, date: Option[String] ?= None, limit: Int ?= 12, exact: Boolean ?= false) +GET /api/datasets @api.Datasets.list(when: Option[String] ?= None, title: Option[String] ?= None, date: Option[String] ?= None, limit: Int ?= 12, exact: Boolean ?= false) +GET /api/datasets/canEdit @api.Datasets.listCanEdit(when: Option[String] ?= None, title: Option[String] ?= None, date: Option[String] ?= None, limit: Int ?= 12, exact: Boolean ?= false) GET /api/datasets/moveFileToDataset @api.Datasets.listMoveFileToDataset(file_id: UUID, title: Option[String] ?= None, limit: Int ?= 12, exact: Boolean ?= false) POST /api/datasets @api.Datasets.createDataset POST /api/datasets/createempty @api.Datasets.createEmptyDataset From 158dbab6fd8c77fdfeb3538c514375fbfd1f4689 Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Wed, 18 Aug 2021 11:54:22 -0500 Subject: [PATCH 22/38] add when in collection and space as well --- app/api/Collections.scala | 54 +++++++++++++++++++++++++++++---------- app/api/Datasets.scala | 12 ++++----- app/api/Spaces.scala | 49 ++++++++++++++++++++++++++--------- conf/routes | 14 +++++----- 4 files changed, 90 insertions(+), 39 deletions(-) diff --git a/app/api/Collections.scala b/app/api/Collections.scala index 09d399014..ebff20038 100644 --- a/app/api/Collections.scala +++ b/app/api/Collections.scala @@ -237,15 +237,15 @@ class Collections @Inject() (datasets: DatasetService, } } - def list(title: Option[String], date: Option[String], limit: Int, exact: Boolean) = PrivateServerAction { implicit request => - Ok(toJson(listCollections(title, date, limit, Set[Permission](Permission.ViewCollection), false, request.user, request.user.fold(false)(_.superAdminMode), exact))) + def list(when: Option[String], title: Option[String], date: Option[String], limit: Int, exact: Boolean) = PrivateServerAction { implicit request => + Ok(toJson(listCollections(when, title, date, limit, Set[Permission](Permission.ViewCollection), false, request.user, request.user.fold(false)(_.superAdminMode), exact))) } - def listCanEdit(title: Option[String], date: Option[String], limit: Int, exact: Boolean) = PrivateServerAction { implicit request => - Ok(toJson(listCollections(title, date, limit, Set[Permission](Permission.AddResourceToCollection, Permission.EditCollection), false, request.user, request.user.fold(false)(_.superAdminMode), exact))) + def listCanEdit(when: Option[String], title: Option[String], date: Option[String], limit: Int, exact: Boolean) = PrivateServerAction { implicit request => + Ok(toJson(listCollections(when, title, date, limit, Set[Permission](Permission.AddResourceToCollection, Permission.EditCollection), false, request.user, request.user.fold(false)(_.superAdminMode), exact))) } - def addDatasetToCollectionOptions(datasetId: UUID, title: Option[String], date: Option[String], limit: Int, exact: Boolean) = PrivateServerAction { implicit request => + def addDatasetToCollectionOptions(when: Option[String], datasetId: UUID, title: Option[String], date: Option[String], limit: Int, exact: Boolean) = PrivateServerAction { implicit request => implicit val user = request.user var listAll = false var collectionList: List[Collection] = List.empty @@ -265,7 +265,7 @@ class Collections @Inject() (datasets: DatasetService, } } if(listAll) { - collectionList = listCollections(title, date, limit, Set[Permission](Permission.AddResourceToCollection, Permission.EditCollection), false, request.user, request.user.fold(false)(_.superAdminMode), exact) + collectionList = listCollections(when, title, date, limit, Set[Permission](Permission.AddResourceToCollection, Permission.EditCollection), false, request.user, request.user.fold(false)(_.superAdminMode), exact) } Ok(toJson(collectionList)) } @@ -274,10 +274,10 @@ class Collections @Inject() (datasets: DatasetService, collections.get(current_collections.map(_.child_collection_ids).flatten).found } - def listPossibleParents(currentCollectionId : String, title: Option[String], date: Option[String], limit: Int, exact: Boolean) = PrivateServerAction { implicit request => + def listPossibleParents(when: Option[String], currentCollectionId : String, title: Option[String], date: Option[String], limit: Int, exact: Boolean) = PrivateServerAction { implicit request => val selfAndAncestors = collections.getSelfAndAncestors(UUID(currentCollectionId)) val descendants = collections.getAllDescendants(UUID(currentCollectionId)).toList - val allCollections = listCollections(title, date, limit, Set[Permission](Permission.AddResourceToCollection, Permission.EditCollection), false, + val allCollections = listCollections(when, title, date, limit, Set[Permission](Permission.AddResourceToCollection, Permission.EditCollection), false, request.user, request.user.fold(false)(_.superAdminMode), exact) val possibleNewParents = allCollections.filter(c => if(play.api.Play.current.plugin[services.SpaceSharingPlugin].isDefined) { @@ -303,29 +303,55 @@ class Collections @Inject() (datasets: DatasetService, * Returns list of collections based on parameters and permissions. * TODO this needs to be cleaned up when do permissions for adding to a resource */ - private def listCollections(title: Option[String], date: Option[String], limit: Int, permission: Set[Permission], mine: Boolean, user: Option[User], superAdmin: Boolean, exact: Boolean) : List[Collection] = { + private def listCollections(when: Option[String], title: Option[String], date: Option[String], limit: Int, permission: Set[Permission], mine: Boolean, user: Option[User], superAdmin: Boolean, exact: Boolean) : List[Collection] = { if (mine && user.isEmpty) return List.empty[Collection] - (title, date) match { - case (Some(t), Some(d)) => { + (when, title, date) match { + case (Some(w), Some(t), Some(d)) => { + if (mine) + collections.listUser(d, nextPage=(w=="a"), limit, t, user, superAdmin, user.get, exact) + else + collections.listAccess(d, nextPage=(w=="a"), limit, t, permission, user, superAdmin, true,false, exact) + } + case (Some(w), Some(t), None) => { + if (mine) + collections.listUser(limit, t, user, superAdmin, user.get, exact) + else + collections.listAccess(limit, t, permission, user, superAdmin, true,false, exact) + } + case (Some(w), None, Some(d)) => { + if (mine) + collections.listUser(d, nextPage=(w=="a"), limit, user, superAdmin, user.get) + else + collections.listAccess(d, nextPage=(w=="a"), limit, permission, user, superAdmin, true,false) + } + case (Some(w), None, None) => { + if (mine) + collections.listUser(limit, user, superAdmin, user.get) + else + collections.listAccess(limit, permission, user, superAdmin, true,false) + } + + // default when to be "after" if not present in parameters. i.e. nextPage=true + case (None, Some(t), Some(d)) => { if (mine) collections.listUser(d, true, limit, t, user, superAdmin, user.get, exact) else collections.listAccess(d, true, limit, t, permission, user, superAdmin, true,false, exact) } - case (Some(t), None) => { + case (None, Some(t), None) => { if (mine) collections.listUser(limit, t, user, superAdmin, user.get, exact) else collections.listAccess(limit, t, permission, user, superAdmin, true,false, exact) } - case (None, Some(d)) => { + case (None, None, Some(d)) => { if (mine) collections.listUser(d, true, limit, user, superAdmin, user.get) else collections.listAccess(d, true, limit, permission, user, superAdmin, true,false) } - case (None, None) => { + case (None, None, None) => { if (mine) collections.listUser(limit, user, superAdmin, user.get) else diff --git a/app/api/Datasets.scala b/app/api/Datasets.scala index 95701e64a..07c7b9a55 100644 --- a/app/api/Datasets.scala +++ b/app/api/Datasets.scala @@ -154,16 +154,16 @@ class Datasets @Inject()( */ private def listDatasets(when: Option[String], title: Option[String], date: Option[String], limit: Int, permission: Set[Permission], user: Option[User], superAdmin: Boolean, exact: Boolean) : List[Dataset] = { (when, title, date) match { - case (Some(when), Some(t), Some(d)) => { - datasets.listAccess(d, nextPage=(when=="a"), limit, t, permission, user, superAdmin, true,false, exact) + case (Some(w), Some(t), Some(d)) => { + datasets.listAccess(d, nextPage=(w=="a"), limit, t, permission, user, superAdmin, true,false, exact) } - case (Some(when), Some(t), None) => { + case (Some(w), Some(t), None) => { datasets.listAccess(limit, t, permission, user, superAdmin, true,false, exact) } - case (Some(when), None, Some(d)) => { - datasets.listAccess(d, nextPage=(when=="a"), limit, permission, user, superAdmin, true,false) + case (Some(w), None, Some(d)) => { + datasets.listAccess(d, nextPage=(w=="a"), limit, permission, user, superAdmin, true,false) } - case (Some(when), None, None) => { + case (Some(w), None, None) => { datasets.listAccess(limit, permission, user, superAdmin, true,false) } // default when to be "after" if not present in parameters. i.e. nextPage=true diff --git a/app/api/Spaces.scala b/app/api/Spaces.scala index 21931c678..7d762485f 100644 --- a/app/api/Spaces.scala +++ b/app/api/Spaces.scala @@ -86,45 +86,70 @@ class Spaces @Inject()(spaces: SpaceService, } } - def list(title: Option[String], date: Option[String], limit: Int) = UserAction(needActive=false) { implicit request => - Ok(toJson(listSpaces(title, date, limit, Set[Permission](Permission.ViewSpace), false, request.user, request.user.fold(false)(_.superAdminMode), true).map(spaceToJson))) + def list(when: Option[String], title: Option[String], date: Option[String], limit: Int) = UserAction(needActive=false) { implicit request => + Ok(toJson(listSpaces(when, title, date, limit, Set[Permission](Permission.ViewSpace), false, request.user, request.user.fold(false)(_.superAdminMode), true).map(spaceToJson))) } - def listCanEdit(title: Option[String], date: Option[String], limit: Int) = UserAction(needActive=true) { implicit request => - Ok(toJson(listSpaces(title, date, limit, Set[Permission](Permission.AddResourceToSpace, Permission.EditSpace), false, request.user, request.user.fold(false)(_.superAdminMode), true).map(spaceToJson))) + def listCanEdit(when: Option[String], title: Option[String], date: Option[String], limit: Int) = UserAction(needActive=true) { implicit request => + Ok(toJson(listSpaces(when, title, date, limit, Set[Permission](Permission.AddResourceToSpace, Permission.EditSpace), false, request.user, request.user.fold(false)(_.superAdminMode), true).map(spaceToJson))) } - def listCanEditNotAlreadyIn(collectionId : UUID, title: Option[String], date: Option[String], limit: Int) = UserAction(needActive=true ){ implicit request => - Ok(toJson(listSpaces(title, date, limit, Set[Permission](Permission.AddResourceToSpace, Permission.EditSpace), false, request.user, request.user.fold(false)(_.superAdminMode), true).map(spaceToJson))) + def listCanEditNotAlreadyIn(when: Option[String], collectionId : UUID, title: Option[String], date: Option[String], limit: Int) = UserAction(needActive=true ){ implicit request => + Ok(toJson(listSpaces(when, title, date, limit, Set[Permission](Permission.AddResourceToSpace, Permission.EditSpace), false, request.user, request.user.fold(false)(_.superAdminMode), true).map(spaceToJson))) } /** * Returns list of collections based on parameters and permissions. * TODO this needs to be cleaned up when do permissions for adding to a resource */ - private def listSpaces(title: Option[String], date: Option[String], limit: Int, permission: Set[Permission], mine: Boolean, user: Option[User], superAdmin: Boolean, showPublic: Boolean, onlyTrial: Boolean = false) : List[ProjectSpace] = { + private def listSpaces(when: Option[String], title: Option[String], date: Option[String], limit: Int, permission: Set[Permission], mine: Boolean, user: Option[User], superAdmin: Boolean, showPublic: Boolean, onlyTrial: Boolean = false) : List[ProjectSpace] = { if (mine && user.isEmpty) return List.empty[ProjectSpace] - (title, date) match { - case (Some(t), Some(d)) => { + (when, title, date) match { + case (Some(w), Some(t), Some(d)) => { + if (mine) + spaces.listUser(d, nextPage=(w=="a"), limit, t, user, superAdmin, user.get) + else + spaces.listAccess(d, nextPage=(w=="a"), limit, t, permission, user, superAdmin, showPublic, showOnlyShared = false) + } + case (Some(w), Some(t), None) => { + if (mine) + spaces.listUser(limit, t, user, superAdmin, user.get) + else + spaces.listAccess(limit, t, permission, user, superAdmin, showPublic, showOnlyShared = false) + } + case (Some(w), None, Some(d)) => { + if (mine) + spaces.listUser(d, nextPage=(w=="a"), limit, user, superAdmin, user.get) + else + spaces.listAccess(d, nextPage=(w=="a"), limit, permission, user, superAdmin, showPublic, onlyTrial, showOnlyShared = false) + } + case (Some(w), None, None) => { + if (mine) + spaces.listUser(limit, user, superAdmin, user.get) + else + spaces.listAccess(limit, permission, user, superAdmin, showPublic, onlyTrial, showOnlyShared = false) + } + // default when to be "after" if not present in parameters. i.e. nextPage=true + case (None, Some(t), Some(d)) => { if (mine) spaces.listUser(d, true, limit, t, user, superAdmin, user.get) else spaces.listAccess(d, true, limit, t, permission, user, superAdmin, showPublic, showOnlyShared = false) } - case (Some(t), None) => { + case (None, Some(t), None) => { if (mine) spaces.listUser(limit, t, user, superAdmin, user.get) else spaces.listAccess(limit, t, permission, user, superAdmin, showPublic, showOnlyShared = false) } - case (None, Some(d)) => { + case (None, None, Some(d)) => { if (mine) spaces.listUser(d, true, limit, user, superAdmin, user.get) else spaces.listAccess(d, true, limit, permission, user, superAdmin, showPublic, onlyTrial, showOnlyShared = false) } - case (None, None) => { + case (None, None, None) => { if (mine) spaces.listUser(limit, user, superAdmin, user.get) else diff --git a/conf/routes b/conf/routes index 1f360aca4..519ee1cc1 100644 --- a/conf/routes +++ b/conf/routes @@ -484,8 +484,8 @@ GET /api/files/:three_d_file_id/:filename # ---------------------------------------------------------------------- # SPACES ENDPOINT # ---------------------------------------------------------------------- -GET /api/spaces @api.Spaces.list(title: Option[String] ?= None, date: Option[String] ?= None, limit: Int ?= 12) -GET /api/spaces/canEdit @api.Spaces.listCanEdit(title: Option[String] ?= None, date: Option[String] ?= None, limit: Int ?= 12) +GET /api/spaces @api.Spaces.list(when: Option[String] ?= None, title: Option[String] ?= None, date: Option[String] ?= None, limit: Int ?= 12) +GET /api/spaces/canEdit @api.Spaces.listCanEdit(when: Option[String] ?= None, title: Option[String] ?= None, date: Option[String] ?= None, limit: Int ?= 12) GET /api/spaces/:id @api.Spaces.get(id: UUID) POST /api/spaces @api.Spaces.createSpace() DELETE /api/spaces/:id @api.Spaces.removeSpace(id: UUID) @@ -508,10 +508,10 @@ GET /api/spaces/:id/collections # ---------------------------------------------------------------------- # COLLECTIONS ENDPOINT # ---------------------------------------------------------------------- -GET /api/collections @api.Collections.list(title: Option[String] ?= None, date: Option[String] ?= None, limit: Int ?= 12, exact: Boolean ?= false) -GET /api/collections/canEdit @api.Collections.listCanEdit(title: Option[String] ?= None, date: Option[String] ?= None, limit: Int ?= 12, exact: Boolean ?= false) -GET /api/collections/datasetPossibleParents/:ds_id @api.Collections.addDatasetToCollectionOptions(ds_id: UUID, title: Option[String]?= None, date: Option[String] ?= None, limit: Int ?= 12, exact: Boolean ?= false) -GET /api/collections/possibleParents @api.Collections.listPossibleParents(currentCollectionId: String, title: Option[String] ?= None, date: Option[String] ?= None, limit: Int ?= 12, exact: Boolean ?= false) +GET /api/collections @api.Collections.list(when: Option[String] ?= None, title: Option[String] ?= None, date: Option[String] ?= None, limit: Int ?= 12, exact: Boolean ?= false) +GET /api/collections/canEdit @api.Collections.listCanEdit(when: Option[String] ?= None, title: Option[String] ?= None, date: Option[String] ?= None, limit: Int ?= 12, exact: Boolean ?= false) +GET /api/collections/datasetPossibleParents/:ds_id @api.Collections.addDatasetToCollectionOptions(when: Option[String] ?= None, ds_id: UUID, title: Option[String]?= None, date: Option[String] ?= None, limit: Int ?= 12, exact: Boolean ?= false) +GET /api/collections/possibleParents @api.Collections.listPossibleParents(when: Option[String] ?= None, currentCollectionId: String, title: Option[String] ?= None, date: Option[String] ?= None, limit: Int ?= 12, exact: Boolean ?= false) POST /api/collections @api.Collections.createCollection() GET /api/collections/rootCollections @api.Collections.getRootCollections GET /api/collections/topLevelCollections @api.Collections.getTopLevelCollections @@ -522,7 +522,7 @@ DELETE /api/collections/emptyTrash DELETE /api/collections/clearOldCollectionsTrash @api.Collections.clearOldCollectionsTrash(days : Int ?= 30) PUT /api/collections/restore/:collectionId @api.Collections.restoreCollection(collectionId : UUID) # deprecrated -GET /api/collections/list @api.Collections.list(title: Option[String] ?= None, date: Option[String] ?= None, limit: Int ?= 12, exact: Boolean ?= false) +GET /api/collections/list @api.Collections.list(when: Option[String] ?= None, title: Option[String] ?= None, date: Option[String] ?= None, limit: Int ?= 12, exact: Boolean ?= false) POST /api/collections/:id/follow @api.Collections.follow(id: UUID) POST /api/collections/:id/unfollow @api.Collections.unfollow(id: UUID) GET /api/collections/:coll_id/datasets @api.Datasets.listInCollection(coll_id: UUID) From 5641b92df71d5d0efef8a3beaf364f6f0de7854b Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Tue, 31 Aug 2021 15:17:45 -0500 Subject: [PATCH 23/38] add parameters to yml --- public/swagger.yml | 76 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/public/swagger.yml b/public/swagger.yml index de2fe2628..4eeee5d98 100644 --- a/public/swagger.yml +++ b/public/swagger.yml @@ -1768,6 +1768,22 @@ paths: - datasets summary: List all datasets the user can view description: This will check for Permission.ViewDataset + parameters: + - name: when + in: query + required: false + schema: + type: string + - name: date + in: query + required: false + schema: + type: string + - name: title + in: query + required: false + schema: + type: string responses: 200: description: OK @@ -2622,6 +2638,22 @@ paths: description: | This will check for Permission.AddResourceToDataset and Permission.EditDataset + parameters: + - name: when + in: query + required: false + schema: + type: string + - name: date + in: query + required: false + schema: + type: string + - name: title + in: query + required: false + schema: + type: string responses: 200: description: OK @@ -3346,6 +3378,11 @@ paths: view permission description: Collections are groupings of datasets parameters: + - name: when + in: query + required: false + schema: + type: string - name: title in: query description: The title/ name of colletions @@ -3497,6 +3534,11 @@ paths: This will check for Permission.AddResourceToCollection and Permission.EditCollection parameters: + - name: when + in: query + required: false + schema: + type: string - name: currentCollectionId in: query required: true @@ -3595,6 +3637,11 @@ paths: This will check for Permission.AddResourceToCollection and Permission.EditCollection parameters: + - name: when + in: query + required: false + schema: + type: string - name: title in: query schema: @@ -4092,6 +4139,11 @@ paths: summary: List all collections the user can view description: This will check for Permission.ViewCollection parameters: + - name: when + in: query + required: false + schema: + type: string - name: title in: query schema: @@ -4168,6 +4220,11 @@ paths: summary: List spaces the user can view description: Retrieves information about spaces parameters: + - name: when + in: query + required: false + schema: + type: string - name: title in: query schema: @@ -4454,6 +4511,25 @@ paths: summary: List spaces the user can add to description: Retrieves a list of spaces that the user has permission to add to + parameters: + - name: when + in: query + required: false + schema: + type: string + - name: title + in: query + schema: + type: string + - name: date + in: query + schema: + type: string + - name: limit + in: query + description: default as 12 + schema: + type: number responses: 200: description: OK From 8cd8acc835125debe3073c396b7361f1f51c0409 Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Tue, 31 Aug 2021 15:22:07 -0500 Subject: [PATCH 24/38] add additional missing paramters to swagger --- public/swagger.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/public/swagger.yml b/public/swagger.yml index 4eeee5d98..b3f4ff00d 100644 --- a/public/swagger.yml +++ b/public/swagger.yml @@ -1784,6 +1784,15 @@ paths: required: false schema: type: string + - name: limit + in: query + description: The number of collections returns, default as 12. + schema: + type: integer + - name: exact + in: query + schema: + type: boolean responses: 200: description: OK @@ -2654,6 +2663,15 @@ paths: required: false schema: type: string + - name: limit + in: query + description: The number of collections returns, default as 12. + schema: + type: integer + - name: exact + in: query + schema: + type: boolean responses: 200: description: OK From bbbe0c0a54011168a15a7f3956495ddc031e5609 Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Tue, 31 Aug 2021 15:27:10 -0500 Subject: [PATCH 25/38] add when in datasetPossibleParents --- public/swagger.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/public/swagger.yml b/public/swagger.yml index b3f4ff00d..1580e9358 100644 --- a/public/swagger.yml +++ b/public/swagger.yml @@ -3805,6 +3805,11 @@ paths: the dataset in this space, otherwise list all possiable collections description: Collections are groupings of datasets parameters: + - name: when + in: query + required: false + schema: + type: string - name: coll_id in: path required: true From 3454c4d3643f645ba059a7378f954133ce04a982 Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Tue, 31 Aug 2021 17:02:52 -0500 Subject: [PATCH 26/38] add changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 686046231..eff74f999 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## Unreleased +### Added +- Add "when" parameter in a few GET API endpoints to enable pagination [#266](https://github.com/clowder-framework/clowder/issues/266) + + ## 1.18.1 - 2021-08-16 This release fixes a critical issue where invalid zip files could result in the files not being uploaded correctly. To check to see if you are affected, please use the following query: From fa651f199f72bc434b36ee2779f67e92296fba73 Mon Sep 17 00:00:00 2001 From: Rob Kooper Date: Fri, 17 Sep 2021 10:03:38 -0500 Subject: [PATCH 27/38] fix typos (#269) * fix typos * Added changelog entry and Tim Yardley to CONTRIBUTORS.md. Co-authored-by: Tim Yardley Co-authored-by: Luigi Marini Co-authored-by: Luigi Marini --- CHANGELOG.md | 5 ++++- CONTRIBUTORS.md | 1 + app/api/Selected.scala | 6 ++--- app/api/Sensors.scala | 2 +- app/assets/javascripts/select-bulk.js | 2 +- app/assets/javascripts/select.js | 4 ++-- app/views/admin/customize.scala.html | 2 +- app/views/admin/tos.scala.html | 2 +- app/views/admin/users.scala.html | 2 +- app/views/bookmarklet.scala.html | 14 ++++++------ app/views/collectionPreviews.scala.html | 4 ++-- app/views/commentform.scala.html | 2 +- app/views/dataset.scala.html | 6 ++--- app/views/datasetInfo.scala.html | 2 +- app/views/datasets/create.scala.html | 2 +- app/views/datasets/editInfo.scala.html | 2 +- app/views/datasets/filesAndFolders.scala.html | 6 ++--- app/views/datasets/tags.scala.html | 4 ++-- app/views/datasetsContainingFile.scala.html | 2 +- app/views/emailAdmin.scala.html | 2 +- app/views/errorPage.scala.html | 2 +- .../submitDatasetExtraction.scala.html | 2 +- .../submitFileExtraction.scala.html | 2 +- .../submitSelectedExtraction.scala.html | 2 +- app/views/file.scala.html | 10 ++++----- app/views/files/share.scala.html | 2 +- app/views/geostreams/map.scala.html | 4 ++-- app/views/logoSelect.scala.html | 6 ++--- app/views/main.scala.html | 8 +++---- .../manageMetadataDefinitions.scala.html | 2 +- app/views/metadatald/addMetadata.scala.html | 4 ++-- app/views/metadatald/newCard.scala.html | 4 ++-- app/views/metadatald/newTableRow.scala.html | 4 ++-- app/views/metadatald/search.scala.html | 4 ++-- app/views/metadatald/view.scala.html | 4 ++-- app/views/searchResults.scala.html | 2 +- app/views/selectDataset.scala.html | 2 +- app/views/spaces/spaceAllocation.scala.html | 2 +- app/views/util/masonryTabbed.scala.html | 2 +- app/views/viewDumpers.scala.html | 6 ++--- .../user_metadata_model_allowedNodes.txt~ | 13 ----------- .../DTSbookmarklet/jquery-1.7.2.js | 2 +- public/javascripts/adminIndex.js | 22 +++++++++---------- .../collectionChildCollectionsList.js | 4 ++-- public/javascripts/collectionDatasetsList.js | 4 ++-- public/javascripts/collectionListProcess.js | 8 +++---- public/javascripts/collectionModify.js | 4 ++-- public/javascripts/comment-delete.js | 2 +- public/javascripts/comment-edit.js | 2 +- public/javascripts/curationProcess.js | 8 +++---- .../dataset-attach-fileuploader.js | 2 +- public/javascripts/datasetListProcess.js | 8 +++---- public/javascripts/datasets/collections.js | 6 ++--- public/javascripts/deleteUtils.js | 4 ++-- public/javascripts/extractors/extractors.js | 6 ++--- .../jquery.fileupload-clowder-auth.js | 4 ++-- public/javascripts/fileListProcess.js | 4 ++-- public/javascripts/files/datasets.js | 6 ++--- public/javascripts/files/main.js | 2 +- public/javascripts/folderListProcess.js | 2 +- public/javascripts/manageAdmins.js | 2 +- public/javascripts/manageRoles.js | 2 +- public/javascripts/metadata/addMetadata.js | 4 ++-- public/javascripts/people.js | 2 +- .../previewers/sectionRubberband.js | 2 +- .../previewers/x3dom/some-library.js | 4 ++-- public/javascripts/repositories.js | 2 +- public/javascripts/searchUserMetadata.js | 2 +- public/javascripts/sectionsListProcess.js | 2 +- public/javascripts/sensors/assign.js | 4 ++-- public/javascripts/sensors/removeRelation.js | 2 +- public/javascripts/spaceModify.js | 16 +++++++------- public/javascripts/spaceconfiguration.js | 2 +- public/javascripts/updateLicenseInfo.js | 2 +- public/javascripts/userMetadata.js | 2 +- 75 files changed, 150 insertions(+), 159 deletions(-) delete mode 100644 public/datasetsUserMetadataModel/user_metadata_model_allowedNodes.txt~ diff --git a/CHANGELOG.md b/CHANGELOG.md index d976aef72..73cf5b602 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,13 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased + +### Fixed +- Typos "success" when returning status from API and "occurred" when logging to console. + ### Added - Add "when" parameter in a few GET API endpoints to enable pagination [#266](https://github.com/clowder-framework/clowder/issues/266) - ## 1.18.1 - 2021-08-16 This release fixes a critical issue where invalid zip files could result in the files not being uploaded correctly. To check to see if you are affected, please use the following query: diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 372a6c941..dffa18f38 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -34,6 +34,7 @@ Following is a list of contributors in alphabetical order: - Sandeep Puthanveetil Satheesan - Smruti Padhy - Theerasit Issaranon +- Tim Yardley - Todd Nicholson - Varun Kethineedi - Ward Poelmans diff --git a/app/api/Selected.scala b/app/api/Selected.scala index 3f5b3996e..0ef35436f 100644 --- a/app/api/Selected.scala +++ b/app/api/Selected.scala @@ -87,7 +87,7 @@ class Selected @Inject()(selections: SelectionService, selections.get(user.email.get).map(d => { selections.remove(d.id, user.email.get) }) - Ok(toJson(Map("sucess"->"true"))) + Ok(toJson(Map("success"->"true"))) } } } @@ -100,7 +100,7 @@ class Selected @Inject()(selections: SelectionService, datasets.removeDataset(d.id, Utils.baseUrl(request), request.apiKey, request.user) selections.remove(d.id, user.email.get) }) - Ok(toJson(Map("sucess"->"true"))) + Ok(toJson(Map("success"->"true"))) } } } @@ -189,7 +189,7 @@ class Selected @Inject()(selections: SelectionService, events.addObjectEvent(request.user, d.id, d.name, EventType.ADD_TAGS_DATASET.toString) datasets.index(d.id) }) - Ok(toJson(Map("sucess"->"true"))) + Ok(toJson(Map("success"->"true"))) } } } diff --git a/app/api/Sensors.scala b/app/api/Sensors.scala index 6a5b49eee..e6b5bad90 100644 --- a/app/api/Sensors.scala +++ b/app/api/Sensors.scala @@ -5,7 +5,7 @@ import play.api.Play.current import services.PostgresPlugin /** - * Metadata about sensors registered with the system. Datastreams can be associalted with sensors. + * Metadata about sensors registered with the system. Datastreams can be associated with sensors. */ object Sensors extends Controller with ApiController { diff --git a/app/assets/javascripts/select-bulk.js b/app/assets/javascripts/select-bulk.js index b2dd09111..30a4f68f9 100644 --- a/app/assets/javascripts/select-bulk.js +++ b/app/assets/javascripts/select-bulk.js @@ -20,7 +20,7 @@ $(function() { }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+ textStatus, errorThrown); + console.error("The following error occurred: "+ textStatus, errorThrown); window.location = "../login"; // FIXME hardcoded }); diff --git a/app/assets/javascripts/select.js b/app/assets/javascripts/select.js index ba387aad7..137bc2be5 100644 --- a/app/assets/javascripts/select.js +++ b/app/assets/javascripts/select.js @@ -19,7 +19,7 @@ $(function() { }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+ textStatus, errorThrown); + console.error("The following error occurred: "+ textStatus, errorThrown); window.location = "../login"; // FIXME hardcoded }); @@ -40,7 +40,7 @@ $(function() { }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+ textStatus, errorThrown); + console.error("The following error occurred: "+ textStatus, errorThrown); window.location = "../login"; // FIXME hardcoded }); diff --git a/app/views/admin/customize.scala.html b/app/views/admin/customize.scala.html index f1c0c41fa..e8e00cef5 100644 --- a/app/views/admin/customize.scala.html +++ b/app/views/admin/customize.scala.html @@ -130,7 +130,7 @@

Customize

}); }); }).fail(function(jqXHR) { - console.error("The following error occured: " + jqXHR.responseText); + console.error("The following error occurred: " + jqXHR.responseText); notify("The application preferences was not updated", "error"); }); diff --git a/app/views/admin/tos.scala.html b/app/views/admin/tos.scala.html index f577f4dc9..7b22bb833 100644 --- a/app/views/admin/tos.scala.html +++ b/app/views/admin/tos.scala.html @@ -54,7 +54,7 @@

Terms of Service

}).done(function() { notify("The terms of service have been updated", "success"); }).fail(function(jqXHR) { - console.error("The following error occured: " + jqXHR.responseText); + console.error("The following error occurred: " + jqXHR.responseText); notify("The terms of service are not updated", "error"); }); diff --git a/app/views/admin/users.scala.html b/app/views/admin/users.scala.html index dc467b1e3..a5b0ed626 100644 --- a/app/views/admin/users.scala.html +++ b/app/views/admin/users.scala.html @@ -212,7 +212,7 @@

users = { active: [], inactive: [], admin: [], unadmin: [] }; notify("Users successfully updated.", "success", false, 5000); }).fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); notify("Could not update users : " + errorThrown, "error"); }); } else { diff --git a/app/views/bookmarklet.scala.html b/app/views/bookmarklet.scala.html index 3a5819985..6efe8ed5f 100644 --- a/app/views/bookmarklet.scala.html +++ b/app/views/bookmarklet.scala.html @@ -31,7 +31,7 @@ var outstandingExtractions = new Array(); var tags = {}; var failedCountDown; - var sucessfullyDone; + var successfullyDone; var countDone; var status = ''; var imgdocs = {}; @@ -352,7 +352,7 @@ outstandingExtractions = new Array(); tags = {}; failedCountDown = 0; - sucessfullyDone = 0; + successfullyDone = 0; countDone = 0; status = ''; imgdocs = {}; @@ -430,7 +430,7 @@ // console.log("Checking submission status"); countDone=0; failedCountDown=0; - sucessfullyDone=0; + successfullyDone=0; for ( var j = 0; j < filesInfo.length; j++) { if (filesInfo[j].status == 'Processing') { @@ -438,8 +438,8 @@ } else if (filesInfo[j].status == 'Done') { countDone++; console.log('---countDone Incremented-- '); - sucessfullyDone++; - // jQuery('#DTSSuccessfulExtractions').text(sucessfullyDone); + successfullyDone++; + // jQuery('#DTSSuccessfulExtractions').text(successfullyDone); } else if (filesInfo[j].status == 'Required Extractor is either busy or is not currently running. Try after some time.') { console.log('---[check Results]---status: Required Extractor is either busy or is not currently running. Try after some time.'); } @@ -457,9 +457,9 @@ } } // TODO update ui counts - jQuery('#DTSSuccessfulExtractions').text(sucessfullyDone); + jQuery('#DTSSuccessfulExtractions').text(successfullyDone); jQuery('#DTSFailedExtractions').text(failedCountDown); - console.log('Successfull: ' + sucessfullyDone); + console.log('Successfull: ' + successfullyDone); console.log('Failed: ' + failedCountDown); if (countDone > 0 && countDone == filesInfo.length) { status = 'Done'; diff --git a/app/views/collectionPreviews.scala.html b/app/views/collectionPreviews.scala.html index 69fca56a2..ddb792f27 100644 --- a/app/views/collectionPreviews.scala.html +++ b/app/views/collectionPreviews.scala.html @@ -62,12 +62,12 @@

Previewers available for this collection

}); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); }); }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); }); diff --git a/app/views/commentform.scala.html b/app/views/commentform.scala.html index d8355d53f..5622f0613 100644 --- a/app/views/commentform.scala.html +++ b/app/views/commentform.scala.html @@ -182,7 +182,7 @@ }); request.fail(function(jqXHR, textStatus, errorThrown) { - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to post a comment."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("Error posting comment : " + errorThrown, "error"); diff --git a/app/views/dataset.scala.html b/app/views/dataset.scala.html index e03ce1819..87e0589a0 100644 --- a/app/views/dataset.scala.html +++ b/app/views/dataset.scala.html @@ -450,7 +450,7 @@

Statistics

}); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); notify("Failed to archive dataset.", "error"); }); } @@ -466,7 +466,7 @@

Statistics

}); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); notify("Failed to unarchive dataset.", "error"); }); } @@ -694,7 +694,7 @@

Statistics

request.fail(function (jqXHR, textStatus, errorThrown){ $('#name').val(""); - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to create a new folder."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("Error in creating folder: " + errorThrown, "error"); diff --git a/app/views/datasetInfo.scala.html b/app/views/datasetInfo.scala.html index 1b1d16d9f..df82b6b32 100644 --- a/app/views/datasetInfo.scala.html +++ b/app/views/datasetInfo.scala.html @@ -23,7 +23,7 @@ request.fail(function (jqXHR, textStatus, errorThrown){ console.error( - "The following error occured: "+ + "The following error occurred: "+ textStatus, errorThrown ); diff --git a/app/views/datasets/create.scala.html b/app/views/datasets/create.scala.html index 11bce6e76..44e7b1854 100644 --- a/app/views/datasets/create.scala.html +++ b/app/views/datasets/create.scala.html @@ -233,7 +233,7 @@

@Messages("create.header", Messages("dataset.title"))

}); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to create a new dataset."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("Error in creating dataset. : " + errorThrown, "error"); diff --git a/app/views/datasets/editInfo.scala.html b/app/views/datasets/editInfo.scala.html index 0d6837bc3..3df2c0692 100644 --- a/app/views/datasets/editInfo.scala.html +++ b/app/views/datasets/editInfo.scala.html @@ -99,7 +99,7 @@ }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); var errMsg = "You must be logged in to update the information about a dataset."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The dataset information was not updated due to : " + errorThrown, "error"); diff --git a/app/views/datasets/filesAndFolders.scala.html b/app/views/datasets/filesAndFolders.scala.html index 5b64717e1..ff1350de1 100644 --- a/app/views/datasets/filesAndFolders.scala.html +++ b/app/views/datasets/filesAndFolders.scala.html @@ -175,7 +175,7 @@ getUpdatedFilesAndFolders(); }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); notify("Could not delete marked file "+selected[entry]+": " + errorThrown, "error"); // Add it back to selected list addFileToMarked(selected[entry]); @@ -263,7 +263,7 @@ getUpdatedFilesAndFolders(); }); request.fail(function (jqXHR, textStatus, errorThrown) { - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); notify("Could not tag marked file " + selected[entry] + ": " + errorThrown, "error"); }); } @@ -282,7 +282,7 @@ notify("Tag(s) successfully added.", "success", false, 2000); }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); notify("Could not tag selections: " + errorThrown, "error"); }); diff --git a/app/views/datasets/tags.scala.html b/app/views/datasets/tags.scala.html index f51aa02fd..38a57a104 100644 --- a/app/views/datasets/tags.scala.html +++ b/app/views/datasets/tags.scala.html @@ -95,7 +95,7 @@

Tags

}); request.fail(function (jqXHR, textStatus, errorThrown) { - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to remove a tag from a dataset."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The tag was not removed from the dataset due to : " + errorThrown, "error"); @@ -150,7 +150,7 @@

Tags

}); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); var errMsg = "You must be logged in to add a tag to a dataset."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The tag was not added to the dataset due to : " + errorThrown, "error"); diff --git a/app/views/datasetsContainingFile.scala.html b/app/views/datasetsContainingFile.scala.html index db553bf71..fb7e548a7 100644 --- a/app/views/datasetsContainingFile.scala.html +++ b/app/views/datasetsContainingFile.scala.html @@ -42,7 +42,7 @@ $(event.target.parentNode.parentNode).remove(); }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); var errMsg = "You must be logged in to remove a file from a dataset."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The file was not removed from the dataset due to : " + errorThrown, "error"); diff --git a/app/views/emailAdmin.scala.html b/app/views/emailAdmin.scala.html index a0687adab..d7f158d95 100644 --- a/app/views/emailAdmin.scala.html +++ b/app/views/emailAdmin.scala.html @@ -59,7 +59,7 @@

Email admins

$("#subject").val('') $("#body").val('') }).fail(function (jqXHR, textStatus, errorThrown) { - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); notify("The application preferences was not updated due to : " + errorThrown, "error", false); }); return false; diff --git a/app/views/errorPage.scala.html b/app/views/errorPage.scala.html index b65de45d0..53ad47f8d 100644 --- a/app/views/errorPage.scala.html +++ b/app/views/errorPage.scala.html @@ -32,7 +32,7 @@

@re.toString()

}); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); }); }); diff --git a/app/views/extractions/submitDatasetExtraction.scala.html b/app/views/extractions/submitDatasetExtraction.scala.html index 000a7743c..e5f878b41 100644 --- a/app/views/extractions/submitDatasetExtraction.scala.html +++ b/app/views/extractions/submitDatasetExtraction.scala.html @@ -91,7 +91,7 @@

Submit dataset for extraction

}); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); }); } diff --git a/app/views/extractions/submitFileExtraction.scala.html b/app/views/extractions/submitFileExtraction.scala.html index 4b7a24c46..cb78ed618 100644 --- a/app/views/extractions/submitFileExtraction.scala.html +++ b/app/views/extractions/submitFileExtraction.scala.html @@ -130,7 +130,7 @@

Submit file for extraction

}); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); }); } diff --git a/app/views/extractions/submitSelectedExtraction.scala.html b/app/views/extractions/submitSelectedExtraction.scala.html index f1cacdd73..b0fb0d3a0 100644 --- a/app/views/extractions/submitSelectedExtraction.scala.html +++ b/app/views/extractions/submitSelectedExtraction.scala.html @@ -100,7 +100,7 @@

Submit marked files for extraction

}); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); }); } diff --git a/app/views/file.scala.html b/app/views/file.scala.html index 5b4a7a032..b765c1ad7 100644 --- a/app/views/file.scala.html +++ b/app/views/file.scala.html @@ -937,7 +937,7 @@

License

}); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); notify("Failed to archive file.", "error"); }); @@ -964,7 +964,7 @@

License

}); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); notify("Failed to unarchive file.", "error"); }); @@ -1152,7 +1152,7 @@

Tags

}); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to remove a tag from a file."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The tag was not removed from the file due to : " + errorThrown, "error"); @@ -1185,7 +1185,7 @@

Tags

}); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to remove a tag from a file."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The tag was not removed from the file due to : " + errorThrown, "error"); @@ -1249,7 +1249,7 @@

Tags

}); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to add a tag to a file."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The tag was not added to the file due to : " + errorThrown, "error"); diff --git a/app/views/files/share.scala.html b/app/views/files/share.scala.html index f99678ede..4576d0678 100644 --- a/app/views/files/share.scala.html +++ b/app/views/files/share.scala.html @@ -50,7 +50,7 @@

Datapoints

}); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); }); } @@ -148,7 +148,7 @@

Datapoints

request.fail(function (jqXHR, textStatus, errorThrown){ console.error( - "The following error occured: "+ + "The following error occurred: "+ textStatus, errorThrown ); }); diff --git a/app/views/logoSelect.scala.html b/app/views/logoSelect.scala.html index 5aafeb113..f5515c9c3 100644 --- a/app/views/logoSelect.scala.html +++ b/app/views/logoSelect.scala.html @@ -79,7 +79,7 @@ }).done(function() { nextFunction(); }).fail(function(jqXHR) { - console.error("The following error occured: " + jqXHR.responseText); + console.error("The following error occurred: " + jqXHR.responseText); notify("The logo for @path/@name was not updated", "error"); }); } @@ -101,7 +101,7 @@ }).done(function(response, textStatus, jqXHR) { nextFunction(); }).fail(function(jqXHR) { - console.error("The following error occured: " + jqXHR.responseText); + console.error("The following error occurred: " + jqXHR.responseText); notify("The logo for @path/@name was not updated", "error"); }); break; @@ -125,7 +125,7 @@ }).done(function() { nextFunction(); }).fail(function(jqXHR) { - console.error("The following error occured: " + jqXHR.responseText); + console.error("The following error occurred: " + jqXHR.responseText); notify("The logo for @path/@name was not updated", "error"); }); } else { diff --git a/app/views/main.scala.html b/app/views/main.scala.html index e19f845b0..2e580ab1a 100644 --- a/app/views/main.scala.html +++ b/app/views/main.scala.html @@ -340,7 +340,7 @@ updateSelectedCount(response.length); }); request.fail(function (jqXHR, textStatus, errorThrown) { - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); if (errorThrown != "Unauthorized") { notify("Could not get selections: " + errorThrown, "error"); } @@ -385,7 +385,7 @@ location.reload() }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); notify("Could not delete selections: " + errorThrown, "error"); }); } @@ -407,7 +407,7 @@ location.reload() }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); notify("Could not clear selections: " + errorThrown, "error"); }); } @@ -458,7 +458,7 @@ notify("Tag(s) successfully added.", "success", false, 2000); }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); notify("Could not tag selections: " + errorThrown, "error"); }); diff --git a/app/views/manageMetadataDefinitions.scala.html b/app/views/manageMetadataDefinitions.scala.html index 7bd957af8..4dbb29827 100644 --- a/app/views/manageMetadataDefinitions.scala.html +++ b/app/views/manageMetadataDefinitions.scala.html @@ -190,7 +190,7 @@ }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to delete metadata definition"; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("Metadata definition was not removed due to : " + errorThrown, "error"); diff --git a/app/views/metadatald/addMetadata.scala.html b/app/views/metadatald/addMetadata.scala.html index 9b643ac75..c1d07f9cc 100644 --- a/app/views/metadatald/addMetadata.scala.html +++ b/app/views/metadatald/addMetadata.scala.html @@ -216,7 +216,7 @@
Add metadata
}); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to add metadata"; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("Metadata was not added due to : " + errorThrown, "error"); @@ -499,7 +499,7 @@
Add metadata
}); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to retrieve metadata definitions"; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("Metadata was not added due to : " + errorThrown, "error"); diff --git a/app/views/metadatald/newCard.scala.html b/app/views/metadatald/newCard.scala.html index d9de55923..75af6fb6e 100644 --- a/app/views/metadatald/newCard.scala.html +++ b/app/views/metadatald/newCard.scala.html @@ -84,7 +84,7 @@ }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to add metadata"; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("Metadata was not removed due to : " + errorThrown, "error"); @@ -131,7 +131,7 @@ request.fail(function (jqXHR, textStatus, errorThrown){ $("#panel_@m.id > #"+mid).removeClass("popping"); - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to retrieve metadata definitions"; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("Metadata context was not shown due to : " + errorThrown, "error"); diff --git a/app/views/metadatald/newTableRow.scala.html b/app/views/metadatald/newTableRow.scala.html index cb58b23b3..a903ccf7f 100644 --- a/app/views/metadatald/newTableRow.scala.html +++ b/app/views/metadatald/newTableRow.scala.html @@ -69,7 +69,7 @@ }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to add metadata"; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("Metadata was not removed due to : " + errorThrown, "error"); @@ -116,7 +116,7 @@ request.fail(function (jqXHR, textStatus, errorThrown){ $("#"+mid).removeClass("popping"); - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to retrieve metadata definitions"; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("Metadata context was not shown due to : " + errorThrown, "error"); diff --git a/app/views/metadatald/search.scala.html b/app/views/metadatald/search.scala.html index eaf3e4311..713dd31e4 100644 --- a/app/views/metadatald/search.scala.html +++ b/app/views/metadatald/search.scala.html @@ -164,7 +164,7 @@

Search Metadata within Space: "@space.name"

}); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to retrieve metadata definitions"; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("Unable to fetch Metadata definitions : " + errorThrown, "error"); @@ -498,7 +498,7 @@

Search Metadata within Space: "@space.name"

}); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); searching = false; }); } diff --git a/app/views/metadatald/view.scala.html b/app/views/metadatald/view.scala.html index 0b040c391..ce8cc0acc 100644 --- a/app/views/metadatald/view.scala.html +++ b/app/views/metadatald/view.scala.html @@ -223,7 +223,7 @@

Metadata

}); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to add metadata"; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("Metadata was not removed due to : " + errorThrown, "error"); @@ -270,7 +270,7 @@

Metadata

request.fail(function (jqXHR, textStatus, errorThrown){ $("#"+mid).removeClass("popping"); - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to retrieve metadata definitions"; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("Metadata context was not shown due to : " + errorThrown, "error"); diff --git a/app/views/searchResults.scala.html b/app/views/searchResults.scala.html index 2cd2d1655..756cee08d 100644 --- a/app/views/searchResults.scala.html +++ b/app/views/searchResults.scala.html @@ -200,7 +200,7 @@

Search

}); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); }); } diff --git a/app/views/selectDataset.scala.html b/app/views/selectDataset.scala.html index 3d4466c55..a2ec882ec 100644 --- a/app/views/selectDataset.scala.html +++ b/app/views/selectDataset.scala.html @@ -22,7 +22,7 @@ }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+ textStatus, errorThrown); + console.error("The following error occurred: "+ textStatus, errorThrown); window.location = "../login"; // FIXME hardcoded }); }); diff --git a/app/views/spaces/spaceAllocation.scala.html b/app/views/spaces/spaceAllocation.scala.html index f68def0ef..a97659209 100644 --- a/app/views/spaces/spaceAllocation.scala.html +++ b/app/views/spaces/spaceAllocation.scala.html @@ -142,7 +142,7 @@

Copy Collection to Space

}); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "Error message."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("Error : " + errorThrown, "error"); diff --git a/app/views/util/masonryTabbed.scala.html b/app/views/util/masonryTabbed.scala.html index edd4d471a..e1af6ae19 100644 --- a/app/views/util/masonryTabbed.scala.html +++ b/app/views/util/masonryTabbed.scala.html @@ -44,7 +44,7 @@ }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); notify("Could not get moe events: " + errorThrown, "error"); }); } diff --git a/app/views/viewDumpers.scala.html b/app/views/viewDumpers.scala.html index 73ba9fa99..9bf56689f 100644 --- a/app/views/viewDumpers.scala.html +++ b/app/views/viewDumpers.scala.html @@ -27,7 +27,7 @@

Data dumps

alert(response); }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); alert("The metadata of the files (some or all of them) were not dumped due to : " + errorThrown); }); return false; @@ -43,7 +43,7 @@

Data dumps

alert(response); }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); alert("The metadata of the datasets (some or all of them) were not dumped due to : " + errorThrown); }); return false; @@ -59,7 +59,7 @@

Data dumps

alert(response); }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); alert("The file groupings of the datasets (some or all of them) were not dumped due to : " + errorThrown); }); return false; diff --git a/public/datasetsUserMetadataModel/user_metadata_model_allowedNodes.txt~ b/public/datasetsUserMetadataModel/user_metadata_model_allowedNodes.txt~ deleted file mode 100644 index c113d5886..000000000 --- a/public/datasetsUserMetadataModel/user_metadata_model_allowedNodes.txt~ +++ /dev/null @@ -1,13 +0,0 @@ -Abstract,String,http://purl.org/dc/terms/abstract -Alternative title,String,http://purl.org/dc/terms/alternative -Audience,String,http://purl.org/dc/terms/audience -Bibliographic citation,String,http://purl.org/dc/terms/bibliographicCitation -Contact,Node,http://dts.ncsa.illinois.edu/terms/contact -Name,String,http://dts.ncsa.illinois.edu/terms/name -Email,String,http://dts.ncsa.illinois.edu/terms/email -Creator,String,http://purl.org/dc/elements/1.1/creator -Description,String,http://purl.org/dc/elements/1.1/description -Spatial Reference,Node,http://dts.ncsa.illinois.edu/terms/spatialReference -Latitude,String,http://dts.ncsa.illinois.edu/terms/latitude -Longitude,String,http://dts.ncsa.illinois.edu/terms/longitude -Altitude,String,http://dts.ncsa.illinois.edu/terms/altitude diff --git a/public/javascripts/DTSbookmarklet/jquery-1.7.2.js b/public/javascripts/DTSbookmarklet/jquery-1.7.2.js index 3774ff986..75ce26177 100644 --- a/public/javascripts/DTSbookmarklet/jquery-1.7.2.js +++ b/public/javascripts/DTSbookmarklet/jquery-1.7.2.js @@ -8249,7 +8249,7 @@ if ( jQuery.support.ajax ) { xml; // Firefox throws exceptions when accessing properties - // of an xhr when a network error occured + // of an xhr when a network error occurred // http://helpful.knobs-dials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE) try { diff --git a/public/javascripts/adminIndex.js b/public/javascripts/adminIndex.js index d92be7a9e..d4c1ec783 100644 --- a/public/javascripts/adminIndex.js +++ b/public/javascripts/adminIndex.js @@ -61,7 +61,7 @@ function getAllAdapters() { }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); }); } @@ -79,7 +79,7 @@ function getAllExtractors() { } }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); }); } @@ -95,7 +95,7 @@ function getAllMeasures() { } }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); }); } @@ -114,7 +114,7 @@ function getIndexers() { $("#indexerDropDown").html(''+availableIndexers); }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); }); } @@ -135,7 +135,7 @@ function getIndexes() { $("#indexDel").html(''+availableIndexes); }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); }); } @@ -378,7 +378,7 @@ var createBtn=document.getElementById('create'); $('#createIndexFeedback').text( data); }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); $('#createIndexFeedback').text( "Index could not be created"); }); } @@ -397,7 +397,7 @@ var buildBtn=document.getElementById('build'); $('#buildmsg').text(response); }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); $('#buildmsg').text( "Could not build index"); }); } @@ -438,7 +438,7 @@ var listBtn=document.getElementById('list'); } }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); }); } @@ -458,7 +458,7 @@ var listBtn=document.getElementById('list'); $('#deletemsg').text(response); }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); $('#deletemsg').text( "Could not delete index"); }); } @@ -480,7 +480,7 @@ var listBtn=document.getElementById('list'); $('#deleteallmsg').text(response); }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); $('#deleteallmsg').text( "Could not delete all indexes"); }); } @@ -499,7 +499,7 @@ reindex.onclick=function(evt){ $('#reindexmsg').text(response.status); }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); $('#reindexmsg').text( "Could not reindex"); }); } diff --git a/public/javascripts/collectionChildCollectionsList.js b/public/javascripts/collectionChildCollectionsList.js index 9c4f233b6..1c88adf82 100644 --- a/public/javascripts/collectionChildCollectionsList.js +++ b/public/javascripts/collectionChildCollectionsList.js @@ -39,7 +39,7 @@ }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); var errMsg = "You must be logged in to add a child collection to a collection."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The dataset was not added to the collection due to : " + errorThrown, "error"); @@ -60,7 +60,7 @@ }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); var errMsg = "You must be logged in to remove a child collection from a collection."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The child collection was not removed from the collection due to : " + errorThrown, "error"); diff --git a/public/javascripts/collectionDatasetsList.js b/public/javascripts/collectionDatasetsList.js index 267674387..ce02925ed 100644 --- a/public/javascripts/collectionDatasetsList.js +++ b/public/javascripts/collectionDatasetsList.js @@ -37,7 +37,7 @@ }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); var errMsg = "You must be logged in to add a dataset to a collection."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The dataset was not added to the collection due to : " + errorThrown, "error"); @@ -58,7 +58,7 @@ }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); var errMsg = "You must be logged in to remove a dataset from a collection."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The dataset was not removed from the collection due to : " + errorThrown, "error"); diff --git a/public/javascripts/collectionListProcess.js b/public/javascripts/collectionListProcess.js index fe7b26702..191ead481 100644 --- a/public/javascripts/collectionListProcess.js +++ b/public/javascripts/collectionListProcess.js @@ -24,7 +24,7 @@ function removeCollection(id, isreload, newurl, resourceFromType){ } }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); var errMsg = "You must be logged in to remove a collection from the system."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The collection was not removed due to : " + errorThrown, "error"); @@ -46,7 +46,7 @@ function removeCollectionAndRedirect(collectionId, url){ }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to delete a collection from the system."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The collection was not deleted from the system due to : " + errorThrown, "error"); @@ -68,7 +68,7 @@ function removeChildCollectionFromParent(parentId, childId, url) { }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to remove a subcollection from the system."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The child collection was not removed from the system due to : " + errorThrown, "error"); @@ -86,7 +86,7 @@ function restoreCollection(id,isreload,newurl){ }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); var errMsg = "You must be logged in to remove a collection from the system."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The collection was not restored due to : " + errorThrown, "error"); diff --git a/public/javascripts/collectionModify.js b/public/javascripts/collectionModify.js index 43f541d06..a50244ff1 100644 --- a/public/javascripts/collectionModify.js +++ b/public/javascripts/collectionModify.js @@ -42,7 +42,7 @@ function addCollectionToParentCollection(id) { }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to add a collection to a space."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The collection was not added to the space due to the following : " + errorThrown, "error"); @@ -86,7 +86,7 @@ function addDatasetToCollection(id) { }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to add a dataset to a collection."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The dataset was not added to the collection due to the following : " + errorThrown, "error"); diff --git a/public/javascripts/comment-delete.js b/public/javascripts/comment-delete.js index 18e48fe16..d6f362e7b 100644 --- a/public/javascripts/comment-delete.js +++ b/public/javascripts/comment-delete.js @@ -21,7 +21,7 @@ function deleteComment(commentId, reloadPage){ request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to delete a comment."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The comment was not deleted due to : " + errorThrown, "error"); diff --git a/public/javascripts/comment-edit.js b/public/javascripts/comment-edit.js index 31457eca7..1cfef5c64 100644 --- a/public/javascripts/comment-edit.js +++ b/public/javascripts/comment-edit.js @@ -35,7 +35,7 @@ function editComment(commentId, commentText, senderName, senderEmail, commentLin request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to edit a comment."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The comment was not edited due to : " + errorThrown, "error"); diff --git a/public/javascripts/curationProcess.js b/public/javascripts/curationProcess.js index 5b72a8e24..2797604b1 100644 --- a/public/javascripts/curationProcess.js +++ b/public/javascripts/curationProcess.js @@ -16,7 +16,7 @@ function removeCuration(id, isreload, url){ }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to delete a publication request from the system."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The publication request was not deleted from the system due to : " + errorThrown, "error"); @@ -56,7 +56,7 @@ function removeCurationFile(id, currentFolder, curationid){ }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to remove a file from the publication request"; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("File was not removed from the request due to : " + jqXHR.textResponse, "error"); @@ -75,7 +75,7 @@ function removeCurationFolder(id, parentCurationObject, parentId){ }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to remove a folder from the publication request"; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("Folder was not removed due to : " + errorThrown, "error"); @@ -107,7 +107,7 @@ function getUpdatedFilesAndFolders(curationObject, limit) { $('#files').html(response); }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to see files and folders."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("Error in getting more files and folders. : " + errorThrown, "error"); diff --git a/public/javascripts/dataset-attach-fileuploader.js b/public/javascripts/dataset-attach-fileuploader.js index ef1fbf4bd..25900ae4f 100644 --- a/public/javascripts/dataset-attach-fileuploader.js +++ b/public/javascripts/dataset-attach-fileuploader.js @@ -32,7 +32,7 @@ $(function () { }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); var errMsg = "You must be logged in to add a dataset to a collection."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The fileset message was not generated due to : " + errorThrown, "error"); diff --git a/public/javascripts/datasetListProcess.js b/public/javascripts/datasetListProcess.js index c87d866b0..8058307c1 100644 --- a/public/javascripts/datasetListProcess.js +++ b/public/javascripts/datasetListProcess.js @@ -19,7 +19,7 @@ function removeDataset(datasetId, isreload, url){ } }); request.fail(function (jqXHR, textStatus, errorThrown) { - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); var errMsg = "You must be logged in to remove a dataset from the system."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The dataset was not removed due to : " + errorThrown, "error"); @@ -40,7 +40,7 @@ function restoreDataset(id,isreload,newurl){ }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); var errMsg = "You must be logged in to remove a collection from the system."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The collection was not restored due to : " + errorThrown, "error"); @@ -61,7 +61,7 @@ function removeDatasetAndRedirect(datasetId, url){ }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to delete a dataset from the system."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The dataset was not deleted from the system due to : " + errorThrown, "error"); @@ -83,7 +83,7 @@ function detachAndRemoveDatasetAndRedirect(datasetId, url){ }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to detach files and then delete a dataset from the system."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The dataset was not deleted from the system due to : " + errorThrown, "error"); diff --git a/public/javascripts/datasets/collections.js b/public/javascripts/datasets/collections.js index 1d2df4e17..2fbe48223 100644 --- a/public/javascripts/datasets/collections.js +++ b/public/javascripts/datasets/collections.js @@ -37,7 +37,7 @@ function addToCollection(datasetId) { }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to add a dataset to a collection."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The dataset was not added to the collection due to the following : " + errorThrown, "error"); @@ -57,7 +57,7 @@ function removeFromCollection(collectionId, datasetId, event){ }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to remove a dataset from a collection."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The dataset was not removed from the collection due to : " + errorThrown, "error"); @@ -86,7 +86,7 @@ function removeDatasetFromCollectionAndRedirect(collectionId, datasetId, isreloa }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to remove a dataset from a collection."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The dataset was not removed from the collection due to : " + errorThrown, "error"); diff --git a/public/javascripts/deleteUtils.js b/public/javascripts/deleteUtils.js index eb903357b..aed9a2c4c 100644 --- a/public/javascripts/deleteUtils.js +++ b/public/javascripts/deleteUtils.js @@ -239,7 +239,7 @@ function clearTrashCollections(url){ window.location.href=url; }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); var errMsg = "You must be logged in to remove a collection from the system."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The trash was not removed due to : " + errorThrown, "error"); @@ -255,7 +255,7 @@ function clearTrashDatasets(url){ window.location.href=url; }); request.fail(function (jqXHR, textStatus, errorThrown) { - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); var errMsg = "You must be logged in to remove a dataset from the system."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The dataset trash was not cleared due to : " + errorThrown, "error"); diff --git a/public/javascripts/extractors/extractors.js b/public/javascripts/extractors/extractors.js index ada1d865c..1ef10a15b 100644 --- a/public/javascripts/extractors/extractors.js +++ b/public/javascripts/extractors/extractors.js @@ -20,7 +20,7 @@ function cancelSubmission(id, submit_id, extractor_id, extractType) { }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); }); } @@ -136,7 +136,7 @@ function saveExtractorsLabel(label) { }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var operation = label.id ? "update" : "create"; var specificError = jqXHR.responseText; var msg = "Failed to " + operation + " label: " + specificError; @@ -158,7 +158,7 @@ function deleteExtractorsLabel(id) { }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var specificError = jqXHR.responseText; var msg = "Failed to delete label: " + specificError notify(msg, "error"); diff --git a/public/javascripts/file-uploader/jquery.fileupload-clowder-auth.js b/public/javascripts/file-uploader/jquery.fileupload-clowder-auth.js index 672fd5073..c39a113ad 100644 --- a/public/javascripts/file-uploader/jquery.fileupload-clowder-auth.js +++ b/public/javascripts/file-uploader/jquery.fileupload-clowder-auth.js @@ -50,7 +50,7 @@ $(function () { request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("addCallback - fileUploader - The following error occured: " + textStatus, errorThrown); + console.error("addCallback - fileUploader - The following error occurred: " + textStatus, errorThrown); authInProcess = false; var errMsg = "You must be logged in to upload new files."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { @@ -132,7 +132,7 @@ $(function () { request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("submitCallback - fileUploader - The following error occured: " + textStatus, errorThrown); + console.error("submitCallback - fileUploader - The following error occurred: " + textStatus, errorThrown); authInProcess = false; var errMsg = "You must be logged in to upload new files."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { diff --git a/public/javascripts/fileListProcess.js b/public/javascripts/fileListProcess.js index ff3da0aa9..c4cc6a2d3 100644 --- a/public/javascripts/fileListProcess.js +++ b/public/javascripts/fileListProcess.js @@ -21,7 +21,7 @@ function removeFile(fileId, isreload, url){ }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to delete a file from the system."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The file was not deleted from the system due to : " + errorThrown, "error"); @@ -42,7 +42,7 @@ function removeFileAndRedirect(fileId, url){ }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to delete a file from the system."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The file was not deleted from the system due to : " + errorThrown, "error"); diff --git a/public/javascripts/files/datasets.js b/public/javascripts/files/datasets.js index ad6ac8059..cf3f2642f 100644 --- a/public/javascripts/files/datasets.js +++ b/public/javascripts/files/datasets.js @@ -20,7 +20,7 @@ function moveFromDatasetToDataset(folderFromId, datasetFromId, fileId) { }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to move a file to a dataset."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The file was not moved to the dataset due to the following : " + errorThrown, "error"); @@ -52,7 +52,7 @@ function addToDataset(fileId) { }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to add a file to a dataset."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The file was not added to the dataset due to the following : " + errorThrown, "error"); @@ -71,7 +71,7 @@ function removeFromDataset(datasetId, fileId, event){ }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to remove a file from a dataset."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The file was not removed from the dataset due to : " + errorThrown, "error"); diff --git a/public/javascripts/files/main.js b/public/javascripts/files/main.js index fd256a918..d7dfcf352 100644 --- a/public/javascripts/files/main.js +++ b/public/javascripts/files/main.js @@ -26,7 +26,7 @@ function detachFile(fileId, fileName,event){ $("#fileUserMetadata_"+fileId).remove(); }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); var errMsg = "You must be logged in to remove a file from a dataset."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The file was not removed from the dataset due to : " + errorThrown, "error"); diff --git a/public/javascripts/folderListProcess.js b/public/javascripts/folderListProcess.js index 90155ebb0..5a637c33f 100644 --- a/public/javascripts/folderListProcess.js +++ b/public/javascripts/folderListProcess.js @@ -12,7 +12,7 @@ function removeFolder(folderId, parentDataset) { } }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); var errMsg = "You must be logged in to remove a folder from the system."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The folder was not removed due to : " + errorThrown, "error"); diff --git a/public/javascripts/manageAdmins.js b/public/javascripts/manageAdmins.js index 8ed5ac170..f296e6729 100644 --- a/public/javascripts/manageAdmins.js +++ b/public/javascripts/manageAdmins.js @@ -19,7 +19,7 @@ function removeAdmin(email){ }); request.fail(function (jqXHR, textStatus, errorThrown){ console.error( - "The following error occured: "+ + "The following error occurred: "+ textStatus, errorThrown ); notify("ERROR: " + errorThrown +". Admin not removed.", error); diff --git a/public/javascripts/manageRoles.js b/public/javascripts/manageRoles.js index 50f7819c1..6b6cf8f1f 100644 --- a/public/javascripts/manageRoles.js +++ b/public/javascripts/manageRoles.js @@ -15,7 +15,7 @@ function removeRole(roleId, url) }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in and be an administrator to remove a role from the system."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The role was not deleted from the system due to : " + errorThrown, "error"); diff --git a/public/javascripts/metadata/addMetadata.js b/public/javascripts/metadata/addMetadata.js index 5a856d48f..9ac91a1cb 100644 --- a/public/javascripts/metadata/addMetadata.js +++ b/public/javascripts/metadata/addMetadata.js @@ -85,7 +85,7 @@ function loadExternalResource(field_label, field_description, field_id, field_ty }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); notify("Could not retrieve external vocabulary: " + errorThrown, "error"); }); @@ -228,7 +228,7 @@ function loadExternalResource(field_label, field_description, field_id, field_ty }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); notify("Could not retrieve external vocabulary: " + errorThrown, "error"); }); } diff --git a/public/javascripts/people.js b/public/javascripts/people.js index 2b8b74c39..9b62f09e2 100644 --- a/public/javascripts/people.js +++ b/public/javascripts/people.js @@ -26,7 +26,7 @@ request.fail(function (jqXHR, textStatus, errorThrown){ if(jqXHR.status != 404) { - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); } }); } diff --git a/public/javascripts/previewers/sectionRubberband.js b/public/javascripts/previewers/sectionRubberband.js index fb5402538..442cd91d1 100644 --- a/public/javascripts/previewers/sectionRubberband.js +++ b/public/javascripts/previewers/sectionRubberband.js @@ -350,7 +350,7 @@ function rubberbandAddText(tag, comment, sectionid, prNum) { request.done(function (response, textStatus, jqXHR) { }); request.fail(function (jqXHR, textStatus, errorThrown) { - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); window.jsRoutes.api.Sections.delete(sectionid).ajax({ type: "DELETE", contentType: "application/json" diff --git a/public/javascripts/previewers/x3dom/some-library.js b/public/javascripts/previewers/x3dom/some-library.js index 42bf6b71d..38eefc137 100644 --- a/public/javascripts/previewers/x3dom/some-library.js +++ b/public/javascripts/previewers/x3dom/some-library.js @@ -169,7 +169,7 @@ function clearConfigTabAnnotations(prNum){ request.fail(function (jqXHR, textStatus, errorThrown){ console.error( - "The following error occured: "+ + "The following error occurred: "+ textStatus, errorThrown ); notify("ERROR: " + errorThrown +". Annotation edit not submitted.", "error"); @@ -301,7 +301,7 @@ function clearConfigTabAnnotations(prNum){ request.fail(function (jqXHR, textStatus, errorThrown){ console.error( - "The following error occured: "+ + "The following error occurred: "+ textStatus, errorThrown ); notify("ERROR: " + errorThrown +". Annotation not submitted.", "error"); diff --git a/public/javascripts/repositories.js b/public/javascripts/repositories.js index 3fee59c13..751ec6ef0 100644 --- a/public/javascripts/repositories.js +++ b/public/javascripts/repositories.js @@ -24,7 +24,7 @@ function expandRepositories() { request.fail(function(jqXHR, textStatus, errorThrown) { if (jqXHR.status != 404) { - console.error("The following error occured: " + textStatus, + console.error("The following error occurred: " + textStatus, errorThrown); } }); diff --git a/public/javascripts/searchUserMetadata.js b/public/javascripts/searchUserMetadata.js index f7884bb15..e8ffde016 100644 --- a/public/javascripts/searchUserMetadata.js +++ b/public/javascripts/searchUserMetadata.js @@ -536,7 +536,7 @@ $(function() { request.fail(function (jqXHR, textStatus, errorThrown){ console.error( - "The following error occured: "+ + "The following error occurred: "+ textStatus, errorThrown ); notify("ERROR: " + errorThrown +". Search not executed.", "error"); diff --git a/public/javascripts/sectionsListProcess.js b/public/javascripts/sectionsListProcess.js index 522ef77af..96d252367 100644 --- a/public/javascripts/sectionsListProcess.js +++ b/public/javascripts/sectionsListProcess.js @@ -19,7 +19,7 @@ $('#sectiondescrEdit_'+id).css("display","inline"); }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); var errMsg = "You must be logged in to submit a section description."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { alert("The section description was not posted due to : " + errorThrown); diff --git a/public/javascripts/sensors/assign.js b/public/javascripts/sensors/assign.js index 44208be0c..3b43f3673 100644 --- a/public/javascripts/sensors/assign.js +++ b/public/javascripts/sensors/assign.js @@ -11,7 +11,7 @@ function associateWithSensor(resource_type, item_id, dashboard_url) { }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); }); return false; @@ -64,7 +64,7 @@ function associateItemWithSensor(resource_type, item_id, sensor_id, sensor_name, }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); }); return false; diff --git a/public/javascripts/sensors/removeRelation.js b/public/javascripts/sensors/removeRelation.js index d282d8427..1b1fc0b28 100644 --- a/public/javascripts/sensors/removeRelation.js +++ b/public/javascripts/sensors/removeRelation.js @@ -12,7 +12,7 @@ function removeRelation(relation_id) { }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); if (errorThrown == 'Unauthorized') { window.alert("You are not authorized to remove the relationship."); } diff --git a/public/javascripts/spaceModify.js b/public/javascripts/spaceModify.js index 36c02a91f..3cead258a 100644 --- a/public/javascripts/spaceModify.js +++ b/public/javascripts/spaceModify.js @@ -44,7 +44,7 @@ function addCollectionToSpace(id,spaceTitle) { }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to add a collection"; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The collection was not added due to the following : " + errorThrown, "error"); @@ -68,7 +68,7 @@ function removeCollectionFromSpace(spaceId, id, event){ }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to remove a collection."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The collection was not removed due to : " + errorThrown, "error"); @@ -101,7 +101,7 @@ function removeCollectionFromSpaceAndRedirect(spaceId, collectionId, isreload, u }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to remove a collection."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The collection was not removed due to : " + errorThrown, "error"); @@ -149,7 +149,7 @@ function addDatasetToSpace(id, spaceTitle) { }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to add a dataset."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The dataset was not added due to the following : " + errorThrown, "error"); @@ -191,7 +191,7 @@ function removeDatasetFromSpace(spaceId, datasetId, event){ }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to remove a dataset."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The dataset was not removed due to : " + errorThrown, "error"); @@ -229,7 +229,7 @@ function removeDatasetFromSpaceAndRedirect(spaceId, datasetId, isreload, url){ }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to remove a dataset."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The dataset was not removed due to : " + errorThrown, "error"); @@ -309,7 +309,7 @@ function acceptSpaceRequest(spaceId, userId, userName){ console.log("Successful accept request"); }); request.fail(function(jqXHR, textStatus, errorThrown) { - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to accept request."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("Accept request failed due to : " + errorThrown, "error"); @@ -330,7 +330,7 @@ function rejectSpaceRequest(id, user){ console.log("Successful reject request"); }); request.fail(function(jqXHR, textStatus, errorThrown) { - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to reject request."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("Reject request failed due to : " + errorThrown, "error"); diff --git a/public/javascripts/spaceconfiguration.js b/public/javascripts/spaceconfiguration.js index 621b383e8..05a76250e 100644 --- a/public/javascripts/spaceconfiguration.js +++ b/public/javascripts/spaceconfiguration.js @@ -58,7 +58,7 @@ function updateSpaceConfig(spaceId) { }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+textStatus, errorThrown); + console.error("The following error occurred: "+textStatus, errorThrown); var errMsg = "You must be logged in to update the information about a space."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { alert("The space information was not updated due to : " + errorThrown); diff --git a/public/javascripts/updateLicenseInfo.js b/public/javascripts/updateLicenseInfo.js index b32bfeea8..be2131031 100644 --- a/public/javascripts/updateLicenseInfo.js +++ b/public/javascripts/updateLicenseInfo.js @@ -185,7 +185,7 @@ function updateData(id, imageBase, sourceObject, authorName) { request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: " + textStatus, errorThrown); + console.error("The following error occurred: " + textStatus, errorThrown); var errMsg = "You must be logged in to edit license information."; if (!checkErrorAndRedirect(jqXHR, errMsg)) { notify("The license information was not modified due to : " + errorThrown, "error"); diff --git a/public/javascripts/userMetadata.js b/public/javascripts/userMetadata.js index d3b17e9ee..d0a526f8e 100644 --- a/public/javascripts/userMetadata.js +++ b/public/javascripts/userMetadata.js @@ -189,7 +189,7 @@ }); request.fail(function (jqXHR, textStatus, errorThrown){ - console.error("The following error occured: "+ textStatus, errorThrown); + console.error("The following error occurred: "+ textStatus, errorThrown); notify("Encountered error " + errorThrown + ". Metadata not added.", true) }); From c0d846a3d9233841bcb0d040003416783a98ee93 Mon Sep 17 00:00:00 2001 From: Michael-D-Johnson Date: Fri, 17 Sep 2021 10:04:04 -0500 Subject: [PATCH 28/38] Update extractor docs (#255) * adding additional extractor documentation * fixing anchors * fixing some links * fixing code blocks * fixing code block * fixing code block * fixing code-blocks * fixing code-blocks * formatting python examples * formatting python examples * formatting python examples * fixing anchors * fixing anchors * fixing anchors * updating extractor pngs replacing images to use ncsa csvheader extractor * updating image removing secret key * replacing csv extractor URL * Fixed images in extractors.rst. Co-authored-by: Michael Johnson Co-authored-by: Max Burnette Co-authored-by: Luigi Marini Co-authored-by: Luigi Marini --- doc/src/sphinx/_static/ug_extractors-1.png | Bin 0 -> 36400 bytes doc/src/sphinx/_static/ug_extractors-2.png | Bin 0 -> 64646 bytes doc/src/sphinx/_static/ug_extractors-3.png | Bin 0 -> 14734 bytes doc/src/sphinx/_static/ug_extractors-4.png | Bin 0 -> 112080 bytes doc/src/sphinx/develop/extractors.rst | 273 ++++++++++++++++++++- 5 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 doc/src/sphinx/_static/ug_extractors-1.png create mode 100644 doc/src/sphinx/_static/ug_extractors-2.png create mode 100644 doc/src/sphinx/_static/ug_extractors-3.png create mode 100644 doc/src/sphinx/_static/ug_extractors-4.png diff --git a/doc/src/sphinx/_static/ug_extractors-1.png b/doc/src/sphinx/_static/ug_extractors-1.png new file mode 100644 index 0000000000000000000000000000000000000000..e817d2e269ad4d21a099cb28e7ac28ff0e2a858a GIT binary patch literal 36400 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 literal 0 HcmV?d00001 diff --git a/doc/src/sphinx/_static/ug_extractors-2.png b/doc/src/sphinx/_static/ug_extractors-2.png new file mode 100644 index 0000000000000000000000000000000000000000..79de6802d004d601bfcfa6302c929173a4cf705d GIT binary patch literal 64646 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 W7@zFStOVq}JSj0b(Xvkl{{I7?X~B*F literal 0 HcmV?d00001 diff --git a/doc/src/sphinx/_static/ug_extractors-4.png b/doc/src/sphinx/_static/ug_extractors-4.png new file mode 100644 index 0000000000000000000000000000000000000000..7b70afd787e51dbfa1b204b70d0e54b07f0a8258 GIT binary patch literal 112080 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 + @@ -80,4 +80,4 @@

} } }); - \ No newline at end of file + From 1ce57e4e1763838fe199a1cd780b856228d551ee Mon Sep 17 00:00:00 2001 From: Todd Nicholson <40038535+tcnichol@users.noreply.github.com> Date: Mon, 4 Oct 2021 15:19:35 -0500 Subject: [PATCH 35/38] 199 show space bytes (#263) * adding parameter spaceBytes inside getSpace * add method getBytesPerSpace changing html * adding to space statistics need cleanup * statistics changed - replaced with more data modeled on the index page We now see Collections, Datasets, Files, Bytes per space and Users in right column * changelog * adding spaceBytes parameter, does not work yet * new method, get bytes for dataset * methods need to match * adding and removing datasets from space will increment spaceBytes. * increment bytes when file added to dataset * decrement space bytes on remove file * remove commented out code * use cached value of bytes per space not method call * adding update space bytes method * added space bytes update * fixig test * fix few issues with migration - no datasets in space would result in not writing out count - very large databsae would have cursor timeout - simplified logic to count bytes * Space layout with statistics (#283) * make space layout more like datasets/files * update changelog - undo removal - add comment + link to issue for space stats - comment about new layout * Lowered margin from 20px to 15px for button links. Co-authored-by: Luigi Marini Co-authored-by: Luigi Marini Co-authored-by: Max Burnette Co-authored-by: Rob Kooper Co-authored-by: Luigi Marini --- CHANGELOG.md | 4 +- app/api/Files.scala | 4 + app/api/Spaces.scala | 2 +- app/controllers/SecuredController.scala | 7 +- app/controllers/Spaces.scala | 43 +++-- app/models/Space.scala | 4 +- app/services/DatasetService.scala | 2 + app/services/SpaceService.scala | 4 + .../mongodb/MongoDBDatasetService.scala | 11 ++ .../mongodb/MongoDBSpaceService.scala | 12 ++ app/services/mongodb/MongoSalatPlugin.scala | 29 ++- app/util/FileUtils.scala | 18 +- app/views/spaces/otherActions.scala.html | 75 +++++--- app/views/spaces/space.scala.html | 174 ++++++++++-------- app/views/spaces/statistics.scala.html | 35 +++- public/stylesheets/main.css | 2 +- .../integration/spaces/SpaceMongoDBSpec.scala | 1 + 17 files changed, 271 insertions(+), 156 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e4c2f9c6..fa3955f0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added - Adding mime type for geojson - Add "when" parameter in a few GET API endpoints to enable pagination [#266](https://github.com/clowder-framework/clowder/issues/266) +- Show statistics of spaces (bytes, users. etc) [#119](https://github.com/clowder-framework/clowder/issues/119) + +### Changed - Add "id" in GET metadata.jsonld endpoints [#278](https://github.com/clowder-framework/clowder/issues/278) ## 1.18.1 - 2021-08-16 @@ -71,7 +74,6 @@ If any files are returned, you should check to see if these files affected and a ### Changed - Updated Sphinx dependencies due to security and changes in required packages. - - Updated the three.js libraries for the FBX previewer ## 1.16.0 - 2021-03-31 diff --git a/app/api/Files.scala b/app/api/Files.scala index f19084dd9..7f141c4ec 100644 --- a/app/api/Files.scala +++ b/app/api/Files.scala @@ -1675,6 +1675,10 @@ class Files @Inject()( // notify rabbitmq datasets.findByFileIdAllContain(file.id).foreach { ds => routing.fileRemovedFromDataset(file, ds, Utils.baseUrl(request), request.apiKey) + val ds_spaces = ds.spaces + for (ds_s <- ds_spaces) { + spaces.decrementSpaceBytes(ds_s, file.length) + } } //this stmt has to be before files.removeFile diff --git a/app/api/Spaces.scala b/app/api/Spaces.scala index 7d762485f..d82ba2c5e 100644 --- a/app/api/Spaces.scala +++ b/app/api/Spaces.scala @@ -41,7 +41,7 @@ class Spaces @Inject()(spaces: SpaceService, val userId = request.user.get.id val c = ProjectSpace(name = name, description = description, created = new Date(), creator = userId, homePage = List.empty, logoURL = None, bannerURL = None, collectionCount = 0, - datasetCount = 0, userCount = 0, metadata = List.empty) + datasetCount = 0, userCount = 0, spaceBytes = 0, metadata = List.empty) spaces.insert(c) match { case Some(id) => { appConfig.incrementCount('spaces, 1) diff --git a/app/controllers/SecuredController.scala b/app/controllers/SecuredController.scala index be5035931..0f527f3db 100644 --- a/app/controllers/SecuredController.scala +++ b/app/controllers/SecuredController.scala @@ -2,14 +2,11 @@ package controllers import api.Permission.Permission import api.{Permission, UserRequest} -import models.{ClowderUser, RequestResource, ResourceRef, User, UserStatus} -import org.apache.commons.lang.StringEscapeUtils._ +import models.{ClowderUser, ResourceRef, User, UserStatus} import play.api.i18n.Messages import play.api.mvc._ import securesocial.core.{Authenticator, SecureSocial, UserService} import services._ -import securesocial.core.IdentityProvider -import securesocial.core.providers.utils.RoutesHelper import scala.concurrent.Future @@ -156,7 +153,7 @@ trait SecuredController extends Controller { val spaces: SpaceService = DI.injector.getInstance(classOf[SpaceService]) spaces.get(id) match { case None => Future.successful(BadRequest(views.html.notFound(spaceTitle + " does not exist.")(user))) - case Some(space) => Future.successful(Forbidden(views.html.spaces.space(space,List(),List(),List(),List(),"", Map(),List())(user))) + case Some(space) => Future.successful(Forbidden(views.html.spaces.space(space,List(),List(),List(),List(),"", Map(),List(),0,0)(user))) } } diff --git a/app/controllers/Spaces.scala b/app/controllers/Spaces.scala index f95d5b07b..ec0ae5d2b 100644 --- a/app/controllers/Spaces.scala +++ b/app/controllers/Spaces.scala @@ -1,31 +1,22 @@ package controllers -import java.net.URL -import java.util.{ Calendar, Date } -import javax.inject.Inject - import api.Permission import api.Permission._ import models._ -import play.api.{ Logger, Play } +import org.joda.time.DateTime import play.api.data.Forms._ -import play.api.data.{ Form, Forms } -import play.api.libs.json.JsValue -import play.api.libs.json.Json +import play.api.data.{Form, Forms} import play.api.i18n.Messages +import play.api.{Logger, Play} +import securesocial.core.providers.{Token, UsernamePasswordProvider} import services._ -import securesocial.core.providers.{ Token, UsernamePasswordProvider } -import org.joda.time.DateTime -import play.api.i18n.Messages -import play.api.libs.ws._ -import services.AppConfiguration -import util.{ Formatters, Mail, Publications } +import util.{Formatters, Mail, Publications} +import java.net.URL +import java.util.{Calendar, Date} +import javax.inject.Inject import scala.collection.immutable.List -import scala.collection.mutable.{ ArrayBuffer, ListBuffer } -import scala.concurrent.{ Future, Await } -import scala.concurrent.duration._ -import org.apache.commons.lang.StringEscapeUtils.escapeJava +import scala.collection.mutable.{ArrayBuffer, ListBuffer} /** * Spaces allow users to partition the data into realms only accessible to users with the right permissions. @@ -176,6 +167,8 @@ class Spaces @Inject() (spaces: SpaceService, users: UserService, events: EventS var creatorActual: User = null val collectionsInSpace = spaces.getCollectionsInSpace(Some(id.stringify), Some(size)) val datasetsInSpace = datasets.listSpace(size, id.toString(), user) + val spaceBytes : Long = s.spaceBytes + val spaceFiles : Integer = getFilesPerSpace(id, user.get) val publicDatasetsInSpace = datasets.listSpaceStatus(size, id.toString(), "publicAll", user) val usersInSpace = spaces.getUsersInSpace(id, None) var curationObjectsInSpace: List[CurationObject] = List() @@ -224,7 +217,7 @@ class Spaces @Inject() (spaces: SpaceService, users: UserService, events: EventS case None => List.empty } sinkService.logSpaceViewEvent(s, user) - Ok(views.html.spaces.space(Utils.decodeSpaceElements(s), collectionsInSpace, publicDatasetsInSpace, datasetsInSpace, rs, play.Play.application().configuration().getString("SEADservices.uri"), userRoleMap, userSelections)) + Ok(views.html.spaces.space(Utils.decodeSpaceElements(s), collectionsInSpace, publicDatasetsInSpace, datasetsInSpace, rs, play.Play.application().configuration().getString("SEADservices.uri"), userRoleMap, userSelections, spaceBytes, spaceFiles)) } case None => BadRequest(views.html.notFound(spaceTitle + " does not exist.")) } @@ -421,7 +414,7 @@ class Spaces @Inject() (spaces: SpaceService, users: UserService, events: EventS val newSpace = ProjectSpace(name = formData.name, description = formData.description, created = new Date, creator = userId, homePage = formData.homePage, logoURL = formData.logoURL, bannerURL = formData.bannerURL, - collectionCount = 0, datasetCount = 0, userCount = 0, metadata = List.empty, + collectionCount = 0, datasetCount = 0, userCount = 0, spaceBytes = 0, metadata = List.empty, resourceTimeToLive = formData.resourceTimeToLive * 60 * 60 * 1000L, isTimeToLiveEnabled = formData.isTimeToLiveEnabled, status = formData.access, affiliatedSpaces = formData.affSpace) @@ -648,4 +641,14 @@ class Spaces @Inject() (spaces: SpaceService, users: UserService, events: EventS } } + private def getFilesPerSpace(spaceId: UUID, user: models.User) : Integer = { + var spaceFiles: Integer = 0 + val allDatasetsInSpace = datasets.listSpace(0, spaceId.toString(), Some(user)) + for (ds <- allDatasetsInSpace) { + val files_in_ds = ds.files.length + spaceFiles += files_in_ds + } + spaceFiles + } + } diff --git a/app/models/Space.scala b/app/models/Space.scala index 0e0df7079..6f2d545e1 100644 --- a/app/models/Space.scala +++ b/app/models/Space.scala @@ -23,6 +23,7 @@ case class ProjectSpace ( collectionCount: Integer, datasetCount: Integer, userCount: Integer, + spaceBytes: Long, metadata: List[Metadata], resourceTimeToLive: Long = SpaceConfig.getTimeToLive(), isTimeToLiveEnabled: Boolean = SpaceConfig.getIsTimeToLiveEnabled(), @@ -52,7 +53,8 @@ case class UserSpace ( bannerURL: Option[URL], collectionCount: Integer, datasetCount: Integer, - userCount: Integer) + userCount: Integer, + spaceBytes: Long) case class SpaceInvite( id: UUID = UUID.generate, diff --git a/app/services/DatasetService.scala b/app/services/DatasetService.scala index d2538b4b1..071a707bf 100644 --- a/app/services/DatasetService.scala +++ b/app/services/DatasetService.scala @@ -295,6 +295,8 @@ trait DatasetService { def findMetadataChangedDatasets(): List[Dataset] + def getBytesForDataset(datasetId: UUID) : Long + /** * Check recursively whether a dataset's user-input metadata match a requested search tree. */ diff --git a/app/services/SpaceService.scala b/app/services/SpaceService.scala index a0cc4b486..3a9f1e60b 100644 --- a/app/services/SpaceService.scala +++ b/app/services/SpaceService.scala @@ -94,6 +94,10 @@ trait SpaceService { def incrementCollectionCounter(collection: UUID, space: UUID, increment: Int) + def incrementSpaceBytes(space: UUID, increment: Long) + + def decrementSpaceBytes(space: UUID, decrement: Long) + def addDataset(dataset: UUID, space: UUID) def removeDataset(dataset:UUID, space: UUID) diff --git a/app/services/mongodb/MongoDBDatasetService.scala b/app/services/mongodb/MongoDBDatasetService.scala index f2e2616c0..dac16f0c1 100644 --- a/app/services/mongodb/MongoDBDatasetService.scala +++ b/app/services/mongodb/MongoDBDatasetService.scala @@ -1068,6 +1068,17 @@ class MongoDBDatasetService @Inject() ( Dataset.find(MongoDBObject("userMetadataWasModified" -> true)).toList } + def getBytesForDataset(datasetId: UUID) : Long = { + val dataset = Dataset.findOneById(new ObjectId(datasetId.stringify)).get + val datasetFiles = dataset.files + var datasetBytes : Long = 0 + datasetFiles.foreach{ f => { + val currentFileBytes = files.get(f).get.length + datasetBytes += currentFileBytes + }} + datasetBytes + } + def removeTag(id: UUID, tagId: UUID) { Logger.debug("Removing tag " + tagId) val result = Dataset.update(MongoDBObject("_id" -> new ObjectId(id.stringify)), $pull("tags" -> MongoDBObject("_id" -> new ObjectId(tagId.stringify))), false, false, WriteConcern.Safe) diff --git a/app/services/mongodb/MongoDBSpaceService.scala b/app/services/mongodb/MongoDBSpaceService.scala index 749d46f72..6f164f8d0 100644 --- a/app/services/mongodb/MongoDBSpaceService.scala +++ b/app/services/mongodb/MongoDBSpaceService.scala @@ -378,6 +378,14 @@ class MongoDBSpaceService @Inject() ( ProjectSpaceDAO.update(MongoDBObject("_id" -> new ObjectId(space.stringify)), $inc("collectionCount" -> -1), upsert=false, multi=false, WriteConcern.Safe) } + def incrementSpaceBytes(space: UUID, increment: Long ): Unit = { + ProjectSpaceDAO.update(MongoDBObject("_id" -> new ObjectId(space.stringify)), $inc("spaceBytes" -> increment), upsert=false, multi=false, WriteConcern.Safe) + } + + def decrementSpaceBytes(space: UUID, decrement: Long): Unit = { + ProjectSpaceDAO.update(MongoDBObject("_id" -> new ObjectId(space.stringify)), $inc("spaceBytes" -> decrement), upsert=false, multi=false, WriteConcern.Safe) + } + def removeCollection(collection:UUID, space:UUID): Unit = { log.debug(s"Space Service - removing $collection from $space") collections.removeFromSpace(collection, space) @@ -390,7 +398,9 @@ class MongoDBSpaceService @Inject() ( */ def addDataset(dataset: UUID, space: UUID): Unit = { log.debug(s"Space Service - Adding $dataset to $space") + val datasetBytes = datasets.getBytesForDataset(dataset) datasets.addToSpace(dataset, space) + ProjectSpaceDAO.update(MongoDBObject("_id" -> new ObjectId(space.stringify)), $inc("spaceBytes" -> datasetBytes), upsert=false, multi=false, WriteConcern.Safe) ProjectSpaceDAO.update(MongoDBObject("_id" -> new ObjectId(space.stringify)), $inc("datasetCount" -> 1), upsert=false, multi=false, WriteConcern.Safe) } @@ -404,6 +414,8 @@ class MongoDBSpaceService @Inject() ( def removeDataset(dataset:UUID, space:UUID): Unit = { log.debug(s"Space Service - removing $dataset from $space") datasets.removeFromSpace(dataset, space) + val datasetBytes = datasets.getBytesForDataset(dataset) + ProjectSpaceDAO.update(MongoDBObject("_id" -> new ObjectId(space.stringify)), $inc("spaceBytes" -> -datasetBytes), upsert=false, multi=false, WriteConcern.Safe) ProjectSpaceDAO.update(MongoDBObject("_id" -> new ObjectId(space.stringify)), $inc("datasetCount" -> -1), upsert=false, multi=false, WriteConcern.Safe) } diff --git a/app/services/mongodb/MongoSalatPlugin.scala b/app/services/mongodb/MongoSalatPlugin.scala index a695cb602..b5ede2229 100644 --- a/app/services/mongodb/MongoSalatPlugin.scala +++ b/app/services/mongodb/MongoSalatPlugin.scala @@ -2,7 +2,6 @@ package services.mongodb import java.net.URL import java.util.{Calendar, Date} - import com.mongodb.{BasicDBObject, CommandFailureException} import com.mongodb.casbah.Imports._ import com.mongodb.casbah.commons.MongoDBObject @@ -15,10 +14,7 @@ import org.bson.BSONException import play.api.libs.json._ import play.api.{Application, Logger, Play, Plugin} import play.api.Play.current -import com.mongodb.casbah.MongoURI -import com.mongodb.casbah.MongoConnection -import com.mongodb.casbah.MongoDB -import com.mongodb.casbah.MongoCollection +import com.mongodb.casbah.{MongoCollection, MongoConnection, MongoDB, MongoURI, commons} import com.mongodb.casbah.gridfs.GridFS import com.mongodb.casbah.Imports.DBObject import org.bson.types.ObjectId @@ -453,6 +449,9 @@ class MongoSalatPlugin(app: Application) extends Plugin { // Updates extractors enabled and disabled in a space updateMongo("update-space-extractors-selection", updateSpaceExtractorsSelection) + + // Adds space bytes to space + updateMongo(updateKey = "update-space-bytes", updateSpaceBytes) } private def updateMongo(updateKey: String, block: () => Unit): Unit = { @@ -514,7 +513,7 @@ class MongoSalatPlugin(app: Application) extends Plugin { val spacename = java.net.InetAddress.getLocalHost.getHostName val newspace = new ProjectSpace(name = spacename, description = "", created = new Date(), creator = UUID("000000000000000000000000"), homePage = List.empty[URL], logoURL = None, bannerURL = None, metadata = List.empty[Metadata], - collectionCount = collections.toInt, datasetCount = datasets.toInt, userCount = users.toInt) + collectionCount = collections.toInt, datasetCount = datasets.toInt, userCount = users.toInt, spaceBytes = 0) ProjectSpaceDAO.save(newspace) val spaceId = new ObjectId(newspace.id.stringify) @@ -1690,4 +1689,22 @@ class MongoSalatPlugin(app: Application) extends Plugin { } print("DONE") } + + private def updateSpaceBytes(): Unit = { + val spaces = collection("spaces.projects").find().toList.foreach{ space => + var currentSpaceBytes: Long = 0 + val spaceId = space.get("_id") + val spaceDatasets = collection("datasets").find(MongoDBObject("spaces" -> spaceId)).toList + spaceDatasets.foreach{ spaceDataset => + val datasetFileIds = spaceDataset.getAsOrElse[MongoDBList]("files", MongoDBList.empty) + datasetFileIds.foreach{ fileId => + collection("uploads").findOne(MongoDBObject("_id" -> fileId)) match { + case Some(file) => currentSpaceBytes += file.get("length").asInstanceOf[Long] + case None => Logger.info(s"Could not find file ${fileId} in space ${spaceId}") + } + } + } + collection("spaces.projects").update(MongoDBObject("_id" -> spaceId), $set("spaceBytes" -> currentSpaceBytes)) + } + } } diff --git a/app/util/FileUtils.scala b/app/util/FileUtils.scala index 26fc7e86d..949fa6dab 100644 --- a/app/util/FileUtils.scala +++ b/app/util/FileUtils.scala @@ -1,15 +1,9 @@ package util -import java.io.{File => JFile} -import java.net.URL -import java.util.Date - -import collection.JavaConversions._ import api.UserRequest import controllers.Utils import fileutils.FilesUtils import models._ -import org.apache.commons.codec.digest.DigestUtils import play.api.Logger import play.api.Play._ import play.api.libs.Files @@ -18,13 +12,14 @@ import play.api.mvc.MultipartFormData import play.libs.Akka import services._ +import java.net.{URL, URLEncoder} +import java.util.Date +import javax.mail.internet.MimeUtility +import scala.collection.JavaConversions._ import scala.collection.mutable import scala.concurrent.{ExecutionContext, Future} import scala.util.Try -import javax.mail.internet.MimeUtility -import java.net.URLEncoder - object FileUtils { val appConfig: AppConfigurationService = DI.injector.getInstance(classOf[AppConfigurationService]) @@ -41,6 +36,7 @@ object FileUtils { lazy val thumbnails : ThumbnailService = DI.injector.getInstance(classOf[ThumbnailService]) lazy val routing : ExtractorRoutingService = DI.injector.getInstance(classOf[ExtractorRoutingService]) lazy val sinkService : EventSinkService = DI.injector.getInstance(classOf[EventSinkService]) + lazy val spaceService : SpaceService = DI.injector.getInstance(classOf[SpaceService]) def getContentType(filename: Option[String], contentType: Option[String]): String = { @@ -596,6 +592,10 @@ object FileUtils { events.addObjectEvent(Some(user), ds.id, ds.name, EventType.ADD_FILE.toString) } datasets.addFile(ds.id, file) + val datasetSpaces = dataset.get.spaces + for (s <- datasetSpaces){ + spaceService.incrementSpaceBytes(s, file.length) + } } } } diff --git a/app/views/spaces/otherActions.scala.html b/app/views/spaces/otherActions.scala.html index 3a79be035..47f4d0c37 100644 --- a/app/views/spaces/otherActions.scala.html +++ b/app/views/spaces/otherActions.scala.html @@ -1,38 +1,61 @@ @(space: ProjectSpace)(implicit user: Option[models.User]) @import api.Permission @import play.api.i18n.Messages -
-
+ + +
+ Extractors - - } else { -
  • + +
  • + } else { + + + +
    + Extractors - - } - @if(play.api.Play.current.plugin[services.StagingAreaPlugin].isDefined && Permission.checkPermission(Permission.EditStagingArea, ResourceRef(ResourceRef.space, space.id))) { -
  • Staging Area
  • - } -
  • @spaces.follow(space)
  • - + +
    + } + @if(play.api.Play.current.plugin[services.StagingAreaPlugin].isDefined && Permission.checkPermission(Permission.EditStagingArea, ResourceRef(ResourceRef.space, space.id))) { + + } +
    + @spaces.follow(space) +
    diff --git a/app/views/spaces/space.scala.html b/app/views/spaces/space.scala.html index 419a443f6..28ca667b2 100644 --- a/app/views/spaces/space.scala.html +++ b/app/views/spaces/space.scala.html @@ -1,4 +1,4 @@ -@(space: ProjectSpace, collections: List[Collection], publicDatasets: List[Dataset], datasets: List[Dataset], publishedData: List[play.api.libs.json.JsValue], servicesUrl: String, userRoleMap: Map[User, String], userSelections: List[String])(implicit user: Option[models.User]) +@(space: ProjectSpace, collections: List[Collection], publicDatasets: List[Dataset], datasets: List[Dataset], publishedData: List[play.api.libs.json.JsValue], servicesUrl: String, userRoleMap: Map[User, String], userSelections: List[String], spaceBytes: Long, spaceFiles: Integer)(implicit user: Option[models.User]) @import play.api.libs.json._ @import play.api.Play.current @@ -19,110 +19,122 @@ } -
    - -
    -

    @space.name

    - -

    @Html(space.description.replace("\n","
    "))

    - @if(user.isDefined) { -
    - @if(Permission.checkPermission(Permission.DeleteSpace, ResourceRef(ResourceRef.space, space.id))){ - - } - @if(Permission.checkPermission(Permission.CreateDataset, ResourceRef(ResourceRef.space, space.id))) { - - @Messages("create.title", Messages("dataset.title")) - } - @if(Permission.checkPermission(Permission.CreateCollection, ResourceRef(ResourceRef.space, space.id))) { - - @Messages("create.title", Messages("collections.title")) - } - - @if(Permission.checkPermission(Permission.ViewSpace, ResourceRef(ResourceRef.space, space.id)) || user.get.identityId.userId.equals(space.creator)){ - - Search - - } - @if(play.Play.application().configuration().getBoolean("enablePublic") && (Permission.checkPermission(Permission.EditSpace, ResourceRef(ResourceRef.space, space.id)) || Permission.checkPermission(Permission.DeleteSpace, ResourceRef(ResourceRef.space, space.id)))){ -
    - } - } -
    -
    + + @* left column, space details, datasets, collections *@
    - -
    - @if(user.isDefined){ -
    - @spaces.datasetsBySpace(datasets, space, None, userSelections) - @spaces.collectionsBySpace(collections, space, None) -
    - -
    -

    The @Messages("space.title") team has made the following @Messages("datasets.title").toLowerCase and @Messages("collections.title").toLowerCase publicly available.

    - @spaces.datasetsBySpace(publicDatasets, space, Some(true), userSelections) - - @if(space.isPublic) { - @spaces.collectionsBySpace(collections, space, Some(true)) - } else { -

    @Messages("collections.title")

    -

    There are no public collections associated with this @Messages("space.title").

    + @if(Permission.checkPermission(Permission.CreateCollection, ResourceRef(ResourceRef.space, space.id))) { + + @Messages("create.title", Messages("collections.title")) + } + + @if(Permission.checkPermission(Permission.ViewSpace, ResourceRef(ResourceRef.space, space.id)) || user.get.identityId.userId.equals(space.creator)){ + + Search + + } + @if(play.Play.application().configuration().getBoolean("enablePublic") && (Permission.checkPermission(Permission.EditSpace, ResourceRef(ResourceRef.space, space.id)) || Permission.checkPermission(Permission.DeleteSpace, ResourceRef(ResourceRef.space, space.id)))){ +
    }
    - } else { -
    -

    The @Messages("space.title") team has made the following @Messages("datasets.title").toLowerCase and @Messages("collections.title").toLowerCase publicly available. - You must be a logged-in member of the @Messages("space.title") to access all the @Messages("datasets.title").toLowerCase and @Messages("collections.title").toLowerCase. -

    - @spaces.datasetsBySpace(publicDatasets, space, Some(true), userSelections) - @if(space.isPublic) { - @spaces.collectionsBySpace(collections, space, Some(true)) + } +
    + +
    + +
    + @if(user.isDefined){ +
    + @spaces.datasetsBySpace(datasets, space, None, userSelections) + @spaces.collectionsBySpace(collections, space, None) +
    + +
    +

    The @Messages("space.title") team has made the following @Messages("datasets.title").toLowerCase and @Messages("collections.title").toLowerCase publicly available.

    + @spaces.datasetsBySpace(publicDatasets, space, Some(true), userSelections) + + @if(space.isPublic) { + @spaces.collectionsBySpace(collections, space, Some(true)) + } else { +

    @Messages("collections.title")

    +

    There are no public collections associated with this @Messages("space.title").

    + } +
    + } else { +
    +

    The @Messages("space.title") team has made the following @Messages("datasets.title").toLowerCase and @Messages("collections.title").toLowerCase publicly available. + You must be a logged-in member of the @Messages("space.title") to access all the @Messages("datasets.title").toLowerCase and @Messages("collections.title").toLowerCase. +

    + @spaces.datasetsBySpace(publicDatasets, space, Some(true), userSelections) + @if(space.isPublic) { + @spaces.collectionsBySpace(collections, space, Some(true)) + } else { +

    @Messages("collections.title")

    +

    There are no public collections associated with this @Messages("space.title").

    + } +
    + } +
    +

    The following @Messages("datasets.title").toLowerCase have been published through this @Messages("space.title") and any affiliated @Messages("space.title")s.

    + @curations.publishedGrid(publishedData, servicesUrl, None)
    - } -
    -

    The following @Messages("datasets.title").toLowerCase have been published through this @Messages("space.title") and any affiliated @Messages("space.title")s.

    - @curations.publishedGrid(publishedData, servicesUrl, None)
    + + @* right column, space statistics, actions *@
    @if(user.isDefined) { + @spaces.statistics(space, spaceBytes, None, spaceFiles ) @spaces.otherActions(space) } else { - @spaces.statistics(space, "row ds-section-sm space-col-right") + @spaces.statistics(space, spaceBytes, None, spaceFiles) } - @spaces.externalLinks(space.homePage, space, "row ds-section-sm break-word space-col-right") + @spaces.externalLinks(space.homePage, space, "row ds-section-sm") @if(play.Play.application().configuration().getBoolean("enablePublic")) { - @spaces.access(space, userRoleMap, "row ds-section-sm break-word space-col-right") + @spaces.access(space, userRoleMap, "row ds-section-sm") } @if(play.api.Play.configuration.getBoolean("enable_expiration").getOrElse(false)) { @spaces.spaceConfiguration(space) }
    + +