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,806 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-concerns
|
|
3
|
+
description: "Rails concerns: model and controller concern patterns, shared behavior, and testing"
|
|
4
|
+
group: rails
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Concerns
|
|
8
|
+
|
|
9
|
+
Comprehensive patterns and best practices for Rails concerns (both model and controller).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Philosophy
|
|
14
|
+
|
|
15
|
+
**Concerns extract shared or feature-specific behavior into reusable modules.**
|
|
16
|
+
|
|
17
|
+
Two types:
|
|
18
|
+
1. **Model Concerns** - Feature-specific (`Card::Closeable`) or shared (`Searchable`)
|
|
19
|
+
2. **Controller Concerns** - Shared behavior (`Authentication`, `CardScoped`)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Model Concerns
|
|
24
|
+
|
|
25
|
+
### File Structure
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
app/models/
|
|
29
|
+
├── card/
|
|
30
|
+
│ ├── closeable.rb # Feature concern (Card::Closeable)
|
|
31
|
+
│ ├── golden.rb # Feature concern
|
|
32
|
+
│ └── pinnable.rb # Feature concern
|
|
33
|
+
└── concerns/
|
|
34
|
+
├── eventable.rb # Shared concern
|
|
35
|
+
└── searchable.rb # Shared concern
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Feature Concern Template (Model-Specific)
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
# app/models/card/closeable.rb
|
|
42
|
+
module Card::Closeable
|
|
43
|
+
extend ActiveSupport::Concern
|
|
44
|
+
|
|
45
|
+
# included block runs when module is included
|
|
46
|
+
included do
|
|
47
|
+
# Associations for this feature
|
|
48
|
+
has_one :closure, dependent: :destroy
|
|
49
|
+
|
|
50
|
+
# Scopes
|
|
51
|
+
scope :closed, -> { joins(:closure) }
|
|
52
|
+
scope :open, -> { where.missing(:closure) }
|
|
53
|
+
scope :recently_closed_first, -> {
|
|
54
|
+
closed.order("closures.created_at": :desc)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Callbacks (if needed for this feature)
|
|
58
|
+
after_update_commit :broadcast_closure_change, if: :saved_change_to_closure?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Class methods (optional)
|
|
62
|
+
class_methods do
|
|
63
|
+
def close_all_stale
|
|
64
|
+
open.where(last_active_at: ..1.month.ago).find_each(&:close)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Instance methods - Query methods
|
|
69
|
+
def closed?
|
|
70
|
+
closure.present?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def closed_by
|
|
74
|
+
closure&.user
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def closed_at
|
|
78
|
+
closure&.created_at
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Instance methods - Action methods (use transactions + events)
|
|
82
|
+
def close(user: Current.user)
|
|
83
|
+
return if closed?
|
|
84
|
+
|
|
85
|
+
transaction do
|
|
86
|
+
create_closure! user: user
|
|
87
|
+
track_event :closed, creator: user
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def reopen(user: Current.user)
|
|
92
|
+
return unless closed?
|
|
93
|
+
|
|
94
|
+
transaction do
|
|
95
|
+
closure.destroy
|
|
96
|
+
track_event :reopened, creator: user
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Private methods specific to this concern
|
|
101
|
+
private
|
|
102
|
+
def broadcast_closure_change
|
|
103
|
+
broadcast_refresh_later
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Shared Concern Template
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
# app/models/concerns/eventable.rb
|
|
112
|
+
module Eventable
|
|
113
|
+
extend ActiveSupport::Concern
|
|
114
|
+
|
|
115
|
+
included do
|
|
116
|
+
has_many :events, as: :eventable, dependent: :destroy
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def track_event(action, creator: Current.user, board: self.board, **particulars)
|
|
120
|
+
if should_track_event?
|
|
121
|
+
board.events.create!(
|
|
122
|
+
action: "#{eventable_prefix}_#{action}",
|
|
123
|
+
creator: creator,
|
|
124
|
+
board: board,
|
|
125
|
+
eventable: self,
|
|
126
|
+
particulars: particulars
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
def eventable_prefix
|
|
133
|
+
self.class.name.demodulize.underscore
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def should_track_event?
|
|
137
|
+
true # Override in including class if needed
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Production Examples
|
|
143
|
+
|
|
144
|
+
#### Card::Golden
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
module Card::Golden
|
|
148
|
+
extend ActiveSupport::Concern
|
|
149
|
+
|
|
150
|
+
included do
|
|
151
|
+
has_one :goldness, dependent: :destroy, class_name: "Card::Goldness"
|
|
152
|
+
|
|
153
|
+
scope :golden, -> { joins(:goldness) }
|
|
154
|
+
scope :with_golden_first, -> {
|
|
155
|
+
left_outer_joins(:goldness)
|
|
156
|
+
.prepend_order("card_goldnesses.id IS NULL")
|
|
157
|
+
.preload(:goldness)
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def golden?
|
|
162
|
+
goldness.present?
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def gild
|
|
166
|
+
create_goldness! unless golden?
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def ungild
|
|
170
|
+
goldness&.destroy
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
#### Card::Pinnable
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
module Card::Pinnable
|
|
179
|
+
extend ActiveSupport::Concern
|
|
180
|
+
|
|
181
|
+
included do
|
|
182
|
+
has_many :pins, dependent: :destroy
|
|
183
|
+
|
|
184
|
+
after_update_commit :broadcast_pin_updates, if: :preview_changed?
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def pinned_by?(user)
|
|
188
|
+
pins.exists?(user: user)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def pin_for(user)
|
|
192
|
+
pins.find_by(user: user)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def pin_by(user)
|
|
196
|
+
pins.find_or_create_by!(user: user)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def unpin_by(user)
|
|
200
|
+
pins.find_by(user: user)&.destroy
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
private
|
|
204
|
+
def broadcast_pin_updates
|
|
205
|
+
pins.find_each do |pin|
|
|
206
|
+
pin.broadcast_replace_later_to [ pin.user, :pins_tray ],
|
|
207
|
+
partial: "my/pins/pin"
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
#### Card::Taggable
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
module Card::Taggable
|
|
217
|
+
extend ActiveSupport::Concern
|
|
218
|
+
|
|
219
|
+
included do
|
|
220
|
+
has_many :taggings, dependent: :destroy
|
|
221
|
+
has_many :tags, through: :taggings
|
|
222
|
+
|
|
223
|
+
scope :tagged_with, ->(tags) {
|
|
224
|
+
joins(:taggings).where(taggings: { tag: tags })
|
|
225
|
+
}
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def toggle_tag_with(title)
|
|
229
|
+
tag = account.tags.find_or_create_by!(title: title)
|
|
230
|
+
|
|
231
|
+
transaction do
|
|
232
|
+
if tagged_with?(tag)
|
|
233
|
+
taggings.destroy_by tag: tag
|
|
234
|
+
else
|
|
235
|
+
taggings.create tag: tag
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def tagged_with?(tag)
|
|
241
|
+
tags.include? tag
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
#### Searchable (Shared)
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
module Searchable
|
|
250
|
+
extend ActiveSupport::Concern
|
|
251
|
+
|
|
252
|
+
included do
|
|
253
|
+
after_create_commit :create_in_search_index
|
|
254
|
+
after_update_commit :update_in_search_index
|
|
255
|
+
after_destroy_commit :remove_from_search_index
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def reindex
|
|
259
|
+
update_in_search_index
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
private
|
|
263
|
+
def create_in_search_index
|
|
264
|
+
search_record_class.create!(search_record_attributes)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def update_in_search_index
|
|
268
|
+
search_record_class.upsert!(search_record_attributes)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def remove_from_search_index
|
|
272
|
+
search_record_class
|
|
273
|
+
.find_by(searchable_type: self.class.name, searchable_id: id)
|
|
274
|
+
&.destroy
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def search_record_attributes
|
|
278
|
+
{
|
|
279
|
+
account_id: account_id,
|
|
280
|
+
searchable_type: self.class.name,
|
|
281
|
+
searchable_id: id,
|
|
282
|
+
card_id: search_card_id,
|
|
283
|
+
board_id: search_board_id,
|
|
284
|
+
title: search_title,
|
|
285
|
+
content: search_content,
|
|
286
|
+
created_at: created_at
|
|
287
|
+
}
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def search_record_class
|
|
291
|
+
Search::Record.for(account_id)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Including models must implement:
|
|
295
|
+
# - search_title
|
|
296
|
+
# - search_content
|
|
297
|
+
# - search_card_id
|
|
298
|
+
# - search_board_id
|
|
299
|
+
end
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
#### Broadcastable
|
|
303
|
+
|
|
304
|
+
```ruby
|
|
305
|
+
module Card::Broadcastable
|
|
306
|
+
extend ActiveSupport::Concern
|
|
307
|
+
|
|
308
|
+
included do
|
|
309
|
+
broadcasts_refreshes # Turbo auto-refresh
|
|
310
|
+
|
|
311
|
+
before_update :remember_if_preview_changed
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
private
|
|
315
|
+
def remember_if_preview_changed
|
|
316
|
+
@preview_changed ||= title_changed? || column_id_changed? || board_id_changed?
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def preview_changed?
|
|
320
|
+
@preview_changed
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## Controller Concerns
|
|
328
|
+
|
|
329
|
+
### File Structure
|
|
330
|
+
|
|
331
|
+
```
|
|
332
|
+
app/controllers/concerns/
|
|
333
|
+
├── authentication.rb # Authentication/authorization
|
|
334
|
+
├── card_scoped.rb # Scoping concern
|
|
335
|
+
├── board_scoped.rb # Scoping concern
|
|
336
|
+
└── filter_scoped.rb # Filter concern
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Authentication Concern
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
# app/controllers/concerns/authentication.rb
|
|
343
|
+
module Authentication
|
|
344
|
+
extend ActiveSupport::Concern
|
|
345
|
+
|
|
346
|
+
included do
|
|
347
|
+
before_action :require_account
|
|
348
|
+
before_action :require_authentication
|
|
349
|
+
|
|
350
|
+
helper_method :authenticated?, :current_user, :current_identity
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
class_methods do
|
|
354
|
+
# Allow unauthenticated access to specific actions
|
|
355
|
+
def allow_unauthenticated_access(**options)
|
|
356
|
+
skip_before_action :require_authentication, **options
|
|
357
|
+
before_action :resume_session, **options
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Require unauthenticated (for login/signup pages)
|
|
361
|
+
def require_unauthenticated_access(**options)
|
|
362
|
+
allow_unauthenticated_access **options
|
|
363
|
+
before_action :redirect_authenticated_user, **options
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
private
|
|
368
|
+
def authenticated?
|
|
369
|
+
Current.identity.present?
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def current_user
|
|
373
|
+
Current.user
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def current_identity
|
|
377
|
+
Current.identity
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def require_authentication
|
|
381
|
+
redirect_to new_session_path unless authenticated?
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def require_account
|
|
385
|
+
unless Current.account
|
|
386
|
+
redirect_to root_url(untenanted: true)
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def resume_session
|
|
391
|
+
if session_cookie = cookies.signed[:session_id]
|
|
392
|
+
Current.session = Session.find_by(id: session_cookie)
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def redirect_authenticated_user
|
|
397
|
+
redirect_to root_path if authenticated?
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Scoping Concern
|
|
403
|
+
|
|
404
|
+
```ruby
|
|
405
|
+
# app/controllers/concerns/card_scoped.rb
|
|
406
|
+
module CardScoped
|
|
407
|
+
extend ActiveSupport::Concern
|
|
408
|
+
|
|
409
|
+
included do
|
|
410
|
+
before_action :set_card, :set_board
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
private
|
|
414
|
+
def set_card
|
|
415
|
+
@card = Current.user.accessible_cards.find_by!(number: params[:card_id])
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def set_board
|
|
419
|
+
@board = @card.board
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# Helper methods for this resource
|
|
423
|
+
def render_card_replacement
|
|
424
|
+
render turbo_stream: turbo_stream.replace(
|
|
425
|
+
[ @card, :card_container ],
|
|
426
|
+
partial: "cards/container",
|
|
427
|
+
method: :morph,
|
|
428
|
+
locals: { card: @card.reload }
|
|
429
|
+
)
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def render_card_preview_replacement
|
|
433
|
+
render turbo_stream: turbo_stream.replace(
|
|
434
|
+
[ @card, :preview ],
|
|
435
|
+
partial: "cards/display/preview",
|
|
436
|
+
locals: { card: @card.reload }
|
|
437
|
+
)
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
### Feature Toggle Concern
|
|
443
|
+
|
|
444
|
+
```ruby
|
|
445
|
+
module FeatureGuarded
|
|
446
|
+
extend ActiveSupport::Concern
|
|
447
|
+
|
|
448
|
+
included do
|
|
449
|
+
before_action :ensure_feature_enabled
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
private
|
|
453
|
+
def ensure_feature_enabled
|
|
454
|
+
feature_name = controller_name.singularize
|
|
455
|
+
|
|
456
|
+
unless Current.account.feature_enabled?(feature_name)
|
|
457
|
+
respond_to do |format|
|
|
458
|
+
format.html { redirect_to root_path, alert: "Feature not available" }
|
|
459
|
+
format.json { head :forbidden }
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### Pagination Concern
|
|
467
|
+
|
|
468
|
+
```ruby
|
|
469
|
+
module Paginatable
|
|
470
|
+
extend ActiveSupport::Concern
|
|
471
|
+
|
|
472
|
+
private
|
|
473
|
+
def paginate(collection, per_page: 25)
|
|
474
|
+
page = params[:page].to_i.clamp(1..)
|
|
475
|
+
offset = (page - 1) * per_page
|
|
476
|
+
|
|
477
|
+
collection.limit(per_page).offset(offset)
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def pagination_meta(collection, per_page: 25)
|
|
481
|
+
total = collection.count
|
|
482
|
+
page = params[:page].to_i.clamp(1..)
|
|
483
|
+
|
|
484
|
+
{
|
|
485
|
+
current_page: page,
|
|
486
|
+
per_page: per_page,
|
|
487
|
+
total_pages: (total.to_f / per_page).ceil,
|
|
488
|
+
total_count: total
|
|
489
|
+
}
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
---
|
|
495
|
+
|
|
496
|
+
## Job Concerns
|
|
497
|
+
|
|
498
|
+
### SMTP Error Handling
|
|
499
|
+
|
|
500
|
+
```ruby
|
|
501
|
+
# app/jobs/concerns/smtp_delivery_error_handling.rb
|
|
502
|
+
module SmtpDeliveryErrorHandling
|
|
503
|
+
extend ActiveSupport::Concern
|
|
504
|
+
|
|
505
|
+
included do
|
|
506
|
+
# Retry delivery to possibly-unavailable remote mailservers
|
|
507
|
+
retry_on Net::OpenTimeout, Net::ReadTimeout, Socket::ResolutionError,
|
|
508
|
+
wait: :polynomially_longer
|
|
509
|
+
|
|
510
|
+
# Net::SMTPServerBusy is SMTP error code 4xx (temporary error)
|
|
511
|
+
retry_on Net::SMTPServerBusy, wait: :polynomially_longer
|
|
512
|
+
|
|
513
|
+
# SMTP syntax errors (50x)
|
|
514
|
+
rescue_from Net::SMTPSyntaxError do |error|
|
|
515
|
+
case error.message
|
|
516
|
+
when /\A501 5\.1\.3/
|
|
517
|
+
# Ignore undeliverable email addresses, but log for monitoring
|
|
518
|
+
Sentry.capture_exception error, level: :info
|
|
519
|
+
else
|
|
520
|
+
raise
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# SMTP fatal errors (5xx)
|
|
525
|
+
rescue_from Net::SMTPFatalError do |error|
|
|
526
|
+
case error.message
|
|
527
|
+
when /\A550 5\.1\.1/, /\A552 5\.6\.0/, /\A555 5\.5\.4/
|
|
528
|
+
Sentry.capture_exception error, level: :info
|
|
529
|
+
else
|
|
530
|
+
raise
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
end
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
---
|
|
538
|
+
|
|
539
|
+
## Concern Patterns
|
|
540
|
+
|
|
541
|
+
### Dependency Injection
|
|
542
|
+
|
|
543
|
+
```ruby
|
|
544
|
+
module Notifiable
|
|
545
|
+
extend ActiveSupport::Concern
|
|
546
|
+
|
|
547
|
+
# Including class must implement notify_users method
|
|
548
|
+
def send_notifications
|
|
549
|
+
users_to_notify.each do |user|
|
|
550
|
+
notify_user(user)
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
private
|
|
555
|
+
def users_to_notify
|
|
556
|
+
raise NotImplementedError, "Include class must implement users_to_notify"
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
def notify_user(user)
|
|
560
|
+
raise NotImplementedError, "Include class must implement notify_user"
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
### Configuration
|
|
566
|
+
|
|
567
|
+
```ruby
|
|
568
|
+
module Configurable
|
|
569
|
+
extend ActiveSupport::Concern
|
|
570
|
+
|
|
571
|
+
included do
|
|
572
|
+
class_attribute :config, default: {}
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
class_methods do
|
|
576
|
+
def configure(&block)
|
|
577
|
+
self.config = config.dup
|
|
578
|
+
yield config
|
|
579
|
+
end
|
|
580
|
+
end
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
# Usage:
|
|
584
|
+
class Card < ApplicationRecord
|
|
585
|
+
include Configurable
|
|
586
|
+
|
|
587
|
+
configure do |config|
|
|
588
|
+
config[:auto_close_after] = 30.days
|
|
589
|
+
config[:max_attachments] = 10
|
|
590
|
+
end
|
|
591
|
+
end
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
### State Machine (Simple)
|
|
595
|
+
|
|
596
|
+
```ruby
|
|
597
|
+
module Stateful
|
|
598
|
+
extend ActiveSupport::Concern
|
|
599
|
+
|
|
600
|
+
included do
|
|
601
|
+
validates :state, inclusion: { in: states }
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
class_methods do
|
|
605
|
+
def states
|
|
606
|
+
@states ||= []
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
def state(name, &block)
|
|
610
|
+
states << name.to_s
|
|
611
|
+
|
|
612
|
+
define_method("#{name}?") do
|
|
613
|
+
state == name.to_s
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
define_method("#{name}!") do
|
|
617
|
+
update!(state: name.to_s)
|
|
618
|
+
end
|
|
619
|
+
end
|
|
620
|
+
end
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
# Usage:
|
|
624
|
+
class Order < ApplicationRecord
|
|
625
|
+
include Stateful
|
|
626
|
+
|
|
627
|
+
state :pending
|
|
628
|
+
state :processing
|
|
629
|
+
state :shipped
|
|
630
|
+
state :delivered
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
order.pending? # => true
|
|
634
|
+
order.processing! # => updates state to processing
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
---
|
|
638
|
+
|
|
639
|
+
## Best Practices
|
|
640
|
+
|
|
641
|
+
### ✅ DO
|
|
642
|
+
|
|
643
|
+
1. **Extract features to concerns**
|
|
644
|
+
```ruby
|
|
645
|
+
# app/models/card.rb
|
|
646
|
+
include Closeable, Pinnable, Taggable
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
2. **Use namespacing for model-specific concerns**
|
|
650
|
+
```ruby
|
|
651
|
+
# app/models/card/closeable.rb
|
|
652
|
+
module Card::Closeable
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
3. **Keep concerns focused**
|
|
656
|
+
- One responsibility per concern
|
|
657
|
+
- Clear, descriptive names
|
|
658
|
+
|
|
659
|
+
4. **Use class_methods block**
|
|
660
|
+
```ruby
|
|
661
|
+
class_methods do
|
|
662
|
+
def find_stale
|
|
663
|
+
# ...
|
|
664
|
+
end
|
|
665
|
+
end
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
5. **Document required methods**
|
|
669
|
+
```ruby
|
|
670
|
+
# Including class must implement:
|
|
671
|
+
# - search_title
|
|
672
|
+
# - search_content
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
6. **Use transactions in action methods**
|
|
676
|
+
```ruby
|
|
677
|
+
def close
|
|
678
|
+
transaction do
|
|
679
|
+
create_closure!
|
|
680
|
+
track_event :closed
|
|
681
|
+
end
|
|
682
|
+
end
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
7. **Test concerns independently**
|
|
686
|
+
|
|
687
|
+
### ❌ DON'T
|
|
688
|
+
|
|
689
|
+
1. **God concerns** - Keep focused
|
|
690
|
+
2. **Complex dependencies** - Keep concerns independent
|
|
691
|
+
3. **Deep nesting** - Avoid concerns including concerns
|
|
692
|
+
4. **Mixing responsibilities** - One concern = one feature
|
|
693
|
+
5. **Skipping documentation** - Document required methods
|
|
694
|
+
|
|
695
|
+
---
|
|
696
|
+
|
|
697
|
+
## Testing Concerns
|
|
698
|
+
|
|
699
|
+
### Model Concern Test
|
|
700
|
+
|
|
701
|
+
```ruby
|
|
702
|
+
# test/models/card/closeable_test.rb
|
|
703
|
+
class Card::CloseableTest < ActiveSupport::TestCase
|
|
704
|
+
setup do
|
|
705
|
+
Current.session = sessions(:david)
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
test "close creates closure" do
|
|
709
|
+
card = cards(:logo)
|
|
710
|
+
|
|
711
|
+
assert_not card.closed?
|
|
712
|
+
|
|
713
|
+
assert_difference -> { Card::Closure.count }, +1 do
|
|
714
|
+
card.close(user: users(:kevin))
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
assert card.closed?
|
|
718
|
+
assert_equal users(:kevin), card.closed_by
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
test "reopen removes closure" do
|
|
722
|
+
card = cards(:logo)
|
|
723
|
+
card.close
|
|
724
|
+
|
|
725
|
+
assert card.closed?
|
|
726
|
+
|
|
727
|
+
assert_difference -> { Card::Closure.count }, -1 do
|
|
728
|
+
card.reopen
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
assert_not card.closed?
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
test "closed scope returns closed cards" do
|
|
735
|
+
card = cards(:logo)
|
|
736
|
+
card.close
|
|
737
|
+
|
|
738
|
+
assert_includes Card.closed, card
|
|
739
|
+
assert_not_includes Card.open, card
|
|
740
|
+
end
|
|
741
|
+
end
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
### Controller Concern Test
|
|
745
|
+
|
|
746
|
+
```ruby
|
|
747
|
+
# test/controllers/concerns/authentication_test.rb
|
|
748
|
+
class AuthenticationTest < ActionDispatch::IntegrationTest
|
|
749
|
+
class TestController < ApplicationController
|
|
750
|
+
include Authentication
|
|
751
|
+
|
|
752
|
+
def index
|
|
753
|
+
head :ok
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
def public_action
|
|
757
|
+
head :ok
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
allow_unauthenticated_access only: :public_action
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
setup do
|
|
764
|
+
Rails.application.routes.draw do
|
|
765
|
+
get "test/index" => "authentication_test/test#index"
|
|
766
|
+
get "test/public_action" => "authentication_test/test#public_action"
|
|
767
|
+
end
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
teardown do
|
|
771
|
+
Rails.application.reload_routes!
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
test "requires authentication for protected actions" do
|
|
775
|
+
get "/test/index"
|
|
776
|
+
|
|
777
|
+
assert_redirected_to new_session_path
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
test "allows unauthenticated access to public actions" do
|
|
781
|
+
get "/test/public_action"
|
|
782
|
+
|
|
783
|
+
assert_response :success
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
test "authenticated user can access protected actions" do
|
|
787
|
+
sign_in_as :david
|
|
788
|
+
|
|
789
|
+
get "/test/index"
|
|
790
|
+
|
|
791
|
+
assert_response :success
|
|
792
|
+
end
|
|
793
|
+
end
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
---
|
|
797
|
+
|
|
798
|
+
## Summary
|
|
799
|
+
|
|
800
|
+
- **Model Concerns**: Extract features (`Card::Closeable`) or shared behavior (`Searchable`)
|
|
801
|
+
- **Controller Concerns**: Authentication, scoping, shared actions
|
|
802
|
+
- **Structure**: `included do`, `class_methods`, instance methods, private methods
|
|
803
|
+
- **Focused**: One responsibility per concern
|
|
804
|
+
- **Documented**: Document required methods for including classes
|
|
805
|
+
- **Tested**: Test concerns independently and in context
|
|
806
|
+
- **Organized**: Model-specific in `model/`, shared in `concerns/`
|