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,441 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-controllers-advanced
|
|
3
|
+
description: "Rails controllers advanced: error handling, filters, streaming, API patterns, security, and testing"
|
|
4
|
+
group: rails
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Controllers (Advanced)
|
|
8
|
+
|
|
9
|
+
## Error Handling
|
|
10
|
+
|
|
11
|
+
### Rescue from Exceptions
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
class ApplicationController < ActionController::Base
|
|
15
|
+
# Rescue specific errors
|
|
16
|
+
rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
|
|
17
|
+
rescue_from ActiveRecord::RecordInvalid, with: :record_invalid
|
|
18
|
+
rescue_from ActionController::ParameterMissing, with: :parameter_missing
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
def record_not_found
|
|
22
|
+
respond_to do |format|
|
|
23
|
+
format.html { redirect_to root_path, alert: "Not found" }
|
|
24
|
+
format.json { head :not_found }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def record_invalid(exception)
|
|
29
|
+
@errors = exception.record.errors
|
|
30
|
+
|
|
31
|
+
respond_to do |format|
|
|
32
|
+
format.html { render :edit, status: :unprocessable_entity }
|
|
33
|
+
format.json { render json: { errors: @errors }, status: :unprocessable_entity }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def parameter_missing(exception)
|
|
38
|
+
respond_to do |format|
|
|
39
|
+
format.html { redirect_back fallback_location: root_path, alert: "Invalid request" }
|
|
40
|
+
format.json { render json: { error: exception.message }, status: :bad_request }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Handling Validation Errors
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
def create
|
|
50
|
+
@board = Board.new(board_params)
|
|
51
|
+
|
|
52
|
+
if @board.save
|
|
53
|
+
redirect_to @board, notice: "Created!"
|
|
54
|
+
else
|
|
55
|
+
render :new, status: :unprocessable_entity
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# With create! (raises exception)
|
|
60
|
+
def create
|
|
61
|
+
@board = Board.create!(board_params)
|
|
62
|
+
redirect_to @board, notice: "Created!"
|
|
63
|
+
rescue ActiveRecord::RecordInvalid
|
|
64
|
+
render :new, status: :unprocessable_entity
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Before/After/Around Actions
|
|
71
|
+
|
|
72
|
+
### Before Actions
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
# Run before specific actions
|
|
76
|
+
before_action :set_board, only: %i[ show edit update destroy ]
|
|
77
|
+
before_action :set_board, except: %i[ index new create ]
|
|
78
|
+
|
|
79
|
+
# Run for all actions
|
|
80
|
+
before_action :require_authentication
|
|
81
|
+
|
|
82
|
+
# Conditional
|
|
83
|
+
before_action :check_admin, if: :admin_required?
|
|
84
|
+
|
|
85
|
+
# With Proc
|
|
86
|
+
before_action -> { redirect_to root_path unless admin? }, only: :admin_dashboard
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### After Actions
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
after_action :log_action
|
|
93
|
+
after_action :set_cache_headers, only: :show
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
def log_action
|
|
97
|
+
Rails.logger.info "Action: #{action_name} by #{Current.user&.email}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def set_cache_headers
|
|
101
|
+
expires_in 5.minutes, public: true
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Around Actions
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
around_action :wrap_in_transaction, only: :complex_operation
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
def wrap_in_transaction
|
|
112
|
+
ActiveRecord::Base.transaction do
|
|
113
|
+
yield
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Skip Actions
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
skip_before_action :require_authentication, only: :public_page
|
|
122
|
+
skip_after_action :log_action, only: :health_check
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Flash Messages
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
# Set flash
|
|
131
|
+
redirect_to @board, notice: "Board created"
|
|
132
|
+
redirect_to @board, alert: "Something went wrong"
|
|
133
|
+
|
|
134
|
+
# Custom flash keys
|
|
135
|
+
redirect_to @board, flash: { warning: "Please verify your email" }
|
|
136
|
+
|
|
137
|
+
# Flash.now (doesn't persist to next request)
|
|
138
|
+
def create
|
|
139
|
+
@board = Board.new(board_params)
|
|
140
|
+
if @board.save
|
|
141
|
+
redirect_to @board
|
|
142
|
+
else
|
|
143
|
+
flash.now[:alert] = "Could not create board"
|
|
144
|
+
render :new
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Keep flash for another request
|
|
149
|
+
flash.keep(:notice)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Streaming & Live Updates
|
|
155
|
+
|
|
156
|
+
### Turbo Streams
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
# In controller
|
|
160
|
+
def create
|
|
161
|
+
@comment = @card.comments.create!(comment_params)
|
|
162
|
+
|
|
163
|
+
respond_to do |format|
|
|
164
|
+
format.turbo_stream # Renders create.turbo_stream.erb
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Server-Sent Events
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
def stream
|
|
173
|
+
response.headers["Content-Type"] = "text/event-stream"
|
|
174
|
+
|
|
175
|
+
sse = SSE.new(response.stream)
|
|
176
|
+
|
|
177
|
+
begin
|
|
178
|
+
loop do
|
|
179
|
+
sse.write({ message: "Hello" })
|
|
180
|
+
sleep 1
|
|
181
|
+
end
|
|
182
|
+
rescue IOError
|
|
183
|
+
# Client disconnected
|
|
184
|
+
ensure
|
|
185
|
+
sse.close
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Performance Patterns
|
|
193
|
+
|
|
194
|
+
### Eager Loading
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
def index
|
|
198
|
+
@cards = Card.includes(:creator, :tags, :assignees)
|
|
199
|
+
.preload(board: :columns)
|
|
200
|
+
.where(board: Current.user.boards)
|
|
201
|
+
end
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Caching
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
# Fragment caching (in view)
|
|
208
|
+
<% cache @board do %>
|
|
209
|
+
<%= render @board %>
|
|
210
|
+
<% end %>
|
|
211
|
+
|
|
212
|
+
# HTTP caching
|
|
213
|
+
def show
|
|
214
|
+
fresh_when etag: @board, last_modified: @board.updated_at, public: true
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Stale check
|
|
218
|
+
def show
|
|
219
|
+
if stale?(@board)
|
|
220
|
+
# Render view
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Pagination
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
def index
|
|
229
|
+
@cards = Card.page(params[:page]).per(25)
|
|
230
|
+
end
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## API Patterns
|
|
236
|
+
|
|
237
|
+
### JSON API Responses
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
def show
|
|
241
|
+
render json: @board
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def create
|
|
245
|
+
@board = Board.create!(board_params)
|
|
246
|
+
render json: @board, status: :created, location: @board
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# With serializer/Jbuilder
|
|
250
|
+
render json: @board, serializer: BoardSerializer
|
|
251
|
+
# or
|
|
252
|
+
render :show # Uses show.json.jbuilder
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### API Error Responses
|
|
256
|
+
|
|
257
|
+
```ruby
|
|
258
|
+
rescue_from ActiveRecord::RecordInvalid do |exception|
|
|
259
|
+
render json: {
|
|
260
|
+
error: "Validation failed",
|
|
261
|
+
details: exception.record.errors.full_messages
|
|
262
|
+
}, status: :unprocessable_entity
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
rescue_from ActiveRecord::RecordNotFound do
|
|
266
|
+
render json: { error: "Not found" }, status: :not_found
|
|
267
|
+
end
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### API Versioning
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
# Namespace approach
|
|
274
|
+
namespace :api do
|
|
275
|
+
namespace :v1 do
|
|
276
|
+
resources :boards
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Or header-based (in ApplicationController)
|
|
281
|
+
before_action :set_api_version
|
|
282
|
+
|
|
283
|
+
private
|
|
284
|
+
def set_api_version
|
|
285
|
+
@api_version = request.headers["X-API-Version"] || "v1"
|
|
286
|
+
end
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Security Patterns
|
|
292
|
+
|
|
293
|
+
### CSRF Protection
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
# Enabled by default
|
|
297
|
+
class ApplicationController < ActionController::Base
|
|
298
|
+
protect_from_forgery with: :exception
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Skip for API endpoints
|
|
302
|
+
class ApiController < ApplicationController
|
|
303
|
+
skip_before_action :verify_authenticity_token
|
|
304
|
+
end
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Strong Parameters
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
# Only permitted params get through
|
|
311
|
+
def board_params
|
|
312
|
+
params.expect(board: [ :name, :description ])
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Attempting to pass other params will raise ActionController::ParameterMissing
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Authorization Checks
|
|
319
|
+
|
|
320
|
+
```ruby
|
|
321
|
+
before_action :ensure_owner, only: %i[ destroy ]
|
|
322
|
+
|
|
323
|
+
private
|
|
324
|
+
def ensure_owner
|
|
325
|
+
unless @board.owner?(Current.user)
|
|
326
|
+
head :forbidden
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Testing Controllers
|
|
334
|
+
|
|
335
|
+
```ruby
|
|
336
|
+
class BoardsControllerTest < ActionDispatch::IntegrationTest
|
|
337
|
+
setup do
|
|
338
|
+
sign_in_as :kevin
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
test "index shows user's boards" do
|
|
342
|
+
get boards_path
|
|
343
|
+
|
|
344
|
+
assert_response :success
|
|
345
|
+
assert_select "h1", "Boards"
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
test "create creates board" do
|
|
349
|
+
assert_difference -> { Board.count }, +1 do
|
|
350
|
+
post boards_path, params: { board: { name: "New Board" } }
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
assert_redirected_to board_path(Board.last)
|
|
354
|
+
assert_equal "New Board", Board.last.name
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
test "update updates board" do
|
|
358
|
+
board = boards(:writebook)
|
|
359
|
+
|
|
360
|
+
patch board_path(board), params: { board: { name: "Updated" } }
|
|
361
|
+
|
|
362
|
+
assert_redirected_to board_path(board)
|
|
363
|
+
assert_equal "Updated", board.reload.name
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
test "destroy removes board" do
|
|
367
|
+
board = boards(:writebook)
|
|
368
|
+
|
|
369
|
+
assert_difference -> { Board.count }, -1 do
|
|
370
|
+
delete board_path(board)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
assert_redirected_to boards_path
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
test "non-admin cannot update board" do
|
|
377
|
+
logout_and_sign_in_as :member
|
|
378
|
+
|
|
379
|
+
board = boards(:writebook)
|
|
380
|
+
original_name = board.name
|
|
381
|
+
|
|
382
|
+
patch board_path(board), params: { board: { name: "Hacked" } }
|
|
383
|
+
|
|
384
|
+
assert_response :forbidden
|
|
385
|
+
assert_equal original_name, board.reload.name
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
test "turbo stream response on create" do
|
|
389
|
+
post boards_path,
|
|
390
|
+
params: { board: { name: "Test" } },
|
|
391
|
+
as: :turbo_stream
|
|
392
|
+
|
|
393
|
+
assert_response :success
|
|
394
|
+
assert_match "turbo-stream", response.body
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
test "json response includes board data" do
|
|
398
|
+
board = boards(:writebook)
|
|
399
|
+
|
|
400
|
+
get board_path(board), as: :json
|
|
401
|
+
|
|
402
|
+
assert_response :success
|
|
403
|
+
json = JSON.parse(response.body)
|
|
404
|
+
assert_equal board.name, json["name"]
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## Best Practices
|
|
412
|
+
|
|
413
|
+
### DO
|
|
414
|
+
|
|
415
|
+
1. **Keep controllers thin** - Delegate to models
|
|
416
|
+
2. **Use concerns for shared behavior**
|
|
417
|
+
3. **Respond to multiple formats**
|
|
418
|
+
4. **Use strong parameters**
|
|
419
|
+
5. **Test permissions thoroughly**
|
|
420
|
+
6. **Return appropriate status codes**
|
|
421
|
+
7. **Use before_action for setup**
|
|
422
|
+
|
|
423
|
+
### DON'T
|
|
424
|
+
|
|
425
|
+
1. **Business logic in controllers** - Belongs in models
|
|
426
|
+
2. **Multiple responsibilities** - One resource per controller
|
|
427
|
+
3. **Complex queries** - Use model scopes
|
|
428
|
+
4. **Rescue exceptions broadly** - Be specific
|
|
429
|
+
5. **Skip CSRF protection** - Unless API
|
|
430
|
+
6. **Fat controllers** - Extract to concerns/models
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
## Summary
|
|
435
|
+
|
|
436
|
+
- **Structure**: Concerns, before_actions, REST actions, private methods
|
|
437
|
+
- **Delegation**: Controllers delegate to models
|
|
438
|
+
- **Responses**: Multi-format with appropriate status codes
|
|
439
|
+
- **Security**: Strong parameters, CSRF, authorization
|
|
440
|
+
- **Testing**: Test happy path, edge cases, and permissions
|
|
441
|
+
- **Performance**: Eager loading, caching, pagination
|