Table: RailsNotes
User: dreamable
Created at: 2021-03-05 06:08:30 UTC
Updated at: 2024-11-19 16:31:37 UTC
Reference:(Table ID 3, Record ID 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

Tag: