rails_templatable 0.1.1 → 0.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a6b1d092af3d36bb5050dc292d447db84040459eff960a6fb4f7e6d012ef07c
4
- data.tar.gz: 0115da00cf3adc178efaccdacf79c1942894886931543f81d5620aa5467ecf2f
3
+ metadata.gz: 1dc98737cfeeef797928b4f4c51fdeccbda1c4418a44232af9c856d7aab646e3
4
+ data.tar.gz: 97c2a8611ac44f12ca03b69461fa287e8da5ff978008af045087dfa56e915b18
5
5
  SHA512:
6
- metadata.gz: d2ec972556b40651aecfaba05b9a23ec0321126838fda933adef084baea0003e474d1b1f0e3dccd7d23c20ac836fb1ce55052ec32f2689ccf9cd1dc7bad9feb7
7
- data.tar.gz: af8843f690fa3b1c6c2bef5e982d540c00f7ef44d573ff9976b80c3601bbd5c02b2799c62118a6a74425334d08c383f077909059eca7af1f0f5c96d1f38dff11
6
+ metadata.gz: cebaebeec326a51b9e21cd53076845094f22f04b73f99afd672dc1eb2c9223ff01950698dcdc177d8b9923aae3e8dec64875f9e1e134a030ec80d4553a6171e8
7
+ data.tar.gz: 01357a634728a3243afa4e3919d6bd2c8333e5ec51a67f79194d27add6ae214f03ab4a835b3e2f2b14dbbd7f4a69eec5463f4917da90b421752f917e35527fb9
data/README.md CHANGED
@@ -1,14 +1,13 @@
1
1
  # RailsTemplatable
2
2
 
3
- A lightweight Rails Engine that enables any ActiveRecord model to associate with templates for content management. Built with a polymorphic many-to-many relationship, allowing any model to have multiple templates.
3
+ A lightweight Rails Engine that enables any ActiveRecord model to use predefined templates for content management. Built with a simple one-to-many relationship, allowing each model instance to have one template while templates can be reused across multiple instances.
4
4
 
5
5
  ## Features
6
6
 
7
- - 🎯 **Polymorphic Associations** - Attach templates to any ActiveRecord model
7
+ - 🎯 **Simple 1:N Relationship** - Each record has one template, templates can be reused
8
8
  - 📝 **Multiple Content Formats** - Support for HTML, Markdown, and plain text
9
- - 🔗 **Many-to-Many Relationship** - Models can have multiple templates, templates can be used by multiple models
10
- - 🏷️ **Flexible Categories** - User-defined template categories (no hardcoded types)
11
- - 🔒 **Database Constraints** - Unique constraints at database level
9
+ - 🏷️ **Flexible Categories** - Pre-defined categories (feature_request, bug_report, etc.) or custom
10
+ - 🔗 **Direct Foreign Key** - No join table needed, cleaner database schema
12
11
  - 🚀 **Easy Integration** - Simple concern-based inclusion
13
12
 
14
13
  ## Installation
@@ -26,54 +25,90 @@ $ bin/rails railties:install:migrations FROM=rails_templatable
26
25
  $ bin/rails db:migrate
27
26
  ```
28
27
 
28
+ For each model that will use templates, add a foreign key:
29
+
30
+ ```ruby
31
+ add_reference :posts, :template,
32
+ foreign_key: { to_table: :rails_templatable_templates },
33
+ index: false
34
+ ```
35
+
29
36
  ## Quick Start
30
37
 
31
38
  ### 1. Enable templates in your models
32
39
 
33
40
  ```ruby
34
41
  class Post < ApplicationRecord
35
- include RailsTemplatable::HasTemplates
42
+ include RailsTemplatable::HasTemplate
36
43
  end
37
44
 
38
45
  class WorkLog < ApplicationRecord
39
- include RailsTemplatable::HasTemplates
46
+ include RailsTemplatable::HasTemplate
40
47
  end
41
48
  ```
42
49
 
43
50
  ### 2. Create templates
44
51
 
45
52
  ```ruby
46
- # HTML template
47
- email_template = RailsTemplatable::Template.create!(
48
- category: "email_template",
49
- content: "<h1>Welcome!</h1><p>Hello {{name}}</p>",
50
- content_format: :html
53
+ # Feature request template
54
+ feature_template = RailsTemplatable::Template.create!(
55
+ category: "feature_request",
56
+ content: "# Feature Request\n\n## Description\n\n## Acceptance Criteria",
57
+ content_format: :markdown
51
58
  )
52
59
 
53
- # Markdown template
54
- doc_template = RailsTemplatable::Template.create!(
55
- category: "documentation",
56
- content: "# Documentation\n\nThis is a **markdown** template.",
60
+ # Bug report template
61
+ bug_template = RailsTemplatable::Template.create!(
62
+ category: "bug_report",
63
+ content: "## Bug Description\n\n## Steps to Reproduce\n\n## Expected Behavior",
57
64
  content_format: :markdown
58
65
  )
59
66
  ```
60
67
 
61
- ### 3. Associate templates with models
68
+ ### 3. Assign templates to records
62
69
 
63
70
  ```ruby
64
- post = Post.create(title: "My Post", content: "Content here")
71
+ post = Post.create!(
72
+ title: "Add user authentication",
73
+ content: "Implement OAuth2 login",
74
+ template: feature_template
75
+ )
76
+
77
+ # View template
78
+ post.template.category # => "feature_request"
79
+ post.template.content # => "# Feature Request..."
80
+ ```
81
+
82
+ ## Template Categories
83
+
84
+ Common template categories included:
85
+
86
+ | Category | Description |
87
+ |----------|-------------|
88
+ | `feature_request` | Feature requests |
89
+ | `bug_report` | Bug reports |
90
+ | `tech_improvement` | Technical improvements |
91
+ | `meeting_note` | Meeting notes |
92
+ | `api_design` | API designs |
65
93
 
66
- # Add templates
67
- post.templates << email_template
68
- post.templates << doc_template
94
+ You can create any custom category as needed.
69
95
 
70
- # Query templates
71
- post.templates.where(category: "email_template")
96
+ ## Relationship Model
97
+
98
+ **One-to-Many (1:N)**
99
+ - One Post/WorkLog instance → One Template
100
+ - One Template → Many instances
101
+
102
+ Example:
103
+ ```
104
+ Post 1 → Template A (feature_request)
105
+ Post 2 → Template B (bug_report)
106
+ Post 3 → Template A (feature_request) # Reusable
72
107
  ```
73
108
 
74
109
  ## Content Formats
75
110
 
76
- The engine supports three content formats out of the box:
111
+ Three content formats are supported:
77
112
 
78
113
  - `html` (0) - HTML content
79
114
  - `markdown` (1) - Markdown content
@@ -87,6 +122,22 @@ RailsTemplatable::Template.create!(
87
122
  )
88
123
  ```
89
124
 
125
+ ## Query Examples
126
+
127
+ ```ruby
128
+ # Get a record's template
129
+ post.template
130
+
131
+ # Query by category
132
+ Post.joins(:template).where(rails_templatable_templates: { category: 'feature_request' })
133
+
134
+ # Get all records using a specific template
135
+ Post.where(template: feature_template)
136
+
137
+ # Query by content format
138
+ Post.joins(:template).where(rails_templatable_templates: { content_format: 1 })
139
+ ```
140
+
90
141
  ## Database Schema
91
142
 
92
143
  ### rails_templatable_templates
@@ -99,41 +150,49 @@ RailsTemplatable::Template.create!(
99
150
  | `created_at` | datetime | Creation timestamp |
100
151
  | `updated_at` | datetime | Update timestamp |
101
152
 
102
- ### rails_templatable_assignments
153
+ ### Target models (e.g., posts)
154
+
155
+ Add `template_id` foreign key:
103
156
 
104
157
  | Column | Type | Description |
105
158
  |--------|------|-------------|
106
- | `template_id` | integer | Foreign key to template |
107
- | `templatable_id` | integer | Polymorphic foreign key ID |
108
- | `templatable_type` | string | Polymorphic foreign key type |
109
- | `created_at` | datetime | Creation timestamp |
110
- | `updated_at` | datetime | Update timestamp |
159
+ | `template_id` | integer | Foreign key to rails_templatable_templates |
111
160
 
112
161
  ## Advanced Usage
113
162
 
114
- ### Query helper methods
163
+ ### Change template
115
164
 
116
165
  ```ruby
117
- # Get all objects using a specific template
118
- template.assigned_to(Post)
119
-
120
- # Get all templates used on a specific model class
121
- RailsTemplatable::Template.for_model(Post)
166
+ # Assigning a new template replaces the old one
167
+ post.update(template: bug_template)
168
+ post.template.category # => "bug_report"
122
169
  ```
123
170
 
124
- ### Checking template associations
171
+ ### Remove template
125
172
 
126
173
  ```ruby
127
- # Check if a post has email templates
128
- post.templates.where(category: "email_template").exists?
174
+ post.update(template: nil)
175
+ post.template # => nil
176
+ ```
177
+
178
+ ### Assign template after creation
129
179
 
130
- # Get all HTML format templates
131
- post.templates.html
180
+ ```ruby
181
+ post = Post.create(title: "New post")
182
+ post.update(template: feature_template)
132
183
  ```
133
184
 
134
- ## Why `category` instead of `type`?
185
+ ## Migration Example
135
186
 
136
- We use `category` instead of `type` to avoid conflicts with Rails' Single Table Inheritance (STI) feature, which reserves the `type` column for storing class names.
187
+ ```ruby
188
+ class AddTemplateToPosts < ActiveRecord::Migration[6.0]
189
+ def change
190
+ add_reference :posts, :template,
191
+ foreign_key: { to_table: :rails_templatable_templates },
192
+ index: false
193
+ end
194
+ end
195
+ ```
137
196
 
138
197
  ## Development
139
198
 
@@ -144,45 +203,20 @@ cd test/dummy
144
203
  ruby test_templatable.rb
145
204
  ```
146
205
 
147
- ### Database schema
148
-
149
- After running migrations, your schema will include:
150
-
151
- ```ruby
152
- create_table "rails_templatable_templates" do |t|
153
- t.string "category", null: false
154
- t.text "content"
155
- t.integer "content_format", default: 2, null: false
156
- t.index ["category"], name: "index_rails_templatable_templates_on_category"
157
- end
158
-
159
- create_table "rails_templatable_assignments" do |t|
160
- t.integer "template_id", null: false
161
- t.string "templatable_type", null: false
162
- t.integer "templatable_id", null: false
163
- t.index ["template_id", "templatable_type", "templatable_id"],
164
- name: "index_templatable_assignments", unique: true
165
- end
166
- ```
167
-
168
206
  ## Architecture
169
207
 
170
- This engine follows the same pattern as [rails_badgeable](https://github.com/afeiship/rails_badgeable):
208
+ - **Direct Foreign Key** - No join table needed
209
+ - **Simple & Clean** - One record = One template
210
+ - **Flexible** - Templates can be changed anytime
211
+ - **Efficient Queries** - Direct JOIN without intermediate table
171
212
 
172
- - **Isolated namespace** - All models namespaced under `RailsTemplatable`
173
- - **Polymorphic associations** - Using `templatable` for flexible model associations
174
- - **Concern-based inclusion** - Simple `include RailsTemplatable::HasTemplates` to enable
175
- - **Database-level constraints** - Unique indexes prevent duplicate associations
213
+ ## Comparison with N:N Design
176
214
 
177
- ## Comparison with rails_badgeable
215
+ If you need "one record has multiple templates", consider using a tag system or polymorphic many-to-many associations instead.
178
216
 
179
- | Feature | rails_badgeable | rails_templatable |
180
- |---------|-----------------|-------------------|
181
- | Main Model | Badge | Template |
182
- | Fields | name, description | category, content, content_format |
183
- | Enum Support | No | Yes (content_format) |
184
- | Polymorphic Name | assignable | templatable |
185
- | Concern | HasBadges | HasTemplates |
217
+ ## Why `category` instead of `type`?
218
+
219
+ We use `category` instead of `type` to avoid conflicts with Rails' Single Table Inheritance (STI) feature, which reserves the `type` column for storing class names.
186
220
 
187
221
  ## Future Enhancements
188
222
 
@@ -0,0 +1,12 @@
1
+ module RailsTemplatable
2
+ module HasTemplate
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ # Each model instance can have only one template
7
+ belongs_to :template,
8
+ class_name: "RailsTemplatable::Template",
9
+ optional: true
10
+ end
11
+ end
12
+ end
@@ -2,24 +2,21 @@ module RailsTemplatable
2
2
  class Template < ApplicationRecord
3
3
  self.table_name = "rails_templatable_templates"
4
4
 
5
- has_many :assignments, class_name: "RailsTemplatable::TemplateAssignment", dependent: :destroy
6
-
7
5
  enum :content_format, { html: 0, markdown: 1, txt: 2 }
8
6
 
9
7
  validates :category, presence: true
10
8
  validates :content, presence: true
11
9
  validates :content_format, presence: true
12
10
 
13
- # Returns all associated records of the given class that have this template
14
- def assigned_to(klass)
15
- templatable_ids = assignments.where(templatable_type: klass.base_class.name).pluck(:templatable_id)
16
- klass.where(id: templatable_ids)
17
- end
18
-
19
- # Class method: Returns all templates ever used on the given model class
20
- def self.for_model(klass)
21
- template_ids = TemplateAssignment.where(templatable_type: klass.base_class.name).distinct.pluck(:template_id)
22
- where(id: template_ids)
23
- end
11
+ # Note: This template can be referenced by any model that includes HasTemplate concern.
12
+ # Target models should define their own has_many relationship if needed.
13
+ #
14
+ # Example in target model:
15
+ # class Post < ApplicationRecord
16
+ # include RailsTemplatable::HasTemplate
17
+ #
18
+ # # Optional: define reverse association
19
+ # has_many :feature_posts, class_name: "Post", foreign_key: :template_id
20
+ # end
24
21
  end
25
22
  end
@@ -16,21 +16,14 @@ class CreateRailsTemplatableTables < ActiveRecord::Migration[6.0]
16
16
 
17
17
  add_index :rails_templatable_templates, :category
18
18
 
19
- # Create join table for polymorphic associations
20
- # To add custom fields to assignments, add columns here, e.g.:
21
- # t.integer :priority
22
- # t.boolean :active, default: true
23
- create_table :rails_templatable_assignments do |t|
24
- t.references :template, null: false, foreign_key: { to_table: :rails_templatable_templates }
25
- t.references :templatable, polymorphic: true, null: false, index: false
26
-
27
- t.timestamps
28
- end
29
-
30
- # Custom composite index with short name
31
- add_index :rails_templatable_assignments,
32
- [:template_id, :templatable_type, :templatable_id],
33
- unique: true,
34
- name: "index_templatable_assignments"
19
+ # Note: No join table needed.
20
+ # Target models (posts, work_logs, etc.) should add their own template_id foreign key:
21
+ #
22
+ # add_reference :posts, :template,
23
+ # foreign_key: { to_table: :rails_templatable_templates },
24
+ # index: false
25
+ # add_reference :work_logs, :template,
26
+ # foreign_key: { to_table: :rails_templatable_templates },
27
+ # index: false
35
28
  end
36
29
  end
@@ -1,3 +1,3 @@
1
1
  module RailsTemplatable
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.2"
3
3
  end
data/llms.txt CHANGED
@@ -1,14 +1,14 @@
1
1
  # Rails Templatable
2
2
 
3
- > Rails Templatable is a lightweight, reusable Rails Engine that enables any ActiveRecord model to associate with templates for content management via polymorphic many-to-many relationships.
3
+ > Rails Templatable is a lightweight, reusable Rails Engine that enables any ActiveRecord model to use predefined templates for content management via a simple one-to-many relationship.
4
4
 
5
5
  It provides a clean, namespace-isolated way to add template functionality to your Rails applications without polluting your models or database schema. The engine uses standard Rails conventions and is fully compatible with Rails 6.0+.
6
6
 
7
7
  **Important notes for AI agents:**
8
8
  - This is a Rails Engine, not a mountable engine (no routes, controllers, or views)
9
9
  - All models are namespaced under `RailsTemplatable` module
10
- - Table names are explicitly set to avoid conflicts: `rails_templatable_templates` and `rails_templatable_assignments`
11
- - The engine uses polymorphic associations, allowing templates to be assigned to ANY model
10
+ - Uses 1:N relationship (one record has one template, one template can have many records)
11
+ - No join table - uses direct foreign key `template_id` on target models
12
12
  - Uses `category` instead of `type` to avoid Rails STI (Single Table Inheritance) conflicts
13
13
  - Supports three content formats: HTML (0), Markdown (1), and TXT (2)
14
14
  - The `content_format` field uses Rails enum with new syntax: `enum :content_format, { html: 0, markdown: 1, txt: 2 }`
@@ -17,18 +17,17 @@ It provides a clean, namespace-isolated way to add template functionality to you
17
17
 
18
18
  - [GitHub Repository](https://github.com/afeiship/rails_templatable): Source code and issue tracking
19
19
  - [README](README.md): Installation guide and basic usage examples
20
- - [PRD](docs/01-prd.md): Detailed product requirements and design specifications
20
+ - [Design Document](docs/01-design.md): Detailed design specifications
21
21
  - [Usage Guide](docs/02-usage.md): Comprehensive usage examples
22
- - [Implementation Summary](docs/03-implementation-summary.md): Implementation details and testing results
22
+ - [Redesign Summary](docs/03-redesign-summary.md): Architecture redesign from N:N to 1:N
23
23
 
24
24
  ## Key Features
25
25
 
26
- - **Polymorphic Associations:** Assign templates to any ActiveRecord model (Post, WorkLog, User, etc.)
27
- - **Multiple Content Formats:** Support for HTML, Markdown, and plain text templates
28
- - **Database-level Constraints:** Unique indexes prevent duplicate template assignments
29
- - **Namespace Isolated:** All models under `RailsTemplatable` namespace to avoid conflicts
30
- - **Explicit Table Names:** No reliance on Rails auto-inflection
31
- - **Flexible Categories:** User-defined template categories (no hardcoded types)
26
+ - **Simple 1:N Relationship:** Each record has one template, templates can be reused
27
+ - **Multiple Content Formats:** Support for HTML, Markdown, and plain text
28
+ - **Flexible Categories:** User-defined template categories (feature_request, bug_report, etc.)
29
+ - **Direct Foreign Key:** No join table needed, cleaner database schema
30
+ - **Namespace Isolated:** All models under `RailsTemplatable` namespace
32
31
  - **Rails 6.0+ Compatible:** Works with modern Rails versions (tested on Rails 8.1.2)
33
32
  - **Zero Configuration:** Works out of the box after migration
34
33
 
@@ -47,79 +46,147 @@ rails railties:install:migrations FROM=rails_templatable
47
46
  rails db:migrate
48
47
  ```
49
48
 
49
+ Add foreign key to each model that will use templates:
50
+
51
+ ```ruby
52
+ add_reference :posts, :template,
53
+ foreign_key: { to_table: :rails_templatable_templates },
54
+ index: false
55
+ ```
56
+
50
57
  ## Basic Usage
51
58
 
52
59
  ### Include the Concern
53
60
 
54
61
  ```ruby
55
62
  class Post < ApplicationRecord
56
- include RailsTemplatable::HasTemplates
63
+ include RailsTemplatable::HasTemplate
57
64
  end
58
65
 
59
66
  class WorkLog < ApplicationRecord
60
- include RailsTemplatable::HasTemplates
67
+ include RailsTemplatable::HasTemplate
61
68
  end
62
69
  ```
63
70
 
64
- ### Create and Assign Templates
71
+ ### Create Templates
65
72
 
66
73
  ```ruby
67
- # Create an HTML template
68
- email_template = RailsTemplatable::Template.create!(
69
- category: "email_template",
70
- content: "<h1>Welcome!</h1><p>Hello {{name}}</p>",
71
- content_format: :html
74
+ # Feature request template
75
+ feature_template = RailsTemplatable::Template.create!(
76
+ category: "feature_request",
77
+ content: "# Feature Request\n\n## Description\n\n## Acceptance Criteria",
78
+ content_format: :markdown
79
+ )
80
+
81
+ # Bug report template
82
+ bug_template = RailsTemplatable::Template.create!(
83
+ category: "bug_report",
84
+ content: "## Bug Description\n\n## Steps to Reproduce\n\n## Expected Behavior",
85
+ content_format: :markdown
72
86
  )
73
87
 
74
- # Create a Markdown template
75
- doc_template = RailsTemplatable::Template.create!(
76
- category: "documentation",
77
- content: "# Documentation\n\nThis is a **markdown** template.",
88
+ # Meeting note template
89
+ meeting_template = RailsTemplatable::Template.create!(
90
+ category: "meeting_note",
91
+ content: "# Meeting: {title}\n\nDate: {date}\n\n## Attendees\n\n## Agenda",
78
92
  content_format: :markdown
79
93
  )
80
94
 
81
- # Assign to a post
82
- post = Post.create(title: "Hello World")
83
- post.templates << email_template
84
- post.templates << doc_template
95
+ # Tech improvement template
96
+ tech_template = RailsTemplatable::Template.create!(
97
+ category: "tech_improvement",
98
+ content: "## Current State\n\n## Proposed Improvement\n\n## Benefits",
99
+ content_format: :markdown
100
+ )
85
101
 
86
- # Check if post has the template
87
- post.templates.include?(email_template) # => true
102
+ # API design template
103
+ api_template = RailsTemplatable::Template.create!(
104
+ category: "api_design",
105
+ content: "## Endpoint\n\n## Request\n\n## Response",
106
+ content_format: :markdown
107
+ )
88
108
  ```
89
109
 
90
- ### Query Methods
110
+ ### Assign Templates to Records
91
111
 
92
112
  ```ruby
93
- # Get all posts with a specific template
94
- template.assigned_to(Post)
95
- # => [#<Post id: 1, ...>, #<Post id: 3, ...>]
113
+ # Create with template
114
+ post = Post.create!(
115
+ title: "Add user authentication",
116
+ content: "Implement OAuth2",
117
+ template: feature_template
118
+ )
96
119
 
97
- # Get all templates ever used on Post model
98
- RailsTemplatable::Template.for_model(Post)
99
- # => [#<RailsTemplatable::Template category: "email_template">, ...]
120
+ # Assign template later
121
+ work_log = WorkLog.create!(title: "Daily standup")
122
+ work_log.update(template: meeting_template)
100
123
 
101
- # Filter templates by category
102
- post.templates.where(category: "email_template")
124
+ # View template
125
+ post.template.category # => "feature_request"
126
+ post.template.content # => "# Feature Request..."
127
+ ```
128
+
129
+ ### Query Templates
130
+
131
+ ```ruby
132
+ # Get a record's template
133
+ post.template
103
134
 
104
- # Filter templates by content format
105
- post.templates.html # HTML templates only
106
- post.templates.markdown # Markdown templates only
107
- post.templates.txt # Plain text templates only
135
+ # Query by category
136
+ Post.joins(:template).where(rails_templatable_templates: { category: 'feature_request' })
137
+
138
+ # Get all records using a specific template
139
+ Post.where(template: feature_template)
140
+
141
+ # Query by content format
142
+ Post.joins(:template).where(rails_templatable_templates: { content_format: 1 })
108
143
  ```
109
144
 
110
- ### Remove Templates
145
+ ### Change Template
111
146
 
112
147
  ```ruby
113
- # Remove a specific template
114
- post.templates.destroy(email_template)
148
+ # Assigning a new template replaces the old one
149
+ post.update(template: bug_template)
150
+ post.template.category # => "bug_report"
151
+ ```
152
+
153
+ ### Remove Template
154
+
155
+ ```ruby
156
+ post.update(template: nil)
157
+ post.template # => nil
158
+ ```
159
+
160
+ ## Template Categories
161
+
162
+ Common template categories:
115
163
 
116
- # Remove all templates
117
- post.templates.clear
164
+ | Category | Description |
165
+ |----------|-------------|
166
+ | `feature_request` | Feature requests |
167
+ | `bug_report` | Bug reports |
168
+ | `tech_improvement` | Technical improvements |
169
+ | `meeting_note` | Meeting notes |
170
+ | `api_design` | API designs |
171
+
172
+ Users can create any custom category.
173
+
174
+ ## Relationship Model
175
+
176
+ **One-to-Many (1:N)**
177
+ - One Post/WorkLog instance → One Template
178
+ - One Template → Many instances
179
+
180
+ Example:
181
+ ```
182
+ Post 1 → Template A (feature_request)
183
+ Post 2 → Template B (bug_report)
184
+ Post 3 → Template A (feature_request) # Reusable
118
185
  ```
119
186
 
120
187
  ## Database Schema
121
188
 
122
- ### templates table
189
+ ### rails_templatable_templates
123
190
 
124
191
  ```ruby
125
192
  create_table :rails_templatable_templates do |t|
@@ -133,27 +200,22 @@ add_index :rails_templatable_templates, :category
133
200
  ```
134
201
 
135
202
  **Field Details:**
136
- - `category` (string) - User-defined template category (e.g., "email_template", "documentation")
203
+ - `category` (string) - User-defined template category
137
204
  - `content` (text) - Template content (static text, no variable replacement)
138
205
  - `content_format` (integer) - Content format: 0=HTML, 1=Markdown, 2=TXT (default)
139
206
  - Index on `category` for fast lookups
140
207
 
141
- ### assignments table (join table)
208
+ ### Target Models (e.g., posts)
142
209
 
143
- ```ruby
144
- create_table :rails_templatable_assignments do |t|
145
- t.references :template, null: false, foreign_key: { to_table: :rails_templatable_templates }
146
- t.references :templatable, polymorphic: true, null: false
147
- t.timestamps
148
- end
210
+ Add `template_id` foreign key:
149
211
 
150
- add_index :rails_templatable_assignments,
151
- [:template_id, :templatable_type, :templatable_id],
152
- unique: true,
153
- name: "index_templatable_assignments"
212
+ ```ruby
213
+ add_reference :posts, :template,
214
+ foreign_key: { to_table: :rails_templatable_templates },
215
+ index: false
154
216
  ```
155
217
 
156
- **Constraint:** Unique composite index prevents duplicate template assignments to the same record.
218
+ **Constraint:** Each record can have only one template (foreign key is single, not array).
157
219
 
158
220
  ## Content Formats
159
221
 
@@ -170,7 +232,12 @@ enum :content_format, {
170
232
  **Usage:**
171
233
 
172
234
  ```ruby
173
- # Create different format templates
235
+ # Query by format
236
+ post.template.html? # => true/false
237
+ post.template.markdown? # => true/false
238
+ post.template.txt? # => true/false
239
+
240
+ # Create different formats
174
241
  html_template = RailsTemplatable::Template.create!(
175
242
  category: "email",
176
243
  content: "<h1>Welcome</h1>",
@@ -179,116 +246,19 @@ html_template = RailsTemplatable::Template.create!(
179
246
 
180
247
  markdown_template = RailsTemplatable::Template.create!(
181
248
  category: "docs",
182
- content: "# Guide\n\nInstructions here",
249
+ content: "# Guide\n\nInstructions",
183
250
  content_format: :markdown
184
251
  )
185
-
186
- txt_template = RailsTemplatable::Template.create!(
187
- category: "notification",
188
- content: "Simple text message",
189
- content_format: :txt
190
- )
191
-
192
- # Query by format
193
- post.templates.html # => [#<RailsTemplatable::Template content_format: 0>]
194
- post.templates.markdown # => [#<RailsTemplatable::Template content_format: 1>]
195
- post.templates.txt # => [#<RailsTemplatable::Template content_format: 2>]
196
252
  ```
197
253
 
198
254
  ## Available Methods
199
255
 
200
- ### Template Instance Methods
201
-
202
- - `template.assigned_to(klass)` - Returns all records of the given class that have this template
203
- ```ruby
204
- template.assigned_to(Post) # => [post1, post2]
205
- template.assigned_to(WorkLog) # => [work_log1, work_log3]
206
- ```
207
-
208
- ### Template Class Methods
209
-
210
- - `RailsTemplatable::Template.for_model(klass)` - Returns all templates ever used on the given model class
211
- ```ruby
212
- RailsTemplatable::Template.for_model(Post) # => [template1, template2]
213
- ```
214
-
215
- ### Model Methods (with HasTemplates concern)
216
-
217
- - `model.templates` - Returns all templates assigned to this record
218
- - `model.templates << template` - Assign a template to this record
219
- - `model.templates.delete(template)` - Remove a specific template
220
- - `model.templates.clear` - Remove all templates
221
- - `model.rails_templatable_assignments` - Direct access to join records
222
- - `model.templates.where(category: "email_template")` - Filter by category
223
- - `model.templates.html` - Get HTML format templates only
224
- - `model.templates.markdown` - Get Markdown format templates only
225
- - `model.templates.txt` - Get plain text templates only
226
-
227
- ## Advanced Usage
256
+ ### Model Methods (with HasTemplate concern)
228
257
 
229
- ### Custom Fields
230
-
231
- You can add custom fields to templates or assignments via migrations:
232
-
233
- ```ruby
234
- # Add custom fields to templates
235
- add_column :rails_templatable_templates, :name, :string
236
- add_column :rails_templatable_templates, :description, :text
237
- add_column :rails_templatable_templates, :locale, :string, default: 'en'
238
-
239
- # Add custom fields to assignments
240
- add_column :rails_templatable_assignments, :priority, :integer, default: 0
241
- add_column :rails_templatable_assignments, :active, :boolean, default: true
242
- add_column :rails_templatable_assignments, :assigned_at, :datetime
243
- ```
244
-
245
- ### Accessing Assignment Data
246
-
247
- ```ruby
248
- # Get assignment metadata
249
- assignment = post.rails_templatable_assignments.find_by(template_id: 1)
250
- assignment.priority # => 1
251
- assignment.created_at # => assignment time
252
- ```
253
-
254
- ### Conditional Scopes
255
-
256
- Add scopes to your models:
257
-
258
- ```ruby
259
- class Post < ApplicationRecord
260
- include RailsTemplatable::HasTemplates
261
-
262
- scope :with_email_template, -> {
263
- joins(:templates)
264
- .where(rails_templatable_templates: { category: 'email_template' })
265
- }
266
-
267
- scope :with_html_templates, -> {
268
- joins(:templates)
269
- .where(rails_templatable_templates: { content_format: 0 })
270
- }
271
- end
272
-
273
- # Usage
274
- Post.with_email_template # All posts with email templates
275
- Post.with_html_templates # All posts with HTML templates
276
- ```
277
-
278
- ### Checking Template Existence
279
-
280
- ```ruby
281
- # Check if a post has templates from a specific category
282
- post.templates.where(category: "email_template").exists? # => true/false
283
-
284
- # Check if a post has HTML templates
285
- post.templates.html.any? # => true/false
286
-
287
- # Count templates by format
288
- post.templates.html.count # => 2
289
- post.templates.markdown.count # => 1
290
- post.templates.txt.count # => 0
291
- ```
258
+ - `model.template` - Returns the assigned template or nil
259
+ - `model.template=(template)` - Assign a template (replaces existing)
260
+ - `model.update(template: template)` - Update template
261
+ - `model.update(template: nil)` - Remove template association
292
262
 
293
263
  ## Testing
294
264
 
@@ -301,27 +271,23 @@ ruby test_templatable.rb
301
271
 
302
272
  Expected output:
303
273
  ```
304
- 🚀 Testing Rails Templatable Engine
305
- ==================================================
274
+ 🚀 Testing Rails Templatable Engine (1:N Relationship)
275
+ ============================================================
306
276
 
307
277
  📝 Creating templates...
308
- ✓ Created HTML email template
309
- ✓ Created Markdown documentation template
310
- ✓ Created text notification template
311
-
312
- 📄 Creating Posts and WorkLogs...
313
- ✓ Created 2 Posts and 2 WorkLogs
314
-
315
- 🔗 Associating templates with models...
316
- ✓ Templates associated successfully
317
-
318
- 🔍 Testing queries...
319
- Post1 has 2 templates
320
- Post1 templates: email_template, documentation
321
- ...
278
+ ✓ Created feature_request template
279
+ ✓ Created bug_report template
280
+ ...
322
281
 
323
282
  ✅ All tests passed successfully!
324
- ==================================================
283
+ ============================================================
284
+
285
+ 📊 Summary:
286
+ - Created 5 templates
287
+ - Created 6 posts
288
+ - Created 3 work logs
289
+ - 1:N relationship verified: one record → one template
290
+ - 1:N relationship verified: one template → many records
325
291
  ```
326
292
 
327
293
  ## Project Structure
@@ -335,75 +301,78 @@ lib/
335
301
  app/models/
336
302
  ├── rails_templatable/
337
303
  │ ├── application_record.rb # Base AR class
338
- ├── template.rb # Template model
339
- │ └── template_assignment.rb # Join model
304
+ └── template.rb # Template model
340
305
  └── models/concerns/
341
306
  └── rails_templatable/
342
- └── has_templates.rb # Concern for host models
307
+ └── has_template.rb # Concern for host models
343
308
  db/migrate/
344
309
  └── 20250207000000_create_rails_templatable_tables.rb # Migration
345
310
  test/dummy/
346
311
  ├── app/models/
347
- │ ├── post.rb # Example: Post with HasTemplates
348
- │ └── work_log.rb # Example: WorkLog with HasTemplates
349
- ├── db/schema.rb # Database schema
312
+ │ ├── post.rb # Example: Post with HasTemplate
313
+ │ └── work_log.rb # Example: WorkLog with HasTemplate
314
+ ├── db/migrate/
315
+ │ ├── xxx_add_template_to_posts.rb # Add template_id to posts
316
+ │ └── xxx_add_template_to_work_logs.rb # Add template_id to work_logs
350
317
  └── test_templatable.rb # Integration test script
351
318
  docs/
352
- ├── 01-prd.md # Product Requirements Document
353
- ├── 02-usage.md # Usage Guide
354
- └── 03-implementation-summary.md # Implementation Summary
319
+ ├── 01-design.md # Design document
320
+ ├── 02-usage.md # Usage guide
321
+ └── 03-redesign-summary.md # Architecture redesign summary
355
322
  ```
356
323
 
324
+ ## Architecture Decisions
325
+
326
+ ### Why 1:N instead of N:N?
327
+
328
+ The original design used N:N (many-to-many) with a join table, but this was changed to 1:N (one-to-many) because:
329
+ - In practice, a document/post typically needs only one template
330
+ - Simpler database schema (no join table)
331
+ - More efficient queries (direct foreign key)
332
+ - Clearer semantics: "This post uses which template?"
333
+
334
+ ### Why `category` instead of `type`?
335
+
336
+ Rails reserves the `type` column for Single Table Inheritance (STI). To avoid conflicts:
337
+ - We use `category` for template classification
338
+ - Users can define any category string
339
+ - No hardcoded enum or predefined types
340
+
341
+ ### Why explicit table names?
342
+
343
+ To prevent any naming conflicts:
344
+ - `rails_templatable_templates` - Template storage
345
+ - No reliance on Rails auto-inflection
346
+ - Clear namespace isolation
347
+
357
348
  ## Comparison with rails_badgeable
358
349
 
359
350
  | Feature | rails_badgeable | rails_templatable |
360
351
  |---------|-----------------|-------------------|
361
352
  | Main Model | Badge | Template |
353
+ | Relationship | N:N (many-to-many) | 1:N (one-to-many) |
354
+ | Join Table | badge_assignments | None (direct foreign key) |
355
+ | Model Association | has_many :badges | belongs_to :template |
362
356
  | Fields | name, description | category, content, content_format |
363
- | Enum Support | No | Yes (content_format: html/markdown/txt) |
364
- | Polymorphic Name | assignable | templatable |
365
- | Concern | HasBadges | HasTemplates |
366
- | STI Conflict Handling | N/A | Uses `category` to avoid `type` conflict |
357
+ | Enum Support | No | Yes (content_format) |
358
+ | Polymorphic Name | assignable | N/A (not polymorphic) |
359
+ | Concern | HasBadges | HasTemplate |
367
360
 
368
361
  ## Usage Notes for Agents
369
362
 
370
- - **Always use full namespace:** `RailsTemplatable::Template` (no alias generator yet)
363
+ - **Always use full namespace:** `RailsTemplatable::Template` (no alias generator)
371
364
  - **Categories are user-defined:** No predefined categories, use any string value
372
365
  - **Content formats are enforced:** Use symbols: `:html`, `:markdown`, `:txt`
373
- - **Polymorphic type stores class name:** Uses `base_class.name` for STI support
374
- - **Database prevents duplicates:** Unique index on `[template_id, templatable_type, templatable_id]`
366
+ - **One record = One template:** Assigning a new template replaces the old one
367
+ - **Direct foreign key:** Each model needs its own `template_id` column
368
+ - **No join table:** Simpler schema than rails_badgeable
375
369
  - **Engine is not mountable:** No routes, controllers, or views included
376
370
  - **Migrations must be copied:** Use `rails railties:install:migrations FROM=rails_templatable`
377
371
  - **Custom fields require migration:** Add columns directly to tables if needed
378
- - **Join table accessible:** `model.rails_templatable_assignments` for metadata access
379
372
  - **`category` field is indexed:** Efficient queries by category
380
373
  - **Default content_format is TXT:** New templates default to plain text (2)
381
374
  - **Rails 8.x enum syntax:** Must use `enum :content_format, { ... }` not `enum content_format: { ... }`
382
375
 
383
- ## Architecture Decisions
384
-
385
- ### Why `category` instead of `type`?
386
-
387
- Rails reserves the `type` column for Single Table Inheritance (STI). To avoid conflicts:
388
- - We use `category` for template classification
389
- - Users can define any category string (email_template, documentation, etc.)
390
- - No hardcoded enum or predefined types
391
-
392
- ### Why explicit table names?
393
-
394
- To prevent any naming conflicts:
395
- - `rails_templatable_templates` - Template storage
396
- - `rails_templatable_assignments` - Polymorphic join table
397
- - No reliance on Rails auto-inflection
398
-
399
- ### Why enum for content_format?
400
-
401
- Using Rails enum provides:
402
- - Type safety (only valid formats)
403
- - Helper methods (`html`, `markdown`, `txt`)
404
- - Integer storage for efficiency
405
- - Easy to extend with new formats
406
-
407
376
  ## Development
408
377
 
409
378
  - Rails Engine (non-mountable)
@@ -411,17 +380,16 @@ Using Rails enum provides:
411
380
  - Rails 6.0+ compatible (tested on Rails 8.1.2)
412
381
  - MIT License
413
382
  - Author: aric.zheng <1290657123@qq.com>
414
- - Version: 0.1.0
383
+ - Version: 0.1.1
415
384
 
416
385
  ## Future Enhancements
417
386
 
418
387
  Planned features for future versions:
419
388
  - Template variable interpolation (replace `{{name}}` with actual data)
420
389
  - Template versioning system
421
- - Template inheritance and overriding
422
390
  - Template preview functionality
423
391
  - I18n multi-language support
424
- - Short alias generator (like rails_badgeable)
392
+ - Template validation based on structure
425
393
 
426
394
  ## Contributing
427
395
 
@@ -431,3 +399,4 @@ When contributing to rails_templatable:
431
399
  3. Add tests for new features
432
400
  4. Update this llms.txt file with API changes
433
401
  5. Ensure Rails 6.0+ compatibility
402
+ 6. Remember: 1:N relationship, not N:N
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_templatable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - aric.zheng
@@ -31,7 +31,9 @@ dependencies:
31
31
  - !ruby/object:Gem::Version
32
32
  version: '9.0'
33
33
  description: A lightweight Rails Engine that enables any ActiveRecord model to use
34
- templates for content generation and management.
34
+ predefined templates via a simple one-to-many relationship. Each record has one
35
+ template, templates can be reused across multiple records. Supports HTML, Markdown,
36
+ and plain text formats with flexible categories.
35
37
  email:
36
38
  - 1290657123@qq.com
37
39
  executables: []
@@ -41,16 +43,9 @@ files:
41
43
  - MIT-LICENSE
42
44
  - README.md
43
45
  - Rakefile
44
- - app/assets/stylesheets/rails_templatable/application.css
45
- - app/controllers/rails_templatable/application_controller.rb
46
- - app/helpers/rails_templatable/application_helper.rb
47
- - app/jobs/rails_templatable/application_job.rb
48
- - app/mailers/rails_templatable/application_mailer.rb
49
- - app/models/concerns/rails_templatable/has_templates.rb
46
+ - app/models/concerns/rails_templatable/has_template.rb
50
47
  - app/models/rails_templatable/application_record.rb
51
48
  - app/models/rails_templatable/template.rb
52
- - app/models/rails_templatable/template_assignment.rb
53
- - app/views/layouts/rails_templatable/application.html.erb
54
49
  - config/routes.rb
55
50
  - db/migrate/20250207000000_create_rails_templatable_tables.rb
56
51
  - lib/rails_templatable.rb
@@ -84,5 +79,5 @@ requirements: []
84
79
  rubygems_version: 3.5.22
85
80
  signing_key:
86
81
  specification_version: 4
87
- summary: A Rails engine for templatable models.
82
+ summary: A Rails engine for template management with 1:N relationship.
88
83
  test_files: []
@@ -1,15 +0,0 @@
1
- /*
2
- * This is a manifest file that'll be compiled into application.css, which will include all the files
3
- * listed below.
4
- *
5
- * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
- * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
- *
8
- * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
- * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
- * files in this directory. Styles in this file should be added after the last require_* statement.
11
- * It is generally better to create a new file per style scope.
12
- *
13
- *= require_tree .
14
- *= require_self
15
- */
@@ -1,4 +0,0 @@
1
- module RailsTemplatable
2
- class ApplicationController < ActionController::Base
3
- end
4
- end
@@ -1,4 +0,0 @@
1
- module RailsTemplatable
2
- module ApplicationHelper
3
- end
4
- end
@@ -1,4 +0,0 @@
1
- module RailsTemplatable
2
- class ApplicationJob < ActiveJob::Base
3
- end
4
- end
@@ -1,6 +0,0 @@
1
- module RailsTemplatable
2
- class ApplicationMailer < ActionMailer::Base
3
- default from: "from@example.com"
4
- layout "mailer"
5
- end
6
- end
@@ -1,16 +0,0 @@
1
- module RailsTemplatable
2
- module HasTemplates
3
- extend ActiveSupport::Concern
4
-
5
- included do
6
- has_many :rails_templatable_assignments,
7
- as: :templatable,
8
- class_name: "RailsTemplatable::TemplateAssignment",
9
- dependent: :destroy
10
-
11
- has_many :templates,
12
- through: :rails_templatable_assignments,
13
- class_name: "RailsTemplatable::Template"
14
- end
15
- end
16
- end
@@ -1,12 +0,0 @@
1
- module RailsTemplatable
2
- class TemplateAssignment < ApplicationRecord
3
- self.table_name = "rails_templatable_assignments"
4
-
5
- belongs_to :template, class_name: "RailsTemplatable::Template"
6
- belongs_to :templatable, polymorphic: true
7
-
8
- validates :template, presence: true
9
- validates :templatable, presence: true
10
- validates :template_id, uniqueness: { scope: [:templatable_type, :templatable_id] }
11
- end
12
- end
@@ -1,17 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>Rails templatable</title>
5
- <%= csrf_meta_tags %>
6
- <%= csp_meta_tag %>
7
-
8
- <%= yield :head %>
9
-
10
- <%= stylesheet_link_tag "rails_templatable/application", media: "all" %>
11
- </head>
12
- <body>
13
-
14
- <%= yield %>
15
-
16
- </body>
17
- </html>