plutonium 0.34.1 → 0.35.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.
- checksums.yaml +4 -4
- data/.claude/skills/plutonium/skill.md +53 -0
- data/.claude/skills/{assets → plutonium-assets}/SKILL.md +13 -8
- data/.claude/skills/{connect-resource → plutonium-connect-resource}/SKILL.md +1 -1
- data/.claude/skills/{controller → plutonium-controller}/SKILL.md +27 -13
- data/.claude/skills/{create-resource → plutonium-create-resource}/SKILL.md +1 -1
- data/.claude/skills/{definition → plutonium-definition}/SKILL.md +10 -10
- data/.claude/skills/{definition-actions → plutonium-definition-actions}/SKILL.md +34 -9
- data/.claude/skills/{definition-fields → plutonium-definition-fields}/SKILL.md +38 -10
- data/.claude/skills/plutonium-definition-query/SKILL.md +356 -0
- data/.claude/skills/{forms → plutonium-forms}/SKILL.md +6 -6
- data/.claude/skills/{installation → plutonium-installation}/SKILL.md +9 -9
- data/.claude/skills/{interaction → plutonium-interaction}/SKILL.md +20 -19
- data/.claude/skills/{model → plutonium-model}/SKILL.md +3 -3
- data/.claude/skills/{model-features → plutonium-model-features}/SKILL.md +3 -3
- data/.claude/skills/{nested-resources → plutonium-nested-resources}/SKILL.md +5 -5
- data/.claude/skills/{package → plutonium-package}/SKILL.md +7 -8
- data/.claude/skills/{policy → plutonium-policy}/SKILL.md +26 -4
- data/.claude/skills/{portal → plutonium-portal}/SKILL.md +33 -31
- data/.claude/skills/{resource → plutonium-resource}/SKILL.md +27 -27
- data/.claude/skills/{rodauth → plutonium-rodauth}/SKILL.md +5 -5
- data/.claude/skills/plutonium-theming/SKILL.md +424 -0
- data/.claude/skills/{views → plutonium-views}/SKILL.md +7 -7
- data/CHANGELOG.md +52 -0
- data/CLAUDE.md +215 -0
- data/CONTRIBUTING.md +72 -18
- data/README.md +100 -19
- data/app/assets/plutonium.css +1 -11
- data/app/assets/plutonium.js +1685 -1146
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +70 -70
- data/app/assets/plutonium.min.js.map +4 -4
- data/app/views/resource/interactive_bulk_action.html.erb +1 -5
- data/app/views/rodauth/_email_auth_request_form.html.erb +1 -1
- data/app/views/rodauth/_login_form.html.erb +15 -55
- data/app/views/rodauth/_login_form_footer.html.erb +2 -2
- data/app/views/rodauth/_password_visibility.html.erb +2 -8
- data/app/views/rodauth/add_recovery_codes.html.erb +2 -2
- data/app/views/rodauth/change_login.html.erb +36 -19
- data/app/views/rodauth/change_password.html.erb +34 -10
- data/app/views/rodauth/close_account.html.erb +12 -4
- data/app/views/rodauth/confirm_password.html.erb +19 -17
- data/app/views/rodauth/create_account.html.erb +30 -109
- data/app/views/rodauth/email_auth.html.erb +1 -1
- data/app/views/rodauth/logout.html.erb +4 -4
- data/app/views/rodauth/otp_auth.html.erb +13 -4
- data/app/views/rodauth/otp_disable.html.erb +12 -4
- data/app/views/rodauth/otp_setup.html.erb +29 -12
- data/app/views/rodauth/otp_unlock.html.erb +19 -10
- data/app/views/rodauth/otp_unlock_not_available.html.erb +7 -7
- data/app/views/rodauth/recovery_auth.html.erb +12 -4
- data/app/views/rodauth/recovery_codes.html.erb +12 -4
- data/app/views/rodauth/remember.html.erb +7 -7
- data/app/views/rodauth/reset_password.html.erb +23 -7
- data/app/views/rodauth/reset_password_request.html.erb +14 -10
- data/app/views/rodauth/sms_auth.html.erb +13 -4
- data/app/views/rodauth/sms_confirm.html.erb +13 -4
- data/app/views/rodauth/sms_disable.html.erb +12 -4
- data/app/views/rodauth/sms_request.html.erb +1 -1
- data/app/views/rodauth/sms_setup.html.erb +23 -7
- data/app/views/rodauth/two_factor_auth.html.erb +2 -2
- data/app/views/rodauth/two_factor_disable.html.erb +12 -4
- data/app/views/rodauth/two_factor_manage.html.erb +7 -7
- data/app/views/rodauth/unlock_account.html.erb +13 -5
- data/app/views/rodauth/unlock_account_request.html.erb +2 -2
- data/app/views/rodauth/verify_account.html.erb +25 -7
- data/app/views/rodauth/verify_account_resend.html.erb +14 -10
- data/app/views/rodauth/verify_login_change.html.erb +1 -1
- data/app/views/rodauth/webauthn_auth.html.erb +1 -1
- data/app/views/rodauth/webauthn_remove.html.erb +18 -8
- data/app/views/rodauth/webauthn_setup.html.erb +12 -4
- data/docs/.vitepress/config.ts +15 -26
- data/docs/.vitepress/theme/custom.css +388 -29
- data/docs/getting-started/index.md +1 -1
- data/docs/getting-started/tutorial/02-first-resource.md +9 -0
- data/docs/getting-started/tutorial/06-nested-resources.md +2 -2
- data/docs/getting-started/tutorial/07-author-portal.md +191 -0
- data/docs/getting-started/tutorial/{07-customizing-ui.md → 08-customizing-ui.md} +7 -7
- data/docs/getting-started/tutorial/index.md +5 -2
- data/docs/guides/authorization.md +33 -0
- data/docs/guides/creating-packages.md +12 -16
- data/docs/guides/custom-actions.md +36 -0
- data/docs/guides/search-filtering.md +121 -42
- data/docs/guides/theming.md +232 -36
- data/docs/index.md +203 -57
- data/docs/public/og-image.png +0 -0
- data/docs/reference/controller/index.md +14 -16
- data/docs/reference/definition/actions.md +38 -3
- data/docs/reference/definition/fields.md +3 -3
- data/docs/reference/definition/index.md +2 -2
- data/docs/reference/generators/index.md +0 -1
- data/docs/reference/interaction/index.md +14 -10
- data/docs/reference/model/index.md +0 -1
- data/docs/reference/portal/index.md +13 -27
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/pkg/portal/portal_generator.rb +0 -2
- data/lib/generators/pu/pkg/portal/templates/app/views/package/dashboard/index.html.erb +28 -72
- data/lib/plutonium/action/interactive.rb +2 -2
- data/lib/plutonium/core/controller.rb +2 -1
- data/lib/plutonium/definition/actions.rb +2 -2
- data/lib/plutonium/lib/deep_freezer.rb +3 -7
- data/lib/plutonium/query/filter.rb +14 -0
- data/lib/plutonium/query/filters/association.rb +49 -0
- data/lib/plutonium/query/filters/boolean.rb +35 -0
- data/lib/plutonium/query/filters/date.rb +97 -0
- data/lib/plutonium/query/filters/date_range.rb +58 -0
- data/lib/plutonium/query/filters/select.rb +55 -0
- data/lib/plutonium/resource/controllers/crud_actions.rb +24 -6
- data/lib/plutonium/resource/controllers/interactive_actions.rb +76 -58
- data/lib/plutonium/resource/controllers/queryable.rb +4 -2
- data/lib/plutonium/resource/query_object.rb +1 -1
- data/lib/plutonium/ui/action_button.rb +23 -65
- data/lib/plutonium/ui/actions_dropdown.rb +103 -0
- data/lib/plutonium/ui/block.rb +1 -1
- data/lib/plutonium/ui/breadcrumbs.rb +12 -19
- data/lib/plutonium/ui/color_mode_selector.rb +1 -1
- data/lib/plutonium/ui/component/kit.rb +6 -0
- data/lib/plutonium/ui/component_classes.rb +102 -0
- data/lib/plutonium/ui/display/base.rb +15 -0
- data/lib/plutonium/ui/display/components/attachment.rb +6 -5
- data/lib/plutonium/ui/display/components/boolean.rb +23 -0
- data/lib/plutonium/ui/display/components/color.rb +23 -0
- data/lib/plutonium/ui/display/resource.rb +1 -1
- data/lib/plutonium/ui/display/theme.rb +29 -15
- data/lib/plutonium/ui/empty_card.rb +3 -3
- data/lib/plutonium/ui/form/base.rb +20 -0
- data/lib/plutonium/ui/form/components/key_value_store.rb +11 -11
- data/lib/plutonium/ui/form/components/resource_select.rb +31 -0
- data/lib/plutonium/ui/form/components/secure_association.rb +1 -2
- data/lib/plutonium/ui/form/components/uppy.rb +5 -4
- data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +4 -4
- data/lib/plutonium/ui/form/interaction.rb +17 -1
- data/lib/plutonium/ui/form/query.rb +133 -80
- data/lib/plutonium/ui/form/theme.rb +50 -35
- data/lib/plutonium/ui/frame_navigator_panel.rb +2 -2
- data/lib/plutonium/ui/layout/base.rb +1 -1
- data/lib/plutonium/ui/layout/header.rb +4 -7
- data/lib/plutonium/ui/layout/rodauth_layout.rb +7 -7
- data/lib/plutonium/ui/layout/sidebar.rb +1 -1
- data/lib/plutonium/ui/nav_grid_menu.rb +7 -6
- data/lib/plutonium/ui/nav_user.rb +9 -8
- data/lib/plutonium/ui/page/interactive_action.rb +5 -5
- data/lib/plutonium/ui/page_header.rb +29 -10
- data/lib/plutonium/ui/panel.rb +4 -4
- data/lib/plutonium/ui/sidebar_menu.rb +8 -8
- data/lib/plutonium/ui/skeleton_table.rb +7 -8
- data/lib/plutonium/ui/tab_list.rb +5 -5
- data/lib/plutonium/ui/table/base.rb +3 -0
- data/lib/plutonium/ui/table/components/attachment.rb +4 -3
- data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +82 -0
- data/lib/plutonium/ui/table/components/pagy_info.rb +2 -2
- data/lib/plutonium/ui/table/components/pagy_pagination.rb +13 -8
- data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +101 -0
- data/lib/plutonium/ui/table/components/scopes_bar.rb +2 -2
- data/lib/plutonium/ui/table/components/selection_column.rb +100 -0
- data/lib/plutonium/ui/table/display_theme.rb +6 -6
- data/lib/plutonium/ui/table/resource.rb +93 -52
- data/lib/plutonium/ui/table/theme.rb +28 -15
- data/lib/plutonium/version.rb +1 -1
- data/package.json +2 -2
- data/plutonium.gemspec +5 -4
- data/src/css/components.css +471 -0
- data/src/css/intl_tel_input.css +2 -2
- data/src/css/plutonium.css +2 -0
- data/src/css/tokens.css +149 -0
- data/src/js/controllers/bulk_actions_controller.js +109 -0
- data/src/js/controllers/filter_panel_controller.js +35 -0
- data/src/js/controllers/register_controllers.js +5 -1
- data/src/js/controllers/resource_drop_down_controller.js +25 -1
- data/src/js/controllers/slim_select_controller.js +6 -2
- data/src/js/turbo/turbo_actions.js +1 -1
- metadata +52 -39
- data/.claude/skills/definition-query/SKILL.md +0 -334
- data/docs/concepts/architecture.md +0 -226
- data/docs/concepts/auto-detection.md +0 -254
- data/docs/concepts/index.md +0 -61
- data/docs/concepts/packages-portals.md +0 -304
- data/docs/concepts/resources.md +0 -224
- data/docs/cookbook/blog.md +0 -411
- data/docs/cookbook/index.md +0 -289
- data/docs/cookbook/saas.md +0 -481
- data/docs/public/CLAUDE.md +0 -578
- data/lib/generators/pu/pkg/portal/templates/app/controllers/resource_controller.rb.tt +0 -5
data/docs/concepts/resources.md
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
# Resources
|
|
2
|
-
|
|
3
|
-
In Plutonium, a **resource** is a complete unit that represents a domain concept. Unlike plain Rails models, a Plutonium resource combines data, presentation, and authorization.
|
|
4
|
-
|
|
5
|
-
## What Makes a Resource?
|
|
6
|
-
|
|
7
|
-
A resource consists of four parts:
|
|
8
|
-
|
|
9
|
-
| Component | Purpose | Example |
|
|
10
|
-
|-----------|---------|---------|
|
|
11
|
-
| **Model** | Data structure and validation | `Post` |
|
|
12
|
-
| **Definition** | How it renders | `PostDefinition` |
|
|
13
|
-
| **Policy** | Who can do what | `PostPolicy` |
|
|
14
|
-
| **Controller** | HTTP handling | `PostsController` |
|
|
15
|
-
|
|
16
|
-
## Resource Models
|
|
17
|
-
|
|
18
|
-
Resource models inherit from `ResourceRecord`:
|
|
19
|
-
|
|
20
|
-
```ruby
|
|
21
|
-
class Post < ResourceRecord
|
|
22
|
-
belongs_to :user
|
|
23
|
-
has_many :comments
|
|
24
|
-
|
|
25
|
-
validates :title, presence: true
|
|
26
|
-
end
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
This base class adds:
|
|
30
|
-
- Automatic field introspection
|
|
31
|
-
- Association detection
|
|
32
|
-
- Integration with definitions and policies
|
|
33
|
-
|
|
34
|
-
## Creating Resources
|
|
35
|
-
|
|
36
|
-
### Using the Generator
|
|
37
|
-
|
|
38
|
-
The fastest way to create a resource:
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
rails generate pu:res:scaffold Post title:string body:text published:boolean
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
This generates:
|
|
45
|
-
- Model with attributes and validations
|
|
46
|
-
- Definition with default configuration
|
|
47
|
-
- Policy with standard permissions
|
|
48
|
-
- Migration
|
|
49
|
-
|
|
50
|
-
### Manual Creation
|
|
51
|
-
|
|
52
|
-
You can also create resources manually:
|
|
53
|
-
|
|
54
|
-
```ruby
|
|
55
|
-
# app/models/post.rb
|
|
56
|
-
class Post < ResourceRecord
|
|
57
|
-
validates :title, presence: true
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# app/definitions/post_definition.rb
|
|
61
|
-
class PostDefinition < Plutonium::Resource::Definition
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# app/policies/post_policy.rb
|
|
65
|
-
class PostPolicy < Plutonium::Resource::Policy
|
|
66
|
-
end
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
## Resource vs Model
|
|
70
|
-
|
|
71
|
-
| Aspect | Plain Model | Resource |
|
|
72
|
-
|--------|-------------|----------|
|
|
73
|
-
| Inheritance | `ApplicationRecord` | `ResourceRecord` |
|
|
74
|
-
| Fields | Manual configuration | Auto-detected |
|
|
75
|
-
| Authorization | Separate concern | Integrated via Policy |
|
|
76
|
-
| UI | Manual forms/views | Auto-generated |
|
|
77
|
-
| CRUD | Write manually | Generated |
|
|
78
|
-
|
|
79
|
-
## Resource Discovery
|
|
80
|
-
|
|
81
|
-
Plutonium automatically discovers resources based on naming conventions:
|
|
82
|
-
|
|
83
|
-
```
|
|
84
|
-
Post → PostDefinition, PostPolicy, PostsController
|
|
85
|
-
Blogging::Post → Blogging::PostDefinition, Blogging::PostPolicy
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
## Field Introspection
|
|
89
|
-
|
|
90
|
-
Resources automatically detect their fields from the database schema:
|
|
91
|
-
|
|
92
|
-
```ruby
|
|
93
|
-
# Given this schema:
|
|
94
|
-
create_table :posts do |t|
|
|
95
|
-
t.string :title, null: false
|
|
96
|
-
t.text :body
|
|
97
|
-
t.boolean :published, default: false
|
|
98
|
-
t.belongs_to :user
|
|
99
|
-
t.timestamps
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Plutonium auto-detects:
|
|
103
|
-
# - title: string input, required
|
|
104
|
-
# - body: textarea
|
|
105
|
-
# - published: checkbox
|
|
106
|
-
# - user: association select
|
|
107
|
-
# - created_at, updated_at: datetime displays
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
## Resource Registration
|
|
111
|
-
|
|
112
|
-
Resources must be registered with a portal to be accessible:
|
|
113
|
-
|
|
114
|
-
```bash
|
|
115
|
-
rails generate pu:res:conn Post --dest=admin_portal
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
Or manually in routes:
|
|
119
|
-
|
|
120
|
-
```ruby
|
|
121
|
-
# packages/admin_portal/config/routes.rb
|
|
122
|
-
AdminPortal::Engine.routes.draw do
|
|
123
|
-
register_resource ::Post
|
|
124
|
-
end
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
## Nested Resources
|
|
128
|
-
|
|
129
|
-
Resources are automatically nested via `belongs_to` associations:
|
|
130
|
-
|
|
131
|
-
```ruby
|
|
132
|
-
class Comment < ResourceRecord
|
|
133
|
-
belongs_to :post
|
|
134
|
-
end
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
When both resources are registered in a portal, Plutonium creates nested URLs like `/posts/:post_id/comments`.
|
|
138
|
-
|
|
139
|
-
## Resource Features
|
|
140
|
-
|
|
141
|
-
### Entity Scoping (Multi-tenancy)
|
|
142
|
-
|
|
143
|
-
Entity scoping is configured on the portal engine, not the model:
|
|
144
|
-
|
|
145
|
-
```ruby
|
|
146
|
-
# packages/customer_portal/lib/engine.rb
|
|
147
|
-
module CustomerPortal
|
|
148
|
-
class Engine < Rails::Engine
|
|
149
|
-
include Plutonium::Portal::Engine
|
|
150
|
-
|
|
151
|
-
config.after_initialize do
|
|
152
|
-
scope_to_entity Organization
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
end
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
### Monetary Fields
|
|
159
|
-
|
|
160
|
-
```ruby
|
|
161
|
-
class Product < ResourceRecord
|
|
162
|
-
# Store as cents, expose as decimal
|
|
163
|
-
has_cents :price_cents
|
|
164
|
-
end
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
## Resource Lifecycle
|
|
168
|
-
|
|
169
|
-
```
|
|
170
|
-
1. User requests /posts/new
|
|
171
|
-
2. Controller builds new Post instance
|
|
172
|
-
3. Policy checks create? permission
|
|
173
|
-
4. Definition provides form fields
|
|
174
|
-
5. Form rendered to user
|
|
175
|
-
|
|
176
|
-
6. User submits form
|
|
177
|
-
7. Controller receives params
|
|
178
|
-
8. Policy filters permitted attributes
|
|
179
|
-
9. Model validates and saves
|
|
180
|
-
10. Controller redirects or re-renders
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
## Best Practices
|
|
184
|
-
|
|
185
|
-
### Keep Models Thin
|
|
186
|
-
Put business logic in Interactions, not models.
|
|
187
|
-
|
|
188
|
-
```ruby
|
|
189
|
-
# Good: Model handles data
|
|
190
|
-
class Post < ResourceRecord
|
|
191
|
-
validates :title, presence: true
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
# Interaction handles logic
|
|
195
|
-
class PublishPost < Plutonium::Interaction::Base
|
|
196
|
-
def execute
|
|
197
|
-
resource.update!(published: true, published_at: Time.current)
|
|
198
|
-
notify_subscribers
|
|
199
|
-
succeed(resource)
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
### Use Meaningful Scopes
|
|
205
|
-
|
|
206
|
-
```ruby
|
|
207
|
-
class Post < ResourceRecord
|
|
208
|
-
scope :published, -> { where(published: true) }
|
|
209
|
-
scope :recent, -> { order(created_at: :desc) }
|
|
210
|
-
scope :by_author, ->(user) { where(user: user) }
|
|
211
|
-
end
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
### Validate at the Right Level
|
|
215
|
-
|
|
216
|
-
- **Model**: Data integrity (presence, format, uniqueness)
|
|
217
|
-
- **Interaction**: Business rules (can only publish once)
|
|
218
|
-
- **Policy**: Authorization (user must own post)
|
|
219
|
-
|
|
220
|
-
## Related Topics
|
|
221
|
-
|
|
222
|
-
- [Architecture](./architecture) - How layers work together
|
|
223
|
-
- [Model Reference](/reference/model/) - Complete model documentation
|
|
224
|
-
- [Definition Reference](/reference/definition/) - Field configuration
|
data/docs/cookbook/blog.md
DELETED
|
@@ -1,411 +0,0 @@
|
|
|
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 --dest=blogging
|
|
393
|
-
rails generate pu:res:scaffold Comment body:text approved:boolean post:belongs_to --dest=blogging
|
|
394
|
-
rails generate pu:res:scaffold Category name:string slug:string --dest=blogging
|
|
395
|
-
|
|
396
|
-
# Create portals
|
|
397
|
-
rails generate pu:pkg:portal admin
|
|
398
|
-
rails generate pu:pkg:portal public
|
|
399
|
-
|
|
400
|
-
# Connect resources to portal
|
|
401
|
-
rails generate pu:res:conn Blogging::Post Blogging::Comment Blogging::Category --dest=admin_portal
|
|
402
|
-
|
|
403
|
-
rails db:migrate
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
## Next Steps
|
|
407
|
-
|
|
408
|
-
- Add image uploads with Active Storage
|
|
409
|
-
- Implement RSS feeds
|
|
410
|
-
- Add social sharing
|
|
411
|
-
- Set up full-text search with PostgreSQL
|