Make a generator of models inheriting from a pre-built model - ruby-on-rails

I'm writing a little Ruby on Rails CMS, and I want to make a generator that would create models inheriting from the pre-built CMS data model. For example, I have a Entry model in my CMS, and I want to create a Post model that would be child of this model. This is the source code:
db/migrations/*create_mycms_entries.rb:
class CreateMyCmsEntries < ActiveRecord::Migration[5.0]
def change
create_table :my_cms_entries do |t|
t.string :type, index: true
t.string :title
t.string :slug, index: true
t.json :payload
t.integer :user_id, index: true
t.string :author_name
t.datetime :published_at
t.timestamps null: false
end
end
end
entry.rb:
module MyCms
class Entry < ActiveRecord::Base
scope :published, -> { where('published_at <= ?', Time.zone.now) }
def self.content_attr(attr_name, attr_type = :string)
content_attributes[attr_name] = attr_type
define_method(attr_name) do
self.payload ||= {}
self.payload[attr_name.to_s]
end
define_method('#{attr_name}='.to_sym) do |value|
self.payload ||= {}
self.payload[attr_name.to_s] = value
end
end
def self.content_attributes
#content_attributes ||= {}
end
end
end
and, at my blog side, post.rb:
class Post < MyCms::Entry
content_attrs :body, :text
# Rest of the stuff...
end
I want the final command look something like this:
$ rails generate entry Post body:text
But I'm not quite sure I know how to implement it.

If you want to create custom model generator then you can use Thor (https://github.com/erikhuda/thor) which rails team has documented here
http://guides.rubyonrails.org/generators.html
First start by typing this command:
bin/rails g generator entry
So you are triggering generator to generate generator :). You will get something like this after running your command
create lib/generators/entry
create lib/generators/entry/entry_generator.rb
create lib/generators/entry/USAGE
create lib/generators/entry/templates
Then what you can do is just copy the code that ActiveRecord has for creating models. Inside your templates you will have this file model.rb like here
https://github.com/rails/rails/blob/master/activerecord/lib/rails/generators/active_record/model/templates/model.rb
You will see that it has something like this:
class <%= class_name %> < <%= parent_class_name.classify %>
class_name is a variable that you pass to your generator, like in your example Post. And you can define it like this inside your entry_generator
argument :class_name, type: :string, default: "Post"
I would definitely recommend that you look at this file since the rails generator has everything you need...you would just customize it for your specific need:
https://github.com/rails/rails/blob/master/activerecord/lib/rails/generators/active_record/model/model_generator.rb
In generator you have a class that invokes the template. Here is another video from Ryan Bytes that explains creating generator on custom layout which you can find it useful for your CMS.
http://railscasts.com/episodes/218-making-generators-in-rails-3?view=asciicast
Good luck

What you are trying to do is single-table inheritance (STI) and Rails has the support for that. Basically you need column type in your entries table which would store the class name within your hierarchy. You don't need to use the parent attribute as it seems to be in your example. See more in Rails docs or blogs, e.g. this one.

Related

Ruby on Rails - Is it possible to add a costum column whenever I create a New Table?

Whenever I create a table for User, it always have a ID column. I want that whenever I create a new table, a costum_column (uuid, in my case) always be added as a column, just like the ID.
(assuming you are talking about ruby on rails)
if you want the id to be a uuid the easiest is to have the following
class MyMigration < ActiveRecord::Migration
create_table "my_table", id: :uuid do |t|
# ...
end
end
If you want all create_table to ALWAYS add another custom column, such as public_id, then you could add a monkey patch to your create_table method
# initializers/custom_table_column.rb
module ActiveRecord
class Migration
class Current
module CustomColumnOnCreateTable
def create_table(*args)
add_custom_column = ->(t) { t.uuid("public_id", null: false) } # for example
if block_given?
super do |t|
add_custom_column.call(t)
yield compatible_table_definition(t)
end
else
super { |t| add_custom_column.call(t) }
end
end
end
prepend CustomColumnOnCreateTable
end
end
end
The issue with this solution IMO is that you are hiding things. People (collaborators or colleagues, especially more junior ones) who use the method are not expecting it and will search hours from where this unexpected behavior come from
IMO a better alternative is to create a helper that would allow to still manually have to call it, but in one line, or with a simple method call. so nothing is hidden, but it's easy.
module ActiveRecord
class Migration
class Current
module CustomColumnOnCreateTable
def add_public_id_column(t)
t.uuid("public_id", null: false) # for example
end
end
prepend CustomColumnOnCreateTable
end
end
end
then you can just
# my migration
# ...
def up
create_table :test, id: :uuid do |t|
add_public_id_column(t)
t.string :something_else
# ...
end
end

How to seed data with ActionText has_rich_text?

In my model, I have:
class Listing < ApplicationRecord
...
has_rich_text :description
...
end
In my seeds.rb:
#listing = Listing.new(
title: 'CODE VEIN',
price: 59.99 * 100,
description: "<p>#{Faker::Lorem.paragraphs(number: 30).join(' ')}</p>",
seller_id: seller.id,
release_date: Date.parse('Sep 26, 2019'),
status: :active,
esrb: 'MATURE'
)
Listing.description comes up nil, causing my NOT NULL constraint to error.
I've debugged with pry, and tried #listing.description.body= text or #listing.description.body = ActionText::Content.new(text), both still cause the listing#description to be nil.
This is an API Only project, but I use Trix RTE in the front-end react app. Is there a specific method to seed rich_text columns?
ActionText stores the actual contents in a seperate action_text_rich_texts table which uses a polymorphic assocation to link back to the model that its attached to.
# This migration comes from action_text (originally 20180528164100)
class CreateActionTextTables < ActiveRecord::Migration[6.0]
def change
create_table :action_text_rich_texts do |t|
t.string :name, null: false
t.text :body, size: :long
t.references :record, null: false, polymorphic: true, index: false
t.timestamps
t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true
end
end
end
The JavaScript component of ActionText (which is really the whole point) automatically sends AJAX requests when the user user interacts with Trix to create/update the row in the action_text_rich_texts table even before you have saved the record you're creating.
When you then submit the form you're actually submitting the id to the row on the action_text_rich_texts table and not the contents. Then when you save your model it updates the corresponding row on action_text_rich_texts with the record_type and record_id from the model. Its a pretty awkward solution that is built around the idea of never having to add columns to your model.
I don't really see the point in using ActionText in an API as the whole point is to add a quick and dirty Trix implementation to classical rails apps. You really should be able to solve this with just a single text column instead. Especially as that will let you access the contents without joining.
There must be something else going on with your code. Maybe if you share your migration and the complete Listing class file it might be easier to spot what's going on.
Here are a few steps to make sure you got it right:
Create a brand new rails app (you can delete it later):
rails new testrichtext -d mysql --api
Create the db
cd testrichclient
rake db:create
Create the model
rails g model listing description:text
Change your newly created migration file to make sure the column is not null:
class CreateListings < ActiveRecord::Migration[6.0]
def change
create_table :listings do |t|
t.text :description, null: false
t.timestamps
end
end
end
Run the migration
rake db:migrate
Now you should be able to log in into the console, and create a new listing with something as the description:
rails c
And inside console:
l = Listing.new(description: "<p>Something nice</p>")
l.save!
Listing.first.description
As you can see, this is enough to save/seed a new listing with rich text. So anything you may have going on there should be something you're causing somewhere else, by adding a validation differently, or callbacks. Hard to say without looking at the entire file
I found this to work for me:
15.times do
Post.create!(title: Faker::Book.unique.title)
end
then
Post.all.each do |post|
ActionText::RichText.create!(record_type: 'Post', record_id: post.id, name: 'content', body: Faker::Lorem.sentence)
end
To read:
Post.first.content.body.to_plain_text
or:
Post.first.content.body.to_trix_html
...as stated above by OP.
Source:
https://linuxtut.com/rails6-input-the-initial-data-of-actiontext-using-seed-9b4f2/

Has and belong to many Rails 4

I want to create a many to many relationship between two models, and I'd like to know step by step what to do. I'd like to have a explanation on HOW to do the migrations and HOW to ideally create the models. The way i'm trying to do right now is:
I create two models in the Ruby command line:
rails g model Location name:string
rails g model Datetime date:datetime
Now I have to open the recently created models and add:
//models/location.rb
class Location < ActiveRecord::Base
has_and_belong_to_many :datetimes
end
//models/datetime.rb
class Datetime< ActiveRecord::Base
has_and_belong_to_many :locations
end
Now apparently I have to do a migration, but I don't understand what it is, and the I guess some sources for oldest versions really got me confused. Can someone please explain in details?
Obs: There are some similar questions but they do not answer my question because they don't explain in depth what to do.
As suggested by ruby tutorial, we generate a new migration:
rails g migration CreateDatetimesAndLocations
Inside this migration i have:
class CreateDatetimesAndLocations < ActiveRecord::Migration
def change
create_table :locations_datetimes, id:false do |t|
t.belongs_to :datetime, index: true
t.belongs_to :location, index: true
end
end
end
This is done exactly like the ruby tutorial. Now I have this controller, that i'm testing on and it goes like that:
class WeatherController < ApplicationController
def data
#location = Location.new
#location.name = "test"
#datetime = Datetime.new
#datetime.date = DateTime.new(2001,2,3)
#location.datetimes << #datetime // **PROBLEM ON THIS LINE**
#location.save
#location = Location.new
#location.name = "teste2"
#location.locations << #location
#locations = Location.all //(show locations in view)
end
end
The problem I was having was because the locations_datetimes have to be datetimes_locations (in alphabetic order apparently).

Rails - using `rails generate model` to specify non-nullable field type

According to the rails documentation
http://guides.rubyonrails.org/migrations.html
2.3 Supported Type Modifiers says it should be possible to modify fields to allow or disallow NULL in the column, and that it's possible to do this on the terminal
This is what I want to appear in the migration file
class CreateTestModels < ActiveRecord::Migration
def change
create_table :test_models do |t|
t.string:non_nullable, :null => false
t.timestamps
end
end
end
On the terminal, I've tried
rails generate model TestModel non_nullable:string{null}
rails generate model TestModel 'non_nullable:string{null: false}'
I can't think of any other way to express it
Note: I already know you can go into the migration file and manually add it. That's not what I'm looking for.
The docs mention that
Some commonly used type modifiers can be passed directly on the command line. They are enclosed by curly braces and follow the field type
but they don't give details about which "commonly used" modifiers willl work.
As pointed out by mr rogers
there are only three supported options:
length for string/text/binary/integer (name:string{255})
precision,scale for decimal (dollar_fragment:decimal{3,2})
polymorphic for references/belongs_to (agent:references{polymorphic})
As mentioned by user2903934
it may be possible to make this work from the command line as a hack.
NOTE: this is a hack. i wouldn't recommend doing this but it does answer your question.
rails generate model TestModel 'non_nullable, null => false:string'
It looks like it splits on the first colon, so we can use a hashrocket syntax to sneak options in there. This yields:
class CreateTestModels < ActiveRecord::Migration
def change
create_table :test_models do |t|
t.string :non_nullable, null => false
t.timestamps
end
end
end
That obviously isn't officially supported, it just happens to work.
The closest I can get to your solution is something like this:
rails generate model TestModel non_nullable,null:string
I couldn't work out what comes after the , but that should give you a start
You can open editor by utilising https://github.com/rails/rails/pull/38870 (available for Rails versions > 6.1.0)
To create migration with null: false from command line, first you need to enable EDITOR_FOR_GENERATOR
# config/application.rb
# https://github.com/rails/rails/pull/38870#issuecomment-609018444
config.generators.after_generate do |files|
if ENV["EDITOR_FOR_GENERATOR"]
files.each do |file|
system("#{ENV["EDITOR_FOR_GENERATOR"]} #{file}")
end
end
end
Than use sed to append to specific columns.
For example that you want to create a model with jti and exp columns with not null constrains and add index to them (index is supported on command line using :index).
We need to match line t.string :jti and append to it so end result is t.string :jti, null: false
Here is command I use:
# rails g model CreateJwtDenylist jti:index exp:datetime:index
# replace jti and exp with your column names
EDITOR_FOR_GENERATOR='sed -i "" -r -e "/^[[:space:]]*t.*(jti|exp)$/ s/$/, null: false/"' rails g model CreateJwtDenylist jti:index exp:datetime:index
This works both for rails g migration and rails g model.
Resulting migration is
# db/migrate/20230121091319_create_jwt_denylist.rb
class CreateJwtDenylist < ActiveRecord::Migration[7.0]
def change
create_table :jwt_denylists do |t|
t.string :jti, null: false
t.datetime :exp, null: false
t.timestamps
end
add_index :jwt_denylists, :jti
add_index :jwt_denylists, :exp
end
end
You can do it in your model class like this-
class TestModel < ActiveRecord::Base
validates_presence_of :non_nullable
end

Rails 3 ActiveRecord Table Name Issue

I'm on Windows XP...
Ruby 1.9.2
Rails 3.0.9
MS SQL Server 2005
I'm using tiny_tds + activerecord-sqlserver-adapter
In my database I have table named t4.
I have created a model like this: rails generate model t4
Here is my simple model t4.rb:
class T4 < ActiveRecord::Base
end
Here is migration file 20111013125957_create_t4s.rb:
class CreateT4s < ActiveRecord::Migration
def self.up
create_table :t4s do |t|
t.timestamps
end
end
def self.down
drop_table :t4s
end
end
I have schema.rb:
...
create_table "t4", :force => true do |t|
t.string "title", :limit => 50
end
...
Problem:
When I try T4.select("title").where(["id = 3"]) I get error message:
ActiveRecord::StatementInvalid: TinyTds::Error: Invalid object name 't4s'.: SELECT title FROM [t4s] WHERE (id = 3)
P.S.:
I have another some tables named Adddocs and Eclaims.
There are not problems with queries to them.
I guess that problem is that T4.select("title").where(["id = 3"]) maps to SELECT title FROM [t4s] WHERE (id = 3) (T4 to t4s).
Why? I don't know
But when I edited file config/initializers/inflections.rb like this:
ActiveSupport::Inflector.inflections do |inflect|
inflect.irregular 't4', 't4'
end
Everything works! But it's not suitable solution (bad tone i think) :(((
By convention, if you create a model named album, rails will create a table called albums.
To override this (which isn't usually done) and use a custom defined table name you can use set_table_name method like this in your model:
class Album < ActiveRecord::Base
self.table_name="album" * define here whatever table name you have
end
When you create a new model it is the singular of the object your storing, whereas when it then generates the table, it uses the plural as there are multiple stored within the table. Example...
If you create a model named user, you'll end up with app/models/user.rb but the table it makes will be called users.
If you don't like that you have a few options...
You can override the inflections as you've done, however you should only do this if Rails is getting the pluralization wrong, rather than forcing it to use a certain word.
You can override the table name using the set_table_name method in the model file, providing it with a parameter of what your table name actually is.
You could disable the whole pluralized table names with config.active_record.pluralize_table_names = false in your config/application.rb file, however I strongly recommend you don't do this.
Ultimately I would suggest using more descriptive table names which can be pluralized over ones that can't. T4 as a model and table name don't explain to me what's stored within, where as Post does.

Resources