dreamable/RailsNotes
ID 标题 笔记 标签
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. 
    
    • use turbo_frame_tag
50
Rails Webpacker & stylesheets
  1. I have an issue that select2 css is not included in production, although OK in development.
  2. Further, I found all css included in app/javascripts/packs/application.js not working
  3. 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
  4. 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.
  5. One solution is to update extract_css=false for production as well, this leads to embed css in HTML, which works but is ugly.
  6. I workaround the problem by moving all css from packs to assets/stylesheets/application.scss
  7. 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.
  1. css file not found.
/* in application.scss */
@import "./github_markdown.css";
-> 
@import "github_markdown";
  1. 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
  1. 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, "")
    
  2. 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);
      });
    });
    
  3. 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

  • View: 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 %>
  • Controller: handle array
      @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

    • in models/user.rb
    has_one_attached :avatar
    
    • in views
    <%= f.file_field :avatar,  class: "form-control" %>
    
    • in 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
    
    • show the avatar
    <%= 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

  • install
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)
});
  • html
<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

  1. html2markdown: pretty old, don't know how to use
  2. reverse_markdown

    input  = '<strong>feelings</strong>'
    result = ReverseMarkdown.convert input
    result.inspect # " **feelings** "
    
  3. 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
  1. DNS setting: add CNAME api -> @ or * -> @
  2. 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.
  3. 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.

  1. Remove the daemonization
  2. Remove logfile
  3. Remove pidfile Check here for discussion, why daemonization is bad, the pidfile problem

My current workaround:

  1. Logfile: redirect stdout to a file
  2. 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:

  1. Prawn: native and more flexible
  2. Wicked-PDF: Wicked depends on wkhtmltopdf and uses systems call
  3. CombinePDF: combine PDF files
  4. pdfcrowd: API for http://pdfcrowd.com

DOC:

  1. Caracal
  2. docx
  3. docx_replace
  4. docx
  5. html2doc

Prawn Notes: guide

  1. 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") } )
      
  2. support Chinese

    • gkai00mp.ttf is not downloaded by bundle install

      • The standard prawn gem is of version 2.4.0, may be dated without all fonts
      • Change to use the latest version from GitHub
       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

      • guide
      • May have some bug, has to set font.
      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
    
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
    1. tags: name:string counter:integer
    2. taggings: tag:references table:references record_id:integer
    3. I added a counter to tags for ranking. Not used yet.
    4. Special tags fixed to table or table records. Use polymorphic for tagging anything.
  • Model

    1. Add has_many: tags to table.rb, make sure record_id is nil for table tags. Define tag_list and tag_list= methods
    2. 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

    1. Add tag_list filed to table form. Such that use can input tag list as tag1, tag2 ...
    2. Process tag_list params in record_controllers. Update views similar with table.
    3. We change from text_field to select for using auto-completion.

Tag Cloud
Follow guide

  1. 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
    
  2. 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
    
  3. 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;}
    
  4. 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

  1. 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
    
  2. 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 %>
    
  3. 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

  1. Install select2

    yarn add select2
    yarn add select2-bootstrap-theme
    
  2. 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}
          }
        }
      });
    });
    
  3. 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
    
  4. 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

  1. 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.

  2. UI is ugly is using bootstrap style
    Need to install select2-bootstrap-theme guide

  3. 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

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
    1. comments: user:references, table:references, record_id:integer parent:references rely:references content:text
    2. limit to table or table record only. Use polymorphic for commenting anything.
    3. 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
    1. Add has_many :children and has_many: replies through parent/reply_id.
    2. Once a comment deleted, we deleted all its children and replied comments recursively
    3. Add cascade_reply_ids which find all comment IDs that need to be deleted.
  • Views
    1. The comment container has two parts: new comment and current comments
    2. 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
    3. For reply part, its similar, on top, we show new reply, followed by all replies.
    4. To make view nice, we show only two level of comments. All deeper comments are flatten by setting parent_id to top-level comment.
    5. We use reply_id to link to the comment it replies to. We also show short notes of the comment.
    6. Once a new reply added, we insert the reply on the top, update # of replies as well.
    7. 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:

  1. stranger
  2. friend
  3. pending (request sent)
  4. waiting (request received)
  5. rejected (request rejected)
  6. declined (declined request)

The action includes

  1. stranger: -> request friendship
  2. friend: -> unfriend
  3. pending: -> withdraw
  4. waiting: -> accept; -> reject
  5. rejected
  6. 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

    • Add to js
    // Tooltips
    $(document).on("turbolinks:load", function() {
      $('[data-toggle="tooltip"]').tooltip();
    });
    
    • In the views, add
      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.

  1. 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();
      }
    });
    });
    });
    
  2. 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
    
  3. 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 %>
    
  4. 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

  • model/activity.rb
    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
  1. loop method

    <% users.each do |user| %>
    <%= render user %>
    <% end %>
    
  2. render collection directly

    <= render @users %>
    
  3. view in other directory

    <= render (partial: 'home/user', collection: @users) %>
    
19
Rails JavaScript

Working with JavaScript in Rails

How to Use AJAX With Ruby on Rails

  • 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
  • Models
# 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

  1. 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 %>
  1. 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

  1. First rails console failed with error cannot load such file -- rouge; the second try works.
  2. 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:

  1. Rails.application.credentials.secret_key_base
  2. 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

  • Gemfile
  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
  include MarkdownHelper
  • usage: markdown(text)

  • Rouge themes

    • we use GitHub
  • css (app/assets/stylesheets)

    • application.css
     *= require rouge
     *= require github_markdown
    
    • rouge.scss.erb
    <%= 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

  1. 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
    
  2. 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

  1. 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'
    
  2. Generate template

    bundle
    # use cap in rbenv, source ~/.bashrc if cap not found
    cap install STAGES=production
    
  3. Edit Capfile

  4. Capfile:

      require 'capistrano/rails'
      require 'capistrano/passenger'
      require 'capistrano/rbenv'
      set :rbenv_type, :user
      set :rbenv_ruby, '2.6.5'
    
  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
    
  6. deploy/production.rb

      server 'yourdomain.com', user: 'web', roles: %w{app db web}
    
  7. 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
    
  8. 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;
    
  9. Make swap, otherwise, too slow.

    cap production deploy
    

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

  • jQuery: follow guide

    • install jquery if not yet: yarn add jquery
    • in config/webpack/environment.js
    const webpack = require('webpack')
    environment.plugins.prepend('Provide',
      new webpack.ProvidePlugin({
        $: 'jquery/src/jquery',
        jQuery: 'jquery/src/jquery'
      })
    )
    
    • in app/javascript/packs/application.js
    require('jquery')
    
    • check webpack-dev-server to make sure no error
  • support coffee files: follow guide

    • rails webpacker:install:coffee
      • updated config/webpack/environment.js
      • updated config/webpacker.yml
      • updated config/webpack/loaders/coffee.js
    • rm app/javascript/packs/hello_coffee.coffee
    • put coffee files in app/javascript/coffee
    • in application.js, add require('../coffee/application.coffee')

Bootstrap:

Guide

  1. Install

    yarn add bootstrap popper.js jquery
    
  2. In app/javascript/stylesheets/application.scss

    @import 'bootstrap/scss/bootstrap';
    
  3. In app/javascript/packs/application.js

    import 'stylesheets/application' # include scss above
    import 'bootstrap/dist/js/bootstrap';
    
  4. On-the-fly build for dev:

    bin/webpack-dev-server
    

Email

  1. Install rails g mailer admin_mailer send_passwd
  2. Allow less secure apps to access your Gmail account: Guide
  3. 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
    }
    
  4. 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

  1. add zh.yml file under lib/locale (default config/locales)
  2. 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
    
  3. 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
    
  4. config/routes.rb, put everything in

    scope "/:locale" do
      resources :books
    end
    

Google

Google analytics

  1. Register accounts
  2. 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>
    
  3. 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 %>
    

Google recaptcha

  1. Get API key
  2. in Gemfile: gem "recaptcha"
  3. Configure keys in config/initializers/recaptcha.rb

    Recaptcha.configure do |config|
      config.site_key  = 'xxx'
      config.secret_key = 'xxxx'
    end
    
  4. Add to views: <%= recaptcha_tags %>

  5. 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

jQuery date picker problem in Safari

  • Safari does not include a native datepicker for its desktop version (although it does for iOS). Incidentally, neither does IE. It's very frustrating as it could save developers a lot of time if they did.
  • This is a useful link for tracking support for it: http://caniuse.com/#feat=input-datetime
  • 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" />
    
9
RichText(Quill)

Why Quill?

  1. support math
  2. internal format, not html

Question:

  1. where local image saved?
  2. support image url?

Problem:

  1. can't have two editors on the same page
  2. jquery syntax not work to find body
  3. missing images under packs
  4. limit size by bootstrap
    • wrapper by another div, col-10 ok, do not use form-control
  5. math
    • need kaTex
    • include js in application.html.erb directly
  6. image with URL

Guide:

  1. https://github.com/abhinavmathur/quilljs-rails
  2. https://www.kohrvid.com/blog/posts/using-quilljs-with-rails-6
  3. https://medium.com/@technoblogueur/rails-6-and-webpacker-what-you-need-to-know-e3534fded7ff
  4. https://stackoverflow.com/questions/56198624
  5. demo: https://harlemsquirrel.github.io/jekyll/update/2016/12/11/rails-and-quill.html

Install

  1. yarn add quill
  2. include in application, import css as well.
  3. view:
    • add hidden_field with class 'rich-text-content',
    • add a div with id 'editor', wrapped by div with 'col-10' to limit size
  4. 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
  5. support math
    • include js in application.html.erb directly
  6. image with URL
  7. min height

    .ql-editor{
      min-height: 200px;
    }
    
8
Rails chart

chartkick

  1. 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")
    
  2. chart library

  3. Google chart

    • Load JS <%= javascript_include_tag "https://www.gstatic.com/charts/loader.js" %>
    • Get an API Key
      1. To use google geo_chart, need a API key. (Seems OK for timeline chart).
      2. get a key
      3. 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. 
      
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