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 +7 -0
- data/CHANGELOG.md +39 -0
- data/LICENSE +21 -0
- data/README.md +231 -0
- data/app/controllers/mycowriter/autocomplete_controller.rb +86 -0
- data/app/javascript/mycowriter/autocomplete_controller.js +270 -0
- data/config/routes.rb +6 -0
- data/lib/generators/mycowriter/install/README +40 -0
- data/lib/generators/mycowriter/install/install_generator.rb +23 -0
- data/lib/generators/mycowriter/install/templates/mycowriter.rb +26 -0
- data/lib/generators/mycowriter/mb_lists_migration/README +55 -0
- data/lib/generators/mycowriter/mb_lists_migration/mb_lists_migration_generator.rb +25 -0
- data/lib/generators/mycowriter/mb_lists_migration/templates/create_mb_lists.rb +39 -0
- data/lib/mycowriter/engine.rb +13 -0
- data/lib/mycowriter/version.rb +5 -0
- data/lib/mycowriter.rb +20 -0
- metadata +89 -0
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}">×</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
|
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: []
|