Latest from the Rocketship

We like writing about our personal experiences, our challenges and our take on programming and management. Enjoy!

TILJanuary 31, 2019by Maksim Agafonov

Form_for and Rails attributes API

Attributes API allows you to have complex object with specified structure in fields. But how to make forms with this fields?

The answer is pretty simple: you work with them like with nested attributes.

Let’s say, we have class Provider with attribute address.

 class Provider < ApplicationRecord attribute :address, Provider::Address::Type.new, default: Provider::Address.new 

Address has following attributes:

 class Provider::Address extend Dry::Initializer option :country, optional: true option :zip, optional: true option :city, optional: true option :address, optional: true 

Form will look like this:

 = form_for resource_provider, provider_url do |f| = f.fields_for :address do |ff| = ff.label 'Country' = ff.text_field :country, value: @provider.billing_address.country = ff.label 'Zip' = ff.text_field :zip, value: @provider.billing_address.zip = ff.label 'City' = ff.text_field :city, value: @provider.billing_address.city = ff.label 'Address' = ff.text_field :address, value: @provider.billing_address.address 

In controller you have to permit parameters:

 def provider_params params.require(:provider).permit(address: [:city, :zip, :country, :address]) end 

And that’s all. Now, in action method you can assign attributes like this:

 resource_provider.attributes = provider_params resource_provider.save 
TILJanuary 29, 2019by Suhomozgy Andrey

Passing events between browser tabs without WebSockets

What if need to pass some event from one browser tab to each others? And we don't have WebSockets...

It turns out that localStorage raises events 😍

Namely, events are triggered when an item is added, deleted, or changed in another context. In essence, this means that when you change localStorage in one tab, other tabs can find out about this by listening to the storage event of the global window object. For example:

 window.addEventListener('storage', function (event) { console.log(event.key, event.newValue); }); 

Of course, there are some restrictions on use (you can read about them here https://www.w3.org/TR/webstorage/), but for simple cases it is match perfect.

TILJanuary 29, 2019by Alexander Blinov

Rename the key name in the javascript object

How to rename the key name in the javascript object? That’s easy!

Lets create function to do that:

 const renameKey = (object, key, newKey) => { const clonedObj = clone(object); const targetKey = clonedObj[key]; delete clonedObj[key]; clonedObj[newKey] = targetKey; return clonedObj; }; 

Here is clone function:

 const clone = (obj) => Object.assign({}, obj); 

Example:

 let contact = { id: 1, name: "contact name" }; contact = renameKey(contact, 'id', 'value'); contact = renameKey(contact, 'name', 'label'); console.log(contact); // { value: 1, label: "contact name" }; 
TILJanuary 28, 2019by Andrey Morozov

Use gem Recorder for db log 💪

Gem Recorder tracks changes of your Rails models

For start use this library in Rails 5.2 you need add gem to the Gemfile:

 gem 'recorder', github: 'jetrockets/recorder', branch: 'rails_5_2' 

Next:

 > bundle install > rails g recorder:install > rails db:migrate 

And final step -- add to model:

 include ::Recorder::Observer recorder async: false 

And it's all 🎉

For an example - logs can be issued in the representer:

 Recorder::Revision.where(item: object).order(id: :DESC).limit(10).map do |revision| { revision_id: revision.id, userId: revision.user_id, userEmail: revision.user&.email, change: changes_type(revision) } end def changes_type(revision) if revision.data['changes'].include?('drive_file_id') && revision.data['changes'].size > 4 && revision.data['changes']['drive_file_id'].present? 'Object updated & Document generated' elsif revision.data['changes'].include?('drive_file_id') && revision.data['changes'].size == 4 'Document generated' elsif revision.data['changes'].include?('number') && revision.data['changes']['number'].nil? 'Object created' else 'Object updated' end end 

But of course this is an abstract example with my data 😏


https://github.com/jetrockets/recorder

TILJanuary 26, 2019by Andrey Morozov

Command for create zip archive without gem's 📁

 class CreateZipCommand def call(files) # Create temp directory for files tmp_dir = Dir.mktmpdir tmp_zip_path = File.join(tmp_dir, "files.zip") # Move files to the temporary folder you created above files.map do |file| download_file(file, tmp_dir) end # Go to the folder and archive the entire contents `cd #{tmp_dir} && zip #{tmp_zip_path} ./*` # Return zip path tmp_zip_path end end > CreateZipCommand.new.call(files) => "/var/folders/bk/0c864z710654sx555jpdpx9c0000gn/T/d20190126-7447-d27fpl/files.zip") 

Most gems for working with archives eat a lot of memory when working with large files. This solution does not have these problems.

Make sure that the zip utility is installed on your computer - it don't work without it

TILJanuary 24, 2019by Alexander Blinov

Reset values in React Final Form w/ keepDirtyOnReinitialize

How to reset values with keepDirtyOnReinitialize in React Final Form after submitting.

The Issue

If keepDirtyOnReinitialize is applied to your form, then form.reset() no longer able to remove field's value.

The Solution

The Solution is simple— if the form submitted successfully: first change keepDirtyOnReinitialize to false -> perform form reset form.reset() -> and change keepDirtyOnReinitialize back to true.

 <Form onSubmit={onSubmit} keepDirtyOnReinitialize render={({ handleSubmit, form }) => ( <form onSubmit={(event) => { const promise = handleSubmit(event); promise && promise.then(() => { form.setConfig('keepDirtyOnReinitialize', false); form.reset(); form.setConfig('keepDirtyOnReinitialize', true); }) return promise; }} > ... </form> }/> 
TILJanuary 23, 2019by Roman Smirnov

How to add index to a large working app on Rails?

Well known fact is that PostgreSQL and many other RDBMS lock write access on the table while the index is being created. It is not acceptable when your project is large enough to allow a downtime for such the small adjustment like a new index.

There is a way to avoid the write-lock though. You can create the index concurrently. But how to do it with Rails Migrations?

First of all you can pass :algorithm option:

 class AddIndexOnBatchIdToFundTrades < ActiveRecord::Migration[5.0] def change add_index :fund_trades, :batch_id, algorithm: :concurrently end end 

But when you try to run such the migration, you would get the following error:

 PG::ActiveSqlTransaction: ERROR: CREATE INDEX CONCURRENTLY cannot run inside a transaction block : CREATE INDEX CONCURRENTLY "index_fund_trades_on_batch_id" ON "fund_trades" ("batch_id") 

That's because any migration by default is executed inside a transaction. Thankfully there is a way to pass it through - use disable_ddl_transaction! to run your migration without a transaction:

 class AddIndexOnBatchIdToFundTrades < ActiveRecord::Migration[5.0] disable_ddl_transaction! def change add_index :fund_trades, :batch_id, algorithm: :concurrently end end 
TILJanuary 23, 2019by Andrey Morozov

Webhook integration in development with Ngrok 🚀

All begin when I’m using the Pipedrive (is a sales management tool designed to help small sales teams manage intricate or lengthy sales processes) webhook.

The solution was easy I just create an ngrok tunnel

 > ngrok http 3000 Session Status online Account Andrey (Plan: Free) Version 2.2.8 Region United States (us) Web Interface http://127.0.0.1:4040 Forwarding http://b1256cb6.ngrok.io -> localhost:3000 Forwarding https://b1256cb6.ngrok.io -> localhost:3000 

and then sent the webhook to the generated address and it works. In local development I can user webhook from another api! It's work 🚀

To use it you need to do 4 steps:

  1. Register on ngrok

  2. Download ngrok from site

  3. Connect your account

  4. Run it 🚀


ps

In addition, this service has a ngrok-tunel gem that allows you to fully integrate it with your application. But that's another story 💎

TILJanuary 22, 2019by Alexander Blinov

Reset values in React Final Form

How to reset values in React Final Form after submitting

At first I wrote the following code:

 <Form onSubmit={onSubmit} render={({ handleSubmit, form }) => ( <form onSubmit={event => { handleSubmit(event).then(() => { form.reset(); }) }} > ... </form> }/> 

If we implement a function like this, we will get an error "Uncaught TypeError: Cannot read property 'then' of undefined”, when we try to submit invalid form.

Invalid forms — form with validation errors.

The solution

To avoid this error, we need to place handleSubmit(event) to a variable, and if the variable is not undefined call .then().

 onSubmit={(event) => { const promise = handleSubmit(event); promise && promise.then(() => { form.reset(); }) return promise; }} 
TILJanuary 21, 2019by Suhomozgy Andrey

Zip two arrays repeating values of the smaller one

So, we have two arrays and we need map each element of the first array to an element from the second. The length of the second array may be less or equal than the first one. What’s if we need to repeat values of the smaller one?

For example, there are two arrays:

 const numbers = [1, 2, 3, 4, 5, 6, 7, 8] const letters = ['a', 'b', 'c'] 

And we need to get something like:

 [ { number: 1, letter: 'a' }, { number: 2, letter: 'b' }, { number: 3, letter: 'c' }, { number: 4, letter: 'a' }, { number: 5, letter: 'b' }, { number: 6, letter: 'c' }, { number: 7, letter: 'a' }, { number: 8, letter: 'b' } ] 

You can use Google or your math skills 😎 and finally will come to this:

 const getNormalizedIndex = (index, array) => ((index + array.length) % array.length) % array.length const numbers_letters = numbers.map((item, index) => { return { number: item, letter: letters[getNormalizedIndex(index, letters)] } }) 

Run it and you’ll get the expected result.

TILJanuary 21, 2019by Dmitry Voronov

Render and combine PDF files into one

Render PDF files from HTML templates in Rails can be done using WickedPDF.

First you need to use ActionView to render HTML to a string:

 def render_to_string(data) action_view = ActionView::Base.new action_view.view_paths = ActionController::Base.view_paths action_view.class_eval do include ApplicationHelper include PDFHelper # or other helpers end action_view.render template: 'pdf/template.html', layout: 'layout/pdf.html', locals: { data: data } end 

Then you can render pdf with the desired settings like this:

 # first pdf file with some view settings and values from data1... pdf1 = WickedPdf.new.pdf_from_string( render_to_string(data1), { pdf: 'report1', page_size: 'Letter', orientation: 'Portrait' }) # ...and second pdf file with some view settings and values from data2 pdf2 = WickedPdf.new.pdf_from_string( render_to_string(data2), { pdf: 'report2', page_size: 'Letter', orientation: 'Landscape' }) 

And now you can combine it with CombinePDF gem, that provide you parse method to get PDF content and to_pdfmethod to render the result back to PDF.

 combiner = CombinePDF.new combiner << CombinePDF.parse(pdf1) combiner << CombinePDF.parse(pdf2) combiner.to_pdf 
TILJanuary 16, 2019by Dmitry Voronov

Use image files from S3 in WickedPdf

If you need to use image files from S3 in your generated pdf file using WickedPdf, then you need first to download the image. You can create method that does this and add it to the helper.

 require 'open-uri' module PdfHelper def embed_remote_image(url, content_type) asset = open(url, "r:UTF-8", &:read) base64 = Base64.encode64(asset.to_s).gsub(/\s+/, "") "data:#{content_type};base64,#{Rack::Utils.escape(base64)}" end end 

And use image_tag instead of wicked_pdf_image_tag

 = image_tag embed_remote_image(file.logo_url, 'image/jpeg') 
TILJanuary 12, 2019by Dmitry Voronov

Manage Elixir versions like RVM & Rbenv

You can install different versions of the Elixir with help of Kiex, like in Ruby with a RVM and Rbenv.

Download and install Kiex

 \curl -sSL https://raw.githubusercontent.com/taylor/kiex/master/install | bash -s ```bash In .bashrc (or .zshrc if you use z shell), add the following ```bash [[ -s "$HOME/.kiex/scripts/kiex" ]] && source "$HOME/.kiex/scripts/kiex" ```bash Install required Elixir ```bash kiex install 1.7 # or another version ```bash And then you can use any version ```bash kiex use 1.7 ```bash 
TILJanuary 07, 2019by Dmitry Voronov

How to create zip files on the fly w/o Tempfile

There are many articles about how to archive files from the server and send a zip-file to a client without persisting it on the server. But usually they don't literally do it, because they use temporary files.

There is a simple way to do it without creating any file though. You just have to put files directly to Zip::OutputStream and then read from it. Btw pay attention: you must rewind the stream before reading it.

 # some files objects def download(files) zip_stream = Zip::OutputStream.write_buffer do |zip| files.each.with_index(1) do |file, index| # file name must be uniq in archive zip.put_next_entry("#{file.name}--#{index}.#{file.extension}") zip.write(file.read.force_encoding('utf-8')) end end # important - rewind the steam zip_stream.rewind send_data zip_stream.read, type: 'application/zip', disposition: 'attachment', filename: 'files-archive.zip' end 
TILDecember 28, 2018by Alexander Spitsyn

How to implement inheritance in Grape resources?

Grape uses specific DSL to define endpoints in API, that’s why you can’t use base class’ instance methods in descendant resources. But there’s one trick:

There’s an #inherited class method in Ruby which is triggered every time some class inherits from ancestor class. It passes one argument - descendant class. Calling descendant’s #instance_eval method we can place any useful stuff inside a block: methods, helpers, before-do’s, etc, in this way evaluating it in context of subclass.

 class Base < Grape::API def self.inherited(subclass) super subclass.instance_eval do helpers do def current_user @current_user ||= User.find(params[:user_id]) end end # ... end end end class DocumentsResource < Base post '/documents' do @document = current_user.documents.build # ... end end 

Note that it’s not real inheritance because Base class has not methods defined inside subclass#instance_eval block.