class CommentPolicy < ApplicationPolicy
def permitted_attributes
%i[body]
end
def permitted_attributes_for_create
%i[body application_id]
end
end
class CommentController
def create
@comment = Comment.create(comment_attributes) # { body: 'body', application_id: 1 }
end
def update
@comment = Comment.find(params[:id])
@comment.update(comment_attributes) # { body: 'new body' }
end
private def comment_attributes
permitted_attributes(Comment)
end
end
// ./.eslintrc.json
{
"ignorePatterns": [
"vendor/bundle/*"
]
}
# ./.rubocop.yml
AllCops:
Exclude:
- 'vendor/**/*'
$ ruby --version
3.0.1
$ rails --version
Rails 6.1.3.1
$ rails new MyApp --skip-bundle --skip-coffee --database=postgresql
$ cd MyApp
# Add Hotwire-rails to Gemfile
$ echo "\ngem 'hotwire-rails'" >> Gemfile
$ bundle
$ ./bin/rails webpacker:install
# lets keep javascript and stylesheets in a app/frontend folder
$ mv app/javascript app/frontend
# add bootstrap dependencies
$ yarn add bootstrap@next
# when prompted, choose the latest version here:
$ yarn add @popperjs/core@next
# Inside webpacker.yml
default: &default
source_path: app/frontend # Change here
$ mkdir app/frontend/js && touch app/frontend/js/bootstrap_js_files.js
$ touch app/frontend/packs/application.scss
# Warning, this removes application.css (which we replace with application.scss)
$ touch app/assets/stylesheets/.keep && rm app/assets/stylesheets/application.css
// inside app/frontend/js/bootstrap_js_files.js
// enable plugins as you require them
// import 'bootstrap/js/src/alert'
// import 'bootstrap/js/src/button'
// import 'bootstrap/js/src/carousel'
import 'bootstrap/js/src/collapse'
import 'bootstrap/js/src/dropdown'
// import 'bootstrap/js/src/modal'
// import 'bootstrap/js/src/popover'
import 'bootstrap/js/src/scrollspy'
// import 'bootstrap/js/src/tab'
// import 'bootstrap/js/src/toast'
// import 'bootstrap/js/src/tooltip'
// inside app/frontend/packs/application.js
// Add this line before import Rails …
import '../js/bootstrap_js_files.js'
// inside app/frontend/packs/application.scss
// Import Bootstrap v5
@import "~bootstrap/scss/bootstrap";
# create ./bin/bundle; currently required for the Remove Turbolinks task by Stimulus # see: https://github.com/hotwired/stimulus-rails/issues/55 $ bundle binstubs bundler $ ./bin/rails hotwire:install
<!DOCTYPE html>
<html>
<head>
<title>MyApp</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<!-- Warning !! ensure that "stylesheet_pack_tag" is used, line below; also change turbolinks to turbo for hotwire -->
<%= stylesheet_pack_tag 'application', media: 'all', 'data-turbo-track': 'reload' %>
<%= javascript_pack_tag 'application', 'data-turbo-track': 'reload' %>
</head>
<body>
<%= yield %>
</body>
</html>
$ ./bin/rails assets:clobber
# You can probably ignore the warnings :)
$ ./bin/rails webpacker:compile
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
config.public_file_server.headers = {
'Cache-Control' => 'public, max-age=31536000',
# You need to enable this (or set it to your domain) for fonts to work
'Access-Control-Allow-Origin' => '*'
}
if ENV['ASSET_HOST'].present?
config.action_controller.asset_host = ENV['ASSET_HOST'] # 'https://cdn.example.com'
end
dom_id(Post.find(12)) # => "post_23"
dom_id(Post.new) # => "new_post"
dom_id(Post.find(23), :edit) # => "edit_post_23"
dom_id(Post.new, :custom) # => "custom_post"
$ heroku run 'bundle exec rails console -- --nomultiline'
This query shows how to use the PostgreSQL UPDATE join syntax to update data in a table based on values in another table. for example;
updating subscription_missions.giftbox_type with rewards.gift_box_type through mission_rewards table (tested on 300k records, crazy level fast)
UPDATE
subscription_missions sm
SET
giftbox_type = r.gift_box_type
FROM
mission_rewards mr,
rewards r
WHERE
sm.giftbox_type IS NULL
AND sm.id = mr.subscription_mission_id
AND mr.reward_id = r.id;
$ RUBYOPT='-W0' rails runner 'Rails.application.eager_load!; puts ApplicationRecord.descendants.collect { |type| type.name }'
NOTE: RUBYOPT=-W0 avoids verbosity when running rails command which suppresses warnnings.
Using postgres functions inside a where clause can make a query non-sargable.
Database Structure
tracks has_many artists
Non-Sargable Query
Using a LOWER function prevents DBMS engine from using indexes.
Track.joins(:artists).where('LOWER(tracks.display_name) LIKE ?', "eric clapton%").explain
Gather (cost=1000.85..56714.32 width=4061)
Workers Planned: 2
-> Nested Loop (cost=0.85..55713.62 rows=3 width=4061)
-> Nested Loop (cost=0.42..55708.37 rows=3 width=4069)
-> Parallel Seq Scan on tracks (cost=0.00..55365.17 rows=41 width=4061)
Filter: (lower((display_name)::text) ~~* 'eric clapton%'::text)
-> Index Scan using index_artist_relations_on_artist_item_type_and_artist_item_id on artist_relations (cost=0.42..8.36 rows=1 width=16)
Index Cond: (((artist_item_type)::text = 'Track'::text) AND (artist_item_id = tracks.id))
-> Index Only Scan using idx_35952_primary on artists (cost=0.43..1.75 rows=1 width=8)
Index Cond: (id = artist_relations.artist_id)
Sargable Query
Removing LOWER function allows DBMS engine to use indexes, resulting in faster execution.
Track.joins(:artists).where('tracks.display_name ILIKE ?', "eric clapton%").explain
Nested Loop (cost=1497.60..2695.43 width=4061)
-> Nested Loop (cost=1497.17..2684.94 rows=6 width=4069)
-> Bitmap Heap Scan on tracks (cost=1496.75..1873.05 rows=97 width=4061)
Recheck Cond: ((display_name)::text ~~* 'eric clapton%'::text)
-> Bitmap Index Scan on index_tracks_on_display_name (cost=0.00..1496.73 rows=97 width=0)
Index Cond: ((display_name)::text ~~* 'eric clapton%'::text)
-> Index Scan using index_artist_relations_on_artist_item_type_and_artist_item_id on artist_relations (cost=0.42..8.36 rows=1 width=16)
Index Cond: (((artist_item_type)::text = 'Track'::text) AND (artist_item_id = tracks.id))
-> Index Only Scan using idx_35952_primary on artists (cost=0.43..1.75 rows=1 width=8)
Index Cond: (id = artist_relations.artist_id)
Go to your project root and cd into app/models (using CLI obviously) then run wc -lw * | sort -u
$ wc -lw * | sort -u
103 186 deal_broker.rb
116 313 physical_sim_report.rb
126 229 coupon_code.rb
126 260 subscription_mission.rb
129 284 payment.rb
145 290 deal_value.rb
147 255 single_use_coupon.rb
166 344 sim_card.rb
166 406 coupon_campaign.rb
225 451 order_summary.rb
260 534 deal.rb
308 783 package.rb
443 1031 user.rb
728 1744 subscription.rb
7262 15841 total
So now you can see Rocky is all about User ,Subscription and Package.
Note: First column number of lines and the second column number of words.
Just learned about tagged logging in rails. Did you know you can tag a log by using
logger.tagged('Your Tag') { logger.info('Message') }
You can also do it globally with
# config/application.rb
config.log_tags = [:method_on_request_object, lambda { |request| request.method.modifier_method }]
Add client Device OS and OS Version information to logs (requires clients to send X-OS and X-OS-Version headers)
# config/application.rb
config.log_tags = [ :request_id, lambda { |request| "#{request.headers['HTTP_X_OS']} #{request.headers['HTTP_X_OS_VERSION']}" } ]
# app/models/user.rb
has_one :four_car_garage
has_many :cars
validates :cars, length: { maximum: 4 }
When you want to write destructive migrations please use if_exists: true when you’re trying to remove a table and check for a table existence when you want to add or remove a column ActiveRecord::Base.connection.table_exists?
Case 1:
def up
drop_table :kittens, if_exists: true
end
Case 2:
def up
return unless ActiveRecord::Base.connection.table_exists?(:kittens)
add_column :kittens, :kind, :integer
end
If you want to override previously set order (even through default_scope), use reorder() instead.
E.g.
User.order('id ASC').reorder('name DESC')
would ignore ordering by id completely
If you want to attach a file you generated on disk or downloaded from a user-submitted URL, you can do it like this.
@model.image.attach(
io: File.open('/path/to/file'),
filename: 'file.pdf',
# content type is optional
content_type: 'application/pdf'
)
The Rails 5 attributes API allows us to build form objects very easily.
I had previously been using an after_initialize callback to downcase emails, with the following code I can define an :email date type and values are automatically cast when set.
This has the advantage of casting subsequent calls to set the email value.
class EmailType < ActiveModel::Type::String
def cast(value)
return super unless value.is_a?(String)
super(value.downcase)
end
end
ActiveModel::Type.register(:email, EmailType)
class Foo
include ActiveModel::Model
include ActiveModel::Attributes
attribute :email, :email
end
> Foo.new(email: 'EMAIL@MAIL.com').email
=> 'email@mail.com'
# app/models/user.rb
...
enum role: { user: 0, author: 1, admin: 2, robot: 3 }
scope :human, -> { user.or(author) }
scope :terminator, -> { admin.or(robot) }
Produces
User.human.to_sql
#=> "SELECT \"users\".* FROM \"users\" WHERE (\"users\".\"role\" = 0 OR \"users\".\"role\" = 1)"
I've known for a long time that you can submit values with submit buttons so you can track which button was used to submit the form e.g.
language-htmlHowever, today I needed to actually submit the form to a completely differnt action. Turns out you can do this with formaction attributes
language-htmlDid you know you can show all TODOs/OPTIMIZE/FIXME in your rails app with rails notes?
$ rails notes
app/helpers/users_helper.rb:
* [10] [TODO] Use ActiveSupport extension methods for Date/Time
app/services/slack.rb:
* [20] [OPTIMIZE] Replace library with core Net::HTTP
You can even focus with notes:todo etc.:
$ rails notes:todo
app/helpers/users_helper.rb:
* [10] [TODO] Use ActiveSupport extension methods for Date/Time
New method allowing you to create a Hash from an Enumerable:
%w(driver owner drivy).index_with(nil)
# => { 'driver' => nil, 'owner' => nil, 'drivy' => nil }
%w(driver owner drivy).index_with { |role| delta_amount_for(role) }
# => { 'driver' => '...', 'owner' => '...', 'drivy' => '...' }
To make your collection JSON schemas more reilable use minItems and maxItems so that you can trust your API.
{
"type": "array",
"minItems": 1,
"items": {
"$ref": "subscription_notification.json"
}
}
instead of
{
"type": "array",
"items": {
"$ref": "subscription_notification.json"
}
}
If your API returns [] empty array that last one would pass if you make an assertion in your specs.
expect(response).to match_response_schema(:subscription_notifications)
But not the first one
The property '#/' did not contain a minimum number of items 1 in schema
https://json-schema.org/understanding-json-schema/reference/array.html