When redirecting use
flash[:notice] = "This message value is available in next request-response cycle"
When rendering use
flash.now[:notice] = "Message is available in same request-response cycle"
Info from here
must put an empty option if want to use html options, e.g. class.
# class won't work.
<%= form.select:status, User.statuses.keys.to_a, class:'form-control col'%>
# works
<%= form.select:status, User.statuses.keys.to_a, {},class:'form-control col'%>
Rails7有很大的改动。其中影响最大的一个就是Hotwire
config/application.rb
中改为config.load_defaults 7.0
才会采用Rails7的默认值。
可以把webpacker相关的内容全部删除了,新的东西可以在rails7中生成一个新的project,相应的拷贝过来。
app/javascripts
只需要application.js,删除其他所有。增加controller目录。注意该目录下需要application.js,否则js报错不能正常工作,我卡在这里好久。
Gemfile中
# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
gem "importmap-rails"
# remove webpacker
gem 'webpacker', '~> 4.0'
Gemfile中
# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
gem "turbo-rails"
# remove
# gem 'turbolinks', '~> 5'
# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
gem "stimulus-rails"
需要的改动
+ 查询所有turbolinks地方,替换为turbo。如把所有 HTML 的 data 属性的data-turbolinks更新为data-turbo。turbolinks:load -> turbo:load
+ link_to data: :delete|post
-> data: {turbo_method: :delete|post}
+ confirm message: data: { confirm: 'Are you sure?' }
-> data:{turbo_confirm: 'Are you sure?'}
. Together: data: {turbo_method: :delete, turbo_confirm: 'Are you sure?'},
+ method works for button_to
# link_to works
<%= link_to t('destroy'), offered_path(@offered), data: {turbo_method: :delete, turbo_confirm: 'Are you sure??'} %>
# not working, still post method.
<%= button_to t('destroy'), offered_path(@offered), data: {turbo_method: :delete, turbo_confirm: 'Are you sure??'} %>
# works
<%= button_to t('destroy'), offered_path(@offered), method: :delete, data: {turbo_confirm: 'Are you sure??'} %>
# confirm not work
<%= button_to t('destroy'), offered_path(@offered), method: :delete, confirm: 'Are you sure??' %>
ruby
# not work
<%= link_to t("download"), link, data: { turbo_confirm: 'sure'} %>
# works, 注意,如果设置了data-turbo=false,则不work
<%= link_to t("download"), link, data: { turbo_method: :get, turbo_confirm:'sure?'}%>
# works
<%= button_to t("download"), link, data: { turbo_method: :get, turbo_confirm: 'sure?'} %>
+ controller中render需要增加status,否则有Form responses must redirect to another location
错误 例如
format.html { render :signup } =>
format.html { render :signup, status: :unprocessable_entity}
+ 出错后render自身页面,需要增加redirect_to
format.html { flash[:error] = "ERROR" } =>
format.html { flash[:error] = "ERROR"; redirect_to({action: :login})}
The "default" rails way to do things that I usually see is:
submit a form
redirect on success
render html on error
As such, since Turbo currently requires redirects on all forms, it's a major blocker to prevent folks switching over.
app/assets/config/manifset.js
用于配置需要编译的静态内容。典型配置如
/* Sprockets config, static resources to be compiled */
//= link_tree ../images
//= link_directory ../stylesheets .css
//= link_tree ../../javascript .js
//= link_tree ../../../vendor/javascript .js
迁移项目到Rails7,遇到一下问题:
undefined method `javascript_pack_tag'
<%= javascript_pack_tag 'application', 'data-turbo-track': 'reload' %>
根源: javascript_pack_tag is a Webpacker-specific method, so unless you're using Webpacker, you can't use it.
Rails中webpacker被 importmapped Hotwire取代
正确的做法是
<%= javascript_importmap_tags %>
Rails开发正常,部署之后403错误,Nginx配置之前用过,没有问题啊。
折腾半天,原来是语法错误,丢失了分号。
server_name server1.com www.server1.com # 没分号不行
server_name server1.com www.server1.com; # 有分号就好了
When I started a new project in Rails 7 and copy code from previous project, I encountered the several problems. For example, for login page, if password is wrong, the page doesn't show errors. For signup, similarly, it doesn't show any error messages.
I found JS console error
responses must redirect to another location
Then I found the articles 1 & 2
Here is why:
Turbo
replaced Turbolinks
. format.html { render :signup } # same page
format.html { flash[:error] = "ERROR" } # same page, no redirect
There are two solutions:
format.html { render :signup, status: :unprocessable_entity}
format.html { flash[:error] = "ERROR"; redirect_to({action: :login})} # redirect to itself.
turbo_frame_tag
app/javascripts/packs/application.js
not workingstylesheets_pack_tag
in views/layout/application.html, but no effect, no <link>
generated. why? Seems to be a Rails6.1.3/webpacker5.0 bugextract_css=false
setting in config/webpacker.yml
, so all css are embed in the HTML. In production, the css is extracted, put in a separated file public/packs/css/applicationxxx.css
. However, it's not included in html, so not work. assets/stylesheets/application.scss
@import 'select2/dist/css/select2.min';
instead of @import 'select2/dist/css/select2.min.css';
. The later cases works in development, but the file is empty in production. I guess because Rails production doesn't know the local directory of node_modules. The former case works, as the css is copied to application.css, instead of a separated file. /* in application.scss */
@import "./github_markdown.css";
->
@import "github_markdown";
# in Gemfile
gem 'font_awesome5_rails'
# in application.scss
@import 'font_awesome5_webfont';
/home/eda/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/net-ssh-6.1.0/lib/net/ssh/authentication/ed25519_loader.rb:21:in `raiseUnlessLoaded': OpenSSH keys only supported if ED25519 is available (NotImplementedError)
net-ssh requires the following gems for ed25519 support:
* ed25519 (>= 1.2, < 2.0)
* bcrypt_pbkdf (>= 1.0, < 2.0)
See https://github.com/net-ssh/net-ssh/issues/565 for more information
Gem::MissingSpecError : "Could not find 'ed25519' (~> 1.2) among 175 total gem(s)
Checked in 'GEM_PATH=/home/eda/.local/share/gem/ruby/3.0.0:/home/eda/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0' , execute `gem env` for more information"
ssh-add ~/.ssh/id_rsa.pub
# error
Could not open a connection to your authentication agent.
eval `ssh-agent -s`
ssh-add
s = s.replace(/[^\x00-\x7F]/g, "")
add a JS function
input
event is HTML5, feels like onchange without the need to lose focus on the element. It's not supported by IE8 and below, which should use onpropertychange
. onkeydown oncut onpaste
require('./parameterize.js')
$(document).on("turbolinks:load", function() {
$('form').on('input', '.parameterize-to', function(event) {
console.log("parameterize value updated")
let tid = $(this).attr('dst-id');
console.log("DEBUG:: The dst ID is: %s!",tid)
let content= $(this).val().trim()
console.log("DEBUG:: The form value is: %s!",content)
let slug = parameterize(content)
console.log("DEBUG:: Set value of %s to is: %s!",tid,content)
$("#"+tid).val(slug);
});
});
Apply it in views. Set class and ID properly. It works with input, cut & paste in Chrome
There should be no modifications to the content you specify as part of the internationalization process. It sounds like something is calling humanize on the string before it is output. Some of the standard Rails form helper methods do this I believe. If you just output the translation using t('email') you should see 'E-Mail' correctly.
Update: From your comments it seems like it is a label that is causing the problem. If you explicitly specify the text for the label rather than relying on the default behaviour you will get the translation exactly as you specify. So,
<%= f.label(:email, t('email')) %>
should generate the correct label from the translations.
yarn add clipboard.js
app/javascripts/packs/application.js
var ClipboardJS = require('clipboard')
$(document).on("turbolinks:load", function() {
console.log("DEBUG:: turbolinks.load!")
var clipboard = new ClipboardJS('.btn');
console.log(clipboard);
});
gem 'clipboard-rails'
/app/assets/javascripts/application.js
//= require clipboard
$(document).ready(function(){
var clipboard = new Clipboard('.clipboard-btn');
console.log(clipboard);
});
Usage
<!-- Target -->
<textarea id="bar">Mussum ipsum cacilds...</textarea>
<!-- Trigger -->
<button class="btn clipboard-btn" data-clipboard-action="copy" data-clipboard-target="#bar">
Cut to clipboard
</button>
<button class="btn clipboard-btn" data-clipboard-text="foo bar hello">
Cut to clipboard
</button>
guide
rubyzip: zip dir recurisively
def download
if params[:id] # Folder ID
folder = current_user.folders.find(params[:id])
title = folder.name
eids = [folder.id] | folder.descendants.map{|f| f.id}
docs = current_user.documents.where(folder_id: eids);
else
title = current_user.name
docs = current_user.documents;
end
title = title.downcase.gsub(/\s+/, '_');
# Tmp folder to store the download files
tmpd = "tmp/download/#{current_user.slug}"
# Create a tmp folder if not exists
FileUtils.rm_rf([tmpd,"#{tmpd}.zip"]);
FileUtils.mkdir_p(tmpd);
# Download and save documents to our tmp folder
docs.each do |doc|
file = doc.file; folder = doc.folder;
dir = folder ? "#{tmpd}/#{folder.full_path}" : tmpd
FileUtils.mkdir_p(dir) unless Dir.exists?(dir)
fname = file.filename.to_s
if File.exist?("#{dir}/#{fname}") # incase two file with same name
fname = "DUP_ID#{doc.id}_#{fname}"
end
# User should be able to download files if not yet removed from tmp folder
# if the folder is already there, we'd get an error
download_doc_to_dir(file, dir, fname)
end
#---------- Convert to .zip --------------------------------------- #
zg = ZipFileGenerator.new(tmpd,"#{tmpd}.zip")
zg.write
# Sends the *.zip file to be download to the client's browser
send_file(Rails.root.join("#{tmpd}.zip"), :type => 'application/zip', :filename => "#{title}.zip", :disposition => 'attachment')
# TODO: Remove dir but leave zip file
FileUtils.rm_rf(tmpd)
end
def download_doc_to_dir(doc, tmpd, fname)
File.open(File.join(tmpd, fname), 'wb') do |file|
doc.download { |chunk| file.write(chunk) }
end
end
enable multiple
<% if false %>
<-- Not work: InvalidSignature -->
<%= file_field_tag('files[]',class: "form-control", multiple: true, id: :files) %>
<% end %>
<%= form.file_field :files, class: "form-control", multiple: true %>
@folder = Folder.find(params[:folder_id]) if params[:folder_id].present?
(params[:files]||[]).each do |f|
ps = {note: params[:note], file: f, user_id: current_user.id, folder: @folder}
@document = Document.create(ps)
end
options = Carmake.all.unshift Carmake.new(id: 0, name: 'Any')
collection_select(:service, :carmake_id, options, :id, :name, include_blank: 'Any')
class Customer
def descendants
self.children | self.children.map(&:descendants).flatten
end
end
|
is array union.
[ "a", "b", "c" ] | [ "c", "d", "a" ] #=> [ "a", "b", "c", "d" ]
[ "c", "d", "a" ] | [ "a", "b", "c" ] #=> [ "c", "d", "a", "b" ]
Setup
rails active_storage:install
rails db:migrate
# in config/storage.yml, setup cloud if you want, I just want to use local first
# in environments/development.rb[production.rb], set
config.active_storage.service = :local
# need public:true for local? Not work, error. It seems always public.
Add avatar to User
models/user.rb
has_one_attached :avatar
views
<%= f.file_field :avatar, class: "form-control" %>
controllers
# add permit to params
params.permit(:name, :password, :password_confirmation, :email, :avatar)
# remove avatar if necessary
if @user.avatar.attached? && uparams[:avatar].present?
@user.avatar.purge_later
end
<%= image_tag @user.avatar, style: 'width:20%;height:auto' if @user.avatar.attached? %>
storage
directory, e.g. storage/ky/b0/kyb05r52e6l8kr6l4pfytggq6lks
config/deploy.rb
, add storage to linked_dir
FileCenter
For Godday, you need to add a text record with
@
_googlewhich worked. But this time I have to set it to
@. Otherwise, Google can't find the record
google-site-verification=xxxx
Installation
gem 'sitemap_generator'
config/sitemap.rb
bundle install
rake sitemap:install
config/sitemap.rb
rails -s sitemap:refresh
, which will ping search engine by default. Setup crontab to refresh daily
Add the link to robot.txt
Sitemap: https://www.example.com/sitemap.xml.gz
Add the link in Google search console
crypto-js
yarn add crypto-js
javascript/packs/application.js
var CryptoJS = require("crypto-js");
$(document).on("turbolinks:load", function() {
console.log("DEBUG:: turbolinks.load!")
var encrypted = CryptoJS.AES.encrypt("加密Message", "Secret Passphrase");
var decrypted = CryptoJS.AES.decrypt(encrypted, "Secret Passphrase");
var plaintext = decrypted.toString(CryptoJS.enc.Utf8);
$("#demo1").text(encrypted)
$("#demo2").text(decrypted)
$("#demo3").text(plaintext)
});
<br>
<label>encrypted</label>
<div id="demo1"></div>
<br>
<label>decrypted</label>
<div id="demo2"></div>
<br>
<label>Actual Message</label>
<div id="demo3"></div>
It's not working (development OK, production empty)
Rails.application.secrets.secret_key_base
Starting from Rails 5.2 there is no more secrets.yml file and the right way to get the env variables saved in credential.yml.enc
is as follows:
Rails.application.credentials.dig(:secret_key_base)
input = '<strong>feelings</strong>'
result = ReverseMarkdown.convert input
result.inspect # " **feelings** "
html = 'How to convert <b>HTML</b> to <i>Markdown</i> on <a href="http://stackoverflow.com">Stack Overflow</a>.'
document = Kramdown::Document.new(html, :html_to_native => true)
document.to_kramdown
# "How to convert **HTML** to *Markdown* on [Stack Overflow][1].\n\n\n\n[1]: http://stackoverflow.com\n"
The result is :
How to convert HTML to Markdown on Stack Overflow.
add CNAME api -> @
or * -> @
sudo certonly certonly -a webroot --webroot-path=/var/www/<your site>/current/public -d www.example.com -d example.com -d api.example.com
certonly
. With the webroot plugin, you probably want to use the "certonly" command, eg:
*.example.com, requires a plugin.
Client with the currently selected authenticator does not support any combination of challenges that will satisfy the CA. You may need to use an authenticator plugin that can do challenges over DNS.
/var/www/<your site>
not work for rails project.A typical Sidekiq config file is like
:verbose: false
:concurrency: 4
:strict: false
:logfile: log/sidekiq.log
:pidfile: tmp/pids/sidekiq.pid
:queues:
- default
- mailers
development:
:verbose: true
:concurrency: 2
:logfile: log/sidekiq_dev.log
:pidfile: tmp/pids/sidekiq_dev.pid
However, the logfile and pidfile do not work with version 6.2.0. That's because the changes on 6.0.
My current workaround:
pid=`ps aux | grep sidekiq | grep -v grep | awk '{print $2}'`;
kill $pid
cd /var/www/$proj/current/
bundle exec sidekiq -e production -C config/sidekiq.yml >& log/sidekiq.log &
What is the best way to generate PDF/HTML/DOCX in Ruby/Rails
PDF:
DOC:
Prawn Notes: guide
Add links:
inline_format: works
pdf.text "<link href='#{link}'>#{table.name}</link>", align: :center, size: 32, style: :bold, inline_format: true
formatted_text: works, but can't align by center automatically, has to move_down for new text
pdf.formatted_text_box([{text: table.name, link: link, styles: [:bold], size: 32, align: :center }])
link_annotation: has to put position?
pdf.link_annotation([100, 100, 5, 5], :Border => [0,0,1], :A => { :Type => :Action, :S => :URI, :URI => Prawn::LiteralString.new("http://google.com") } )
support Chinese
gkai00mp.ttf is not downloaded by bundle install
prawn
gem is of version 2.4.0, may be dated without all fonts gem 'prawn', :git => 'https://github.com/prawnpdf/prawn.git'
Specify fonts:
pdf.font("#{Prawn::BASEDIR}/data/fonts/gkai00mp.ttf") do
pdf.text '测试中文'
pdf.text 'test English'
pdf.text 'test English 和中文'
# NOTE: gkai doesn't support style like italic
end
fallback fonts for both languages
Prawn::Document.generate(file) do |pdf|
kai_path = "#{Prawn::BASEDIR}/data/fonts/gkai00mp.ttf"
kai_spec = { file: kai_path, font: 'Kai' }
#don't use a symbol to define the name of the font!
pdf.font_families['Kai'] = { normal: kai_spec,
bold: kai_spec,
italic: kai_spec,
bold_italic: kai_spec};
pdf.fallback_fonts(["Kai"]);
link = Rails.application.routes.url_helpers.table_records_path(table)
pdf.text "<link href='#{link}'>#{table.name}</link>", align: :center, size: 32, style: :bold, inline_format: true
pdf.text table.user.name, align: :center, size: 16 , style: :italic
head,rows = self.get_head_rows(table,records)
pdf.font("Kai") # Set default font as "Kai". Otherwise, my have error.
rows.each do |row|
pdf.start_new_page
head.zip(row).each do |name,value|
pdf.text "\n"+name.to_s+":"
pdf.text "#{value}" # Not work without using "Kai"
#pdf.text "测试中文English" # works without using "Kai"
end
end
end
Caracal Notes: guide
link & center
docx.h1 do
link(table.name, tlink)
align :center
end
What is the difference between include and extend in Ruby?
Tag
name:string counter:integer
tag:references table:references record_id:integer
Model
has_many: tags
to table.rb, make sure record_id is nil for table tags. Define tag_list
and tag_list=
methods tags, tag_list, tag_list=
function to record.rb# models/table.rb
has_many :taggings
# keep tagging order
has_many :tags, -> {Tagging.where(record_id: nil).order(id: :ASC)}, through: :taggings attr_accessor :tag_list
def tag_list
tags.map(&:name)
end
# Array or String, e.g. "tag1, tag2, tag3"
def tag_list=(names)
tag_names = [];
if names.present?
names = names.split(',') if names.instance_of?(String)
tag_names = names.collect(&:strip).reject(&:blank?)
end
self.tags = tag_names.map do |n|
Tag.where(name: n.strip).first_or_create!
end
end
# models/record.rb
def tags
Tag.joins(:taggings).where({taggings: {table: self.table, record_id: self.id}).order("taggings.id": :ASC)
end
def tag_list
self.tags.map(&:name)
end
def tag_list=(names)
tag_names = []
if names.present?
names = names.split(',') if names.instance_of?(String)
tag_names = names.collect(&:strip).reject(&:blank?)
end
tags = tag_names.map do |n|
Tag.where(name: n).first_or_create!
end
# Create or update new tags
taggings = tags.map do |tag|
Tagging.where(tag:tag,table:self.table,record_id:self.id).first_or_create!
end
# Delete tags not used anymore
tids = taggings.pluck(:id);
Tagging.where(table:self.table,record_id:self.id).where.not(id: tids).delete_all
end
Controller & Views
tag_list
filed to table form. Such that use can input tag list as tag1, tag2 ...
Tag Cloud
Follow guide
Define tag_count in models
class Tag < ApplicationRecord
has_many :taggings
has_many :tables, through: :taggings
validates_presence_of :name
def to_param
return self.name || self.id
end
def self.tag_counts
Tag.select("tags.id, tags.name,count(taggings.tag_id) as count").joins(:taggings).group("taggings.tag_id, tags.id, tags.name")
end
end
define tag_cloud in helper/application.rb
def tag_cloud(tags, classes)
max = tags.sort_by(&:count).last
tags.each do |tag|
index = tag.count.to_f / Integer(max.count) * (classes.size - 1)
yield(tag, classes[index.round])
end
end
Define css in javascript/stylesheets/application.scss
.tag-cloud-1 { font-size: 1.0em;}
.tag-cloud-2 { font-size: 1.2em;}
.tag-cloud-3 { font-size: 1.4em;}
.tag-cloud-4 { font-size: 1.6em;}
.tag-cloud-5 { font-size: 1.8em;}
.tag-cloud-6 { font-size: 2.0em;}
show in the views
<div class="tags-cloud glassy-bg">
<% tag_cloud Tag.tag_counts, %w{tag-cloud-1 tag-cloud-2 tag-cloud-3 tag-cloud-4 tag-cloud-5 tag-cloud-6} do |tag, css_class| %>
<%= link_to "#{tag.name}(#{tag.count})", tag_path(tag.name), class: css_class %>
<% end %>
</div>
Load More
tag_controller
def show
@limit = params[:limit] || 20;
@section=params[:section] # operation, also section ID
@record = params[:record];
cond = {}
cond[:id] = (0..@record.to_i-1) if @record
if @section.blank? || @section=="table"
@tables =@tag.tables.where(cond).order(id: :DESC).limit(@limit)
end
if @section.blank? || @section=="record"
@taggings = Tagging.where(tag: @tag).where.not(record_id: nil).where(cond).order(id: :DESC).limit(@limit)
@records = @taggings.map {|r| Table.find(r.table_id).find_record(r.record_id)};
end
respond_to do |format|
format.html
format.js
end
end
show.js.erb
<% if @section.present? %>
<% if @section=="table"%>
<% if @tables.empty? %>
$('#<%=@section%> .load-more-button').hide()
<% else %>
$('#<%=@section%> .load-more-records').append('<%= escape_javascript(render(partial: "tables/table_card",collection: @tables)) %>')
<% end %>
<% elsif @section=="record" %>
<% if @records.empty? %>
$('#<%=@section%> .load-more-button').hide()
<% else %>
$('#<%=@section%> .load-more-records').append('<%= escape_javascript(render("tags/tag_records", taggings: @taggings, records: @records)) %>')
<% end %>
<% end %>
<% else %>
// Nothing, reload
<% end %>
views.
Note that we using tagging ID instead of record ID for record load more.
<% tabs = [:table, :record] %>
<ul class="nav nav-tabs nav-pills" role="tablist">
<% tabs.each_with_index do |opt,idx| %>
<% cls = idx==0 ? "active" : "" %>
<% select = (idx==0) %>
<li class="nav-item">
<a class="nav-link <%=cls%>" id="<%=opt%>-tab" href="#<%=opt%>" data-toggle="tab" role="tab" aria-controls="<%=opt%>" aria-selected="<%= select %>"><%=t("models.#{opt.to_s.capitalize}")%></a>
</li>
<% end %>
</ul>
<div class="tab-content mt-2">
<% tabs.each_with_index do |opt,idx| %>
<% cls = (idx==0) ? "active" : "" %>
<div class="tab-pane fade show <%=cls%>" id="<%=opt%>" role="tabpanel" aria-labelledby="<%=opt%>-tab">
<div class="load-more-records">
<% if(opt==:table) %>
<%= render partial: "tables/table_card", collection: @tables %>
<% else %>
<%= render "tags/tag_records", taggings: @taggings, records: @records %>
<% end %>
</div>
<div class="load-more-button text-center mt-2">
<%= image_tag "ajax-loader.gif", style: "display:none;", class: "loading-gif" %>
<%= link_to t('views.load_more'), "#", class: "load-more"%>
</div>
</div>
<% end %>
</div>
// _tag_records.html.erb
<% taggings.zip(records).each do |tagging,record| %>
<div class="record" id="tagging-<%=tagging.id%>" data-id="<%=tagging.id%>">
<%= render "records/record", record: record %>
</div>
<% end %>
AJAX auto-completion
Install select2
yarn add select2
yarn add select2-bootstrap-theme
Apply JS
// javascript/packs/application.js
import 'select2'
import 'select2/dist/css/select2.min.css'
import "select2-bootstrap-theme/dist/select2-bootstrap.min.css";
// javascript/packs/tag.js
import $ from 'jquery' // must import jquery
$(document).on("turbolinks:load", function() {
$('.tag-input').select2({
tags: true,
multiple: true,
theme: 'bootstrap',
//tokenSeperators: [','],
placeholder: 'Please enter your tags',
minimumInputLength: 2,
ajax: {
url: "/tags.json",
dataType: 'json',
data: function (params) {
return { term: params.term };
},
processResults: function (tags) {
console.log(tags)
var tlist = $.map(tags, function (tag) {
return { id: tag.name, text: tag.name }
});
console.log(tlist)
return {results: tlist}
}
}
});
});
Tag search
def index
cond = "";
if params[:term].present?
cond = "name LIKE '%#{params[:term]}%'"
end
@tags = Tag.where(cond).order(id: :DESC)
respond_to do |format|
format.html
format.json
end
end
Apply in views. Note, must use select_tag, text_tag not work
<%= form.select :tag_list, form.object.tag_list, {}, {multiple: true, class: "form-control tag-input"} %>
// OR
<% opt = options_for_select(@record.tag_list, @record.tag_list) %>
<%= select_tag :tag_list, opt, {multiple: true, class: "form-control tag-input"} %>
Issues
When submit form, blank value added automatically.
"tag_list"=>["", "adafa", "1111"]
Solution: add option to select include_hidden: false
. See here
<%= form.select :tag_list, form.object.tag_list, {include_hidden: false}, {multiple: true, class: "form-control tag-input"} %>
DO NOT do this. This will make update tag list to empty not working. As we don't know it's emptying or keeping the tag list.
UI is ugly is using bootstrap
style
Need to install select2-bootstrap-theme
guide
i18n
Failed. Seems i18n.js has syntax error. I updated the file and it passes. See here for details
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd){
Still can't work. language: $('html').attr('lang')
or language: 'zh-CN'
still not work.
References
I have bind a click events to <a>
like:
$(document).on("turbolinks:load", function() {
$('a.click-toggle-id').click(function (e) {
console.log("click-toggle-id clicked")
// prevent the default click action
e.preventDefault();
let tid = $(this).attr('toggle-id');
console.log(tid)
$("#"+tid).toggle();
});
});
This works well for static elements, but not for dynamically generated elements, e.g. elements added by js.erb files.
I found this guide. The problem is I should use on
instead of click
and use the top-level container document
.
$(document).on("turbolinks:load", function() {
// NOT WORK $('a').on('click','.click-toggle-id',function (e) {
$(document).on('click','a.click-toggle-id',function (e) {
console.log("click-toggle-id clicked")
// prevent the default click action
e.preventDefault();
let tid = $(this).attr('toggle-id');
console.log(tid)
$("#"+tid).toggle();
});
});
$( selector ).click( function()… )
$( selector ).on( 'click', function()… )
The problem with this method is that your event handlers will need to be attached each time your DOM is modified. JS updated your DOM, but the turbolinks:load not fired.
Delegated event handling:
$( document ).on( 'click', '.click_link', function()… )
When using jQuery on() with delegate events, the event is bound to the top level element, but when it runs, the children of the element are traversed looking for matches for the selector. This has the added benefit of making your code idempotent, and further, can be used in instances where you are modifying your DOM structure on the fly and have new elements created which should handle events.
The only downside (that I know of) to this approach is that event propagation may happen more slowly.This means if you stop event propagation (using event.preventDefault() or event.stopPropagation()), it may not occur in time to prevent all the events from firing that you want. This doesn't cause problems for links to '#' which are handled with event.preventDefault(), but it may cause issues in other cases where you have multiple event handlers on one element.
render(options = {}, locals = {}, &block)
The primary options are:
:partial - See ActionView::PartialRenderer.
:file - Renders an explicit template file (this used to be the old default), add :locals to pass in those.
:inline - Renders an inline template similar to how it's done in the controller.
:plain - Renders the text passed in out. Setting the content type as text/plain.
:html - Renders the HTML safe string passed in out, otherwise performs HTML escape on the string first. Setting the content type as text/html.
:body - Renders the text passed in, and inherits the content type of text/plain from ActionDispatch::Response object.
Specify layout
<%= render partial: "link_area", layout: "graybar" %>
Block
# users/index.html.erb
<%= render "shared/search_filters", search: @q do |form| %>
<p>
Name contains: <%= form.text_field :name_contains %>
</p>
<% end %>
#
# shared/_search_filters.html.erb
<%= form_with model: search do |form| %>
<h1>Search form:</h1>
<fieldset>
<%= yield form %>
</fieldset>
<p>
<%= form.submit "Search" %>
</p>
<% end %>
<%= render partial: "form", locals: {zone: @zone} %>
:as
and :object
options
By default, PartialRenderer uses the template name for the local name of the object passed into the template. These examples are effectively the same:
<%= render :partial => "contract", :locals => { :contract => @contract } %>
<%= render :partial => "contract" %>
By specifying the :as option we can change the way the local variable is named in the template. These examples are effectively the same:
<%= render :partial => "contract", :as => :agreement %>
<%= render :partial => "contract", :locals => { :agreement => @contract } %>
The :object option can be used to directly specify which object is rendered into the partial.
<%= render :partial => "contract", :object => @buyer %>
<%= render :partial => "contract", :locals => { :contact => @buyer } %>
The :object and :as options can be used together. We might have a partial which we have named genericly, such as ‘form’. Using :object and :as together helps us.
<%= render :partial => "form", :object => @contract, :as => :contract %>
<%= render :partial => "form", :locals => {:contract => @contract} %>
Collection
<%= render :partial => "ad", :collection => @ads %>
Also, you can specify a partial which will be render as a spacer between each element by passing partial name to :spacer_template.
<%= render :partial => "ad", :collection => @ads, :spacer_template => "ad_divider" %>
Rendering the default case
Instead of explicitly naming the location of a partial, you can also let the RecordIdentifier do the work if you’re following its conventions for RecordIdentifier#partial_path.
# @account is an Account instance, so it uses the RecordIdentifier to replace
# <%= render :partial => "accounts/account", :locals => { :account => @account} %>
<%= render :partial => @account %>
# @posts is an array of Post instances, so it uses the RecordIdentifier to replace
# <%= render :partial => "posts/post", :collection => @posts %>
<%= render :partial => @posts %>
If you’re not going to be using any of the options like collections or layouts, you can also use the short-hand defaults of render to render partials. Examples:
# Instead of <%= render :partial => "account" %>
<%= render "account" %>
# Instead of <%= render :partial => "account", :locals => { :account => @buyer } %>
<%= render "account", :account => @buyer %>
# @account is an Account instance, so it uses the RecordIdentifier to replace
# <%= render :partial => "accounts/account", :locals => { :account => @account } %>
<%= render(@account) %>
# @posts is an array of Post instances, so it uses the RecordIdentifier to replace
# <%= render :partial => "posts/post", :collection => @posts %>
<%= render(@posts) %>
Question: some place says render
will not accept additional local variables for the partial. But it seems I can pass multiple variables for render. New feature (Rails 6)?
<%= render 'record', table: @table, record: @record %>
Comment
user:references, table:references, record_id:integer parent:references rely:references content:text
has_many :children
and has_many: replies
through parent/reply_id. cascade_reply_ids
which find all comment IDs that need to be deleted. Code
1. use data-turbolinks=false
to make anchor works without reloading pages
2. use js.erb
file to make operating comments AJAX without reloading pages
# db/migrate/20210227120803_create_comments.rb
class CreateComments < ActiveRecord::Migration[6.0]
def change
create_table :comments do |t|
t.references :user, null: false, foreign_key: true
t.references :table, null: false, foreign_key: true
t.integer :record_id, null: true
# For nested comment, parent links to the top-level comment, reply links to its reply
# parent comment
t.references :parent, null: true, foreign_key: {to_table: "comments"}
# reply comment
t.references :reply, null: true, foreign_key: {to_table: "comments"}
t.text :content, null: false, default: ""
t.timestamps
end
add_index :comments, [:table_id, :record_id]
add_index :comments, [:table_id, :record_id, :parent_id]
end
end
# app/models/comment.rb
class Comment < ApplicationRecord
default_scope { order('id DESC') }
belongs_to :user
belongs_to :table
belongs_to :parent, class_name: "Comment", optional: true
belongs_to :reply, class_name: "Comment", optional: true
has_many :children, class_name: 'Comment', foreign_key: :parent_id, dependent: :destroy
has_many :replies, class_name: 'Comment', foreign_key: :reply_id, dependent: :destroy
# delete_all has better performance if many records. InvalidForeignKey error
#has_many :children, class_name: 'Comment', foreign_key: :parent_id, dependent: :delete_all
#has_many :replies, class_name: 'Comment', foreign_key: :reply_id, dependent: :delete_all
validates_presence_of :content
def cascade_reply_ids
# top-level comment
return self.children.pluck(:id) unless self.parent_id?
# non-top-level comment
ids = self.replies.pluck(:id); nids=ids.clone
while nids.any?
id = nids.pop
nn = Comment.where(table: self.table, record_id: self.record_id, parent_id:self.parent_id, reply_id: id).pluck(:id)
ids += nn;
nids.unshift(*nn)
end
return ids;
end
end
# app/views/comments/_comment_section.html.erb
<div class="comment-container mt-5 small">
<% if logged_in? %>
<div class="new-comment" id="new-reply">
<%= render "comments/new_comment", comment: comment %>
</div>
<% end %>
<div class="comments" id="children">
<%= render comments %>
</div>
</div>
# app/views/comments/_comment.html.erb
<% comment_id = "comment-#{comment.id}" %>
<% reply_container_id = "reply-container-#{comment.id}" %>
<% new_reply_id = "new-reply-#{comment.id}" %>
<% children_id = "children-#{comment.id}" %>
<div class="card mx-4 my-1" id="<%=comment_id%>">
<div class="card-body">
<div class="card-title">
<h6><%= link_to comment.user.name, f_user_path(comment.user) %></h6>
</div>
<% if (comment.parent_id && comment.reply_id && (comment.parent_id!=comment.reply_id)) %>
<div class='markdown-body'>
<% rc = "> @#{comment.reply.user.name} \n >> #{comment.reply.content.truncate(100)}" %>
<a href="#comment-<%=comment.reply.id%>" data-turbolinks="false">
<%= markdown(rc) %>
</a>
</div>
<% end %>
<div class='markdown-body'>
<%= markdown(comment.content)%>
</div>
<% tstyle = fmt_sh_style(comment.children.any?) %>
<% cid = "children-num-#{comment.id}" %>
<% cnum = comment.children.count %>
<% tstr = t_cat([("<div id='#{cid}' style='#{tstyle}'>"+cnum.to_s+"</div>"),t('reply'),">"]).html_safe %>
<div class="d-flex flex-row justify-content-between mt-2">
<div>
<%= link_to tstr, "javascript:void(0)", class: "click-toggle-id", "toggle-id": reply_container_id%>
<%= fmt_time(comment.created_at) %>
</div>
<% if logged_in? %>
<% if current_user==comment.user || admin? %>
<%= link_to fa_destroy_icon, comment_path(comment), class: "#{lh_btn_cls} btn-sm", method: :delete, remote: true, data: { confirm: t('destroy_confirm') } %>
<% end %>
<% end %>
</div>
<div class="comment-reply-container" id="<%=reply_container_id%>" style="<%=fmt_hide_style%>">
<div class="new-comment" id="<%=new_reply_id%>">
<% if logged_in? %>
<%= render "comments/new_comment", comment: Comment.new(table: comment.table, record_id: comment.record_id, parent_id: comment.parent_id || comment.id, reply_id: comment.id) %>
<% end %>
</div>
<div class="comment-children" id="<%=children_id%>" %>
<%= render comment.children if comment.children.any? %>
</div>
</div>
</div>
</div>
# app/views/comments/_new_comment.html.erb
<%= form_with(model: comment, remote: true) do |form| %>
<% if comment.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(comment.errors.count, "error") %> prohibited this comment from being saved:</h2>
<ul>
<% comment.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field form-group row justify-content-between">
<%= form.hidden_field :table_id %>
<%= form.hidden_field :record_id %>
<%= form.hidden_field :parent_id %>
<%= form.hidden_field :reply_id %>
<div class="col-md-10 col-xs-12">
<%= form.text_area :content, rows: 1, class:"form-control", placeholder: t("comment"), required: true %>
</div>
<div class="col-md-2 col-xs-12 text-center">
<%= form.submit t("submit"), class: "btn btn-secondary form-control"%>
</div>
</div>
<% end %>
# app/views/comments/create.js.erb
<% if @comment.parent_id.present? %>
// Clear form
$('.comment-container #new-reply-<%=@comment.reply_id%> form')[0].reset()
<% if @comment.reply_id!=@comment.parent_id %>
$('.comment-container #reply-container-<%=@comment.reply_id%>').hide()
<% end %>
// Add comment
$('.comment-container #children-<%=@comment.parent_id%>').prepend('<%= escape_javascript(render(@comment)) %>')
// Update children number
$('.comment-container #children-num-<%=@comment.parent_id%>').attr('style','display: inline-block;')
<% cnum = @comment.parent.children.count %>
$('.comment-container #children-num-<%=@comment.parent_id%>').text(<%=cnum%>)
<% else %>
$('.comment-container #new-reply form')[0].reset()
$('.comment-container #children').prepend('<%= escape_javascript(render(@comment)) %>')
<% end %>
# app/views/comments/destroy.js.erb
// Remove comments
$('.comment-container #comment-<%=@comment.id%>').remove()
<% if @comment.parent_id.present? %>
// Remove comments's replies
<% @reply_ids.each do |id| %>
$('.comment-container #comment-<%=id%>').remove()
<% end %>
// Update children number
<% cnum = @comment.parent.children.count %>
// Set reply num
$('.comment-container #children-num-<%=@comment.parent_id%>').text(<%=cnum%>)
// Hide if no reply
<% if cnum <=0 %>
$('.comment-container #children-num-<%=@comment.parent_id%>').attr('style','display: none;')
<% end %>
<% end %>
ActiveJob provides an uniform interface for declaring and run backedn jobs. Note that it is just a wrapper, it relies on background processing. If no backend is set, the job is immediately executed. The backend is set like:
# Set in config/application.rb
# Be sure to have the adapter's gem in your Gemfile and follow
# the adapter's specific installation and deployment instructions.
config.active_job.queue_adapter = :sidekiq
Commonly used backends include delayed job, sidekiq, Resque. For comparison, please check Delayed Job vs Resque vs Sidekiq . delayed_job is easier to use, sidekiq is faster but replies on Redis.
We use Sidekiq as the backend. It has good document. Here is my configuration:
# 1. Configuration
# install redis
sudo apt-get install redis-server # 3.0.6
sudo service redis-server restart
# install sidekiq in Gemfile
gem 'sidekiq'
bundle install # 5.2.5
# config active job in config/application.rb
config.active_job.queue_adapter = :sidekiq # if not set, all jobs will perform immediately.
# Remember to start the sidekiq process
# use "RAILS_ENV=production" or "-e production/development" to specify environment.
bundle exec sidekiq # connect to localhost:6379/0 by default.
# 2. Jobs, example.
# create job app/jobs/notify_iftt.rb
class NotifyIftttJob < ApplicationJob
queue_as :default
include IftttHelper
def perform(tids)
# Do something later notify_ifttt(tids) if tids
end
end
# call it
tids = tids.map{|tid| tid.tid}
NotifyIftttJob.perform_later(tids) if tids # preform_now does not go to backend.
It works as:
It is very important to start the Sidekiq process. Otherwise, the jobs will stay in the Redis unprocessed.
# use RAILS_ENV or -e to specify environment
RAILS_ENV=production bundle exec sidekiq -e production bundle exec sidekiq -e production
# use -C to specify configuration.
bundle exec sidekiq -e production -C config/sidekiq.xml
# config/sidekiq.yml
:verbose: false
:concurrency: 25
:strict: false
:logfile: log/sidekiq.log :pidfile: tmp/pids/sidekiq.pid development:
:verbose: true
:concurrency: 15
:logfile: log/sidekiq_dev.log :pidfile: tmp/pids/sidekiq_dev.pid
production:
It is also important to process in correct Rails environment, otherwise, error ocurrs when processing it. For example, the job is stored by production Rails, but got by Sidekiq working on development Rails. So it is better to use different database for different environments or code bases. By default, Redis provides 16 databases (0-15). Race condition is problematic to have two sidekiq/rails shares the same database.
# config/initializers/sidekiq.rb
# Redis by default have 0-15 database. Must config both server and client.
Sidekiq.configure_server do |config|
if Rails.env.development?
config.redis = { url: 'redis://localhost:6379/15'}
else
config.redis = { url: 'redis://localhost:6379/0'}
end
end
Sidekiq.configure_client do |config|
if Rails.env.development?
config.redis = { url: 'redis://localhost:6379/15'}
else
config.redis = { url: 'redis://localhost:6379/0'}
end
end
Sidekiq provide a web interface.
# Add in config/routes.rb
require 'sidekiq/web'
require 'admin_constraint' # require admin for access
mount Sidekiq::Web => '/sys/sidekiq', :constraints => AdminConstraint.new
# lib/admin_constraint.rb
class AdminConstraint
def matches?(request)
return false unless request.session[:user_id]
user = User.find request.session[:user_id]
user && user.ADMIN?
end
end
I want to show friend status and action in the home page. The friendship status includes:
The action includes
I list all states and actions in the home page, and show the state only according to current friendship status
<% h = [:waiting, :declined].include?(status.to_sym) ? {inviter_id: user.id} : {invitee_id: user.id} %>
<% btn_params = {params: h, remote: true, class: "#{lh_btn_cls} btn-outline-primary btn-sm"} %>
<div class='fsm' id='fsm-stranger' style='<%=status.to_sym==:stranger ? fmt_show_style : fmt_hide_style%>' >
<div class="d-inline-block">
<%= fa_friends_icon %> <%= t('models.friendship.statuses.stranger') %>
</div>
<div class="d-inline-block">
<%= button_to t('models.friendship.operations.request'), activity_request_friendship_path(format: :json), btn_params.merge({form: {class: "fsm-switch", "curr-state": "fsm-stranger", "next-state": "fsm-pending"}}) %>
</div>
</div>
<div class='fsm' id='fsm-pending' style='<%=status.to_sym==:pending ? fmt_show_style : fmt_hide_style%>' >
<div class="d-inline-block">
<%= fa_friends_icon %> <%= t('models.friendship.statuses.pending') %>
</div>
<div class="d-inline-block">
<%= button_to t('models.friendship.operations.withdraw'), activity_withdraw_friendship_path(format: :json), btn_params.merge({form: {class: "fsm-switch", "curr-state": "fsm-pending", "next-state": "fsm-stranger"}}) %>
</div>
</div>
<div class='fsm' id='fsm-friend' style='<%=status.to_sym==:friend ? fmt_show_style : fmt_hide_style%>' >
<div class="d-inline-block">
<%= fa_friends_icon %> <%= t('models.friendship.statuses.friend') %>
</div>
<div class="d-inline-block">
<%= button_to t('models.friendship.operations.unfriend'), activity_unfriend_path(format: :json), btn_params.merge({form: {class: "fsm-switch", "curr-state": "fsm-friend", "next-state": "fsm-stranger"}}) %>
</div>
</div>
<div class='fsm' id='fsm-rejected' style='<%=status.to_sym==:rejected ? fmt_show_style : fmt_hide_style%>' >
<%= fa_friends_icon %> <%= t('models.friendship.statuses.rejected') %>
</div>
<div class='fsm' id='fsm-waiting' style='<%=status.to_sym==:waiting ? fmt_show_style : fmt_hide_style%>' >
<div class="d-inline-block">
<%= fa_friends_icon %> <%= t('models.friendship.statuses.waiting') %>
</div>
<div class="d-inline-block">
<%= button_to t('models.friendship.operations.accept'), activity_accept_friendship_path(format: :json), btn_params.merge({form: {class: "fsm-switch", "curr-state": "fsm-waiting", "next-state": "fsm-friend"}}) %>
</div>
<div class="d-inline-block">
<%= button_to t('models.friendship.operations.reject'), activity_reject_friendship_path(format: :json), btn_params.merge({form: {class: "fsm-switch", "curr-state": "fsm-waiting", "next-state": "fsm-declined"}}) %>
</div>
</div>
<div class='fsm' id='fsm-declined' style='<%=status.to_sym==:declined ? fmt_show_style : fmt_hide_style%>' >
<div class="d-inline-block">
<%= fa_friends_icon %> <%= t('models.friendship.statuses.declined') %>
</div>
<div class="d-inline-block">
<%= button_to t('models.friendship.operations.accept'), activity_accept_friendship_path(format: :json), btn_params.merge({form: {class: "fsm-switch", "curr-state": "fsm-declined", "next-state": "fsm-friend"}}) %>
</div>
</div>
The Javascript code:
// button-to with fsm-switch. After success, show ID specified by "next-state"
// finite-state-machine switch
$(document).on("turbolinks:load", function() {
$('.fsm-switch').on('ajax:success', function (e) {
console.log("FSM switch after AJAX success")
let cid = $(this).attr('curr-state');
let nid = $(this).attr('next-state');
console.log(cid);
console.log(nid);
$("#"+cid).toggle()
$("#"+nid).toggle()
});
});
Usages
// Tooltips
$(document).on("turbolinks:load", function() {
$('[data-toggle="tooltip"]').tooltip();
});
data-toggle="tooltip" data-placement="left" title="Tooltip on left"
// Or define a helper
def fmt_tooltip(title,position="top")
{'data-toggle': 'tooltip', 'data-placement': position, title: title}
end
I want to improve it by using multiple tabs in the same page.
improve Javascript to pass both record ID and section ID
// Improvement: multiple in the same page. The HTML layout must be
// <div id="unique_id">
// <div class="container">
// <div class="record" date-id=object_id>
// </div>
// <div class="record" date-id=object_id>
// </div>
// </div>
// <div class="load-more-container">
// <img class="loading-gif"></img>
// <a class="load-more">load more </a>
// </div>
// </div>
$(document).on("turbolinks:load", function() {
// when the load more link is clicked
$('a.load-more').click(function (e) {
console.log("Load-more clicked")
// prevent the default click action
e.preventDefault();
// hide load more link
$(this).siblings('.load-more').hide();
// show loading gif
$(this).siblings('.loading-gif').show();
// Section
var section = $(this).parent().parent()
var section = section.attr('id')
console.log(`Section ID: ${section}`)
// Container
var cobj = $(this).parent().siblings(".container") // container object
// get the last id and save it in a variable 'last-id'
var last_id = cobj.find('.record').last().attr('data-id');
console.log(`Last ID: ${last_id}`)
// make an ajax call passing along our last user id
$.ajax({
// make a get request to the server
type: "GET",
// get the url from the href attribute of our link
url: $(this).attr('href'),
// send the last id to our rails app
data: {
record: last_id,
section: section
},
// the response will be a script
dataType: "script",
// upon success
success: function (data,status,xhr) {
console.log("AJAX done")
// hide the loading gif. TODO: how about mutliple?
$('.loading-gif').hide();
// show our load more link
$('.load-more').show();
}
});
});
});
Use Record ID and Section ID in controller to get new records
@limit = params[:limit] || 20;
@record = params[:record]; @section=params[:section] # operation, also section ID
cond = {user: current_user};
cond[:id] = (0..@record.to_i-1) if @record
if @section
# List
cond[:operation] = @section
@activities = Activity.where(cond).limit(@limit)
else
# Hash of activities
@activities = {}
Activity.operations.each do |opt, value|
cond[:operation] = opt
@activities[opt] = Activity.where(cond).limit(@limit)
end
end
respond_to do |format|
format.html
format.js
end
Append result to specified section in js.erb
<% if @activities.empty? %>
$('#<%=@section%> .load-more-container').hide()
<% else %>
$('#<%=@section%> .container').append('<%= escape_javascript(render(partial: "home/activity",collection: @activities)) %>')
<% end %>
Set multiple tabs by using Bootstrap
<ul class="nav nav-tabs nav-pills" role="tablist">
<% Activity.operations.each_with_index do |opt,idx| %>
<% opt = opt[0]%>
<% cls = idx==0 ? "active" : "" %>
<% select = (idx==0) %>
<li class="nav-item">
<a class="nav-link <%=cls%>" id="<%=opt%>-tab" href="#<%=opt%>" data-toggle="tab" role="tab" aria-controls="<%=opt%>" aria-selected="<%= select %>"><%=t("models.activity.operations.#{opt}")%></a>
</li>
<% end %>
</ul>
<div class="tab-content mt-2">
<% Activity.operations.each_with_index do |opt,idx| %>
<% opt = opt[0] %>
<% cls = (idx==0) ? "active" : "" %>
<div class="tab-pane fade show <%=cls%>" id="<%=opt%>" role="tabpanel" aria-labelledby="<%=opt%>-tab">
<div class="container row">
<%= render partial: "home/activity", collection: @activities[opt] %>
</div>
<div class="load-more-container text-center">
<%= image_tag "ajax-loader.gif", style: "display:none;", class: "loading-gif" %>
<%= link_to t('views.load_more'), "#", class: "load-more"%>
</div>
</div>
<% end %>
</div>
I want to show user activity history which could be huge in AJAX way.
Basically I follow this guide, with improvement from this guide
<div class="container">
<%= render partial: "home/activity", collection: @activities %>
</div>
<div class="load-more-container">
<%= image_tag "ajax-loader.gif", style: "display:none;", class: "loading-gif" %>
<%= link_to "Load More", "#", class: "load-more"%>
</div>
Please note
1. the partial is under views/home/
instead of views/activities
by default. So we need to specify the path, instead of using <%= render @activities%>
directly.
2. don't add remote: true
in the link_to
tag, otherwise, it will fetch twice, one with id, the other one without, leading to duplicate result.
3. download the ajax-loader.gif to assets/images
default_scope { order('id DESC') }
I use ID instead of created_at because we do not have index for created_id. Usually, the ID order should be same with created_at, with some corner case exceptions.
<div class="record" data-id="<%= activity.id %>">
<%= activity.id %> | <%= activity.operation %> | <%= activity.table.name %> | <%= activity.record_id %> | <%= activity.updated_at %>
</div>
Please not that you must specify the class
and data-id
attribute, which are use by the Javascript below.
$(document).on("turbolinks:load", function() {
// when the load more link is clicked
$('a.load-more').click(function (e) {
console.log("Load-more clicked")
// prevent the default click action
e.preventDefault();
// hide load more link
$('.load-more').hide();
// show loading gif
$('.loading-gif').show();
// get the last id and save it in a variable 'last-id'
var last_id = $('.record').last().attr('data-id');
console.log(`Last ID: ${last_id}`)
// make an ajax call passing along our last user id
$.ajax({
// make a get request to the server
type: "GET",
// get the url from the href attribute of our link
url: $(this).attr('href'),
// send the last id to our rails app
data: {
id: last_id
},
// the response will be a script
dataType: "script",
// upon success
success: function () {
// hide the loading gif
$('.loading-gif').hide();
// show our load more link
$('.load-more').show();
}
});
});
});
def activities
if params[:id]
@activities = Activity.where(user: current_user, id: (0..params[:id].to_i-1)).limit(5)
else
@activities = Activity.where(user: current_user).limit(5)
end
respond_to do |format|
format.html
format.js
end
end
Please note that the class variable solution here does not work, reloading the page will not reset the class variable, leading to empty page.
<% if @activities.empty? %>
$('.load-more-container').hide()
<% else %>
$('.container').append('<%= escape_javascript(render(partial: "home/activity",collection: @activities)) %>')
<% end %>
Please note that we use the solution here to disable the link once all loaded. It depends on the class container
and load-more-container
set in views/home/activities.html.erb
loop method
<% users.each do |user| %>
<%= render user %>
<% end %>
render collection directly
<= render @users %>
view in other directory
<= render (partial: 'home/user', collection: @users) %>
behavior layer) from a Web page's structure/content and presentation)
data-*
属性关联local: false
| remote: true
-> data-remote='true'
data-url , data-params, data-method, data-type
data: {confirm: "Are your sure?"}
-> data-confirm
data: {disable_with: "Saving..."}
-> data-disable-with
Rails-ujs event handlers
event
. In this parameter, there is an additional attribute detail
which contains an array of extra parameters.document.body.addEventListener("ajax:success", (event) => {
const [data, status, xhr] = event.detail;
});
Event name | Extra parameters (event.detail) | Fired |
---|---|---|
ajax:before | Before the whole ajax business. | |
ajax:beforeSend | [xhr, options] | Before the request is sent. |
ajax:send | [xhr] | When the request is sent. |
ajax:stopped | When the request is stopped. | |
ajax:success | [response, status, xhr] | After completion, if the response was a success. |
ajax:error | [response, status, xhr] | After completion, if the response was an error. |
ajax:complete | [xhr, status] | After the request has been completed, no matter the outcome. |
falseattribute to the tag
<a href="..." data-turbolinks="false">No turbolinks here</a>.
load
-> turbolinks:load
document.addEventListener("turbolinks:load", () => {
alert("page has loaded!");
});
Server side
// foo.js.erb
var users = document.querySelector("#users");
users.insertAdjacentHTML("beforeend", "<%= j render(@user) %>");
// JS style
$.get( "/vegetables", function(data) {
alert("Vegetables are good for you!");
});
// Rails style
Rails.ajax({
url: "/books",
type: "get",
data: "",
success: function(data) {},
error: function(data) {}
})
Server side
// Javascript view example:
Rails.$('.random-number')[0].innerHTML = ("<%= j (render partial: 'random') %>")
// jQuery example:
$('.random-number').html("<%= j (render partial: 'random') %>")
// `j` is an alias for `escape_javascript`
# controller
render json: { html: render_to_string(partial: 'random') }
# client JS
Rails.ajax({
url: "/books",
type: "get",
success: function(data) { Rails.$(".random-number")[0].innerHTML = data.html; }
})
Hide Load More buttons when all items have been rendered in Ruby on Rails
Failed to get data in JS, found this question
For AJAX returns, jQuery returns events likes
$(document).ready ->
$("#new_message").on("ajax:success", (e, data, status, xhr) ->
console.log(xhr);
).on "ajax:error", (e, xhr, status, error) ->
console.log(xhr);
However, as shown in the Rails Guide, Rails-ujs returns only one parameter event
with all other in detail
attribute. So the code should be like
document.body.addEventListener("ajax:success", (event) => {
const [data, status, xhr] = event.detail;
});
class CreateFriendships < ActiveRecord::Migration[6.0]
def change
create_table :friendships do |t|
t.references :inviter, null: false, foreign_key: {to_table: "users"}
t.references :invitee, null: false, foreign_key: {to_table: "users"}
t.integer :status, null: false, default: 0
t.timestamps
end
add_index :friendships, :status
end
end
# app/models/friendships
class Friendship < ApplicationRecord
belongs_to :invitee, class_name: "User"
belongs_to :inviter, class_name: "User"
enum status: {pending: 0, accepted: 1}, _suffix: true, _prefix: :is
end
# app/models/users
has_many :friendships_as_invitee, class_name: "Friendship", foreign_key: :invitee_id
has_many :friendships_as_inviter, class_name: "Friendship", foreign_key: :inviter_id
def friends
ids1 = friendships_as_invitee.where(status: :accepted).pluck(:inviter_id)
ids2 = friendships_as_inviter.where(status: :accepted).pluck(:invitee_id)
User.where(id: ids1+ids2)
end
def all_friends
ids1 = friendships_as_invitee.pluck(:inviter_id)
ids2 = friendships_as_inviter.pluck(:invitee_id)
User.where(id: ids1+ids2)
end
def pending_friends
ids1 = friendships_as_invitee.where(status: :pending).pluck(:inviter_id)
ids2 = friendships_as_inviter.where(status: :pending).pluck(:invitee_id)
User.where(id: ids1+ids2)
end
def pending_inviters
ids = friendships_as_invitee.where(status: :pending).pluck(:inviter_id)
User.where(id: ids)
end
def pending_invitees
ids = friendships_as_inviter.where(status: :pending).pluck(:invitee_id)
User.where(id: ids)
end
<script src="https://localhost/assets/xmlhr.debug-1284139606.js"></script>
<script src="/packs/js/application-be5eb61178f8249f882f.js" data-turbolinks-track="reload"></script>
<link>
for CSS<script>
for JS<%= form_with(url: upload_table_records_path(@table), method: :POST, local: true) do |form| %>
<div class="form-group row justify-content-center">
<%= form.file_field :file, class: 'form-control col-6' %>
</div>
<div class="actions text-center">
<%= submit_tag t("upload"), class: "btn btn-primary"%>
</div>
<% end %>
f = params[:file]; contents = f.read;
fpath = f.path; # same with f.tempfile.path
File.open(fpath, 'wb') do |file|
file.write(contents)
file.close
end
Record.from_csv(@table,fpath)
ActionDispatch::Http::UploadedFile
object/tmp/RackMultipart20210118-18052-19hic1b.csv
, which will be deleted when the file is closed. To use rouge, I include an erb file in application.css. This cause some problems in Rails console
rails console
failed with error cannot load such file -- rouge
; the second try works. Solution: Not work
#config/application.rb
config.assets.precompile += ["rouge"]
use Rails.application.credentials
.secret_key_base
check for details
There are two ways to access secret_key_base:
you can change Rails.application.credentials.secret_key_base by rails credentials:edit.
gem 'redcarpet'
gem 'rouge' // for themes
module MarkdownHelper
require 'redcarpet'
require 'rouge'
require 'rouge/plugins/redcarpet'
class HTML < Redcarpet::Render::HTML
include Rouge::Plugins::Redcarpet
end
def markdown(text)
render_options = {
# filer_html: true,
hard_wrap: true,
link_attributes: { rel: 'nofollow' },
prettify: true
}
#renderer = Redcarpet::Render::HTML.new(render_options)
renderer = HTML.new(render_options)
extras = {
no_intra_emphasis: true,
tables: true, // support table
fenced_code_blocks: true,
autolink: true,
disable_indented_code_blocks: true,
strikethrough: true,
lax_spacing: true,
space_after_headers: true,
superscript: true,
underline: true,
highlight: true,
quote: true,
footnotes: true,
}
markdown = Redcarpet::Markdown.new(renderer, extras)
raw markdown.render(text)
end
end
include MarkdownHelper
usage: markdown(text)
css (app/assets/stylesheets
)
*= require rouge
*= require github_markdown
<%= Rouge::Themes::Github.render(scope: '.highlight') %>
copy table, blockquote, code format from github.css
<div class="markdown-body"> // enable github_markdown.css
<%= markdown(v) %>
</div>
Issues:
Install ubuntu 18.04 LTS
Guide: default version is 10, we use the latest
sudo apt-get install wget ca-certificates
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" >> /etc/apt/sources.list.d/pgdg.list'
sudo apt-get install postgresql postgresql-contrib libpq-dev
sudo -u postgres createuser db_user -s
sudo -u postgres psql
postgres=# \password (db_user)
By default, psql use peer neection (login by linux user, no password).
To change it, modify /etc/postgresql/9.5/main/pg_hba.conf
from:
local all all peer
to:
local all all md5
then restart by
sudo service postgresql restart
Command to login:
psql -U db_user -d postgres # must specify db
Rails
sudo apt install curl
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get update
sudo apt-get install git-core zlib1g-dev build-essential libssl-dev libreadline-dev libyaml-dev libsqlite3-dev sqlite3 libxml2-dev libxslt1-dev libcurl4-openssl-dev software-properties-common libffi-dev nodejs yarn
cd
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
exec $SHELL
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
echo 'export PATH="$HOME/.rbenv/plugins/ruby-build/bin:$PATH"' >> ~/.bashrc
exec $SHELL
rbenv install 2.6.5
rbenv global 2.6.5
ruby -v
gem install bundler
gem install rails -v 6.0.0
rbenv rehash
rails -v
Rails app
rails new my_site -d postgresql
# configure db database.yml
# add domain
add config.hosts << "yourdomain.com" to config/environments/development.rb
# config generator in application.rb
config.generators do |g|
# Disable jbuilder as we use JR
# g.jbuilder false
## Disable empty files
# helper files
g.helper false
# assets files
g.assets false
end
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 561F9B9CAC40B2F7
sudo sh -c 'echo deb https://oss-binaries.phusionpassenger.com/apt/passenger bionic main > /etc/apt/sources.list.d/passenger.list'
sudo apt-get update
sudo apt-get install -y nginx-extras libnginx-mod-http-passenger
if [ ! -f /etc/nginx/modules-enabled/50-mod-http-passenger.conf ]; then sudo ln -s /usr/share/nginx/modules-available/mod-http-passenger.load /etc/nginx/modules-enabled/50-mod-http-passenger.conf ; fi
sudo ls /etc/nginx/conf.d/mod-http-passenger.conf
sudo vim /etc/nginx/conf.d/mod-http-passenger.conf
# We simply want to change the passenger_ruby line to match the following:
passenger_ruby /home/<deploy_user>/.rbenv/shims/ruby;
sudo service nginx start
Add the following gems to our Gemfile
gem 'capistrano', '~> 3.11'
gem 'capistrano-rails', '~> 1.4'
gem 'capistrano-passenger', '~> 0.2.0'
gem 'capistrano-rbenv', '~> 2.1', '>= 2.1.4'
Generate template
bundle
# use cap in rbenv, source ~/.bashrc if cap not found
cap install STAGES=production
Edit Capfile
Capfile:
require 'capistrano/rails'
require 'capistrano/passenger'
require 'capistrano/rbenv'
set :rbenv_type, :user
set :rbenv_ruby, '2.6.5'
deploy.rb
set :application, "my_site"
set :repo_url, "/home/web/codes/rails"
set :deploy_to, "/var/www/my_site"
append :linked_files, 'config/database.yml', 'config/master.key'
append :linked_dirs, 'log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', '.bundle', 'public/system', 'public/uploads'
set :keep_releases, 5
deploy/production.rb
server 'yourdomain.com', user: 'web', roles: %w{app db web}
Prepare
RAILS_ENV=production rails db:create # create db
sudo mkdir /var/www/my_site
sudo chown -R web:web /var/www/my_site
cp master.key database.yml to /varw/www/my_site/shared/config
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys # Otherwise, ask for password when deploy
Nginx config: /etc/nginx/site-available/default
listen 80;
listen [::]:80;
server_name my_site.com;
passenger_enabled on;
rails_env production;
# Because of incorrect path in /etc/nginx/conf.d/mod-http-passenger.conf, we have to set it again here. It's optional if it's right above.
#pssenger_ruby /home/web/.rbenv/shims/ruby;
root /var/www/my_site/current/public;
Make swap, otherwise, too slow.
cap production deploy
Install certbot
By snapd:
sudo snap install core; sudo snap refresh core
sudo apt-get remove certbot
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
By APT:
sudo apt-get install certbot python-certbot-nginx
sudo certbot --nginx (choose direct, disable http)
usage:
sudo certbot renew # for renew
sudo certbot renew --webroot-path /var/www/xxx # renew with webroot path
sudo certbot certonly -a webroot --webroot-path=/var/www/<your site>/current/public -d www.example.com -d example.com -d api.example.com. # multiple sub-domain
sudo certbot certificates # expiration day
sudo certbot delete --cert-name # remove unused domain
It seems the auto cron will renew the certification once per two months. But Ngnix should be restarted to load the new file.
Config generator in application.rb
config.generators.test_framework false
config.generators do |g|
# Disable jbuilder as we use JR
# g.jbuilder false
# Disable empty files
# helper files
g.helper false
# assets files
g.assets false
end
in config/application.rb
config.middleware.use Rack::Deflater
not working by https://gtmetrix.com/
compress js/css by Nginx
Leverage browser caching for Nginx
jQuery: follow guide
yarn add jquery
config/webpack/environment.js
const webpack = require('webpack')
environment.plugins.prepend('Provide',
new webpack.ProvidePlugin({
$: 'jquery/src/jquery',
jQuery: 'jquery/src/jquery'
})
)
app/javascript/packs/application.js
require('jquery')
webpack-dev-server
to make sure no errorsupport coffee files: follow guide
config/webpack/environment.js
config/webpacker.yml
config/webpack/loaders/coffee.js
rm app/javascript/packs/hello_coffee.coffee
app/javascript/coffee
require('../coffee/application.coffee')
Install
yarn add bootstrap popper.js jquery
In app/javascript/stylesheets/application.scss
@import 'bootstrap/scss/bootstrap';
In app/javascript/packs/application.js
import 'stylesheets/application' # include scss above
import 'bootstrap/dist/js/bootstrap';
On-the-fly build for dev:
bin/webpack-dev-server
rails g mailer admin_mailer send_passwd
Email configuration for sending emails
config.action_mailer.raise_delivery_errors = true
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
:address => "smtp.gmail.com",
# NOTE: Please use 587 not 465. Details: https://github.com/rails/rails/issues/27298
#:port => 465, # EOF error
# Remember to enable less secure apps, otherwise Net::SMTPAuthenticationError
:port => 587,
:domain => "my_site.com", # does it matter?
:user_name => "my_site@gmail.com",
:password => "my_password",
# Plain vs Login, encry username and password together or separately.
# Details: http://www.samlogic.net/articles/smtp-commands-reference-auth.htm
#:authentication => "plain",
:authentication => "login",
:enable_starttls_auto => true
}
Check your email, Gmail may still block your access. You may need to use server to login your gmail account, then unlock, following the guide
ssh -Y
in Mac to connect server, ssh -X not workzh.yml
file under lib/locale
(default config/locales)config in config/initializers/locale.rb
I18n.load_path += Dir[Rails.root.join('lib', 'locale', '*.{rb,yml}')]
I18n.available_locales = [:en, :zh]
I18n.default_locale = :zh
In application.rb
around_action :switch_locale
def switch_locale(&action)
locale = params[:locale] || I18n.default_locale
I18n.with_locale(locale, &action)
end
def default_url_options
{ locale: I18n.locale }
end
config/routes.rb
, put everything in
scope "/:locale" do
resources :books
end
Add js to head
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-159082166-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-159082166-1');
</script>
Add js to Rails, NOTE the hack for turbolinks
<!-- Global site tag (gtag.js) - Google Analytics -->
<% if Rails.env.production? %>
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-159082166-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-159082166-1');
// turbolinks
document.addEventListener('turbolinks:load', event => {
if (typeof gtag === 'function') {
gtag('config', 'UA-159082166-1', {
'page_location': event.data.url
});
}
});
</script>
<% end %>
gem "recaptcha"
Configure keys in config/initializers/recaptcha.rb
Recaptcha.configure do |config|
config.site_key = 'xxx'
config.secret_key = 'xxxx'
end
Add to views: <%= recaptcha_tags %>
Check in controller
skip_verify = request.format.json?
if (skip_verify || verify_recaptcha(model: @user)) && @user.save
// OK
else
// ERROR
end
jQuery date picker problem in Safari
Although there is no native datepicker for Safari (or IE) a pretty good workaround is to add a placeholder attribute to the date input. This informs Safari and IE users which format the fallback text input should be (which is yyyy-mm-dd). The placeholder doesn't display on browsers that support type=date
so this workaround won't affect other browsers.
e.g. <input type="date" placeholder="yyyy-mm-dd" />
Why Quill?
Question:
Problem:
Guide:
Install
min height
.ql-editor{
min-height: 200px;
}
installation
# Add this line to your application's Gemfile:
gem "chartkick" # This is not optional. Without it, function (e.g. line_chart) won't work even if js is ready.
# For Rails 6 / Webpacker, run:
yarn add chartkick chart.js
# And in app/javascript/packs/application.js, add:
require("chartkick")
require("chart.js")
chart library
Google chart
<%= javascript_include_tag "https://www.gstatic.com/charts/loader.js" %>
AIzaSyCo0AKH9n0-vu92BsU9TwUDKjpmi6c8HZU
Set it
<script type="text/javascript">
google.charts.load('current', {
'packages':['geochart'],
'mapsApiKey': 'AIzaSyCo0AKH9n0-vu92BsU9TwUDKjpmi6c8HZU'
});
</script>
NOTE: I tried to set it in config/initializer/chartkick.rb
Chartkick.options = {
language: "en",
mapsApiKey: "AIzaSyCo0AKH9n0-vu92BsU9TwUDKjpmi6c8HZU", // NOT WORK
height: "400px", // SEEMS WORK
colors: ["#b00", "#666"]
}
Chartkick.configure({language: "de", mapsApiKey: "..."}) // ERROR to load, no configure function.
link_to "Page", params.merge(:c => "e")
Error: Rails Unable to convert unpermitted parameters to hash
#If you know which parameters should be allowed in the link, you can call permit with those listed.
params.permit(:param_1, :param_2).merge(:sort => column, :direction => direction, :page => nil)
# OR
params.merge(:sort => column, :direction => direction, :page => nil).permit(:param_1, :param_2, :sort, :direction, :page)
# Risk solution
params.to_unsafe_h.merge