agent-notes 2.0.4__py3-none-any.whl
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.
- agent_notes/VERSION +1 -0
- agent_notes/__init__.py +1 -0
- agent_notes/__main__.py +4 -0
- agent_notes/cli.py +348 -0
- agent_notes/commands/__init__.py +27 -0
- agent_notes/commands/_install_helpers.py +262 -0
- agent_notes/commands/build.py +170 -0
- agent_notes/commands/doctor.py +112 -0
- agent_notes/commands/info.py +95 -0
- agent_notes/commands/install.py +99 -0
- agent_notes/commands/list.py +169 -0
- agent_notes/commands/memory.py +430 -0
- agent_notes/commands/regenerate.py +152 -0
- agent_notes/commands/set_role.py +143 -0
- agent_notes/commands/uninstall.py +26 -0
- agent_notes/commands/update.py +169 -0
- agent_notes/commands/validate.py +199 -0
- agent_notes/commands/wizard.py +720 -0
- agent_notes/config.py +154 -0
- agent_notes/data/agents/agents.yaml +352 -0
- agent_notes/data/agents/analyst.md +45 -0
- agent_notes/data/agents/api-reviewer.md +47 -0
- agent_notes/data/agents/architect.md +46 -0
- agent_notes/data/agents/coder.md +28 -0
- agent_notes/data/agents/database-specialist.md +45 -0
- agent_notes/data/agents/debugger.md +47 -0
- agent_notes/data/agents/devil.md +47 -0
- agent_notes/data/agents/devops.md +38 -0
- agent_notes/data/agents/explorer.md +23 -0
- agent_notes/data/agents/integrations.md +44 -0
- agent_notes/data/agents/lead.md +216 -0
- agent_notes/data/agents/performance-profiler.md +44 -0
- agent_notes/data/agents/refactorer.md +48 -0
- agent_notes/data/agents/reviewer.md +44 -0
- agent_notes/data/agents/security-auditor.md +44 -0
- agent_notes/data/agents/system-auditor.md +38 -0
- agent_notes/data/agents/tech-writer.md +32 -0
- agent_notes/data/agents/test-runner.md +36 -0
- agent_notes/data/agents/test-writer.md +39 -0
- agent_notes/data/cli/claude.yaml +25 -0
- agent_notes/data/cli/copilot.yaml +18 -0
- agent_notes/data/cli/opencode.yaml +22 -0
- agent_notes/data/commands/brainstorm.md +8 -0
- agent_notes/data/commands/debug.md +9 -0
- agent_notes/data/commands/review.md +10 -0
- agent_notes/data/global-claude.md +290 -0
- agent_notes/data/global-copilot.md +27 -0
- agent_notes/data/global-opencode.md +40 -0
- agent_notes/data/hooks/session-context.md.tpl +19 -0
- agent_notes/data/models/claude-haiku-4-5.yaml +15 -0
- agent_notes/data/models/claude-opus-4-1.yaml +16 -0
- agent_notes/data/models/claude-opus-4-5.yaml +16 -0
- agent_notes/data/models/claude-opus-4-6.yaml +16 -0
- agent_notes/data/models/claude-opus-4-7.yaml +15 -0
- agent_notes/data/models/claude-sonnet-4-5.yaml +16 -0
- agent_notes/data/models/claude-sonnet-4-6.yaml +15 -0
- agent_notes/data/models/claude-sonnet-4.yaml +16 -0
- agent_notes/data/pricing.yaml +33 -0
- agent_notes/data/roles/orchestrator.yaml +5 -0
- agent_notes/data/roles/reasoner.yaml +5 -0
- agent_notes/data/roles/scout.yaml +5 -0
- agent_notes/data/roles/worker.yaml +5 -0
- agent_notes/data/rules/code-quality.md +9 -0
- agent_notes/data/rules/safety.md +10 -0
- agent_notes/data/scripts/cost-report +211 -0
- agent_notes/data/skills/brainstorming/SKILL.md +57 -0
- agent_notes/data/skills/code-review/SKILL.md +64 -0
- agent_notes/data/skills/debugging-protocol/SKILL.md +51 -0
- agent_notes/data/skills/docker-compose/SKILL.md +318 -0
- agent_notes/data/skills/docker-compose-advanced/SKILL.md +575 -0
- agent_notes/data/skills/docker-dockerfile/SKILL.md +385 -0
- agent_notes/data/skills/docker-dockerfile-languages/SKILL.md +293 -0
- agent_notes/data/skills/git/SKILL.md +87 -0
- agent_notes/data/skills/rails-active-storage/SKILL.md +321 -0
- agent_notes/data/skills/rails-broadcasting/SKILL.md +374 -0
- agent_notes/data/skills/rails-concerns/SKILL.md +806 -0
- agent_notes/data/skills/rails-controllers/SKILL.md +510 -0
- agent_notes/data/skills/rails-controllers-advanced/SKILL.md +441 -0
- agent_notes/data/skills/rails-helpers/SKILL.md +677 -0
- agent_notes/data/skills/rails-initializers/SKILL.md +79 -0
- agent_notes/data/skills/rails-javascript/SKILL.md +567 -0
- agent_notes/data/skills/rails-jobs/SKILL.md +700 -0
- agent_notes/data/skills/rails-kamal/SKILL.md +483 -0
- agent_notes/data/skills/rails-lib/SKILL.md +101 -0
- agent_notes/data/skills/rails-mailers/SKILL.md +321 -0
- agent_notes/data/skills/rails-migrations/SKILL.md +268 -0
- agent_notes/data/skills/rails-models/SKILL.md +459 -0
- agent_notes/data/skills/rails-models-advanced/SKILL.md +398 -0
- agent_notes/data/skills/rails-routes/SKILL.md +804 -0
- agent_notes/data/skills/rails-style/SKILL.md +538 -0
- agent_notes/data/skills/rails-testing-controllers/SKILL.md +343 -0
- agent_notes/data/skills/rails-testing-models/SKILL.md +296 -0
- agent_notes/data/skills/rails-testing-system/SKILL.md +375 -0
- agent_notes/data/skills/rails-validations/SKILL.md +108 -0
- agent_notes/data/skills/rails-view-components/SKILL.md +511 -0
- agent_notes/data/skills/rails-view-components-advanced/SKILL.md +376 -0
- agent_notes/data/skills/rails-views/SKILL.md +413 -0
- agent_notes/data/skills/rails-views-advanced/SKILL.md +450 -0
- agent_notes/data/skills/refactoring-protocol/SKILL.md +64 -0
- agent_notes/data/skills/tdd/SKILL.md +57 -0
- agent_notes/data/templates/__init__.py +1 -0
- agent_notes/data/templates/__pycache__/__init__.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__init__.py +1 -0
- agent_notes/data/templates/frontmatter/__pycache__/__init__.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/claude.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/cursor.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/opencode.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/claude.py +44 -0
- agent_notes/data/templates/frontmatter/opencode.py +104 -0
- agent_notes/doctor_checks.py +189 -0
- agent_notes/domain/__init__.py +17 -0
- agent_notes/domain/agent.py +34 -0
- agent_notes/domain/cli_backend.py +40 -0
- agent_notes/domain/diagnostics.py +29 -0
- agent_notes/domain/diff.py +44 -0
- agent_notes/domain/model.py +27 -0
- agent_notes/domain/role.py +13 -0
- agent_notes/domain/rule.py +13 -0
- agent_notes/domain/skill.py +15 -0
- agent_notes/domain/state.py +46 -0
- agent_notes/install_state.py +11 -0
- agent_notes/registries/__init__.py +16 -0
- agent_notes/registries/_base.py +46 -0
- agent_notes/registries/agent_registry.py +107 -0
- agent_notes/registries/cli_registry.py +89 -0
- agent_notes/registries/model_registry.py +85 -0
- agent_notes/registries/role_registry.py +64 -0
- agent_notes/registries/rule_registry.py +80 -0
- agent_notes/registries/skill_registry.py +141 -0
- agent_notes/services/__init__.py +8 -0
- agent_notes/services/diagnostics/__init__.py +47 -0
- agent_notes/services/diagnostics/_checks.py +272 -0
- agent_notes/services/diagnostics/_display.py +346 -0
- agent_notes/services/diagnostics/_fix.py +169 -0
- agent_notes/services/diff.py +349 -0
- agent_notes/services/fs.py +195 -0
- agent_notes/services/install_state_builder.py +210 -0
- agent_notes/services/installer.py +293 -0
- agent_notes/services/memory_backend.py +155 -0
- agent_notes/services/rendering.py +329 -0
- agent_notes/services/session_context.py +23 -0
- agent_notes/services/settings_writer.py +79 -0
- agent_notes/services/state_store.py +249 -0
- agent_notes/services/ui.py +419 -0
- agent_notes/services/user_config.py +62 -0
- agent_notes/services/validation.py +67 -0
- agent_notes/state.py +21 -0
- agent_notes-2.0.4.dist-info/METADATA +14 -0
- agent_notes-2.0.4.dist-info/RECORD +162 -0
- agent_notes-2.0.4.dist-info/WHEEL +5 -0
- agent_notes-2.0.4.dist-info/entry_points.txt +2 -0
- agent_notes-2.0.4.dist-info/licenses/LICENSE +21 -0
- agent_notes-2.0.4.dist-info/top_level.txt +2 -0
- tests/conftest.py +20 -0
- tests/functional/__init__.py +0 -0
- tests/functional/test_build_commands.py +88 -0
- tests/functional/test_registries.py +128 -0
- tests/integration/__init__.py +0 -0
- tests/integration/test_build_output.py +129 -0
- tests/plugins/__init__.py +0 -0
- tests/plugins/test_agents.py +93 -0
- tests/plugins/test_skills.py +77 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-models
|
|
3
|
+
description: "Rails models: structure template, associations, callbacks, scopes, and validations"
|
|
4
|
+
group: rails
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Models
|
|
8
|
+
|
|
9
|
+
Comprehensive patterns and best practices for Rails Active Record models.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## File Structure
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
app/models/
|
|
17
|
+
├── card.rb # Main model
|
|
18
|
+
├── card/
|
|
19
|
+
│ ├── closeable.rb # Feature concern
|
|
20
|
+
│ ├── golden.rb # Feature concern
|
|
21
|
+
│ ├── pinnable.rb # Feature concern
|
|
22
|
+
│ └── goldness.rb # Associated model (Card::Goldness)
|
|
23
|
+
├── concerns/
|
|
24
|
+
│ ├── eventable.rb # Shared concern (multiple models)
|
|
25
|
+
│ └── searchable.rb # Shared concern
|
|
26
|
+
└── application_record.rb # Base model
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Model Structure Template
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
class Card < ApplicationRecord
|
|
35
|
+
# 1. CONCERNS (alphabetically for easy scanning)
|
|
36
|
+
include Assignable, Attachments, Broadcastable, Closeable,
|
|
37
|
+
Colored, Entropic, Eventable, Exportable, Golden, Mentions,
|
|
38
|
+
Multistep, Pinnable, Postponable, Promptable, Readable,
|
|
39
|
+
Searchable, Stallable, Statuses, Storage::Tracked,
|
|
40
|
+
Taggable, Triageable, Watchable
|
|
41
|
+
|
|
42
|
+
# 2. ASSOCIATIONS
|
|
43
|
+
# Order: belongs_to, has_one, has_many, has_and_belongs_to_many,
|
|
44
|
+
# has_one_attached, has_many_attached, has_rich_text
|
|
45
|
+
|
|
46
|
+
# belongs_to (with defaults if needed)
|
|
47
|
+
belongs_to :account, default: -> { board.account }
|
|
48
|
+
belongs_to :board
|
|
49
|
+
belongs_to :creator, class_name: "User", default: -> { Current.user }
|
|
50
|
+
|
|
51
|
+
# has_one
|
|
52
|
+
has_one :closure, dependent: :destroy
|
|
53
|
+
has_one :goldness, dependent: :destroy
|
|
54
|
+
|
|
55
|
+
# has_many
|
|
56
|
+
has_many :comments, dependent: :destroy
|
|
57
|
+
has_many :events, as: :eventable, dependent: :destroy
|
|
58
|
+
has_many :assignments, dependent: :destroy
|
|
59
|
+
has_many :assignees, through: :assignments, source: :user
|
|
60
|
+
|
|
61
|
+
# Active Storage attachments
|
|
62
|
+
has_one_attached :image, dependent: :purge_later
|
|
63
|
+
|
|
64
|
+
# Action Text
|
|
65
|
+
has_rich_text :description
|
|
66
|
+
|
|
67
|
+
# 3. CALLBACKS (in lifecycle order)
|
|
68
|
+
# Lifecycle: before_validation, after_validation, before_save,
|
|
69
|
+
# around_save, before_create, around_create, after_create,
|
|
70
|
+
# before_update, around_update, after_update,
|
|
71
|
+
# before_destroy, around_destroy, after_destroy,
|
|
72
|
+
# after_save, after_commit, after_rollback
|
|
73
|
+
|
|
74
|
+
before_validation :normalize_title
|
|
75
|
+
before_save :set_default_title, if: :published?
|
|
76
|
+
before_create :assign_number
|
|
77
|
+
|
|
78
|
+
after_save -> { board.touch }, if: :published?
|
|
79
|
+
after_touch -> { board.touch }, if: :published?
|
|
80
|
+
after_update :handle_board_change, if: :saved_change_to_board_id?
|
|
81
|
+
|
|
82
|
+
after_create_commit :broadcast_creation
|
|
83
|
+
after_update_commit :broadcast_updates
|
|
84
|
+
after_destroy_commit :broadcast_removal
|
|
85
|
+
|
|
86
|
+
# 4. VALIDATIONS
|
|
87
|
+
validates :title, presence: true, if: :published?
|
|
88
|
+
validates :number, uniqueness: { scope: :account_id }
|
|
89
|
+
validates :status, inclusion: { in: %w[draft published] }
|
|
90
|
+
|
|
91
|
+
# Custom validations
|
|
92
|
+
validate :ensure_board_accessible, if: :board_id_changed?
|
|
93
|
+
|
|
94
|
+
# 5. NORMALIZATIONS (Rails 7.1+)
|
|
95
|
+
normalizes :title, with: -> value { value.strip }
|
|
96
|
+
|
|
97
|
+
# 6. SCOPES (grouped by purpose)
|
|
98
|
+
# Ordering scopes
|
|
99
|
+
scope :reverse_chronologically, -> { order created_at: :desc, id: :desc }
|
|
100
|
+
scope :chronologically, -> { order created_at: :asc, id: :asc }
|
|
101
|
+
scope :latest, -> { order last_active_at: :desc, id: :desc }
|
|
102
|
+
|
|
103
|
+
# Filtering scopes
|
|
104
|
+
scope :published, -> { where(status: :published) }
|
|
105
|
+
scope :drafted, -> { where(status: :draft) }
|
|
106
|
+
|
|
107
|
+
# Association scopes
|
|
108
|
+
scope :assigned_to, ->(users) {
|
|
109
|
+
joins(:assignees).where(assignees: { user: users })
|
|
110
|
+
}
|
|
111
|
+
scope :tagged_with, ->(tags) {
|
|
112
|
+
joins(:taggings).where(taggings: { tag: tags })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# Complex query scopes
|
|
116
|
+
scope :preloaded, -> {
|
|
117
|
+
with_users.preload(:column, :tags, :steps, :closure, :goldness,
|
|
118
|
+
board: [:columns]).with_rich_text_description_and_embeds
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# Conditional scopes
|
|
122
|
+
scope :indexed_by, ->(index) do
|
|
123
|
+
case index
|
|
124
|
+
when "stalled" then stalled
|
|
125
|
+
when "active" then published.latest
|
|
126
|
+
when "closed" then closed
|
|
127
|
+
else all
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# 7. ENUMS
|
|
132
|
+
enum :status, %w[ draft published ].index_by(&:itself)
|
|
133
|
+
enum :color, %w[ red blue green ].index_by(&:itself), prefix: true
|
|
134
|
+
|
|
135
|
+
# 8. DELEGATIONS
|
|
136
|
+
delegate :accessible_to?, to: :board
|
|
137
|
+
delegate :name, to: :creator, prefix: true
|
|
138
|
+
|
|
139
|
+
# 9. PUBLIC METHODS
|
|
140
|
+
# Action methods (change state, use transactions)
|
|
141
|
+
def move_to(new_board)
|
|
142
|
+
transaction do
|
|
143
|
+
update!(board: new_board)
|
|
144
|
+
events.update_all(board_id: new_board.id)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def archive
|
|
149
|
+
transaction do
|
|
150
|
+
update!(archived_at: Time.current)
|
|
151
|
+
track_event :archived
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Query methods (return data/booleans)
|
|
156
|
+
def archived?
|
|
157
|
+
archived_at.present?
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def filled?
|
|
161
|
+
title.present? || description.present?
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# 10. PRIVATE METHODS (ordered by invocation)
|
|
165
|
+
private
|
|
166
|
+
def normalize_title
|
|
167
|
+
self.title = title&.strip
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def set_default_title
|
|
171
|
+
self.title = "Untitled" if title.blank?
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def assign_number
|
|
175
|
+
self.number ||= account.increment!(:cards_count).cards_count
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def handle_board_change
|
|
179
|
+
old_board = account.boards.find_by(id: board_id_before_last_save)
|
|
180
|
+
|
|
181
|
+
transaction do
|
|
182
|
+
update! column: nil
|
|
183
|
+
track_board_change_event(old_board.name)
|
|
184
|
+
grant_access_to_assignees unless board.all_access?
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
remove_inaccessible_notifications_later
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def track_board_change_event(old_board_name)
|
|
191
|
+
track_event "board_changed",
|
|
192
|
+
particulars: { old_board: old_board_name, new_board: board.name }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def grant_access_to_assignees
|
|
196
|
+
board.accesses.grant_to(assignees)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def ensure_board_accessible
|
|
200
|
+
unless creator.boards.include?(board)
|
|
201
|
+
errors.add(:board, "must be accessible to creator")
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Association Patterns
|
|
210
|
+
|
|
211
|
+
### belongs_to with Defaults
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
# Default value from lambda
|
|
215
|
+
belongs_to :account, default: -> { board.account }
|
|
216
|
+
belongs_to :creator, class_name: "User", default: -> { Current.user }
|
|
217
|
+
|
|
218
|
+
# Optional association
|
|
219
|
+
belongs_to :parent, optional: true
|
|
220
|
+
|
|
221
|
+
# Polymorphic
|
|
222
|
+
belongs_to :eventable, polymorphic: true
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### has_many with Extensions
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
has_many :accesses, dependent: :delete_all do
|
|
229
|
+
def revise(granted: [], revoked: [])
|
|
230
|
+
transaction do
|
|
231
|
+
grant_to granted
|
|
232
|
+
revoke_from revoked
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def grant_to(user_ids)
|
|
237
|
+
user_ids.each { |id| create(user_id: id) }
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def revoke_from(user_ids)
|
|
241
|
+
where(user_id: user_ids).delete_all
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Usage:
|
|
246
|
+
board.accesses.revise(granted: [user1.id], revoked: [user2.id])
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Counter Caches
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
# In parent model
|
|
253
|
+
has_many :comments, dependent: :destroy
|
|
254
|
+
|
|
255
|
+
# In child model
|
|
256
|
+
belongs_to :card, counter_cache: true
|
|
257
|
+
|
|
258
|
+
# Migration
|
|
259
|
+
add_column :cards, :comments_count, :integer, default: 0, null: false
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Dependent Options
|
|
263
|
+
|
|
264
|
+
```ruby
|
|
265
|
+
has_many :comments, dependent: :destroy # Calls destroy on each
|
|
266
|
+
has_many :events, dependent: :delete_all # SQL DELETE (faster, no callbacks)
|
|
267
|
+
has_one :closure, dependent: :destroy
|
|
268
|
+
has_one :avatar, dependent: :purge_later # For Active Storage
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Callback Patterns
|
|
274
|
+
|
|
275
|
+
### Conditional Callbacks
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
# With if/unless
|
|
279
|
+
before_save :set_defaults, if: :new_record?
|
|
280
|
+
after_save :notify_users, unless: :draft?
|
|
281
|
+
|
|
282
|
+
# With Proc
|
|
283
|
+
before_save :set_title, if: -> { title.blank? && published? }
|
|
284
|
+
|
|
285
|
+
# Multiple conditions
|
|
286
|
+
after_update :reindex,
|
|
287
|
+
if: :saved_change_to_title?,
|
|
288
|
+
unless: :draft?
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Lambda Callbacks
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
# Inline logic
|
|
295
|
+
after_save -> { board.touch }, if: :published?
|
|
296
|
+
|
|
297
|
+
# With parameters (using stabby lambda)
|
|
298
|
+
after_create ->(record) { NotificationJob.perform_later(record) }
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Transaction Callbacks
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
# After transaction commits
|
|
305
|
+
after_create_commit :send_notifications
|
|
306
|
+
after_update_commit :reindex_search
|
|
307
|
+
after_destroy_commit :cleanup_storage
|
|
308
|
+
|
|
309
|
+
# All commits
|
|
310
|
+
after_commit :broadcast_changes
|
|
311
|
+
|
|
312
|
+
# On rollback
|
|
313
|
+
after_rollback :log_failure
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Callback Methods Location
|
|
317
|
+
|
|
318
|
+
```ruby
|
|
319
|
+
class Card < ApplicationRecord
|
|
320
|
+
after_save :do_something
|
|
321
|
+
|
|
322
|
+
private
|
|
323
|
+
# Callback methods in private section
|
|
324
|
+
def do_something
|
|
325
|
+
# Implementation
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## Scope Patterns
|
|
333
|
+
|
|
334
|
+
### Basic Scopes
|
|
335
|
+
|
|
336
|
+
```ruby
|
|
337
|
+
# Simple where
|
|
338
|
+
scope :published, -> { where(status: :published) }
|
|
339
|
+
scope :recent, -> { where(created_at: 1.week.ago..) }
|
|
340
|
+
|
|
341
|
+
# Ordering
|
|
342
|
+
scope :latest, -> { order(created_at: :desc) }
|
|
343
|
+
scope :alphabetical, -> { order(name: :asc) }
|
|
344
|
+
|
|
345
|
+
# Joins
|
|
346
|
+
scope :with_comments, -> { joins(:comments).distinct }
|
|
347
|
+
scope :closed, -> { joins(:closure) }
|
|
348
|
+
|
|
349
|
+
# Missing associations (Rails 7+)
|
|
350
|
+
scope :open, -> { where.missing(:closure) }
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Parameterized Scopes
|
|
354
|
+
|
|
355
|
+
```ruby
|
|
356
|
+
scope :created_after, ->(date) { where(created_at: date..) }
|
|
357
|
+
scope :assigned_to, ->(user) { where(assignee: user) }
|
|
358
|
+
scope :tagged_with, ->(tag_titles) {
|
|
359
|
+
joins(:taggings).where(taggings: { tag: Tag.where(title: tag_titles) })
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Complex Scopes
|
|
364
|
+
|
|
365
|
+
```ruby
|
|
366
|
+
scope :preloaded, -> {
|
|
367
|
+
includes(:creator, :assignees, :tags)
|
|
368
|
+
.preload(board: :columns)
|
|
369
|
+
.with_rich_text_description_and_embeds
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
scope :search_results, ->(query) do
|
|
373
|
+
left_joins(:tags)
|
|
374
|
+
.where("cards.title LIKE ? OR tags.title LIKE ?", "%#{query}%", "%#{query}%")
|
|
375
|
+
.distinct
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
scope :indexed_by, ->(index) do
|
|
379
|
+
case index
|
|
380
|
+
when "stalled" then stalled.latest
|
|
381
|
+
when "active" then published.latest
|
|
382
|
+
when "closed" then closed.recently_closed_first
|
|
383
|
+
else all
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Scope Composition
|
|
389
|
+
|
|
390
|
+
```ruby
|
|
391
|
+
# Scopes are chainable
|
|
392
|
+
Card.published.tagged_with(["bug"]).assigned_to(current_user).latest
|
|
393
|
+
|
|
394
|
+
# Can be used in associations
|
|
395
|
+
has_many :published_cards, -> { published }, class_name: "Card"
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Validation Patterns
|
|
401
|
+
|
|
402
|
+
### Built-in Validations
|
|
403
|
+
|
|
404
|
+
```ruby
|
|
405
|
+
# Presence
|
|
406
|
+
validates :title, presence: true
|
|
407
|
+
validates :title, presence: true, if: :published?
|
|
408
|
+
|
|
409
|
+
# Uniqueness
|
|
410
|
+
validates :email, uniqueness: true
|
|
411
|
+
validates :number, uniqueness: { scope: :account_id }
|
|
412
|
+
validates :slug, uniqueness: { case_sensitive: false }
|
|
413
|
+
|
|
414
|
+
# Format
|
|
415
|
+
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
416
|
+
validates :url, format: { with: /\Ahttps?:\/\// }
|
|
417
|
+
|
|
418
|
+
# Length
|
|
419
|
+
validates :title, length: { maximum: 200 }
|
|
420
|
+
validates :password, length: { minimum: 8 }
|
|
421
|
+
validates :code, length: { is: 6 }
|
|
422
|
+
|
|
423
|
+
# Inclusion/Exclusion
|
|
424
|
+
validates :status, inclusion: { in: %w[draft published] }
|
|
425
|
+
validates :role, exclusion: { in: %w[super_admin] }
|
|
426
|
+
|
|
427
|
+
# Numericality
|
|
428
|
+
validates :age, numericality: { only_integer: true }
|
|
429
|
+
validates :price, numericality: { greater_than: 0 }
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Custom Validations
|
|
433
|
+
|
|
434
|
+
```ruby
|
|
435
|
+
# Method validation
|
|
436
|
+
validate :url_must_be_valid
|
|
437
|
+
|
|
438
|
+
private
|
|
439
|
+
def url_must_be_valid
|
|
440
|
+
return if url.blank?
|
|
441
|
+
|
|
442
|
+
uri = URI.parse(url)
|
|
443
|
+
unless PERMITTED_SCHEMES.include?(uri.scheme)
|
|
444
|
+
errors.add(:url, "must use http or https")
|
|
445
|
+
end
|
|
446
|
+
rescue URI::InvalidURIError
|
|
447
|
+
errors.add(:url, "is not a valid URL")
|
|
448
|
+
end
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### Conditional Validations
|
|
452
|
+
|
|
453
|
+
```ruby
|
|
454
|
+
validates :title, presence: true, if: :published?
|
|
455
|
+
validates :description, presence: true, on: :update
|
|
456
|
+
validates :email, uniqueness: true, unless: :skip_email_validation?
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
---
|