<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.9.5">Jekyll</generator><link href="https://blog.eq8.eu/feed.xml" rel="self" type="application/atom+xml" /><link href="https://blog.eq8.eu/" rel="alternate" type="text/html" /><updated>2024-02-23T11:46:24+00:00</updated><id>https://blog.eq8.eu/feed.xml</id><title type="html">EquiValent</title><subtitle>Ruby, Ruby on Rails, Elixir fullstack developer, DevOps and code philosopher.</subtitle><author><name>EquiValent - Tomas Valent</name></author><entry><title type="html">Inline SVG in Ruby on Rails</title><link href="https://blog.eq8.eu/til/inline-svg-in-ruby-on-rails.html" rel="alternate" type="text/html" title="Inline SVG in Ruby on Rails" /><published>2024-01-16T00:00:00+00:00</published><updated>2024-01-16T00:00:00+00:00</updated><id>https://blog.eq8.eu/til/inline-svg-in-ruby-on-rails</id><content type="html" xml:base="https://blog.eq8.eu/til/inline-svg-in-ruby-on-rails.html"><![CDATA[<p><img src="https://images.unsplash.com/photo-1506729623306-b5a934d88b53?crop=entropy&amp;cs=tinysrgb&amp;fit=crop&amp;fm=jpg&amp;h=600&amp;ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY4MTM2NzQ3MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;utm_campaign=api-credit&amp;utm_medium=referral&amp;utm_source=unsplash_source&amp;w=1600" alt="" /></p>

<p>SVG image/icon has the benefit that it can be rendered as a part
of HTML rendering</p>

<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
  &lt;title&gt;SVG can be inline&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;h2&gt;Here is a SVG inline 👍&lt;/h2&gt;

  &lt;svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"&gt;
    &lt;path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /&gt;
    &lt;path stroke-linecap="round" stroke-linejoin="round" d="M9 9.563C9 9.252 9.252 9 9.563 9h4.874c.311 0 .563.252.563.563v4.874c0 .311-.252.563-.563.563H9.564A.562.562 0 0 1 9 14.437V9.564Z" /&gt;
  &lt;/svg&gt;

  &lt;p&gt;Solution will save us extra HTTP call &lt;/p&gt;


  &lt;h2&gt;Here is a SVG rendered as a regular image 😐 (non-inline)&lt;/h2&gt;

  &lt;img src="/assets/images/my_svg_image.svg"&gt;

  &lt;p&gt;
     Solution is not as good as it will create extra HTTP call.
     In 2024 not that a big deal (browser cache, CDNs, HTTP2) but still
     if you have lot of SVG images (icons) it's nice to avoid it
  &lt;/p&gt;

&lt;/body&gt;
&lt;/html&gt;
</code></pre>

<p>So how to render SVG images inline in Ruby on Rails?</p>

<h3 id="solution-1---gem">Solution 1 - gem</h3>

<p>Best choice is to use old but very relevant and maintained gem <a href="https://github.com/jamesmartin/inline_svg">inline_svg</a></p>

<pre><code class="language-erb">&lt;%= inline_svg_tag("my_svg_image", height: 50, class: "red-icon" ) %&gt;
</code></pre>

<h3 id="solution-2---render-partial">Solution 2 - render partial</h3>

<p>But if you don’t want to install another gem just to render icon you can
just render it from partial</p>

<pre><code class="language-erb">&lt;!-- app/views/application/_my_svg_image.html.erb --&gt;
&lt;svg height="&lt;%= local_assigns[:height] %&gt;" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"&gt;
  &lt;path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /&gt;
  &lt;path stroke-linecap="round" stroke-linejoin="round" d="M9 9.563C9 9.252 9.252 9 9.563 9h4.874c.311 0 .563.252.563.563v4.874c0 .311-.252.563-.563.563H9.564A.562.562 0 0 1 9 14.437V9.564Z" /&gt;
&lt;/svg&gt;
</code></pre>

<pre><code class="language-erb">&lt;%= render "my_svg_image" %&gt;
&lt;%= render "my_svg_image", height: 50 %&gt;
</code></pre>

<p>Obvious benefit is that these are just partials = your IDE will provide same benefits (e.g. RubyMine has feature “find usage” and you will find every line of code loading the partial/svg file)</p>

<p>But once you have more icons you will find out this is difficult to maintain
as you cannot preview the icons from partials (browser, IDE,..).</p>

<p>But more important you need to edit every partial where SVG  needs to accept HTML argument (e.g. height)</p>

<h3 id="solution-3">Solution 3</h3>

<p>So here is a solution where no extra gem is needed (given your project already uses Nokogiri) &amp; you can preview SVG images &amp; you can pass HTML arguments</p>

<p>You will load SVG content and ouutput it in ERB and if any SVG HTML tag arguments require addition/alteration you do that with Nokogiri</p>

<pre><code class="language-ruby">module SvgHelper
  SVGFileNotFoundError = Class.new(StandardError)

  def inline_svg_tag(svg_path, options = {})
    path = Rails.root.join("app/assets/images/#{svg_path}.svg")
    File.exist?(path) || raise(SVGFileNotFoundError, "SVG image file does not exist: #{path}")
    svg_file_content = File.binread(path)

    if options.any?
      doc = Nokogiri::XML::Document.parse(svg_file_content)
      svg = doc.at_css("svg")
      svg["height"] = options[:height] if options[:height]
      svg["width"] = options[:width] if options[:width]
      svg["class"] = options[:class] if options[:class]
      svg_file_content = doc.to_html.strip
    end

    raw svg_file_content
  end
end
</code></pre>

<blockquote>
  <p>Note the Helper code is pretty much what <a href="https://github.com/jamesmartin/inline_svg">inline_svg</a> gem does.</p>
</blockquote>

<pre><code class="language-erb">&lt;%= inline_svg_tag("my_svg_image", height: 50, width: 50 class: "red-icon" ) %&gt;
</code></pre>

<h5 id="test">Test</h5>

<pre><code class="language-ruby">require "rails_helper"

RSpec.describe SvgHelper do
  describe "#inline_svg_tag" do
    it "raises an error when the file does not exist" do
      expect { helper.inline_svg_tag("does-not-exist") }.to raise_error(SvgHelper::SVGFileNotFoundError)
    end

    it "when no options passed returns the SVG file contents with original HTML attribute values" do
      result = helper.inline_svg_tag("my_test_svg_image")
      expect(result).to include("&lt;svg")
      expect(result).to include('height="20"')
      expect(result).to include('width="20"')
      expect(result).not_to include("class")
    end

    it "when class option passed returns the SVG file contents with class HTML attribute" do
      result = helper.inline_svg_tag("my_test_svg_image", class: "whatever")
      expect(result).to include("&lt;svg")
      expect(result).to include('class="whatever"')
    end

    it "when height passed returns the SVG file contents with new height" do
      result = helper.inline_svg_tag("my_test_svg_image", height: "12345")
      expect(result).to include("&lt;svg")
      expect(result).not_to include('height="20"')
      expect(result).to include('height="12345"')
    end

    it "when width passed returns the SVG file contents with new width" do
      result = helper.inline_svg_tag("my_test_svg_image", width: "54321")
      expect(result).to include("&lt;svg")
      expect(result).not_to include('width="20"')
      expect(result).to include('width="54321"')
    end
  end
end
</code></pre>

<p>image <code>app/assets/images/my_test_svg_image.svg</code>:</p>

<pre><code>&lt;svg height="20" width="20" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"&gt;
  &lt;path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /&gt;
  &lt;path stroke-linecap="round" stroke-linejoin="round" d="M9 9.563C9 9.252 9.252 9 9.563 9h4.874c.311 0 .563.252.563.563v4.874c0 .311-.252.563-.563.563H9.564A.562.562 0 0 1 9 14.437V9.564Z" /&gt;
&lt;/svg&gt;
</code></pre>

<blockquote>
  <p>SVG in example is outline <code>stop-circle</code> icon from <a href="https://heroicons.com">https://heroicons.com</a></p>
</blockquote>

<h4 id="bonus-dont-raise-on-production">Bonus: Don’t raise on Production</h4>

<ul>
  <li>Don’t raise on Production as typos happen</li>
  <li>notify your Error manager (e.g Airbrake, Appsignal, …)</li>
  <li>render html comment instead</li>
</ul>

<pre><code class="language-ruby">def inline_svg_tag(svg_path, options = {})
  # ...
rescue SVGFileNotFoundError =&gt; error
  if Rails.env.production?
    Appsignal.send_error(error)
    return raw("&lt;!-- SVG file missing: #{svg_path}.svg --&gt;")
  else
    raise error
  end
end
</code></pre>

<pre><code class="language-ruby">   # ...
it "when the file does not exist in production sends an error to Appsignal and output a comment" do
  expect(Rails).to receive_message_chain(:env, :production?).and_return(true)
  expect(Appsignal).to receive(:send_error)
  result = helper.inline_svg_tag("icons/does-not-exist")
  expect(result).to eq("&lt;!-- SVG file missing: icons/does-not-exist.svg --&gt;")
end
</code></pre>

<h3 id="why-bother-with-svgs">why bother with SVGs</h3>

<p><a href="https://www.adobe.com/creativecloud/file-types/image/comparison/png-vs-svg.html">https://www.adobe.com/creativecloud/file-types/image/comparison/png-vs-svg.html</a></p>

<h3 id="sources">Sources</h3>

<ul>
  <li>Photo by Harpal Singh on <a href="https://unsplash.com/photos/white-paper-_zKxPsGOGKg?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Unsplash</a></li>
  <li><a href="https://stackoverflow.com/questions/36986925/how-do-i-display-svg-image-in-rails">SO thread</a></li>
</ul>]]></content><author><name>EquiValent - Tomas Valent</name></author><category term="til" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">System.d service (daemon) for Puma server instaled under RVM (Rails)</title><link href="https://blog.eq8.eu/til/systemd-service-daemon-for-puma-server-instaled-under-rvm.html" rel="alternate" type="text/html" title="System.d service (daemon) for Puma server instaled under RVM (Rails)" /><published>2023-11-30T00:00:00+00:00</published><updated>2023-11-30T00:00:00+00:00</updated><id>https://blog.eq8.eu/til/systemd-service-daemon-for-puma-server-instaled-under-rvm</id><content type="html" xml:base="https://blog.eq8.eu/til/systemd-service-daemon-for-puma-server-instaled-under-rvm.html"><![CDATA[<p>Systemd ignores any <code>PATH</code> settings = you need to use full path to rvm.
In order to use puma under RVM with systemd you need to create a wrapper for RVM:</p>

<pre><code class="language-bash">cd /root/myapp
rvm current                                   # e.g.: ruby-3.2.2@myapp_2023

# create RVM wrapper
rvm alias create myapp ruby-3.2.2@myapp_2023  # rybyversion@gemsetname
</code></pre>

<p>Depending where is your RVM instaled (check with <code>which rvm</code>) this creates a wrapper in RVM folder. Mine is <code>/usr/share/rvm/wrappers/myapp</code> . This you  can refer in systemd service file. (RVM wrapper is something similar to  aias or symbolic link)</p>

<pre><code># /etc/systemd/system/myapp_puma.service

[Unit]
Description=Puma HTTP Server
After=network.target

# Uncomment for socket activation (see below)
# Requires=puma.socket

[Service]
Type=notify

WatchdogSec=10

# Preferably configure a non-privileged user
# User=

WorkingDirectory=/root/myapp_tw

# Explicitly define your ENV variables as they may be ignored
Environment=WEB_CONCURRENCY=3
Environment=RAILS_ENV=production

# Helpful for debugging socket activation, etc.
# Environment=PUMA_DEBUG=1

ExecStart=/usr/share/rvm/wrappers/myapp/bundle exec puma -C ./config/puma.rb

Restart=always

[Install]
WantedBy=multi-user.target
</code></pre>

<blockquote>
  <p>note: I <code>ln -s </code> my <code>master.key</code> to app <code>config/master.key</code> so I don’t use <code>RAILS_MASTER_KER</code></p>
</blockquote>

<pre><code class="language-bash">systemctl daemon-reload              #each  time you change the service file
systemctl start myapp_puma.service
systemctl status myapp_puma.service

# something goes wrong
journalctl -u myapp_puma -e -f
</code></pre>

<blockquote>
  <p>Note: any time you change ruby version or gemset you need to recreate the RVM wrapper</p>
</blockquote>

<p>source:</p>
<ul>
  <li><a href="https://github.com/puma/puma/blob/master/docs/systemd.md">https://github.com/puma/puma/blob/master/docs/systemd.md</a></li>
  <li><a href="https://rvm.io/deployment/init-d">https://rvm.io/deployment/init-d</a></li>
</ul>

<p>Rails 7.1, Ruby 3.2, Puma 6, RVM rvm 1.29.12, Ubuntu 22.04. Created 2023-11-30</p>

<p>keywords: init.d,  system.d, puma webserver, ruby on rails, rvm</p>]]></content><author><name>EquiValent - Tomas Valent</name></author><category term="til" /><summary type="html"><![CDATA[Systemd ignores any PATH settings = you need to use full path to rvm. In order to use puma under RVM with systemd you need to create a wrapper for RVM:]]></summary></entry><entry><title type="html">Update millions of records in Rails</title><link href="https://blog.eq8.eu/til/update-millions-of-records-in-rails.html" rel="alternate" type="text/html" title="Update millions of records in Rails" /><published>2023-04-26T00:00:00+00:00</published><updated>2023-04-26T00:00:00+00:00</updated><id>https://blog.eq8.eu/til/update-millions-of-records-in-rails</id><content type="html" xml:base="https://blog.eq8.eu/til/update-millions-of-records-in-rails.html"><![CDATA[<p>How to update half a billion entries on a PostgreSQL table with Ruby on Rails &amp; <a href="https://github.com/sidekiq/sidekiq">Sidekiq</a></p>

<p>TL;DR: create a Sidekiq job that would construct a single UPSERT (or UPDATE) fields SQL call for that batch of records given your business rules. Then schedule the job in small batches and monitor how fast will the job finish. Tweak the batch size and number of threads for your Sidekiq worker until you find the sweet spot. Then schedule it by  millions.</p>

<p>Longer version:</p>

<h3 id="worker">Worker</h3>

<pre><code class="language-ruby"># app/workers/update_addresses_worker.rb
class UpdateAddressesWorker
  include Sidekiq::Worker
  sidekiq_options queue: :manual

  def perform(min_id, max_id, batch_size = 1_000)
    Address
      .where(id: min_id..max_id)
      .in_batches(of: batch_size) do |address_batch|
        MyService.new.call(address_batch)
      end
  end
end
</code></pre>

<blockquote>
  <p>note: my queue name is “<em>manual</em>” you can use “<em>default</em>” or whatever you use in your app.</p>
</blockquote>

<h3 id="service">Service</h3>

<p>For simplicity <code>MyService</code> will just downcase <code>city</code> name &amp; <code>state</code> for entire batch of Address objects.</p>

<blockquote>
  <p>Yes this can be done with a single SQL query (If you can afford to lock entire table for couple of minutes)
Please consider  <strong>this is just an example</strong> and the real script where you want to use this will be more complex  with  <strong>business logic code directly involved</strong>.</p>
</blockquote>

<p>In this example I’m using gem <a href="https://github.com/zdennis/activerecord-import">activerecord-import</a> in order to update/insert multiple records with <em>one SQL query</em> (including validations). Project I work for already uses this gem so it’s well tested solution for our use case.</p>

<p><strong>However</strong> Vanilla Rails has <a href="https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-upsert_all">.upsert_all</a> that serves similar purpose and you can achieve the same result with it.
Reason why the article is not using <code>.upsert_all</code> is because I didn’t used it in production yet  so I’m not going to recommend something I didn’t truly use 😉. But it’s worth checking it out.</p>

<blockquote>
  <p>Note <code>upsert</code> SQL operation is pretty much “insert or update” = is slower than <code>update</code> operation where you already know the IDs and you don’t expect conflict (Thank you <a href="https://www.reddit.com/r/ruby/comments/12zmxkb/comment/jhtp201/?utm_source=share&amp;utm_medium=web2x&amp;context=3">Seuros</a> for pointing this out).</p>
</blockquote>

<pre><code class="language-ruby">class MyService
  def call(address_batch)
    addresses = address_batch.map do |address|
      # some real business logic code here manipulating the address object state, this is just an example
      address.city.downcase!
      address.state.downcase!
      address
    end

    # `Model.import` is from activerecord-import gem
    ::Address.import(
      addresses,
      on_duplicate_key_update: {
        conflict_target: %i[id],
        validate: true,
        columns: [:city, :state]
      }
    )
  end
end
</code></pre>

<p>Notice <a href="https://github.com/zdennis/activerecord-import#duplicate-key-update">on_duplicate_key_update</a> options which takes care of updates of <code>city</code> &amp; <code>state</code> (columns) when <code>addresses</code> db row matching the <code>id</code> (conflict_target) already exist.</p>

<blockquote>
  <p>Given the scenario you may do this even faster by avoiding ActiveRecord and constructing (&amp; calling) a custom SQL query within this service (check <a href="https://www.reddit.com/r/ruby/comments/12zmxkb/comment/jhwmgkc/?utm_source=share&amp;utm_medium=web2x&amp;context=3">BiackPanda’s</a> comment). Depends on your use case.</p>
</blockquote>

<h3 id="how-to-schedule-this">How to schedule this</h3>

<p>Once you deploy the worker code to prod run a <code>rails c</code> console in production env.</p>

<p>Now try scheduling the worker in small chunks like this:</p>

<pre><code class="language-ruby">Address.in_batches(of: 1_000, start: 300_000_000, finish: 300_010_000) do |address_batch|
   min_id = address_batch.minimum(:id)
   max_id = address_batch.maximum(:id)
   worker_batch_size = 250
   puts "#{min_id} - #{max_id}  [#{worker_batch_size}]"
   UpdateAddressesWorker.perform_async(min_id, max_id, worker_batch_size)
end

# 300_000_000 - 300_002_000  [250]
# 300_002_001 - 300_004_029  [250]
# 300_004_030 - 300_006_567  [250]
# ...
</code></pre>

<h3 id="figure-out-the-best-for-you">Figure out the best for you</h3>

<p>Because we (<a href="https://www.postpilot.com/">PostPilot</a>) are dealing with several hundred millions of records it’s not easy to get those numbers right. You need to schedule few thousand record samples and <strong>monitor</strong> how well/bad will your worker perform</p>

<blockquote>
  <p>e.g in Heroku monitor your worker dyno Memory usage, in tool like NewRelic or AppSignal monitor DB Load &amp; I/O Operations, Monitor errors, In Sidekiq Web UI monitor number of jobs in queue and how long the job takes to finish (aim for “finish fast” jobs - up to 2 minutes was my goal)</p>
</blockquote>

<p>Maybe your worker will consume all the memory and you need to schedule smaller batches. Maybe you need to increase memory on the underlying VM running your Sidekiq workers</p>

<blockquote>
  <p>For example Heroku Standard 1x Dyno has only 512MB, maybe increase it to Standard 2x Dyno (1GB could be enough), or in some cases it make sense to go Performance-M Dyno with 2,5GB. More in <a href="https://devcenter.heroku.com/articles/dyno-types">heroku dynos</a> and <a href="https://judoscale.com/guides/how-many-dynos">common dyno type issues</a></p>
</blockquote>

<p>Maybe your worker will be underutilized and therefore you can increase <code>worker_batch_size</code> or <strong>number of threads for your Sidekiq worker</strong></p>

<blockquote>
  <p>just be mindfull on how many active connections your PosgreSQL DB can handle.
For example Heroku’s Standard 7 has 500 <a href="https://elements.heroku.com/addons/heroku-postgresql#pricing">Connection Limit</a>. For example 30 dyno with 5 threads == 150 DB connections + you still need connections for rest of the app (webserver, other workers)</p>
</blockquote>

<p>Try tweeking those numbers and for each schedule a sample of couple thousand of records.</p>

<p>Once you got this right you can go wild and increas number of Sidekiq workers (for example have 30, 40, 50 Heroku dynos for your worker)</p>

<p>Recommendation here is not to schedule all 500 M records. But try to schedule 100K see how it goes (monitor), then 1M (monitor), then 10M, 100M, …</p>

<h3 id="implement-killswitch">Implement Killswitch</h3>

<p>You are enqueuing a LOT of jobs. Be sure you have a way to kill those jobs if something goes wrong.</p>

<h4 id="option-1---separate-worker-for-script-jobs">Option 1 - Separate worker for script jobs</h4>

<p>You don’t need to add any special killswitch code for exit a job.</p>

<p>We recommend to have separate Sidekiq worker dedicated to script jobs like this.
Benefit is that if something goes wrong you can just scale these worker VMs to 0 (or 0 worker dynos on Heroku) and just delete the enqued jobs from Sidekiq Web UI.</p>

<p><code>cat config/manual_sidekiq.yml</code></p>

<pre><code>:concurrency: &lt;%= (ENV['MANUAL_MAX_THREADS'] || 1).to_i %&gt;
:queues:
  - [manual, 4]
</code></pre>

<blockquote>
  <p>note: the “MANUAL_MAX_THREADS” ENV variable, you can use this to scale the number of threads for your Sidekiq worker that would be running this script jobs. For example if you have 30 dynos for this worker you can set this to 5 and you will have 150 threads running in parallel.</p>
</blockquote>

<h4 id="option-2---killswitch-flag">Option 2 - Killswitch flag</h4>

<p>If Option 1 is not possible for you, you can implement a killswitch flag in your worker code.</p>

<p>If you use something like <a href="https://github.com/jnunemaker/flipper">Flipper</a> you can exit a job if a flag is set, etc…</p>

<pre><code class="language-ruby"># app/workers/update_addresses_worker.rb
class UpdateAddressesWorker
  # ...

  def perform(min_id, max_id, batch_size = 1_000)
    return if Flipper[:killswitch].enabled? # optional
    # ...or `return if ENV['KILLSWITCH'].present?`
    # ...or just deploy updated worker with `return` on beginning of this method

    #...
</code></pre>

<blockquote>
  <p>e.g. in Heroku when you change ENV variable dyno will reinstantiate . So you can set e.g. <code>KILLSWITCH</code> ENV variable.</p>
</blockquote>

<h3 id="how-long-did-it-take">How long did it take?</h3>

<p>The service had a quite fast business logic code resulting in constructing a SQL that would update couple of fields on a table.</p>

<p>The process of probing different batch sizes &amp; Sidekiq thread numbers with couple of thousands/millions records took about 5 hours. We ended up with 5 threads on 40 Standard 2x Heroku dynos.</p>

<p>Then the actual run of the script with rest of the  half a billion records was finished by the morning (I’ve run it like 11 PM, I’ve checked 7AM next day and all was finished).</p>

<blockquote>
  <p>We (<a href="https://www.postpilot.com/">PostPilot</a>) use <a href="https://elements.heroku.com/addons/judoscale">Judoscale</a> so the dyno number was back to 0 by the morning.</p>
</blockquote>

<p>Again this is very specific to our setup. Your setup will be different. You need to monitor and adjust accordingly.
Also our DB was not under heavy load during the night. If you have a lot of usage on your DB you need to be more careful.</p>

<h3 id="credits">Credits</h3>

<p>Full credit for this solution goes to  <a href="https://github.com/mbbertino">Matt Bertino</a> who taught me this. He is a true PostgeSQL &amp; Ruby on Rails wizard 🧙‍♂️.</p>

<p>Do you want to see what we do? Check us out at <a href="https://www.postpilot.com/">postpilot.com</a></p>

<h3 id="source">Source</h3>

<ul>
  <li><a href="https://github.com/zdennis/activerecord-import#introduction">https://github.com/zdennis/activerecord-import#introduction</a></li>
  <li><a href="https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-upsert_all">https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-upsert_all</a></li>
  <li><a href="https://apidock.com/rails/v6.0.0/ActiveRecord/Persistence/ClassMethods/upsert_all">https://apidock.com/rails/v6.0.0/ActiveRecord/Persistence/ClassMethods/upsert_all</a></li>
  <li><a href="https://blog.kiprosh.com/rails-7-adds-new-options-to-upsert_all/">https://blog.kiprosh.com/rails-7-adds-new-options-to-upsert_all/</a></li>
  <li><a href="https://judoscale.com/guides/how-many-dynos">https://judoscale.com/guides/how-many-dynos</a></li>
</ul>

<h3 id="discussion">Discussion</h3>

<ul>
  <li><a href="https://www.reddit.com/r/rubyonrails/comments/12zo2pp/update_millions_of_records_in_rails_fast/">Reddit r/rubyonrails</a></li>
  <li><a href="https://www.reddit.com/r/ruby/comments/12zmxkb/update_millionsbillions_of_records_in_rails/">Reddit r/ruby</a></li>
  <li><a href="https://www.reddit.com/r/rails/comments/12zmw17/update_millionsbillions_of_records_in_rails/">Reddit r/rails</a></li>
  <li><a href="https://news.ycombinator.com/item?id=35717344">https://news.ycombinator.com/item?id=35717344</a></li>
</ul>]]></content><author><name>EquiValent - Tomas Valent</name></author><category term="til" /><summary type="html"><![CDATA[How to update half a billion entries on a PostgreSQL table with Ruby on Rails &amp; Sidekiq]]></summary></entry><entry><title type="html">Responsive Navbar with Tailwind &amp;amp; Stimulus JS</title><link href="https://blog.eq8.eu/til/responsive-navbar-with-tailwind-stimulusjs.html" rel="alternate" type="text/html" title="Responsive Navbar with Tailwind &amp;amp; Stimulus JS" /><published>2023-01-07T00:00:00+00:00</published><updated>2023-01-07T00:00:00+00:00</updated><id>https://blog.eq8.eu/til/responsive-navbar-with-tailwind-stimulusjs</id><content type="html" xml:base="https://blog.eq8.eu/til/responsive-navbar-with-tailwind-stimulusjs.html"><![CDATA[<p><img src="https://raw.githubusercontent.com/equivalent/equivalent.github.io/master/assets/2022/tailwind-stimulous-navbar-lg.png" alt="screenshot" />
<img src="https://raw.githubusercontent.com/equivalent/equivalent.github.io/master/assets/2022/tailwind-stimulus-navbar.png" alt="screenshot" /></p>

<pre><code class="language-html">&lt;!-- app/views/layouts/_navbar.html.erb --&gt;

&lt;header
  class="bg-gray-500 sm:flex sm:justify-between sm:px-4 sm:py-1 sm:items-center"
  data-controller="navbar" data-navbar-state-value="false"&gt;
  &lt;div class="flex justify-between px-4 py-1 sm:p-0 items-center"&gt;
    &lt;div class="font-bold text-xl font-mono text-gray-200"&gt;
      &lt;span class="text-orange-400"&gt;Dev&lt;/span&gt;Prof
    &lt;/div&gt;
    &lt;div class="sm:hidden"&gt;
      &lt;button class="text-orange-400 focus:text-white focus:outline-none hover:text-white block"
        type="button"
        data-action="click-&gt;navbar#toggle" &gt;
        &lt;span class="sr-only"&gt;Open main menu&lt;/span&gt;
        &lt;svg class="h-8 w-8 fill-current" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"&gt;
          &lt;path data-navbar-target="x"    stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" class="hidden" /&gt;
          &lt;path data-navbar-target="bars" stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" /&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="hidden sm:flex px-2 pt-2 pb-4 sm:pb-2" data-navbar-target="menu"&gt;
    &lt;a href="/" class="block text-gray-200 font-semibold hover:bg-gray-800 rounded px-2 py-1"&gt;Home&lt;/a&gt;
    &lt;a href="/" class="block text-gray-200 font-semibold hover:bg-gray-800 rounded px-2 py-1 mt-1 sm:mt-0 sm:ml-2"&gt;Developers&lt;/a&gt;
    &lt;a href="/" class="block text-gray-200 font-semibold hover:bg-gray-800 rounded px-2 py-1 mt-1 sm:mt-0 sm:ml-2"&gt;Cool Stuff&lt;/a&gt;
  &lt;/div&gt;
&lt;/header&gt;
</code></pre>

<pre><code class="language-javascript">/* app/javascript/controllers/navbar_controller.js */

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static values  = { state: Boolean }
  static targets = [ "menu", "x", "bars" ]

  connect() {
    console.log(this.stateValue)
  }

  toggle() {
    this.stateValue = !this.stateValue

    if (this.stateValue) {
      this.openMenu()
      this.showX()
    } else {
      this.closeMenu()
      this.showBars()
    }
  }

  openMenu() {
    this.menuTarget.classList.remove("hidden");
  }

  closeMenu() {
    this.menuTarget.classList.add("hidden");
  }

  showBars() {
    this.xTarget.classList.add("hidden")
    this.barsTarget.classList.remove("hidden")
  }

  showX() {
    this.xTarget.classList.remove("hidden")
    this.barsTarget.classList.add("hidden")
  }
}
</code></pre>

<h3 id="sources">Sources</h3>

<ul>
  <li><a href="https://www.youtube.com/watch?v=ZT5vwF6Ooig">https://www.youtube.com/watch?v=ZT5vwF6Ooig</a></li>
  <li><a href="https://stimulus.hotwired.dev/handbook/managing-state">https://stimulus.hotwired.dev/handbook/managing-state</a></li>
</ul>]]></content><author><name>EquiValent - Tomas Valent</name></author><category term="til" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Responsibility On Rails</title><link href="https://blog.eq8.eu/article/responsibility-on-rails.html" rel="alternate" type="text/html" title="Responsibility On Rails" /><published>2022-12-08T00:00:00+00:00</published><updated>2022-12-08T00:00:00+00:00</updated><id>https://blog.eq8.eu/article/responsibility-on-rails</id><content type="html" xml:base="https://blog.eq8.eu/article/responsibility-on-rails.html"><![CDATA[<p><img src="https://raw.githubusercontent.com/equivalent/equivalent.github.io/master/assets/2022/responsibility.jpeg" alt="" /></p>

<p>November 2022 I’ve celebrated 13 years of being a Ruby on Rails developer. It just feels like a good time to get something off my chest. Here’s the story:</p>

<p>One project I’ve worked for was originally developed by a dude who used <a href="https://rubyonrails.org/">Ruby on Rails</a> for the first time in his life. Don’t get me wrong, there’s nothing wrong with that, in fact I encourage everyone to choose Ruby on Rails for their personal dream or an experiment.</p>

<p>Problem is that this dude was hired as a super expensive contractor with tons of experience but that experience was all with different programming languages. After 6 months his contract expired and he moved on to a different  project probably trying out different web development tools.</p>

<p>As I’ve been hired to take over the project from where he left I can truthfully testify the dude has never read a single Ruby on Rails book or design pattern book. I’m not kidding when I say some controllers were 4000 lines and the entire codebase was without a single test. The burden of his “I will learn on the job” attitude is that the company inherited a barely functional mess.</p>

<p>This may not be a problem if one accepts a contract for a company that can afford to throw money left and right. But it’s just evil if you do this to a fresh startup that just managed to scrape a couple of thousands dollars to make their dream real.</p>

<p>And this article is a call to the moral side of all fellow web-developer colleagues: Our job is not just about the money. Sometimes people’s dreams are at stake. Sometimes people take personal loans and draw their life savings to make those projects real.</p>

<p>If you’re planning to accept a greenfield project honestly ask yourself: Are my skills up to the task?</p>

<p>I don’t want this article hanging on a plea. Here are my personal opinions what is the solution</p>

<h3 id="learn-but-dont-forget-to-learn-responsibility">Learn, but don’t forget to learn responsibility</h3>

<p>Look, I’m not trying to bring down anyone who wants to learn. Far from it. I think everyone should explore and experiment. Technology my project uses may not be the best match for your project and vice versa. We need to broaden our horizons.</p>

<p>It’s more about the terms of this exploration and experimentation.</p>

<p>Many companies allow their employees to play around with new technologies and that’s good. They either allow side projects or dedicate separate environments for the experiment.</p>

<p>Many companies can’t afford it (e.g.: fresh startups)</p>

<p>Many companies can afford it but straight up refuse to invest in such things</p>

<blockquote>
  <p>There are usually some fundamental issues with those kinds of projects and good developers don’t stick around for very long or the projects fail on their own. I worked for a company where the manager was frequently telling us he would not pay us to write tests. Yeah, that project didn’t last long.</p>
</blockquote>

<p>Whatever the case you need to be responsible with anything you introduce. You want to introduce a new gem or JS library? You want to try a new code design pattern? You want to split monolith into microservices? You want to implement a new DevOps tool? Ok cool, but how long are you planning to stay on the project and carry on this decision? 5 years? 5 months? 5 weeks ? Who will carry on this  responsibility after you leave?</p>

<p>Last project I worked for I worked there as a Lead for 7 years. I’ve learned the word Responsibility with capital R.</p>

<p>So what if my company doesn’t allow me to experiment? Should I just experiment in my free time without anyone paying me for it ? …well, yes.</p>

<h3 id="creative-process">Creative process</h3>

<p>I’ve heard music producer <a href="https://en.wikipedia.org/wiki/Rick_Rubin">Rick Rubin</a> talk about the creative process of Eminem:</p>

<blockquote>
  <p>99% of what he writes is never used. He does this just to stay engaged in the creative process of writing and finding new ways to write. He does this so that when he needs it, it just comes.</p>
</blockquote>

<p>Look at my <a href="https://github.com/equivalent?tab=repositories">Github profile</a>. It’s a mess! At the time of writing this article I have around 180 public repos of which 89 are sources (I have many many more private ones). I  really need like 10% of them and rest are about the process.</p>

<p>New design pattern? Create a dummy project to test it out. Interesting deployment solution? Create a dummy project and try it out. New programming language I would like to try? Create a repo and try it out,…</p>

<p>I create all that mess so that once I’m paid to deliver a solution I already done the drafts and it’s time to bring real rhymes.</p>

<p>Same applies to my attitude towards OpenSource projects. Many times I fork a repo, start writing an improvement and abandon the work. Many time I realize my idea is stupid, many times I just don’t have the time to finish it to a real pull request. Finished or not finished, I win either way as I absorb new coding styles, thinking patterns, …new beats.</p>

<p>Look if you are doing this 9 to 5 just to get paid I have no more arguments for you. I’m surprised you made it this far reading this. I’m surprised you are reading an article no one is paying you to read.</p>

<p>Throughout my career I’ve met true web developer artists both in Frontend and Backend. People who enjoy what they create and love the craft. I salute to all of you.</p>

<p>For the rest of you at least think about it.</p>

<h3 id="convention">Convention</h3>

<p>You need to realize that some companies or teams are about consistency with their approach.</p>

<p>For example look at what <a href="https://dhh.dk/">DHH</a> and good folks at <a href="https://dev.37signals.com/">37 Signals</a> (creators of Ruby on Rails) have been preaching about all these years. <a href="https://m.signalvnoise.com/the-majestic-monolith/">Monolith over microservices</a>, <a href="https://dev.37signals.com/vanilla-rails-is-plenty/">limit the use of service objects</a>, <a href="https://hotwired.dev/">limit the use of JavaScript</a>, …</p>

<p>They know about code designs, SOLID principles, popular architecture opinions and all the cool JS libraries out there. However they’ve deliberately chosen to limit their toolset so that everyone using vanilla Rails is on the same page.</p>

<p>This is not just for some idealistic open source strategy. They’ve decided to stick with this convention so that a junior developer creating his first Ruby on Rails project has the same tools for success as what they use on their products (<a href="https://basecamp.com/">Basecamp</a> &amp; <a href="https://www.hey.com/">Hey</a>). My friends, that takes a lot of courage!</p>

<blockquote>
  <p>Understanding the source code behind Rails will take you to the next level. Understanding decisions behind Rails will take you to the next dimension.</p>
</blockquote>

<p>So remember next time the lead engineer in your company doesn’t allow you to install a cool gem or use a new revolutionary JavaScript library: Maybe it’s not because he/she is against your career “growth”. Maybe it’s because that decision would carry a burden that the whole team will have to carry for next decade.</p>

<p>The convention is sometimes better than what’s cool.</p>

<h3 id="discussion">Discussion</h3>

<ul>
  <li>Reddit discussion
    <ul>
      <li><a href="https://www.reddit.com/r/ruby/comments/zftjxq/responsibility_on_rails/">r/ruby</a></li>
      <li><a href="https://www.reddit.com/r/rails/comments/zfvf8n/responsibility_on_rails_projects/">r/rails</a></li>
      <li><a href="https://www.reddit.com/r/programming/comments/zfw8su/learning_learning_responsibility/">r/programming</a></li>
      <li><a href="https://www.reddit.com/r/webdev/comments/zfvjyr/learning_learning_responsibility/">r/webdev</a></li>
    </ul>
  </li>
  <li><a href="https://dev.to/equivalent/responsibility-on-rails-3091">Article mirror on Dev.to</a></li>
  <li><a href="https://rubyflow.com/p/jpfve5-responsibility-on-rails">RubyFlow</a></li>
</ul>]]></content><author><name>EquiValent - Tomas Valent</name></author><category term="article" /><summary type="html"><![CDATA[Being a web-developer is one of the coolest jobs in the world. It brings freedom and growth opportunities like no other job in the world. It’s easy to just enjoy the fruits of this craft. But remember with great benefits comes great responsibility.]]></summary></entry><entry><title type="html">Elasticsearch 7 under Ubuntu - protect with basic password</title><link href="https://blog.eq8.eu/til/elasticsearch-7-protect-with-basic-password-ubuntu.html" rel="alternate" type="text/html" title="Elasticsearch 7 under Ubuntu - protect with basic password" /><published>2022-08-16T00:00:00+00:00</published><updated>2022-08-16T00:00:00+00:00</updated><id>https://blog.eq8.eu/til/elasticsearch-7-protect-with-basic-password-ubuntu</id><content type="html" xml:base="https://blog.eq8.eu/til/elasticsearch-7-protect-with-basic-password-ubuntu.html"><![CDATA[<p>Set up simple password for ElasticSearch <code>7.17.5</code> localhost running under Ubuntu 20.04 from standard atp-get instalation (<a href="https://blog.eq8.eu/article/set-up-ubuntu-1804-for-rails-developer-2019.html">example</a>)</p>

<p>` sudo vim /etc/elasticsearch/elasticsearch.yml`</p>

<pre><code class="language-yaml"># .....
# xpack.security.enabled: false  # make sure this is commented


discovery.type: single-node
xpack.security.enabled: true

</code></pre>

<pre><code class="language-bash">sudo service elasticsearch stop 
sudo service elasticsearch status  
sudo service elasticsearch start  
sudo service elasticsearch status  
</code></pre>

<p>to set up password:</p>

<pre><code class="language-bash">$ sudo  /usr/share/elasticsearch/bin/elasticsearch-setup-passwords interactive
</code></pre>

<p>Let say I hoose a pasword <code>xxmypaswdxx</code> :</p>

<p>test</p>

<pre><code class="language-bash">$  curl  -XGET localhost:9200

{"error":{"root_cause":[{"type":"security_exception","reason":"missing authentication credentials for REST request [/]","header":{"WWW-Authenticate":"Basic realm=\"security\" charset=\"UTF-8\""}}],"type":"security_exception","reason":"missing authentication credentials for REST request [/]","header":{"WWW-Authenticate":"Basic realm=\"security\" charset=\"UTF-8\""}},"status":401




$  curl --user elastic:xxmypaswdxx -XGET localhost:9200

{
  "name" : "xxxxxxxx",
  "cluster_name" : "xxxxxxxx",
  "cluster_uuid" : "FWhvJOvmTCmp_Nevybmb2g",
  "version" : {
    "number" : "7.17.5",
    "build_flavor" : "default",
    "build_type" : "deb",
    "build_hash" : "8d61b4f7ddf931f219e3745f295ed2bbc50c8e84",
    "build_date" : "2022-06-23T21:57:28.736740635Z",
    "build_snapshot" : false,
    "lucene_version" : "8.11.1",
    "minimum_wire_compatibility_version" : "6.8.0",
    "minimum_index_compatibility_version" : "6.0.0-beta1"
  },
  "tagline" : "You Know, for Search"
}
</code></pre>

<p>you can also <code>base64(username:pssword)</code> eg and pass it as header. E.g.: <code>base64(elastic:xxmypaswdxx) = "ZWxhc3RpYzp4eG15cGFzd2R4eA=="</code></p>

<pre><code class="language-bash">$ curl -H 'Authorization: Basic ZWxhc3RpYzp4eG15cGFzd2R4eA==' -XGET localhost:9200

{
  "name" : "xxxxxxxx",
  ...
}
</code></pre>

<pre><code class="language-bash">$  curl  -XGET http://elastic:xxmypaswdxx@localhost:9200

{
  "name" : "xxxxxxxx",
  ...
}

</code></pre>

<h3 id="ruby-on-rails">Ruby on Rails</h3>

<p>Most imortant for <a href="https://github.com/elastic/elasticsearch-rails">Ruby/Rails ElasticSearch Client gem</a> you can pass it as a host, that means in Rails you can:</p>

<pre><code class="language-ruby"># config/initializers/elasticsearch.rb
  client = Elasticsearch::Client.new(url: ENV.fetch('ELASTICSEARCH_HOST') )
</code></pre>

<p>make sure your <code>ENV['ELASTICSEARCH_HOST']="http://elastic:xxmypaswdxx@localhost:9200"</code></p>

<h3 id="sources">sources</h3>

<ul>
  <li>https://www.elastic.co/guide/en/elasticsearch/reference/7.17/security-minimal-setup.html</li>
  <li>https://www.elastic.co/guide/en/elasticsearch/reference/current/http-clients.html</li>
</ul>

<h3 id="discusion">discusion</h3>]]></content><author><name>EquiValent - Tomas Valent</name></author><category term="til" /><summary type="html"><![CDATA[Set up simple password for ElasticSearch 7.17.5 localhost running under Ubuntu 20.04 from standard atp-get instalation (example)]]></summary></entry><entry><title type="html">Use Importmaps without Rails</title><link href="https://blog.eq8.eu/til/use-importmaps-without-rails.html" rel="alternate" type="text/html" title="Use Importmaps without Rails" /><published>2022-05-09T00:00:00+00:00</published><updated>2022-05-09T00:00:00+00:00</updated><id>https://blog.eq8.eu/til/use-importmaps-without-rails</id><content type="html" xml:base="https://blog.eq8.eu/til/use-importmaps-without-rails.html"><![CDATA[<p><img src="https://images.unsplash.com/photo-1621839673705-6617adf9e890?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1332&amp;q=80" alt="" /></p>

<p>Rails 7 embraced the use of <a href="https://github.com/rails/importmap-rails">Import maps</a> and they are awesome.</p>

<p>If you wonder how to use importmap in plain HTML here is an example:</p>

<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
  &lt;head&gt;
    &lt;meta charset="UTF-8" /&gt;
    &lt;title&gt;Import maps without Rails - Local-time example&lt;/title&gt;

    &lt;script async src="https://unpkg.com/es-module-shims@1.2.0/dist/es-module-shims.js"&gt;&lt;/script&gt;
    &lt;script type="importmap-shim"&gt;
      {
        "imports": {
          "local-time": "https://ga.jspm.io/npm:local-time@2.1.0/app/assets/javascripts/local-time.js"
        }
      }
    &lt;/script&gt;
    &lt;script type="module-shim"&gt;
      import LocalTime from "local-time"
      LocalTime.start()
    &lt;/script&gt;

    &lt;style&gt;
      time { color: #c11; font-size: 1.1em; }
    &lt;/style&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;h1&gt;Import maps without Rails - Local-time JS example&lt;/h1&gt;

    &lt;p&gt;
      Last time I had chocolate was &lt;time datetime="2022-05-08T23:00:00+02:00" data-local="time-ago"&gt;8th of May&lt;/time&gt;
    &lt;/p&gt;

  &lt;/body&gt;
&lt;/html&gt;
</code></pre>

<blockquote>
  <p>to see the example in action check <a href="https://jsfiddle.net/8oa9fjbs/">this JS fiddle</a></p>
</blockquote>

<p>Example uses importmap to loads <a href="https://www.npmjs.com/package/local-time">local-time js</a>
that converts <code>&lt;time&gt;</code> HTML elements from UTC to the browser’s local time (<a href="https://github.com/basecamp/local_time">more info</a>).</p>

<h3 id="other-examples">Other examples</h3>

<p>Looking for Hotwire Stimulus examples ?</p>
<ul>
  <li>Some can be found in <a href="https://github.com/afcapel/stimulus-autocomplete/tree/main/examples">stimulus-autocomplete gem examples</a></li>
</ul>

<h3 id="source">Source</h3>

<ul>
  <li>Photo by Jackson So via <a href="https://unsplash.com/photos/_t-l5FFH8VA">unsplash</a></li>
  <li><a href="https://www.npmjs.com/package/local-time">https://www.npmjs.com/package/local-time</a></li>
  <li><a href="https://github.com/basecamp/local_time">https://github.com/basecamp/local_time</a></li>
  <li><a href="https://github.com/afcapel/stimulus-autocomplete/tree/main/examples">https://github.com/afcapel/stimulus-autocomplete/tree/main/examples</a></li>
  <li><a href="https://github.com/rails/importmap-rails">https://github.com/rails/importmap-rails</a></li>
</ul>

<h3 id="discussion">Discussion</h3>

<ul>
  <li><a href="https://www.reddit.com/r/rails/comments/ulq85p/use_importmaps_without_rails_pure_html_example/">https://www.reddit.com/r/rails/comments/ulq85p/use_importmaps_without_rails_pure_html_example/</a></li>
</ul>]]></content><author><name>EquiValent - Tomas Valent</name></author><category term="til" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Simple way how to use Bootstrap 5 in Rails 7 - importmaps &amp;amp; sprockets</title><link href="https://blog.eq8.eu/til/how-to-use-bootstrap-5-in-rails-7.html" rel="alternate" type="text/html" title="Simple way how to use Bootstrap 5 in Rails 7 - importmaps &amp;amp; sprockets" /><published>2022-04-28T00:00:00+00:00</published><updated>2022-04-28T00:00:00+00:00</updated><id>https://blog.eq8.eu/til/how-to-use-bootstrap-5-in-rails-7</id><content type="html" xml:base="https://blog.eq8.eu/til/how-to-use-bootstrap-5-in-rails-7.html"><![CDATA[<p><img src="https://images.unsplash.com/photo-1615752865424-62638daceeae?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=2064&amp;q=80" alt="" /></p>

<p>Rails 7 is a breath of fresh air. Thanks to
<a href="https://github.com/rails/importmap-rails">importmaps</a> everything is
simple again. JavaScript (JS) is easy to be implemented without the need
to install node,npm,yarn,webpack,..other 150 non-Ruby tools on your Laptop</p>

<p>But what about CSS ?</p>

<p>Well there is good old  Sprockets (a.k.a <a href="https://guides.rubyonrails.org/asset_pipeline.html">Rails asset pipeline</a>) and good old gems contanining SCSS (remember those?)</p>

<p>Let’s make life easy again</p>

<h2 id="instalation-of-bootstrap-5-in-rails-7">Instalation of Bootstrap 5 in Rails 7</h2>

<h3 id="javascript-js">JavaScript (JS)</h3>

<p>If you don’t have <a href="https://github.com/rails/importmap-rails">importmaps</a> yet in your Rails project:</p>

<pre><code class="language-bash"># to check if you already have importmaps 
$ cat config/importmap.rb

# to install importmaps in your Rails7 project
$ rails importmap:install
</code></pre>

<p>To add Bootstrap 5 JS to Rails 7 project via importmaps:</p>

<pre><code>$ bin/importmap pin bootstrap
</code></pre>

<p>…this will add necessary JS (bootstrap and popperjs)  to <code>config/importmaps.rb</code></p>

<p>Then you need to just import bootstrap in your <code>application.js</code></p>

<pre><code class="language-js">// app/javascript/application.js
// ...
import 'bootstrap'
</code></pre>

<h4 id="quick-note">Quick Note:</h4>

<blockquote>
  <p>For some reason popperjs acts broken in my Rails7 project  when I load it from
default <code>ga.jspm.io</code> CDN. That’s why I recommend to load it from <code>unpkg.com</code>:</p>
</blockquote>

<pre><code class="language-ruby"># config/importmaps.rb
# ...
pin "bootstrap", to: "https://ga.jspm.io/npm:bootstrap@5.1.3/dist/js/bootstrap.esm.js"
pin "@popperjs/core", to: "https://unpkg.com/@popperjs/core@2.11.2/dist/esm/index.js" # use unpkg.com as ga.jspm.io contains a broken popper package
# ...
</code></pre>

<h3 id="css-js">CSS (JS)</h3>

<p>To install official
<a href="https://github.com/twbs/bootstrap-rubygem">Bootstrap 5 Ruby gem</a></p>

<pre><code class="language-ruby"># Gemfile
# ...
gem 'bootstrap', '~&gt; 5.1.3'
# ...
</code></pre>

<p>and <code>bundle install</code></p>

<p>Then just edit your <code>app/assets/stylesheets/application.scss</code></p>

<pre><code class="language-scss">// app/assets/stylesheets/application.scss
// ...
@import "bootstrap";
// ...
</code></pre>

<blockquote>
  <p>note: be sure you replace your application.<strong>css</strong> with application.<strong>scss</strong>.
That means <code>app/assets/stylesheets/application.css</code> should not exist!</p>
</blockquote>

<p>If you want to change some variables:</p>

<pre><code class="language-scss">// app/assets/stylesheets/application.scss
// ...
$primary: #c11;
@import "bootstrap";
// ...
</code></pre>

<ul>
  <li><a href="https://github.com/twbs/bootstrap-rubygem/blob/master/assets/stylesheets/bootstrap/_variables.scss">list of all variables</a></li>
  <li><a href="https://github.com/twbs/bootstrap-rubygem/issues/210">advanced way how to change variables</a></li>
</ul>

<h3 id="layout-files">Layout files</h3>

<p>Make sure your layout (<code>app/views/application.html.erb</code>) contains:</p>

<pre><code class="language-erb">&lt;%# ... %&gt;
&lt;head&gt;
&lt;%# ... %&gt;
&lt;%= stylesheet_link_tag "application", "data-turbo-track": "reload" %&gt;  &lt;%# this loads Sprockets/Rails asset pipeline %&gt;
    &lt;%= javascript_importmap_tags %&gt; &lt;%#  this loads JS from importmaps %&gt;
    &lt;%# ... %&gt;
  &lt;/head&gt;
  &lt;!-- ... --&gt;
</code></pre>

<h2 id="alternative-solutions">Alternative solutions</h2>

<ul>
  <li><a href="https://dev.to/coorasse/rails-7-bootstrap-5-and-importmaps-without-nodejs-4g8">gem bootstrap and importmaps to load vendor javascript in the gem</a> - good solution if you want to avoid CDN</li>
  <li>you can use the <code>rails new --css bootstrap</code> option but that will
require <code>esbuild</code> which requires all the JS shenanigans in your laptop this article wants to
avoid</li>
  <li>you can use <a href="https://guides.rubyonrails.org/webpacker.html">webpacker</a> but again you need node,yarn,… So, have fun</li>
</ul>

<h2 id="counterarguments">counterarguments</h2>

<blockquote>
  <p>“but this way you load a gem and you don’t use the JS bit of it”</p>
</blockquote>

<p>So what? Like if there’s no single gem in your project you don’t use at 100%. I love “vanilla Rails” approach and
love to avoid 3rd party gems as much as I can but this will save you so
much hustle, especially if you are a beginner new to Rails or you are
starting a sideproject (there’s always a time to refactor if you really
need to)</p>

<blockquote>
  <p>“but Sprockets are no longer used”</p>
</blockquote>

<p>Yes they are. There was a period of time with RoR 5.2 &amp; 6.x where webpacker
was taking over and developers were ditching Rails asset pipeline but
this new importmaps approach is fresh breath to bring gems with scss
back.</p>

<p><del>Basecamp (&amp; DHH) were quite clear about it that Sprockets will not
disappear  anyday soon.</del></p>

<p><strong>update</strong> Well actually I was wrong. Sprockets will probably be
replaced by <a href="https://github.com/rails/propshaft">Propshaft</a> in Rails 8.
<a href="https://rubyrogues.com/propshaft-with-david-heinemeier-hansson-dhh-ruby-542">source of this claim</a></p>

<p>But still Sprockets are the most convinient way how to use CSS in
Rails7</p>

<blockquote>
  <p>what about DartSass</p>
</blockquote>

<p>if you decide to configure <a href="https://github.com/rails/dartsass-rails">DartSass Rails</a> go for it.</p>

<blockquote>
  <p>but <code>--css</code> (esbuild) is there to replace sprockets</p>
</blockquote>

<p>No it’s not, same way how webpacker didn’t replace it</p>

<blockquote>
  <p>But what if CDN provider goes down, then my application JS will not work</p>
</blockquote>

<p>Yes you and other billion websites as well.  If your project is a bank then yeah sure use your
own CDN or load from vendor. But if your project is
startup to sell T-shirts  then I’m pretty sure everyone will
survive that 5 min downtime.</p>

<h2 id="sources">Sources</h2>

<ul>
  <li><a href="https://www.youtube.com/watch?v=PtxZvFnL2i0">Learn more on importmaps - DHH video</a></li>
</ul>

<p>Photo by Pablo Arroyo via <a href="https://unsplash.com/photos/_SEbdtH4ZLM">unsplash</a></p>

<h2 id="discussion">Discussion</h2>

<ul>
  <li><a href="https://www.reddit.com/r/ruby/comments/udtsz8/how_to_use_bootstrap_5_in_rails_7_importmaps/">https://www.reddit.com/r/ruby/comments/udtsz8/how_to_use_bootstrap_5_in_rails_7_importmaps/</a></li>
</ul>]]></content><author><name>EquiValent - Tomas Valent</name></author><category term="til" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">How to test performance of caching with RSpec in Rails</title><link href="https://blog.eq8.eu/til/how-to-test-caching-on-individual-tests-rails-rspec.html" rel="alternate" type="text/html" title="How to test performance of caching with RSpec in Rails" /><published>2022-01-24T00:00:00+00:00</published><updated>2022-01-24T00:00:00+00:00</updated><id>https://blog.eq8.eu/til/how-to-test-caching-on-individual-tests-rails-rspec</id><content type="html" xml:base="https://blog.eq8.eu/til/how-to-test-caching-on-individual-tests-rails-rspec.html"><![CDATA[<p>e.g.: if you implemeted fragment caching or russian doll caching</p>

<pre><code>account = Account.last
Rails.cache.fetch ['posts', account] do
  # ....
end
</code></pre>

<h3 id="how-to-enable-cache-in-single-test">how to enable cache in single test</h3>

<p>NOTE by default caching is disabled in test enviroment (which is a good
thing). You <strong>don’t</strong> need to change default <code>null_store</code> caching</p>

<pre><code class="language-ruby"># config/environments/test.rb
Rails.application.configure do
  # ...
  config.cache_store = :null_store #  feel free to keep this as it is
</code></pre>

<p>What we want is enable the caching only for particular test:</p>

<pre><code class="language-ruby"># spec/any_spec.rb
module TestFileCachingHelper
  def self.cache
    return @file_cache if @file_cache
    path = "tmp/test#{ENV['TEST_ENV_NUMBER']}/cache"
    FileUtils::mkdir_p(path)
    @file_cache = ActiveSupport::Cache.lookup_store(:file_store, path)
    @file_cache
  end
end

before do
  allow(Rails).to receive(:cache).and_return(TestFileCachingHelper.cache)
  Rails.cache.clear
end

it do
  expect(Rails.cache.exist?('some_key')).to be(false)
  Rails.cache.write('some_key', 'test')
  expect(Rails.cache.exist?('some_key')).to be(true)
end
</code></pre>

<blockquote>
  <p>Credit for this part of article goes to Emanuel De and his article <a href="https://makandracards.com/makandra/46189-how-to-rails-cache-for-individual-rspec-tests">How to: Rails cache for individual rspec tests</a> Consider this as a mirror article</p>
</blockquote>

<h3 id="rspec-tag-to-mark-which-tests-should-enable-cache">RSpec tag to mark which tests should enable cache</h3>

<p>we can go step further and enable cache only on tests with specific  RSpec tag / filters</p>

<pre><code class="language-ruby"># spec/support/test_file_caching_helper.rb
module TestFileCachingHelper
  def self.cache
    return @file_cache if @file_cache
    path = "tmp/test#{ENV['TEST_ENV_NUMBER']}/cache"
    FileUtils::mkdir_p(path)
    @file_cache = ActiveSupport::Cache.lookup_store(:file_store, path)
    @file_cache
  end
end
</code></pre>

<pre><code class="language-ruby"># spec/rails_helper.rb

Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }

RSpec.configure do |config|
  # ...

  # tests that use Rails cache https://blog.eq8.eu/til/how-to-test-caching-on-individual-tests-rails-rspec.html
  config.before(:example, :cache_enabled) do
    Rails.cache.clear
    allow(Rails).to receive(:cache).and_return(TestFileCachingHelper.cache)
  end
  # ...
end

</code></pre>

<pre><code class="language-ruby"># spec/any_spec.rb
require 'rails_helper'
RSpec.describe 'Anything' do
  it 'should behave like test without cache enabled'
    # ...
  end

  it 'should behave like test with enabled cache', :cache_enabled
    # ...
  end

  context 'entire section under influence of cache', :cache_enabled do
    it 'should behave like test with cache enabled' do
      # ...
    end

    it do
      Rails.cache.fetch 'hello' { 123 }
      expect(Rails.cache.fetch('hello').to eq 123
    end
  end
end
</code></pre>

<h3 id="how-to-test-performance-of-implemented-caching">how to test performance of implemented caching</h3>

<p>Gem  <a href="https://github.com/civiccc/db-query-matchers">db-query-matchers</a>
will help you test how many SQL calls the request has made</p>

<pre><code class="language-ruby"># Gemfile
# ...
group :test do
  gem 'rspec-rails'
  # ...
  gem 'db-query-matchers'
</code></pre>

<pre><code class="language-ruby"># spec/controllers/accounts_controller_spec.rb
RSpec.describe AccountsController do
  # ...
  def trigger
    get :index
  end

  it 'is performant', :cache_enabled do
    #First call
    expect { trigger }.to make_database_queries(count: 420..430)

    # cache kicked in
    expect { trigger }.to make_database_queries(count: 7)
  end
</code></pre>

<blockquote>
  <p>note don’t use let(:trigger) { get :index } as that will memoize the
call =&gt; second call will not trigger</p>
</blockquote>

<h3 id="sources">sources</h3>

<ul>
  <li><a href="https://guides.rubyonrails.org/caching_with_rails.html">https://guides.rubyonrails.org/caching_with_rails.html</a></li>
  <li><a href="https://makandracards.com/makandra/46189-how-to-rails-cache-for-individual-rspec-tests">https://makandracards.com/makandra/46189-how-to-rails-cache-for-individual-rspec-tests</a></li>
</ul>

<h3 id="discusion">Discusion</h3>]]></content><author><name>EquiValent - Tomas Valent</name></author><category term="til" /><summary type="html"><![CDATA[e.g.: if you implemeted fragment caching or russian doll caching]]></summary></entry><entry><title type="html">Order attachments in Rails ActiveStorage has_many_attached</title><link href="https://blog.eq8.eu/article/order-attachments-in-rails-activestorage-has_many_attached.html" rel="alternate" type="text/html" title="Order attachments in Rails ActiveStorage has_many_attached" /><published>2022-01-15T00:00:00+00:00</published><updated>2022-01-15T00:00:00+00:00</updated><id>https://blog.eq8.eu/article/order-attachments-in-rails-activestorage-has_many_attached</id><content type="html" xml:base="https://blog.eq8.eu/article/order-attachments-in-rails-activestorage-has_many_attached.html"><![CDATA[<p>Ruby on Rails <a href="https://edgeguides.rubyonrails.org/active_storage_overview.html">Active Storage</a>  introduced
bunch of cool features for uploading files. One large advantage is a
simple way how to store multiple attachments for a model with
<a href="https://edgeguides.rubyonrails.org/active_storage_overview.html#has-many-attached">has_many_attached</a> but also 
ability to upload  files with <a href="https://edgeguides.rubyonrails.org/active_storage_overview.html#direct-uploads">direct upload</a></p>

<p><code>has_many_attached</code> is a cool feature but developers may feels like it’s
missing one critical feature: change order of attachments.</p>

<p>In this article I’ll show you one simple way how to order attachments of
a simple Entry model that has many pictures.</p>

<blockquote>
  <p>To limit the scope of this article I’ll  assume your application have a basic setup of
<a href="https://edgeguides.rubyonrails.org/active_storage_overview.html">ActiveStorage</a>
such as <code>bin/rails active_storage:install</code></p>
</blockquote>

<h2 id="basic-solution">Basic solution</h2>

<p>Here is our <code>Entry</code> model. As you can see it has_many_attached <code>#pictures</code></p>

<pre><code class="language-ruby"># app/models/entry.rb
class Entry &lt; ApplicationRecord
  has_many_attached :pictures

  # ...
end
</code></pre>

<p>We need to add a new Array field to the <code>Entry</code> model that will hold ids
of attached <code>pictures</code> in order. That means if attachments were uploaded
in order:</p>

<ol>
  <li><code>ActiveStorage::Attachment id=1</code></li>
  <li><code>ActiveStorage::Attachment id=2</code></li>
  <li><code>ActiveStorage::Attachment id=3</code></li>
</ol>

<p>…we can store the ids <code>[1,2,3]</code> in any order we want see them appear in e.g.: <code>[3,1,2]</code></p>

<p>Assuming we use <strong>PostgreSQL database</strong> lets add a <code>json</code> field to our
database which defalts to an empty Array</p>

<pre><code class="language-ruby">class AddOrderedPictureIdsToEntries &lt; ActiveRecord::Migration[6.1]
  def change
    add_column :entries, :ordered_picture_ids, :json, default: []
  end
end
</code></pre>

<blockquote>
  <p>if you are not using Posgres database you can use Rails model
<a href="https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html">serialize</a>
field as an Array</p>
</blockquote>

<pre><code class="language-bash">$ bin/rails db:migrate
$ bin/rails c
</code></pre>

<pre><code class="language-ruby">entry = Entry.new
entry.ordered_picture_ids
# =&gt; []
entry.ordered_picture_ids = [3,1,2]
entry.save!
entry.ordered_picture_ids
# =&gt; [3,1,2]
</code></pre>

<p>Now we will intreduce method <code>#ordered_pictures</code> which will return
<code>#pictures</code> ordered by the values in <code>#ordered_picture_ids</code></p>

<pre><code class="language-ruby"># app/models/entry.rb
class Entry &lt; ApplicationRecord
  has_many_attached :pictures


  def ordered_pictures
    pictures.sort_by{ |pic| ordered_picture_ids.index(pic.id) || (pic.id*100) }
  end

  def ordered_picture_ids=(ids)
    super(ids.map(&amp;:to_i)) # convert any ids passed to this method to integer
                           # this is just for security reasons,
                           # you don't need to do this for the feature to work
  end
end
</code></pre>

<blockquote>
  <p>reason why we do <code>|| (pic.id*100)</code> is so that we give default order to records without explicit order (E.g stuff that was uploaded before we start changing order).  Please have a look at RSpec specs bellow to fully understand edgecases</p>
</blockquote>

<p>Great now when you call <code>entry.ordered_pictures</code> you will get attached
pictures in order you like:</p>

<pre><code class="language-slim">-# app/views/entries/edit.html.slim

- @entry.ordered_pictures.each do |picture|
  = image_tag(picture)

</code></pre>

<p>If you use some JavaScript solution (e.g drag and drop sort) that will send  an Array of ids to your backend then
this is all you need. Just update your controller to allow our new <code>#ordered_picture_ids</code> property in <code>params</code></p>

<pre><code class="language-ruby">class EntriesController &lt; ApplicationController
  # ...
  def update
    entry_params = params
      .require(:entry)
      .permit(:title, pictures: [], ordered_picture_ids: [])

    @entry.attributes = entry_params

    @entry.save
    # ...
  end
end
</code></pre>

<h2 id="move-up-and-down">Move up and down</h2>

<p>Rails 7 introduced <a href="https://turbo.hotwired.dev/">Hotwire Turbo</a> which
makes developers that prefere  to write as little of JavaScript as
possible (like me) extremly happy.</p>

<p>With this technology a really elegant solution would be to have buttons that would
change order of attachments <strong>Up</strong> or <strong>Down</strong> within same turbo frame. Let’s have a look how
this would look like:</p>

<p><img src="/assets/2022/reorder.gif" alt="result" /></p>

<h4 id="model">Model</h4>

<p>First let’s introduce methods for moving attachement picture:</p>

<pre><code class="language-ruby"># app/models/entry.rb
class Entry &lt; ApplicationRecord
  has_many_attached :pictures

  # ...

  def ordered_pictures
    pictures.sort_by{ |pic| ordered_picture_ids.index(pic.id) || (pic.id*100) }
  end

  def ordered_picture_ids=(ids)
    super(ids.map(&amp;:to_i))
  end

  def ordered_picture_move_up!(picture)
    ordered_picture_move!(picture, :up)
  end

  def ordered_picture_move_down!(picture)
    ordered_picture_move!(picture, :down)
  end

  private
    def ordered_picture_move!(picture, where)
      raise TypeError, "#{picture} must be a ActiveStorage::Attachment" unless picture.is_a?(ActiveStorage::Attachment)
      pics = ordered_pictures.dup
      case where
      when :up   then ArrayElementMove.up!(pics, picture)
      when :down then ArrayElementMove.down!(pics, picture)
      else
        raise "unknown option #{where}"
      end
      self.ordered_picture_ids=pics.map(&amp;:id)
      self.save!
      self.reload
      true
    end
end
</code></pre>

<p>Methods <code>#ordered_pictures</code> and <code>#ordered_picture_ids</code> didn’t change
compared to previous example.
We introduced two more methods <code>#ordered_picture_move_up!</code> and <code>#ordered_picture_move_down!</code>
that will be our interface to move items up and down.</p>

<p>Both uses private method <code>#ordered_picture_move</code> that will manipulate
order of pictures ids in array <code>#ordered_picture_ids</code> and save new order to
this field</p>

<blockquote>
  <p>For details please see RSpec specs at the bottom of this article</p>
</blockquote>

<p>Tricky bit here is how to move items up and down in a Ruby Array. As far
as I’m aware there is no built in feature directly in Ruby so I
 created a small helper <code>ArrayElementMove</code> to do that:</p>

<pre><code class="language-ruby"># lib/array_element_move.rb
module ArrayElementMove
  MustBeUniqArray = Class.new(StandardError)
  ItemNotInArray  = Class.new(StandardError)

  def self.up!(array, item)
    self.check_if_uniq!(array)
    return array if array.first == item
    position = array.index(item) || raise(ItemNotInArray)
    array.insert((position - 1), array.delete_at(position))
  end

  def self.down!(array, item)
    self.check_if_uniq!(array)
    return array if array.last == item
    position = array.index(item) || raise(ItemNotInArray)
    array.insert((position + 1), array.delete_at(position))
  end

  def self.check_if_uniq!(array)
    raise MustBeUniqArray if array.size != array.uniq.size
  end
end
</code></pre>

<blockquote>
  <p>more about this Class in <a href="https://blog.eq8.eu/til/move-position-of-item-in-array-up-and-down-in-ruby-lang.html">this til note</a></p>
</blockquote>

<p>Don’t forget to require this class in your Rails app:</p>

<pre><code class="language-ruby"># config/application.rb
# ...
require './lib/array_element_move'
# ...
</code></pre>

<p>So this will allow us to do:</p>

<pre><code class="language-ruby">a = [1,2,3]
ArrayElementMove.up!(a, 2)
a == [2,1,3]
</code></pre>

<h4 id="controller--views">Controller &amp; views</h4>

<p>Let’s introduce seperate controller <code>EntryPicturesController</code> that will
be responsible for operations related to entry pictures (so that we don’t
polute <code>EntryController</code>)</p>

<pre><code class="language-ruby"># config/routes.rb
resources :entries do
  resources :pictures, only: [:destroy], controller: 'entry_pictures' do
    post :up,   on: :member
    post :down, on: :member
  end
end
</code></pre>

<pre><code class="language-ruby"># app/controllers/entry_pictures_controller.rb
class EntryPicturesController &lt; ApplicationController
  before_action :find_entry
  before_action :find_picture

  def up
    @entry.ordered_picture_move_up!(@picture)
    redirect_to(edit_entry_path(@entry))
  end

  def down
    @entry.ordered_picture_move_down!(@picture)
    redirect_to(edit_entry_path(@entry))
  end

  # not required, just to show why it's nice to separate concerns
  def destroy
    @picture.purge
    redirect_to(edit_entry_path(@entry))
  end

  private
    def find_entry_id
      @entry = Entry.find(params[:entry_id])
    end

    def find_picture
      @picture = @entry.pictures.find(params[:id])
    end
end
</code></pre>

<pre><code class="language-slim">-# app/views/entries/edit.html.slim

= turbo_frame_for 'pictures' do
  - @entry.ordered_pictures.each do |picture|
    div.entry-picture
      = image_tag(picture)
      = button_to 'move left', up_entry_picture_path(@entry, picture)
      = button_to 'move right', down_entry_picture_path(@entry, picture)
      = button_to 'Delete', entry_picture_path(@entry, picture), method: :delete, data: {confirm: 'Delete picture?'}

</code></pre>

<p>That’s it</p>

<h2 id="final-words">Final words</h2>

<p>If you are here just for technical solution you don’t have to read
further. I just want to close this article with some opinions.</p>

<h4 id="why-activestorage-has_many_attached-dont-have-built-in-ordering-">Why ActiveStorage has_many_attached don’t have built in ordering ?</h4>

<p>I don’t know.</p>

<p>I personally think this feature is missing from ActiveStorage by design
because your application may have a different iterpretation on how to
order attachments.</p>

<p>For example maybe within same has_many_attached your application is ordering PDFs in front of
images.</p>

<p>So it sounds straight forward but order logic may have many meanings</p>

<h4 id="wouldnt-be-custom-picture-model-better-">Wouldn’t be custom Picture model better ?</h4>

<p>So imagine we do something like:</p>

<pre><code class="language-ruby"># app/models/entry.rb
class Entry &lt; ApplicationRecord
  has_many :pictures
end

class Picture &lt; ApplicationRecord
  belongs_to :entry
  has_one_attached :image
end
</code></pre>

<p>In this case our <code>pictures</code> table can be more dynamic and have an order
field upon which we can  do our re-ordering:</p>

<pre><code class="language-ruby"># db/schema.rb
# ...
  create_table "pictures", force: :cascade do |t|
    t.bigint "entry_id"
    t.integer "order", default: 0
    # ...
</code></pre>

<p>Yes sure this is a good solution (I’m using simmilar solutions plenty in
other projects) So if it works for you go ahead. Just realize you are
giving up native ActiveStorage has_many_attached features that come
default in Rails (like no sweat direct upload). If that’s not a big deal
for you then no problem.</p>

<h3 id="rspec-specs">RSpec specs</h3>

<pre><code class="language-ruby"># spec/model/entry_spec.rb
require 'rails_helper'

RSpec.describe Entry, type: :model do
  describe 'ordered_pictures' do
    let!(:entry) { create :entry, :with_pictures }

    before do
      @pic1, @pic2, @pic3 = entry.pictures
    end

    context 'when no exact order' do
      it do
        expect(entry.ordered_pictures).to eq([@pic1, @pic2, @pic3])
      end

      describe 'up' do
        it do
          expect(entry.ordered_pictures).to eq([@pic1, @pic2, @pic3])

          entry.ordered_picture_move_up!(@pic3)
          expect(entry.ordered_pictures).to eq([@pic1, @pic3, @pic2])

          entry.ordered_picture_move_up!(@pic3)
          expect(entry.ordered_pictures).to eq([@pic3, @pic1, @pic2])

          entry.ordered_picture_move_up!(@pic3)
          expect(entry.ordered_pictures).to eq([@pic3, @pic1, @pic2])

          entry.ordered_picture_move_up!(@pic2)
          expect(entry.ordered_pictures).to eq([@pic3, @pic2, @pic1])
        end

        it 'check type' do
          expect { entry.ordered_picture_move_up!(@pic2.blob) }
            .to raise_exception(TypeError, /ActiveStorage::Blob/)
          expect(entry.ordered_pictures).to eq([@pic1, @pic2, @pic3])
        end
      end

      describe 'down' do
        it do
          expect(entry.ordered_pictures).to eq([@pic1, @pic2, @pic3])

          entry.ordered_picture_move_down!(@pic1)
          expect(entry.ordered_pictures).to eq([@pic2, @pic1, @pic3])

          entry.ordered_picture_move_down!(@pic1)
          expect(entry.ordered_pictures).to eq([@pic2, @pic3, @pic1])

          entry.ordered_picture_move_down!(@pic1)
          expect(entry.ordered_pictures).to eq([@pic2, @pic3, @pic1])

          entry.ordered_picture_move_down!(@pic2)
          expect(entry.ordered_pictures).to eq([@pic3, @pic2, @pic1])
        end

        it 'check type' do
          expect { entry.ordered_picture_move_down!(2) }
            .to raise_exception(TypeError, "2 must be a ActiveStorage::Attachment")
          expect(entry.ordered_pictures).to eq([@pic1, @pic2, @pic3])
        end
      end
    end

    context 'when order' do
      it do
        entry.ordered_picture_ids = [@pic2.id, @pic3.id, @pic1.id]
        expect(entry.ordered_pictures).to eq([@pic2, @pic3, @pic1])
      end
    end

    context 'when order with mistakes' do
      it do
        entry.ordered_picture_ids = [@pic2.id, nil, @pic3.id, 'poop', @pic1.id]
        expect(entry.ordered_pictures).to eq([@pic2, @pic3, @pic1])
      end
    end

    context 'when order but element missing' do
      it do
        entry.ordered_picture_ids = [@pic2.id, @pic1.id]
        expect(entry.ordered_pictures).to eq([@pic2, @pic1, @pic3])
      end
    end

    context 'when order but element missing' do
      it do
        entry.ordered_picture_ids = [@pic2.id]
        expect(entry.ordered_pictures).to eq([@pic2, @pic1, @pic3])
      end
    end
  end
end
</code></pre>

<pre><code class="language-ruby"># spec/lib/array_element_move_spec.rb
require 'rails_helper'
RSpec.describe ArrayElementMove do
  let(:arr) { [1,2,3,4,5,6] }

  it do
    ArrayElementMove.up!(arr, 4)
    expect(arr).to eq([1,2,4,3,5,6])

    expect(ArrayElementMove.up!(arr, 4)).to eq([1,4,2,3,5,6])
    expect(arr).to eq([1,4,2,3,5,6])

    ArrayElementMove.up!(arr, 4)
    expect(arr).to eq([4,1,2,3,5,6])

    ArrayElementMove.up!(arr, 4)
    expect(arr).to eq([4,1,2,3,5,6])
  end

  it do
    ArrayElementMove.down!(arr, 4)
    expect(arr).to eq([1,2,3,5,4,6])

    expect(ArrayElementMove.down!(arr, 4)).to eq([1,2,3,5,6,4])
    expect(arr).to eq([1,2,3,5,6,4])

    expect(ArrayElementMove.down!(arr, 4)).to eq([1,2,3,5,6,4])
    expect(arr).to eq([1,2,3,5,6,4])
  end

  context 'when non uniq array' do
    let(:arr) { [1,4,2,3,4,5,6] }

    it do
      expect { ArrayElementMove.down!(arr, 3) }.to raise_exception(ArrayElementMove::MustBeUniqArray)
      expect(arr).to eq([1,4,2,3,4,5,6])
    end

    it do
      expect { ArrayElementMove.down!(arr, 3) }.to raise_exception(ArrayElementMove::MustBeUniqArray)
      expect(arr).to eq([1,4,2,3,4,5,6])
    end
  end

  context 'when non existing item' do
    it do
      expect { ArrayElementMove.up!(arr, 9) }.to raise_exception(ArrayElementMove::ItemNotInArray)
      expect(arr).to eq([1,2,3,4,5,6])
    end

    it do
      expect { ArrayElementMove.up!(arr, 9) }.to raise_exception(ArrayElementMove::ItemNotInArray)
      expect(arr).to eq([1,2,3,4,5,6])
    end
  end
end
</code></pre>

<h3 id="related-articles">Related articles</h3>

<ul>
  <li><a href="https://blog.eq8.eu/til/rails-activestorage-aws-s3-bucket-policy-permissions.html">https://blog.eq8.eu/til/rails-activestorage-aws-s3-bucket-policy-permissions.html</a></li>
  <li><a href="https://blog.eq8.eu/til/rails-active-storage-cdn.html">https://blog.eq8.eu/til/rails-active-storage-cdn.html</a></li>
  <li><a href="https://blog.eq8.eu/til/image-width-and-height-in-rails-activestorage.html">https://blog.eq8.eu/til/image-width-and-height-in-rails-activestorage.html</a></li>
  <li><a href="https://blog.eq8.eu/til/rails-active-storage-crop-and-resize.html">https://blog.eq8.eu/til/rails-active-storage-crop-and-resize.html</a></li>
  <li><a href="https://blog.eq8.eu/til/upload-remote-file-from-url-with-activestorage-rails.html">https://blog.eq8.eu/til/upload-remote-file-from-url-with-activestorage-rails.html</a></li>
  <li><a href="https://blog.eq8.eu/til/factory-bot-trait-for-active-storange-has_attached.html">https://blog.eq8.eu/til/factory-bot-trait-for-active-storange-has_attached.html</a></li>
  <li><a href="https://blog.eq8.eu/til/ruby-on-rails-active-storage-how-to-change-host-for-url_for.html">https://blog.eq8.eu/til/ruby-on-rails-active-storage-how-to-change-host-for-url_for.html</a></li>
  <li><a href="https://blog.eq8.eu/til/move-position-of-item-in-array-up-and-down-in-ruby-lang.html">https://blog.eq8.eu/til/move-position-of-item-in-array-up-and-down-in-ruby-lang.html</a></li>
</ul>

<h3 id="discussion">Discussion</h3>

<ul>
  <li><a href="https://www.reddit.com/r/ruby/comments/s4w02y/how_to_change_order_of_attachments_in_rails/">Reddit</a></li>
</ul>]]></content><author><name>EquiValent - Tomas Valent</name></author><category term="article" /><summary type="html"><![CDATA[How to change order of Active Storage has_many_attached attachments]]></summary></entry></feed>