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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec236d26e4c1e0a5a62e9932606a1baa28de1bcadf8b447f1da545fb35d029b6
4
- data.tar.gz: 24e4aa004924ab09c6104cad770229041b8c8d24556ce5f2f2d190bf15071e90
3
+ metadata.gz: 56752c6458384a8ebb0bf5eb0e6e02adbd8bc822ce817705cc85d0502abce8aa
4
+ data.tar.gz: 8b8ce273a9b81d572f044154dd46486d1ad06086a7585b302875cd34069d28fc
5
5
  SHA512:
6
- metadata.gz: 77d291e54bcbbb179eabd842290530d0f42e03636981e93bfdeb6575d5d0e02960d359072c0b0db0144f35cdbea647d463bf7320037c557552f6fce8365a393e
7
- data.tar.gz: bfe3bf61206cb343dce4d84b2ff643e719c09b93a623de77e127a00aadbce646d25bfcc2cd66ca3bd725b6af224a82eb7cff2e059f79f45c8e026d5d878c95d6
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 nine powerful concerns that work seamlessly together:
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** 🆕 - Complete audit trail with time-travel and rollback
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** 🆕 - Declarative state machines with guards & callbacks
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
- ## 📌 Version & Changelog
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: **91.45%** (1272 / 1391 lines).
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`) - lines 278-376 in `lib/better_model/predicable.rb`
630
- - **Traceable**: PostgreSQL JSONB queries and MySQL JSON_EXTRACT queries for field-specific change tracking - lines 454-489 in `lib/better_model/traceable.rb`
631
- - **Sortable**: MySQL NULLS emulation with CASE statements - lines 198-203 in `lib/better_model/sortable.rb`
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
- scope :archived, -> { archived_at_present }
108
- scope :not_archived, -> { archived_at_null }
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.featured_true # WHERE featured = TRUE
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, _false, _present
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
- # String types need to check for both nil and empty string
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", -> { where(field.not_eq(nil)) }
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
- scope :"#{field_name}_present", -> { where(field.not_eq(nil).and(field.not_eq(""))) }
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) - _present is overridden above for string-specific behavior
209
- scope :"#{field_name}_blank", -> { where(field.eq(nil).or(field.eq(""))) }
210
- scope :"#{field_name}_null", -> { where(field.eq(nil)) }
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 (2 scope)
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
- table = arel_table
265
- field = table[field_name]
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 convenience shortcuts (8)
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
- scope :"#{field_name}_blank", -> { where(field.eq(nil)) }
436
- scope :"#{field_name}_null", -> { where(field.eq(nil)) }
437
- scope :"#{field_name}_not_null", -> { where(field.not_eq(nil)) }
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",