56
|
Rails Flash.now not working
|
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
|
|
55
|
Rails select with html options.
|
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'%>
|
|
54
|
Rails7 Hotwire
|
Rails7有很大的改动。其中影响最大的一个就是Hotwire
- webpacker被importmap取代
- Turbolinks被Turbo取代
- 使用Sprocket进行静态资源管理(和Hotwire无关)
config/application.rb 中改为config.load_defaults 7.0 才会采用Rails7的默认值。
importmap
可以把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'
Turbo
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"
需要的改动
Turbo Drive
+ 查询所有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??' %>
- turobo_confirm doesn't work with link_to alone, check https://stackoverflow.com/a/75046023
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?'} %>
Turbo Frame:
+ 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.
Turbo Steam: 目前没使用
Sprocket
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
|
|
53
|
Rails 7 undefined method `javascript_pack_tag'
|
迁移项目到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 %>
|
Rails7
webpacker
|
52
|
奇怪的Nginx 403错误
|
Rails开发正常,部署之后403错误,Nginx配置之前用过,没有问题啊。
折腾半天,原来是语法错误,丢失了分号。
server_name server1.com www.server1.com # 没分号不行
server_name server1.com www.server1.com; # 有分号就好了
|
|
51
|
Rails Turbo & "responses must redirect to another location"
|
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:
- In Rails 7,
Turbo replaced Turbolinks .
- Turbo requires each response must redirect to another location.
format.html { render :signup } # same page
format.html { flash[:error] = "ERROR" } # same page, no redirect
There are two solutions:
- same page, but different status
format.html { render :signup, status: :unprocessable_entity}
format.html { flash[:error] = "ERROR"; redirect_to({action: :login})} # redirect to itself.
|
|
50
|
Rails Webpacker & stylesheets
|
- I have an issue that select2 css is not included in production, although OK in development.
- Further, I found all css included in
app/javascripts/packs/application.js not working
- I do add
stylesheets_pack_tag in views/layout/application.html, but no effect, no <link> generated. why? Seems to be a Rails6.1.3/webpacker5.0 bug
- In development, according to the
extract_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.
- One solution is to update extract_css=false for production as well, this leads to embed css in HTML, which works but is ugly.
- I workaround the problem by moving all css from packs to
assets/stylesheets/application.scss
- To import css in node_modules, remember to use
@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.
|
|
49
|
Rails 6.0 deploy problems.
|
- css file not found.
/* in application.scss */
@import "./github_markdown.css";
->
@import "github_markdown";
- Font awesome icons not found. solution
# in Gemfile
gem 'font_awesome5_rails'
# in application.scss
@import 'font_awesome5_webfont';
|
|
48
|
deploy error
|
/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
|
|
47
|
Generate slug from name automatically
|
parameterize
- copy code from github
- but I don't want to exclude Chinese chars, based on this, I added
s = s.replace(/[^\x00-\x7F]/g, "")
add a JS function
- According to this The
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 .
- For non HTML5 solution, according to this, we need
onkeydown oncut onpaste
- onchange occurs only when you blur the textbox
- onkeyup & onkeypress doesn't always occur on text change
- onkeydown occurs on text change (but cannot track cut & paste with mouse click)
- onpaste & oncut occurs with keypress and even with the mouse right click.
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
|
|
46
|
Rails i18n strings auto-lowercased?
|
source
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.
|
|
45
|
Rails copy to clipboard with clipboard.js
|
clipboard.js
yarn add clipboard.js
- In
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);
});
clipboard-rails
- Install Gem
gem 'clipboard-rails'
- In
/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>
|
|
44
|
Loop through all, zip and download from ActiveStorage
|
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
|
|
43
|
Rails Upload Multiple Files
|
guide
<% 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
|
|
42
|
Rails collection_select set value for include_blank
|
source
options = Carmake.all.unshift Carmake.new(id: 0, name: 'Any')
collection_select(:service, :carmake_id, options, :id, :name, include_blank: 'Any')
|
|
41
|
Get all children of children and so on
|
source
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" ]
|
|
40
|
Rails ActiveStorage
|
Guide[Rails 6.0]
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
<%= f.file_field :avatar, class: "form-control" %>
# 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? %>
- Once a file uploaded, it's located in
storage directory, e.g. storage/ky/b0/kyb05r52e6l8kr6l4pfytggq6lks
- In
config/deploy.rb , add storage to linked_dir
FileCenter
- Folder Model: name, note, parent (for nested, could be null).
- Document Model: folder(null for root), note, has_one_attached:file
- Folder Controller & Views
- index: for showing root directory folders and documents,
- show: for showing specific directory folders and documents
- new: for new folder
- update: for rename
- destroy: for delete, destroy recursively by model.
- Document Controller & Views
- new: for upload file. Redirect to folder
- update: for rename note ? Redirect to folder
- destroy: delete. Redirect to folder
- Move document/folder to other folders
- A folder can't move to its children and so on
- A document can be moved to any folder
- Upload multiple files
- Download directory
- Upload directory: TODO
|
|
39
|
Google Search Console DNS setting
|
For Godday, you need to add a text record with
- type: TXT
- Name:
@
- I used to set as
_google which worked. But this time I have to set it to @ . Otherwise, Google can't find the record
- Value: copy the value from Google console, e.g.
google-site-verification=xxxx
|
|
38
|
Rails Sitemap
|
- Use the 'sitemap_generator' gem
Installation
- Add to Gemfile
gem 'sitemap_generator'
- Generate
config/sitemap.rb
bundle install
rake sitemap:install
- edit the
config/sitemap.rb
- specify the domain
- add links
- test by
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
|
|
37
|
Build a dropbox-like File Sharing Site with ROR
|
|
|
36
|
JavaScript string encryption and decryption?
|
Guide
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>
|
|
35
|
Raills secret
|
guide
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)
|
|
34
|
Convert HTML to Markdown
|
guide
- html2markdown: pretty old, don't know how to use
reverse_markdown
input = '<strong>feelings</strong>'
result = ReverseMarkdown.convert input
result.inspect # " **feelings** "
kramdown
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.
|
|
33
|
HTTPS on subdomain
|
- DNS setting:
add CNAME api -> @ or * -> @
- Expand certification to sub-domains
sudo certonly certonly -a webroot --webroot-path=/var/www/<your site>/current/public -d www.example.com -d example.com -d api.example.com
- Must use
certonly . With the webroot plugin, you probably want to use the "certonly" command, eg:
- Can't use
*.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.
- Must use the static directory,
/var/www/<your site> not work for rails project.
- references:
|
|
32
|
Sidekiq does not support logfile and pidfile since 6.0
|
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.
- Remove the daemonization
- Remove logfile
- Remove pidfile
Check here for discussion, why daemonization is bad, the pidfile problem
My current workaround:
- Logfile: redirect stdout to a file
- Pidfile: get from ps
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 &
|
sidekiq
|
31
|
Generate PDF/DOC
|
What is the best way to generate PDF/HTML/DOCX in Ruby/Rails
PDF:
- Prawn: native and more flexible
- Wicked-PDF: Wicked depends on wkhtmltopdf and uses systems call
- CombinePDF: combine PDF files
- pdfcrowd: API for http://pdfcrowd.com
DOC:
- Caracal
- docx
- docx_replace
- docx
- html2doc
Prawn Notes: guide
Add links:
- methods
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
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
Caracal Notes: guide
link & center
docx.h1 do
link(table.name, tlink)
align :center
end
|
|
30
|
Include vs Extend
|
What is the difference between include and extend in Ruby?
- include: mixes in specified module methods as instance methods in the target class
- extend: mixes in specified module methods as class methods in the target class
|
Rails
|
29
|
Rails Tag System
|
Tag
- DB
- tags:
name:string counter:integer
- taggings:
tag:references table:references record_id:integer
- I added a counter to tags for ranking. Not used yet.
- Special tags fixed to table or table records. Use polymorphic for tagging anything.
Model
- Add
has_many: tags to table.rb, make sure record_id is nil for table tags. Define tag_list and tag_list= methods
- Add
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
- Add
tag_list filed to table form. Such that use can input tag list as tag1, tag2 ...
- Process tag_list params in record_controllers. Update views similar with table.
- We change from text_field to select for using auto-completion.
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
References
|
Rails
|
28
|
Make JQuery binding click events work for dynamically generated elements.
|
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();
});
});
More details
$( 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.
|
|
27
|
Rails Render
|
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 %>
Passing local variables
<%= 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 %>
|
|
26
|
Rails Comment System
|
Comment
- DB
- comments:
user:references, table:references, record_id:integer parent:references rely:references content:text
- limit to table or table record only. Use polymorphic for commenting anything.
- Add parent_id/reply_id for replying comments, could be nil. Parent points to the top-level comment, reply points to the comment it replies.
- Model
- Add
has_many :children and has_many: replies through parent/reply_id.
- Once a comment deleted, we deleted all its children and replied comments recursively
- Add
cascade_reply_ids which find all comment IDs that need to be deleted.
- Views
- The comment container has two parts: new comment and current comments
- For each comment, we show its content, followed by # of replies, which will show all replies once clicked. Also show when it's created, and icon for deleting
- For reply part, its similar, on top, we show new reply, followed by all replies.
- To make view nice, we show only two level of comments. All deeper comments are flatten by setting parent_id to top-level comment.
- We use reply_id to link to the comment it replies to. We also show short notes of the comment.
- Once a new reply added, we insert the reply on the top, update # of replies as well.
- Once a top-level comment deleted, we delete all its children; once a reply deleted, we delete all replies recursively; update # of replies as well.
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 %>
|
|
25
|
Active Job
|
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.
Sidekiq
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:
- Rails serialize jobs into Redis database
- Sidekiq process fetch jobs from Redis
- restore environment in Rails code, then process it.
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
|
|
24
|
State Machine Transition by JS
|
I want to show friend status and action in the home page. The friendship status includes:
- stranger
- friend
- pending (request sent)
- waiting (request received)
- rejected (request rejected)
- declined (declined request)
The action includes
- stranger: -> request friendship
- friend: -> unfriend
- pending: -> withdraw
- waiting: -> accept; -> reject
- rejected
- declined: -> accept
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()
});
});
|
|
23
|
Bootstrap Tooltips
|
- Tooltips vs popover
- Popovers require Tooltips to be included.
- popovers have an option to display both a title and content, while tooltips only have an option to display a title.
- Tooltips are typically only visible on hover, Popovers are typically dismissable, whether by click on other parts of the page or second clicking the popover target (depending on implementation)
- Although it should be noted due to accessibility problems, potential SEO issues, and lack of discovery of the tooltip/popover, neither are recommended unless you absolutely need them.
- Install
- check package.json, make sure 'bootstrap' and 'popper' are installed
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
|
|
22
|
Rails Load More (2)
|
Rails Load More
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>
|
|
21
|
Rails Load More
|
I want to show user activity history which could be huge in AJAX way.
Basically I follow this guide, with improvement from this guide
- views/home/activities.html.erb
<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.
- views/home/_activity.html.erb
<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.
- javascript/packs/utils.js
$(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();
}
});
});
});
- controllers/home_controller.rb
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.
- views/home/activities.js.erb
<% 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
|
|
20
|
render collection
|
loop method
<% users.each do |user| %>
<%= render user %>
<% end %>
render collection directly
view in other directory
<= render (partial: 'home/user', collection: @users) %>
|
|
19
|
Rails JavaScript
|
- UJS (Unobtrusive JavaScript): 主要的两个原则
- HTML JS 分离 (separation of functionality (the
behavior layer ) from a Web page's structure/content and presentation)
- 锦上添花,没JS也能用 (progressive enhancement to support user agents that may not support certain JavaScript functionality and users that have disabled JavaScript)
- Rails内嵌JS
- 通过
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
- Rails 5.1 introduced rails-ujs and dropped jQuery as a dependency.
- Unlike the version with jQuery, all custom events return only one parameter:
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. |
<a href="..." data-turbolinks="false">No turbolinks here</a>.
- Page change events:
load -> turbolinks:load
document.addEventListener("turbolinks:load", () => {
alert("page has loaded!");
});
Server side
- return JSON
- format.json
- JS generate HTML based on JSON data
- return HTML
- format.js
- return HTML directly
// foo.js.erb
var users = document.querySelector("#users");
users.insertAdjacentHTML("beforeend", "<%= j render(@user) %>");
- Client make a AJAX request
// 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) {}
})
|
|
18
|
AJAX events
|
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;
});
|
|
17
|
mutual friends
|
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
|
|
16
|
javascript_include_tag vs javascript_pack_tag
|
- javascript_include_tag: for app/assets
<script src="https://localhost/assets/xmlhr.debug-1284139606.js"></script>
- javascript_pack_tag: for webpacker
<script src="/packs/js/application-be5eb61178f8249f882f.js" data-turbolinks-track="reload"></script>
- similar for stylesheet
- html
|
|
15
|
upload file
|
guide
- form
<%= 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 %>
- controller
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)
- Note that params[:file] is
ActionDispatch::Http::UploadedFile object
- It has tempfile stored in tmp files, e.g.
/tmp/RackMultipart20210118-18052-19hic1b.csv , which will be deleted when the file is closed.
|
|
14
|
include css.erb file in application.css
|
To use rouge, I include an erb file in application.css. This cause some problems in Rails console
- First
rails console failed with error cannot load such file -- rouge ; the second try works.
- Can't reload! Second try doesn't work either.
Solution: Not work
#config/application.rb
config.assets.precompile += ["rouge"]
|
|
13
|
Rails.application.secrets.secret_key_base is empty in production.
|
use Rails.application.credentials .secret_key_base
check for details
There are two ways to access secret_key_base:
- Rails.application.credentials.secret_key_base
- Rails.application.secrets.secret_key_base
Rails 5 took the first way by default.
you can change Rails.application.credentials.secret_key_base by rails credentials:edit.
|
|
12
|
Markdown
|
Guide
Install redcarpet
gem 'redcarpet'
gem 'rouge' // for themes
- app/helpers/markdown_helper.rb
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
- application_controller.rb
usage: markdown(text)
Rouge themes
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:
- List with code not working well, must have new line between
|
|
11
|
Rails建站指南
|
How to start a new Rails app
System
Install ubuntu 18.04 LTS
DB:PostgreSQL
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
Guide
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
Ngnix
Guide
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
Deploy
Guide
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.
HTTPS
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.
Basic
Generator
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
compression
Javascript
Bootstrap:
Guide
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:
Email
- Install
rails g mailer admin_mailer send_passwd
- Allow less secure apps to access your Gmail account: Guide
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
- NOTE: use
ssh -Y in Mac to connect server, ssh -X not work
I18n
Guide
- add
zh.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
Google
- Register accounts
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 %>
- Get API key
- in Gemfile:
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
|
|
10
|
HTML5 date picker doesn't show on Safari
|
|
|
9
|
RichText(Quill)
|
Why Quill?
- support math
- internal format, not html
Question:
- where local image saved?
- support image url?
Problem:
- can't have two editors on the same page
- jquery syntax not work to find body
- missing images under packs
- limit size by bootstrap
- wrapper by another div, col-10 ok, do not use form-control
- math
- need kaTex
- include js in application.html.erb directly
- image with URL
Guide:
- https://github.com/abhinavmathur/quilljs-rails
- https://www.kohrvid.com/blog/posts/using-quilljs-with-rails-6
- https://medium.com/@technoblogueur/rails-6-and-webpacker-what-you-need-to-know-e3534fded7ff
- https://stackoverflow.com/questions/56198624
- demo: https://harlemsquirrel.github.io/jekyll/update/2016/12/11/rails-and-quill.html
Install
- yarn add quill
- include in application, import css as well.
- view:
- add hidden_field with class 'rich-text-content',
- add a div with id 'editor', wrapped by div with 'col-10' to limit size
- in js:
- copy content from div with id='editor' to input with class 'rich-text-content', thus save and upload to server
- config options and toolbar
- support math
- include js in application.html.erb directly
- image with URL
min height
.ql-editor{
min-height: 200px;
}
|
|
8
|
Rails chart
|
chartkick
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
|
|
6
|
Link to current page with plus/merged with a param
|
Solution
link_to "Page", params.merge(:c => "e")
Error: Rails Unable to convert unpermitted parameters to hash
solution
#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
|
|