Maksym Prokopov personal blog
Idea is a something worth sharing

Adding PDF previews to ActionText with ActiveStorage

15.09.2025

Rails is a great framework, once you stop thinking in Java patterns and embraice DHH way of writing web apps.

Interestingly, Basecamp uses exactly the same ActiveStorage implementation, but shows full-size and download links alone with the PDF attachment. But if you only follow the guide, it’s hard to implement by your own without some gotchas.

These gotchas I want to capture here in this how-to article. It assumes reader followed official guide and stuck with PDF thumbnails preview implementation.

PDF Previews

For download and view full-size links, standard template should be improved.

Create views/active_storage/preview/_blob.html.erb

<figure class="attachment attachment--<%= blob.representable? ? "preview" : "file" %> attachment--<%= blob.filename.extension %>">
  <% if blob.representable? %>
  <%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
  <% end %>

  <figcaption class="attachment__caption">
    <% if caption = blob.try(:caption) %>
    <%= caption %>
    <% else %>
    <span class="attachment__name"><%= blob.filename %></span>
    <span class="attachment__size"><%= number_to_human_size blob.byte_size %></span>
    <span class="attachment__attribute attachment__attribute--download">
      <%= link_to 'View full-size', rails_blob_path(blob), target: '_blank' %>
    </span>
    <span class="attachment__attribute attachment__attribute--download">
      <%= link_to 'Download', rails_blob_path(blob, disposition: "attachment"), target: '_blank' %>
    </span>
    <% end %>
  </figcaption>
</figure>

Dockerfile

Official documentation mentions Poppler project as a dependency.

So, don’t forget to install poppler-utils in Dockerfile

RUN apt-get update --fix-missing -qq && apt-get install -y build-essential nodejs libcurl4 git-core \
    libxml2-dev libmariadb-dev imagemagick libyaml-dev poppler-utils

For macOS it’s as simple as brew install poppler

ApplicationController

in application controller include SetCurrent concern, to enable local disk support.

  include ActiveStorage::SetCurrent

During the deployment, if you use Amazon S3, it’s important to set CORS policy, otherwise the preview will fail

Mailer Previews

In mailer preview, the helper rails_blob_path can’t be found, so let’s address it with the full path

Rails.application.routes.url_helpers.rails_blob_path(blob)

End snippet is

<figure class="attachment attachment--<%= blob.representable? ? "preview" : "file" %> attachment--<%= blob.filename.extension %>">
    <% if blob.representable? %>
        <%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]).processed.url %>
  <% end %>

  <figcaption class="attachment__caption">
    <% if caption = blob.try(:caption) %>
      <%= caption %>
    <% else %>
      <span class="attachment__name"><%= blob.filename %></span>
      <span class="attachment__size"><%= number_to_human_size blob.byte_size %></span>
      <span class="attachment__attribute attachment__attribute--download">
          <%= link_to 'View full-size', Rails.application.routes.url_helpers.rails_blob_path(blob) %>
      </span>
      <span class="attachment__attribute attachment__attribute--download">
          <%= link_to 'Download', Rails.application.routes.url_helpers.rails_blob_path(blob, disposition: "attachment") %>
      </span>
    <% end %>
  </figcaption>
</figure>

CORS configuration

[
    {
        "AllowedHeaders": [
            "Origin",
            "Content-Type",
            "Content-MD5",
            "Content-Disposition"
        ],
        "AllowedMethods": [
            "PUT"
        ],
        "AllowedOrigins": [
            "*.it-premium.internal"
        ],
        "ExposeHeaders": [
            "Access-Control-Allow-Origin"
        ]
    }
]

Stop writing data migrations

15.09.2025

As many of us, I was writing data migrations as a part of database schema migrations.

That’s a bad practice, and here is how I perform the same nowadays.

Example

Migration from the plain text fields to rich text fields with ActiveText. How?

  1. Create a migration to add action_text_rich_texts table.
  2. Create a migration to rename old column containing plain text to the field prefixed with old_
  3. Write a rake task to convert the data from old column to the new place.
  4. Write a migration to remove old content after everything went smooth

Steps 1-2-3 are done within a single new PR. Once the schema is in the new state, I perform rake tasks for data movement.

The subsequent PR just tears down old fields and voila.

Travel Time and Date in Rails Console

28.06.2025

Adding RSpec support to VSCode

in Gemfile, and Ruby-LSP from Shopify

group :development do
  gem "ruby-lsp-rspec", require: false
  gem "ruby-lsp-rails", require: false
end
include ActiveSupport::Testing::TimeHelpers

travel_to 2.days.ago

Legacy Rails project and modern Assets Pipeline

22.05.2025

One of the projects I’ve started a long-long time ago still powers the business. It was quite a journey starting from Rails 2, than Rails 3 and so on up until currently Rails 7 with plans to bump to Rails 8.

The project has live updates with Server Sent Events and a sidecar microservice to keep the connections and push updates. Quite similar to what DHH have done with ActiveCable, but based on different stack.

Here is a list of technologies, thanks to browser backward compatibility, still works:

The Javascript and SCSS got bundled together using sprockets gem.

It’s definitely time to move on.

But, rewriting the whole project would have been a mistake! https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/

The current state of the art in Rails community and in version 8 is

The scale of the problem with the transition towards the new stack without major rewrite was clear after the whole was spent trying to play legacy stuff like jQuery nicely with importmap.

Woah! Somebody should have told me, there is fundamental incompatibility with ES6 modules style and legacy “no style” Javascript.

Long story short, the proper combination is to ADD importmap gem to the existing codebase, WITHOUT trying to convert all existing scripts in one shot.

Only after adding importmap + stimulus + turbo it’s time to refactor small pieces one-by-one, carefully removing the old Javascript code.

It worth noting, you might have to rename existing application.js to app.js to avoid clashes in importmaps vs sprockets. Other significant aspect, is to turn off Turbo for the entire site and enable it only on certain pieces.

import { Application } from "@hotwired/stimulus"
import "@hotwired/turbo-rails"

// gradually enable turbo
Turbo.session.drive = false

and activate somewhere

<div data-turbo="true">
...
</div>

So far, the difference in complexity is huge! The same functionality is by far easier to do with turbo-stream elements coming from backend, than juggling data with calls to a separate API Engine (!) with JSON output.

The rewrite is still in progress, but the foot is definitely in the door.

Other gotchas

turbo-frame vs turbo-stream

The difficulty is, the both tags intendend to replace parts of the page dynamically, but it’s not immediately clear what tag to use when.

Here is my rule of thumb.

Use turbo-frame for updating single parts and splitting a parts of a page into deferred loading.

Use turbo-stream to update parts of the page from ActiveCable or when you need to updates several parts of the page at the same time.

Nice use case, you could add the whole controller template as “turbo-stream” type. In this example it’s a contents of show.turvbo-stream.erb file

<%= turbo_stream.update :employeeDetails do %>
    <%= render 'employee', employee: @employee %>
<% end %>

<%= turbo_stream.update :clientDetails do %>
    <%= render 'client', client: @client %>
<% end %>

<%= turbo_stream.update :lastEmployeeTickets do %>
    <div class="new_ticket-header"><%= t 'Last5EmployeeTickets' %></div>
    <%= render 'tickets', tickets: @employee_tickets %>
<% end %>

<%= turbo_stream.update :lastClientTickets do %>
    <div class="new_ticket-header"><%= t 'OpenClientTickets', name: @client.name %></div>
    <%= render 'tickets', tickets: @client_open_tickets %>
<% end %>

<%= turbo_stream.update :subscribers do %>
    <%= render 'subscribers', subscribers: @subscribers %>
<% end %>

Most valuable experience on how to connect and use Stimulus was from reading https://once.com/writebook source code.

Ruby on Rails хозяйке на заметку: менеджер локальных сайтов Pow

12.04.2011

Чрезвычайно удобной оказалась возможность работы с несколькими сайтами на базе RoR (по факту на Rack), которую предоставляет утилита Pow.

Вкратце, если у вас достаточно много сайтов разработке, утилита предоставляет хук в DNS зону dev и позволяет обращаться к вашим сайтам по url вида http://myapp.dev , http://myapp2.dev, а также добавляет правило в фаерволе, которое заворачивает обращения к self с порта 80 на внутренний порт утилиты.

Сам Pow состоит из двух частей — DNS responder и проксирующего web server. Реализовано все счастье на Node.js.

Очень интересна реализация зоны .dev, в mac os в каталоге /etc/resolver/ добавляется файл dev такого вида:

nameserver 127.0.0.1 port 20560

что перенаправляет все обращения xxx.dev на соответствующее приложение

 

Утилита поддерживает работу с RVM, что позволяет упражняться с различными версиями руби.

Для поддержки в rails 2 через Pow можно воспользоваться таким хаком, создаем в корне приложениея config.ru, где прописываем

Деинсталляция также проста:

curl get.pow.cx/uninstall.sh | sh
# Rails.root/config.ru
require "./config/environment"
run ActionController::Dispatcher.new

Утилита что-то наподобие denwer для php.

 

Cucumber говорит по-русски

26.12.2009

Для тех ребят, которые увлекаются канбанами, TDD и прочими agile системами разработки следующим шагом является приобщение к BDD.

По-сути Cucumber представляет собой интеграционные тесты при помощи webrat или других высокоуровневых библиотек для эмуляции работы браузера, открывания странц, нажимания на кнопки и кликания по ссылкам.

Теперь с cucumber жить еще проще, рассказывать по-русски можно при помощи тега

@language ru

Типичный сценарий:

# language: ru
Функционал: отчеты по времени
  Чтобы получить понятие о стоимости часа для конкретной компании
  Я как руководитель
  Хочу иметь возможность просматривать отчеты по временным затратам на каждую компанию

Предыстория:
  Допустим существует клиент, админ и сотрудник клиента

Сценарий: просмотр суммарно затраченного времени за текущий месяц
  Допустим админ авторизован
  И открывет страницу отчетов
  То видит список клиентов с затраченным временем за последний месяц
cucumber тесты по-русски

cucumber тесты по-русски