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,398 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-models-advanced
|
|
3
|
+
description: "Rails models advanced: transactions, enums, normalization, serialization, storage, and common patterns"
|
|
4
|
+
group: rails
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Models (Advanced)
|
|
8
|
+
|
|
9
|
+
## Transaction Safety
|
|
10
|
+
|
|
11
|
+
### Use Transactions for Multi-Step Changes
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
def close(user: Current.user)
|
|
15
|
+
transaction do
|
|
16
|
+
create_closure! user: user
|
|
17
|
+
track_event :closed, creator: user
|
|
18
|
+
broadcast_refresh
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def move_to(new_board)
|
|
23
|
+
transaction do
|
|
24
|
+
old_board_name = board.name
|
|
25
|
+
|
|
26
|
+
update!(board: new_board, column: nil)
|
|
27
|
+
events.update_all(board_id: new_board.id)
|
|
28
|
+
track_event :board_changed,
|
|
29
|
+
particulars: { old_board: old_board_name, new_board: new_board.name }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Rollback on Exceptions
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
# Automatically rolls back on exceptions
|
|
38
|
+
def complex_operation
|
|
39
|
+
transaction do
|
|
40
|
+
step_one! # If this raises, transaction rolls back
|
|
41
|
+
step_two! # If this raises, transaction rolls back
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Manual rollback
|
|
46
|
+
def conditional_save
|
|
47
|
+
transaction do
|
|
48
|
+
save!
|
|
49
|
+
raise ActiveRecord::Rollback unless valid_state?
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Enum Patterns
|
|
57
|
+
|
|
58
|
+
### Basic Enum
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
# Symbol keys with integer values
|
|
62
|
+
enum :status, { draft: 0, published: 1, archived: 2 }
|
|
63
|
+
|
|
64
|
+
# String values (preferred for readability)
|
|
65
|
+
enum :status, %w[ draft published archived ].index_by(&:itself)
|
|
66
|
+
|
|
67
|
+
# With prefix/suffix
|
|
68
|
+
enum :color, %w[ red blue green ].index_by(&:itself), prefix: true
|
|
69
|
+
# Generates: color_red?, color_blue?, color_green?
|
|
70
|
+
|
|
71
|
+
enum :role, %w[ owner admin member ].index_by(&:itself), suffix: :role
|
|
72
|
+
# Generates: owner_role?, admin_role?, member_role?
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Enum Inquiry Methods
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
card.status # => "published"
|
|
79
|
+
card.published? # => true
|
|
80
|
+
card.draft? # => false
|
|
81
|
+
|
|
82
|
+
# Bang methods to update
|
|
83
|
+
card.published! # => updates status to published
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Enum Scopes
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
# Automatically generated
|
|
90
|
+
Card.published # => cards where status = 'published'
|
|
91
|
+
Card.draft # => cards where status = 'draft'
|
|
92
|
+
|
|
93
|
+
Card.not_published # => cards where status != 'published'
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Normalization (Rails 7.1+)
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
# Strip whitespace
|
|
102
|
+
normalizes :title, with: -> value { value.strip }
|
|
103
|
+
|
|
104
|
+
# Downcase
|
|
105
|
+
normalizes :email, with: -> value { value.downcase }
|
|
106
|
+
|
|
107
|
+
# Array normalization
|
|
108
|
+
normalizes :subscribed_actions,
|
|
109
|
+
with: ->(value) { Array.wrap(value).map(&:to_s).uniq & PERMITTED_ACTIONS }
|
|
110
|
+
|
|
111
|
+
# Custom normalization
|
|
112
|
+
normalizes :phone,
|
|
113
|
+
with: -> value { value.gsub(/\D/, '') }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Serialization
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
# JSON serialization
|
|
122
|
+
serialize :metadata, type: Hash, coder: JSON
|
|
123
|
+
serialize :tags, type: Array, coder: JSON
|
|
124
|
+
|
|
125
|
+
# Usage
|
|
126
|
+
card.metadata = { source: "api", version: 2 }
|
|
127
|
+
card.tags = ["bug", "urgent"]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Secure Tokens
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
class MagicLink < ApplicationRecord
|
|
136
|
+
# Generates a unique token on create
|
|
137
|
+
has_secure_token :code
|
|
138
|
+
|
|
139
|
+
# Custom token
|
|
140
|
+
has_secure_token :auth_token, length: 32
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Usage
|
|
144
|
+
magic_link = MagicLink.create
|
|
145
|
+
magic_link.code # => "abc123xyz789"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Active Storage Patterns
|
|
151
|
+
|
|
152
|
+
### Single Attachment
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
has_one_attached :image, dependent: :purge_later
|
|
156
|
+
|
|
157
|
+
# Usage
|
|
158
|
+
card.image.attach(io: File.open('image.jpg'), filename: 'image.jpg')
|
|
159
|
+
card.image.attached? # => true
|
|
160
|
+
card.image.purge # Delete immediately
|
|
161
|
+
card.image.purge_later # Delete via background job
|
|
162
|
+
|
|
163
|
+
# In views
|
|
164
|
+
url_for(card.image) if card.image.attached?
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Multiple Attachments
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
has_many_attached :documents, dependent: :purge_later
|
|
171
|
+
|
|
172
|
+
# Usage
|
|
173
|
+
card.documents.attach(io: file, filename: 'doc.pdf')
|
|
174
|
+
card.documents.each { |doc| url_for(doc) }
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Validations
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
# Using validate callback
|
|
181
|
+
has_one_attached :avatar
|
|
182
|
+
|
|
183
|
+
validate :avatar_content_type_allowed
|
|
184
|
+
|
|
185
|
+
private
|
|
186
|
+
def avatar_content_type_allowed
|
|
187
|
+
return unless avatar.attached?
|
|
188
|
+
|
|
189
|
+
unless avatar.content_type.in?(%w[image/png image/jpg image/jpeg])
|
|
190
|
+
errors.add(:avatar, "must be a PNG or JPG")
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Action Text (Rich Text)
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
has_rich_text :description
|
|
201
|
+
|
|
202
|
+
# Usage
|
|
203
|
+
card.description = "Hello <strong>world</strong>"
|
|
204
|
+
card.description.to_plain_text # => "Hello world"
|
|
205
|
+
card.description.to_s # => "<div>Hello <strong>world</strong></div>"
|
|
206
|
+
|
|
207
|
+
# In views
|
|
208
|
+
<%= card.description %>
|
|
209
|
+
|
|
210
|
+
# Searching
|
|
211
|
+
Card.with_rich_text_description
|
|
212
|
+
.where("action_text_rich_texts.body LIKE ?", "%search%")
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Best Practices
|
|
218
|
+
|
|
219
|
+
### DO
|
|
220
|
+
|
|
221
|
+
1. **Use concerns for features** - One feature = one module
|
|
222
|
+
```ruby
|
|
223
|
+
include Closeable, Pinnable, Taggable
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
2. **Use transactions for state changes**
|
|
227
|
+
```ruby
|
|
228
|
+
def close
|
|
229
|
+
transaction do
|
|
230
|
+
create_closure!
|
|
231
|
+
track_event :closed
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
3. **Use scopes instead of class methods for queries**
|
|
237
|
+
```ruby
|
|
238
|
+
# Good
|
|
239
|
+
scope :published, -> { where(status: :published) }
|
|
240
|
+
|
|
241
|
+
# Avoid
|
|
242
|
+
def self.published
|
|
243
|
+
where(status: :published)
|
|
244
|
+
end
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
4. **Default values with lambdas**
|
|
248
|
+
```ruby
|
|
249
|
+
belongs_to :account, default: -> { board.account }
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
5. **Validate at boundaries**
|
|
253
|
+
```ruby
|
|
254
|
+
validates :title, presence: true, if: :published?
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
6. **Use counter caches for counts**
|
|
258
|
+
```ruby
|
|
259
|
+
belongs_to :card, counter_cache: true
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
7. **Use dependent options**
|
|
263
|
+
```ruby
|
|
264
|
+
has_many :comments, dependent: :destroy
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### DON'T
|
|
268
|
+
|
|
269
|
+
1. **Fat models** - Extract to concerns
|
|
270
|
+
2. **Business logic in callbacks** - Keep callbacks simple
|
|
271
|
+
3. **Complex queries in models** - Use scopes or query objects
|
|
272
|
+
4. **Skipping validations** - Use sparingly, only when intentional
|
|
273
|
+
5. **N+1 queries** - Use includes/preload
|
|
274
|
+
6. **Callbacks that call external services** - Use jobs
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Common Patterns
|
|
279
|
+
|
|
280
|
+
### Soft Delete
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
scope :active, -> { where(deleted_at: nil) }
|
|
284
|
+
scope :deleted, -> { where.not(deleted_at: nil) }
|
|
285
|
+
|
|
286
|
+
def soft_delete
|
|
287
|
+
update(deleted_at: Time.current)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def restore
|
|
291
|
+
update(deleted_at: nil)
|
|
292
|
+
end
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Positioning
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
acts_as_list scope: :board
|
|
299
|
+
|
|
300
|
+
# Or manual
|
|
301
|
+
before_create :set_position
|
|
302
|
+
|
|
303
|
+
def move_higher
|
|
304
|
+
# Implementation
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
private
|
|
308
|
+
def set_position
|
|
309
|
+
self.position = board.cards.maximum(:position).to_i + 1
|
|
310
|
+
end
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### State Machine (Simple)
|
|
314
|
+
|
|
315
|
+
```ruby
|
|
316
|
+
def publish
|
|
317
|
+
return if published?
|
|
318
|
+
|
|
319
|
+
transaction do
|
|
320
|
+
self.created_at = Time.current
|
|
321
|
+
published!
|
|
322
|
+
track_event :published
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def draft
|
|
327
|
+
return if draft?
|
|
328
|
+
|
|
329
|
+
transaction do
|
|
330
|
+
draft!
|
|
331
|
+
track_event :drafted
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Touch Parent
|
|
337
|
+
|
|
338
|
+
```ruby
|
|
339
|
+
belongs_to :board, touch: true
|
|
340
|
+
|
|
341
|
+
# Or manual
|
|
342
|
+
after_save -> { board.touch }, if: :published?
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## Testing Models
|
|
348
|
+
|
|
349
|
+
```ruby
|
|
350
|
+
class CardTest < ActiveSupport::TestCase
|
|
351
|
+
test "belongs to board" do
|
|
352
|
+
card = cards(:logo)
|
|
353
|
+
assert_equal boards(:writebook), card.board
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
test "validates title presence when published" do
|
|
357
|
+
card = Card.new(status: :published)
|
|
358
|
+
assert_not card.valid?
|
|
359
|
+
assert_includes card.errors[:title], "can't be blank"
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
test "assigns number on create" do
|
|
363
|
+
account = accounts("37s")
|
|
364
|
+
board = boards(:writebook)
|
|
365
|
+
|
|
366
|
+
card = account.cards.create!(board: board, title: "Test")
|
|
367
|
+
|
|
368
|
+
assert_not_nil card.number
|
|
369
|
+
assert card.number > 0
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
test "transaction rolls back on error" do
|
|
373
|
+
card = cards(:logo)
|
|
374
|
+
|
|
375
|
+
assert_no_difference "Card::Closure.count" do
|
|
376
|
+
assert_raises(ActiveRecord::RecordInvalid) do
|
|
377
|
+
card.transaction do
|
|
378
|
+
card.create_closure!
|
|
379
|
+
raise ActiveRecord::RecordInvalid # Simulates error
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## Summary
|
|
390
|
+
|
|
391
|
+
- **Structure**: Concerns, associations, callbacks, validations, scopes, enums, methods
|
|
392
|
+
- **Concerns**: Extract features to modules
|
|
393
|
+
- **Associations**: Use defaults, extensions, and proper dependent options
|
|
394
|
+
- **Callbacks**: Keep simple, use transactions for state changes
|
|
395
|
+
- **Scopes**: Chainable, focused queries
|
|
396
|
+
- **Validations**: At boundaries, conditional when needed
|
|
397
|
+
- **Transactions**: Wrap multi-step state changes
|
|
398
|
+
- **Testing**: Test associations, validations, and business logic
|