diff --git a/config.example.edn b/config.example.edn index b09ea8329..d69737579 100644 --- a/config.example.edn +++ b/config.example.edn @@ -7,12 +7,16 @@ :triangulum.server/mode "dev" :triangulum.server/log-dir "logs" - :triangulum.server/handler collect-earth-online.routing/authenticated-routing-handler + :triangulum.server/handler triangulum.handler/authenticated-routing-handler :triangulum.server/keystore-file "keystore.pkcs12" :triangulum.server/keystore-type "pkcs12" :triangulum.server/keystore-password "foobar" ;; handler (server) + :triangulum.handler/not-found-handler triangulum.views/not-found-page + :triangulum.handler/redirect-handler collect-earth-online.handlers/redirect-handler + :triangulum.handler/route-authenticator collect-earth-online.handlers/route-authenticator + :triangulum.handler/routing-tables [collect-earth-online.routing/routes] :triangulum.handler/session-key "changeme12345678" ; must be 16 characters :triangulum.handler/bad-tokens #{".php"} :triangulum.handler/private-request-keys #{:base64Image :plotFileBase64 :sampleFileBase64} diff --git a/deps.edn b/deps.edn index f31083d69..3d5650efc 100644 --- a/deps.edn +++ b/deps.edn @@ -7,7 +7,7 @@ org.clojure/data.json {:mvn/version "1.0.0"} ring/ring {:mvn/version "1.8.2"} sig-gis/triangulum {:git/url "https://github.com/sig-gis/triangulum" - :git/sha "5cffefc2e8a8027a178d7ff1103cb2e38e174bba"}} + :git/sha "3d41dab63e1bc8ebe046f64db44ae3df986f5bdf"}} :aliases {:build-db {:main-opts ["-m" "triangulum.build-db"]} :config {:main-opts ["-m" "triangulum.config"]} diff --git a/src/clj/collect_earth_online/db/doi.clj b/src/clj/collect_earth_online/db/doi.clj index bc5a38dfc..7fdf05b3c 100644 --- a/src/clj/collect_earth_online/db/doi.clj +++ b/src/clj/collect_earth_online/db/doi.clj @@ -25,7 +25,7 @@ (defn get-doi-reference [{:keys [params]}] (let [project-id (tc/val->int (:projectId params)) - doi-path (:doi_path (first (call-sql "select_doi_by_project" project-id)))] + doi-path (:doi_path (last (call-sql "select_doi_by_project" project-id)))] (data-response {:doiPath doi-path}))) (defn create-contributors-list @@ -110,59 +110,68 @@ (update :survey_questions #(tc/jsonb->clj %)) (update :aoi_features #(tc/jsonb->clj %)) (update :created_date #(str %)) + (update :closed_date (fn [x] (when x (str x)))) (update :published_date #(str %))))) (defn upload-deposition-files! - [bucket-url project-id zip-file] + [doi-id zip-file] (let [headers (req-headers)] - (http/put (str bucket-url "/" project-id) - {:content-type :multipart/form-data - :headers headers - :as :json - :multipart [{:name "Content/type" :content "application/octet-stream"} - {:name "file" :content (io/file zip-file)}]}))) + (http/post (str base-url "/deposit/depositions/" doi-id "/files") + {:headers headers + :multipart [{:name "Content/type" :content "application/octet-stream"} + {:name "file" :content (io/file zip-file)}]}))) (defn upload-doi-files! [doi-id project-id] (let [project-id project-id doi (first (call-sql "select_doi_by_id" doi-id)) - bucket (-> doi :full_data tc/jsonb->clj :links :bucket) project-data (json/write-str (get-project-data project-id)) zip-file (create-and-zip-files-for-doi project-id project-data)] (try - (:body (upload-deposition-files! bucket project-id zip-file)) - (catch Exception _ + (:body (upload-deposition-files! (:doi_uid doi) zip-file)) + (catch Exception e (throw (ex-info "Failed to upload files." {:details "Error in file upload to zenodo"})))))) (defn create-doi! [{:keys [params session]}] - (let [user-id (:userId session -1) - project-id (:projectId params) - project-name (:projectName params) - institution-name (:name (first (call-sql "select_institution_by_id" (-> params :institution) user-id))) - description (:description params) - creator (first (call-sql "get_user_by_id" user-id)) - contributors (call-sql "select_assigned_users_by_project" project-id)] - (try - (-> - (create-zenodo-deposition! institution-name project-name creator contributors description) - :body - (insert-doi! project-id user-id) - (upload-doi-files! project-id)) - (data-response {:message "DOI created successfully"}) - (catch Exception _ - (data-response {:message "Failed to create DOI."} - {:status 500}))))) + (let [user-id (:userId session -1) + project-id (:projectId params) + project-name (:projectName params) + institution-name (:name (first (call-sql "select_institution_by_id" (-> params :institution) user-id))) + description (:description params) + creator (first (call-sql "get_user_by_id" user-id)) + contributors (call-sql "select_assigned_users_by_project" project-id) + project-published? (:availability (first (call-sql "select_project_by_id" project-id))) + doi-published? (:submitted (last (call-sql "select_doi_by_project" project-id)))] + (cond + (and (not= "published" project-published?) + (not= "closed" project-published?)) (data-response {:message "In order to create a DOI, the project must be published"} + {:status 500}) + doi-published? (data-response {:message "A DOI for this project has already been published."} + {:status 500}) + :else + (try + (-> + (create-zenodo-deposition! institution-name project-name creator contributors description) + :body + (insert-doi! project-id user-id) + (upload-doi-files! project-id)) + (data-response {:message "DOI created successfully"}) + (catch Exception _ + (data-response {:message "Failed to create DOI."} + {:status 500})))))) (defn publish-doi! "request zenodo to publish the DOI on DataCite" [{:keys [params]}] (let [project-id (:projectId params) - doi-id (:doi_uid (first (call-sql "select_doi_by_project" project-id)))] + doi-id (:doi_uid (last (call-sql "select_doi_by_project" project-id))) + req (http/post (str base-url "/deposit/depositions/" doi-id "/actions/publish") + {:as :json + :headers (req-headers)})] (try - (http/post (str base-url "/deposition/depositions/" doi-id "/actions/publish") - {:headers (req-headers)}) + (call-sql "update_doi" doi-id (tc/clj->jsonb (:body req))) (data-response {}) (catch Exception _ (data-response {:message "Failed to publish DOI"} diff --git a/src/clj/collect_earth_online/db/plots.clj b/src/clj/collect_earth_online/db/plots.clj index cb5eec7ed..c0e63bc88 100644 --- a/src/clj/collect_earth_online/db/plots.clj +++ b/src/clj/collect_earth_online/db/plots.clj @@ -256,22 +256,23 @@ ;;; (defn add-user-samples [{:keys [params session]}] - (let [project-id (tc/val->int (:projectId params)) - plot-id (tc/val->int (:plotId params)) - session-user-id (:userId session -1) - current-user-id (tc/val->int (:currentUserId params -1)) - review-mode? (and (tc/val->bool (:inReviewMode params)) + (let [project-id (tc/val->int (:projectId params)) + plot-id (tc/val->int (:plotId params)) + session-user-id (:userId session -1) + current-user-id (tc/val->int (:currentUserId params -1)) + review-mode? (and (tc/val->bool (:inReviewMode params)) (pos? current-user-id) (is-proj-admin? session-user-id project-id nil)) - confidence (tc/val->int (:confidence params)) - collection-start (tc/val->long (:collectionStart params)) - user-samples (:userSamples params) - user-images (:userImages params) - new-plot-samples (:newPlotSamples params) - user-id (if review-mode? current-user-id session-user-id) + confidence (tc/val->int (:confidence params)) + confidence-comment (:confidenceComment params) + collection-start (tc/val->long (:collectionStart params)) + user-samples (:userSamples params) + user-images (:userImages params) + new-plot-samples (:newPlotSamples params) + user-id (if review-mode? current-user-id session-user-id) ;; Samples created in the UI have IDs starting with 1. When the new sample is created ;; in Postgres, it gets different ID. The user sample ID needs to be updated to match. - id-translation (when new-plot-samples + id-translation (when new-plot-samples (call-sql "delete_user_plot_by_plot" plot-id user-id) (call-sql "delete_samples_by_plot" plot-id) (reduce (fn [acc {:keys [id visibleId sampleGeom]}] @@ -288,6 +289,7 @@ plot-id user-id (when (pos? confidence) confidence) + (when confidence-comment confidence-comment) (when-not review-mode? (Timestamp. collection-start)) (tc/clj->jsonb (set/rename-keys user-samples id-translation)) (tc/clj->jsonb (set/rename-keys user-images id-translation))) diff --git a/src/clj/collect_earth_online/generators/external_file.clj b/src/clj/collect_earth_online/generators/external_file.clj index a64bf6843..c223e04eb 100644 --- a/src/clj/collect_earth_online/generators/external_file.clj +++ b/src/clj/collect_earth_online/generators/external_file.clj @@ -277,7 +277,7 @@ (create-shape-files folder-name "sample" project-id) (create-data-file folder-name project-data) (sh-wrapper tmp-dir {} - (str "7z a " folder-name "/files" ".zip " folder-name "/*")) + (str "7z a " folder-name "files" ".zip " folder-name "*")) (str folder-name "files.zip"))) (defn zip-shape-files diff --git a/src/clj/collect_earth_online/handlers.clj b/src/clj/collect_earth_online/handlers.clj new file mode 100644 index 000000000..33c7f4005 --- /dev/null +++ b/src/clj/collect_earth_online/handlers.clj @@ -0,0 +1,33 @@ +(ns collect-earth-online.handlers + (:require [collect-earth-online.db.institutions :refer [is-inst-admin?]] + [collect-earth-online.db.projects :refer [can-collect? is-proj-admin?]] + [ring.util.codec :refer [url-encode]] + [ring.util.response :refer [redirect]] + [triangulum.response :refer [no-cross-traffic?]] + [triangulum.type-conversion :refer [val->int]])) + +(defn route-authenticator [{:keys [session params headers] :as _request} auth-type] + (let [user-id (:userId session -1) + institution-id (val->int (:institutionId params)) + project-id (val->int (:projectId params)) + token-key (:tokenKey params)] + (condp = auth-type + :user (pos? user-id) + :super (= 1 user-id) + :collect (can-collect? user-id project-id token-key) + :token (can-collect? -99 project-id token-key) + :admin (cond + (pos? project-id) (is-proj-admin? user-id project-id token-key) + (pos? institution-id) (is-inst-admin? user-id institution-id)) + :no-cross (no-cross-traffic? headers) + true))) + +(defn redirect-handler [{:keys [session query-string uri] :as _request}] + (let [full-url (url-encode (str uri (when query-string (str "?" query-string))))] + (if (:userId session) + (redirect (str "/home?flash_message=You do not have permission to access " + full-url)) + (redirect (str "/login?returnurl=" + full-url + "&flash_message=You must login to see " + full-url))))) diff --git a/src/clj/collect_earth_online/routing.clj b/src/clj/collect_earth_online/routing.clj index d9b09147a..9128517e5 100644 --- a/src/clj/collect_earth_online/routing.clj +++ b/src/clj/collect_earth_online/routing.clj @@ -1,17 +1,13 @@ (ns collect-earth-online.routing - (:require [triangulum.views :refer [render-page not-found-page]] - [ring.util.response :refer [redirect]] - [ring.util.codec :refer [url-encode]] - [triangulum.type-conversion :as tc] - [triangulum.response :refer [forbidden-response no-cross-traffic?]] - [collect-earth-online.db.doi :as doi] + (:require [collect-earth-online.db.doi :as doi] [collect-earth-online.db.geodash :as geodash] [collect-earth-online.db.imagery :as imagery] [collect-earth-online.db.institutions :as institutions] [collect-earth-online.db.plots :as plots] [collect-earth-online.db.projects :as projects] [collect-earth-online.db.users :as users] - [collect-earth-online.proxy :as proxy])) + [collect-earth-online.proxy :as proxy] + [triangulum.views :refer [render-page]])) (def routes {;; Page Routes @@ -194,41 +190,3 @@ [:get "/get-nicfi-tiles"] {:handler proxy/get-nicfi-tiles :auth-type :no-cross :auth-action :block}}) - -(defn- redirect-auth [user-id] - (fn [request] - (let [{:keys [query-string uri]} request - full-url (url-encode (str uri (when query-string (str "?" query-string))))] - (if (pos? user-id) - (redirect (str "/home?flash_message=You do not have permission to access " - full-url)) - (redirect (str "/login?returnurl=" - full-url - "&flash_message=You must login to see " - full-url)))))) - -(defn authenticated-routing-handler [{:keys [uri request-method params headers session] :as request}] - (let [{:keys [auth-type auth-action handler] :as route} (get routes [request-method uri]) - user-id (:userId session -1) - institution-id (tc/val->int (:institutionId params)) - project-id (tc/val->int (:projectId params)) - next-handler (if route - (if (condp = auth-type - :user (pos? user-id) - :super (= 1 user-id) - :collect (projects/can-collect? user-id project-id (:tokenKey params)) - :token (projects/can-collect? -99 project-id (:tokenKey params)) - :admin (cond - (pos? project-id) - (projects/is-proj-admin? user-id project-id (:tokenKey params)) - - (pos? institution-id) - (institutions/is-inst-admin? user-id institution-id)) - :no-cross (no-cross-traffic? headers) - true) - handler - (if (= :redirect auth-action) - (redirect-auth user-id) - forbidden-response)) - not-found-page)] - (next-handler request))) diff --git a/src/clj/collect_earth_online/workers.clj b/src/clj/collect_earth_online/workers.clj index aa6a8e601..ba2143704 100644 --- a/src/clj/collect_earth_online/workers.clj +++ b/src/clj/collect_earth_online/workers.clj @@ -19,7 +19,7 @@ file (reverse (file-seq d))] (io/delete-file file)))) -(defn- start-clean-up-service! [] +(defn start-clean-up-service! [] (log-str "Starting temp file removal service.") (future (while true diff --git a/src/css/custom.css b/src/css/custom.css index 14414bc46..8365183d3 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -541,6 +541,7 @@ a:hover { } #lPanel ul { padding-left: 0; + margin-left: 5px; list-style: none; margin-bottom: 0; } @@ -859,5 +860,7 @@ input:checked + .switch-slider:before { } body { - padding-top:60px; + margin-left: 10px; + margin-top: 60px; + overflow-x: hidden; } diff --git a/src/js/collection.jsx b/src/js/collection.jsx index b01d29985..a7f6e8923 100644 --- a/src/js/collection.jsx +++ b/src/js/collection.jsx @@ -734,6 +734,9 @@ class Collection extends React.Component { confidence: this.state.currentProject.projectOptions.collectConfidence ? this.state.currentPlot.confidence : -1, + confidenceComment: this.state.currentProject.projectOptions.collectConfidence + ? this.state.currentPlot.confidenceComment + : null, collectionStart: this.state.collectionStart, userSamples: this.state.userSamples, userImages: this.state.userImages, @@ -970,6 +973,9 @@ class Collection extends React.Component { setFlaggedReason = (flaggedReason) => this.setState({ currentPlot: { ...this.state.currentPlot, flaggedReason } }); + setConfidenceComment = (confidenceComment) => + this.setState({ currentPlot: { ...this.state.currentPlot, confidenceComment } }); + render() { return (
@@ -1014,6 +1020,7 @@ class Collection extends React.Component { surveyQuestions={this.state.currentProject.surveyQuestions} toggleQuitModal={this.toggleQuitModal} userName={this.props.userName} + collectConfidence={this.state.currentProject.projectOptions?.collectConfidence} > { - const { answerMode, currentPlot, inReviewMode, surveyQuestions } = this.props; + const { answerMode, currentPlot, inReviewMode, surveyQuestions, collectConfidence } = this.props; + const { confidence, confidenceComment } = currentPlot; const visibleSurveyQuestions = filterObject(surveyQuestions, ([_id, val]) => val.hideQuestion != true); const noneAnswered = everyObject(visibleSurveyQuestions, ([_id, sq]) => safeLength(sq.answered) === 0); const hasSamples = safeLength(currentPlot.samples) > 0; @@ -1154,6 +1164,9 @@ class SideBar extends React.Component { } else if (!allAnswered) { alert("All questions must be answered to save the collection."); return false; + } else if (!(collectConfidence && (confidence && confidenceComment))) { + alert("You must input a confidence and write a comment about it before saving the interpretation."); + return false; } else { return true; } @@ -1585,9 +1598,15 @@ class ImageryOptions extends React.Component { super(props); this.state = { showImageryOptions: true, + enableGrid: false, }; } + enableGrid() { + this.setState({ enableGrid: !this.state.enableGrid }); + return mercator.addGridLayer(this.props.mapConfig, !this.state.enableGrid); + } + render() { const { props } = this; const commonProps = { @@ -1658,6 +1677,16 @@ class ImageryOptions extends React.Component { }[imagery.sourceConfig.type] ); })} + this.enableGrid()} + type="checkbox" + /> +
); diff --git a/src/js/geodash/MapWidget.jsx b/src/js/geodash/MapWidget.jsx index 4eb501dce..0697c74c5 100644 --- a/src/js/geodash/MapWidget.jsx +++ b/src/js/geodash/MapWidget.jsx @@ -160,7 +160,9 @@ export default class MapWidget extends React.Component { this.props.imageryList[0]; const basemapLayer = new TileLayer({ source: mercator.createSource( - sourceConfig, + (widget.basemapType === "PlanetNICFI") ? + {... sourceConfig, time: widget.basemapNICFIDate} + : sourceConfig, id, attribution, isProxied, diff --git a/src/js/geodash/TimeSeriesDesigner.jsx b/src/js/geodash/TimeSeriesDesigner.jsx index c26e22537..d50b0a187 100644 --- a/src/js/geodash/TimeSeriesDesigner.jsx +++ b/src/js/geodash/TimeSeriesDesigner.jsx @@ -24,7 +24,6 @@ export async function getBandsFromGateway(setBands, assetId, assetType) { } ); const data = await res.json(); - console.log("data is: ", data); if (data.hasOwnProperty("bands")) { setBands(data.bands); } else if (data.hasOwnProperty("errMsg")) { diff --git a/src/js/geodash/form/BasemapSelector.jsx b/src/js/geodash/form/BasemapSelector.jsx index 74a2accc2..bae824c50 100644 --- a/src/js/geodash/form/BasemapSelector.jsx +++ b/src/js/geodash/form/BasemapSelector.jsx @@ -1,11 +1,68 @@ -import React, { useContext } from "react"; +import React, { useContext, useState, useEffect } from "react"; import { EditorContext } from "../constants"; import SvgIcon from "../../components/svg/SvgIcon"; + export default function BasemapSelector() { - const { setWidgetDesign, getWidgetDesign, imagery, getInstitutionImagery, institutionId } = - useContext(EditorContext); + const { widget, widgetDesign, setWidgetDesign, getWidgetDesign, imagery, getInstitutionImagery, institutionId } = + useContext(EditorContext); + const [nicfiLayers, setNICFILayers] = useState([]); + const [imageryType, setImageryType] = useState(""); + const [basemap, setBasemap] = useState(imagery.filter((i)=>i.id === getWidgetDesign("basemapId"))); + + function nicfiDateSelector(){ + const options = ( + nicfiLayers || []).map( + (l)=>{ + const name = l.slice(34, l.length - 7); + return ( + );} + ); + return ( +
+ + +
); + } + + const getNICFILayers = () => { + fetch("/get-nicfi-dates") + .then((response) => (response.ok ? response.json() : Promise.reject(response))) + .then((layers) => { + setNICFILayers(layers); + setWidgetDesign("basemapNICFIDate", widget[0].basemapNICFIDate); + }) + .catch((error) => console.error(error)); + }; + + useEffect(()=> { + setWidgetDesign("basemapType", imageryType); + (imageryType === "PlanetNICFI") ? + getNICFILayers() + : setWidgetDesign("basemapNICFIDate", null); + }, [imageryType]); + + useEffect(()=>{ + setImageryType((imagery || []).filter((i)=> i.id === getWidgetDesign("basemapId"))[0].sourceConfig.type); + setBasemap((imagery || []).filter((i)=> i.id === getWidgetDesign("basemapId"))[0]); + }, [imagery]); + return (
@@ -22,16 +79,20 @@ export default function BasemapSelector() { + {(imageryType === "PlanetNICFI") && nicfiDateSelector()}
Adding imagery to basemaps is available on the  { + const { nicfiLayers, selectedTime } = state; + const index = nicfiLayers.indexOf(selectedTime); + + return { + isDisabledLeft: index >= nicfiLayers.length - 1, + isDisabledRight: index <= 0 + } + }) + } } getNICFILayers = () => { @@ -231,6 +246,21 @@ export class PlanetNICFIMenu extends React.Component { } }; + switchImagery = (direction) => { + this.setState((prevState) => { + const { nicfiLayers, selectedTime } = prevState; + const currentIndex = nicfiLayers.indexOf(selectedTime); + + const move = direction === "forward" ? -1 : 1; + const newIndex = currentIndex + move; + + // Check boundary conditions + if (newIndex < 0 || newIndex >= nicfiLayers.length) return {}; + + return { selectedTime: nicfiLayers[newIndex] }; + }); + } + updatePlanetLayer = () => { this.updateImageryInformation(); mercator.updateLayerSource( @@ -250,14 +280,17 @@ export class PlanetNICFIMenu extends React.Component { return ( nicfiLayers.length > 0 && (
-
-
-
- -
) ); diff --git a/src/js/imagery/imageryOptions.js b/src/js/imagery/imageryOptions.js index 1d1a45017..c55f12862 100644 --- a/src/js/imagery/imageryOptions.js +++ b/src/js/imagery/imageryOptions.js @@ -253,18 +253,18 @@ export const imageryOptions = [ { key: "min", display: "Min", - type: "number", + type: "text", options: { - placeholder: "1-100", + placeholder: "[1-100] | [0.1,0.2,0.1]", step: "0.01", }, }, { key: "max", display: "Max", - type: "number", + type: "text", options: { - placeholder: "2800-3200", + placeholder: "[2800-3200] | [0.3,0.5,0.3]", step: "0.01", }, }, @@ -309,18 +309,18 @@ export const imageryOptions = [ { key: "min", display: "Min", - type: "number", + type: "text", options: { - placeholder: "1-100", + placeholder: "[1-100] | [0.1,0.2,0.1]", step: "0.01", }, }, { key: "max", display: "Max", - type: "number", + type: "text", options: { - placeholder: "2800-3200", + placeholder: "[2800-3200] | [0.3,0.5,0.3]", step: "0.01", }, }, diff --git a/src/js/project/PlotDesign.jsx b/src/js/project/PlotDesign.jsx index ade920e2c..bcbc1aacb 100644 --- a/src/js/project/PlotDesign.jsx +++ b/src/js/project/PlotDesign.jsx @@ -430,6 +430,7 @@ export class PlotDesign extends React.Component { }, shp: { display: "SHP File", + alert: "CEO may overestimate the number of project plots when using a ShapeFile.", description: "Specify your own plot boundaries by uploading a zipped Shapefile (containing SHP, SHX, DBF, and PRJ files) of polygon features. Each feature must have a unique PLOTID value.", layout: this.renderFileInput("shp"), @@ -456,6 +457,8 @@ export class PlotDesign extends React.Component {

{`- ${plotOptions[plotDistribution].description}`}

+ {plotOptions[plotDistribution].alert && +

- {plotOptions[plotDistribution].alert}

}
{plotOptions[plotDistribution].layout}

+
+ +