better_model 1.2.0 → 2.0.0
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 +4 -4
- data/README.md +211 -11
- data/lib/better_model/archivable.rb +3 -2
- data/lib/better_model/predicable.rb +73 -52
- data/lib/better_model/schedulable/occurrence_calculator.rb +1034 -0
- data/lib/better_model/schedulable/schedule_builder.rb +269 -0
- data/lib/better_model/schedulable.rb +356 -0
- data/lib/better_model/searchable.rb +4 -4
- data/lib/better_model/taggable.rb +466 -0
- data/lib/better_model/traceable.rb +123 -9
- data/lib/better_model/version.rb +1 -1
- data/lib/better_model/version_record.rb +6 -3
- data/lib/better_model.rb +6 -0
- data/lib/generators/better_model/taggable/taggable_generator.rb +129 -0
- data/lib/generators/better_model/taggable/templates/README.tt +62 -0
- data/lib/generators/better_model/taggable/templates/migration.rb.tt +21 -0
- metadata +9 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 56752c6458384a8ebb0bf5eb0e6e02adbd8bc822ce817705cc85d0502abce8aa
|
|
4
|
+
data.tar.gz: 8b8ce273a9b81d572f044154dd46486d1ad06086a7585b302875cd34069d28fc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a509b30bdb21978f8f75cced201a5b4bc6bc77b96dfd7a28dba6242c3d8b274984d6bf7e1d2f73902d404fab47eda51edb7b67cd8fcca918e3f85f5ad745f9c9
|
|
7
|
+
data.tar.gz: 69a04f82516a2de9bfa837091ee4823c7c4036f764d9cd8ad1e12615cdeac291929a0f491320c1a698fd2f9d6988e904716b7c08ad942d2683f16a4e99a3eb3c
|
data/README.md
CHANGED
|
@@ -94,6 +94,9 @@ class Article < ApplicationRecord
|
|
|
94
94
|
# 8. TRACEABLE - Audit trail with time-travel (opt-in)
|
|
95
95
|
traceable do
|
|
96
96
|
track :title, :content, :status, :published_at
|
|
97
|
+
track :password_hash, sensitive: :full # Complete redaction
|
|
98
|
+
track :credit_card, sensitive: :partial # Pattern-based masking
|
|
99
|
+
track :api_token, sensitive: :hash # SHA256 hashing
|
|
97
100
|
versions_table :article_versions # Optional: custom table
|
|
98
101
|
end
|
|
99
102
|
|
|
@@ -104,6 +107,17 @@ class Article < ApplicationRecord
|
|
|
104
107
|
default_order [:sort_published_at_desc]
|
|
105
108
|
security :status_required, [:status_eq]
|
|
106
109
|
end
|
|
110
|
+
|
|
111
|
+
# 10. TAGGABLE - Tag management with statistics (opt-in)
|
|
112
|
+
taggable do
|
|
113
|
+
tag_field :tags
|
|
114
|
+
normalize true # Automatic lowercase
|
|
115
|
+
strip true # Remove whitespace
|
|
116
|
+
min_length 2 # Minimum tag length
|
|
117
|
+
max_length 30 # Maximum tag length
|
|
118
|
+
delimiter "," # CSV delimiter
|
|
119
|
+
validates_tags minimum: 1, maximum: 10
|
|
120
|
+
end
|
|
107
121
|
end
|
|
108
122
|
```
|
|
109
123
|
|
|
@@ -172,6 +186,22 @@ Article.search(
|
|
|
172
186
|
orders: [:sort_published_at_desc],
|
|
173
187
|
pagination: { page: 1, per_page: 25 }
|
|
174
188
|
)
|
|
189
|
+
|
|
190
|
+
# 🏷️ Manage tags
|
|
191
|
+
article.tag_with("ruby", "rails", "tutorial")
|
|
192
|
+
article.untag("tutorial")
|
|
193
|
+
article.tagged_with?("ruby") # => true
|
|
194
|
+
article.tag_list = "ruby, rails, api"
|
|
195
|
+
|
|
196
|
+
# 📊 Query with tags (via Predicable)
|
|
197
|
+
Article.tags_contains("ruby")
|
|
198
|
+
Article.tags_overlaps(["ruby", "python"])
|
|
199
|
+
Article.tags_contains_all(["ruby", "rails"])
|
|
200
|
+
|
|
201
|
+
# 📈 Tag statistics
|
|
202
|
+
Article.tag_counts # => {"ruby" => 45, "rails" => 38}
|
|
203
|
+
Article.popular_tags(limit: 10) # => [["ruby", 45], ["rails", 38]]
|
|
204
|
+
Article.related_tags("ruby", limit: 5) # => ["rails", "gem", "tutorial"]
|
|
175
205
|
```
|
|
176
206
|
|
|
177
207
|
### 🎯 Including Individual Concerns (Advanced)
|
|
@@ -189,26 +219,168 @@ class Article < ApplicationRecord
|
|
|
189
219
|
include BetterModel::Validatable # Only validations
|
|
190
220
|
include BetterModel::Stateable # Only state machine
|
|
191
221
|
include BetterModel::Searchable # Only search (requires Predicable & Sortable)
|
|
222
|
+
include BetterModel::Taggable # Only tag management
|
|
192
223
|
|
|
193
224
|
# Define your features...
|
|
194
225
|
end
|
|
195
226
|
```
|
|
196
227
|
|
|
228
|
+
## 🛠️ Generators
|
|
229
|
+
|
|
230
|
+
Better Model provides Rails generators to help you quickly set up migrations for features that require database tables or columns.
|
|
231
|
+
|
|
232
|
+
### Traceable Generator
|
|
233
|
+
|
|
234
|
+
Create migrations for audit trail version tables:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
# Basic usage - shows setup instructions
|
|
238
|
+
rails g better_model:traceable Article
|
|
239
|
+
|
|
240
|
+
# Create migration for versions table
|
|
241
|
+
rails g better_model:traceable Article --create-table
|
|
242
|
+
|
|
243
|
+
# Custom table name
|
|
244
|
+
rails g better_model:traceable Article --create-table --table-name=audit_log
|
|
245
|
+
|
|
246
|
+
# Run migrations
|
|
247
|
+
rails db:migrate
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Generated migration includes:**
|
|
251
|
+
- Polymorphic association (`item_type`, `item_id`)
|
|
252
|
+
- Event tracking (`created`, `updated`, `destroyed`)
|
|
253
|
+
- Change tracking (`object_changes` as JSON)
|
|
254
|
+
- User attribution (`updated_by_id`)
|
|
255
|
+
- Change reason (`updated_reason`)
|
|
256
|
+
- Optimized indexes
|
|
257
|
+
|
|
258
|
+
### Archivable Generator
|
|
259
|
+
|
|
260
|
+
Add soft-delete columns to existing models:
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
# Basic usage - shows setup instructions
|
|
264
|
+
rails g better_model:archivable Article
|
|
265
|
+
|
|
266
|
+
# Add archivable columns to articles table
|
|
267
|
+
rails g better_model:archivable Article --create-columns
|
|
268
|
+
|
|
269
|
+
# Run migrations
|
|
270
|
+
rails db:migrate
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**Generated migration adds:**
|
|
274
|
+
- `archived_at` (datetime) - when archived
|
|
275
|
+
- `archived_by_id` (integer) - who archived it
|
|
276
|
+
- `archive_reason` (string) - why archived
|
|
277
|
+
- Index on `archived_at`
|
|
278
|
+
|
|
279
|
+
### Stateable Generator
|
|
280
|
+
|
|
281
|
+
Create state machine with state column and transitions tracking:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
# Basic usage - shows setup instructions
|
|
285
|
+
rails g better_model:stateable Article
|
|
286
|
+
|
|
287
|
+
# Create both state column and transitions table
|
|
288
|
+
rails g better_model:stateable Article --create-tables
|
|
289
|
+
|
|
290
|
+
# Custom initial state (default: draft)
|
|
291
|
+
rails g better_model:stateable Article --create-tables --initial-state=pending
|
|
292
|
+
|
|
293
|
+
# Custom transitions table name
|
|
294
|
+
rails g better_model:stateable Article --create-tables --table-name=article_state_history
|
|
295
|
+
|
|
296
|
+
# Run migrations
|
|
297
|
+
rails db:migrate
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Generated migrations include:**
|
|
301
|
+
1. **State column migration:**
|
|
302
|
+
- `state` (string) with default value and index
|
|
303
|
+
|
|
304
|
+
2. **Transitions table migration:**
|
|
305
|
+
- Polymorphic association (`transitionable_type`, `transitionable_id`)
|
|
306
|
+
- Event name and state tracking
|
|
307
|
+
- Optional metadata (JSON)
|
|
308
|
+
- Optimized indexes
|
|
309
|
+
|
|
310
|
+
### Taggable Generator
|
|
311
|
+
|
|
312
|
+
Add tags column with database-specific optimizations:
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
# Basic usage - adds tags column
|
|
316
|
+
rails g better_model:taggable Article
|
|
317
|
+
|
|
318
|
+
# Custom column name
|
|
319
|
+
rails g better_model:taggable Article --column-name=categories
|
|
320
|
+
|
|
321
|
+
# Skip GIN index (PostgreSQL only)
|
|
322
|
+
rails g better_model:taggable Article --skip-index
|
|
323
|
+
|
|
324
|
+
# Run migrations
|
|
325
|
+
rails db:migrate
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**Generated migration includes:**
|
|
329
|
+
- **PostgreSQL**: Native array column (`array: true, default: []`) with GIN index for optimal performance
|
|
330
|
+
- **SQLite/MySQL**: Text column with serialization instructions
|
|
331
|
+
- Database-specific detection and optimization
|
|
332
|
+
- Automatic index creation for PostgreSQL (GIN index)
|
|
333
|
+
|
|
334
|
+
**Database-specific behavior:**
|
|
335
|
+
- PostgreSQL: Ready to use immediately with native array support
|
|
336
|
+
- SQLite/MySQL: Requires adding `serialize :tags, coder: JSON, type: Array` to model
|
|
337
|
+
|
|
338
|
+
### Generator Options
|
|
339
|
+
|
|
340
|
+
All generators support these common options:
|
|
341
|
+
|
|
342
|
+
- `--pretend` - Dry run, show what would be generated
|
|
343
|
+
- `--skip-model` - Only generate migrations, don't show model setup instructions
|
|
344
|
+
- `--force` - Overwrite existing files
|
|
345
|
+
|
|
346
|
+
**Example workflow:**
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
# 1. Generate migrations (dry-run first to preview)
|
|
350
|
+
rails g better_model:traceable Article --create-table --pretend
|
|
351
|
+
rails g better_model:archivable Article --create-columns --pretend
|
|
352
|
+
rails g better_model:stateable Article --create-tables --pretend
|
|
353
|
+
rails g better_model:taggable Article --pretend
|
|
354
|
+
|
|
355
|
+
# 2. Generate for real
|
|
356
|
+
rails g better_model:traceable Article --create-table
|
|
357
|
+
rails g better_model:archivable Article --create-columns
|
|
358
|
+
rails g better_model:stateable Article --create-tables
|
|
359
|
+
rails g better_model:taggable Article
|
|
360
|
+
|
|
361
|
+
# 3. Run migrations
|
|
362
|
+
rails db:migrate
|
|
363
|
+
|
|
364
|
+
# 4. Enable in your model (generators show you the code)
|
|
365
|
+
# See model setup instructions after running each generator
|
|
366
|
+
```
|
|
367
|
+
|
|
197
368
|
## 📋 Features Overview
|
|
198
369
|
|
|
199
|
-
BetterModel provides
|
|
370
|
+
BetterModel provides ten powerful concerns that work seamlessly together:
|
|
200
371
|
|
|
201
372
|
### Core Features
|
|
202
373
|
|
|
203
374
|
- **✨ Statusable** - Declarative status management with lambda-based conditions
|
|
204
375
|
- **🔐 Permissible** - State-based permission system
|
|
205
376
|
- **🗄️ Archivable** - Soft delete with tracking (by user, reason)
|
|
206
|
-
- **⏰ Traceable**
|
|
377
|
+
- **⏰ Traceable** - Complete audit trail with time-travel and rollback
|
|
207
378
|
- **⬆️ Sortable** - Type-aware sorting scopes
|
|
208
379
|
- **🔍 Predicable** - Advanced filtering with rich predicate system
|
|
209
380
|
- **🔎 Searchable** - Unified search interface (Predicable + Sortable)
|
|
210
381
|
- **✅ Validatable** - Declarative validation DSL with conditional rules
|
|
211
|
-
- **🔄 Stateable**
|
|
382
|
+
- **🔄 Stateable** - Declarative state machines with guards & callbacks
|
|
383
|
+
- **🏷️ Taggable** 🆕 - Tag management with normalization, validation, and statistics
|
|
212
384
|
|
|
213
385
|
[See all features in detail →](#-features)
|
|
214
386
|
|
|
@@ -329,10 +501,11 @@ Track all changes to your records with complete audit trail, time-travel capabil
|
|
|
329
501
|
**🎯 Key Benefits:**
|
|
330
502
|
- 🎛️ Opt-in activation: only enabled when explicitly configured
|
|
331
503
|
- 📝 Automatic change tracking on create, update, and destroy
|
|
504
|
+
- 🔐 Sensitive data protection: 3-level redaction system (full, partial, hash)
|
|
332
505
|
- 👤 User attribution: track who made each change
|
|
333
506
|
- 💬 Change reasons: optional context for changes
|
|
334
507
|
- ⏰ Time-travel: reconstruct object state at any point in history
|
|
335
|
-
- ↩️ Rollback support: restore records to previous versions
|
|
508
|
+
- ↩️ Rollback support: restore records to previous versions (with sensitive field protection)
|
|
336
509
|
- 🔍 Rich query API: find changes by user, time, or field transitions
|
|
337
510
|
- 📊 Flexible table naming: per-model, shared, or custom tables
|
|
338
511
|
- 🔗 Polymorphic association for efficient storage
|
|
@@ -391,6 +564,24 @@ Orchestrate Predicable and Sortable into a powerful, secure search interface wit
|
|
|
391
564
|
|
|
392
565
|
---
|
|
393
566
|
|
|
567
|
+
### 🏷️ Taggable - Tag Management with Statistics
|
|
568
|
+
|
|
569
|
+
Manage tags with automatic normalization, validation, and comprehensive statistics - integrated with Predicable for powerful searches.
|
|
570
|
+
|
|
571
|
+
**🎯 Key Benefits:**
|
|
572
|
+
- 🎛️ Opt-in activation: only enabled when explicitly configured
|
|
573
|
+
- 🤖 Automatic normalization (lowercase, strip, length limits)
|
|
574
|
+
- ✅ Validation (min/max count, whitelist, blacklist)
|
|
575
|
+
- 📊 Statistics (tag counts, popularity, co-occurrence)
|
|
576
|
+
- 🔍 Automatic Predicable integration for searches
|
|
577
|
+
- 📝 CSV import/export with tag_list
|
|
578
|
+
- 🐘 PostgreSQL arrays or serialized JSON for SQLite
|
|
579
|
+
- 🎯 Thread-safe configuration
|
|
580
|
+
|
|
581
|
+
**[📖 Full Documentation →](docs/taggable.md)** | **[📚 Examples →](docs/examples/11_taggable.md)**
|
|
582
|
+
|
|
583
|
+
---
|
|
584
|
+
|
|
394
585
|
### 📜 Traceable - Audit Trail & Change Tracking
|
|
395
586
|
|
|
396
587
|
Track all changes to your records with comprehensive audit trail functionality, time-travel queries, and rollback capabilities.
|
|
@@ -532,9 +723,7 @@ article.update!(title: "Corrected Title")
|
|
|
532
723
|
|
|
533
724
|
---
|
|
534
725
|
|
|
535
|
-
## 📌
|
|
536
|
-
|
|
537
|
-
**Current Version:** 1.0.0
|
|
726
|
+
## 📌 Changelog
|
|
538
727
|
|
|
539
728
|
See [CHANGELOG.md](CHANGELOG.md) for version history and release notes.
|
|
540
729
|
|
|
@@ -623,12 +812,13 @@ bundle exec rubocop
|
|
|
623
812
|
|
|
624
813
|
### 📊 Test Coverage Notes
|
|
625
814
|
|
|
626
|
-
The test suite runs on **SQLite** for performance and portability. Current coverage: **
|
|
815
|
+
The test suite runs on **SQLite** for performance and portability. Current coverage: **92.57%** (1507 / 1628 lines).
|
|
627
816
|
|
|
628
817
|
**Database-Specific Features Not Covered:**
|
|
629
|
-
- **Predicable**: PostgreSQL array predicates (`_overlaps`, `_contains`, `_contained_by`) and JSONB predicates (`_has_key`, `_has_any_key`, `_has_all_keys`, `_jsonb_contains`)
|
|
630
|
-
- **Traceable**: PostgreSQL JSONB queries and MySQL JSON_EXTRACT queries for field-specific change tracking
|
|
631
|
-
- **Sortable**: MySQL NULLS emulation with CASE statements
|
|
818
|
+
- **Predicable**: PostgreSQL array predicates (`_overlaps`, `_contains`, `_contained_by`) and JSONB predicates (`_has_key`, `_has_any_key`, `_has_all_keys`, `_jsonb_contains`)
|
|
819
|
+
- **Traceable**: PostgreSQL JSONB queries and MySQL JSON_EXTRACT queries for field-specific change tracking
|
|
820
|
+
- **Sortable**: MySQL NULLS emulation with CASE statements
|
|
821
|
+
- **Taggable**: PostgreSQL native array operations (covered by Predicable tests)
|
|
632
822
|
|
|
633
823
|
These features are fully implemented with proper SQL sanitization but require manual testing on PostgreSQL/MySQL:
|
|
634
824
|
|
|
@@ -652,3 +842,13 @@ All code has inline comments marking database-specific sections for maintainabil
|
|
|
652
842
|
## 📝 License
|
|
653
843
|
|
|
654
844
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
845
|
+
|
|
846
|
+
---
|
|
847
|
+
|
|
848
|
+
<div align="center">
|
|
849
|
+
|
|
850
|
+
**Made with ❤️ by [Alessio Bussolari](https://github.com/alessiobussolari)**
|
|
851
|
+
|
|
852
|
+
[Report Bug](https://github.com/alessiobussolari/better_model/issues) · [Request Feature](https://github.com/alessiobussolari/better_model/issues) · [Documentation](https://github.com/alessiobussolari/better_model)
|
|
853
|
+
|
|
854
|
+
</div>
|
|
@@ -104,8 +104,9 @@ module BetterModel
|
|
|
104
104
|
sort :archived_at unless sortable_field?(:archived_at)
|
|
105
105
|
|
|
106
106
|
# Definisci gli scope alias (approccio ibrido)
|
|
107
|
-
|
|
108
|
-
scope :
|
|
107
|
+
# v2.0.0: predicates require parameters
|
|
108
|
+
scope :archived, -> { archived_at_present(true) }
|
|
109
|
+
scope :not_archived, -> { archived_at_null(true) }
|
|
109
110
|
|
|
110
111
|
# Configura se passato un blocco
|
|
111
112
|
if block_given?
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
# Article.title_i_cont("rails") # WHERE LOWER(title) LIKE '%rails%'
|
|
18
18
|
# Article.view_count_gt(100) # WHERE view_count > 100
|
|
19
19
|
# Article.published_at_lteq(Date.today) # WHERE published_at <= '2025-10-29'
|
|
20
|
-
# Article.
|
|
20
|
+
# Article.featured_eq(true) # WHERE featured = TRUE
|
|
21
21
|
# Article.status_in(["draft", "published"]) # WHERE status IN ('draft', 'published')
|
|
22
22
|
#
|
|
23
23
|
module BetterModel
|
|
@@ -44,7 +44,7 @@ module BetterModel
|
|
|
44
44
|
# Genera automaticamente scope di filtro basati sul tipo di colonna:
|
|
45
45
|
# - String: _eq, _not_eq, _matches, _start, _end, _cont, _not_cont, _i_cont, _not_i_cont, _in, _not_in, _present, _blank, _null
|
|
46
46
|
# - Numeric: _eq, _not_eq, _lt, _lteq, _gt, _gteq, _in, _not_in, _present
|
|
47
|
-
# - Boolean: _eq, _not_eq, _true
|
|
47
|
+
# - Boolean: _eq, _not_eq, _present (Note: _true and _false removed in v2.0.0 - use _eq(true/false))
|
|
48
48
|
# - Date: _eq, _not_eq, _lt, _lteq, _gt, _gteq, _in, _not_in, _present, _blank, _null, _not_null
|
|
49
49
|
#
|
|
50
50
|
# Esempio:
|
|
@@ -151,9 +151,17 @@ module BetterModel
|
|
|
151
151
|
scope :"#{field_name}_not_eq", ->(value) { where(field.not_eq(value)) }
|
|
152
152
|
|
|
153
153
|
# Presence - skip for string/text types as they get specialized version
|
|
154
|
-
#
|
|
154
|
+
# Required boolean parameter:
|
|
155
|
+
# - true: returns present records (NOT NULL)
|
|
156
|
+
# - false: returns null records (NULL)
|
|
155
157
|
unless [ :string, :text ].include?(column_type)
|
|
156
|
-
scope :"#{field_name}_present", ->
|
|
158
|
+
scope :"#{field_name}_present", ->(condition) {
|
|
159
|
+
if condition
|
|
160
|
+
where(field.not_eq(nil))
|
|
161
|
+
else
|
|
162
|
+
where(field.eq(nil))
|
|
163
|
+
end
|
|
164
|
+
}
|
|
157
165
|
end
|
|
158
166
|
|
|
159
167
|
scopes_to_register = [ :"#{field_name}_eq", :"#{field_name}_not_eq" ]
|
|
@@ -170,7 +178,16 @@ module BetterModel
|
|
|
170
178
|
field = table[field_name]
|
|
171
179
|
|
|
172
180
|
# String-specific presence check (checks both nil and empty string)
|
|
173
|
-
|
|
181
|
+
# Required boolean parameter:
|
|
182
|
+
# - true: returns present records (NOT NULL AND NOT '')
|
|
183
|
+
# - false: returns blank records (NULL OR '')
|
|
184
|
+
scope :"#{field_name}_present", ->(condition) {
|
|
185
|
+
if condition
|
|
186
|
+
where(field.not_eq(nil).and(field.not_eq("")))
|
|
187
|
+
else
|
|
188
|
+
where(field.eq(nil).or(field.eq("")))
|
|
189
|
+
end
|
|
190
|
+
}
|
|
174
191
|
|
|
175
192
|
# Pattern matching (4)
|
|
176
193
|
scope :"#{field_name}_matches", ->(pattern) { where(field.matches(pattern)) }
|
|
@@ -205,9 +222,24 @@ module BetterModel
|
|
|
205
222
|
scope :"#{field_name}_in", ->(values) { where(field.in(Array(values))) }
|
|
206
223
|
scope :"#{field_name}_not_in", ->(values) { where.not(field.in(Array(values))) }
|
|
207
224
|
|
|
208
|
-
# Presence (2)
|
|
209
|
-
|
|
210
|
-
|
|
225
|
+
# Presence (2)
|
|
226
|
+
# Required boolean parameter:
|
|
227
|
+
# - true: returns blank/null records
|
|
228
|
+
# - false: returns present/not-null records
|
|
229
|
+
scope :"#{field_name}_blank", ->(condition) {
|
|
230
|
+
if condition
|
|
231
|
+
where(field.eq(nil).or(field.eq("")))
|
|
232
|
+
else
|
|
233
|
+
where(field.not_eq(nil).and(field.not_eq("")))
|
|
234
|
+
end
|
|
235
|
+
}
|
|
236
|
+
scope :"#{field_name}_null", ->(condition) {
|
|
237
|
+
if condition
|
|
238
|
+
where(field.eq(nil))
|
|
239
|
+
else
|
|
240
|
+
where(field.not_eq(nil))
|
|
241
|
+
end
|
|
242
|
+
}
|
|
211
243
|
|
|
212
244
|
register_predicable_scopes(
|
|
213
245
|
:"#{field_name}_matches",
|
|
@@ -258,20 +290,16 @@ module BetterModel
|
|
|
258
290
|
)
|
|
259
291
|
end
|
|
260
292
|
|
|
261
|
-
# Genera predicati per campi booleani
|
|
293
|
+
# Genera predicati per campi booleani
|
|
262
294
|
# Base predicates (_eq, _not_eq, _present) are defined separately
|
|
295
|
+
#
|
|
296
|
+
# Note: In v2.0.0, we removed _true and _false predicates as they were
|
|
297
|
+
# redundant with the base _eq predicate. Use these instead:
|
|
298
|
+
# field_eq(true) # instead of field_true
|
|
299
|
+
# field_eq(false) # instead of field_false
|
|
263
300
|
def define_boolean_predicates(field_name)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
# Boolean shortcuts (2)
|
|
268
|
-
scope :"#{field_name}_true", -> { where(field.eq(true)) }
|
|
269
|
-
scope :"#{field_name}_false", -> { where(field.eq(false)) }
|
|
270
|
-
|
|
271
|
-
register_predicable_scopes(
|
|
272
|
-
:"#{field_name}_true",
|
|
273
|
-
:"#{field_name}_false"
|
|
274
|
-
)
|
|
301
|
+
# Boolean fields only use base predicates (_eq, _not_eq, _present)
|
|
302
|
+
# No additional predicates needed
|
|
275
303
|
end
|
|
276
304
|
|
|
277
305
|
# Genera predicati per campi array PostgreSQL (3 scope)
|
|
@@ -403,28 +431,7 @@ module BetterModel
|
|
|
403
431
|
scope :"#{field_name}_in", ->(values) { where(field.in(Array(values))) }
|
|
404
432
|
scope :"#{field_name}_not_in", ->(values) { where.not(field.in(Array(values))) }
|
|
405
433
|
|
|
406
|
-
# Date
|
|
407
|
-
scope :"#{field_name}_today", -> {
|
|
408
|
-
where(field.between(Date.current.beginning_of_day..Date.current.end_of_day))
|
|
409
|
-
}
|
|
410
|
-
scope :"#{field_name}_yesterday", -> {
|
|
411
|
-
where(field.between(1.day.ago.beginning_of_day..1.day.ago.end_of_day))
|
|
412
|
-
}
|
|
413
|
-
scope :"#{field_name}_this_week", -> {
|
|
414
|
-
where(field.gteq(Date.current.beginning_of_week))
|
|
415
|
-
}
|
|
416
|
-
scope :"#{field_name}_this_month", -> {
|
|
417
|
-
where(field.gteq(Date.current.beginning_of_month))
|
|
418
|
-
}
|
|
419
|
-
scope :"#{field_name}_this_year", -> {
|
|
420
|
-
where(field.gteq(Date.current.beginning_of_year))
|
|
421
|
-
}
|
|
422
|
-
scope :"#{field_name}_past", -> {
|
|
423
|
-
where(field.lt(Time.current))
|
|
424
|
-
}
|
|
425
|
-
scope :"#{field_name}_future", -> {
|
|
426
|
-
where(field.gt(Time.current))
|
|
427
|
-
}
|
|
434
|
+
# Date time-based filter (1)
|
|
428
435
|
scope :"#{field_name}_within", ->(duration) {
|
|
429
436
|
# Auto-detect: ActiveSupport::Duration or numeric (days)
|
|
430
437
|
time_ago = duration.respond_to?(:ago) ? duration.ago : duration.to_i.days.ago
|
|
@@ -432,9 +439,30 @@ module BetterModel
|
|
|
432
439
|
}
|
|
433
440
|
|
|
434
441
|
# Presence (3) - _present is defined in base predicates
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
442
|
+
# Required boolean parameter:
|
|
443
|
+
# - true: returns blank/null records
|
|
444
|
+
# - false: returns not-blank/not-null records
|
|
445
|
+
scope :"#{field_name}_blank", ->(condition) {
|
|
446
|
+
if condition
|
|
447
|
+
where(field.eq(nil))
|
|
448
|
+
else
|
|
449
|
+
where(field.not_eq(nil))
|
|
450
|
+
end
|
|
451
|
+
}
|
|
452
|
+
scope :"#{field_name}_null", ->(condition) {
|
|
453
|
+
if condition
|
|
454
|
+
where(field.eq(nil))
|
|
455
|
+
else
|
|
456
|
+
where(field.not_eq(nil))
|
|
457
|
+
end
|
|
458
|
+
}
|
|
459
|
+
scope :"#{field_name}_not_null", ->(condition) {
|
|
460
|
+
if condition
|
|
461
|
+
where(field.not_eq(nil))
|
|
462
|
+
else
|
|
463
|
+
where(field.eq(nil))
|
|
464
|
+
end
|
|
465
|
+
}
|
|
438
466
|
|
|
439
467
|
register_predicable_scopes(
|
|
440
468
|
:"#{field_name}_lt",
|
|
@@ -445,13 +473,6 @@ module BetterModel
|
|
|
445
473
|
:"#{field_name}_not_between",
|
|
446
474
|
:"#{field_name}_in",
|
|
447
475
|
:"#{field_name}_not_in",
|
|
448
|
-
:"#{field_name}_today",
|
|
449
|
-
:"#{field_name}_yesterday",
|
|
450
|
-
:"#{field_name}_this_week",
|
|
451
|
-
:"#{field_name}_this_month",
|
|
452
|
-
:"#{field_name}_this_year",
|
|
453
|
-
:"#{field_name}_past",
|
|
454
|
-
:"#{field_name}_future",
|
|
455
476
|
:"#{field_name}_within",
|
|
456
477
|
:"#{field_name}_blank",
|
|
457
478
|
:"#{field_name}_null",
|