Skip to content

Commit

Permalink
implement association-based search
Browse files Browse the repository at this point in the history
- Add vertical-align to crowdsource css to override value set within reset.css
- Force refresh of crowdsource.css within client's browsers

crowdsource editor styling issue

- resolve vertical-align issue, which persisted when opening in crowdsourcing mode and then switching between long and short forms
- Fix fieldset padding issue within image popout

Api media 2024 06 (#1463)

- Fix route bug interfering with POST write calls
- Add recordID UUID to POST call
- Improve error output media object isn't found (400 error)
- Improve standardization of all response output
- Add sort fields to api documentation
- Add function that attempts to determine mime type via file header. Has added benefit of ensuring input file exists.
- Update swagger documentation

Media API

- Add ability to match images based on recordID UUID within PATCH call

added confirmation windows for project and editor delete icons (#1454)

[3.1] -burpsuite-fix: do not add taxa to query if there are special characters or quotes (#1436)

* do not add taxa to query if there are special characters or quotes

* cherry pick fix in 6250abd where getTaxonWhereFrag was getting un-cleaned taxon string

* respond to code review feedback

Occurrence download bug (#1478)

- searchvar term is being sanitized prior to being set as a session variable, which is converting & to &, thus interfering with parsing of all terms after the first term. Translating & to & fixes the issue.
- The cleanOutStr function santizes output using htmlspecialchars. The htmlspecialchars on line 638 will double sanitize output, thus converting "east & west" to "east & west".
- Variable is getting sanitized within index.php with htmlspecialchars twice. Once on line 12 and again on line 205

Addresses GH issue: #1470

subfunctionalize the chip button click to the handleRemoval method

only run getAssociatedTaxaSqlFragment when the relevant form field values are present in the query

fix the behavior of the chip removal

persist search terms

fix the non-persisting taxon type selection

remove var_dump

fix bug where association search is not playing well with taxon accordion

get the synonym searching to work

get the sql command for wider taxa to work but not to display successfully on the UI yet

remove spurious TODO and correct issue where usethes-associations and usethes were reversed

Fix bug where synonym choice is not being respected

Fix bug where synonym choice is not being respected

Add all reciprocal relationships to drop down menu and Exclude non-relevant controlled vocabulary from the association dropdown list

Accommodate no particular relationship in the search

Accommodate no particular taxon in the search

remove print statement

add none as an option to the association dropdown and handle its logic

remove cruft from classes/AssociationManager.php

clean up OccurrenceListManager

fix bug where family-based search was not working

make the taxstatus join only happen when family is selected from the dropdown

fix bug where there were duplicates in the relationship dropdown

respond to self code review

remove cruft

add a button to populate missing reverse relationships

make improvements to populating the missing reverse relationships and also add more ways to match the search results

remove reverse association section because the new entries in the database fixed that issue. This speeds up the search by a huge amount

add reverse search back in

get union statement to work, dramatically increasing speed of search

creates an inverse record if it can for each new association made and handles an error on that front more gracefully now

make pushing the button take the user back to the search page

relax uniqueness constraint on index UQ_omoccurassoc_sciname

relax uniqueness constraint on index UQ_omoccurassoc_sciname

fix bug where searching for taxon and then relationship separately was breaking

exclude reverse entries in omoccurassociations with basisOfRecord values of scriptGenerated from search results of getAssociationArr by default
  • Loading branch information
Atticus29 committed Aug 6, 2024
1 parent dbfdf05 commit c3e30b2
Show file tree
Hide file tree
Showing 11 changed files with 213 additions and 201 deletions.
150 changes: 120 additions & 30 deletions classes/AssociationManager.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

use function PHPUnit\Framework\isEmpty;

include_once($SERVER_ROOT.'/classes/OccurrenceTaxaManager.php');
include_once($SERVER_ROOT.'/classes/TaxonomyUtilities.php');

Expand All @@ -9,6 +11,7 @@ class AssociationManager extends OccurrenceTaxaManager{

function __construct(){
parent::__construct();
parent::__construct('write');
if($GLOBALS['USER_RIGHTS']){
if($GLOBALS['IS_ADMIN'] || array_key_exists("Taxonomy",$GLOBALS['USER_RIGHTS'])){
$this->isEditor = true;
Expand Down Expand Up @@ -44,35 +47,122 @@ public function getRelationshipTypes(){
}
}

public function getAssociatedRecords($associationArr){
$sql='';

if(array_key_exists('relationship', $associationArr) && $associationArr['relationship'] !== 'none'){
$familyJoinStr = '';
$shouldUseFamily = array_key_exists('associated-taxa', $associationArr) && $associationArr['associated-taxa'] == '3';
if($shouldUseFamily) $familyJoinStr = 'LEFT JOIN taxstatus ts ON o.tidinterpreted = ts.tid';


// "Forward" association
$relationshipType = (array_key_exists('relationship', $associationArr) && $associationArr['relationship'] !== 'any') ? $associationArr['relationship'] : 'IS NOT NULL';
$relationshipStr = (array_key_exists('relationship', $associationArr) && $associationArr['relationship'] !== 'any') ? ("='" . $relationshipType . "'") : (' IS NOT NULL');
// $sql .= "AND (o.occid IN (SELECT DISTINCT o.occid FROM omoccurrences o INNER JOIN omoccurassociations oa on o.occid=oa.occid WHERE oa.relationship" . $relationshipStr . " ";
$sql .= "AND (o.occid IN (SELECT DISTINCT o.occid FROM omoccurrences o INNER JOIN omoccurassociations oa on o.occid=oa.occid " . $familyJoinStr . " WHERE oa.relationship" . $relationshipStr . " ";
$sql .= $this->getAssociatedTaxonWhereFrag($associationArr) . ')';

// @TODO handle situation where the associationType is external and there's no occidAssociate; pull resourceUrl instead?
//something like:
// $sql =. 'SELECT DISTINCT resourceUrl from omoccurassociations WHERE associationType="externalOccurrence" AND occidAssociate IS NULL AND relationship = ' . $relationshipType . ' AND ';

// "Reverse" association
$reverseAssociationType = (array_key_exists('relationship', $associationArr) && $associationArr['relationship'] !== 'any') ? $this->getInverseRelationshipOf($relationshipType) : 'IS NOT NULL';
$reverseRelationshipStr = (array_key_exists('relationship', $associationArr) && $associationArr['relationship'] !== 'any') ? ("='" . $reverseAssociationType . "'") : (' IS NOT NULL');
$sql .= " OR o.occid IN (SELECT DISTINCT oa.occidAssociate FROM omoccurrences o INNER JOIN omoccurassociations oa on o.occid=oa.occid INNER JOIN omoccurdeterminations od ON oa.occid=od.occid " . $familyJoinStr . " where oa.relationship " . $reverseRelationshipStr . " "; //isCurrent="1" AND my thought was that we want these results to be as relaxed as possible
$sql .= $this->getAssociatedTaxonWhereFrag($associationArr) . ')';
public function establishInverseRelationshipRecords(){
$sql = "SELECT * FROM omoccurassociations where occid IS NOT NULL AND occidAssociate IS NOT NULL;";
if($statement = $this->conn->prepare($sql)){
$statement->execute();
$result = $statement->get_result();
while ($row = $result->fetch_assoc()) {
// $returnVal = $row['inverseRelationship'];
if(!$this->hasInverseRecord($row)){
$this->createInverseRecord($row);
}
// if that record has an inverse present, do nothing
// else, create an inverse record
}
$statement->close();
// return $returnVal;
}else{
return '';
}
}

private function hasInverseRecord($record){
// var_dump($record);
$sql = "SELECT * FROM omoccurassociations WHERE occidAssociate = ? AND occid = ? and relationship = ?;";
$recordOccid = array_key_exists('occid', $record) ? $record['occid'] : '';
$recordOccidAssociate = array_key_exists('occidAssociate', $record) ? $record['occidAssociate'] : '';
$relationship = array_key_exists('relationship', $record) ? $record['relationship'] : '';
$inverseRelationship = $this->getInverseRelationshipOf($relationship);
if($statement = $this->conn->prepare($sql)){
$statement->bind_param('iis', $recordOccid, $recordOccidAssociate, $inverseRelationship);
$statement->execute();
$result = $statement->get_result();
$returnVal = false;
if ($row = $result->fetch_assoc()) {
// $returnVal = $row['inverseRelationship'];
$returnVal = true;
}
$statement->close();
return $returnVal;
}else{
return '';
}
}
public function createInverseRecord($record){
$recordOccid = array_key_exists('occid', $record) ? $record['occid'] : '';
$recordOccidAssociate = array_key_exists('occidAssociate', $record) ? $record['occidAssociate'] : '';
$relationship = array_key_exists('relationship', $record) ? $record['relationship'] : '';
$inverseRelationship = $this->getInverseRelationshipOf($relationship);
$verbatimsciname = $this->getCorrespondingVerbatimsciname($recordOccid);
$createdUid = $GLOBALS['SYMB_UID'];
$basisOfRecord = 'scriptGenerated';
$sql = 'INSERT INTO omoccurassociations(occid, occidAssociate, relationship, basisOfRecord, createdUid, verbatimsciname)';
$sql .= ' VALUES(?,?,?,?,?,?);';
$returnVal = false;
// $this->resetConnection();
$shouldCreateInverseRecord = !empty($recordOccid) && !empty($recordOccidAssociate) && !empty($relationship) && !empty($inverseRelationship) && !empty($recordOccid);
if($shouldCreateInverseRecord && $statement = $this->conn->prepare($sql)){
$statement->bind_param('iissis', $recordOccidAssociate, $recordOccid, $inverseRelationship, $basisOfRecord, $createdUid, $verbatimsciname);
if($statement->execute()){
$returnVal = true;
}
$statement->close();
}
// $this->resetConnectionToRead();
return $returnVal;
}

private function getCorrespondingVerbatimsciname($targetOccid){
$sql = 'SELECT sciname from omoccurrences where occid=?';
$returnVal = '';
if($statement = $this->conn->prepare($sql)){
$statement->bind_param('s', $targetOccid);
$statement->execute();
$result = $statement->get_result();
if ($row = $result->fetch_assoc()) {
$returnVal = array_key_exists('sciname', $row) ? $row['sciname'] : '';
}
$statement->close();
}
return $sql;
return $returnVal;
}

// protected function resetConnection(){
// $this->conn = MySQLiConnectionFactory::getCon('write');
// }

// protected function resetConnectionToRead(){
// $this->conn = MySQLiConnectionFactory::getCon('readonly');
// }

public function getAssociatedRecords($associationArr) {
$sql = '';

if (array_key_exists('relationship', $associationArr) && $associationArr['relationship'] !== 'none') {
$familyJoinStr = '';
$shouldUseFamily = array_key_exists('associated-taxa', $associationArr) && $associationArr['associated-taxa'] == '3';
if ($shouldUseFamily) $familyJoinStr = 'LEFT JOIN taxstatus ts ON o.tidinterpreted = ts.tid';

// "Forward" association
$relationshipType = (array_key_exists('relationship', $associationArr) && $associationArr['relationship'] !== 'any') ? $associationArr['relationship'] : 'IS NOT NULL';
$relationshipStr = (array_key_exists('relationship', $associationArr) && $associationArr['relationship'] !== 'any') ? ("='" . $relationshipType . "'") : ' IS NOT NULL';

$forwardSql = "SELECT o.occid FROM omoccurrences o INNER JOIN omoccurassociations oa ON o.occid = oa.occid " . $familyJoinStr . " WHERE oa.relationship " . $relationshipStr . " ";
$forwardSql .= $this->getAssociatedTaxonWhereFrag($associationArr);

// "Reverse" association
$reverseAssociationType = (array_key_exists('relationship', $associationArr) && $associationArr['relationship'] !== 'any') ? $this->getInverseRelationshipOf($relationshipType) : 'IS NOT NULL';
$reverseRelationshipStr = (array_key_exists('relationship', $associationArr) && $associationArr['relationship'] !== 'any') ? ("='" . $reverseAssociationType . "'") : ' IS NOT NULL';

$reverseSql = "SELECT oa.occidAssociate FROM omoccurrences o INNER JOIN omoccurassociations oa ON o.occid = oa.occid INNER JOIN omoccurdeterminations od ON oa.occid = od.occid " . $familyJoinStr . " WHERE oa.relationship " . $reverseRelationshipStr . " ";
$reverseSql .= $this->getAssociatedTaxonWhereFrag($associationArr);

$sql .= "AND (o.occid IN (SELECT occid FROM ( " . $forwardSql . " UNION " . $reverseSql . " ) AS occids)";
}
return $sql;
}

public function getAssociatedTaxonWhereFrag($associationArr){
$sqlWhereTaxa = '';
if(isset($associationArr['taxa'])){
Expand Down Expand Up @@ -122,21 +212,21 @@ public function getAssociatedTaxonWhereFrag($associationArr){
//Return matches that are not linked to thesaurus
if($rankid > 179){
if($this->exactMatchOnly) $sqlWhereTaxa .= 'OR (o.sciname = "' . $term . '") ';
else $sqlWhereTaxa .= "OR (o.sciname LIKE '" . $term . "%') ";
else $sqlWhereTaxa .= "OR (o.sciname LIKE '" . $term . "%') OR (oa.verbatimsciname LIKE '" . $term . "%') ";
}
}
else{
//Protect against someone trying to download big pieces of the occurrence table through the user interface
if(strlen($term) < 4) $term .= ' ';
if($this->exactMatchOnly){
$sqlWhereTaxa .= 'OR (o.sciname = "' . $term . '") ';
$sqlWhereTaxa .= 'OR (o.sciname = "' . $term . '") OR (oa.verbatimsciname LIKE "' . $term . '%") ';
}
else{
$sqlWhereTaxa .= 'OR (o.sciname LIKE "' . $term . '%") ';
$sqlWhereTaxa .= 'OR (o.sciname LIKE "' . $term . '%") OR (oa.verbatimsciname LIKE "' . $term . '%") ';
if(!strpos($term,' _ ')){
//Accommodate for formats of hybrid designations within input and target data (e.g. x, multiplication sign, etc)
$term2 = preg_replace('/^([^\s]+\s{1})/', '$1 _ ', $term);
$sqlWhereTaxa .= "OR (o.sciname LIKE '" . $term2 . "%') ";
$sqlWhereTaxa .= "OR (o.sciname LIKE '" . $term2 . "%') OR (oa.verbatimsciname LIKE '" . $term . "%') ";
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions classes/OccurrenceListManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public function getSpecimenMap($pageRequest,$cntPerPage){
$pageRequest = ($pageRequest - 1)*$cntPerPage;
}
$sql .= ' LIMIT ' . $pageRequest . ',' . $cntPerPage;
// echo '<div>Spec sql: ' . $sql . '</div>'; exit;
// echo '<div>Spec sql: ' . $sql . '</div>'; exit; // @TODO HERE
$result = $this->conn->query($sql);
if($result){
$securityCollArr = array();
Expand Down Expand Up @@ -125,7 +125,7 @@ private function setImages($occArr,&$retArr){
private function setRecordCnt($sqlWhere){
if($sqlWhere){
$sql = "SELECT COUNT(DISTINCT o.occid) AS cnt FROM omoccurrences o ".$this->getTableJoins($sqlWhere).$sqlWhere;
// echo "<div>Count sql: ".$sql."</div>";
// echo "<div>Count sql: ".$sql."</div>"; exit;
$result = $this->conn->query($sql);
if($result){
if($row = $result->fetch_object()){
Expand Down
18 changes: 2 additions & 16 deletions classes/OccurrenceManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ class OccurrenceManager extends OccurrenceTaxaManager {
public function __construct($type='readonly'){
parent::__construct($type);
if(array_key_exists('reset',$_REQUEST) && $_REQUEST['reset']) $this->reset();
$this->associationManager = new AssociationManager();
$this->readRequestVariables();
$langTag = '';
if(!empty($GLOBALS['LANG_TAG'])) $langTag = $GLOBALS['LANG_TAG'];
if($langTag != 'en' && file_exists($GLOBALS['SERVER_ROOT'] . '/content/lang/classes/OccurrenceManager.' . $langTag . '.php'))
include_once($GLOBALS['SERVER_ROOT'] . '/content/lang/classes/OccurrenceManager.' . $langTag . '.php');
else include_once($GLOBALS['SERVER_ROOT'] . '/content/lang/classes/OccurrenceManager.en.php');
$this->LANG = $LANG;
$this->associationManager = new AssociationManager();
}

public function __destruct(){
Expand Down Expand Up @@ -60,15 +60,11 @@ public function getSqlWhere(){

protected function setSqlWhere(){
$sqlWhere = '';
// var_dump($this->searchTermArr);
// $deleteMe = $this->associationManager->getAssociatedTaxonWhereFrag($this->associationArr);
// var_dump($deleteMe);
if(array_key_exists("targetclid",$this->searchTermArr) && is_numeric($this->searchTermArr["targetclid"])){
if(!$this->voucherManager){
$this->setChecklistVariables($this->searchTermArr['targetclid']);
}
$voucherVariableArr = $this->voucherManager->getQueryVariableArr();
// var_dump($voucherVariableArr);
if($voucherVariableArr){
if(isset($voucherVariableArr['association-type'])) $this->searchTermArr['association-type'] = $voucherVariableArr['association-type'];
if(isset($voucherVariableArr['taxontype-association'])) $this->searchTermArr['taxontype-association'] = $voucherVariableArr['taxontype-association'];
Expand Down Expand Up @@ -129,11 +125,8 @@ protected function setSqlWhere(){
$this->displaySearchArr[] = $this->LANG['DATASETS'] . ': ' . $this->getDatasetTitle($this->searchTermArr['datasetid']);
}
$sqlWhere .= $this->getTaxonWhereFrag();
// echo "<div>this dot getTaxonWhereFrag() is: " . $this->getTaxonWhereFrag() . "</div>";
// echo "<div>sqlWhere before getting the association taxa is: " . $sqlWhere . "</div>";
$hasValidRelationship = isset(($this->associationArr['relationship'])) && $this->associationArr['relationship']!=='none';
// $hasValidAssociatedTaxon = isset($this->associationArr['search']);
if($hasValidRelationship){ // || $hasValidAssociatedTaxon // @TODO
if($hasValidRelationship){
$sqlWhere = substr_replace($sqlWhere,'',-1);
$sqlWhere .= $this->associationManager->getAssociatedRecords($this->associationArr) . ')';
}
Expand Down Expand Up @@ -547,7 +540,6 @@ protected function getTableJoins($sqlWhere){
$sqlJoin .= 'INNER JOIN taxaenumtree e ON o.tidinterpreted = e.tid ';
}
if(strpos($sqlWhere,'ts.family')){
echo 'ts.family entered';
$sqlJoin .= 'LEFT JOIN taxstatus ts ON o.tidinterpreted = ts.tid ';
}
if(strpos($sqlWhere,'ds.datasetid')){
Expand Down Expand Up @@ -668,7 +660,6 @@ public function getQueryTermStr(){
$retStr .= '&taxontype=1';
}
}
// var_dump($this->associationArr);
$patternOfOnlyLettersDigitsAndSpaces = '/^[a-zA-Z0-9\s\-]*$/'; // TOOD accommodate symbols associated with extinct taxa, hybrid crosses, and abbreviations with periods, e.g. "var."?
if(isset($this->associationArr['search'])){
if (preg_match($patternOfOnlyLettersDigitsAndSpaces, $this->associationArr['search'])==1) {
Expand Down Expand Up @@ -715,11 +706,9 @@ private function getDatasetTitle($dsIdStr){
}

protected function readRequestVariables(){
// var_dump($_REQUEST); // @TODO this is an intervention point
if(array_key_exists('searchvar',$_REQUEST)){
$parsedArr = array();
$taxaArr = array();
// parse_str($_REQUEST['searchvar'], $parsedArr);
$searchVar = str_replace('&amp;', '&', $_REQUEST['searchvar']);
parse_str($searchVar, $parsedArr);

Expand All @@ -735,7 +724,6 @@ protected function readRequestVariables(){
unset($parsedArr['taxontype']);
}
$this->setTaxonRequestVariable($taxaArr);
// $this->setAssociationRequestVariable();
}
foreach($parsedArr as $k => $v){
$k = $this->cleanInputStr($k);
Expand Down Expand Up @@ -776,7 +764,6 @@ protected function readRequestVariables(){
if($hasEverythingRequiredForAssociationSearch){
$this->setAssociationRequestVariable();
}
// var_dump($this->searchTermArr);
if(array_key_exists('country',$_REQUEST)){
$country = $this->cleanInputStr($_REQUEST['country']);
if($country){
Expand Down Expand Up @@ -1010,7 +997,6 @@ protected function readRequestVariables(){
$this->searchTermArr['footprintwkt'] = $this->cleanInputStr($_REQUEST['footprintwkt']);
}

// var_dump($this->searchTermArr);
}

private function setChecklistVariables($clid){
Expand Down
Loading

0 comments on commit c3e30b2

Please sign in to comment.