plutonium 0.33.1 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/# Plutonium: The pre-alpha demo.md +4 -2
  3. data/.claude/skills/assets/SKILL.md +416 -0
  4. data/.claude/skills/connect-resource/SKILL.md +112 -0
  5. data/.claude/skills/controller/SKILL.md +302 -0
  6. data/.claude/skills/create-resource/SKILL.md +240 -0
  7. data/.claude/skills/definition/SKILL.md +218 -0
  8. data/.claude/skills/definition-actions/SKILL.md +386 -0
  9. data/.claude/skills/definition-fields/SKILL.md +474 -0
  10. data/.claude/skills/definition-query/SKILL.md +334 -0
  11. data/.claude/skills/forms/SKILL.md +439 -0
  12. data/.claude/skills/installation/SKILL.md +300 -0
  13. data/.claude/skills/interaction/SKILL.md +382 -0
  14. data/.claude/skills/model/SKILL.md +267 -0
  15. data/.claude/skills/model-features/SKILL.md +286 -0
  16. data/.claude/skills/nested-resources/SKILL.md +274 -0
  17. data/.claude/skills/package/SKILL.md +191 -0
  18. data/.claude/skills/policy/SKILL.md +352 -0
  19. data/.claude/skills/portal/SKILL.md +400 -0
  20. data/.claude/skills/resource/SKILL.md +281 -0
  21. data/.claude/skills/rodauth/SKILL.md +452 -0
  22. data/.claude/skills/views/SKILL.md +563 -0
  23. data/Appraisals +46 -4
  24. data/CHANGELOG.md +32 -1
  25. data/app/assets/plutonium.css +2 -2
  26. data/config/brakeman.ignore +239 -0
  27. data/config/initializers/action_policy.rb +1 -1
  28. data/docs/.vitepress/config.ts +132 -47
  29. data/docs/concepts/architecture.md +226 -0
  30. data/docs/concepts/auto-detection.md +254 -0
  31. data/docs/concepts/index.md +61 -0
  32. data/docs/concepts/packages-portals.md +304 -0
  33. data/docs/concepts/resources.md +224 -0
  34. data/docs/cookbook/blog.md +412 -0
  35. data/docs/cookbook/index.md +289 -0
  36. data/docs/cookbook/saas.md +481 -0
  37. data/docs/getting-started/index.md +56 -0
  38. data/docs/getting-started/installation.md +146 -0
  39. data/docs/getting-started/tutorial/01-setup.md +118 -0
  40. data/docs/getting-started/tutorial/02-first-resource.md +180 -0
  41. data/docs/getting-started/tutorial/03-authentication.md +246 -0
  42. data/docs/getting-started/tutorial/04-authorization.md +170 -0
  43. data/docs/getting-started/tutorial/05-custom-actions.md +202 -0
  44. data/docs/getting-started/tutorial/06-nested-resources.md +147 -0
  45. data/docs/getting-started/tutorial/07-customizing-ui.md +254 -0
  46. data/docs/getting-started/tutorial/index.md +64 -0
  47. data/docs/guides/adding-resources.md +420 -0
  48. data/docs/guides/authentication.md +551 -0
  49. data/docs/guides/authorization.md +468 -0
  50. data/docs/guides/creating-packages.md +380 -0
  51. data/docs/guides/custom-actions.md +523 -0
  52. data/docs/guides/index.md +45 -0
  53. data/docs/guides/multi-tenancy.md +302 -0
  54. data/docs/guides/nested-resources.md +411 -0
  55. data/docs/guides/search-filtering.md +266 -0
  56. data/docs/guides/theming.md +321 -0
  57. data/docs/index.md +67 -26
  58. data/docs/public/CLAUDE.md +64 -21
  59. data/docs/reference/assets/index.md +496 -0
  60. data/docs/reference/controller/index.md +363 -0
  61. data/docs/reference/definition/actions.md +400 -0
  62. data/docs/reference/definition/fields.md +350 -0
  63. data/docs/reference/definition/index.md +252 -0
  64. data/docs/reference/definition/query.md +342 -0
  65. data/docs/reference/generators/index.md +469 -0
  66. data/docs/reference/index.md +49 -0
  67. data/docs/reference/interaction/index.md +445 -0
  68. data/docs/reference/model/features.md +248 -0
  69. data/docs/reference/model/index.md +219 -0
  70. data/docs/reference/policy/index.md +385 -0
  71. data/docs/reference/portal/index.md +382 -0
  72. data/docs/reference/views/forms.md +396 -0
  73. data/docs/reference/views/index.md +479 -0
  74. data/gemfiles/rails_7.gemfile +9 -2
  75. data/gemfiles/rails_7.gemfile.lock +146 -111
  76. data/gemfiles/rails_8.0.gemfile +20 -0
  77. data/gemfiles/rails_8.0.gemfile.lock +417 -0
  78. data/gemfiles/rails_8.1.gemfile +20 -0
  79. data/gemfiles/rails_8.1.gemfile.lock +419 -0
  80. data/lib/generators/pu/gem/dotenv/templates/.env +2 -0
  81. data/lib/generators/pu/gem/dotenv/templates/config/initializers/001_ensure_required_env.rb +3 -1
  82. data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +13 -16
  83. data/lib/generators/pu/pkg/portal/USAGE +65 -0
  84. data/lib/generators/pu/pkg/portal/portal_generator.rb +22 -9
  85. data/lib/generators/pu/res/conn/USAGE +71 -0
  86. data/lib/generators/pu/res/model/USAGE +106 -110
  87. data/lib/generators/pu/res/model/templates/model.rb.tt +6 -2
  88. data/lib/generators/pu/res/scaffold/USAGE +85 -0
  89. data/lib/generators/pu/rodauth/install_generator.rb +2 -6
  90. data/lib/generators/pu/rodauth/templates/config/initializers/url_options.rb +17 -0
  91. data/lib/generators/pu/skills/sync/USAGE +14 -0
  92. data/lib/generators/pu/skills/sync/sync_generator.rb +66 -0
  93. data/lib/plutonium/action_policy/sti_policy_lookup.rb +1 -1
  94. data/lib/plutonium/core/controller.rb +2 -2
  95. data/lib/plutonium/interaction/base.rb +1 -0
  96. data/lib/plutonium/package/engine.rb +2 -2
  97. data/lib/plutonium/query/adhoc_block.rb +6 -2
  98. data/lib/plutonium/query/model_scope.rb +1 -1
  99. data/lib/plutonium/railtie.rb +4 -0
  100. data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +1 -1
  101. data/lib/plutonium/resource/query_object.rb +38 -8
  102. data/lib/plutonium/ui/table/components/scopes_bar.rb +39 -34
  103. data/lib/plutonium/version.rb +1 -1
  104. data/lib/tasks/release.rake +19 -4
  105. data/package.json +1 -1
  106. metadata +76 -39
  107. data/brakeman.ignore +0 -28
  108. data/docs/api-examples.md +0 -49
  109. data/docs/guide/claude-code-guide.md +0 -74
  110. data/docs/guide/deep-dive/authorization.md +0 -189
  111. data/docs/guide/deep-dive/multitenancy.md +0 -256
  112. data/docs/guide/deep-dive/resources.md +0 -390
  113. data/docs/guide/getting-started/01-installation.md +0 -165
  114. data/docs/guide/index.md +0 -28
  115. data/docs/guide/introduction/01-what-is-plutonium.md +0 -211
  116. data/docs/guide/introduction/02-core-concepts.md +0 -440
  117. data/docs/guide/tutorial/01-project-setup.md +0 -75
  118. data/docs/guide/tutorial/02-creating-a-feature-package.md +0 -45
  119. data/docs/guide/tutorial/03-defining-resources.md +0 -90
  120. data/docs/guide/tutorial/04-creating-a-portal.md +0 -101
  121. data/docs/guide/tutorial/05-customizing-the-ui.md +0 -128
  122. data/docs/guide/tutorial/06-adding-custom-actions.md +0 -101
  123. data/docs/guide/tutorial/07-implementing-authorization.md +0 -90
  124. data/docs/markdown-examples.md +0 -85
  125. data/docs/modules/action.md +0 -244
  126. data/docs/modules/authentication.md +0 -236
  127. data/docs/modules/configuration.md +0 -599
  128. data/docs/modules/controller.md +0 -443
  129. data/docs/modules/core.md +0 -316
  130. data/docs/modules/definition.md +0 -1308
  131. data/docs/modules/display.md +0 -759
  132. data/docs/modules/form.md +0 -495
  133. data/docs/modules/generator.md +0 -400
  134. data/docs/modules/index.md +0 -167
  135. data/docs/modules/interaction.md +0 -642
  136. data/docs/modules/package.md +0 -151
  137. data/docs/modules/policy.md +0 -176
  138. data/docs/modules/portal.md +0 -710
  139. data/docs/modules/query.md +0 -297
  140. data/docs/modules/resource_record.md +0 -618
  141. data/docs/modules/routing.md +0 -690
  142. data/docs/modules/table.md +0 -301
  143. data/docs/modules/ui.md +0 -631
@@ -0,0 +1,412 @@
1
+ # Recipe: Blog Application
2
+
3
+ Build a full-featured blog with posts, comments, categories, and multi-user support.
4
+
5
+ ## Overview
6
+
7
+ This recipe covers:
8
+ - Post and comment management
9
+ - Categories and tags
10
+ - User roles (admin, author, reader)
11
+ - Publication workflow
12
+ - SEO features
13
+
14
+ ## Architecture
15
+
16
+ ```
17
+ packages/
18
+ ├── blogging/ # Feature package
19
+ │ ├── models/
20
+ │ │ ├── post.rb
21
+ │ │ ├── comment.rb
22
+ │ │ ├── category.rb
23
+ │ │ └── tag.rb
24
+ │ ├── definitions/
25
+ │ ├── policies/
26
+ │ └── interactions/
27
+ ├── admin_portal/ # Admin interface
28
+ └── public_portal/ # Public blog
29
+ ```
30
+
31
+ ## Models
32
+
33
+ ### Post
34
+
35
+ ```ruby
36
+ module Blogging
37
+ class Post < Blogging::ResourceRecord
38
+ belongs_to :author, class_name: 'User'
39
+ belongs_to :category
40
+ has_many :comments, dependent: :destroy
41
+ has_many :taggings, dependent: :destroy
42
+ has_many :tags, through: :taggings
43
+
44
+ validates :title, presence: true, length: { maximum: 200 }
45
+ validates :body, presence: true
46
+ validates :slug, presence: true, uniqueness: true
47
+
48
+ scope :published, -> { where(published: true) }
49
+ scope :draft, -> { where(published: false) }
50
+ scope :featured, -> { where(featured: true) }
51
+ scope :recent, -> { order(published_at: :desc) }
52
+
53
+ before_validation :generate_slug, on: :create
54
+
55
+ def publish!
56
+ update!(published: true, published_at: Time.current)
57
+ end
58
+
59
+ def reading_time
60
+ words_per_minute = 200
61
+ (body.to_plain_text.split.size / words_per_minute.to_f).ceil
62
+ end
63
+
64
+ private
65
+
66
+ def generate_slug
67
+ self.slug ||= title&.parameterize
68
+ end
69
+ end
70
+ end
71
+ ```
72
+
73
+ ### Comment
74
+
75
+ ```ruby
76
+ module Blogging
77
+ class Comment < Blogging::ResourceRecord
78
+ belongs_to :post
79
+ belongs_to :author, class_name: 'User'
80
+ belongs_to :parent, class_name: 'Comment', optional: true
81
+ has_many :replies, class_name: 'Comment', foreign_key: :parent_id
82
+
83
+ validates :body, presence: true
84
+
85
+ scope :approved, -> { where(approved: true) }
86
+ scope :pending, -> { where(approved: false) }
87
+ scope :root, -> { where(parent_id: nil) }
88
+ end
89
+ end
90
+ ```
91
+
92
+ ### Category
93
+
94
+ ```ruby
95
+ module Blogging
96
+ class Category < Blogging::ResourceRecord
97
+ has_many :posts
98
+
99
+ validates :name, presence: true, uniqueness: true
100
+ validates :slug, presence: true, uniqueness: true
101
+
102
+ before_validation :generate_slug
103
+
104
+ private
105
+
106
+ def generate_slug
107
+ self.slug ||= name&.parameterize
108
+ end
109
+ end
110
+ end
111
+ ```
112
+
113
+ ## Definitions
114
+
115
+ ### Post Definition
116
+
117
+ ```ruby
118
+ module Blogging
119
+ class PostDefinition < Plutonium::Resource::Definition
120
+ # Form fields
121
+ field :title
122
+ field :slug, hint: "URL-friendly version (auto-generated if blank)"
123
+ field :body, as: :rich_text
124
+ field :excerpt, as: :text
125
+ field :category
126
+ field :tags, as: :select, multiple: true, collection: -> { Tag.pluck(:name, :id) }
127
+ field :featured_image, as: :file, accept: "image/*"
128
+ field :published, as: :switch
129
+ field :featured, as: :switch
130
+ field :meta_title
131
+ field :meta_description, as: :text
132
+
133
+ # Table columns
134
+ column :title, sortable: true
135
+ column :category
136
+ column :author
137
+ column :published
138
+ column :featured
139
+ column :published_at, sortable: true
140
+
141
+ # Search
142
+ search do |scope, query|
143
+ scope.where("title ILIKE :q OR body ILIKE :q", q: "%#{query}%")
144
+ end
145
+
146
+ # Scopes
147
+ scope :all, default: true
148
+ scope :published, -> { where(published: true) }, badge: true
149
+ scope :drafts, -> { where(published: false) }, badge: true
150
+ scope :featured, -> { where(featured: true) }
151
+
152
+ # Filters
153
+ filter :category, as: :select, collection: -> { Category.pluck(:name, :id) }
154
+ filter :author, as: :select, collection: -> { User.pluck(:name, :id) }
155
+ filter :published, as: :boolean
156
+ filter :created_at, as: :date_range
157
+
158
+ # Actions
159
+ action :publish, interaction: PublishPost, condition: ->(p) { !p.published? }
160
+ action :unpublish, interaction: UnpublishPost, condition: ->(p) { p.published? }
161
+ action :feature, interaction: FeaturePost, condition: ->(p) { !p.featured? }
162
+
163
+ # Associations
164
+ association :comments, fields: [:body, :author, :approved, :created_at]
165
+
166
+ # Eager loading
167
+ includes :author, :category, :tags
168
+ end
169
+ end
170
+ ```
171
+
172
+ ### Comment Definition
173
+
174
+ ```ruby
175
+ module Blogging
176
+ class CommentDefinition < Plutonium::Resource::Definition
177
+ field :body, as: :text
178
+ field :post, as: :hidden
179
+ field :author, as: :hidden
180
+ field :approved, as: :switch
181
+
182
+ column :body
183
+ column :author
184
+ column :approved
185
+ column :created_at
186
+
187
+ scope :all, default: true
188
+ scope :approved, -> { where(approved: true) }
189
+ scope :pending, -> { where(approved: false) }, badge: true
190
+
191
+ action :approve, interaction: ApproveComment, condition: ->(c) { !c.approved? }
192
+ action :reject, interaction: RejectComment, condition: ->(c) { c.approved? }
193
+ end
194
+ end
195
+ ```
196
+
197
+ ## Interactions
198
+
199
+ ### Publish Post
200
+
201
+ ```ruby
202
+ module Blogging
203
+ class PublishPost < Plutonium::Interaction::Base
204
+ presents model_class: Post
205
+ presents label: "Publish"
206
+ presents icon: Phlex::TablerIcons::Send
207
+
208
+ validate :has_content
209
+
210
+ def execute
211
+ resource.update!(
212
+ published: true,
213
+ published_at: Time.current
214
+ )
215
+
216
+ # Notify subscribers
217
+ NotifySubscribersJob.perform_later(resource.id)
218
+
219
+ succeed(resource).with_message("Post published!")
220
+ end
221
+
222
+ private
223
+
224
+ def has_content
225
+ errors.add(:base, "Post must have content") if resource.body.blank?
226
+ end
227
+ end
228
+ end
229
+ ```
230
+
231
+ ### Approve Comment
232
+
233
+ ```ruby
234
+ module Blogging
235
+ class ApproveComment < Plutonium::Interaction::Base
236
+ presents model_class: Comment
237
+ presents label: "Approve"
238
+
239
+ def execute
240
+ resource.update!(approved: true)
241
+
242
+ # Notify comment author
243
+ CommentApprovedMailer.notify(resource).deliver_later
244
+
245
+ succeed(resource).with_message("Comment approved")
246
+ end
247
+ end
248
+ end
249
+ ```
250
+
251
+ ## Policies
252
+
253
+ ### Post Policy
254
+
255
+ ```ruby
256
+ module Blogging
257
+ class PostPolicy < Plutonium::Resource::Policy
258
+ def read?
259
+ record.published? || author? || admin?
260
+ end
261
+
262
+ def create?
263
+ user.present? && (user.author? || user.admin?)
264
+ end
265
+
266
+ def update?
267
+ author? || admin?
268
+ end
269
+
270
+ def destroy?
271
+ author? || admin?
272
+ end
273
+
274
+ def publish?
275
+ (author? || admin?) && !record.published?
276
+ end
277
+
278
+ def relation_scope(relation)
279
+ if admin?
280
+ relation
281
+ elsif user&.author?
282
+ relation.where(author: user).or(relation.where(published: true))
283
+ else
284
+ relation.where(published: true)
285
+ end
286
+ end
287
+
288
+ private
289
+
290
+ def author?
291
+ record.author_id == user&.id
292
+ end
293
+
294
+ def admin?
295
+ user&.admin?
296
+ end
297
+ end
298
+ end
299
+ ```
300
+
301
+ ## Portal Configuration
302
+
303
+ ### Admin Portal
304
+
305
+ Engine:
306
+
307
+ ```ruby
308
+ module AdminPortal
309
+ class Engine < Rails::Engine
310
+ include Plutonium::Portal::Engine
311
+ end
312
+ end
313
+ ```
314
+
315
+ Authentication (in controller concern):
316
+
317
+ ```ruby
318
+ # packages/admin_portal/app/controllers/admin_portal/concerns/controller.rb
319
+ include Plutonium::Auth::Rodauth(:admin)
320
+ ```
321
+
322
+ Admin policy override:
323
+
324
+ ```ruby
325
+ # packages/admin_portal/app/policies/admin_portal/blogging/post_policy.rb
326
+ module AdminPortal
327
+ module Blogging
328
+ class PostPolicy < ::Blogging::PostPolicy
329
+ def read?
330
+ true
331
+ end
332
+
333
+ def destroy?
334
+ true
335
+ end
336
+
337
+ def relation_scope(relation)
338
+ relation
339
+ end
340
+ end
341
+ end
342
+ end
343
+ ```
344
+
345
+ ### Public Portal
346
+
347
+ Engine:
348
+
349
+ ```ruby
350
+ module PublicPortal
351
+ class Engine < Rails::Engine
352
+ include Plutonium::Portal::Engine
353
+ end
354
+ end
355
+ ```
356
+
357
+ Authentication (in controller concern):
358
+
359
+ ```ruby
360
+ # packages/public_portal/app/controllers/public_portal/concerns/controller.rb
361
+ include Plutonium::Auth::Rodauth(:user)
362
+ ```
363
+
364
+ Public policy override:
365
+
366
+ ```ruby
367
+ # packages/public_portal/app/policies/public_portal/blogging/post_policy.rb
368
+ module PublicPortal
369
+ module Blogging
370
+ class PostPolicy < ::Blogging::PostPolicy
371
+ def create?
372
+ false
373
+ end
374
+
375
+ def update?
376
+ false
377
+ end
378
+
379
+ def relation_scope(relation)
380
+ relation.published
381
+ end
382
+ end
383
+ end
384
+ end
385
+ ```
386
+
387
+ ## Usage
388
+
389
+ ```bash
390
+ # Generate the structure
391
+ rails generate pu:pkg:package blogging
392
+ rails generate pu:res:scaffold Post title:string slug:string body:text published:boolean --package blogging
393
+ rails generate pu:res:scaffold Comment body:text approved:boolean post:belongs_to --package blogging
394
+ rails generate pu:res:scaffold Category name:string slug:string --package blogging
395
+
396
+ # Create portals
397
+ rails generate pu:pkg:portal admin
398
+ rails generate pu:pkg:portal public
399
+
400
+ # Connect resources
401
+ rails generate pu:res:conn Post --package blogging --portal admin
402
+ rails generate pu:res:conn Comment --package blogging --portal admin --parent post
403
+
404
+ rails db:migrate
405
+ ```
406
+
407
+ ## Next Steps
408
+
409
+ - Add image uploads with Active Storage
410
+ - Implement RSS feeds
411
+ - Add social sharing
412
+ - Set up full-text search with PostgreSQL
@@ -0,0 +1,289 @@
1
+ # Cookbook
2
+
3
+ Real-world recipes and patterns for building applications with Plutonium.
4
+
5
+ ## Overview
6
+
7
+ These recipes show complete implementations of common application patterns. Each recipe includes:
8
+ - Architecture decisions
9
+ - Code examples
10
+ - Best practices
11
+
12
+ ## Recipes
13
+
14
+ ### [Blog Application](./blog)
15
+ A content management system with posts, comments, categories, and multi-user support.
16
+
17
+ ### [SaaS Application](./saas)
18
+ Multi-tenant application with organizations, team management, and subscription handling.
19
+
20
+ ## Quick Patterns
21
+
22
+ ### Basic CRUD with Authorization
23
+
24
+ ```ruby
25
+ # Model
26
+ class Article < ResourceRecord
27
+ belongs_to :author, class_name: 'User'
28
+ validates :title, :body, presence: true
29
+ end
30
+
31
+ # Definition
32
+ class ArticleDefinition < Plutonium::Resource::Definition
33
+ field :title
34
+ field :body, as: :rich_text
35
+ field :published, as: :switch
36
+
37
+ column :title, sortable: true
38
+ column :author
39
+ column :published
40
+ column :created_at, sortable: true
41
+
42
+ search { |scope, q| scope.where("title ILIKE ?", "%#{q}%") }
43
+ scope :all, default: true
44
+ scope :published, -> { where(published: true) }
45
+ end
46
+
47
+ # Policy
48
+ class ArticlePolicy < Plutonium::Resource::Policy
49
+ def update?
50
+ owner? || admin?
51
+ end
52
+
53
+ def destroy?
54
+ owner? || admin?
55
+ end
56
+
57
+ private
58
+
59
+ def owner?
60
+ record.author_id == user.id
61
+ end
62
+ end
63
+ ```
64
+
65
+ ### Nested Resource Pattern
66
+
67
+ ```ruby
68
+ # Parent
69
+ class Project < ResourceRecord
70
+ has_many :tasks, dependent: :destroy
71
+ end
72
+
73
+ # Child
74
+ class Task < ResourceRecord
75
+ belongs_to :project
76
+ end
77
+
78
+ # Parent policy enables association panel
79
+ class ProjectPolicy < Plutonium::Resource::Policy
80
+ def permitted_associations
81
+ %i[tasks]
82
+ end
83
+ end
84
+ ```
85
+
86
+ ### Custom Action Pattern
87
+
88
+ ```ruby
89
+ # Interaction
90
+ class CompleteTask < Plutonium::Interaction::Base
91
+ presents model_class: Task
92
+ presents label: "Mark Complete"
93
+
94
+ attribute :completion_notes, :text
95
+
96
+ def execute
97
+ resource.update!(
98
+ completed: true,
99
+ completed_at: Time.current,
100
+ completion_notes: completion_notes
101
+ )
102
+
103
+ succeed(resource).with_message("Task completed!")
104
+ end
105
+ end
106
+
107
+ # Register in definition
108
+ class TaskDefinition < Plutonium::Resource::Definition
109
+ action :complete,
110
+ interaction: CompleteTask,
111
+ condition: ->(task) { !task.completed? }
112
+ end
113
+
114
+ # Authorize in policy
115
+ class TaskPolicy < Plutonium::Resource::Policy
116
+ def complete?
117
+ owner? && !record.completed?
118
+ end
119
+ end
120
+ ```
121
+
122
+ ### Multi-Portal Pattern
123
+
124
+ ```ruby
125
+ # Admin portal - full access
126
+ module AdminPortal
127
+ class TaskPolicy < ::TaskPolicy
128
+ def index?
129
+ true
130
+ end
131
+
132
+ def destroy?
133
+ true
134
+ end
135
+
136
+ def relation_scope(relation)
137
+ relation
138
+ end
139
+ end
140
+ end
141
+
142
+ # User portal - limited access
143
+ module UserPortal
144
+ class TaskPolicy < ::TaskPolicy
145
+ def index?
146
+ true
147
+ end
148
+
149
+ def destroy?
150
+ owner?
151
+ end
152
+
153
+ def relation_scope(relation)
154
+ relation.where(user: user)
155
+ end
156
+ end
157
+ end
158
+ ```
159
+
160
+ ## Architecture Patterns
161
+
162
+ ### Feature Package Organization
163
+
164
+ ```
165
+ packages/
166
+ ├── core/ # Shared models (User, Organization)
167
+ ├── projects/ # Project management feature
168
+ ├── billing/ # Subscription & payments
169
+ ├── notifications/ # Email & push notifications
170
+ ├── admin_portal/ # Admin interface
171
+ └── customer_portal/ # Customer interface
172
+ ```
173
+
174
+ ### Service Objects with Interactions
175
+
176
+ ```ruby
177
+ # Complex operations go in Interactions
178
+ class CreateProjectWithTasks < Plutonium::Interaction::Base
179
+ presents model_class: Project
180
+
181
+ attribute :name, :string
182
+ attribute :tasks, :json # Array of task attributes
183
+
184
+ validates :name, presence: true
185
+
186
+ def execute
187
+ project = Project.create!(name: name, user: context[:user])
188
+
189
+ tasks.each do |task_attrs|
190
+ project.tasks.create!(task_attrs)
191
+ end
192
+
193
+ succeed(project).with_message("Project created with #{tasks.size} tasks")
194
+ rescue ActiveRecord::RecordInvalid => e
195
+ fail!(e.message)
196
+ end
197
+ end
198
+ ```
199
+
200
+ ### Event-Driven Updates
201
+
202
+ ```ruby
203
+ class PublishArticle < Plutonium::Interaction::Base
204
+ presents model_class: Article
205
+
206
+ def execute
207
+ resource.update!(published: true, published_at: Time.current)
208
+
209
+ # Trigger downstream effects
210
+ ArticlePublishedJob.perform_later(resource.id)
211
+ NotifySubscribersJob.perform_later(resource.id)
212
+ UpdateSearchIndexJob.perform_later(resource.id)
213
+
214
+ succeed(resource)
215
+ end
216
+ end
217
+ ```
218
+
219
+ ## Common Customizations
220
+
221
+ ### Custom Display Component
222
+
223
+ ```ruby
224
+ class StatusBadge < Plutonium::UI::Component::Base
225
+ COLORS = {
226
+ 'draft' => 'gray',
227
+ 'pending' => 'yellow',
228
+ 'approved' => 'green',
229
+ 'rejected' => 'red'
230
+ }.freeze
231
+
232
+ def initialize(status:)
233
+ @status = status
234
+ @color = COLORS.fetch(status, 'gray')
235
+ end
236
+
237
+ def view_template
238
+ span(class: "px-2 py-1 text-xs rounded bg-#{@color}-100 text-#{@color}-800") do
239
+ @status.titleize
240
+ end
241
+ end
242
+ end
243
+
244
+ # Use in definition
245
+ column :status do |record|
246
+ render StatusBadge.new(status: record.status)
247
+ end
248
+ ```
249
+
250
+ ### Custom Form Section
251
+
252
+ ```ruby
253
+ class ProjectForm < Plutonium::UI::Form::Resource
254
+ def form_template
255
+ div(class: "space-y-8") do
256
+ section("Basic Information") do
257
+ render_field :name
258
+ render_field :description
259
+ end
260
+
261
+ section("Settings") do
262
+ render_field :visibility
263
+ render_field :notifications_enabled
264
+ end
265
+
266
+ section("Team") do
267
+ render_field :team_members, as: :nested
268
+ end
269
+
270
+ render_submit_button
271
+ end
272
+ end
273
+
274
+ private
275
+
276
+ def section(title, &block)
277
+ div(class: "bg-white rounded-lg shadow p-6") do
278
+ h3(class: "text-lg font-medium mb-4") { title }
279
+ yield
280
+ end
281
+ end
282
+ end
283
+ ```
284
+
285
+ ## Next Steps
286
+
287
+ - Explore the [Guides](/guides/) for detailed how-tos
288
+ - Check the [Reference](/reference/) for complete API documentation
289
+ - Visit our [GitHub](https://github.com/radioactive-labs/plutonium-core) for more examples