mycowriter 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3cced08d1171031840e8c3fe3464a16cf344ef8665540a9ac879bbc985509beb
4
+ data.tar.gz: 296ff6a5870048603ed5d9a9e712fc51b9b8865f77fb26de7f54707235377435
5
+ SHA512:
6
+ metadata.gz: 8bdbaac7edd4d193639c5e0ac78d55a7898ee3de5233ac759955f1bb8b173618fc4eb181216baec09c68c699d0ba75b8a1eafbf096c8ea19c713c3625a073735
7
+ data.tar.gz: 01140af242628c80ae50b695c12e0138f11dd0d7adfb3abab55993f88b7936ec6f4ed630642a03910496f225a50d891611840db5acc18d9f6bca062b80a48ed3
data/CHANGELOG.md ADDED
@@ -0,0 +1,39 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.1] - 2025-02-20
9
+
10
+ ### Changed
11
+ - **BREAKING:** Changed default `min_characters` from 3 to 4 to reduce false matches
12
+ - Replaced `case_sensitive` config with `require_uppercase` (default: true)
13
+ - Updated JavaScript controller to validate uppercase first letter for genus names
14
+ - Genus names must now start with uppercase letter (e.g., "Agaricus" not "agaricus")
15
+
16
+ ### Added
17
+ - Optional migration generator: `rails generate mycowriter:mb_lists_migration`
18
+ - Comprehensive MycoBank data download and import instructions in README
19
+ - Uppercase validation with helpful user feedback
20
+ - Configuration option `require_uppercase` to enforce genus name capitalization
21
+ - Example import script for MycoBank MBList.xlsx data
22
+
23
+ ### Fixed
24
+ - Prevented constant autocomplete triggers from lowercase input
25
+ - Improved user feedback messages for validation errors
26
+
27
+ ## [0.1.0] - 2025-02-20
28
+
29
+ ### Added
30
+ - Initial release
31
+ - Real-time autocomplete for genus and species names
32
+ - Token-based UI with visual pills and remove buttons
33
+ - Debounced search (150ms delay)
34
+ - MycoBank data integration via mb_lists table
35
+ - Configurable minimum characters and results limit
36
+ - Support for both mb_lists table and custom Genus/Species models
37
+ - Automatic genus filtering for species autocomplete
38
+ - AJAX save/delete for associations
39
+ - Comprehensive documentation and usage examples
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Will Johnston
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,231 @@
1
+ # Mycowriter
2
+
3
+ Intelligent taxonomic autocomplete for genus and species names in Rails applications. Mycowriter provides real-time autocomplete functionality matching against MycoBank taxonomic data, perfect for mycology, biology, and taxonomy-focused applications.
4
+
5
+ ## Features
6
+
7
+ - 🔍 **Real-time autocomplete** for genus and species names
8
+ - 🏷️ **Token-based UI** with visual pills and remove buttons
9
+ - ⚡ **Debounced search** (150ms) for optimal performance
10
+ - 🎯 **Smart validation** - default 4 characters minimum, uppercase first letter
11
+ - 📊 **MycoBank integration** via mb_lists table
12
+ - 🔧 **Highly configurable** via initializer
13
+ - 🎨 **Framework agnostic** - works with any CSS framework
14
+ - ♿ **Accessible** with ARIA labels and keyboard support
15
+
16
+ ## Installation
17
+
18
+ Add this line to your application's Gemfile:
19
+
20
+ ```ruby
21
+ gem 'mycowriter'
22
+ ```
23
+
24
+ And then execute:
25
+
26
+ ```bash
27
+ bundle install
28
+ rails generate mycowriter:install
29
+ ```
30
+
31
+ The generator will:
32
+ - Create an initializer at `config/initializers/mycowriter.rb`
33
+ - Mount the engine in your `config/routes.rb`
34
+ - Display setup instructions
35
+
36
+ ## Requirements
37
+
38
+ ### Database Schema
39
+
40
+ **Option 1: Use the optional migration generator (recommended for new projects)**
41
+
42
+ ```bash
43
+ rails generate mycowriter:mb_lists_migration
44
+ rails db:migrate
45
+ ```
46
+
47
+ This creates the `mb_lists` table with proper indexes and character encoding for taxonomic data.
48
+
49
+ **Option 2: Use your own Genus/Species models**
50
+
51
+ Mycowriter will automatically detect and use existing `Genus` and `Species` models in your application. No migration needed!
52
+
53
+ ### Getting MycoBank Data
54
+
55
+ After creating the `mb_lists` table, you need to populate it with taxonomic data:
56
+
57
+ 1. **Download MycoBank MBList file:**
58
+ - Visit: https://www.mycobank.org/page/Simple%20names%20search
59
+ - Or direct download: https://www.mycobank.org/MBList.xlsx
60
+ - File contains comprehensive fungal taxonomic data
61
+
62
+ 2. **Import the data:**
63
+
64
+ ```ruby
65
+ # Example import script using 'roo' gem
66
+ require 'roo'
67
+
68
+ xlsx = Roo::Spreadsheet.open('MBList.xlsx')
69
+ xlsx.sheet(0).each_row_streaming(offset: 1) do |row|
70
+ MbList.create!(
71
+ mblist_id: row[0]&.value,
72
+ taxon_name: row[1]&.value,
73
+ authors: row[2]&.value,
74
+ rank_name: row[3]&.value,
75
+ year_of_effective_publication: row[4]&.value,
76
+ name_status: row[5]&.value,
77
+ mycobank_number: row[6]&.value,
78
+ hyperlink: row[7]&.value,
79
+ classification: row[8]&.value,
80
+ current_name: row[9]&.value,
81
+ synonymy: row[10]&.value
82
+ )
83
+ end
84
+ ```
85
+
86
+ 3. **License Requirements (IMPORTANT):**
87
+
88
+ MycoBank data is licensed under **Creative Commons CC BY-NC-ND**:
89
+ - **BY (Attribution):** Must credit MycoBank
90
+ - **NC (Non-Commercial):** Non-commercial use only
91
+ - **ND (No Derivatives):** Use data in unadapted form
92
+
93
+ **Required Attribution:**
94
+ > "MBList taxonomic data provided by MycoBank (www.mycobank.org)"
95
+
96
+ ### JavaScript Setup
97
+
98
+ Register the Stimulus controller in your `app/javascript/controllers/application.js`:
99
+
100
+ ```javascript
101
+ import { application } from "./application"
102
+ import MycowriterAutocomplete from "mycowriter/autocomplete_controller"
103
+
104
+ application.register("mycowriter--autocomplete", MycowriterAutocomplete)
105
+ ```
106
+
107
+ ## Usage
108
+
109
+ ### Basic Genus Autocomplete
110
+
111
+ ```erb
112
+ <div data-controller="mycowriter--autocomplete"
113
+ data-mycowriter--autocomplete-url-value="<%= mycowriter.genera_autocomplete_path %>"
114
+ data-mycowriter--autocomplete-kind-value="genera"
115
+ data-mycowriter--autocomplete-mushroom-id-value="<%= @mushroom.id %>">
116
+
117
+ <input type="text"
118
+ data-mycowriter--autocomplete-target="input"
119
+ placeholder="Type genus name (e.g., Agaricus)..."
120
+ minlength="4"
121
+ class="form-input" />
122
+
123
+ <ul data-mycowriter--autocomplete-target="dropdown"
124
+ class="hidden absolute bg-white border shadow-lg"></ul>
125
+
126
+ <div data-mycowriter--autocomplete-target="list"
127
+ class="flex flex-wrap mt-2"></div>
128
+
129
+ <input type="hidden"
130
+ data-mycowriter--autocomplete-target="hiddenIds"
131
+ name="mushroom[genus_ids]" />
132
+ </div>
133
+ ```
134
+
135
+ ### Species Autocomplete (with Genus Filter)
136
+
137
+ ```erb
138
+ <div data-controller="mycowriter--autocomplete"
139
+ data-mycowriter--autocomplete-url-value="<%= mycowriter.species_autocomplete_path %>"
140
+ data-mycowriter--autocomplete-kind-value="species"
141
+ data-mycowriter--autocomplete-mushroom-id-value="<%= @mushroom.id %>">
142
+
143
+ <input type="text"
144
+ data-mycowriter--autocomplete-target="input"
145
+ placeholder="Type species name..."
146
+ minlength="4" />
147
+
148
+ <ul data-mycowriter--autocomplete-target="dropdown" class="hidden"></ul>
149
+ <div data-mycowriter--autocomplete-target="list"></div>
150
+ </div>
151
+ ```
152
+
153
+ ## Configuration
154
+
155
+ Configure Mycowriter in `config/initializers/mycowriter.rb`:
156
+
157
+ ```ruby
158
+ Mycowriter.configure do |config|
159
+ # Minimum number of characters before autocomplete triggers (default: 4)
160
+ config.min_characters = 4
161
+
162
+ # Require uppercase first letter for genus names (default: true)
163
+ # Genus names always start with capital letter (e.g., Agaricus, not agaricus)
164
+ config.require_uppercase = true
165
+
166
+ # Maximum number of results to return (default: 20)
167
+ config.results_limit = 20
168
+ end
169
+ ```
170
+
171
+ ## Data Targets
172
+
173
+ The Stimulus controller expects these targets:
174
+
175
+ - `input` - The text input field
176
+ - `dropdown` - Container for autocomplete suggestions
177
+ - `list` - Container for selected tokens (pills)
178
+ - `hiddenIds` - Hidden input to store selected IDs
179
+ - `loader` (optional) - Loading indicator
180
+
181
+ ## Data Values
182
+
183
+ - `url` - API endpoint for autocomplete
184
+ - `min` - Minimum characters (default: 3)
185
+ - `mushroomId` - ID for filtering related records
186
+ - `kind` - Type of autocomplete (genera, species, trees, plants)
187
+
188
+ ## Routes
189
+
190
+ The gem provides these routes:
191
+
192
+ ```ruby
193
+ GET /mycowriter/autocomplete/genera # Genus autocomplete
194
+ GET /mycowriter/autocomplete/species # Species autocomplete
195
+ ```
196
+
197
+ ## Integration with Pundit/Devise
198
+
199
+ The gem automatically skips authentication and authorization checks for the autocomplete endpoints. This is configured in the initializer and can be customized.
200
+
201
+ ## Data Attribution
202
+
203
+ This gem is designed to work with MycoBank taxonomic data:
204
+
205
+ **MycoBank License:** Creative Commons CC BY-NC-ND
206
+ - **BY (Attribution):** Credit must be given to MycoBank
207
+ - **NC (Non-Commercial):** Non-commercial use only
208
+ - **ND (No Derivatives):** Data used in unadapted form only
209
+
210
+ **Attribution:** MBList taxonomic data provided by MycoBank (www.mycobank.org)
211
+
212
+ ## Development
213
+
214
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
215
+
216
+ To install this gem onto your local machine, run `bundle exec rake install`.
217
+
218
+ ## Contributing
219
+
220
+ Bug reports and pull requests are welcome on GitHub at https://github.com/mrdbidwill/mycowriter.
221
+
222
+ ## License
223
+
224
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
225
+
226
+ ## Links
227
+
228
+ - **Website:** https://mycowriter.com
229
+ - **Source Code:** https://github.com/mrdbidwill/mycowriter
230
+ - **Documentation:** https://mycowriter.com/docs
231
+ - **MycoBank:** https://www.mycobank.org
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mycowriter
4
+ class AutocompleteController < ApplicationController
5
+ skip_after_action :verify_authorized, raise: false
6
+ skip_before_action :authenticate_user!, raise: false if respond_to?(:authenticate_user!)
7
+
8
+ # GET /mycowriter/autocomplete/genera.json?q=aga
9
+ def genera
10
+ query = params[:q].to_s.strip
11
+ results = if query.length >= Mycowriter.min_characters
12
+ # Check if the host app has a Genus model
13
+ if defined?(::Genus)
14
+ ::Genus
15
+ .where("name LIKE ?", "#{::Genus.sanitize_sql_like(query)}%")
16
+ .select(:id, :name)
17
+ .order(:name)
18
+ .limit(Mycowriter.results_limit)
19
+ .map { |g| { id: g.id, name: g.name } }
20
+ else
21
+ # Fallback to mb_lists table
22
+ autocomplete_from_mb_lists(query, rank: 'genus')
23
+ end
24
+ else
25
+ []
26
+ end
27
+ render json: results
28
+ end
29
+
30
+ # GET /mycowriter/autocomplete/species.json?q=placo&mushroom_id=1
31
+ def species
32
+ query = params[:q].to_s.strip
33
+ mushroom_id = params[:mushroom_id]
34
+
35
+ results = if query.length >= Mycowriter.min_characters
36
+ # Check if the host app has a Species model
37
+ if defined?(::Species)
38
+ scope = ::Species.where("name LIKE ?", "%#{::Species.sanitize_sql_like(query)}%")
39
+
40
+ # Filter by selected genera for this mushroom
41
+ if mushroom_id.present? && defined?(::Mushroom)
42
+ mushroom = ::Mushroom.find_by(id: mushroom_id)
43
+ if mushroom && mushroom.respond_to?(:genera) && mushroom.genera.any?
44
+ genera_ids = mushroom.genera.pluck(:id)
45
+ scope = scope.where(genera_id: genera_ids)
46
+ end
47
+ end
48
+
49
+ # Use includes to eager load genera and avoid N+1 queries
50
+ species_results = scope
51
+ .includes(:genus)
52
+ .select(:id, :name, :genera_id)
53
+ .order(:name)
54
+ .limit(Mycowriter.results_limit)
55
+
56
+ species_results.map do |sp|
57
+ genus_label = sp.respond_to?(:genus) && sp.genus ? "#{sp.genus.name} " : ""
58
+ { id: sp.id, name: "#{genus_label}#{sp.name}" }
59
+ end
60
+ else
61
+ # Fallback to mb_lists table
62
+ autocomplete_from_mb_lists(query, rank: 'species')
63
+ end
64
+ else
65
+ []
66
+ end
67
+ render json: results
68
+ end
69
+
70
+ private
71
+
72
+ # Fallback method to search mb_lists table if host app doesn't have Genus/Species models
73
+ def autocomplete_from_mb_lists(query, rank: nil)
74
+ return [] unless defined?(::MbList)
75
+
76
+ scope = ::MbList.where("taxon_name LIKE ?", "#{::MbList.sanitize_sql_like(query)}%")
77
+ scope = scope.where(rank_name: rank) if rank.present?
78
+
79
+ scope
80
+ .select(:id, :taxon_name)
81
+ .order(:taxon_name)
82
+ .limit(Mycowriter.results_limit)
83
+ .map { |m| { id: m.id, name: m.taxon_name } }
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,270 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Mycowriter Autocomplete Controller
4
+ // Provides token-based autocomplete for genus/species names
5
+ //
6
+ // Usage: Attach data-controller="mycowriter--autocomplete" and appropriate data attributes.
7
+ // Requirements: An autocomplete endpoint that returns [{id, name}] in JSON.
8
+
9
+ export default class extends Controller {
10
+ static targets = [
11
+ "input", "dropdown", "list", "hiddenIds", "loader"
12
+ ]
13
+ static values = {
14
+ url: String,
15
+ min: { type: Number, default: 4 },
16
+ mushroomId: String,
17
+ kind: String,
18
+ requireUppercase: { type: Boolean, default: true }
19
+ }
20
+
21
+ connect() {
22
+ this.selected = new Map()
23
+ this.debounceTimer = null
24
+ this.inputTarget.addEventListener("input", this.onInput.bind(this))
25
+ this.inputTarget.addEventListener("focus", this.onInput.bind(this))
26
+ this.listTarget.querySelectorAll("[data-token-id]").forEach(el => {
27
+ this.selected.set(el.dataset.tokenId, el.textContent.trim())
28
+ })
29
+ this.dropdownTarget.addEventListener("click", this.onDropdownClick.bind(this))
30
+ }
31
+
32
+ onInput() {
33
+ clearTimeout(this.debounceTimer)
34
+ let query = this.inputTarget.value.trim()
35
+
36
+ const minLength = parseInt(this.inputTarget.getAttribute("minlength")) || this.minValue
37
+
38
+ // Check for uppercase requirement (genus names start with capital letter)
39
+ if(this.requireUppercaseValue && query.length > 0 && query[0] !== query[0].toUpperCase()) {
40
+ this.dropdownTarget.innerHTML = `<li class='px-3 py-2 text-sm text-gray-400'>Genus names start with uppercase (e.g., Agaricus)</li>`
41
+ this.dropdownTarget.classList.remove("hidden")
42
+ return
43
+ }
44
+
45
+ // Show immediate feedback
46
+ if(query.length > 0 && query.length < minLength) {
47
+ this.dropdownTarget.innerHTML = `<li class='px-3 py-2 text-sm text-gray-400'>Type ${minLength - query.length} more character(s)...</li>`
48
+ this.dropdownTarget.classList.remove("hidden")
49
+ return
50
+ }
51
+
52
+ if(query.length < minLength) {
53
+ this.hideDropdown()
54
+ return
55
+ }
56
+
57
+ // Show loading immediately before debounce
58
+ this.showLoader()
59
+ this.debounceTimer = setTimeout(() => {
60
+ this.autocomplete(query)
61
+ }, 150)
62
+ }
63
+
64
+ autocomplete(query) {
65
+ this.showLoader()
66
+ const url = new URL(this.urlValue, window.location.origin)
67
+ url.searchParams.append("q", query)
68
+
69
+ // For species autocomplete, pass mushroom_id to filter by selected genera
70
+ if(this.kindValue === "species" && this.hasMushroomIdValue) {
71
+ url.searchParams.append("mushroom_id", this.mushroomIdValue)
72
+ }
73
+
74
+ fetch(url, { headers: {"Accept": "application/json"} })
75
+ .then(r => r.json())
76
+ .then(items => {
77
+ this.renderDropdown(items)
78
+ })
79
+ .catch(_err => this.hideDropdown())
80
+ .finally(() => this.hideLoader())
81
+ }
82
+
83
+ renderDropdown(items) {
84
+ if(!items.length) {
85
+ this.dropdownTarget.innerHTML = "<li class='px-3 py-2 text-sm text-gray-400'>No results</li>"
86
+ this.dropdownTarget.classList.remove("hidden")
87
+ return
88
+ }
89
+ this.dropdownTarget.innerHTML = items
90
+ .map(item =>
91
+ `<li class="hover:bg-blue-100 cursor-pointer px-3 py-2"
92
+ data-id="${item.id}"
93
+ data-label="${item.name}">
94
+ ${item.name}
95
+ </li>`
96
+ ).join("")
97
+ this.dropdownTarget.classList.remove("hidden")
98
+ }
99
+
100
+ hideDropdown() {
101
+ this.dropdownTarget.classList.add("hidden")
102
+ this.dropdownTarget.innerHTML = ""
103
+ }
104
+
105
+ showLoader() {
106
+ if(this.hasLoaderTarget) this.loaderTarget.classList.remove("hidden")
107
+ }
108
+ hideLoader() {
109
+ if(this.hasLoaderTarget) this.loaderTarget.classList.add("hidden")
110
+ }
111
+
112
+ onDropdownClick(e) {
113
+ const li = e.target.closest("li[data-id]")
114
+ if(!li) return
115
+ const id = li.dataset.id
116
+ const label = li.dataset.label
117
+ if(this.selected.has(id)) {
118
+ this.hideDropdown()
119
+ this.inputTarget.value = ""
120
+ return
121
+ }
122
+ this.createToken({id, label})
123
+ this.hideDropdown()
124
+ this.inputTarget.value = ""
125
+ this.inputTarget.focus()
126
+ // Save via AJAX
127
+ this.saveToken(id, label)
128
+ }
129
+
130
+ createToken({id, label}) {
131
+ if(this.selected.has(id)) return
132
+ this.selected.set(id, label)
133
+ // Create pill
134
+ const pill = document.createElement("span")
135
+ pill.className = "inline-flex items-center bg-gray-200 rounded px-2 py-0.5 text-sm mr-1 mb-1"
136
+ pill.setAttribute("data-token-id", id)
137
+ pill.innerHTML = `
138
+ ${label}
139
+ <button type="button" class="ml-1 text-gray-600 hover:text-red-600" aria-label="Remove" data-action="mycowriter--autocomplete#removeToken" data-id="${id}">&times;</button>
140
+ `
141
+ this.listTarget.appendChild(pill)
142
+ this.updateHiddenIds()
143
+ }
144
+
145
+ removeToken(e) {
146
+ const id = e.target.dataset.id
147
+ const pill = this.listTarget.querySelector(`[data-token-id="${id}"]`)
148
+ const itemName = pill ? pill.textContent.trim().replace('×', '').trim() : 'this item'
149
+
150
+ // Show confirmation dialog
151
+ if (!confirm(`Remove ${itemName}?`)) {
152
+ return
153
+ }
154
+
155
+ // Remove via AJAX first, then update UI on success
156
+ this.deleteToken(id, () => {
157
+ // Success callback
158
+ this.selected.delete(id)
159
+ if(pill) pill.remove()
160
+ this.updateHiddenIds()
161
+ }, () => {
162
+ // Error callback - pill stays in place
163
+ })
164
+ }
165
+
166
+ updateHiddenIds() {
167
+ if(this.hasHiddenIdsTarget) {
168
+ this.hiddenIdsTarget.value = Array.from(this.selected.keys()).join(",")
169
+ }
170
+ }
171
+
172
+ // AJAX save for genus, species, tree, or plant association
173
+ saveToken(id, label) {
174
+ const kind = this.kindValue
175
+ const mushroomId = this.mushroomIdValue
176
+ if(!kind || !mushroomId) return
177
+ let route, body
178
+ if(kind=="genera") {
179
+ route = `/genus_mushrooms.json`
180
+ body = JSON.stringify({genus_mushroom: {mushroom_id: mushroomId, genus_id: id}})
181
+ } else if (kind=="species") {
182
+ route = `/mushroom_species.json`
183
+ body = JSON.stringify({mushroom_species: {mushroom_id: mushroomId, species_id: id}})
184
+ } else if (kind=="trees") {
185
+ route = `/mushroom_trees.json`
186
+ body = JSON.stringify({mushroom_tree: {mushroom_id: mushroomId, tree_id: id}})
187
+ } else if (kind=="plants") {
188
+ route = `/mushroom_plants.json`
189
+ body = JSON.stringify({mushroom_plant: {mushroom_id: mushroomId, plant_id: id}})
190
+ } else {
191
+ return
192
+ }
193
+ fetch(route, {
194
+ method: "POST",
195
+ headers: { "Content-Type": "application/json", "Accept": "application/json", "X-CSRF-Token": this.csrfToken() },
196
+ body: body
197
+ }).then(resp => {
198
+ if(!resp.ok) {
199
+ return resp.json().then(data => {
200
+ throw new Error(data.errors ? data.errors.join(", ") : "Save failed")
201
+ })
202
+ }
203
+ // Optionally: visual feedback
204
+ }).catch(err => {
205
+ // Remove pill if failed
206
+ this.removePillById(id)
207
+ alert(err.message || "Failed to add. Please try again.")
208
+ })
209
+ }
210
+
211
+ deleteToken(id, onSuccess, onError) {
212
+ const kind = this.kindValue
213
+ const mushroomId = this.mushroomIdValue
214
+ if(!kind || !mushroomId) {
215
+ if(onError) onError()
216
+ return
217
+ }
218
+ let route
219
+ if(kind=="genera") {
220
+ route = `/genus_mushrooms/destroy_by_relation.json?mushroom_id=${mushroomId}&genus_id=${id}`
221
+ } else if (kind=="species") {
222
+ route = `/mushroom_species/destroy_by_relation.json?mushroom_id=${mushroomId}&species_id=${id}`
223
+ } else if (kind=="trees") {
224
+ route = `/mushroom_trees/destroy_by_relation.json?mushroom_id=${mushroomId}&tree_id=${id}`
225
+ } else if (kind=="plants") {
226
+ route = `/mushroom_plants/destroy_by_relation.json?mushroom_id=${mushroomId}&plant_id=${id}`
227
+ } else {
228
+ if(onError) onError()
229
+ return
230
+ }
231
+ fetch(route, {
232
+ method: "DELETE",
233
+ headers: {"Accept": "application/json", "X-CSRF-Token": this.csrfToken()}
234
+ }).then(resp => {
235
+ if(!resp.ok) {
236
+ // Try to get the response text to see what's actually being returned
237
+ return resp.text().then(text => {
238
+ console.error('Delete failed. Status:', resp.status, 'Response:', text)
239
+ try {
240
+ const data = JSON.parse(text)
241
+ throw new Error(data.message || `Delete failed (${resp.status})`)
242
+ } catch(e) {
243
+ throw new Error(`Delete failed (${resp.status}): ${text.substring(0, 100)}`)
244
+ }
245
+ })
246
+ }
247
+ return resp.json()
248
+ }).then(_data => {
249
+ // Success
250
+ console.log('Delete successful')
251
+ if(onSuccess) onSuccess()
252
+ }).catch(err => {
253
+ console.error('Delete error:', err)
254
+ alert(err.message || "Failed to remove. Please try again.")
255
+ if(onError) onError()
256
+ })
257
+ }
258
+
259
+ removePillById(id) {
260
+ const pill = this.listTarget.querySelector(`[data-token-id="${id}"]`)
261
+ if(pill) pill.remove()
262
+ this.selected.delete(id)
263
+ this.updateHiddenIds()
264
+ }
265
+
266
+ csrfToken() {
267
+ const meta = document.querySelector('meta[name=csrf-token]')
268
+ return meta && meta.content
269
+ }
270
+ }
data/config/routes.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ Mycowriter::Engine.routes.draw do
4
+ get "autocomplete/genera", to: "autocomplete#genera", as: :genera_autocomplete, defaults: { format: :json }
5
+ get "autocomplete/species", to: "autocomplete#species", as: :species_autocomplete, defaults: { format: :json }
6
+ end
@@ -0,0 +1,40 @@
1
+ ===============================================================================
2
+
3
+ Mycowriter has been installed successfully!
4
+
5
+ Next steps:
6
+
7
+ 1. Run database migrations if you haven't already:
8
+ rails db:migrate
9
+
10
+ 2. Ensure you have the mb_lists table in your database with taxonomic data.
11
+ The gem expects a table with at least these columns:
12
+ - id
13
+ - taxon_name
14
+ - rank_name (optional, for filtering by taxonomic rank)
15
+
16
+ 3. Import your controller in app/javascript/controllers/application.js:
17
+
18
+ import MycowriterAutocomplete from "mycowriter/autocomplete_controller"
19
+ application.register("mycowriter--autocomplete", MycowriterAutocomplete)
20
+
21
+ 4. Use the autocomplete in your views:
22
+
23
+ <div data-controller="mycowriter--autocomplete"
24
+ data-mycowriter--autocomplete-url-value="/mycowriter/autocomplete/genera"
25
+ data-mycowriter--autocomplete-kind-value="genera"
26
+ data-mycowriter--autocomplete-mushroom-id-value="<%= @mushroom.id %>">
27
+
28
+ <input type="text"
29
+ data-mycowriter--autocomplete-target="input"
30
+ placeholder="Type genus name..." />
31
+
32
+ <ul data-mycowriter--autocomplete-target="dropdown" class="hidden"></ul>
33
+ <div data-mycowriter--autocomplete-target="list"></div>
34
+ </div>
35
+
36
+ 5. Configure Mycowriter in config/initializers/mycowriter.rb
37
+
38
+ For more information, visit https://mycowriter.com
39
+
40
+ ===============================================================================
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mycowriter
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ desc "Creates Mycowriter initializer and mounts the engine"
9
+
10
+ def copy_initializer
11
+ template "mycowriter.rb", "config/initializers/mycowriter.rb"
12
+ end
13
+
14
+ def add_route
15
+ route 'mount Mycowriter::Engine => "/mycowriter"'
16
+ end
17
+
18
+ def show_readme
19
+ readme "README" if behavior == :invoke
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Mycowriter configuration
4
+ Mycowriter.configure do |config|
5
+ # Minimum number of characters required before autocomplete triggers
6
+ # Default: 4 (prevents excessive matches like "a", "ag", "aga")
7
+ config.min_characters = 4
8
+
9
+ # Require uppercase first letter for genus names
10
+ # Default: true (genus names always start with capital letter: Agaricus, not agaricus)
11
+ config.require_uppercase = true
12
+
13
+ # Maximum number of results to return
14
+ # Default: 20
15
+ config.results_limit = 20
16
+ end
17
+
18
+ # Skip Pundit authorization and Devise authentication for Mycowriter engine controllers
19
+ # This ensures the autocomplete functionality works without requiring user authentication
20
+ Rails.application.config.to_prepare do
21
+ Mycowriter::AutocompleteController.class_eval do
22
+ skip_after_action :verify_authorized, raise: false
23
+ skip_after_action :verify_policy_scoped, raise: false
24
+ skip_before_action :authenticate_user!, raise: false if respond_to?(:authenticate_user!)
25
+ end
26
+ end
@@ -0,0 +1,55 @@
1
+ ===============================================================================
2
+
3
+ Migration for mb_lists table created successfully!
4
+
5
+ Next steps:
6
+
7
+ 1. Run the migration:
8
+ rails db:migrate
9
+
10
+ 2. Download MycoBank data:
11
+
12
+ Visit: https://www.mycobank.org/page/Simple%20names%20search
13
+
14
+ Or use direct download link:
15
+ https://www.mycobank.org/MBList.xlsx
16
+
17
+ The MBList file contains comprehensive taxonomic data for fungi.
18
+
19
+ 3. Import the data into your database:
20
+
21
+ You can use a rake task or script to import the Excel file.
22
+ Example approach:
23
+
24
+ - Save the file as CSV
25
+ - Use the 'roo' gem to read Excel files
26
+ - Import into mb_lists table
27
+
28
+ Sample import code:
29
+
30
+ require 'roo'
31
+ xlsx = Roo::Spreadsheet.open('path/to/MBList.xlsx')
32
+ xlsx.each_row_streaming(offset: 1) do |row|
33
+ MbList.create!(
34
+ taxon_name: row[0]&.value,
35
+ rank_name: row[1]&.value,
36
+ authors: row[2]&.value,
37
+ # ... map other columns
38
+ )
39
+ end
40
+
41
+ 4. MycoBank License Requirements:
42
+
43
+ Creative Commons CC BY-NC-ND
44
+ - Attribution: Credit must be given to MycoBank
45
+ - Non-Commercial: Non-commercial use only
46
+ - No Derivatives: Data used in unadapted form only
47
+
48
+ Required Attribution:
49
+ "MBList taxonomic data provided by MycoBank (www.mycobank.org)"
50
+
51
+ For more information:
52
+ - MycoBank: https://www.mycobank.org
53
+ - Mycowriter gem: https://github.com/mrdbidwill/mycowriter
54
+
55
+ ===============================================================================
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mycowriter
4
+ module Generators
5
+ class MbListsMigrationGenerator < Rails::Generators::Base
6
+ include Rails::Generators::Migration
7
+ source_root File.expand_path("templates", __dir__)
8
+
9
+ desc "Creates migration for mb_lists table to store MycoBank taxonomic data"
10
+
11
+ def self.next_migration_number(dirname)
12
+ next_migration_number = current_migration_number(dirname) + 1
13
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
14
+ end
15
+
16
+ def copy_migration
17
+ migration_template "create_mb_lists.rb", "db/migrate/create_mb_lists.rb"
18
+ end
19
+
20
+ def show_readme
21
+ readme "README" if behavior == :invoke
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,39 @@
1
+ class CreateMbLists < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ create_table :mb_lists do |t|
4
+ t.text :mblist_id
5
+ t.text :taxon_name
6
+ t.text :authors
7
+ t.text :rank_name
8
+ t.text :year_of_effective_publication
9
+ t.text :name_status
10
+ t.text :mycobank_number
11
+ t.text :hyperlink
12
+ t.text :classification
13
+ t.text :current_name
14
+ t.text :synonymy
15
+ t.timestamps
16
+ end
17
+
18
+ # Set character encoding for UTF-8 support (MySQL/MariaDB)
19
+ execute <<-SQL
20
+ ALTER TABLE mb_lists
21
+ CONVERT TO CHARACTER SET utf8mb4
22
+ COLLATE utf8mb4_0900_as_cs;
23
+ SQL
24
+
25
+ # Add indexes for performance
26
+ add_index :mb_lists, [:taxon_name, :rank_name],
27
+ name: "index_mblists_on_taxon_name_and_rank_name",
28
+ length: { taxon_name: 255, rank_name: 255 }
29
+ add_index :mb_lists, :taxon_name,
30
+ name: "index_mblists_on_taxon_name",
31
+ length: 255
32
+ add_index :mb_lists, :rank_name,
33
+ name: "index_mblists_on_rank_name",
34
+ length: 255
35
+ add_index :mb_lists, :name_status,
36
+ name: "index_mblists_on_name_status",
37
+ length: 255
38
+ end
39
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mycowriter
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Mycowriter
6
+
7
+ config.generators do |g|
8
+ g.test_framework :rspec
9
+ g.fixture_replacement :factory_bot
10
+ g.factory_bot dir: 'spec/factories'
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mycowriter
4
+ VERSION = "0.1.1"
5
+ end
data/lib/mycowriter.rb ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mycowriter/version"
4
+ require "mycowriter/engine"
5
+
6
+ module Mycowriter
7
+ # Configuration options
8
+ mattr_accessor :min_characters
9
+ @@min_characters = 4
10
+
11
+ mattr_accessor :require_uppercase
12
+ @@require_uppercase = true
13
+
14
+ mattr_accessor :results_limit
15
+ @@results_limit = 20
16
+
17
+ def self.configure
18
+ yield self
19
+ end
20
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mycowriter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Will Johnston
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ description: Mycowriter provides real-time autocomplete functionality for genus and
41
+ species names, matching against MycoBank taxonomic data. Perfect for mycology, biology,
42
+ and taxonomy-focused applications.
43
+ email:
44
+ - mrdbidwill@gmail.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - CHANGELOG.md
50
+ - LICENSE
51
+ - README.md
52
+ - app/controllers/mycowriter/autocomplete_controller.rb
53
+ - app/javascript/mycowriter/autocomplete_controller.js
54
+ - config/routes.rb
55
+ - lib/generators/mycowriter/install/README
56
+ - lib/generators/mycowriter/install/install_generator.rb
57
+ - lib/generators/mycowriter/install/templates/mycowriter.rb
58
+ - lib/generators/mycowriter/mb_lists_migration/README
59
+ - lib/generators/mycowriter/mb_lists_migration/mb_lists_migration_generator.rb
60
+ - lib/generators/mycowriter/mb_lists_migration/templates/create_mb_lists.rb
61
+ - lib/mycowriter.rb
62
+ - lib/mycowriter/engine.rb
63
+ - lib/mycowriter/version.rb
64
+ homepage: https://mycowriter.com
65
+ licenses:
66
+ - MIT
67
+ metadata:
68
+ allowed_push_host: https://rubygems.org
69
+ homepage_uri: https://mycowriter.com
70
+ source_code_uri: https://github.com/mrdbidwill/mycowriter
71
+ changelog_uri: https://github.com/mrdbidwill/mycowriter/blob/main/CHANGELOG.md
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: 3.2.0
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ requirements: []
86
+ rubygems_version: 3.7.1
87
+ specification_version: 4
88
+ summary: Intelligent taxonomic autocomplete for genus and species names in Rails applications
89
+ test_files: []