From 7186a0e784d69c9a2e2a967e1d2b8d51a1e8d61b Mon Sep 17 00:00:00 2001 From: Jon Van Oast Date: Thu, 7 Nov 2024 13:26:47 -0700 Subject: [PATCH 01/25] utility for get-or-create a user --- src/main/java/org/ecocean/Shepherd.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/org/ecocean/Shepherd.java b/src/main/java/org/ecocean/Shepherd.java index a8bb11e276..266a41a121 100644 --- a/src/main/java/org/ecocean/Shepherd.java +++ b/src/main/java/org/ecocean/Shepherd.java @@ -2683,6 +2683,17 @@ public User getUserByHashedEmailAddress(String hashedEmail) { return null; } + // note: if existing user is found *and* fullName is set, user will get updated with new name! + // (this is to replicate legacy behavior of creating users during encounter submission) + public User getOrCreateUserByEmailAddress(String email, String fullName) { + if (Util.stringIsEmptyOrNull(email)) return null; + User user = getUserByEmailAddress(email); + if (user == null) user = new User(email, Util.generateUUID()); + if (!Util.stringIsEmptyOrNull(fullName)) user.setFullName(fullName); + getPM().makePersistent(user); + return user; + } + public List getUsersWithEmailAddresses() { ArrayList users = new ArrayList(); String filter = "SELECT FROM org.ecocean.User WHERE emailAddress != null"; From 9ef3363cf72eac95079ba887ec493b9ab06a713e Mon Sep 17 00:00:00 2001 From: Jon Van Oast Date: Thu, 7 Nov 2024 13:27:32 -0700 Subject: [PATCH 02/25] gotta throw Shepherd in here of course --- src/main/java/org/ecocean/Base.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/ecocean/Base.java b/src/main/java/org/ecocean/Base.java index 9b6c4fb7ce..3af6492495 100644 --- a/src/main/java/org/ecocean/Base.java +++ b/src/main/java/org/ecocean/Base.java @@ -22,7 +22,6 @@ */ @JsonSerialize(using = BaseSerializer.class) @JsonDeserialize(using = BaseDeserializer.class) public abstract class Base { - /** * Retrieves Id, such as: * @@ -191,12 +190,14 @@ public static Map getAllVersions(Shepherd myShepherd, String sql) return rtn; } - public static Base createFromApi(JSONObject payload, List files) throws ApiException { + public static Base createFromApi(JSONObject payload, List files, Shepherd myShepherd) + throws ApiException { throw new ApiException("not yet supported"); } // TODO should this be an abstract? will we need some base stuff? - public static Object validateFieldValue(String fieldName, JSONObject data) throws ApiException { + public static Object validateFieldValue(String fieldName, JSONObject data) + throws ApiException { return null; } From 2b9a4e8a21d770a6accc69c6f7b44ab319d1c3ef Mon Sep 17 00:00:00 2001 From: Jon Van Oast Date: Thu, 7 Nov 2024 13:29:29 -0700 Subject: [PATCH 03/25] wip: more fields --- src/main/java/org/ecocean/Encounter.java | 59 +++++++++++++++---- src/main/java/org/ecocean/api/BaseObject.java | 47 +++++++-------- 2 files changed, 72 insertions(+), 34 deletions(-) diff --git a/src/main/java/org/ecocean/Encounter.java b/src/main/java/org/ecocean/Encounter.java index 921733ebea..5bbc68e0b5 100644 --- a/src/main/java/org/ecocean/Encounter.java +++ b/src/main/java/org/ecocean/Encounter.java @@ -4657,7 +4657,8 @@ public static int[] opensearchSyncIndex(Shepherd myShepherd, int stopAfter) return rtn; } - public static Base createFromApi(org.json.JSONObject payload, List files) + public static Base createFromApi(org.json.JSONObject payload, List files, + Shepherd myShepherd) throws ApiException { if (payload == null) throw new ApiException("empty payload"); User user = (User)payload.opt("_currentUser"); @@ -4666,23 +4667,52 @@ public static Base createFromApi(org.json.JSONObject payload, List files) String locationID = (String)validateFieldValue("locationId", payload); String dateTime = (String)validateFieldValue("dateTime", payload); String txStr = (String)validateFieldValue("taxonomy", payload); - + String submitterEmail = (String)validateFieldValue("submitterEmail", payload); + String photographerEmail = (String)validateFieldValue("photographerEmail", payload); + String additionalEmailsValue = payload.optString("additionalEmails", null); + String[] additionalEmails = null; + if (!Util.stringIsEmptyOrNull(additionalEmailsValue)) + additionalEmails = additionalEmailsValue.split("[,\\s]+"); + if (additionalEmails != null) { + org.json.JSONObject error = new org.json.JSONObject(); + error.put("fieldName", "additionalEmails"); + for (String email : additionalEmails) { + if (!Util.isValidEmailAddress(email)) { + error.put("code", ApiException.ERROR_RETURN_CODE_INVALID); + error.put("value", email); + throw new ApiException("invalid email address", error); + } + } + } Encounter enc = new Encounter(false); - if (Util.isUUID(payload.optString("_id"))) enc.setId(payload.getString("_id")); enc.setLocationID(locationID); enc.setDateFromISO8601String(dateTime); enc.setTaxonomyFromString(txStr); enc.setComments(payload.optString("comments", null)); - if (user == null) { enc.setSubmitterID("public"); // this seems to be what EncounterForm servlet does so... } else { enc.setSubmitterID(user.getUsername()); enc.addSubmitter(user); } - - // FIXME apply values etc set owner etc + if (!Util.stringIsEmptyOrNull(submitterEmail)) { + User submitterUser = myShepherd.getOrCreateUserByEmailAddress(submitterEmail, + payload.optString("submitterName", null)); + // set this after the owner-submitter being set + enc.addSubmitter(submitterUser); + } + if (!Util.stringIsEmptyOrNull(photographerEmail)) { + User photographerUser = myShepherd.getOrCreateUserByEmailAddress(photographerEmail, + payload.optString("photographerName", null)); + enc.addPhotographer(photographerUser); + } + if (additionalEmails != null) { + for (String email : additionalEmails) { + User addlUser = myShepherd.getOrCreateUserByEmailAddress(email, null); + enc.addInformOther(addlUser); + } + } return enc; } @@ -4693,9 +4723,7 @@ public static Object validateFieldValue(String fieldName, org.json.JSONObject da error.put("fieldName", fieldName); String exMessage = "invalid value for " + fieldName; Object returnValue = null; - switch (fieldName) { - case "locationId": returnValue = data.optString(fieldName, null); if (returnValue == null) { @@ -4733,12 +4761,21 @@ public static Object validateFieldValue(String fieldName, org.json.JSONObject da // FIXME validate taxonomy break; + case "photographerEmail": + case "submitterEmail": + returnValue = data.optString(fieldName, null); + if ((returnValue != null) && !Util.isValidEmailAddress((String)returnValue)) { + error.put("code", ApiException.ERROR_RETURN_CODE_INVALID); + error.put("value", returnValue); + throw new ApiException(exMessage, error); + } + break; + default: - System.out.println("Encounter.validateFieldValue(): WARNING unsupported fieldName=" + fieldName); + System.out.println("Encounter.validateFieldValue(): WARNING unsupported fieldName=" + + fieldName); } - // must be okay! return returnValue; } - } diff --git a/src/main/java/org/ecocean/api/BaseObject.java b/src/main/java/org/ecocean/api/BaseObject.java index 76c095a64d..63ebeb0d54 100644 --- a/src/main/java/org/ecocean/api/BaseObject.java +++ b/src/main/java/org/ecocean/api/BaseObject.java @@ -7,10 +7,10 @@ import javax.servlet.ServletException; import java.io.File; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.ArrayList; import org.joda.time.DateTime; import org.json.JSONArray; import org.json.JSONObject; @@ -44,6 +44,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String uri = request.getRequestURI(); String[] args = uri.substring(8).split("/"); + if (args.length < 1) throw new ServletException("Bad path"); // System.out.println("args => " + java.util.Arrays.toString(args)); @@ -52,18 +53,16 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) if (requestMethod.equals("POST")) { payload = ServletUtilities.jsonFromHttpServletRequest(request); } - if (!ReCAPTCHA.sessionIsHuman(request)) { response.setStatus(401); response.setHeader("Content-Type", "application/json"); response.getWriter().write("{\"success\": false}"); return; } - /* if (!(args[0].equals("encounters") || args[0].equals("individuals") || args[0].equals("occurrences"))) throw new ServletException("Bad class"); -*/ + */ payload.put("_class", args[0]); JSONObject rtn = null; @@ -97,7 +96,6 @@ protected JSONObject processPost(HttpServletRequest request, String[] args, JSON // for background child assets, which has to be after all persisted List maIds = new ArrayList(); - Base obj = null; try { String cls = payload.optString("_class"); @@ -126,12 +124,12 @@ protected JSONObject processPost(HttpServletRequest request, String[] args, JSON error.put("code", ApiException.ERROR_RETURN_CODE_REQUIRED); throw new ApiException("anonymous submission requires valid files", error); } - - obj = Encounter.createFromApi(payload, files); + // might be better to create encounter first; in case it fails. + // or should failing file/MA above happen first and block encounter? + obj = Encounter.createFromApi(payload, files, myShepherd); Encounter enc = (Encounter)obj; myShepherd.getPM().makePersistent(enc); String txStr = enc.getTaxonomyString(); - JSONArray assetsArr = new JSONArray(); ArrayList anns = new ArrayList(); for (MediaAsset ma : validMAs) { @@ -153,10 +151,10 @@ protected JSONObject processPost(HttpServletRequest request, String[] args, JSON rtn.put("statusCode", 200); break; case "occurrences": - obj = Occurrence.createFromApi(payload, files); + obj = Occurrence.createFromApi(payload, files, myShepherd); break; case "individuals": - obj = MarkedIndividual.createFromApi(payload, files); + obj = MarkedIndividual.createFromApi(payload, files, myShepherd); break; default: throw new ApiException("bad class"); @@ -164,18 +162,18 @@ protected JSONObject processPost(HttpServletRequest request, String[] args, JSON rtn.put("id", obj.getId()); rtn.put("class", cls); rtn.put("success", true); - } catch (ApiException apiEx) { - System.out.println("BaseObject.processPost() returning 400 due to " + apiEx + " [errors=" + apiEx.getErrors() + "] on payload " + payload); + System.out.println("BaseObject.processPost() returning 400 due to " + apiEx + + " [errors=" + apiEx.getErrors() + "] on payload " + payload); rtn.put("statusCode", 400); rtn.put("errors", apiEx.getErrors()); } - if ((obj != null) && (rtn.optInt("statusCode", 0) == 200)) { - System.out.println("BaseObject.processPost() success (200) creating " + obj + " from payload " + payload); + System.out.println("BaseObject.processPost() success (200) creating " + obj + + " from payload " + payload); myShepherd.commitDBTransaction(); MediaAsset.updateStandardChildrenBackground(context, maIds); -//FIXME kick off detection etc +// FIXME kick off detection etc } else { myShepherd.rollbackDBTransaction(); } @@ -186,29 +184,30 @@ protected JSONObject processPost(HttpServletRequest request, String[] args, JSON protected JSONObject processGet(HttpServletRequest request, String[] args) throws ServletException, IOException { JSONObject rtn = new JSONObject(); + return rtn; } private List findFiles(HttpServletRequest request, JSONObject payload) throws IOException { List files = new ArrayList(); + if (payload == null) return files; String submissionId = payload.optString("submissionId", null); if (!Util.isUUID(submissionId)) { System.out.println("WARNING: valid submissionId required; no files possible"); return files; } - Map values = new HashMap(); values.put("submissionId", submissionId); File uploadDir = new File(UploadServlet.getUploadDir(request, values)); System.out.println("findFiles() uploadDir=" + uploadDir); - if (!uploadDir.exists()) throw new IOException("uploadDir for submissionId=" + submissionId + " does not exist"); - + if (!uploadDir.exists()) + throw new IOException("uploadDir for submissionId=" + submissionId + " does not exist"); List filenames = new ArrayList(); JSONArray fnArr = payload.optJSONArray("assetFilenames"); if (fnArr != null) { - for (int i = 0 ; i < fnArr.length() ; i++) { + for (int i = 0; i < fnArr.length(); i++) { String fn = fnArr.optString(i, null); if (fn != null) filenames.add(fn); } @@ -217,7 +216,7 @@ private List findFiles(HttpServletRequest request, JSONObject payload) for (File f : uploadDir.listFiles()) { filenames.add(f.getName()); } -*/ + */ } if (filenames.size() < 1) return files; for (String fname : filenames) { @@ -229,10 +228,12 @@ private List findFiles(HttpServletRequest request, JSONObject payload) return files; } - private Map makeMediaAssets(String encounterId, List files, Shepherd myShepherd) + private Map makeMediaAssets(String encounterId, List files, + Shepherd myShepherd) throws ApiException { Map results = new HashMap(); AssetStore astore = AssetStore.getDefault(myShepherd); + for (File file : files) { if (!AssetStore.isValidImage(file)) { System.out.println("BaseObject.makeMediaAssets() failed isValidImage() on " + file); @@ -240,7 +241,8 @@ private Map makeMediaAssets(String encounterId, List fil continue; } String sanitizedItemName = ServletUtilities.cleanFileName(file.getName()); - JSONObject sp = astore.createParameters(new File(Encounter.subdir(encounterId) + File.separator + sanitizedItemName)); + JSONObject sp = astore.createParameters(new File(Encounter.subdir(encounterId) + + File.separator + sanitizedItemName)); sp.put("userFilename", file.getName()); System.out.println("makeMediaAssets(): file=" + file + " => " + sp); MediaAsset ma = new MediaAsset(astore, sp); @@ -258,5 +260,4 @@ private Map makeMediaAssets(String encounterId, List fil } return results; } - } From 63fe22a2e4f1be83469103841c9586e3534fbc60 Mon Sep 17 00:00:00 2001 From: Jon Van Oast Date: Thu, 7 Nov 2024 14:31:12 -0700 Subject: [PATCH 04/25] shocking we never had this --- src/main/java/org/ecocean/Shepherd.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/ecocean/Shepherd.java b/src/main/java/org/ecocean/Shepherd.java index 266a41a121..503ed1d1d1 100644 --- a/src/main/java/org/ecocean/Shepherd.java +++ b/src/main/java/org/ecocean/Shepherd.java @@ -2126,6 +2126,8 @@ public int getNumTaxonomies() { return (Util.count(taxis)); } + // tragically this mixes Taxonomy (class, via db) with commonConfiguration-based values. SIGH + // TODO when property files go away (yay) this should become just db public List getAllTaxonomyNames() { Iterator allTaxonomies = getAllTaxonomies(); Set allNames = new HashSet(); @@ -2140,8 +2142,11 @@ public List getAllTaxonomyNames() { List allNamesList = new ArrayList(allNames); java.util.Collections.sort(allNamesList); - // return (allNamesList); - return (configNames); + return allNamesList; + } + + public boolean isValidTaxonomyName(String sciName) { + return getAllTaxonomyNames().contains(sciName); } public Iterator getAllSurveysNoQuery() { From 6c608ae19f51d39eacc841cdee9e89b2b3039c10 Mon Sep 17 00:00:00 2001 From: Jon Van Oast Date: Thu, 7 Nov 2024 14:31:36 -0700 Subject: [PATCH 05/25] validate taxonomy --- src/main/java/org/ecocean/Encounter.java | 12 +++++++++++- src/main/java/org/ecocean/api/BaseObject.java | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/ecocean/Encounter.java b/src/main/java/org/ecocean/Encounter.java index 5bbc68e0b5..34a7397ed3 100644 --- a/src/main/java/org/ecocean/Encounter.java +++ b/src/main/java/org/ecocean/Encounter.java @@ -4758,7 +4758,17 @@ public static Object validateFieldValue(String fieldName, org.json.JSONObject da error.put("code", ApiException.ERROR_RETURN_CODE_REQUIRED); throw new ApiException(exMessage, error); } - // FIXME validate taxonomy + // this is throwaway read-only shepherd + Shepherd myShepherd = new Shepherd("context0"); + myShepherd.setAction("Encounter.validateFieldValue"); + myShepherd.beginDBTransaction(); + boolean validTaxonomy = myShepherd.isValidTaxonomyName((String)returnValue); + myShepherd.rollbackDBTransaction(); + if (!validTaxonomy) { + error.put("code", ApiException.ERROR_RETURN_CODE_INVALID); + error.put("value", returnValue); + throw new ApiException(exMessage, error); + } break; case "photographerEmail": diff --git a/src/main/java/org/ecocean/api/BaseObject.java b/src/main/java/org/ecocean/api/BaseObject.java index 63ebeb0d54..da82e66b6f 100644 --- a/src/main/java/org/ecocean/api/BaseObject.java +++ b/src/main/java/org/ecocean/api/BaseObject.java @@ -167,6 +167,7 @@ protected JSONObject processPost(HttpServletRequest request, String[] args, JSON " [errors=" + apiEx.getErrors() + "] on payload " + payload); rtn.put("statusCode", 400); rtn.put("errors", apiEx.getErrors()); + rtn.put("debug", apiEx.toString()); } if ((obj != null) && (rtn.optInt("statusCode", 0) == 200)) { System.out.println("BaseObject.processPost() success (200) creating " + obj + From c7d8effb20e20c5faa3f4dff35788fa262fdd6a2 Mon Sep 17 00:00:00 2001 From: erinz2020 Date: Thu, 7 Nov 2024 23:48:31 +0000 Subject: [PATCH 06/25] add submission failed modal --- .../ReportsAndManagamentPages/LocationID.jsx | 1 + .../ReportEncounter.jsx | 46 +++++++- .../ReportEncounterStore.js | 104 +++++++++++------- 3 files changed, 110 insertions(+), 41 deletions(-) diff --git a/frontend/src/pages/ReportsAndManagamentPages/LocationID.jsx b/frontend/src/pages/ReportsAndManagamentPages/LocationID.jsx index 68dfd8a8d2..23f8597e9b 100644 --- a/frontend/src/pages/ReportsAndManagamentPages/LocationID.jsx +++ b/frontend/src/pages/ReportsAndManagamentPages/LocationID.jsx @@ -115,6 +115,7 @@ export const LocationID = observer( treeCheckStrictly treeNodeFilterProp="title" onChange={(selectedValues) => { + store.setLocationError(selectedValues ? false : true); const singleSelection = selectedValues.length > 0 ? selectedValues[selectedValues.length - 1] diff --git a/frontend/src/pages/ReportsAndManagamentPages/ReportEncounter.jsx b/frontend/src/pages/ReportsAndManagamentPages/ReportEncounter.jsx index 6261599e16..9aa40cd3e0 100644 --- a/frontend/src/pages/ReportsAndManagamentPages/ReportEncounter.jsx +++ b/frontend/src/pages/ReportsAndManagamentPages/ReportEncounter.jsx @@ -1,12 +1,12 @@ import React, { useContext, useState, useRef, useEffect } from "react"; -import { Container, Row, Col, Form, Alert } from "react-bootstrap"; +import { Container, Row, Col, Form, Alert, Modal } from "react-bootstrap"; import ThemeColorContext from "../../ThemeColorProvider"; import MainButton from "../../components/MainButton"; import AuthContext from "../../AuthProvider"; import { FormattedMessage } from "react-intl"; import ImageSection from "./ImageSection"; import { DateTimeSection } from "./DateTimeSection"; -import PlaceSection from "./PlaceSection"; +import { PlaceSection } from "./PlaceSection"; import { AdditionalCommentsSection } from "../../components/AdditionalCommentsSection"; import { FollowUpSection } from "../../components/FollowUpSection"; import { observer, useLocalObservable } from "mobx-react-lite"; @@ -23,6 +23,8 @@ export const ReportEncounter = observer(() => { const { data } = useGetSiteSettings(); const procaptchaSiteKey = data?.procaptchaSiteKey; const store = useLocalObservable(() => new ReportEncounterStore()); + const [missingField, setMissingField] = useState(false); + const [loading, setLoading] = useState(false); store.setImageRequired(!isLoggedIn); @@ -84,16 +86,23 @@ export const ReportEncounter = observer(() => { }, []); const handleSubmit = async () => { + setLoading(true); if (!store.validateFields()) { console.log("Field validation failed."); + store.setShowSubmissionFailedAlert(true); + setMissingField(true); + setLoading(false); return; } else { console.log("Fields validated successfully. Submitting report."); const responseData = await store.submitReport(); + console.log("Response data: ", responseData); if (store.finished && store.success) { + setLoading(false); Navigate("/reportConfirm", { state: { responseData } }); } else if (store.finished && !store.success) { - alert("Report submission failed"); + setLoading(false); + store.setShowSubmissionFailedAlert(true); } } }; @@ -216,6 +225,27 @@ export const ReportEncounter = observer(() => { return ( + store.setShowSubmissionFailedAlert(false)} + keyboard + centered + animation + > + +
+ {missingField && } + submission failed. +
+

@@ -373,6 +403,16 @@ export const ReportEncounter = observer(() => { onClick={handleSubmit} > + {loading && ( +
+ + + +
+ )} diff --git a/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js b/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js index 9f4ee815de..ed6d6d7606 100644 --- a/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js +++ b/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js @@ -18,6 +18,8 @@ export class ReportEncounterStore { _finished; _signInModalShow; _exifDateTime; + _showSubmissionFailedAlert; + _error; constructor() { this._imageSectionSubmissionId = null; @@ -60,6 +62,12 @@ export class ReportEncounterStore { this._finished = false; this._signInModalShow = false; this._exifDateTime = []; + this._showSubmissionFailedAlert = false; + this._error = { + message: "", + status: "", + }; + makeAutoObservable(this); } @@ -124,6 +132,14 @@ export class ReportEncounterStore { return this._exifDateTime; } + get showSubmissionFailedAlert() { + return this._showSubmissionFailedAlert; + } + + get error() { + return this._error; + } + // Actions setImageSectionSubmissionId(value) { this._imageSectionSubmissionId = value; @@ -216,6 +232,10 @@ export class ReportEncounterStore { this._signInModalShow = value; } + setShowSubmissionFailedAlert(value) { + this._showSubmissionFailedAlert = value; + } + validateEmails() { const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; @@ -257,59 +277,67 @@ export class ReportEncounterStore { } if (this._imageRequired && this._imageSectionFileNames.length === 0) { + console.log("1"); this._imageSectionError = true; isValid = false; + } + console.log(isValid); - if (!this._dateTimeSection.value && this._dateTimeSection.required) { - this._dateTimeSection.error = true; - isValid = false; - } - - if (!this._placeSection.locationId && this._placeSection.required) { - this._placeSection.error = true; - isValid = false; - } + if (!this._dateTimeSection.value && this._dateTimeSection.required) { + console.log(JSON.stringify(this._dateTimeSection)); + this._dateTimeSection.error = true; + isValid = false; + } + if (!this._placeSection.locationId && this._placeSection.required) { + console.log("3"); + this._placeSection.error = true; + isValid = false; } console.log("Validation result", isValid); return isValid; } async submitReport() { console.log("submitting"); + this._loading = true; const readyCaseone = this.validateFields() && this._imageSectionFileNames.length > 0; const readyCasetwo = this.validateFields() && !this._imageRequired; + console.log(readyCaseone, readyCasetwo); if (readyCaseone || readyCasetwo) { - const response = await axios.post("/api/v3/encounters", { - submissionId: this._imageSectionSubmissionId, - assetFilenames: this._imageSectionFileNames, - dateTime: this._dateTimeSection.value, - taxonomy: this._speciesSection.value, - locationId: this._placeSection.locationId, - // followUp: this._followUpSection.value, - // images: this._imageSectionFileNames, - }); - - if (response.status === 200) { - console.log("Report submitted successfully.", response); - this._speciesSection.value = ""; - this._placeSection.value = ""; - this._followUpSection.value = ""; - this._dateTimeSection.value = ""; - this._imageSectionFileNames = []; - this._imageSectionSubmissionId = null; - this._imageCount = 0; - this._imageSectionError = false; - this._success = true; - this._finished = true; - this._placeSection.value = ""; - return response.data; - } else { - this._finished = true; - this._success = false; - console.error("Report submission failed"); - } + try { + const response = await axios.post("/api/v3/encounters", { + submissionId: this._imageSectionSubmissionId, + assetFilenames: this._imageSectionFileNames, + dateTime: this._dateTimeSection.value, + taxonomy: this._speciesSection.value, + locationId: this._placeSection.locationId, + // followUp: this._followUpSection.value, + // images: this._imageSectionFileNames, + }); + if (response.status === 200) { + console.log("Report submitted successfully.", response); + this._speciesSection.value = ""; + this._placeSection.value = ""; + this._followUpSection.value = ""; + this._dateTimeSection.value = ""; + this._imageSectionFileNames = []; + this._imageSectionSubmissionId = null; + this._imageCount = 0; + this._imageSectionError = false; + this._success = true; + this._finished = true; + + console.log(this._finished); + return response.data; + } + } catch (error) { + console.error("Error submitting report", error); + this._showSubmissionFailedAlert = true; + this._error.code = error.response.status; + this._error.message = error.response.data.message; + } } else { console.error("Validation failed"); } From fe8164e93e0446f596c9d8da1c0f456fecf2b156 Mon Sep 17 00:00:00 2001 From: erinz2020 Date: Fri, 8 Nov 2024 00:54:04 +0000 Subject: [PATCH 07/25] add submitter photographer etc --- .../pages/ReportsAndManagamentPages/ReportConfirm.jsx | 10 +++++----- .../ReportsAndManagamentPages/ReportEncounterStore.js | 7 +++++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/frontend/src/pages/ReportsAndManagamentPages/ReportConfirm.jsx b/frontend/src/pages/ReportsAndManagamentPages/ReportConfirm.jsx index 788e05a377..62be761b78 100644 --- a/frontend/src/pages/ReportsAndManagamentPages/ReportConfirm.jsx +++ b/frontend/src/pages/ReportsAndManagamentPages/ReportConfirm.jsx @@ -18,7 +18,7 @@ export const ReportConfirm = () => {

- {responseData.invalidFiles?.length > 0 && ( + {responseData?.invalidFiles?.length > 0 && (

@@ -36,7 +36,7 @@ export const ReportConfirm = () => { />

- {responseData.invalidFiles.map((invalidFile, index) => ( + {responseData?.invalidFiles?.map((invalidFile, index) => (

{invalidFile.filename}

@@ -44,7 +44,7 @@ export const ReportConfirm = () => {
)}

- {responseData.id} + {responseData?.id}

{" "} @@ -92,14 +92,14 @@ export const ReportConfirm = () => { noArrow={true} style={{ width: "calc(100% - 20px)", - fontSize: "16px", + fontSize: "1rem", marginTop: "20px", marginBottom: "20px", fontWeight: "normal", }} onClick={() => window.open( - `/encounters/encounter.jsp?number=${responseData.id}`, + `/encounters/encounter.jsp?number=${responseData?.id}`, "_blank", ) } diff --git a/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js b/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js index ed6d6d7606..bbecdb62ea 100644 --- a/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js +++ b/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js @@ -312,8 +312,11 @@ export class ReportEncounterStore { dateTime: this._dateTimeSection.value, taxonomy: this._speciesSection.value, locationId: this._placeSection.locationId, - // followUp: this._followUpSection.value, - // images: this._imageSectionFileNames, + comments: this._additionalCommentsSection.value, + submitterName: this._followUpSection.submitter.name, + submitterEmail: this._followUpSection.submitter.email, + photographerName: this._followUpSection.photographer.name, + photographerEmail: this._followUpSection.photographer.email, }); if (response.status === 200) { From 3420ffa734e9f19ab19d39222a29ae60e886ed34 Mon Sep 17 00:00:00 2001 From: Jon Van Oast Date: Thu, 7 Nov 2024 18:08:02 -0700 Subject: [PATCH 08/25] lat/lon via encounter creation api --- src/main/java/org/ecocean/Encounter.java | 37 ++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/main/java/org/ecocean/Encounter.java b/src/main/java/org/ecocean/Encounter.java index 34a7397ed3..33ab3bafc7 100644 --- a/src/main/java/org/ecocean/Encounter.java +++ b/src/main/java/org/ecocean/Encounter.java @@ -4669,6 +4669,18 @@ public static Base createFromApi(org.json.JSONObject payload, List files, String txStr = (String)validateFieldValue("taxonomy", payload); String submitterEmail = (String)validateFieldValue("submitterEmail", payload); String photographerEmail = (String)validateFieldValue("photographerEmail", payload); + Double decimalLatitude = (Double)validateFieldValue("decimalLatitude", payload); + Double decimalLongitude = (Double)validateFieldValue("decimalLongitude", payload); + if (((decimalLatitude == null) && (decimalLongitude != null)) || + ((decimalLatitude != null) && (decimalLongitude == null))) { + org.json.JSONObject error = new org.json.JSONObject(); + error.put("code", ApiException.ERROR_RETURN_CODE_INVALID); + // i guess we pick one, since both are wrong + error.put("fieldName", "decimalLatitude"); + error.put("value", decimalLatitude); + throw new ApiException("cannot send just one of decimalLatitude and decimalLongitude", + error); + } String additionalEmailsValue = payload.optString("additionalEmails", null); String[] additionalEmails = null; if (!Util.stringIsEmptyOrNull(additionalEmailsValue)) @@ -4687,6 +4699,8 @@ public static Base createFromApi(org.json.JSONObject payload, List files, Encounter enc = new Encounter(false); if (Util.isUUID(payload.optString("_id"))) enc.setId(payload.getString("_id")); enc.setLocationID(locationID); + enc.setDecimalLatitude(decimalLatitude); + enc.setDecimalLongitude(decimalLongitude); enc.setDateFromISO8601String(dateTime); enc.setTaxonomyFromString(txStr); enc.setComments(payload.optString("comments", null)); @@ -4723,6 +4737,7 @@ public static Object validateFieldValue(String fieldName, org.json.JSONObject da error.put("fieldName", fieldName); String exMessage = "invalid value for " + fieldName; Object returnValue = null; + double UNSET_LATLON = 9999.99; switch (fieldName) { case "locationId": returnValue = data.optString(fieldName, null); @@ -4781,6 +4796,28 @@ public static Object validateFieldValue(String fieldName, org.json.JSONObject da } break; + case "decimalLatitude": + returnValue = data.optDouble(fieldName, UNSET_LATLON); + if ((double)returnValue == UNSET_LATLON) { + returnValue = null; + } else if (!Util.isValidDecimalLatitude((double)returnValue)) { + error.put("code", ApiException.ERROR_RETURN_CODE_INVALID); + error.put("value", returnValue); + throw new ApiException(exMessage, error); + } + break; + + case "decimalLongitude": + returnValue = data.optDouble(fieldName, UNSET_LATLON); + if ((double)returnValue == UNSET_LATLON) { + returnValue = null; + } else if (!Util.isValidDecimalLongitude((double)returnValue)) { + error.put("code", ApiException.ERROR_RETURN_CODE_INVALID); + error.put("value", returnValue); + throw new ApiException(exMessage, error); + } + break; + default: System.out.println("Encounter.validateFieldValue(): WARNING unsupported fieldName=" + fieldName); From b5b037f54f4385cc823d671f6eeed9cf3aea57b6 Mon Sep 17 00:00:00 2001 From: Jon Van Oast Date: Thu, 7 Nov 2024 18:37:07 -0700 Subject: [PATCH 09/25] enc.sendToIA() --- src/main/java/org/ecocean/Encounter.java | 33 +++++++++++++++++++ src/main/java/org/ecocean/api/BaseObject.java | 6 +++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/ecocean/Encounter.java b/src/main/java/org/ecocean/Encounter.java index 33ab3bafc7..3e2bffe35c 100644 --- a/src/main/java/org/ecocean/Encounter.java +++ b/src/main/java/org/ecocean/Encounter.java @@ -33,6 +33,7 @@ import org.ecocean.api.ApiException; import org.ecocean.genetics.*; import org.ecocean.ia.IA; +import org.ecocean.ia.Task; import org.ecocean.identity.IBEISIA; import org.ecocean.media.*; import org.ecocean.security.Collaboration; @@ -4825,4 +4826,36 @@ public static Object validateFieldValue(String fieldName, org.json.JSONObject da // must be okay! return returnValue; } + + // basically ripped from servlet/EncounterForm + public Task sendToIA(Shepherd myShepherd) { + Task task = null; + + try { + IAJsonProperties iaConfig = IAJsonProperties.iaConfig(); + if (iaConfig.hasIA(this, myShepherd)) { + for (MediaAsset ma : this.getMedia()) { + ma.setDetectionStatus(IBEISIA.STATUS_INITIATED); + } + Task parentTask = null; // this is *not* persisted, but only used so intakeMediaAssets will inherit its params + if (this.getLocationID() != null) { + parentTask = new Task(); + org.json.JSONObject tp = new org.json.JSONObject(); + org.json.JSONObject mf = new org.json.JSONObject(); + mf.put("locationId", this.getLocationID()); + tp.put("matchingSetFilter", mf); + parentTask.setParameters(tp); + } + task = org.ecocean.ia.IA.intakeMediaAssets(myShepherd, this.getMedia(), parentTask); + myShepherd.storeNewTask(task); + System.out.println("sendToIA() success on " + this + " => " + task); + } else { + System.out.println("sendToIA() skipped; no config for " + this); + } + } catch (Exception ex) { + System.out.println("sendToIA() failed on " + this + ": " + ex); + ex.printStackTrace(); + } + return task; + } } diff --git a/src/main/java/org/ecocean/api/BaseObject.java b/src/main/java/org/ecocean/api/BaseObject.java index da82e66b6f..b7369b8523 100644 --- a/src/main/java/org/ecocean/api/BaseObject.java +++ b/src/main/java/org/ecocean/api/BaseObject.java @@ -85,6 +85,7 @@ protected JSONObject processPost(HttpServletRequest request, String[] args, JSON throws ServletException, IOException { if (payload == null) throw new ServletException("empty payload"); JSONObject rtn = new JSONObject(); + Encounter encounterForIA = null; rtn.put("success", false); List files = findFiles(request, payload); String context = ServletUtilities.getContext(request); @@ -129,6 +130,7 @@ protected JSONObject processPost(HttpServletRequest request, String[] args, JSON obj = Encounter.createFromApi(payload, files, myShepherd); Encounter enc = (Encounter)obj; myShepherd.getPM().makePersistent(enc); + encounterForIA = enc; String txStr = enc.getTaxonomyString(); JSONArray assetsArr = new JSONArray(); ArrayList anns = new ArrayList(); @@ -174,7 +176,9 @@ protected JSONObject processPost(HttpServletRequest request, String[] args, JSON " from payload " + payload); myShepherd.commitDBTransaction(); MediaAsset.updateStandardChildrenBackground(context, maIds); -// FIXME kick off detection etc + if (encounterForIA != null) encounterForIA.sendToIA(myShepherd); + // not sure what this is for, but servlet/EncounterForm did it so guessing its important + org.ecocean.ShepherdPMF.getPMF(context).getDataStoreCache().evictAll(); } else { myShepherd.rollbackDBTransaction(); } From 8656a15a44579d872094fde0183b623db4a0deaf Mon Sep 17 00:00:00 2001 From: Jon Van Oast Date: Thu, 7 Nov 2024 20:15:34 -0700 Subject: [PATCH 10/25] kinda handy --- src/main/java/org/ecocean/Util.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/org/ecocean/Util.java b/src/main/java/org/ecocean/Util.java index a74b2cb9fb..300c114edf 100644 --- a/src/main/java/org/ecocean/Util.java +++ b/src/main/java/org/ecocean/Util.java @@ -826,6 +826,7 @@ public static boolean stringExists(String str) { return (str != null && !str.trim().equals("") && !str.toLowerCase().equals("none") && !str.toLowerCase().equals("unknown")); } + public static boolean stringIsEmptyOrNull(String str) { return ((str == null) || str.equals("")); } @@ -1176,6 +1177,18 @@ public static long getVersionFromModified(final String modified) { } } + // note: this respect user.receiveEmails - you have been warned + public static Set getUserEmailAddresses(List users) { + Set addrs = new HashSet(); + + if (users == null) return addrs; + for (User user : users) { + if (user.getReceiveEmails() && !stringIsEmptyOrNull(user.getEmailAddress())) + addrs.add(user.getEmailAddress()); + } + return addrs; + } + public static String getISO8601Date(final String date) { String iso8601 = date.replace(" ", "T"); From 0ca2b8dd28b16d4c1897455dfae1109b04ee23f6 Mon Sep 17 00:00:00 2001 From: Jon Van Oast Date: Thu, 7 Nov 2024 20:16:37 -0700 Subject: [PATCH 11/25] improvements --- src/main/java/org/ecocean/Shepherd.java | 27 +++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/main/java/org/ecocean/Shepherd.java b/src/main/java/org/ecocean/Shepherd.java index 503ed1d1d1..36d0ee4b9f 100644 --- a/src/main/java/org/ecocean/Shepherd.java +++ b/src/main/java/org/ecocean/Shepherd.java @@ -998,6 +998,8 @@ public ArrayList getAllRolesForUser(String username) { } public boolean doesUserHaveRole(String username, String rolename, String context) { + if (username == null) return false; + if (rolename == null) return false; username = username.replaceAll("\\'", "\\\\'"); rolename = rolename.replaceAll("\\'", "\\\\'"); String filter = "this.username == '" + username + "' && this.rolename == '" + rolename + @@ -2710,6 +2712,18 @@ public List getUsersWithEmailAddresses() { return users; } + // this seems more what we want + public List getUsersWithEmailAddressesWhoReceiveEmails() { + ArrayList users = new ArrayList(); + String filter = "SELECT FROM org.ecocean.User WHERE emailAddress != null && receiveEmails"; + Query query = getPM().newQuery(filter); + Collection c = (Collection)(query.execute()); + + if (c != null) users = new ArrayList(c); + query.closeAll(); + return users; + } + public User getUserByAffiliation(String affil) { String filter = "SELECT FROM org.ecocean.User WHERE affiliation == \"" + affil + "\""; Query query = getPM().newQuery(filter); @@ -4213,6 +4227,19 @@ public String getAllUserEmailAddressesForLocationID(String locationID, String co return addresses; } + // you probably dont like the above one, so: + public Set getAllUserEmailAddressesForLocationIDAsSet(String locationID, + String context) { + Set emails = new HashSet(); + + if (Util.stringIsEmptyOrNull(locationID)) return emails; + for (User user : getUsersWithEmailAddressesWhoReceiveEmails()) { + if (doesUserHaveRole(user.getUsername(), locationID, context)) + emails.add(user.getEmailAddress()); + } + return emails; + } + public Iterator getAllOccurrences() { Extent allOccurs = null; Iterator it = null; From 3eaae092c074865c172aa3ee5e38582fe5d63030 Mon Sep 17 00:00:00 2001 From: Jon Van Oast Date: Thu, 7 Nov 2024 20:17:23 -0700 Subject: [PATCH 12/25] hacky way to get serverUrl without a request object. what can i say? --- .../java/org/ecocean/NotificationMailer.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/ecocean/NotificationMailer.java b/src/main/java/org/ecocean/NotificationMailer.java index ec01fd194b..630c3e15a3 100644 --- a/src/main/java/org/ecocean/NotificationMailer.java +++ b/src/main/java/org/ecocean/NotificationMailer.java @@ -602,6 +602,13 @@ public static Map createBasicTagMap(HttpServletRequest req, Enco return map; } + public static Map createBasicTagMap(Encounter enc) { + Map map = new HashMap<>(); + + addTags(map, null, enc, null); + return map; + } + /** * Creates a basic tag map for the specified encounter. This map can subsequently be enhanced with extra tags. Tags included are the union of * those added by @@ -665,9 +672,14 @@ private static void addTags(Map map, HttpServletRequest req, private static void addTags(Map map, HttpServletRequest req, Encounter enc, String scheme) { Objects.requireNonNull(map); - if (!map.containsKey("@URL_LOCATION@")) - map.put("@URL_LOCATION@", - String.format(scheme + "://%s", CommonConfiguration.getURLLocation(req))); + if (!map.containsKey("@URL_LOCATION@")) { + if (req == null) { + map.put("@URL_LOCATION@", CommonConfiguration.getServerURL("context0")); + } else { + map.put("@URL_LOCATION@", + String.format(scheme + "://%s", CommonConfiguration.getURLLocation(req))); + } + } if (enc != null) { // Add useful encounter fields. map.put("@ENCOUNTER_LINK@", From 013beaee9714f0e538cebdd2aa0a15fd101484fe Mon Sep 17 00:00:00 2001 From: Jon Van Oast Date: Thu, 7 Nov 2024 20:21:19 -0700 Subject: [PATCH 13/25] send out emails (in a MUCH better way than before) upon encounter creation --- src/main/java/org/ecocean/Encounter.java | 65 +++++++++++++++++++ src/main/java/org/ecocean/api/BaseObject.java | 6 +- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/ecocean/Encounter.java b/src/main/java/org/ecocean/Encounter.java index 3e2bffe35c..66277ee798 100644 --- a/src/main/java/org/ecocean/Encounter.java +++ b/src/main/java/org/ecocean/Encounter.java @@ -4,9 +4,11 @@ import java.io.*; import java.lang.Math; +import java.net.URI; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.concurrent.ThreadPoolExecutor; import java.util.Calendar; import java.util.Collection; import java.util.Date; @@ -15,6 +17,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Properties; import java.util.Set; import java.util.SortedMap; import java.util.StringTokenizer; @@ -4858,4 +4861,66 @@ public Task sendToIA(Shepherd myShepherd) { } return task; } + + public Set getNotificationEmailAddresses() { + Set addrs = new HashSet(); + + addrs.addAll(Util.getUserEmailAddresses(this.getSubmitters())); + addrs.addAll(Util.getUserEmailAddresses(this.getPhotographers())); + addrs.addAll(Util.getUserEmailAddresses(this.getInformOthers())); + return addrs; + } + + // FIXME passing the langCode is dumb imho, but this is "standard practice" + // better would be that each recipient user's language preference would be used for their email + public void sendCreationEmails(Shepherd myShepherd, String langCode) { + String context = myShepherd.getContext(); + + if (!CommonConfiguration.sendEmailNotifications(context)) return; + myShepherd.beginDBTransaction(); + try { + URI uri = CommonConfiguration.getServerURI(myShepherd); + if (uri == null) throw new IOException("could not find server uri"); + ThreadPoolExecutor es = MailThreadExecutorService.getExecutorService(); + Properties submitProps = ShepherdProperties.getProperties("submit.properties", langCode, + context); + Map tagMap = NotificationMailer.createBasicTagMap(this); + tagMap.put(NotificationMailer.WILDBOOK_COMMUNITY_URL, + CommonConfiguration.getWildbookCommunityURL(context)); + List mailTo = NotificationMailer.splitEmails( + CommonConfiguration.getNewSubmissionEmail(context)); + String mailSubj = submitProps.getProperty("newEncounter") + this.getCatalogNumber(); + for (String emailTo : mailTo) { + NotificationMailer mailer = new NotificationMailer(context, emailTo, langCode, + "newSubmission-summary", tagMap); + mailer.setUrlScheme(uri.getScheme()); + es.execute(mailer); + } + // this will be empty if no locationID + Set locEmails = myShepherd.getAllUserEmailAddressesForLocationIDAsSet( + this.getLocationID(), context); + for (String emailTo : locEmails) { + NotificationMailer mailer = new NotificationMailer(context, langCode, emailTo, + "newSubmission-summary", tagMap); + mailer.setUrlScheme(uri.getScheme()); + es.execute(mailer); + } + // Add encounter dont-track tag for remaining notifications (still needs email-hash assigned). + tagMap.put(NotificationMailer.EMAIL_NOTRACK, "number=" + this.getCatalogNumber()); + // this is a mashup of: submitters, photographers, informOthers.... + for (String emailTo : this.getNotificationEmailAddresses()) { + tagMap.put(NotificationMailer.EMAIL_HASH_TAG, getHashOfEmailString(emailTo)); + NotificationMailer mailer = new NotificationMailer(context, langCode, emailTo, + "newSubmission", tagMap); + mailer.setUrlScheme(uri.getScheme()); + es.execute(mailer); + } + es.shutdown(); + } catch (Exception ex) { + System.out.println("sendCreationEmails() on " + this + " failed: " + ex); + ex.printStackTrace(); + } finally { + myShepherd.rollbackDBTransaction(); + } + } } diff --git a/src/main/java/org/ecocean/api/BaseObject.java b/src/main/java/org/ecocean/api/BaseObject.java index b7369b8523..30cd991822 100644 --- a/src/main/java/org/ecocean/api/BaseObject.java +++ b/src/main/java/org/ecocean/api/BaseObject.java @@ -94,6 +94,7 @@ protected JSONObject processPost(HttpServletRequest request, String[] args, JSON myShepherd.beginDBTransaction(); User currentUser = myShepherd.getUser(request); payload.put("_currentUser", currentUser); // hacky yes, but works. i am going to allow it. + String langCode = ServletUtilities.getLanguageCode(request); // for background child assets, which has to be after all persisted List maIds = new ArrayList(); @@ -176,7 +177,10 @@ protected JSONObject processPost(HttpServletRequest request, String[] args, JSON " from payload " + payload); myShepherd.commitDBTransaction(); MediaAsset.updateStandardChildrenBackground(context, maIds); - if (encounterForIA != null) encounterForIA.sendToIA(myShepherd); + if (encounterForIA != null) { + encounterForIA.sendToIA(myShepherd); + encounterForIA.sendCreationEmails(myShepherd, langCode); + } // not sure what this is for, but servlet/EncounterForm did it so guessing its important org.ecocean.ShepherdPMF.getPMF(context).getDataStoreCache().evictAll(); } else { From 10f8571d14e9d8354dfb759bcab94e24f945c92b Mon Sep 17 00:00:00 2001 From: erinz2020 Date: Sat, 9 Nov 2024 00:35:54 +0000 Subject: [PATCH 14/25] filter out empty fields before submitting --- frontend/package-lock.json | 5 +-- frontend/package.json | 1 + .../ReportEncounterStore.js | 43 +++++++++++++++---- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1dacc92031..865a9428cc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -22,6 +22,7 @@ "lodash-es": "^4.17.21", "mobx": "6.13.3", "mobx-react-lite": "^4.0.7", + "moment": "^2.30.1", "postcss-cli": "^11.0.0", "rc-slider": "^10.6.2", "react": "^18.2.0", @@ -14082,7 +14083,6 @@ "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "license": "MIT", - "peer": true, "engines": { "node": "*" } @@ -31313,8 +31313,7 @@ "moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "peer": true + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==" }, "mrmime": { "version": "2.0.0", diff --git a/frontend/package.json b/frontend/package.json index cbfd128a56..da0a8ff1cb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "lodash-es": "^4.17.21", "mobx": "6.13.3", "mobx-react-lite": "^4.0.7", + "moment": "^2.30.1", "postcss-cli": "^11.0.0", "rc-slider": "^10.6.2", "react": "^18.2.0", diff --git a/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js b/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js index bbecdb62ea..c4cfcc588d 100644 --- a/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js +++ b/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js @@ -20,6 +20,8 @@ export class ReportEncounterStore { _exifDateTime; _showSubmissionFailedAlert; _error; + _lat; + _lon; constructor() { this._imageSectionSubmissionId = null; @@ -63,10 +65,9 @@ export class ReportEncounterStore { this._signInModalShow = false; this._exifDateTime = []; this._showSubmissionFailedAlert = false; - this._error = { - message: "", - status: "", - }; + this._error = null; + this._lat = null; + this._lon = null; makeAutoObservable(this); } @@ -140,6 +141,14 @@ export class ReportEncounterStore { return this._error; } + get lat() { + return this._lat; + } + + get lon() { + return this._lon; + } + // Actions setImageSectionSubmissionId(value) { this._imageSectionSubmissionId = value; @@ -236,6 +245,14 @@ export class ReportEncounterStore { this._showSubmissionFailedAlert = value; } + setLat(value) { + this._lat = value; + } + + setLon(value) { + this._lon = value; + } + validateEmails() { const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; @@ -306,7 +323,8 @@ export class ReportEncounterStore { console.log(readyCaseone, readyCasetwo); if (readyCaseone || readyCasetwo) { try { - const response = await axios.post("/api/v3/encounters", { + + const payload = { submissionId: this._imageSectionSubmissionId, assetFilenames: this._imageSectionFileNames, dateTime: this._dateTimeSection.value, @@ -317,7 +335,16 @@ export class ReportEncounterStore { submitterEmail: this._followUpSection.submitter.email, photographerName: this._followUpSection.photographer.name, photographerEmail: this._followUpSection.photographer.email, - }); + decimalLatitude: this._lat, + decimalLongitude: this._lon, + }; + + const filteredPayload = Object.fromEntries( + Object.entries(payload).filter(([key, value]) => value !== null && value !== ""), + ); + + console.log("Filtered payload", filteredPayload); + const response = await axios.post("/api/v3/encounters", filteredPayload); if (response.status === 200) { console.log("Report submitted successfully.", response); @@ -337,9 +364,9 @@ export class ReportEncounterStore { } } catch (error) { console.error("Error submitting report", error); + console.log(JSON.stringify(error)); this._showSubmissionFailedAlert = true; - this._error.code = error.response.status; - this._error.message = error.response.data.message; + this._error = error.response.data.errors; } } else { console.error("Validation failed"); From 38fc48442ead4734590bbf209ccbd4b99eb4b1fc Mon Sep 17 00:00:00 2001 From: Jon Van Oast Date: Fri, 8 Nov 2024 14:21:32 -0700 Subject: [PATCH 15/25] squash _ in taxonomy for sake of validating --- src/main/java/org/ecocean/Shepherd.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/ecocean/Shepherd.java b/src/main/java/org/ecocean/Shepherd.java index 36d0ee4b9f..1983f23cde 100644 --- a/src/main/java/org/ecocean/Shepherd.java +++ b/src/main/java/org/ecocean/Shepherd.java @@ -2131,6 +2131,11 @@ public int getNumTaxonomies() { // tragically this mixes Taxonomy (class, via db) with commonConfiguration-based values. SIGH // TODO when property files go away (yay) this should become just db public List getAllTaxonomyNames() { + return getAllTaxonomyNames(false); + } + + // forceSpaces will turn `Foo bar_bar` into `Foo bar bar` - use with caution! + public List getAllTaxonomyNames(boolean forceSpaces) { Iterator allTaxonomies = getAllTaxonomies(); Set allNames = new HashSet(); @@ -2144,11 +2149,18 @@ public List getAllTaxonomyNames() { List allNamesList = new ArrayList(allNames); java.util.Collections.sort(allNamesList); + if (forceSpaces) { + List spacey = new ArrayList(); + for (String tx : allNamesList) { + spacey.add(tx.replaceAll("_", " ")); + } + return spacey; + } return allNamesList; } public boolean isValidTaxonomyName(String sciName) { - return getAllTaxonomyNames().contains(sciName); + return getAllTaxonomyNames(true).contains(sciName.replaceAll("_", " ")); } public Iterator getAllSurveysNoQuery() { From c0d2812bd7aac52ada253675af1f50fe3d625d16 Mon Sep 17 00:00:00 2001 From: Jon Van Oast Date: Fri, 8 Nov 2024 16:27:42 -0700 Subject: [PATCH 16/25] robust dateTime validator/setter --- src/main/java/org/ecocean/Encounter.java | 49 ++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/ecocean/Encounter.java b/src/main/java/org/ecocean/Encounter.java index 66277ee798..c4391b7071 100644 --- a/src/main/java/org/ecocean/Encounter.java +++ b/src/main/java/org/ecocean/Encounter.java @@ -2612,8 +2612,25 @@ public void setDateInMilliseconds(long ms) { this.dateInMilliseconds = ms; } + // also supports YYYY and YYYY-MM public void setDateFromISO8601String(String iso8601) { - if (iso8601 == null) return; + if (!validISO8601String(iso8601)) return; + if (iso8601.length() == 4) { // assume year + try { + this.year = Integer.parseInt(iso8601); + } catch (Exception ex) {} + resetDateInMilliseconds(); + return; + } + // this should already be validated so we can trust it (flw) + if (iso8601.length() == 7) { + try { + this.year = Integer.parseInt(iso8601.substring(0, 4)); + this.month = Integer.parseInt(iso8601.substring(5, 7)); + } catch (Exception ex) {} + resetDateInMilliseconds(); + return; + } try { String adjusted = Util.getISO8601Date(iso8601); DateTime dt = new DateTime(adjusted); @@ -2621,6 +2638,32 @@ public void setDateFromISO8601String(String iso8601) { } catch (Exception ex) { System.out.println("setDateFromISO8601String(" + iso8601 + ") failed: " + ex); } + resetDateInMilliseconds(); + } + + // also supports YYYY and YYYY-MM + public static boolean validISO8601String(String iso8601) { + if (iso8601 == null) return false; + if (iso8601.length() == 4) { + Integer yr = null; + try { + yr = Integer.parseInt(iso8601); + } catch (Exception ex) {} + return (yr != null); + } + if (iso8601.length() == 7) { + Integer yr = null; + Integer mo = null; + try { + yr = Integer.parseInt(iso8601.substring(0, 4)); + mo = Integer.parseInt(iso8601.substring(5, 7)); + } catch (Exception ex) {} + if ((yr == null) || (mo == null)) return false; + if ((mo < 1) || (mo > 12)) return false; + return true; + } + long test = Util.getVersionFromModified(iso8601); + return (test > 0); } public Long getEndDateInMilliseconds() { @@ -4762,9 +4805,7 @@ public static Object validateFieldValue(String fieldName, org.json.JSONObject da error.put("code", ApiException.ERROR_RETURN_CODE_REQUIRED); throw new ApiException(exMessage, error); } - // this is a kind of cheap test of validity of dateTime value; likely we will need to make an improved Util for this - long test = Util.getVersionFromModified((String)returnValue); - if (test < 1) { + if (!validISO8601String((String)returnValue)) { error.put("code", ApiException.ERROR_RETURN_CODE_INVALID); error.put("value", returnValue); throw new ApiException(exMessage, error); From ce44815e547aee56fcdba5297de7756bd2bdfdd1 Mon Sep 17 00:00:00 2001 From: Jon Van Oast Date: Tue, 12 Nov 2024 11:37:12 -0700 Subject: [PATCH 17/25] placeholder captcha properties --- src/main/resources/bundles/captcha.properties | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/main/resources/bundles/captcha.properties diff --git a/src/main/resources/bundles/captcha.properties b/src/main/resources/bundles/captcha.properties new file mode 100644 index 0000000000..0a4e6e154d --- /dev/null +++ b/src/main/resources/bundles/captcha.properties @@ -0,0 +1,3 @@ +procaptchaSiteKey=CHANGEME +procaptchaSecretKey=CHANGEME + From 936fc60d18a5c1c837880a3aa5a775658f8aff04 Mon Sep 17 00:00:00 2001 From: Jon Van Oast Date: Tue, 12 Nov 2024 11:50:36 -0700 Subject: [PATCH 18/25] isHuman boolean in site-settings (set true for logged in user and when captcha passed once for session) --- src/main/java/org/ecocean/api/SiteSettings.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/ecocean/api/SiteSettings.java b/src/main/java/org/ecocean/api/SiteSettings.java index 971ae77c9b..e723db5bde 100644 --- a/src/main/java/org/ecocean/api/SiteSettings.java +++ b/src/main/java/org/ecocean/api/SiteSettings.java @@ -19,6 +19,7 @@ import org.ecocean.LocationID; import org.ecocean.Organization; import org.ecocean.Project; +import org.ecocean.servlet.ReCAPTCHA; import org.ecocean.servlet.ServletUtilities; import org.ecocean.Shepherd; import org.ecocean.ShepherdProperties; @@ -194,6 +195,8 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) } settings.put("projectsForUser", jp); } + settings.put("isHuman", ReCAPTCHA.sessionIsHuman(request)); + myShepherd.rollbackDBTransaction(); myShepherd.closeDBTransaction(); response.setStatus(200); From 93ad60c750e5f8fcadaf5e6669b89f01fff3b0ec Mon Sep 17 00:00:00 2001 From: TanyaStere42 Date: Tue, 12 Nov 2024 13:26:17 -0800 Subject: [PATCH 19/25] BE error copy and translations --- frontend/src/locale/de.json | 5 ++++- frontend/src/locale/en.json | 5 ++++- frontend/src/locale/es.json | 5 ++++- frontend/src/locale/fr.json | 5 ++++- frontend/src/locale/it.json | 5 ++++- 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/frontend/src/locale/de.json b/frontend/src/locale/de.json index ce0cc9c532..dcef7649e0 100644 --- a/frontend/src/locale/de.json +++ b/frontend/src/locale/de.json @@ -309,5 +309,8 @@ "LOCATION_ID_REQUIRED_WARNING": "Die Standort-ID ist erforderlich, um Abgleichsgruppen zu erstellen.", "MISSING_REQUIRED_FIELDS": "Es fehlen erforderliche Felder. Überprüfen Sie das Formular und versuchen Sie es erneut.", "INVALID_LAT": "Der Breitengrad muss zwischen -90 und 90 liegen", - "INVALID_LONG": "Der Längengrad muss zwischen -180 und 180 liegen" + "INVALID_LONG": "Der Längengrad muss zwischen -180 und 180 liegen", + "BEERROR_REQUIRED" : "Konnte nicht übermittelt werden; erforderliches Feld fehlt: ", + "BEERROR_INVALID" : "Konnte nicht übermittelt werden; Feldformat war ungültig: ", + "BEERROR_UNKNOWN" : "Konnte aufgrund eines unbekannten Fehlers nicht abgeschickt werden." } \ No newline at end of file diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index c8e7e58e53..4b86eb2554 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -309,6 +309,9 @@ "LOCATION_ID_REQUIRED_WARNING": "Location ID is required to establish match sets.", "MISSING_REQUIRED_FIELDS": "Missing required fields. Review the form and try again.", "INVALID_LAT": "Latitude must be between -90 and 90", - "INVALID_LONG": "Longitude must be between -180 and 180" + "INVALID_LONG": "Longitude must be between -180 and 180", + "BEERROR_REQUIRED" : "Could not submit; missing required field: ", + "BEERROR_INVALID" : "Could not submit; field format was invalid: ", + "BEERROR_UNKNOWN" : "Could not submit due to unknown error." } \ No newline at end of file diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json index de3b585b53..c4ed7c2a4a 100644 --- a/frontend/src/locale/es.json +++ b/frontend/src/locale/es.json @@ -308,5 +308,8 @@ "LOCATION_ID_REQUIRED_WARNING": "Se requiere el ID de ubicación para establecer conjuntos de coincidencias.", "MISSING_REQUIRED_FIELDS": "Faltan campos obligatorios. Revise el formulario e inténtelo de nuevo.", "INVALID_LAT": "La latitud debe estar entre -90 y 90", - "INVALID_LONG": "La longitud debe estar entre -180 y 180" + "INVALID_LONG": "La longitud debe estar entre -180 y 180", + "BEERROR_REQUIRED" : "No se ha podido enviar; falta el campo obligatorio: ", + "BEERROR_INVALID" : "No se pudo enviar; el formato del campo no era válido: ", + "BEERROR_UNKNOWN" : "No se pudo enviar debido a un error desconocido." } \ No newline at end of file diff --git a/frontend/src/locale/fr.json b/frontend/src/locale/fr.json index b7b3b12ab8..acfb2b597c 100644 --- a/frontend/src/locale/fr.json +++ b/frontend/src/locale/fr.json @@ -308,5 +308,8 @@ "LOCATION_ID_REQUIRED_WARNING": "L'ID de l'emplacement est requis pour établir des ensembles de correspondances.", "MISSING_REQUIRED_FIELDS": "Champs obligatoires manquants. Vérifiez le formulaire et réessayez.", "INVALID_LAT": "La latitude doit être comprise entre -90 et 90", - "INVALID_LONG": "La longitude doit être comprise entre -180 et 180" + "INVALID_LONG": "La longitude doit être comprise entre -180 et 180", + "BEERROR_REQUIRED" : "Impossible de soumettre ; champ requis manquant : ", + "BEERROR_INVALID" : "Impossible de soumettre ; le format du champ n'est pas valide : ", + "BEERROR_UNKNOWN" : "Impossible de soumettre en raison d'une erreur inconnue." } \ No newline at end of file diff --git a/frontend/src/locale/it.json b/frontend/src/locale/it.json index fb3344dc32..7311dcc083 100644 --- a/frontend/src/locale/it.json +++ b/frontend/src/locale/it.json @@ -308,5 +308,8 @@ "LOCATION_ID_REQUIRED_WARNING": "L'ID della posizione è necessario per stabilire set di corrispondenze.", "MISSING_REQUIRED_FIELDS": "Mancano i campi obbligatori. Rivedere il modulo e riprovare.", "INVALID_LAT": "La latitudine deve essere compresa tra -90 e 90", - "INVALID_LONG": "La longitudine deve essere compresa tra -180 e 180" + "INVALID_LONG": "La longitudine deve essere compresa tra -180 e 180", + "BEERROR_REQUIRED" : "Impossibile inviare; manca un campo obbligatorio: ", + "BEERROR_INVALID" : "Impossibile inviare; il formato del campo non era valido: ", + "BEERROR_UNKNOWN" : "Impossibile inviare a causa di un errore sconosciuto." } \ No newline at end of file From 96461a870b9e80d8e93eb52387a31118ea9d632f Mon Sep 17 00:00:00 2001 From: erinz2020 Date: Wed, 13 Nov 2024 00:42:06 +0000 Subject: [PATCH 20/25] ad just captcha, etc --- .../ImageSection.jsx | 93 ++++++-- .../ReportsAndManagamentPages/LocationID.jsx | 2 +- .../PlaceSection.jsx | 206 ++++++++++++++++-- .../ReportEncounter.jsx | 111 +++++++--- .../ReportEncounterStore.js | 21 +- 5 files changed, 362 insertions(+), 71 deletions(-) diff --git a/frontend/src/pages/ReportsAndManagamentPages/ImageSection.jsx b/frontend/src/pages/ReportsAndManagamentPages/ImageSection.jsx index 831073abab..025ab700a5 100644 --- a/frontend/src/pages/ReportsAndManagamentPages/ImageSection.jsx +++ b/frontend/src/pages/ReportsAndManagamentPages/ImageSection.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef, useContext } from "react"; -import { ProgressBar, Image, Row, Col } from "react-bootstrap"; +import { ProgressBar, Image as BootstrapImage, Row, Col } from "react-bootstrap"; import Flow from "@flowjs/flow.js"; import { FormattedMessage } from "react-intl"; import ThemeContext from "../../ThemeColorProvider"; @@ -29,9 +29,11 @@ export const FileUploader = observer(({ store }) => { previewData.filter((file) => file.fileSize <= maxSize * 1024 * 1024) .length, ); + console.log("previewData", previewData); const data = previewData.filter( (file) => file.fileSize <= maxSize * 1024 * 1024, ); + console.log("data", data); store.setImagePreview(data); // store.setImageSectionError( // store.imageRequired && @@ -43,10 +45,10 @@ export const FileUploader = observer(({ store }) => { }, [previewData]); useEffect(() => { - if (!flow && fileInputRef.current) { + if (!flow && fileInputRef.current && store.isHumanLocal ) { initializeFlow(); } - }, [flow, fileInputRef]); + }, [flow, fileInputRef, store.isHumanLocal]); useEffect(() => { const savedFiles = JSON.parse(localStorage.getItem("uploadedFiles")); @@ -75,6 +77,8 @@ export const FileUploader = observer(({ store }) => { setFlow(flowInstance); flowInstance.on("fileAdded", (file) => { + if(!store.isHumanLocal) return; + console.log("File added:", file); const supportedTypes = [ "image/jpeg", "image/jpg", @@ -98,18 +102,70 @@ export const FileUploader = observer(({ store }) => { file, ]); - const reader = new FileReader(); - reader.onloadend = () => { - setPreviewData((prevPreviewData) => [ - ...prevPreviewData.filter((p) => p.fileName !== file.name), - { - src: reader.result, - fileName: file.name, - fileSize: file.size, - progress: 0, - }, - ]); + const createThumbnail = (file) => { + console.log(`Thumbnail creation started for: ${file.name}`); + const reader = new FileReader(); + reader.onload = () => { + const img = new Image(); + img.src = reader.result; + + img.onload = () => { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + + const MAX_WIDTH = 150; + const MAX_HEIGHT = 150; + let width = img.width; + let height = img.height; + + if (width > height) { + if (width > MAX_WIDTH) { + height *= MAX_WIDTH / width; + width = MAX_WIDTH; + } + } else { + if (height > MAX_HEIGHT) { + width *= MAX_HEIGHT / height; + height = MAX_HEIGHT; + } + } + + canvas.width = width; + canvas.height = height; + + ctx.drawImage(img, 0, 0, width, height); + + const thumbnail = canvas.toDataURL("image/jpeg", 0.7); + console.log(`Thumbnail size: ${Math.round((thumbnail.length * 3) / 4 / 1024)} KB`); + + setPreviewData((prevPreviewData) => [ + ...prevPreviewData.filter((p) => p.fileName !== file.name), + { + src: thumbnail, + fileName: file.name, + fileSize: file.size, + progress: 0, + }, + ]); + }; + }; + reader.readAsDataURL(file.file); }; + + createThumbnail(file); + + // const reader = new FileReader(); + // reader.onloadend = () => { + // setPreviewData((prevPreviewData) => [ + // ...prevPreviewData.filter((p) => p.fileName !== file.name), + // { + // src: reader.result, + // fileName: file.name, + // fileSize: file.size, + // progress: 0, + // }, + // ]); + // }; EXIF.getData(file.file, function () { const exifData = EXIF.getAllTags(this); @@ -129,7 +185,7 @@ export const FileUploader = observer(({ store }) => { console.warn("EXIF data not available for:", file.name); } }); - reader.readAsDataURL(file.file); + // reader.readAsDataURL(file.file); }); flowInstance.on("fileProgress", (file) => { @@ -329,8 +385,6 @@ export const FileUploader = observer(({ store }) => { "uploadedFiles", JSON.stringify(store.imagePreview), ); - // localStorage.setItem("dateTimeSection", store.dateTimeSection.value); - // localStorage.setItem("placeSection", store.placeSection.value); localStorage.setItem( "submissionId", store.imageSectionSubmissionId, @@ -344,6 +398,9 @@ export const FileUploader = observer(({ store }) => { store.dateTimeSection.value?.toISOString(), ); localStorage.setItem("exifDateTime", store.exifDateTime); + localStorage.setItem("locationID", store.placeSection.locationId); + localStorage.setItem("lat", store.lat); + localStorage.setItem("lon", store.lon); }} > @@ -390,7 +447,7 @@ export const FileUploader = observer(({ store }) => { ); }} > - - + )} diff --git a/frontend/src/pages/ReportsAndManagamentPages/PlaceSection.jsx b/frontend/src/pages/ReportsAndManagamentPages/PlaceSection.jsx index 2bfc68bba6..1a4dd9456d 100644 --- a/frontend/src/pages/ReportsAndManagamentPages/PlaceSection.jsx +++ b/frontend/src/pages/ReportsAndManagamentPages/PlaceSection.jsx @@ -1,35 +1,93 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { Form } from "react-bootstrap"; import { FormattedMessage } from "react-intl"; import { observer } from "mobx-react-lite"; +import { Loader } from "@googlemaps/js-api-loader"; import useGetSiteSettings from "../../models/useGetSiteSettings"; import "./reportEncounter.css"; import { LocationID } from "./LocationID"; - -const MyPin = React.memo(() => { - return ( - - ); -}); - -MyPin.displayName = "MyPin"; +import { Alert } from "react-bootstrap"; +import { set } from "date-fns"; export const PlaceSection = observer(({ store }) => { const { data } = useGetSiteSettings(); - const mapCenterLat = data?.mapCenterLat; - const mapCenterLon = data?.mapCenterLon; - const mapZoom = data?.mapZoom; - const key = data?.googleMapsKey; + const mapCenterLat = data?.mapCenterLat || 51; + const mapCenterLon = data?.mapCenterLon || 7; + const mapZoom = data?.mapZoom || 4; const locationData = data?.locationData.locationID; + const mapKey = data?.googleMapsKey || ''; + const [latAlert, setLatAlert] = useState(false); + const [lonAlert, setLonAlert] = useState(false); + + const mapRef = useRef(null); + const markerRef = useRef(null); + const [map, setMap] = useState(null); + const [pan, setPan] = useState(false); + + useEffect(() => { + console.log("mapKey", mapKey); + if (!mapKey) { + return; + } + const loader = new Loader({ + apiKey: mapKey, + version: "weekly", + }); + + loader + .load() + .then(() => { + const googleMap = new window.google.maps.Map(mapRef.current, { + center: { lat: mapCenterLat, lng: mapCenterLon }, + zoom: mapZoom, + }); + + googleMap.addListener("click", (e) => { + setPan(false); + const lat = e.latLng.lat(); + const lng = e.latLng.lng(); + store.setLat(lat); + store.setLon(lng); + + if (markerRef.current) { + markerRef.current.setPosition({ lat, lng }); + } else { + markerRef.current = new window.google.maps.Marker({ + position: { lat, lng }, + map: googleMap, + }); + } + }); + + setMap(googleMap); + }) + .catch((error) => { + console.error("Error loading Google Maps", error); + }); + }, [mapCenterLat, mapCenterLon, mapZoom, mapKey]); + + useEffect(() => { + const lat = parseFloat(store.lat); + const lng = parseFloat(store.lon); + + if (map && !isNaN(lat) && !isNaN(lng)) { + if (pan) { + map.panTo({ lat, lng }); + } + + if (markerRef.current) { + markerRef.current.setPosition({ lat, lng }); + } else { + markerRef.current = new window.google.maps.Marker({ + position: { lat, lng }, + map: map, + }); + } + } else if (markerRef.current) { + markerRef.current.setMap(null); + markerRef.current = null; + } + }, [store.lat, store.lon, map, pan]); return (

@@ -39,7 +97,109 @@ export const PlaceSection = observer(({ store }) => { mapCenterLat={mapCenterLat} mapCenterLon={mapCenterLon} mapZoom={mapZoom} - /> + /> + + + + + +
+
+ { + setLatAlert(false); + let newLat = e.target.value; + setPan(true); + store.setLat(newLat); + if (newLat < -90 || newLat > 90) { + setLatAlert(true); + } + }} + // onBlur={() => { + // let validLat = parseFloat(store.lat); + // if (!isNaN(validLat)) { + // validLat = Math.min(Math.max(validLat, -90), 90); + // } else { + // validLat = ""; + // } + // store.setLat(validLat); + // }} + /> + {latAlert && + + + + } +
+
+ { + setLonAlert(false); + const newLon = e.target.value; + setPan(true); + store.setLon(newLon); + if (newLon < -180 || newLon > 180) { + setLonAlert(true); + } + }} + // onBlur={() => { + // let validLon = parseFloat(store.lon); + // if (!isNaN(validLon)) { + // validLon = Math.min(Math.max(validLon, -180), 180); + // } else { + // validLon = ""; + // } + // store.setLon(validLon); + // }} + /> + {lonAlert && + + + + } +
+
+
+ +
+
+
); }); diff --git a/frontend/src/pages/ReportsAndManagamentPages/ReportEncounter.jsx b/frontend/src/pages/ReportsAndManagamentPages/ReportEncounter.jsx index 9aa40cd3e0..ac4db9b80b 100644 --- a/frontend/src/pages/ReportsAndManagamentPages/ReportEncounter.jsx +++ b/frontend/src/pages/ReportsAndManagamentPages/ReportEncounter.jsx @@ -17,6 +17,7 @@ import useGetSiteSettings from "../../models/useGetSiteSettings"; import "./recaptcha.css"; export const ReportEncounter = observer(() => { + const themeColor = useContext(ThemeColorContext); const { isLoggedIn } = useContext(AuthContext); const Navigate = useNavigate(); @@ -25,10 +26,20 @@ export const ReportEncounter = observer(() => { const store = useLocalObservable(() => new ReportEncounterStore()); const [missingField, setMissingField] = useState(false); const [loading, setLoading] = useState(false); + const fieldsConfig = getFieldsConfig(store); + const isHuman = data?.isHuman; + if (isHuman) { + store.setIsHumanLocal(true); + } + + console.log("is human", isHuman); + console.log("store.isHumanLocal", store.isHumanLocal); + store.setImageRequired(!isLoggedIn); useEffect(() => { + console.log("Checking local storage for saved data."); localStorage.getItem("species") && store.setSpeciesSectionValue(localStorage.getItem("species")); localStorage.getItem("followUpSection.submitter.name") && @@ -57,8 +68,6 @@ export const ReportEncounter = observer(() => { ); localStorage.getItem("uploadedFiles") && store.setImagePreview(JSON.parse(localStorage.getItem("uploadedFiles"))); - // localStorage.getItem("dateTimeSection") && (store.setDateTimeSectionValue(localStorage.getItem("dateTimeSection"))); - // localStorage.getItem("placeSection") && (store.setPlaceSectionValue(localStorage.getItem("placeSection"))); localStorage.getItem("submissionId") && store.setImageSectionSubmissionId(localStorage.getItem("submissionId")); localStorage.getItem("fileNames") && @@ -69,6 +78,9 @@ export const ReportEncounter = observer(() => { store.setDateTimeSectionValue(new Date(localStorage.getItem("datetime"))); localStorage.getItem("exifDateTime") && store.setExifDateTime(localStorage.getItem("exifDateTime")); + localStorage.getItem("locationID") && store.setLocationId(localStorage.getItem("locationID")); + localStorage.getItem("lat") && store.setLat(localStorage.getItem("lat")); + localStorage.getItem("lon") && store.setLon(localStorage.getItem("lon")); localStorage.removeItem("species"); localStorage.removeItem("followUpSection.submitter.name"); @@ -78,11 +90,25 @@ export const ReportEncounter = observer(() => { localStorage.removeItem("followUpSection.additionalEmails"); localStorage.removeItem("additionalCommentsSection"); localStorage.removeItem("uploadedFiles"); - localStorage.removeItem("dateTimeSection"); localStorage.removeItem("placeSection"); localStorage.removeItem("fileNames"); localStorage.removeItem("datetime"); localStorage.removeItem("exifDateTime"); + localStorage.removeItem("locationID"); + localStorage.removeItem("lat"); + localStorage.removeItem("lon"); + localStorage.removeItem("submissionId"); + + // fieldsConfig.forEach(({ key, setter }) => { + // const value = localStorage.getItem(key); + // if (value !== null && value !== undefined && value !== "") { + // setter(value); + // } + // }); + + // fieldsConfig.forEach(({ key }) => { + // localStorage.removeItem(key); + // }); }, []); const handleSubmit = async () => { @@ -94,6 +120,7 @@ export const ReportEncounter = observer(() => { setLoading(false); return; } else { + setMissingField(false); console.log("Fields validated successfully. Submitting report."); const responseData = await store.submitReport(); console.log("Response data: ", responseData); @@ -175,8 +202,15 @@ export const ReportEncounter = observer(() => { }); }; + console.log(procaptchaSiteKey); + const captchaRef = useRef(null); useEffect(() => { + console.log("isHuman", isHuman); + console.log("store.isHumanLocal", store.isHumanLocal); + if (store.isHumanLocal) return; + console.log("Loading ProCaptcha"); + let isCaptchaRendered = false; const loadProCaptcha = async () => { @@ -217,19 +251,21 @@ export const ReportEncounter = observer(() => { body: JSON.stringify(payload), }); const data = await res.json(); + store.setIsHumanLocal(data.valid); console.log("Response data: ", data); } catch (error) { console.error("Error submitting captcha: ", error); } }; - return ( store.setShowSubmissionFailedAlert(false)} + onHide={() => { + setLoading(false); + store.setShowSubmissionFailedAlert(false) + }} keyboard centered animation @@ -242,8 +278,20 @@ export const ReportEncounter = observer(() => { }} >
- {missingField && } - submission failed. + {missingField && } + {store.error &&
+ {store.error.slice().map((error, index) => ( +
+ {error.code === "INVALID" &&

+ {error.fieldName}

} + {error.code === "REQUIRED" &&

+ {error.fieldName}

} + {!error.code &&

+ {error.fieldName}

} +
+ ))} +
} + {!missingField && !store.error && }
@@ -263,7 +311,7 @@ export const ReportEncounter = observer(() => { color: themeColor.statusColors.yellow800, }} > - + {(isHuman || isHumanLocal) ? : } { color="white" backgroundColor={themeColor.wildMeColors.cyan600} onClick={() => { + console.log("Storing data in local storage"); localStorage.setItem("species", store.speciesSection.value); localStorage.setItem( "followUpSection.submitter.name", store.followUpSection.submitter.name, ); - localStorage.setItem( + store.followUpSection.submitter.email && localStorage.setItem( "followUpSection.submitter.email", store.followUpSection.submitter.email, ); - localStorage.setItem( + store.followUpSection.photographer.name && localStorage.setItem( "followUpSection.photographer.name", store.followUpSection.photographer.name, ); - localStorage.setItem( + store.followUpSection.photographer.email && localStorage.setItem( "followUpSection.photographer.email", store.followUpSection.photographer.email, ); - localStorage.setItem( + store.followUpSection.additionalEmails && localStorage.setItem( "followUpSection.additionalEmails", store.followUpSection.additionalEmails, ); - localStorage.setItem( + store.additionalCommentsSection.value && localStorage.setItem( "additionalCommentsSection", store.additionalCommentsSection.value, ); - localStorage.setItem( + store.imagePreview && localStorage.setItem( "uploadedFiles", JSON.stringify(store.imagePreview), ); - // localStorage.setItem("dateTimeSection", store.dateTimeSection.value); - // localStorage.setItem("placeSection", store.placeSection.value); - localStorage.setItem( + store.imageSectionSubmissionId && localStorage.setItem( "submissionId", store.imageSectionSubmissionId, ); - localStorage.setItem( + store.imageSectionFileNames && localStorage.setItem( "fileNames", JSON.stringify(store.imageSectionFileNames), ); - localStorage.setItem( + store.dateTimeSection.value && localStorage.setItem( "datetime", store.dateTimeSection.value?.toISOString(), ); - localStorage.setItem("exifDateTime", store.exifDateTime); + store.exifDateTime && localStorage.setItem("exifDateTime", store.exifDateTime); + store.placeSection.locationId && localStorage.setItem("locationID", store.placeSection.locationId); + store.lat && localStorage.setItem("lat", store.lat); + store.lon && localStorage.setItem("lon", store.lon); + + // fieldsConfig.forEach(({ key, parser }) => { + // const value = store[key]; + // if (value !== undefined && value !== null && value !== "") { + // localStorage.setItem(key, parser ? parser(value) : value); + // } + // }); }} > -
+ {store.isHumanLocal || isHuman ? null : ( +
+ )}
) : null} diff --git a/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js b/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js index c4cfcc588d..d43f67713d 100644 --- a/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js +++ b/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js @@ -22,6 +22,7 @@ export class ReportEncounterStore { _error; _lat; _lon; + _isHumanLocal; constructor() { this._imageSectionSubmissionId = null; @@ -68,6 +69,7 @@ export class ReportEncounterStore { this._error = null; this._lat = null; this._lon = null; + this._isHumanLocal = false; makeAutoObservable(this); } @@ -149,6 +151,10 @@ export class ReportEncounterStore { return this._lon; } + get isHumanLocal() { + return this._isHumanLocal; + } + // Actions setImageSectionSubmissionId(value) { this._imageSectionSubmissionId = value; @@ -253,6 +259,10 @@ export class ReportEncounterStore { this._lon = value; } + setIsHumanLocal(value) { + this._isHumanLocal = value; + } + validateEmails() { const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; @@ -335,12 +345,17 @@ export class ReportEncounterStore { submitterEmail: this._followUpSection.submitter.email, photographerName: this._followUpSection.photographer.name, photographerEmail: this._followUpSection.photographer.email, + additionalEmails: this._followUpSection.additionalEmails, decimalLatitude: this._lat, decimalLongitude: this._lon, - }; - + }; + const filteredPayload = Object.fromEntries( - Object.entries(payload).filter(([key, value]) => value !== null && value !== ""), + //filter out null and empty value, + Object.entries(payload).filter(([key, value]) => value !== null && value !== "") + // filter out invalid lat + .filter(([key, value]) => key !== "decimalLatitude" || (value >= -90 && value <= 90)) + .filter(([key, value]) => key !== "decimalLongitude" || (value >= -180 && value <= 180)) ); console.log("Filtered payload", filteredPayload); From f9983f32acdcd0169d0df20064f49ffcfcff22bb Mon Sep 17 00:00:00 2001 From: erinz2020 Date: Wed, 13 Nov 2024 16:34:54 +0000 Subject: [PATCH 21/25] clean up --- frontend/src/locale/de.json | 3 +- frontend/src/locale/en.json | 3 +- frontend/src/locale/es.json | 3 +- frontend/src/locale/fr.json | 3 +- frontend/src/locale/it.json | 3 +- .../ImageSection.jsx | 135 ++++++++---------- .../PlaceSection.jsx | 20 --- .../ReportEncounter.jsx | 34 +---- .../ReportEncounterStore.js | 16 --- .../SpeciesSection.jsx | 2 +- 10 files changed, 73 insertions(+), 149 deletions(-) diff --git a/frontend/src/locale/de.json b/frontend/src/locale/de.json index dcef7649e0..395f422e61 100644 --- a/frontend/src/locale/de.json +++ b/frontend/src/locale/de.json @@ -312,5 +312,6 @@ "INVALID_LONG": "Der Längengrad muss zwischen -180 und 180 liegen", "BEERROR_REQUIRED" : "Konnte nicht übermittelt werden; erforderliches Feld fehlt: ", "BEERROR_INVALID" : "Konnte nicht übermittelt werden; Feldformat war ungültig: ", - "BEERROR_UNKNOWN" : "Konnte aufgrund eines unbekannten Fehlers nicht abgeschickt werden." + "BEERROR_UNKNOWN" : "Konnte aufgrund eines unbekannten Fehlers nicht abgeschickt werden.", + "ANON_UPLOAD_IMAGE_WARNING": "Bilder können erst hochgeladen werden, wenn das Captcha abgeschlossen ist." } \ No newline at end of file diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 4b86eb2554..64f1d99725 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -312,6 +312,7 @@ "INVALID_LONG": "Longitude must be between -180 and 180", "BEERROR_REQUIRED" : "Could not submit; missing required field: ", "BEERROR_INVALID" : "Could not submit; field format was invalid: ", - "BEERROR_UNKNOWN" : "Could not submit due to unknown error." + "BEERROR_UNKNOWN" : "Could not submit due to unknown error.", + "ANON_UPLOAD_IMAGE_WARNING": "Images cannot be uploaded until captcha is complete." } \ No newline at end of file diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json index c4ed7c2a4a..3e5b62519f 100644 --- a/frontend/src/locale/es.json +++ b/frontend/src/locale/es.json @@ -311,5 +311,6 @@ "INVALID_LONG": "La longitud debe estar entre -180 y 180", "BEERROR_REQUIRED" : "No se ha podido enviar; falta el campo obligatorio: ", "BEERROR_INVALID" : "No se pudo enviar; el formato del campo no era válido: ", - "BEERROR_UNKNOWN" : "No se pudo enviar debido a un error desconocido." + "BEERROR_UNKNOWN" : "No se pudo enviar debido a un error desconocido.", + "ANON_UPLOAD_IMAGE_WARNING": "No se pueden subir imágenes hasta que se complete el captcha." } \ No newline at end of file diff --git a/frontend/src/locale/fr.json b/frontend/src/locale/fr.json index acfb2b597c..a0b34f7a0f 100644 --- a/frontend/src/locale/fr.json +++ b/frontend/src/locale/fr.json @@ -311,5 +311,6 @@ "INVALID_LONG": "La longitude doit être comprise entre -180 et 180", "BEERROR_REQUIRED" : "Impossible de soumettre ; champ requis manquant : ", "BEERROR_INVALID" : "Impossible de soumettre ; le format du champ n'est pas valide : ", - "BEERROR_UNKNOWN" : "Impossible de soumettre en raison d'une erreur inconnue." + "BEERROR_UNKNOWN" : "Impossible de soumettre en raison d'une erreur inconnue.", + "ANON_UPLOAD_IMAGE_WARNING": "Les images ne peuvent pas être téléchargées tant que le captcha n'est pas complété." } \ No newline at end of file diff --git a/frontend/src/locale/it.json b/frontend/src/locale/it.json index 7311dcc083..98b783c431 100644 --- a/frontend/src/locale/it.json +++ b/frontend/src/locale/it.json @@ -311,5 +311,6 @@ "INVALID_LONG": "La longitudine deve essere compresa tra -180 e 180", "BEERROR_REQUIRED" : "Impossibile inviare; manca un campo obbligatorio: ", "BEERROR_INVALID" : "Impossibile inviare; il formato del campo non era valido: ", - "BEERROR_UNKNOWN" : "Impossibile inviare a causa di un errore sconosciuto." + "BEERROR_UNKNOWN" : "Impossibile inviare a causa di un errore sconosciuto.", + "ANON_UPLOAD_IMAGE_WARNING": "Non è possibile caricare immagini finché il captcha non è completato." } \ No newline at end of file diff --git a/frontend/src/pages/ReportsAndManagamentPages/ImageSection.jsx b/frontend/src/pages/ReportsAndManagamentPages/ImageSection.jsx index 025ab700a5..f0184bda9e 100644 --- a/frontend/src/pages/ReportsAndManagamentPages/ImageSection.jsx +++ b/frontend/src/pages/ReportsAndManagamentPages/ImageSection.jsx @@ -29,11 +29,9 @@ export const FileUploader = observer(({ store }) => { previewData.filter((file) => file.fileSize <= maxSize * 1024 * 1024) .length, ); - console.log("previewData", previewData); const data = previewData.filter( (file) => file.fileSize <= maxSize * 1024 * 1024, ); - console.log("data", data); store.setImagePreview(data); // store.setImageSectionError( // store.imageRequired && @@ -45,7 +43,7 @@ export const FileUploader = observer(({ store }) => { }, [previewData]); useEffect(() => { - if (!flow && fileInputRef.current && store.isHumanLocal ) { + if (!flow && fileInputRef.current && store.isHumanLocal) { initializeFlow(); } }, [flow, fileInputRef, store.isHumanLocal]); @@ -77,8 +75,7 @@ export const FileUploader = observer(({ store }) => { setFlow(flowInstance); flowInstance.on("fileAdded", (file) => { - if(!store.isHumanLocal) return; - console.log("File added:", file); + if (!store.isHumanLocal) return; const supportedTypes = [ "image/jpeg", "image/jpg", @@ -103,21 +100,20 @@ export const FileUploader = observer(({ store }) => { ]); const createThumbnail = (file) => { - console.log(`Thumbnail creation started for: ${file.name}`); const reader = new FileReader(); reader.onload = () => { const img = new Image(); img.src = reader.result; - + img.onload = () => { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); - + const MAX_WIDTH = 150; const MAX_HEIGHT = 150; let width = img.width; let height = img.height; - + if (width > height) { if (width > MAX_WIDTH) { height *= MAX_WIDTH / width; @@ -132,12 +128,8 @@ export const FileUploader = observer(({ store }) => { canvas.width = width; canvas.height = height; - ctx.drawImage(img, 0, 0, width, height); - const thumbnail = canvas.toDataURL("image/jpeg", 0.7); - console.log(`Thumbnail size: ${Math.round((thumbnail.length * 3) / 4 / 1024)} KB`); - setPreviewData((prevPreviewData) => [ ...prevPreviewData.filter((p) => p.fileName !== file.name), { @@ -151,21 +143,8 @@ export const FileUploader = observer(({ store }) => { }; reader.readAsDataURL(file.file); }; - - createThumbnail(file); - // const reader = new FileReader(); - // reader.onloadend = () => { - // setPreviewData((prevPreviewData) => [ - // ...prevPreviewData.filter((p) => p.fileName !== file.name), - // { - // src: reader.result, - // fileName: file.name, - // fileSize: file.size, - // progress: 0, - // }, - // ]); - // }; + createThumbnail(file); EXIF.getData(file.file, function () { const exifData = EXIF.getAllTags(this); @@ -179,13 +158,12 @@ export const FileUploader = observer(({ store }) => { if (f.length == 3) datetime1 = f.join('-'); if ((f.length == 5) || (f.length == 6)) datetime1 = f.slice(0, 3).join('-') + ' ' + f.slice(3, 6).join(':'); store.setExifDateTime(datetime1); - // geo: latitude && longitude ? { latitude, longitude } : null, - + // geo: latitude && longitude ? { latitude, longitude } : null, + } else { console.warn("EXIF data not available for:", file.name); } }); - // reader.readAsDataURL(file.file); }); flowInstance.on("fileProgress", (file) => { @@ -338,6 +316,15 @@ export const FileUploader = observer(({ store }) => { {`${" "}${maxSize} MB`}

+ {!store.isHumanLocal && + + }
{store.imageSectionError && ( @@ -357,50 +344,50 @@ export const FileUploader = observer(({ store }) => { href={`${process.env.PUBLIC_URL}/login?redirect=%2Freport`} onClick={() => { localStorage.setItem("species", store.speciesSection.value); - localStorage.setItem( - "followUpSection.submitter.name", - store.followUpSection.submitter.name, - ); - localStorage.setItem( - "followUpSection.submitter.email", - store.followUpSection.submitter.email, - ); - localStorage.setItem( - "followUpSection.photographer.name", - store.followUpSection.photographer.name, - ); - localStorage.setItem( - "followUpSection.photographer.email", - store.followUpSection.photographer.email, - ); - localStorage.setItem( - "followUpSection.additionalEmails", - store.followUpSection.additionalEmails, - ); - localStorage.setItem( - "additionalCommentsSection", - store.additionalCommentsSection.value, - ); - localStorage.setItem( - "uploadedFiles", - JSON.stringify(store.imagePreview), - ); - localStorage.setItem( - "submissionId", - store.imageSectionSubmissionId, - ); - localStorage.setItem( - "fileNames", - JSON.stringify(store.imageSectionFileNames), - ); - localStorage.setItem( - "datetime", - store.dateTimeSection.value?.toISOString(), - ); - localStorage.setItem("exifDateTime", store.exifDateTime); - localStorage.setItem("locationID", store.placeSection.locationId); - localStorage.setItem("lat", store.lat); - localStorage.setItem("lon", store.lon); + localStorage.setItem( + "followUpSection.submitter.name", + store.followUpSection.submitter.name, + ); + localStorage.setItem( + "followUpSection.submitter.email", + store.followUpSection.submitter.email, + ); + localStorage.setItem( + "followUpSection.photographer.name", + store.followUpSection.photographer.name, + ); + localStorage.setItem( + "followUpSection.photographer.email", + store.followUpSection.photographer.email, + ); + localStorage.setItem( + "followUpSection.additionalEmails", + store.followUpSection.additionalEmails, + ); + localStorage.setItem( + "additionalCommentsSection", + store.additionalCommentsSection.value, + ); + localStorage.setItem( + "uploadedFiles", + JSON.stringify(store.imagePreview), + ); + localStorage.setItem( + "submissionId", + store.imageSectionSubmissionId, + ); + localStorage.setItem( + "fileNames", + JSON.stringify(store.imageSectionFileNames), + ); + localStorage.setItem( + "datetime", + store.dateTimeSection.value?.toISOString(), + ); + localStorage.setItem("exifDateTime", store.exifDateTime); + localStorage.setItem("locationID", store.placeSection.locationId); + localStorage.setItem("lat", store.lat); + localStorage.setItem("lon", store.lon); }} > diff --git a/frontend/src/pages/ReportsAndManagamentPages/PlaceSection.jsx b/frontend/src/pages/ReportsAndManagamentPages/PlaceSection.jsx index 1a4dd9456d..c02b24d2bb 100644 --- a/frontend/src/pages/ReportsAndManagamentPages/PlaceSection.jsx +++ b/frontend/src/pages/ReportsAndManagamentPages/PlaceSection.jsx @@ -7,7 +7,6 @@ import useGetSiteSettings from "../../models/useGetSiteSettings"; import "./reportEncounter.css"; import { LocationID } from "./LocationID"; import { Alert } from "react-bootstrap"; -import { set } from "date-fns"; export const PlaceSection = observer(({ store }) => { const { data } = useGetSiteSettings(); @@ -25,7 +24,6 @@ export const PlaceSection = observer(({ store }) => { const [pan, setPan] = useState(false); useEffect(() => { - console.log("mapKey", mapKey); if (!mapKey) { return; } @@ -121,15 +119,6 @@ export const PlaceSection = observer(({ store }) => { setLatAlert(true); } }} - // onBlur={() => { - // let validLat = parseFloat(store.lat); - // if (!isNaN(validLat)) { - // validLat = Math.min(Math.max(validLat, -90), 90); - // } else { - // validLat = ""; - // } - // store.setLat(validLat); - // }} /> {latAlert && { setLonAlert(true); } }} - // onBlur={() => { - // let validLon = parseFloat(store.lon); - // if (!isNaN(validLon)) { - // validLon = Math.min(Math.max(validLon, -180), 180); - // } else { - // validLon = ""; - // } - // store.setLon(validLon); - // }} /> {lonAlert && { const store = useLocalObservable(() => new ReportEncounterStore()); const [missingField, setMissingField] = useState(false); const [loading, setLoading] = useState(false); - const fieldsConfig = getFieldsConfig(store); const isHuman = data?.isHuman; if (isHuman) { store.setIsHumanLocal(true); } - console.log("is human", isHuman); - console.log("store.isHumanLocal", store.isHumanLocal); - - store.setImageRequired(!isLoggedIn); useEffect(() => { - console.log("Checking local storage for saved data."); localStorage.getItem("species") && store.setSpeciesSectionValue(localStorage.getItem("species")); localStorage.getItem("followUpSection.submitter.name") && @@ -99,31 +93,18 @@ export const ReportEncounter = observer(() => { localStorage.removeItem("lon"); localStorage.removeItem("submissionId"); - // fieldsConfig.forEach(({ key, setter }) => { - // const value = localStorage.getItem(key); - // if (value !== null && value !== undefined && value !== "") { - // setter(value); - // } - // }); - - // fieldsConfig.forEach(({ key }) => { - // localStorage.removeItem(key); - // }); }, []); const handleSubmit = async () => { setLoading(true); if (!store.validateFields()) { - console.log("Field validation failed."); store.setShowSubmissionFailedAlert(true); setMissingField(true); setLoading(false); return; } else { setMissingField(false); - console.log("Fields validated successfully. Submitting report."); const responseData = await store.submitReport(); - console.log("Response data: ", responseData); if (store.finished && store.success) { setLoading(false); Navigate("/reportConfirm", { state: { responseData } }); @@ -202,30 +183,21 @@ export const ReportEncounter = observer(() => { }); }; - console.log(procaptchaSiteKey); - const captchaRef = useRef(null); useEffect(() => { - console.log("isHuman", isHuman); - console.log("store.isHumanLocal", store.isHumanLocal); if (store.isHumanLocal) return; - console.log("Loading ProCaptcha"); let isCaptchaRendered = false; - const loadProCaptcha = async () => { if (isCaptchaRendered || !captchaRef.current) return; - const { render } = await import( "https://js.prosopo.io/js/procaptcha.bundle.js" ); - if (procaptchaSiteKey) { render(captchaRef.current, { siteKey: procaptchaSiteKey, callback: onCaptchaVerified, }); - isCaptchaRendered = true; } }; @@ -241,9 +213,7 @@ export const ReportEncounter = observer(() => { }, [procaptchaSiteKey]); const onCaptchaVerified = async (output) => { - console.log("Captcha verified, output: " + JSON.stringify(output)); const payload = { procaptchaValue: output }; - try { const res = await fetch("/ReCAPTCHA", { method: "POST", @@ -252,7 +222,6 @@ export const ReportEncounter = observer(() => { }); const data = await res.json(); store.setIsHumanLocal(data.valid); - console.log("Response data: ", data); } catch (error) { console.error("Error submitting captcha: ", error); } @@ -311,7 +280,7 @@ export const ReportEncounter = observer(() => { color: themeColor.statusColors.yellow800, }} > - {(isHuman || isHumanLocal) ? : } + {(isHuman || store.isHumanLocal) ? : } { color="white" backgroundColor={themeColor.wildMeColors.cyan600} onClick={() => { - console.log("Storing data in local storage"); localStorage.setItem("species", store.speciesSection.value); localStorage.setItem( "followUpSection.submitter.name", diff --git a/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js b/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js index d43f67713d..349a3c5cc1 100644 --- a/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js +++ b/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js @@ -183,7 +183,6 @@ export class ReportEncounterStore { this._imageSectionFileNames = this._imageSectionFileNames.filter( (name) => name !== fileName, ); - // delete this._exifDateTime[fileName]; } } @@ -299,41 +298,32 @@ export class ReportEncounterStore { } if (!this.validateEmails()) { - console.log("email validation failed"); isValid = false; } if (this._imageRequired && this._imageSectionFileNames.length === 0) { - console.log("1"); this._imageSectionError = true; isValid = false; } - console.log(isValid); if (!this._dateTimeSection.value && this._dateTimeSection.required) { - console.log(JSON.stringify(this._dateTimeSection)); this._dateTimeSection.error = true; isValid = false; } if (!this._placeSection.locationId && this._placeSection.required) { - console.log("3"); this._placeSection.error = true; isValid = false; } - console.log("Validation result", isValid); return isValid; } async submitReport() { - console.log("submitting"); this._loading = true; const readyCaseone = this.validateFields() && this._imageSectionFileNames.length > 0; const readyCasetwo = this.validateFields() && !this._imageRequired; - console.log(readyCaseone, readyCasetwo); if (readyCaseone || readyCasetwo) { try { - const payload = { submissionId: this._imageSectionSubmissionId, assetFilenames: this._imageSectionFileNames, @@ -351,18 +341,14 @@ export class ReportEncounterStore { }; const filteredPayload = Object.fromEntries( - //filter out null and empty value, Object.entries(payload).filter(([key, value]) => value !== null && value !== "") - // filter out invalid lat .filter(([key, value]) => key !== "decimalLatitude" || (value >= -90 && value <= 90)) .filter(([key, value]) => key !== "decimalLongitude" || (value >= -180 && value <= 180)) ); - console.log("Filtered payload", filteredPayload); const response = await axios.post("/api/v3/encounters", filteredPayload); if (response.status === 200) { - console.log("Report submitted successfully.", response); this._speciesSection.value = ""; this._placeSection.value = ""; this._followUpSection.value = ""; @@ -374,12 +360,10 @@ export class ReportEncounterStore { this._success = true; this._finished = true; - console.log(this._finished); return response.data; } } catch (error) { console.error("Error submitting report", error); - console.log(JSON.stringify(error)); this._showSubmissionFailedAlert = true; this._error = error.response.data.errors; } diff --git a/frontend/src/pages/ReportsAndManagamentPages/SpeciesSection.jsx b/frontend/src/pages/ReportsAndManagamentPages/SpeciesSection.jsx index 369ecec22e..05d5c6b28e 100644 --- a/frontend/src/pages/ReportsAndManagamentPages/SpeciesSection.jsx +++ b/frontend/src/pages/ReportsAndManagamentPages/SpeciesSection.jsx @@ -14,7 +14,7 @@ export const ReportEncounterSpeciesSection = observer( label: item?.scientificName, }; }) || []; - speciesList = [...speciesList, { value: "Unknown", label: "Unknown" }]; + speciesList = [...speciesList, { value: "unknown", label: "Unknown" }]; return (
From 2c0659c8ed74c0cf26915a278161a71d9dbc25f9 Mon Sep 17 00:00:00 2001 From: Jon Van Oast Date: Wed, 13 Nov 2024 10:50:51 -0700 Subject: [PATCH 22/25] make taxonomy non-required (null allowed) --- src/main/java/org/ecocean/Encounter.java | 26 +++++++++++------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/ecocean/Encounter.java b/src/main/java/org/ecocean/Encounter.java index c4391b7071..e4519aa4e2 100644 --- a/src/main/java/org/ecocean/Encounter.java +++ b/src/main/java/org/ecocean/Encounter.java @@ -4814,20 +4814,18 @@ public static Object validateFieldValue(String fieldName, org.json.JSONObject da case "taxonomy": returnValue = data.optString(fieldName, null); - if (returnValue == null) { - error.put("code", ApiException.ERROR_RETURN_CODE_REQUIRED); - throw new ApiException(exMessage, error); - } - // this is throwaway read-only shepherd - Shepherd myShepherd = new Shepherd("context0"); - myShepherd.setAction("Encounter.validateFieldValue"); - myShepherd.beginDBTransaction(); - boolean validTaxonomy = myShepherd.isValidTaxonomyName((String)returnValue); - myShepherd.rollbackDBTransaction(); - if (!validTaxonomy) { - error.put("code", ApiException.ERROR_RETURN_CODE_INVALID); - error.put("value", returnValue); - throw new ApiException(exMessage, error); + if (returnValue != null) { // null is allowed, but will not pass validity + // this is throwaway read-only shepherd + Shepherd myShepherd = new Shepherd("context0"); + myShepherd.setAction("Encounter.validateFieldValue"); + myShepherd.beginDBTransaction(); + boolean validTaxonomy = myShepherd.isValidTaxonomyName((String)returnValue); + myShepherd.rollbackDBTransaction(); + if (!validTaxonomy) { + error.put("code", ApiException.ERROR_RETURN_CODE_INVALID); + error.put("value", returnValue); + throw new ApiException(exMessage, error); + } } break; From 7e4f297115daef02801f9b0fc94ca093d24714bd Mon Sep 17 00:00:00 2001 From: erinz2020 Date: Wed, 13 Nov 2024 19:09:59 +0000 Subject: [PATCH 23/25] species required = false --- .../ReportsAndManagamentPages/ImageSection.jsx | 11 ++--------- .../ReportEncounterStore.js | 15 +++++---------- .../ReportsAndManagamentPages/SpeciesSection.jsx | 3 --- 3 files changed, 7 insertions(+), 22 deletions(-) diff --git a/frontend/src/pages/ReportsAndManagamentPages/ImageSection.jsx b/frontend/src/pages/ReportsAndManagamentPages/ImageSection.jsx index f0184bda9e..585d1baa0d 100644 --- a/frontend/src/pages/ReportsAndManagamentPages/ImageSection.jsx +++ b/frontend/src/pages/ReportsAndManagamentPages/ImageSection.jsx @@ -83,13 +83,11 @@ export const FileUploader = observer(({ store }) => { "image/bmp", ]; if (!supportedTypes.includes(file.file.type)) { - console.error("Unsupported file type:", file.file.type); flowInstance.removeFile(file); return false; } if (file.size > maxSize * 1024 * 1024) { - console.warn("File size exceeds limit:", file.name); return false; } @@ -159,10 +157,7 @@ export const FileUploader = observer(({ store }) => { if ((f.length == 5) || (f.length == 6)) datetime1 = f.slice(0, 3).join('-') + ' ' + f.slice(3, 6).join(':'); store.setExifDateTime(datetime1); // geo: latitude && longitude ? { latitude, longitude } : null, - - } else { - console.warn("EXIF data not available for:", file.name); - } + } }); }); @@ -189,7 +184,7 @@ export const FileUploader = observer(({ store }) => { ); }); - flowInstance.on("fileError", (file, message) => { + flowInstance.on("fileError", (file) => { setUploading(false); setPreviewData((prevPreviewData) => prevPreviewData.map((preview) => @@ -198,7 +193,6 @@ export const FileUploader = observer(({ store }) => { : preview, ), ); - console.error("Upload error:", message); }); setupDragAndDropListeners(flowInstance); }; @@ -286,7 +280,6 @@ export const FileUploader = observer(({ store }) => { : preview, ), ); - console.error(`File upload timed out: ${file.name}`); }, 300000); flow.on("fileSuccess", (uploadedFile) => { diff --git a/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js b/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js index 349a3c5cc1..ac0dc1553d 100644 --- a/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js +++ b/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js @@ -39,7 +39,7 @@ export class ReportEncounterStore { this._speciesSection = { value: "", error: false, - required: true, + required: false, }; this._placeSection = { value: "", @@ -287,15 +287,12 @@ export class ReportEncounterStore { } validateFields() { - console.log("Validating fields"); let isValid = true; - if (!this._speciesSection.value) { + if (this._speciesSection.required && !this._speciesSection.value) { this._speciesSection.error = true; isValid = false; - } else { - this._speciesSection.error = false; - } + } if (!this.validateEmails()) { isValid = false; @@ -344,6 +341,7 @@ export class ReportEncounterStore { Object.entries(payload).filter(([key, value]) => value !== null && value !== "") .filter(([key, value]) => key !== "decimalLatitude" || (value >= -90 && value <= 90)) .filter(([key, value]) => key !== "decimalLongitude" || (value >= -180 && value <= 180)) + .filter(([key, value]) => key !== "taxonomy" || value !== "unknown") ); const response = await axios.post("/api/v3/encounters", filteredPayload); @@ -363,13 +361,10 @@ export class ReportEncounterStore { return response.data; } } catch (error) { - console.error("Error submitting report", error); this._showSubmissionFailedAlert = true; this._error = error.response.data.errors; } - } else { - console.error("Validation failed"); - } + } } } diff --git a/frontend/src/pages/ReportsAndManagamentPages/SpeciesSection.jsx b/frontend/src/pages/ReportsAndManagamentPages/SpeciesSection.jsx index 05d5c6b28e..07aa808861 100644 --- a/frontend/src/pages/ReportsAndManagamentPages/SpeciesSection.jsx +++ b/frontend/src/pages/ReportsAndManagamentPages/SpeciesSection.jsx @@ -39,9 +39,6 @@ export const ReportEncounterSpeciesSection = observer( value={store.speciesSection.value} onChange={(e) => { store.setSpeciesSectionValue(e.target.value); - store.setSpeciesSectionError( - e.target.value ? false : true, - ); }} >
); }); diff --git a/frontend/src/pages/ReportsAndManagamentPages/ReportEncounter.jsx b/frontend/src/pages/ReportsAndManagamentPages/ReportEncounter.jsx index 61af207da9..26602dc959 100644 --- a/frontend/src/pages/ReportsAndManagamentPages/ReportEncounter.jsx +++ b/frontend/src/pages/ReportsAndManagamentPages/ReportEncounter.jsx @@ -341,13 +341,6 @@ export const ReportEncounter = observer(() => { store.placeSection.locationId && localStorage.setItem("locationID", store.placeSection.locationId); store.lat && localStorage.setItem("lat", store.lat); store.lon && localStorage.setItem("lon", store.lon); - - // fieldsConfig.forEach(({ key, parser }) => { - // const value = store[key]; - // if (value !== undefined && value !== null && value !== "") { - // localStorage.setItem(key, parser ? parser(value) : value); - // } - // }); }} > From f970f5b1c04ab07e63027f7cc93de90165e2a58b Mon Sep 17 00:00:00 2001 From: erinz2020 Date: Wed, 13 Nov 2024 20:18:18 +0000 Subject: [PATCH 25/25] forget about last commit, it's a bad idea --- .../PlaceSection.jsx | 91 ++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/ReportsAndManagamentPages/PlaceSection.jsx b/frontend/src/pages/ReportsAndManagamentPages/PlaceSection.jsx index 6e82686bbe..2cc2a43b02 100644 --- a/frontend/src/pages/ReportsAndManagamentPages/PlaceSection.jsx +++ b/frontend/src/pages/ReportsAndManagamentPages/PlaceSection.jsx @@ -14,7 +14,7 @@ export const PlaceSection = observer(({ store }) => { const mapCenterLon = data?.mapCenterLon || 7; const mapZoom = data?.mapZoom || 4; const locationData = data?.locationData.locationID; - const mapKey = data?.googleMapsKey || ''; + const mapKey = data?.googleMapsKey || ""; const [latAlert, setLatAlert] = useState(false); const [lonAlert, setLonAlert] = useState(false); @@ -95,7 +95,94 @@ export const PlaceSection = observer(({ store }) => { mapCenterLat={mapCenterLat} mapCenterLon={mapCenterLon} mapZoom={mapZoom} - /> + /> + + + + +
+
+ { + setLatAlert(false); + let newLat = e.target.value; + setPan(true); + store.setLat(newLat); + if (newLat < -90 || newLat > 90) { + setLatAlert(true); + } + }} + /> + {latAlert && ( + + + + + + )} +
+
+ { + setLonAlert(false); + const newLon = e.target.value; + setPan(true); + store.setLon(newLon); + if (newLon < -180 || newLon > 180) { + setLonAlert(true); + } + }} + /> + {lonAlert && ( + + + + + + )} +
+
+
+ +
+
+
); });