scinote-web/app/models/step.rb

237 lines
7.3 KiB
Ruby

class Step < ApplicationRecord
include SearchableModel
include SearchableByNameModel
include TinyMceImages
include ViewableModel
attr_accessor :skip_position_adjust # to be used in bulk deletion
enum assets_view_mode: { thumbnail: 0, list: 1, inline: 2 }
auto_strip_attributes :name, :description, nullify: false
validates :name,
presence: true,
length: { maximum: Constants::NAME_MAX_LENGTH }
validates :description, length: { maximum: Constants::RICH_TEXT_MAX_LENGTH }
validates :position, presence: true
validates :completed, inclusion: { in: [true, false] }
validates :user, :protocol, presence: true
validates :completed_on, presence: true, if: proc { |s| s.completed? }
validates :position, uniqueness: { scope: :protocol }, if: :position_changed?
before_validation :set_completed_on, if: :completed_changed?
before_save :set_last_modified_by
before_destroy :cascade_before_destroy
after_destroy :adjust_positions_after_destroy, unless: -> { skip_position_adjust }
# conditional touch excluding step completion updates
after_destroy :touch_protocol
after_save :touch_protocol
after_touch :touch_protocol
belongs_to :user, inverse_of: :steps
belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User', optional: true
belongs_to :protocol, inverse_of: :steps
delegate :team, to: :protocol
has_many :step_orderable_elements, inverse_of: :step, dependent: :destroy
has_many :checklists, inverse_of: :step, dependent: :destroy
has_many :step_comments, foreign_key: :associated_id, dependent: :destroy
has_many :step_texts, inverse_of: :step, dependent: :destroy
has_many :step_assets, inverse_of: :step, dependent: :destroy
has_many :assets, through: :step_assets, dependent: :destroy
has_many :step_tables, inverse_of: :step, dependent: :destroy
has_many :tables, through: :step_tables, dependent: :destroy
has_many :report_elements, inverse_of: :step, dependent: :destroy
accepts_nested_attributes_for :checklists,
reject_if: :all_blank,
allow_destroy: true
accepts_nested_attributes_for :assets,
reject_if: :all_blank,
allow_destroy: true
accepts_nested_attributes_for :tables,
reject_if: proc { |attributes|
attributes['contents'].blank?
},
allow_destroy: true
scope :in_order, -> { order(position: :asc) }
scope :desc_order, -> { order(position: :desc) }
def self.search(user,
include_archived,
query = nil,
page = 1,
_current_team = nil,
options = {})
protocol_ids = Protocol.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT)
.pluck(:id)
new_query = Step.distinct
.left_outer_joins(:step_texts)
.where(steps: { protocol_id: protocol_ids })
.where_attributes_like(['steps.name', 'step_texts.text'], query, options)
# Show all results if needed
if page == Constants::SEARCH_NO_LIMIT
new_query
else
new_query.limit(Constants::SEARCH_LIMIT).offset((page - 1) * Constants::SEARCH_LIMIT)
end
end
def self.filter_by_teams(teams = [])
return self if teams.blank?
joins(protocol: { my_module: { experiment: :project } })
.where(protocol: { my_modules: { experiments: { projects: { team: teams } } } })
end
def default_view_state
{ 'assets' => { 'sort' => 'new' } }
end
def validate_view_state(view_state)
unless %w(new old atoz ztoa).include?(view_state.state.dig('assets', 'sort'))
view_state.errors.add(:state, :wrong_state)
end
end
def self.viewable_by_user(user, teams)
joins(:protocol).where(protocol: { my_module: MyModule.viewable_by_user(user, teams) })
end
def can_destroy?
!assets.map(&:locked?).any?
end
def my_module
protocol.present? ? protocol.my_module : nil
end
def position_plus_one
position + 1
end
def last_comments(last_id = 1, per_page = Constants::COMMENTS_SEARCH_LIMIT)
last_id = Constants::INFINITY if last_id <= 1
comments = StepComment.joins(:step)
.where(steps: { id: id })
.where('comments.id < ?', last_id)
.order(created_at: :desc)
.limit(per_page)
StepComment.from(comments, :comments).order(created_at: :asc)
end
def space_taken
st = 0
assets.each do |asset|
st += asset.estimated_size
end
st
end
def comments
step_comments
end
def description_step_text
step_texts.order(created_at: :asc).first
end
def duplicate(protocol, user, step_position: nil, step_name: nil)
ActiveRecord::Base.transaction do
assets_to_clone = []
new_step = protocol.steps.new(
name: step_name || name,
position: step_position || protocol.steps.length,
completed: false,
user: user
)
new_step.save!
# Copy texts
step_texts.each do |step_text|
step_text.duplicate(new_step, step_text.step_orderable_element.position)
end
# Copy checklists
checklists.asc.each do |checklist|
checklist.duplicate(new_step, user, checklist.step_orderable_element.position)
end
# "Shallow" Copy assets
assets.each do |asset|
new_asset = asset.dup
new_asset.save!
new_step.assets << new_asset
assets_to_clone << [asset.id, new_asset.id]
end
# Copy tables
tables.each do |table|
table.duplicate(new_step, user, table.step_table.step_orderable_element.position)
end
# Call clone helper
Protocol.delay(queue: :assets).deep_clone_assets(assets_to_clone)
new_step
end
end
def normalize_elements_position
step_orderable_elements.order(:position).each_with_index do |element, index|
element.update!(position: index) unless element.position == index
end
end
private
def touch_protocol
# if only step completion attributes were changed, do not touch protocol
return if saved_changes.keys.sort == %w(completed completed_on updated_at)
# rubocop:disable Rails/SkipsModelValidations
protocol.update(last_modified_by: last_modified_by) if last_modified_by
protocol.touch
# rubocop:enable Rails/SkipsModelValidations
end
def adjust_positions_after_destroy
re_index_following_steps
protocol.steps.where('position > ?', position).order(:position).each do |step|
step.update!(position: step.position - 1)
end
end
def re_index_following_steps
steps = protocol.steps.where(position: position..).order(:position).where.not(id: id)
i = position
steps.each do |step|
i += 1
step.position = i
end
steps.reverse_each do |step|
step.save! if step.position_changed?
end
end
def cascade_before_destroy
assets.each(&:destroy)
tables.each(&:destroy)
end
def set_completed_on
return if completed? && completed_on.present?
self.completed_on = completed? ? DateTime.now : nil
end
def set_last_modified_by
self.last_modified_by_id ||= user_id
end
end