Skip to content

Create a model

Paths on this page are relative to your plugin directory, for example plugins/easyproject/easy_plugins/modification_acme/.

Model

Model files live in app/models. A model inherits from ApplicationRecord and has a singular name that describes the object it represents.

Prefix new entities with Easy (or with your plugin name, see Prefixes) to avoid clashes with Redmine names. See ADR 0001.

The namespace reflects the file path: EasyModule::MyModel is in app/models/easy_module/my_model.rb.

Base model definition
module EasyModule
  class MyModel < ApplicationRecord
    include Redmine::SafeAttributes

    belongs_to :author, class_name: "::User", inverse_of: :my_models
    belongs_to :collection, class_name: "::Collection", inverse_of: :my_models, optional: true

    has_many :items, class_name: "::EasyModule::MyItem", dependent: :destroy, inverse_of: :my_model

    before_validation :set_author, on: :create

    validates :name, presence: true, uniqueness: true
    validates :description, length: { maximum: 1000 }

    scope :visible, -> { all }
    scope :like, ->(q) { where(arel_table[:name].matches("%#{q}%")) }

    enum :status, { active: 0, disabled: 1 }, prefix: true

    safe_attributes "name", "description", "status", if: proc { editable? }

    # public methods implementation

    def editable?(_user = User.current)
      true
    end

    private

    # private methods implementation

    def set_author
      self.author_id ||= User.current.id
    end

  end
end

Model structure

Start the model with the concerns it includes, such as Redmine::SafeAttributes, followed by model properties such as table_name.

include Redmine::SafeAttributes

self.table_name = "my_model_table_name"

Then define associations: belongs_to first, then has_many and has_one. Give each association inverse_of, and class_name when the association name differs from the class name. belongs_to is required by default; add optional: true to change that. Give every has_many a dependent option.

belongs_to :author, class_name: "::User", inverse_of: :my_models

has_many :items, class_name: "::EasyModule::MyItem", dependent: :destroy, inverse_of: :my_model

has_one :address, class_name: "::EasyModule::MyAddress", dependent: :destroy, inverse_of: :my_model

After associations come callbacks and validations. Callbacks may use on: to limit the action they run on. validate takes the name of a custom method; validates takes built-in validators such as presence, uniqueness and length. Put all validators of one attribute on one line, and attributes that share validators on one line as well.

before_validation :set_author, on: :create

validates :name, presence: true, uniqueness: true, length: { maximum: 255 }
validates :description, :text, presence: true, length: { maximum: 1000 }

validate :custom_my_model_validation

Next come scopes and enums. A scope has a name and a lambda; common ones are visible, like and active. An enum takes the attribute name and a hash of values, and defines scopes, getters and setters. Use prefix: or suffix: to avoid collisions with existing methods.

scope :visible, -> { all }
scope :like, ->(q) { where(arel_table[:name].matches("%#{q}%")) }

enum :status, { active: 0, disabled: 1 }, prefix: true

Before the methods, call the class methods of included concerns: safe_attributes (the allow list of attributes assignable from user input) and the acts_as_* declarations that add model behaviour.

safe_attributes "name", "description", "status"
acts_as_searchable columns: %W[#{table_name}.name #{table_name}.description],
                   date_column: "#{table_name}.created_at",
                   permission: :view_my_models,
                   project_key: :project_id

The methods come last. Public methods describe an instance or implement model logic; private methods mostly implement callbacks and validations. Common public methods:

def to_s
  name # or some other instance meaningful representation
end

def visible?(user = User.current)
  # implementation
end

def editable?(user = User.current)
  # implementation
end

def deletable?(user = User.current)
  # implementation
end

Migrations

Every model needs a table. See Database and data migrations.

Tests

Model specs live in test/spec/models and are named model_name_spec.rb. Cover methods, scopes, validations and callbacks.

Example of my_model_spec.rb
require "easy_extensions/spec_helper"

describe EasyModule::MyModel, type: :model do

  describe "#create" do
    subject(:create) { described_class.create(params) }
    # definition of necessary objects

    it "assign author" do
      expect(create).to have_attributes author_id: /\d+/
    end
  end

  describe "#editable?" do
    subject(:editable?) { entity.editable? }
    # definition of necessary objects

    it { is_expected.to eq false }

    it "tests editable method" do
      # test implementation
    end
  end

  describe ".visible" do
    subject(:visible) { described_class.visible }
    # definition of necessary objects

    it "tests visible scope" do
      # test implementation
    end
  end

  describe ".like" do
    subject(:like) { described_class.like(term) }
    # definition of necessary objects

    it "tests like scope" do
      # test implementation
    end
  end
end

Factories

Factories prepare the instances your specs work with. Define one factory per model in test/factories/model_name.rb.

test/factories/my_model.rb
1
2
3
4
5
6
7
8
FactoryBot.define do
  factory :my_model, class: EasyModule::MyModel do
    sequence(:name) { |n| "MyModel #{n}" }
    description { "MyModel description" }
    status { :active }
    association :collection, factory: :collection
  end
end
Define unique values as sequences. Build associations from other factories. Use traits for variants:
example with trait
FactoryBot.define do
  factory :my_model, class: EasyModule::MyModel do
    sequence(:name) { |n| "MyModel #{n}" }
    description { "MyModel description" }
    status { :active }
    association :collection, factory: :collection

    trait :disabled do
      status { :disabled }
    end
  end
end
FactoryBot.create(:my_model, :disabled)