slim

Pagination in ActiveRecord and SQL

Tagged activerecord, pagination  Languages ruby, slim

See https://github.com/christianhellsten/arrr-pagination

How to do pagination in ActiveRecord and SQL without any external dependencies.

Code:

class Pagination
  PER_PAGE ||= 10
  def self.call(relation:, page: 0, per_page: PER_PAGE)
    page = page.to_i
    per_page = per_page.to_i
    offset = page*per_page
    array = relation.offset(offset).limit(per_page+1).to_a
    row_count = relation.count
    page_count = (row_count.to_f / per_page.to_f).ceil
    has_previous_page = page > 0
    has_next_page = array.size > per_page
    pagination = OpenStruct.new(
      page: page,
      page_count: page_count,
      previous_page: has_previous_page ? page - 1 : nil,
      next_page: has_next_page ? page + 1 : nil
    )
    array = array[0..-2] if has_next_page
    [ array, pagination ]
  end
end

Example:

let(:relation) { Measurement.order("created_at DESC") }

it "paginates" do
  measurements, pagination = Pagination.call(relation: relation, per_page: 1, page: 0)
  assert_equal 1, measurements.size
  assert_equal OpenStruct.new(page: 0, page_count: 1, previous_page: nil, next_page: nil), pagination
end

View:

- if @pagination.previous_page || @pagination.next_page
  .pagination
    ul
      - if @pagination.previous_page
        li
          a href=url_for(params.merge(page: nil)) ‹
        li
          a href=url_for(params.merge(page: @pagination.previous_page)) = t('previous')
        - ([0, @pagination.page-4].max..@pagination.page-1).each do |page|
          li
            a href=url_for(params.merge(page: page)) #{page+1}
      - else
        li
          span.muted ‹
        li
          span.muted = t('previous')
      li
        span.muted #{t('page')} #{@pagination.page + 1}
      - if @pagination.next_page
        - (@pagination.page+1..[@pagination.page_count-1, @pagination.page+4].min).each do |page|
          li
            a href=url_for(params.merge(page: page)) #{page+1}
        li
          a href=url_for(params.merge(page: @pagination.next_page)) = t('next')
        li
          a href=url_for(params.merge(page: @pagination.page_count - 1)) ›
      - else
        li
          span.muted = t('next')

Optimistic locking in Rails

Tagged edit conflict, optimistic lock, optimistic locking, rails  Languages ruby, slim

This code is an adaptation of the code found in this Railscasts article: http://railscasts.com/episodes/59-optimistic-locking-revised?view=asciicast

app/models/concerns/optimistic_locking.rb

module OptimisticLocking
  extend ActiveSupport::Concern

  included do
    validate :handle_conflict, on: :update
  end

  def original_updated_at
    @original_updated_at || updated_at.to_f
  end
  attr_writer :original_updated_at

  def handle_conflict
    return unless @conflict || updated_at.to_f > original_updated_at.to_f

    @conflict = true
    # By setting original_updated_at to nil the edit conflict is ignored on a second form submit
    @original_updated_at = nil

    errors.add :base, I18n.t('activerecord.edit_conflict.message')
    changes.each do |attribute, values|
      errors.add(
        attribute,
        I18n.t('activerecord.edit_conflict.was', value: values.first)
      )
    end
  end
end

app/models/project.rb

class Project < ApplicationRecord
  include OptimisticLocking

app/views/_edit_conflict.slim

- object.errors[:base]&.each do |error|
  p.text-danger
    = icon('fas', 'exclamation-circle')
    =< error

app/views/projects/_form.slim

= form @project do |f|
  = render '/edit_conflict', object: @project
  = f.hidden_field :original_updated_at
  = f.text_field :name, class: 'form-control'
  = form_buttons(f, @project)