diff --git a/CHANGELOG.md b/CHANGELOG.md
index e5cc485f68..c3da3d4c6c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,38 @@
+# Version 9.4.0
+*2024-09-23*
+
+* Adds features to trigger reports that allow them to directly send emails, e.g. for replying to
+ partially complete reports of Asian Hornets.
+* Adds fields to the `groups` entity (for iRecord Activities) for the following:
+ * Controlling if blogs are enabled and, if so, whether any member can post a blog or only admins.
+ See https://github.com/BiologicalRecordsCentre/iRecord/issues/1703.
+ * Defining if a group is a container for other groups, e.g. a project divided by years. See
+ https://github.com/BiologicalRecordsCentre/iRecord/issues/1678.
+ * Defining if a group is contained by another group.
+* Update the `library/groups/find_group_by_url` report to include information about container and
+ contained groups. See https://github.com/BiologicalRecordsCentre/iRecord/issues/1678.
+* Bugfixes for the new bulk edit tool. See https://github.com/BiologicalRecordsCentre/iRecord/issues/1673.
+* Updated download field formats to support sensitive record download requirements. See
+ https://github.com/BiologicalRecordsCentre/iRecord/issues/1714 and the columns documentation at
+ https://indicia-docs.readthedocs.io/en/latest/site-building/iform/helpers/elasticsearch-report-helper.html#elasticsearchreporthelper-datagrid.
+* Bugfix for the handling of the current common name when AddSynonym operations are processed by
+ the UKSI History processor. See https://github.com/Indicia-Team/warehouse/pull/522.
+* Update the `library/groups/group_list` report to include a full text search parameter and also so
+ that setting the parameter `userFilterMode=joinable` excludes groups you are already a member of.
+* The Elasticsearch special field handler for "sitename" now supports additional options -
+ `obscureifsensitive` - shows a warning message instead of the site name if sensitive and
+ `showifsensitive` - displays the full site name for sensitive records (only if the user has
+ access to full precision sensitive data).
+* In Elasticsearch, sensitive or private records have their site names replaced by '!' to
+ distinguish them from records where there is no site name given. See
+ https://github.com/BiologicalRecordsCentre/iRecord/issues/1714.
+
+# Version 9.3.0
+*2024-08-19*
+
+* Adds `search_code` to parameters of Rest endpoint, `services/rest/taxa/search`
+ and includes it in the response.
+
# Version 9.2.0
*2024-06-17*
diff --git a/README.md b/README.md
index 706486ea10..d1bf8075bb 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Indicia Warehouse [![Build Status](https://travis-ci.com/Indicia-Team/warehouse.svg?branch=master)](https://travis-ci.com/Indicia-Team/warehouse)
+# Indicia Warehouse
This is the repository for the Indicia Warehouse, the server-side component of Indicia, the online wildlife recording
toolkit. Indicia accelerates development of wildlife recording websites and mobile applications. Documentation is
diff --git a/application/config/version.php b/application/config/version.php
index 50e1759d4a..827dd78688 100644
--- a/application/config/version.php
+++ b/application/config/version.php
@@ -29,14 +29,14 @@
*
* @var string
*/
-$config['version'] = '9.3.3';
+$config['version'] = '9.4.0';
/**
* Version release date.
*
* @var string
*/
-$config['release_date'] = '2024-09-02';
+$config['release_date'] = '2024-09-25';
/**
* Link to the code repository downloads page.
diff --git a/application/controllers/attribute_by_survey.php b/application/controllers/attribute_by_survey.php
index 6245dc03fb..8ecc2b1af3 100644
--- a/application/controllers/attribute_by_survey.php
+++ b/application/controllers/attribute_by_survey.php
@@ -20,7 +20,7 @@
*/
/**
- * Controller providing the ability to configure the list of attributes joined to a survey.
+ * Controller for relationship between custom attributes and surveys.
*/
class Attribute_By_Survey_Controller extends Indicia_Controller {
private $_survey = NULL;
@@ -83,8 +83,10 @@ public function edit($id) {
}
/**
- * Handle the layout_update action, which uses $_POST data to find a list of commands
- * for re-ordering the controls.
+ * Handle the layout_update action.
+ *
+ * Which uses $_POST data to find a list of commands for re-ordering the
+ * controls.
*/
public function layout_update() {
// Get the survey id from the segments in the URI.
@@ -223,8 +225,13 @@ protected function editViewName() {
}
/**
- * Setup the values to be loaded into the edit view. For this class, we need to explode the
- * items out of the validation_rules field, which our base class can do.
+ * Setup the values to be loaded into the edit view.
+ *
+ * For this class, we need to explode the items out of the validation_rules
+ * field, which our base class can do.
+ *
+ * @return array
+ * Key value pairs of data for the edit view.
*/
protected function getModelValues() {
$r = parent::getModelValues();
@@ -308,8 +315,12 @@ protected function prepareOtherViewData(array $values) {
return $otherData;
}
+ /**
+ * Override save handler to format validation rules correctly.
+ */
public function save() {
- // Build the validation_rules field from the set of controls that are associated with it.
+ // Build the validation_rules field from the set of controls that are
+ // associated with it.
$rules = [];
$ruleNames = ([
'required',
@@ -340,12 +351,20 @@ public function save() {
parent::save();
}
+ /**
+ * Determine the page to return to after saving.
+ *
+ * @return string
+ * Path to the page to return to.
+ */
protected function get_return_page() {
- $surveyPostKey = $this->type.'_attributes_website:restrict_to_survey_id';
+ $surveyPostKey = $this->type . '_attributes_website:restrict_to_survey_id';
if (isset($_POST[$surveyPostKey])) {
- return 'attribute_by_survey/'.$_POST[$surveyPostKey].'?type='.$this->type;
- } else {
- // If $_POST data not available, then just return to the survey list. Shouldn't really happen.
+ return 'attribute_by_survey/' . $_POST[$surveyPostKey] . '?type=' . $this->type;
+ }
+ else {
+ // If $_POST data not available, then just return to the survey list.
+ // Shouldn't really happen.
return 'survey';
}
}
@@ -356,7 +375,8 @@ protected function get_return_page() {
protected function defineEditBreadcrumbs() {
$this->page_breadcrumbs[] = html::anchor('survey', 'Survey datasets');
$survey = ORM::Factory('survey', $this->model->restrict_to_survey_id);
- $this->page_breadcrumbs[] = html::anchor('/attribute_by_survey/'.$this->model->restrict_to_survey_id.'?type='.$this->type, 'Attributes for '.$survey->title);
+ $this->page_breadcrumbs[] = html::anchor(
+ "/attribute_by_survey/$survey->id?type=$this->type", "Attributes for $survey->title");
$this->page_breadcrumbs[] = $this->model->caption();
}
@@ -371,8 +391,10 @@ protected function page_authorised() {
$this->_website_id = $survey->website_id;
}
return in_array($this->_website_id, $this->auth_filter['values']);
- } else
+ }
+ else {
return true;
+ }
}
/**
@@ -385,5 +407,4 @@ protected function getSurvey() {
return $this->_survey;
}
-
-}
\ No newline at end of file
+}
diff --git a/application/controllers/forgotten_password.php b/application/controllers/forgotten_password.php
index f3975c6fda..55367b710a 100644
--- a/application/controllers/forgotten_password.php
+++ b/application/controllers/forgotten_password.php
@@ -26,6 +26,9 @@
*/
class Forgotten_Password_Controller extends Indicia_Controller {
+ /**
+ * Set up the main controller page.
+ */
public function index() {
if ($this->auth->logged_in()) {
@@ -59,10 +62,25 @@ public function index() {
}
}
+ /**
+ * Return true if the user can login to the warehouse.
+ *
+ * User's only have login rughts if they have site editor role or higher, or
+ * core admin.
+ *
+ * @param ORM $user
+ * User object.
+ *
+ * @return bool
+ * True if the user is allowed to login.
+ */
public function check_can_login($user) {
- if (is_null($user->core_role_id) && ORM::factory('users_website')
- ->where('user_id', $user->id)->where('site_role_id IS NOT ', NULL)->find_all() === 0) {
- $this->template->content->error_message = $_POST['UserID'] . ' does not have permission to log on to this website';
+ if (is_null($user->core_role_id)
+ && ORM::factory('users_website')
+ ->where('user_id', $user->id)
+ ->where('site_role_id IS NOT ', NULL)
+ ->find_all() === 0) {
+ $this->template->content->error_message = "$_POST[UserID] does not have permission to log on to this website";
return FALSE;
}
return TRUE;
diff --git a/application/controllers/location_comment.php b/application/controllers/location_comment.php
index 051526fabd..eb1cd9cf14 100644
--- a/application/controllers/location_comment.php
+++ b/application/controllers/location_comment.php
@@ -65,6 +65,9 @@ protected function getDefaults() {
*
* After saving a comment you are returned to the location entry which has
* the comment.
+ *
+ * @return string
+ * Page path to return to.
*/
protected function get_return_page() {
if (array_key_exists('location_comment:location_id', $_POST)) {
diff --git a/application/controllers/logout.php b/application/controllers/logout.php
index 91f4f6d3fa..d9a7e0a1b4 100644
--- a/application/controllers/logout.php
+++ b/application/controllers/logout.php
@@ -19,19 +19,17 @@
* @link https://github.com/indicia-team/warehouse
*/
-/*
+/**
* Provides application support for logging users out.
*/
class Logout_Controller extends Indicia_Controller {
- /*
- * description: Logs the current user out of the application. Destroys the current session
- * parameters: None expected.
- */
- public function index()
- {
+ /**
+ * Logs the current user out of the application. Destroys the current session.
+ */
+ public function index() {
$this->auth->logout(TRUE);
url::redirect();
}
-}
\ No newline at end of file
+}
diff --git a/application/controllers/scheduled_tasks.php b/application/controllers/scheduled_tasks.php
index 0fa0d884b2..a4b4961ae4 100644
--- a/application/controllers/scheduled_tasks.php
+++ b/application/controllers/scheduled_tasks.php
@@ -149,7 +149,7 @@ protected function checkTriggers() {
$params = json_decode($trigger->params_json, TRUE);
$reportEngine = new ReportEngine();
// Get parameter for last run specific to this trigger.
- $params['date'] = variable::get("trigger_last_run-$trigger->id", $this->lastRunDate);
+ $params['date'] = variable::get("trigger_last_run-$trigger->id", $this->lastRunDate, FALSE);
$currentTime = time();
try {
$data = $reportEngine->requestReport($trigger->trigger_template_file . '.xml', 'local', 'xml', $params);
@@ -239,6 +239,13 @@ protected function checkTriggers() {
],
$digestMode
);
+ $this->doTriggerImmediateEmails(
+ $trigger->name,
+ [
+ 'headings' => $parsedData['headingData'],
+ 'data' => $parsedData['websiteRecordData'],
+ ]
+ );
}
// Remember when this specific trigger last ran.
variable::set("trigger_last_run-$trigger->id", date('c', $currentTime));
@@ -354,6 +361,101 @@ private function doDirectTriggerNotifications($triggerName, array $data, $digest
}
}
+ /**
+ * Send emails for trigger reports that directly define an email to send.
+ *
+ * These emails can be sent directly, bypassing the notifications system and
+ * therefore can be sent to anyone not just registered users. E.g. a thank
+ * you email to anonymous recorders.
+ *
+ * @param string $triggerName
+ * Name of the trigger which fired.
+ * @param array $data
+ * Info regarding the trigger report columns and associated retrieved data.
+ */
+ private function doTriggerImmediateEmails($triggerName, array $data) {
+ if (count($data['data']) === 0 || !in_array('email_to', $data['headings'])) {
+ return;
+ }
+ $emailConfig = Kohana::config('email');
+ if (!isset($emailConfig['address'])) {
+ self::msg('Email address not provided in email configuration', 'error');
+ return;
+ }
+ $colIndexes = [];
+ $sysCols = ['email_to', 'email_subject', 'email_body', 'email_name'];
+ foreach ($sysCols as $col) {
+ if (($colIdx = array_search($col, $data['headings'])) !== FALSE) {
+ $colIndexes[$col] = $colIdx;
+ }
+ }
+ $defaultSubject = empty(kohana::config('email.notification_subject'))
+ ? kohana::lang('misc.notification_subject')
+ : kohana::config('email.notification_subject');
+ $emails = [];
+ foreach ($data['data'] as $records) {
+ foreach ($records as $record) {
+ if (!empty($record[$colIndexes['email_to']])) {
+ $to = $record[$colIndexes['email_to']];
+ $name = empty($colIndexes['email_name']) || empty($record[$colIndexes['email_name']])
+ ? $to
+ : $record[$colIndexes['email_name']];
+ if (!isset($emails["$to $name"])) {
+ $emails["$to $name"] = [];
+ }
+
+ $subject = empty($colIndexes['email_subject']) || empty($record[$colIndexes['email_subject']])
+ ? $defaultSubject
+ : $record[$colIndexes['email_subject']];
+ if (empty($colIndexes['email_body']) || empty($record[$colIndexes['email_body']])) {
+ // Email body not provided, so construct it from the other columns.
+ $body = '';
+ foreach ($data['headings'] as $idx => $colTitle) {
+ // Skip the functional email columns.
+ if (!in_array($colTitle, $sysCols)) {
+ $body[] = '
' . htmlspecialchars($colTitle) . '
';
+ $body[] = '
' . htmlspecialchars($record[$idx]) . '
';
+ }
+ }
+ }
+ else {
+ $body = $record[$colIndexes['email_body']];
+ }
+ // Aggregate emails per email address, so we don't send multiple.
+ $emails["$to $name"][] = [
+ 'to' => $to,
+ 'name' => $name,
+ 'subject' => $subject,
+ 'body' => $body,
+ ];
+ }
+ }
+ }
+ $swift = email::connect();
+ // Now send the emails as a digest so each recipient only gets one email.
+ foreach ($emails as $infoList) {
+ // If a single email for this recipient we can use the subject, otherwise
+ // we use a generic subject and put each email subject in as a subtitle.
+ $subject = count($infoList) === 1 ? $infoList[0]['subject'] : $defaultSubject;
+ $emailContent = '';
+ foreach ($infoList as $infoItem) {
+ if (count($infoList) > 1) {
+ $emailContent .= '
' . htmlspecialchars($infoItem['subject']) . '
';
+ }
+ $emailContent .= '
' . $infoItem['body'] . '
';
+ }
+ $message = new Swift_Message(
+ $subject,
+ "$emailContent",
+ 'text/html'
+ );
+ $recipients = new Swift_RecipientList();
+ $recipients->addTo($infoList[0]['to'], $infoList[0]['name']);
+ // Send the email.
+ $swift->send($message, $recipients, $emailConfig['address']);
+ }
+ }
+
/**
* Create email digests for trigger notifications.
*
diff --git a/application/controllers/taxa_taxon_list.php b/application/controllers/taxa_taxon_list.php
index 660ac1e7f6..c3ba36928e 100644
--- a/application/controllers/taxa_taxon_list.php
+++ b/application/controllers/taxa_taxon_list.php
@@ -31,9 +31,9 @@ public function __construct() {
$this->columns = [
'taxon' => '',
'authority' => '',
- 'taxon_group' => 'Taxon Group',
+ 'taxon_group' => 'Taxon group',
'language' => '',
- 'taxonomic_sort_order' => 'Sort Order',
+ 'taxonomic_sort_order' => 'Sort order',
];
$this->pagetitle = "Species";
}
diff --git a/application/i18n/en_GB/form_error_messages.php b/application/i18n/en_GB/form_error_messages.php
index 4976e624ff..68cd954abb 100644
--- a/application/i18n/en_GB/form_error_messages.php
+++ b/application/i18n/en_GB/form_error_messages.php
@@ -75,7 +75,7 @@
'default' => 'Invalid input.',
],
'centroid_sref_system' => [
- 'required' => 'The centorid spatial reference system must be supplied.',
+ 'required' => 'The centroid spatial reference system must be supplied.',
'default' => 'Invalid input.',
],
'external_key' => [
diff --git a/application/models/group.php b/application/models/group.php
index 930537752b..dd60bf9f77 100644
--- a/application/models/group.php
+++ b/application/models/group.php
@@ -46,6 +46,8 @@ public function validate(Validation $array, $save = FALSE) {
$array->add_rules('group_type_id', 'required');
$array->add_rules('website_id', 'required');
$array->add_rules('code', 'length[1,20]');
+ $array->add_rules('post_blog_permission', 'regex[/^(A|M)$/]');
+ $array->add_rules('contained_by_group_id', 'integer');
$this->unvalidatedFields = [
'code',
'description',
@@ -59,6 +61,7 @@ public function validate(Validation $array, $save = FALSE) {
'view_full_precision',
'logo_path',
'licence_id',
+ 'container',
];
// Has the private records flag changed?
$this->wantToUpdateReleaseStatus = isset($this->submission['fields']['private_records']) &&
diff --git a/application/models/notification.php b/application/models/notification.php
index facf1a8c6e..04c2e0aadf 100644
--- a/application/models/notification.php
+++ b/application/models/notification.php
@@ -35,13 +35,13 @@ public function validate(Validation $array, $save = FALSE) {
// fields before validation.
$array->pre_filter('trim');
$array->add_rules('source', 'required');
- $array->add_rules('source_type', 'required');
+ $array->add_rules('source_type', 'required', 'regex[/^(T|V|C|Q|A|S|VT|M|PT|GU|FE)$/]');
+ $array->add_rules('digest_mode', 'regex[/^[NDWI]$/]');
$array->add_rules('data', 'required');
$array->add_rules('acknowledged', 'required');
$array->add_rules('user_id', 'required');
$array->add_rules('triggered_on', 'required');
$this->unvalidatedFields = [
- 'digest_mode',
'cc',
'linked_id',
];
diff --git a/application/views/termlists_term/termlists_term_edit.php b/application/views/termlists_term/termlists_term_edit.php
index 211abcc531..612c7dca49 100644
--- a/application/views/termlists_term/termlists_term_edit.php
+++ b/application/views/termlists_term/termlists_term_edit.php
@@ -56,7 +56,7 @@
'label' => 'Code',
'fieldname' => 'term:code',
'default' => html::initial_value($values, 'term:code'),
- 'helpText' => 'A code or other reference number associatd with the term.',
+ 'helpText' => 'A code or other reference number associated with the term.',
]);
echo data_entry_helper::textarea([
'label' => 'Description',
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 67573f4872..43635ea7fb 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -58,8 +58,8 @@ services:
# A fake mail server which allows the warehouse to send emails.
# Received mail can be viewed at http://localhost:8025
- mailhog:
- image: mailhog/mailhog
+ mailpit:
+ image: axllent/mailpit
ports:
- "8025:8025"
@@ -95,7 +95,7 @@ services:
depends_on:
postgres:
condition: service_healthy
- mailhog:
+ mailpit:
condition: service_started
elastic:
condition: service_healthy
diff --git a/docker/warehouse/Dockerfile b/docker/warehouse/Dockerfile
index 2a14cf518f..eecff0644e 100644
--- a/docker/warehouse/Dockerfile
+++ b/docker/warehouse/Dockerfile
@@ -1,6 +1,6 @@
-# This image contains Debian's Apache httpd in conjunction with PHP8.1.
+# This image contains Debian's Apache httpd in conjunction with PHP.
# https://hub.docker.com/_/php
-FROM php:8.1-apache
+FROM php:8.2-apache
# Use PHP development configuration file as a basis
RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini"
diff --git a/docker/warehouse/config/email.php b/docker/warehouse/config/email.php
index 1b8d595635..4ce5bb7c00 100644
--- a/docker/warehouse/config/email.php
+++ b/docker/warehouse/config/email.php
@@ -42,7 +42,7 @@
* @param array smtp: hostname, (username), (password), (port), (auth), (encryption)
*/
$config['options'] = [
- 'hostname' => 'mailhog',
+ 'hostname' => 'mailpit',
'username' => '',
'password' => '',
'port' => '1025',
diff --git a/modules/indicia_setup/db/version_9_3_0/202408051352_groups_fields.sql b/modules/indicia_setup/db/version_9_3_0/202408051352_groups_fields.sql
new file mode 100644
index 0000000000..310b9f3191
--- /dev/null
+++ b/modules/indicia_setup/db/version_9_3_0/202408051352_groups_fields.sql
@@ -0,0 +1,5 @@
+ALTER TABLE groups
+ ADD COLUMN post_blog_permission character,
+ ADD CONSTRAINT groups_blog_post_permisison_check CHECK (post_blog_permission::text = ANY (ARRAY[NULL::bpchar, 'A'::bpchar, 'M'::bpchar]::text[]));
+
+COMMENT ON COLUMN groups.post_blog_permission IS 'Requirements for posting blog entries for the group. Can be "A" - group admins can post, "M" - any group member can post, or NULL if blogs are disabled.';
\ No newline at end of file
diff --git a/modules/indicia_setup/db/version_9_3_0/202408061314_groups_views.sql b/modules/indicia_setup/db/version_9_3_0/202408061314_groups_views.sql
new file mode 100644
index 0000000000..1b424a4a6f
--- /dev/null
+++ b/modules/indicia_setup/db/version_9_3_0/202408061314_groups_views.sql
@@ -0,0 +1,55 @@
+DROP VIEW list_groups;
+CREATE VIEW list_groups AS
+ SELECT g.id,
+ g.title,
+ g.code,
+ g.group_type_id,
+ g.description,
+ g.from_date,
+ g.to_date,
+ g.private_records,
+ g.website_id,
+ g.filter_id,
+ g.joining_method,
+ g.logo_path,
+ g.implicit_record_inclusion,
+ g.licence_id,
+ g.view_full_precision,
+ g.post_blog_permission
+ FROM groups g
+ WHERE g.deleted = false;
+
+DROP VIEW detail_groups;
+CREATE VIEW detail_groups AS
+ SELECT g.id,
+ g.title,
+ g.code,
+ g.group_type_id,
+ g.description,
+ g.from_date,
+ g.to_date,
+ g.private_records,
+ g.website_id,
+ g.joining_method,
+ g.filter_id,
+ f.definition AS filter_definition,
+ g.created_by_id,
+ c.username AS created_by,
+ g.updated_by_id,
+ u.username AS updated_by,
+ g.logo_path,
+ CASE
+ WHEN g.joining_method='P' OR g.joining_method='R' THEN btrim(regexp_replace(regexp_replace(lower(g.title::text), '[ ]'::text, '-'::text, 'g'::text), '[^a-z0-9\-]'::text, ''::text, 'g'::text), '-'::text)
+ ELSE NULL::text
+ END AS url_safe_title,
+ g.implicit_record_inclusion,
+ g.licence_id,
+ l.code AS licence_code,
+ g.view_full_precision,
+ g.post_blog_permission
+ FROM groups g
+ LEFT JOIN filters f ON f.id = g.filter_id AND f.deleted = false
+ LEFT JOIN licences l ON l.id = g.licence_id AND l.deleted = false
+ JOIN users c ON c.id = g.created_by_id
+ JOIN users u ON u.id = g.updated_by_id
+ WHERE g.deleted = false;
diff --git a/modules/indicia_setup/db/version_9_4_0/202409171807_group_containers.sql b/modules/indicia_setup/db/version_9_4_0/202409171807_group_containers.sql
new file mode 100644
index 0000000000..2a918d597a
--- /dev/null
+++ b/modules/indicia_setup/db/version_9_4_0/202409171807_group_containers.sql
@@ -0,0 +1,10 @@
+ALTER TABLE groups
+ ADD COLUMN container BOOLEAN DEFAULT false,
+ ADD COLUMN contained_by_group_id INTEGER,
+ ADD CONSTRAINT fk_groups_contained_by_group_id FOREIGN KEY (contained_by_group_id)
+ REFERENCES groups (id);
+
+COMMENT ON COLUMN groups.container IS 'Is the group a container for other sub-groups? Changes the behaviour of the group as it''s purpose is as an organisational tool for the contained groups, so only has administrator members.';
+COMMENT ON COLUMN groups.contained_by_group_id IS 'If the group is contained by another group, points to the container. Inherits filtering and admin members from the parent group.';
+
+CREATE INDEX fki_groups_contained_by_group_id ON groups(contained_by_group_id);
\ No newline at end of file
diff --git a/modules/indicia_setup/db/version_9_4_0/202409171808_group_container_views.sql b/modules/indicia_setup/db/version_9_4_0/202409171808_group_container_views.sql
new file mode 100644
index 0000000000..34e25bd17a
--- /dev/null
+++ b/modules/indicia_setup/db/version_9_4_0/202409171808_group_container_views.sql
@@ -0,0 +1,57 @@
+CREATE OR REPLACE VIEW list_groups AS
+ SELECT g.id,
+ g.title,
+ g.code,
+ g.group_type_id,
+ g.description,
+ g.from_date,
+ g.to_date,
+ g.private_records,
+ g.website_id,
+ g.filter_id,
+ g.joining_method,
+ g.logo_path,
+ g.implicit_record_inclusion,
+ g.licence_id,
+ g.view_full_precision,
+ g.post_blog_permission,
+ g.container,
+ g.contained_by_group_id
+ FROM groups g
+ WHERE g.deleted = false;
+
+CREATE OR REPLACE VIEW detail_groups AS
+ SELECT g.id,
+ g.title,
+ g.code,
+ g.group_type_id,
+ g.description,
+ g.from_date,
+ g.to_date,
+ g.private_records,
+ g.website_id,
+ g.joining_method,
+ g.filter_id,
+ f.definition AS filter_definition,
+ g.created_by_id,
+ c.username AS created_by,
+ g.updated_by_id,
+ u.username AS updated_by,
+ g.logo_path,
+ CASE
+ WHEN g.joining_method='P' OR g.joining_method='R' THEN btrim(regexp_replace(regexp_replace(lower(g.title::text), '[ ]'::text, '-'::text, 'g'::text), '[^a-z0-9\-]'::text, ''::text, 'g'::text), '-'::text)
+ ELSE NULL::text
+ END AS url_safe_title,
+ g.implicit_record_inclusion,
+ g.licence_id,
+ l.code AS licence_code,
+ g.view_full_precision,
+ g.post_blog_permission,
+ g.container,
+ g.contained_by_group_id
+ FROM groups g
+ LEFT JOIN filters f ON f.id = g.filter_id AND f.deleted = false
+ LEFT JOIN licences l ON l.id = g.licence_id AND l.deleted = false
+ JOIN users c ON c.id = g.created_by_id
+ JOIN users u ON u.id = g.updated_by_id
+ WHERE g.deleted = false;
\ No newline at end of file
diff --git a/modules/indicia_svc_data/controllers/services/data_utils.php b/modules/indicia_svc_data/controllers/services/data_utils.php
index 519c5e58e4..c1c7247e67 100644
--- a/modules/indicia_svc_data/controllers/services/data_utils.php
+++ b/modules/indicia_svc_data/controllers/services/data_utils.php
@@ -1134,7 +1134,6 @@ public function bulk_edit() {
return;
}
$db = new Database();
- // @todo think through behaviour in parent/child sample data.
$results = $this->checkAffectedSamplesDontContainOtherOccurrences($db, $occurrenceIds);
if ($results) {
if (!empty($options->allowSampleSplits)) {
@@ -1157,39 +1156,59 @@ public function bulk_edit() {
$this->fail('Unauthorized', 404, 'You cannot edit samples belonging to other users.');
return FALSE;
}
- $sampleFieldUpdates = [];
- if (!empty($updates->date)) {
- $sampleFieldUpdates[] = "date_start='$updates->date'";
- $sampleFieldUpdates[] = "date_end='$updates->date'";
- $sampleFieldUpdates[] = "date_type='D'";
- }
- if (!empty($updates->location_name)) {
- $locationName = pg_escape_literal($db->getLink(), $updates->location_name);
- $sampleFieldUpdates[] = "location_name=$locationName";
- }
- if (!empty($updates->sref)) {
- $sref = spatial_ref::sref_format_tidy($updates->sref, $updates->sref_system);
- $sampleFieldUpdates[] = "entered_sref=$sref";
- $sampleFieldUpdates[] = "entered_sref_system=$updates->sref_system";
- $geom = "st_geomfromtext('" . spatial_ref::sref_to_internal_wkt($updates->sref, $updates->sref_system) . "', 900913)";
- $sampleFieldUpdates[] = "geom=$geom";
- }
- if (!empty($sampleFieldUpdates)) {
- $sampleFieldUpdateSql = implode(',', $sampleFieldUpdates);
- $qry = <<user_id
- WHERE id in ($sampleIds)
- AND created_by_id=$this->user_id;
+ $sampleFieldUpdates = $this->getSampleFieldUpdates($db, $updates);
+ $sampleFieldUpdateSql = empty($sampleFieldUpdates) ? '' : implode(',', $sampleFieldUpdates) . ', ';
+ $sampleFieldChangedCheckSql = empty($sampleFieldUpdates) ? 'false' : 'NOT (s.' . implode(' AND s.', $sampleFieldUpdates) . ')';
+ $recorderNameFieldChangedCheckSql = empty($updates->recorder_name) ? '' : "OR snf.recorders<>'$updates->recorder_name'";
+ $langRecheck = pg_escape_literal($db->getLink(), kohana::lang('misc.recheck_verification'));
+ $qry = <<user_id;
+
+ UPDATE samples s
+ SET $sampleFieldUpdateSql
+ updated_on=now(),
+ updated_by_id=$this->user_id,
+ record_status='C',
+ verified_by_id=null,
+ verified_on=null
+ FROM changing_samples cs
+ WHERE cs.id=s.id;
+
+ -- Also reset verification status on changed occurrences.
+ INSERT INTO occurrence_comments (occurrence_id, comment, auto_generated, created_on, created_by_id, updated_on, updated_by_id)
+ SELECT o.id, $langRecheck, 't', now(), $this->user_id, now(), $this->user_id
+ FROM changing_samples cs
+ JOIN occurrences o ON o.sample_id=cs.id
+ AND o.deleted=false
+ AND (o.record_status<>'C' OR o.record_substatus IS NOT NULL);
+
+ UPDATE occurrences o
+ SET updated_on=now(),
+ updated_by_id=$this->user_id,
+ record_status='C',
+ record_substatus=null,
+ verified_by_id=null,
+ verified_on=null
+ FROM changing_samples cs
+ WHERE cs.id=o.sample_id
+ AND o.deleted=false;
+
SQL;
- $db->query($qry);
- }
+ $db->query($qry);
if (!empty($updates->recorder_name)) {
// Recorder name a little different as it might be a custom attribute.
$this->bulkEditRecorderNames($db, $sampleIds, $updates->recorder_name);
}
+
// Update the cache_* data using the work queue.
$qry = <<date)) {
+ $sampleFieldUpdates[] = "date_start='$updates->date'::date";
+ $sampleFieldUpdates[] = "date_end='$updates->date'::date";
+ $sampleFieldUpdates[] = "date_type='D'";
+ }
+ if (!empty($updates->location_name)) {
+ $locationName = pg_escape_literal($db->getLink(), $updates->location_name);
+ $sampleFieldUpdates[] = "location_name=$locationName";
+ }
+ if (!empty($updates->sref)) {
+ $sref = pg_escape_literal($db->getLink(), spatial_ref::sref_format_tidy($updates->sref, $updates->sref_system));
+ $sampleFieldUpdates[] = "entered_sref=$sref";
+ $system = pg_escape_literal($db->getLink(), $updates->sref_system);
+ $sampleFieldUpdates[] = "entered_sref_system=$system";
+ $sampleFieldUpdates[] = "geom=st_geomfromtext('" . spatial_ref::sref_to_internal_wkt($updates->sref, $updates->sref_system) . "', 900913)";
+ }
+ return $sampleFieldUpdates;
+ }
+
/**
* Confirms that a list of sample IDs are all created by the current user.
*
@@ -1241,7 +1292,7 @@ private function checkSamplesAllBelongToUser($db, $sampleIds) {
}
/**
- * Handle the bulk edit of recorder names
+ * Handle the bulk edit of recorder names.
*
* Complex due to optional custom attributes vs samples.recorder_names field.
*
diff --git a/modules/log_browser/controllers/browse_server_logs.php b/modules/log_browser/controllers/browse_server_logs.php
index 28b982d76b..366dac0cf5 100644
--- a/modules/log_browser/controllers/browse_server_logs.php
+++ b/modules/log_browser/controllers/browse_server_logs.php
@@ -40,7 +40,7 @@ public function index() {
}
// put the most recent first
arsort($files);
- $this->template->title='Browse Server Logs';
+ $this->template->title='Browse server logs';
$this->template->content = new View('browse_server_logs');
$this->template->content->files = $files;
}
diff --git a/modules/log_browser/views/browse_server_logs.php b/modules/log_browser/views/browse_server_logs.php
index cad79950ef..7d3efe7b80 100644
--- a/modules/log_browser/views/browse_server_logs.php
+++ b/modules/log_browser/views/browse_server_logs.php
@@ -68,7 +68,7 @@
),
));
?>
-
+
23, 'url' => 'http://www.brc.ac.uk/irecord/record-details?occurrence_id=#id#')
-);
+$config['record_details_page_urls'] = [
+ ['website_id' => 23, 'url' => 'http://www.brc.ac.uk/irecord/record-details?occurrence_id=#id#']
+];
// Populate the following array with a list of notification types you want to control the output of email text for.
// Each entry must have the type code as a key ('S', 'C', 'V', 'Q', 'RD', 'A', 'VT', 'M', 'PT', 'GU') and an array as the value.
// The sub arrays can contain optional title and/or description elements to set the text included in the notification
// email.
-$config['notification_types']=array();
\ No newline at end of file
+$config['notification_types'] = [];
\ No newline at end of file
diff --git a/modules/notification_emails/plugins/notification_emails.php b/modules/notification_emails/plugins/notification_emails.php
index af5e43a054..c3456614d2 100644
--- a/modules/notification_emails/plugins/notification_emails.php
+++ b/modules/notification_emails/plugins/notification_emails.php
@@ -195,7 +195,7 @@ function run_email_notification_jobs($db, array $frequenciesToRun) {
}
else {
// Get address to send emails from.
- $email_config = array();
+ $email_config = [];
// Try and get from configuration file if possible.
try {
$email_config['address'] = kohana::config('notification_emails.email_sender_address');
@@ -217,7 +217,7 @@ function run_email_notification_jobs($db, array $frequenciesToRun) {
// user was for the previous notification. When this user id then changes,
// we know we need to start building an new email to a new user.
$previousUserId = 0;
- $notificationIds = array();
+ $notificationIds = [];
$emailContent = start_building_new_email($notificationsToSendEmailsFor[0]);
$currentType = '';
$sourceTypes = notification_emails::getNotificationTypes();
@@ -249,7 +249,7 @@ function run_email_notification_jobs($db, array $frequenciesToRun) {
// Used to mark the notifications in an email if an email send is
// successful, once email send attempt has been made we can reset the
// list ready for the next email.
- $notificationIds = array();
+ $notificationIds = [];
$emailSentCounter++;
// As we just sent out a an email, we can start building a new one.
$emailContent = start_building_new_email($notificationToSendEmailsFor);
@@ -370,7 +370,7 @@ function notification_emails_hyperlink_id($id, $websiteId) {
// Handle config file not present.
}
catch (Exception $e) {
- $recordDetailsPages = array();
+ $recordDetailsPages = [];
}
foreach ($recordDetailsPages as $page) {
$found = $page['website_id'] == $websiteId;
diff --git a/modules/rest_api/i18n/en_GB/es_fields.php b/modules/rest_api/i18n/en_GB/es_fields.php
new file mode 100644
index 0000000000..1bcd31c206
--- /dev/null
+++ b/modules/rest_api/i18n/en_GB/es_fields.php
@@ -0,0 +1,5 @@
+ 'sensitive record, location hidden',
+];
diff --git a/modules/rest_api/libraries/RestApiElasticsearch.php b/modules/rest_api/libraries/RestApiElasticsearch.php
index 4e83c3cb2a..5113b5f1ed 100644
--- a/modules/rest_api/libraries/RestApiElasticsearch.php
+++ b/modules/rest_api/libraries/RestApiElasticsearch.php
@@ -95,7 +95,8 @@ class RestApiElasticsearch {
'field' => 'location.coordinate_uncertainty_in_meters',
],
['caption' => 'Lat/Long', 'field' => 'location.point'],
- ['caption' => 'Location name', 'field' => 'location.verbatim_locality'],
+ ['caption' => 'Location name', 'field' => '#sitename:obscureifsensitive#'],
+ ['caption' => 'Sensitive location', 'field' => '#sitename:showifsensitive#'],
['caption' => 'Higher geography', 'field' => '#higher_geography::name#'],
[
'caption' => 'Vice County',
@@ -156,7 +157,8 @@ class RestApiElasticsearch {
['caption' => 'Order', 'field' => 'taxon.order'],
['caption' => 'Family', 'field' => 'taxon.family'],
['caption' => 'TaxonVersionKey', 'field' => 'taxon.accepted_taxon_id'],
- ['caption' => 'Site name', 'field' => 'location.verbatim_locality'],
+ ['caption' => 'Site name', 'field' => '#sitename:obscureifsensitive#'],
+ ['caption' => 'Sensitive site', 'field' => '#sitename:showifsensitive#'],
['caption' => 'Original map ref', 'field' => 'location.input_sref'],
['caption' => 'Latitude', 'field' => '#lat:decimal#'],
['caption' => 'Longitude', 'field' => '#lon:decimal#'],
@@ -234,6 +236,7 @@ class RestApiElasticsearch {
"mapmate" => [
['caption' => 'Taxon', 'field' => 'taxon.accepted_name'],
['caption' => 'Site', 'field' => '#sitename:mapmate#'],
+ ['caption' => 'Sensitive site', 'field' => '#sitename:showifsensitive#'],
['caption' => 'Gridref', 'field' => 'location.output_sref'],
[
'caption' => 'VC',
@@ -440,8 +443,8 @@ private function applyEsPermissionsQuery(&$postObj) {
}
else {
// Otherwise, only verification or user's own records get full
- // precision, but not if downloading currently.
- $blur = ($this->pagingMode !== 'scroll' && (RestObjects::$scope === 'verification' || substr(RestObjects::$scope, 0, 4) === 'user')) ? 'F' : 'B';
+ // precision.
+ $blur = (RestObjects::$scope === 'verification' || substr(RestObjects::$scope, 0, 4) === 'user') ? 'F' : 'B';
}
$queryStringParts = [];
if (empty($this->resourceOptions['allow_confidential']) || $this->resourceOptions['allow_confidential'] !== TRUE) {
@@ -1107,10 +1110,10 @@ private function esGetSpecialFieldLocality(array $doc) {
$info = [];
if (!empty($doc['location']['verbatim_locality'])) {
$info[] = $doc['location']['verbatim_locality'];
- if (!empty($doc['location']['higher_geography'])) {
- foreach ($doc['location']['higher_geography'] as $loc) {
- $info[] = "$loc[type]: $loc[name]";
- }
+ }
+ if (!empty($doc['location']['higher_geography'])) {
+ foreach ($doc['location']['higher_geography'] as $loc) {
+ $info[] = "$loc[type]: $loc[name]";
}
}
return implode('; ', $info);
@@ -1421,7 +1424,19 @@ private function esGetSpecialFieldSex(array $doc, array $params) {
/**
* Special field handler for ES sitename.
*
- * Return location.verbatim_locality formatted as specified.
+ * Return location.verbatim_locality formatted as specified. The parameter
+ * supplied dictates the format - select one of the following:
+ * * obscureifsensitive - for sensitive records with a site name, replace the
+ * name with a placeholder indicating that it's witheld, for non-sensitive
+ * records the entire site name is returned.
+ * * showifsensitive - only show the name for sensitive records.
+ * * mapmate - for sensitive records with a site name, replace the name with
+ * a placeholder indicating that it's witheld, for non-sensitive records
+ * return the first 62 characters of the site name, or 'unnamed site' if no
+ * name given.
+ * If the parameter is not supplied then the site name is always shown if the
+ * user has permission to see sensitive records unblurred or the record is
+ * not sensitive.
*
* @param array $doc
* Elasticsearch document.
@@ -1432,18 +1447,38 @@ private function esGetSpecialFieldSex(array $doc, array $params) {
* Formatted string
*/
private function esGetSpecialFieldSitename(array $doc, array $params) {
- if (count($params) !== 1) {
- return 'Incorrect params for sitename field';
- }
+ $format = !empty($params) ? $params[0] : '';
$value = $this->getRawEsFieldValue($doc, 'location.verbatim_locality');
- if ($params[0] === 'mapmate') {
- if ($value === '') {
- $value = 'unnamed site';
- }
- // Truncation to 62 characters required for MapMate.
- $value = substr($value, 0, 62);
+ $shouldBlur = $doc['metadata']['sensitive'] === 'true' || $doc['metadata']['private'] === 'true';
+ switch ($format) {
+ case 'obscureifsensitive':
+ if ($shouldBlur && !empty($value)) {
+ return '[' . kohana::lang('es_fields.sensitiveLocation'). ']';
+ }
+ // Full site name.
+ return $value;
+
+ case 'showifsensitive':
+ if ($shouldBlur && $value !== '!') {
+ return $value;
+ }
+ return '';
+
+ case 'mapmate':
+ if ($shouldBlur && !empty($value)) {
+ return '[' . kohana::lang('es_fields.sensitiveLocation'). ']';
+ }
+ // Truncation to 62 characters required for MapMate.
+ if (empty($value)) {
+ return 'unnamed site';
+ }
+ // Truncation to 62 characters required for MapMate, skipping the site
+ // witheld ! character.
+ return $value === '!' ? '' : substr($value, 0, 62);
+
+ default:
+ return $value === '!' ? '' : $value;
}
- return $value;
}
/**
@@ -2071,8 +2106,10 @@ private function getEsPostData($postObj, $format, $file, $isSearch) {
$fields[] = 'event.recorded_by';
$fields[] = 'identification.identified_by';
}
- elseif (preg_match('/^#sitename(.*)#$/', $field)) {
+ elseif (preg_match('/^#sitename(:.*)?#$/', $field)) {
$fields[] = 'location.verbatim_locality';
+ $fields[] = 'metadata.sensitive';
+ $fields[] = 'metadata.private';
}
elseif (preg_match('/^#template(.*)#$/', $field)) {
// Find fields embedded in the template and add them.
diff --git a/modules/uksi_operations/controllers/uksi_operation.php b/modules/uksi_operations/controllers/uksi_operation.php
index f9b6a60627..ee76620faa 100644
--- a/modules/uksi_operations/controllers/uksi_operation.php
+++ b/modules/uksi_operations/controllers/uksi_operation.php
@@ -235,6 +235,7 @@ public function processAddSynonym($operation) {
// Copy over details from the preferred taxon to define a synonym.
$fields['taxa_taxon_list:taxon_meaning_id'] = $allExistingNames->current()->taxon_meaning_id;
$fields['taxa_taxon_list:parent_id'] = $allExistingNames->current()->parent_id;
+ $fields['taxa_taxon_list:common_taxon_id'] = $allExistingNames->current()->common_taxon_id;
$fields['taxon:taxon_rank_id'] = $allExistingNames->current()->taxon_rank_id;
$fields['taxon:taxon_group_id'] = $allExistingNames->current()->taxon_group_id;
$fields['taxon:marine_flag'] = $allExistingNames->current()->marine_flag;
@@ -286,7 +287,11 @@ public function processAmendName($operation) {
$tx->taxon = $operation->taxon_name;
}
if (!empty($operation->attribute)) {
- $tx->attribute = $operation->attribute;
+ if ($operation->attribute === 'NONE') {
+ $tx->attribute = NULL;
+ } else {
+ $tx->attribute = $operation->attribute;
+ }
}
if (!empty($operation->authority)) {
$tx->authority = $operation->authority;
@@ -705,6 +710,7 @@ public function processRenameTaxon($operation) {
}
$fields['taxon:organism_key'] = $operation->current_organism_key;
$fields['taxa_taxon_list:taxon_meaning_id'] = $previousPreferredName->taxon_meaning_id;
+ $fields['taxa_taxon_list:common_taxon_id'] = $previousPreferredName->common_taxon_id;
if (empty($fields['taxa_taxon_list:parent_id'])) {
// If parent not specified in operation, keep the original.
$fields['taxa_taxon_list:parent_id'] = $previousPreferredName->parent_id;
@@ -983,7 +989,7 @@ private function getTaxaForKeys(array $search) {
// Table alias defaults to t.
$where[strpos($key, '.') === FALSE ? "t.$key" : $key] = $value;
}
- return $this->db->select('ttl.id, ttl.taxon_meaning_id, ttl.taxon_id, ttl.preferred, ttl.parent_id, ttl.allow_data_entry, ' .
+ return $this->db->select('ttl.id, ttl.taxon_meaning_id, ttl.taxon_id, ttl.preferred, ttl.parent_id, ttl.allow_data_entry, ttl.common_taxon_id, ' .
't.taxon, t.authority, t.attribute, t.search_code, t.external_key, t.organism_key, t.taxon_rank_id, ' .
't.taxon_group_id, t.marine_flag, t.freshwater_flag, t.terrestrial_flag, t.non_native_flag, ' .
't.organism_deprecated, t.name_deprecated')
diff --git a/reports/library/groups/find_group_by_url.xml b/reports/library/groups/find_group_by_url.xml
index 646f6ec4a9..70a895335a 100644
--- a/reports/library/groups/find_group_by_url.xml
+++ b/reports/library/groups/find_group_by_url.xml
@@ -8,6 +8,7 @@
(((gp.administrator=false OR gu.administrator=true) and gu.id is not null AND gu.pending=false) OR gp.administrator is null) AND
(gu.administrator=true OR COALESCE(gu.access_level, 0)>=COALESCE(gp.access_level, 0))
LEFT JOIN group_pages gpall ON gpall.group_id=g.id AND gpall.deleted=false
+ LEFT JOIN groups gcont ON gcont.id=g.contained_by_group_id AND gcont.container=true AND gcont.deleted=false
JOIN cache_termlists_terms gt on gt.id=g.group_type_id
#joins#
WHERE g.deleted = false
@@ -46,6 +47,12 @@
+
+
+
+
+
+
@@ -58,5 +65,6 @@
aggregate="true" template="{pages}"/>
+
diff --git a/reports/library/groups/groups_list.xml b/reports/library/groups/groups_list.xml
index fc412f8033..0fd9916fc1 100644
--- a/reports/library/groups/groups_list.xml
+++ b/reports/library/groups/groups_list.xml
@@ -24,7 +24,7 @@
datatype="lookup" default="member"
lookup_values='all:All groups,joinable:Groups they can join or request,allvisible:Groups they can join or request or are already a member of,pending:Groups they are pending approval to join,member:Groups they are a member of,admin:Groups they administer,create:Groups they created,create_admin:Groups they created or administer'>
- g.joining_method not in ('A','I')
+ g.joining_method not in ('A','I') AND gu.id IS NULL(g.joining_method not in ('A','I') OR gu.id IS NOT NULL OR '#CMSAdminPerm#' = '1')gu.id is not null AND gu.pending=truegu.id is not null AND gu.pending=false
@@ -70,6 +70,9 @@
(g.title ilike '%#search_text#%' or g.description ilike '%#search_text#%')
+
+ websearch_to_tsquery('english', '#search_fulltext#') @@ to_tsvector('english', coalesce(g.title, '') || ' ' || coalesce(g.description, ''))
+
diff --git a/reports/library/occurrences/list_for_elastic.xml b/reports/library/occurrences/list_for_elastic.xml
index 397b906b99..055bf7a319 100644
--- a/reports/library/occurrences/list_for_elastic.xml
+++ b/reports/library/occurrences/list_for_elastic.xml
@@ -28,7 +28,7 @@
JOIN cache_taxa_taxon_lists cttl ON cttl.id=o.taxa_taxon_list_id
LEFT JOIN locations l ON l.id=o.location_id AND l.deleted=false AND o.private=false AND o.sensitive=false;
- -- Parent sample data
+ -- Parent sample data.
UPDATE output_rows o
SET recorded_parent_location_id = CASE WHEN o.private='false' AND o.sensitive='false' THEN lp.id ELSE null END,
recorded_parent_location_name = CASE WHEN o.private='false' AND o.sensitive='false' THEN lp.name ELSE null END,
@@ -51,6 +51,17 @@
AND o.recorded_parent_location_id IS NULL
AND o.private='false' AND o.sensitive='false';
+ -- Sensitive site name warnings.
+ UPDATE output_rows o
+ SET given_locality_name='!'
+ FROM samples s
+ LEFT JOIN samples sp ON sp.id=s.parent_id AND sp.deleted=false
+ LEFT JOIN locations l ON l.id=s.location_id AND l.deleted=false
+ LEFT JOIN locations lp ON lp.id=sp.location_id AND lp.deleted=false
+ WHERE s.id=o.sample_id AND s.deleted=false
+ AND (o.private='true' OR o.sensitive='true')
+ AND COALESCE(l.name, s.location_name, lp.name, sp.location_name, '')<>'';
+
SELECT *
FROM output_rows o
#order_by#
diff --git a/reports/library/occurrences/list_for_elastic_all.xml b/reports/library/occurrences/list_for_elastic_all.xml
index d0403798bd..aceab85b6d 100644
--- a/reports/library/occurrences/list_for_elastic_all.xml
+++ b/reports/library/occurrences/list_for_elastic_all.xml
@@ -28,7 +28,7 @@
JOIN cache_taxa_taxon_lists cttl ON cttl.id=o.taxa_taxon_list_id
LEFT JOIN locations l ON l.id=o.location_id AND l.deleted=false AND o.private=false AND o.sensitive=false;
- -- Parent sample data
+ -- Parent sample data.
UPDATE output_rows o
SET recorded_parent_location_id = CASE WHEN o.private='false' AND o.sensitive='false' THEN lp.id ELSE null END,
recorded_parent_location_name = CASE WHEN o.private='false' AND o.sensitive='false' THEN lp.name ELSE null END,
@@ -51,6 +51,17 @@
AND o.recorded_parent_location_id IS NULL
AND o.private='false' AND o.sensitive='false';
+ -- Sensitive site name warnings.
+ UPDATE output_rows o
+ SET given_locality_name='!'
+ FROM samples s
+ LEFT JOIN samples sp ON sp.id=s.parent_id AND sp.deleted=false
+ LEFT JOIN locations l ON l.id=s.location_id AND l.deleted=false
+ LEFT JOIN locations lp ON lp.id=sp.location_id AND lp.deleted=false
+ WHERE s.id=o.sample_id AND s.deleted=false
+ AND (o.private='true' OR o.sensitive='true')
+ AND COALESCE(l.name, s.location_name, lp.name, sp.location_name, '')<>'';
+
SELECT *
FROM output_rows o
#order_by#
diff --git a/reports/library/samples/list_for_elastic.xml b/reports/library/samples/list_for_elastic.xml
index 459200b307..db60ba50e3 100644
--- a/reports/library/samples/list_for_elastic.xml
+++ b/reports/library/samples/list_for_elastic.xml
@@ -48,6 +48,7 @@
JOIN cache_samples_nonfunctional snf ON snf.id=s.id
LEFT JOIN locations l ON l.id=s.location_id AND l.deleted=false;
+ -- Parent sample data.
UPDATE output_rows o
SET recorded_parent_location_id = lp.id,
recorded_parent_location_name = lp.name,
@@ -69,6 +70,17 @@
WHERE sc.id=o.sample_id AND sc.deleted=false
AND o.recorded_parent_location_id IS NULL;
+ -- Sensitive site name warnings.
+ UPDATE output_rows o
+ SET given_locality_name='!'
+ FROM samples s
+ LEFT JOIN samples sp ON sp.id=s.parent_id AND sp.deleted=false
+ LEFT JOIN locations l ON l.id=s.location_id AND l.deleted=false
+ LEFT JOIN locations lp ON lp.id=sp.location_id AND lp.deleted=false
+ WHERE s.id=o.id AND s.deleted=false
+ AND o.private='true'
+ AND COALESCE(l.name, s.location_name, lp.name, sp.location_name, '')<>'';
+
SELECT *
FROM output_rows s
#order_by#
diff --git a/reports/library/samples/list_for_elastic_all.xml b/reports/library/samples/list_for_elastic_all.xml
index 78f1c9b848..96882f986d 100644
--- a/reports/library/samples/list_for_elastic_all.xml
+++ b/reports/library/samples/list_for_elastic_all.xml
@@ -48,6 +48,7 @@
JOIN cache_samples_nonfunctional snf ON snf.id=s.id
LEFT JOIN locations l ON l.id=s.location_id AND l.deleted=false;
+ -- Parent sample data.
UPDATE output_rows o
SET recorded_parent_location_id = lp.id,
recorded_parent_location_name = lp.name,
@@ -69,6 +70,17 @@
WHERE sc.id=o.sample_id AND sc.deleted=false
AND o.recorded_parent_location_id IS NULL;
+ -- Sensitive site name warnings.
+ UPDATE output_rows o
+ SET given_locality_name='!'
+ FROM samples s
+ LEFT JOIN samples sp ON sp.id=s.parent_id AND sp.deleted=false
+ LEFT JOIN locations l ON l.id=s.location_id AND l.deleted=false
+ LEFT JOIN locations lp ON lp.id=sp.location_id AND lp.deleted=false
+ WHERE s.id=o.id AND s.deleted=false
+ AND o.private='true'
+ AND COALESCE(l.name, s.location_name, lp.name, sp.location_name, '')<>'';
+
SELECT *
FROM output_rows s
#order_by#
diff --git a/reports/trigger_templates/record_thanks_email.xml b/reports/trigger_templates/record_thanks_email.xml
new file mode 100644
index 0000000000..6645915c8f
--- /dev/null
+++ b/reports/trigger_templates/record_thanks_email.xml
@@ -0,0 +1,27 @@
+
+
+ select #columns#
+ from cache_occurrences_functional o
+ join users u on u.id=o.created_by_id and u.deleted=false
+ join people p on p.id=u.person_id and p.deleted=false
+ where o.updated_on>'#date#'
+ and o.created_on>'#date#'
+ and o.training=false
+
+
+ o.updated_on ASC
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/system/vendor/swift/Swift/Connection/SMTP.php b/system/vendor/swift/Swift/Connection/SMTP.php
index 393261b8ae..547c8d22a5 100644
--- a/system/vendor/swift/Swift/Connection/SMTP.php
+++ b/system/vendor/swift/Swift/Connection/SMTP.php
@@ -88,7 +88,14 @@ class Swift_Connection_SMTP extends Swift_ConnectionBase
* @var string
*/
protected $errstr;
-
+
+ /**
+ * Server hostname.
+ *
+ * @var string
+ */
+ protected $server;
+
/**
* Constructor
* @param string The remote server to connect to
@@ -287,17 +294,17 @@ public function start()
break;
}
}
-
+
$server = $this->server;
if ($this->encryption == self::ENC_TLS) $server = "tls://" . $server;
elseif ($this->encryption == self::ENC_SSL) $server = "ssl://" . $server;
-
+
$log = Swift_LogContainer::getLog();
if ($log->hasLevel(Swift_log::LOG_EVERYTHING))
{
$log->add("Trying to connect to SMTP server at '" . $server . ":" . $this->port);
}
-
+
if (!$this->handle = @fsockopen($server, $this->port, $errno, $errstr, $this->timeout))
{
$error_msg = "The SMTP connection failed to start [" . $server . ":" . $this->port . "]: fsockopen returned Error Number " . $errno . " and Error String '" . $errstr . "'";
@@ -367,10 +374,10 @@ public function runAuthenticators($user, $pass, Swift $swift)
}
closedir($handle);
}
-
+
$tried = 0;
$looks_supported = true;
-
+
//Allow everything we have if the server has the audacity not to help us out.
if (!$this->hasExtension("AUTH"))
{
@@ -381,7 +388,7 @@ public function runAuthenticators($user, $pass, Swift $swift)
$looks_supported = false;
$this->setExtension("AUTH", array_keys($this->authenticators));
}
-
+
foreach ($this->authenticators as $name => $obj)
{
//Server supports this authentication mechanism
@@ -402,11 +409,11 @@ public function runAuthenticators($user, $pass, Swift $swift)
}
}
}
-
+
//Server doesn't support authentication
if (!$looks_supported && $tried == 0)
throw new Swift_ConnectionException("Authentication is not supported by the server but a username and password was given.");
-
+
if ($tried == 0)
throw new Swift_ConnectionException("No authentication mechanisms were tried since the server did not support any of the ones loaded. " .
"Loaded authenticators: [" . implode(", ", array_keys($this->authenticators)) . "]");