Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b358d6d
Merge branch 'hotfix/1.21.1'
pdl Nov 24, 2025
adf33f1
feat: Note CoP changes underway (#427)
pdl Feb 16, 2026
e7af8fb
feat: make date bold to ensure readers pick up it's a new notice
pdl Feb 16, 2026
052f73d
fix: force style because of silly CSS reset
pdl Feb 16, 2026
9b44839
chore: add two files for imports
pdl Feb 19, 2026
c30482c
chore: remove one line from import per KOB
pdl Feb 19, 2026
c2cc91a
chore: convert to lf
pdl Feb 19, 2026
7197812
fix: error should be raised correctly
pdl Feb 19, 2026
597e35d
fix: docs
pdl Feb 19, 2026
d4885a1
fix: with_indifferent_access
pdl Feb 19, 2026
5870e2d
fix: wrap in transactions
pdl Feb 19, 2026
4089a3c
feat: add task import:taxon_name_status_changes
pdl Feb 19, 2026
3355341
docs: Changelog for 1.21.2
pdl Feb 19, 2026
1904f12
fix: a[href] attributes should not be missing; point to documentUrl
pdl Feb 19, 2026
09f3482
fix: remove spaces in import file
pdl Feb 19, 2026
8bf0631
fix: also have autocomplete grab the rank_id to fix a 500 error in th…
pdl Feb 24, 2026
2fc4a45
chore: prepare CITES synonyms import
pdl Mar 4, 2026
a6913b8
chore: gitignore MS Windows' Zone identifiers (hidden files)
pdl Mar 4, 2026
caeb8ef
fix: duplicate synonym
pdl Mar 6, 2026
7df9893
chore: db:trim_taxonomies
pdl Mar 6, 2026
1561e57
chore: implement cascade delete
pdl Mar 6, 2026
d36e83c
fix
pdl Mar 10, 2026
b48b86b
wip
pdl Mar 10, 2026
a812f0d
wip
pdl Mar 10, 2026
72a7df2
done
pdl Mar 11, 2026
0fac9ce
wip
pdl Mar 11, 2026
6ee739c
fix
pdl Mar 11, 2026
13a31a8
fix
pdl Mar 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,6 @@

!.keep
!.gitkeep

# MS Windows downloads hidden file
*:Zone.Identifier
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
### 1.21.2

**Species+**

* Front page includes temporary banner noting post-CoP CITES work ongoing.
* Fixes for common names importer to facilitate new CMS names.
* New importer for changing `name_status` to promote N names to A names in bulk.

**Species+ admin interface**

* Fixes an issue where the serialisation of the autocomplete results for new
taxon concepts is broken until they go through the cascade.

### 1.21.1

**CITES Trade DB**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<td class='event-date-col'>{{date}}</td>
<td class='title-col'>

<a {{bind-attr class=":legal-links isLongTitle:tooltip"}} {{action "startDownload" on="click"}} target="_blank">
<a {{bind-attr class=":legal-links isLongTitle:tooltip"}} {{action "startDownload" on="click"}} target="_blank" {{bind-attr href=documentUrl}}>

{{#if isLongTitle}}
{{truncate fullTitle}}
Expand Down
5 changes: 3 additions & 2 deletions app/assets/javascripts/species/templates/index.handlebars
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
-->
<div id="banner">
<p>
Note: Changes have been made to the Species+ search function, to improve how it handles typographical errors and names in different languages.
Please note that updates to CITES listings and nomenclature are currently being processed to align with the revised CITES Appendices that come into force on the
<b style="font-weight: bold;">5th March 2026</b>.
</p>
<button id="remove">×</button>
</div>
Expand All @@ -17,4 +18,4 @@

{{outlet downloadsButton}}

{{partial 'species/promo_banner'}}
{{partial 'species/promo_banner'}}
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,11 @@ Species.DocumentResultComponent = Ember.Component.extend

actions:
startDownload: () ->
url = "/api/v1/documents/#{@get('documentId')}"

analytics.gtag('event', 'download_single', {
document_type: @get('document_type'),
event_type: @get('event_type'),
event_name: @get('event_name'),
count: 1
})

window.open(url, '_blank')
window.open(@get('documentUrl'), '_blank')
1 change: 1 addition & 0 deletions app/models/application_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ class ApplicationRecord < ActiveRecord::Base
include SearchableRelation
include ProtectedDeletion
include ComparisonAttributes
include CascadeDeletable

primary_abstract_class
end
62 changes: 62 additions & 0 deletions app/models/concerns/cascade_deletable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
module CascadeDeletable
extend ActiveSupport::Concern

class_methods do
def cascade_delete!
cascade_nullify_associated!
cascade_delete_associated!

delete_all
end

##
# Returns an array of `ActiveRecord::Reflection`s
def cascade_deletable_associations
reflect_on_all_associations.filter do |assoc|
[ :has_many, :has_one ].include?(assoc.macro) &&
assoc.has_inverse? &&
!assoc.inverse_of.options[:optional]
end
end

##
# Returns an array of `ActiveRecord::Reflection`s
def cascade_nullable_associations
reflect_on_all_associations.filter do |assoc|
[ :has_many, :has_one ].include?(assoc.macro) &&
assoc.has_inverse? &&
assoc.inverse_of.options[:optional]
end
end

def cascade_nullify_associated! (
nullable_associations = cascade_nullable_associations
)
if limit(1).count > 0
nullable_associations.each do |assoc|
inverted_assoc_rel = assoc.klass.where(assoc.inverse_of.name => all)

inverted_assoc_rel.update_all( # rubocop:disable Rails/SkipsModelValidations
assoc.inverse_of.foreign_key.to_sym => nil
)
end
end
end

def cascade_delete_associated! (
deletable_associations = cascade_deletable_associations
)
if limit(1).count > 0
deletable_associations.each do |assoc|
inverted_assoc_rel = assoc.klass.where(assoc.inverse_of.name => all)

if inverted_assoc_rel.respond_to? :cascade_delete!
inverted_assoc_rel.cascade_delete!
else
inverted_assoc_rel.delete_all
end
end
end
end
end
end
10 changes: 8 additions & 2 deletions app/models/concerns/changeable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,13 @@ def changeable_clear_cache
def changeable_bump_dependents_timestamp_part_one(taxon_concept, updated_by_id)
return unless taxon_concept

TaxonConcept.where(id: taxon_concept.id).update_all(
# Be precise with our where clause to avoid touching the same taxon concept
# multiple times in the same transaction.
# Purposefully skip validation with update_all, this is a touch.
TaxonConcept.where(
'id = ? and (updated_at < now() or id is distinct from ?)',
[ taxon_concept.id ], [ updated_by_id ]
).update_all( # rubocop:disable Rails/SkipsModelValidations
dependents_updated_at: Time.now,
dependents_updated_by_id: updated_by_id
)
Expand All @@ -147,6 +153,6 @@ def changeable_bump_dependents_timestamp_part_two
end

def changeable_clear_show_tc_serializer_cache
Rails.cache.delete_matched("*ShowTaxonConceptSerializer*")
Rails.cache.delete_matched('*ShowTaxonConceptSerializer*')
end
end
66 changes: 54 additions & 12 deletions app/models/listing_change.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,22 +74,64 @@ class ListingChange < ApplicationRecord
:excluded_taxon_concepts_ids # String

belongs_to :event, optional: true
has_many :listing_change_copies, foreign_key: :original_id,
class_name: 'ListingChange', dependent: :nullify

##
# listing_changes_parent_id_fk describes exclusions
belongs_to :parent,
class_name: 'ListingChange',
inverse_of: :exclusions,
optional: true
has_many :exclusions,
class_name: 'ListingChange',
dependent: :destroy,
foreign_key: 'parent_id',
inverse_of: :parent

# listing_changes_source_id_fk
belongs_to :original,
class_name: 'ListingChange',
inverse_of: :listing_change_copies,
optional: true
has_many :listing_change_copies,
class_name: 'ListingChange',
dependent: :nullify,
foreign_key: :original_id,
inverse_of: :original

belongs_to :species_listing
belongs_to :taxon_concept
belongs_to :change_type
has_many :listing_distributions, -> { where is_party: false }, inverse_of: :listing_change, dependent: :destroy
has_one :party_listing_distribution, -> { where is_party: true }, class_name: 'ListingDistribution',
dependent: :destroy, inverse_of: :listing_change
has_many :geo_entities, through: :listing_distributions
has_one :party_geo_entity, class_name: 'GeoEntity',
through: :party_listing_distribution, source: :geo_entity

has_many :all_listing_distributions,
class_name: 'ListingDistribution',
inverse_of: :listing_change,
dependent: :destroy
has_many :listing_distributions, # non-party distributions
-> { where is_party: false },
dependent: :destroy
has_one :party_listing_distribution,
-> { where is_party: true },
class_name: 'ListingDistribution',
dependent: :destroy,
inverse_of: :listing_change

has_many :geo_entities,
through: :listing_distributions
has_one :party_geo_entity,
class_name: 'GeoEntity',
through: :party_listing_distribution,
source: :geo_entity

belongs_to :annotation, optional: true
belongs_to :hash_annotation, class_name: 'Annotation', optional: true
belongs_to :parent, class_name: 'ListingChange', optional: true
belongs_to :inclusion, class_name: 'TaxonConcept', foreign_key: 'inclusion_taxon_concept_id', optional: true
has_many :exclusions, class_name: 'ListingChange', foreign_key: 'parent_id', dependent: :destroy
belongs_to :hash_annotation,
class_name: 'Annotation',
optional: true
belongs_to :inclusion,
class_name: 'TaxonConcept',
foreign_key: 'inclusion_taxon_concept_id',
optional: true,
inverse_of: :listing_change_inclusions

validates :effective_at, presence: true
validate :inclusion_at_higher_rank
validate :species_listing_designation_mismatch
Expand Down
13 changes: 9 additions & 4 deletions app/models/m_auto_complete_taxon_concept.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,15 @@
#
# Indexes
#
# auto_complete_taxon_concepts__name_for_matching_taxonomy_i_idx4 (name_for_matching,taxonomy_is_cites_eu,type_of_match,show_in_species_plus_ac)
# auto_complete_taxon_concepts__name_for_matching_taxonomy_i_idx5 (name_for_matching,taxonomy_is_cites_eu,type_of_match,show_in_checklist_ac)
# auto_complete_taxon_concepts__name_for_matching_taxonomy_i_idx6 (name_for_matching,taxonomy_is_cites_eu,type_of_match,show_in_trade_ac)
# auto_complete_taxon_concepts__name_for_matching_taxonomy_i_idx7 (name_for_matching,taxonomy_is_cites_eu,type_of_match,show_in_trade_internal_ac)
# idx_ac_taxon_checklist_btree (name_for_matching,type_of_match) WHERE (taxonomy_is_cites_eu AND show_in_checklist_ac)
# idx_ac_taxon_checklist_gist (name_for_matching) WHERE (taxonomy_is_cites_eu AND show_in_checklist_ac) USING gist
# idx_ac_taxon_gist (name_for_matching) USING gist
# idx_ac_taxon_splus_btree (name_for_matching,taxonomy_is_cites_eu,type_of_match) WHERE show_in_species_plus_ac
# idx_ac_taxon_splus_gist (name_for_matching) WHERE show_in_species_plus_ac USING gist
# idx_ac_taxon_trade_ac_btree (name_for_matching,type_of_match,taxonomy_is_cites_eu) WHERE show_in_trade_ac
# idx_ac_taxon_trade_ac_gist (name_for_matching) WHERE show_in_trade_ac USING gist
# idx_ac_taxon_trade_internal_btree (name_for_matching,type_of_match,taxonomy_is_cites_eu) WHERE show_in_trade_internal_ac
# idx_ac_taxon_trade_internal_gist (name_for_matching) WHERE show_in_trade_internal_ac USING gist
#

class MAutoCompleteTaxonConcept < ApplicationRecord
Expand Down
14 changes: 7 additions & 7 deletions app/models/m_taxon_concept.rb
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,13 @@
#
# Indexes
#
# taxon_concepts_mview_tmp_cites_show_name_status_cites_listi_idx (cites_show,name_status,cites_listing_original,taxonomy_is_cites_eu,rank_name)
# taxon_concepts_mview_tmp_cms_show_name_status_cms_listing_o_idx (cms_show,name_status,cms_listing_original,taxonomy_is_cites_eu,rank_name)
# taxon_concepts_mview_tmp_countries_ids_ary_idx1 (countries_ids_ary) USING gin
# taxon_concepts_mview_tmp_eu_show_name_status_eu_listing_ori_idx (eu_show,name_status,eu_listing_original,taxonomy_is_cites_eu,rank_name)
# taxon_concepts_mview_tmp_id_idx (id)
# taxon_concepts_mview_tmp_parent_id_idx (parent_id)
# taxon_concepts_mview_tmp_taxonomy_is_cites_eu_cites_listed__idx (taxonomy_is_cites_eu,cites_listed,kingdom_position)
# idx_mtaxon_cites_csv (cites_show,name_status,cites_listing_original,taxonomy_is_cites_eu,rank_name)
# idx_mtaxon_cms_csv (cms_show,name_status,cms_listing_original,taxonomy_is_cites_eu,rank_name)
# idx_mtaxon_eu_csv (eu_show,name_status,eu_listing_original,taxonomy_is_cites_eu,rank_name)
# idx_mtaxon_id (id)
# idx_mtaxon_id_countries_ids (countries_ids_ary) USING gin
# idx_mtaxon_kingdom_position (taxonomy_is_cites_eu,cites_listed,kingdom_position)
# idx_mtaxon_parent_id (parent_id)
#

class MTaxonConcept < ApplicationRecord
Expand Down
6 changes: 3 additions & 3 deletions app/models/taxon_common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ class TaxonCommon < ApplicationRecord

# Rspec file such as `spec/shared/agave.rb`, which assign taxon_concept.common_names = [array of common_name] broken
# if we remove `optional: true`, although it should be false.
belongs_to :common_name, optional: true
belongs_to :taxon_concept, optional: true
belongs_to :common_name, optional: Rails.env.test?
belongs_to :taxon_concept, optional: Rails.env.test?

# rspec ./spec/controllers/admin/taxon_commons_controller_spec.rb borken if we remove the following validates.
validates :common_name_id, presence: true
validates :common_name_id, presence: true if Rails.env.test?

before_validation do
# TaxonCommons can share CommonNames so we don't want to overwrite the
Expand Down
Loading