scinote-web/app/models/my_module.rb

580 lines
19 KiB
Ruby
Raw Normal View History

2017-06-23 21:19:08 +08:00
class MyModule < ApplicationRecord
include ArchivableModel
include SearchableModel
include SearchableByNameModel
include TinyMceImages
2016-02-12 23:52:43 +08:00
enum state: Extends::TASKS_STATES
2016-07-21 19:11:15 +08:00
before_create :create_blank_protocol
2016-02-12 23:52:43 +08:00
auto_strip_attributes :name, :description, nullify: false
2016-02-12 23:52:43 +08:00
validates :name,
length: { minimum: Constants::NAME_MIN_LENGTH,
maximum: Constants::NAME_MAX_LENGTH }
validates :description, length: { maximum: Constants::RICH_TEXT_MAX_LENGTH }
2016-02-12 23:52:43 +08:00
validates :x, :y, :workflow_order, presence: true
validates :experiment, presence: true
validates :my_module_group, presence: true, if: proc { |mm| !mm.my_module_group_id.nil? }
2019-10-22 15:03:33 +08:00
validate :coordinates_uniqueness_check, if: :active?
2017-06-28 21:21:32 +08:00
belongs_to :created_by,
foreign_key: 'created_by_id',
class_name: 'User',
optional: true
belongs_to :last_modified_by,
foreign_key: 'last_modified_by_id',
class_name: 'User',
optional: true
belongs_to :archived_by,
foreign_key: 'archived_by_id',
class_name: 'User',
optional: true
belongs_to :restored_by,
foreign_key: 'restored_by_id',
class_name: 'User',
optional: true
belongs_to :experiment, inverse_of: :my_modules, touch: true
2017-06-28 21:21:32 +08:00
belongs_to :my_module_group, inverse_of: :my_modules, optional: true
has_many :results, inverse_of: :my_module, dependent: :destroy
has_many :my_module_tags, inverse_of: :my_module, dependent: :destroy
2016-02-12 23:52:43 +08:00
has_many :tags, through: :my_module_tags
has_many :task_comments, foreign_key: :associated_id, dependent: :destroy
2020-04-09 23:11:58 +08:00
has_many :inputs, class_name: 'Connection', foreign_key: 'input_id', inverse_of: :to, dependent: :destroy
has_many :outputs, class_name: 'Connection', foreign_key: 'output_id', inverse_of: :from, dependent: :destroy
has_many :my_modules, through: :outputs, source: :to, class_name: 'MyModule'
has_many :my_module_antecessors, through: :inputs, source: :from, class_name: 'MyModule'
2017-06-28 21:21:32 +08:00
has_many :sample_my_modules,
inverse_of: :my_module,
dependent: :destroy
2016-02-12 23:52:43 +08:00
has_many :samples, through: :sample_my_modules
2017-06-07 19:36:39 +08:00
has_many :my_module_repository_rows,
2017-06-06 23:35:29 +08:00
inverse_of: :my_module, dependent: :destroy
2017-06-07 19:36:39 +08:00
has_many :repository_rows, through: :my_module_repository_rows
has_many :repository_snapshots,
dependent: :destroy,
inverse_of: :my_module
2017-06-28 21:21:32 +08:00
has_many :user_my_modules, inverse_of: :my_module, dependent: :destroy
2016-02-12 23:52:43 +08:00
has_many :users, through: :user_my_modules
2017-06-28 21:21:32 +08:00
has_many :report_elements, inverse_of: :my_module, dependent: :destroy
2016-07-21 19:11:15 +08:00
has_many :protocols, inverse_of: :my_module, dependent: :destroy
2019-02-25 22:21:13 +08:00
# Associations for old activity type
has_many :activities, inverse_of: :my_module
2016-02-12 23:52:43 +08:00
scope :is_archived, ->(is_archived) { where('archived = ?', is_archived) }
scope :active, -> { where(archived: false) }
scope :overdue, -> { where('my_modules.due_date < ?', Time.current.utc) }
scope :without_group, -> { active.where(my_module_group: nil) }
scope :one_day_prior, (lambda do
where('my_modules.due_date > ? AND my_modules.due_date < ?',
Time.current.utc,
Time.current.utc + 1.day)
end)
scope :workflow_ordered, -> { order(workflow_order: :asc) }
scope :uncomplete, -> { where(state: 'uncompleted') }
scope :with_step_statistics, (lambda do
left_outer_joins(protocols: :steps)
.group(:id)
.select('my_modules.*')
.select('COUNT(steps.id) AS steps_total')
.select('COUNT(steps.id) FILTER (where steps.completed = true) AS steps_completed')
.select('CASE COUNT(steps.id) WHEN 0 THEN 0 ELSE'\
'((COUNT(steps.id) FILTER (where steps.completed = true)) * 100 / COUNT(steps.id)) '\
'END AS steps_completed_percentage')
end)
# A module takes this much space in canvas (x, y) in database
WIDTH = 30
HEIGHT = 14
2017-01-09 18:33:28 +08:00
def self.search(
user,
include_archived,
query = nil,
page = 1,
2017-05-05 22:41:23 +08:00
current_team = nil,
options = {}
2017-01-09 18:33:28 +08:00
)
exp_ids =
Experiment
.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT)
2017-04-11 20:55:44 +08:00
.pluck(:id)
2016-02-12 23:52:43 +08:00
2017-01-24 23:34:21 +08:00
if current_team
2017-01-12 16:52:29 +08:00
experiments_ids = Experiment
.search(user,
include_archived,
nil,
1,
2017-01-24 23:34:21 +08:00
current_team)
.select('id')
2016-02-12 23:52:43 +08:00
new_query = MyModule
2017-01-09 18:33:28 +08:00
.distinct
2017-01-12 16:52:29 +08:00
.where('my_modules.experiment_id IN (?)', experiments_ids)
2017-05-05 22:41:23 +08:00
.where_attributes_like([:name, :description], query, options)
2017-01-20 00:09:25 +08:00
if include_archived
return new_query
else
return new_query.where('my_modules.archived = ?', false)
end
2017-01-09 18:33:28 +08:00
elsif include_archived
new_query = MyModule
.distinct
.where('my_modules.experiment_id IN (?)', exp_ids)
2017-05-05 22:41:23 +08:00
.where_attributes_like([:name, :description], query, options)
2016-02-12 23:52:43 +08:00
else
new_query = MyModule
2017-01-09 18:33:28 +08:00
.distinct
.where('my_modules.experiment_id IN (?)', exp_ids)
.where('my_modules.archived = ?', false)
2017-05-05 22:41:23 +08:00
.where_attributes_like([:name, :description], query, options)
2016-02-12 23:52:43 +08:00
end
# Show all results if needed
if page == Constants::SEARCH_NO_LIMIT
2016-02-12 23:52:43 +08:00
new_query
else
new_query
.limit(Constants::SEARCH_LIMIT)
.offset((page - 1) * Constants::SEARCH_LIMIT)
2016-02-12 23:52:43 +08:00
end
end
def self.viewable_by_user(user, teams)
where(experiment: Experiment.viewable_by_user(user, teams))
end
def navigable?
!experiment.archived? && experiment.navigable?
end
2016-02-12 23:52:43 +08:00
# Removes assigned samples from module and connections with other
# modules.
def archive(current_user)
2016-02-12 23:52:43 +08:00
self.x = 0
self.y = 0
# Remove association with module group.
self.my_module_group = nil
MyModule.transaction do
archived = super
# Unassociate all samples from module.
2017-06-29 16:29:41 +08:00
archived = SampleMyModule.where(my_module: self).destroy_all if archived
2016-02-12 23:52:43 +08:00
# Remove all connection between modules.
2017-06-29 16:29:41 +08:00
archived = Connection.where(input_id: id).delete_all if archived
archived = Connection.where(output_id: id).delete_all if archived
2016-02-12 23:52:43 +08:00
unless archived
raise ActiveRecord::Rollback
end
end
archived
end
# Similar as super restore, but also calculate new module position
def restore(current_user)
restored = false
# Calculate new module position
new_pos = get_new_position
self.x = new_pos[:x]
self.y = new_pos[:y]
MyModule.transaction do
restored = super
unless restored
raise ActiveRecord::Rollback
end
end
experiment.generate_workflow_img
2016-02-12 23:52:43 +08:00
restored
end
def repository_rows_count(repository)
my_module_repository_rows.joins(repository_row: :repository)
.where('repositories.id': repository.id)
.count
end
def assigned_repositories
2020-05-12 22:26:44 +08:00
team = experiment.project.team
team.repositories
.joins(repository_rows: :my_module_repository_rows)
.where(my_module_repository_rows: { my_module_id: id })
.group(:id)
end
def live_and_snapshot_repositories_list
snapshots = repository_snapshots.left_outer_joins(:original_repository)
selected_snapshots = snapshots.where(selected: true)
2020-05-13 23:29:31 +08:00
.or(snapshots.where(original_repositories_repositories: { id: nil }))
.or(snapshots.where.not(parent_id: assigned_repositories.select(:id)))
2020-05-13 23:29:31 +08:00
.select('DISTINCT ON ("repositories"."parent_id") "repositories".*')
.select('COUNT(repository_rows.id) AS assigned_rows_count')
.joins(:repository_rows)
.group(:parent_id, :id)
.order(:parent_id, updated_at: :desc)
live_repositories = assigned_repositories
.select('repositories.*, COUNT(repository_rows.id) AS assigned_rows_count')
.where.not(id: repository_snapshots.where(selected: true).select(:parent_id))
(live_repositories + selected_snapshots).sort_by { |r| r.name.downcase }
2020-04-02 18:05:07 +08:00
end
def active_snapshot_or_live(rep_or_snap, exclude_snpashot_ids: [])
return unless rep_or_snap
2020-05-20 20:14:47 +08:00
parent_id = rep_or_snap.is_a?(Repository) ? rep_or_snap.id : rep_or_snap.parent_id
selected_snapshot_for_repo(parent_id, exclude_snpashot_ids: exclude_snpashot_ids) ||
assigned_repositories&.where(id: parent_id)&.first ||
repository_snapshots
.where(parent_id: parent_id)
.where.not(id: exclude_snpashot_ids)
.order(updated_at: :desc).first
end
def update_report_repository_references(rep_or_snap_id)
rep_or_snap = RepositoryBase.find(rep_or_snap_id)
ids = if rep_or_snap.is_a?(Repository)
RepositorySnapshot.where(parent_id: rep_or_snap.id).pluck(:id)
else
Repository.where(id: rep_or_snap.parent_id).pluck(:id) +
RepositorySnapshot.where(parent_id: rep_or_snap.parent_id).pluck(:id)
end
report_elements.where(repository_id: ids).update(repository_id: rep_or_snap.id)
2020-05-20 20:14:47 +08:00
end
def selected_snapshot_for_repo(repository_id, exclude_snpashot_ids: [])
repository_snapshots.where(parent_id: repository_id).where.not(id: exclude_snpashot_ids).where(selected: true).first
2020-05-15 19:15:53 +08:00
end
2016-02-12 23:52:43 +08:00
def unassigned_users
User.find_by_sql(
"SELECT DISTINCT users.id, users.full_name FROM users " +
"INNER JOIN user_projects ON users.id = user_projects.user_id " +
"INNER JOIN experiments ON experiments.project_id = user_projects.project_id " +
"WHERE experiments.id = #{experiment_id.to_s}" +
2016-02-12 23:52:43 +08:00
" AND users.id NOT IN " +
"(SELECT DISTINCT user_id FROM user_my_modules WHERE user_my_modules.my_module_id = #{id.to_s})"
)
end
def unassigned_samples
2017-01-24 23:34:21 +08:00
Sample.where(team_id: experiment.project.team).where.not(id: samples)
2016-02-12 23:52:43 +08:00
end
def unassigned_tags
Tag.find_by_sql(
"SELECT DISTINCT tags.id, tags.name, tags.color FROM tags " +
"INNER JOIN experiments ON experiments.project_id = tags.project_id " +
"WHERE experiments.id = #{experiment_id.to_s} AND tags.id NOT IN " +
2016-02-12 23:52:43 +08:00
"(SELECT DISTINCT tag_id FROM my_module_tags WHERE my_module_tags.my_module_id = #{id.to_s})"
)
end
def last_activities(count = Constants::ACTIVITY_AND_NOTIF_SEARCH_LIMIT)
2016-02-12 23:52:43 +08:00
Activity.where(my_module_id: id).order(:created_at).last(count)
end
# Get module comments ordered by created_at time. Results are paginated
# using last comment id and per_page parameters.
def last_comments(last_id = 1, per_page = Constants::COMMENTS_SEARCH_LIMIT)
last_id = Constants::INFINITY if last_id <= 1
comments = TaskComment.joins(:my_module)
.where(my_modules: { id: id })
.where('comments.id < ?', last_id)
.order(created_at: :desc)
.limit(per_page)
2016-09-27 15:00:02 +08:00
comments.reverse
2016-02-12 23:52:43 +08:00
end
def last_activities(last_id = 1,
count = Constants::ACTIVITY_AND_NOTIF_SEARCH_LIMIT)
last_id = Constants::INFINITY if last_id <= 1
2016-02-12 23:52:43 +08:00
Activity.joins(:my_module)
2017-01-24 23:34:21 +08:00
.where(my_module_id: id)
.where('activities.id < ?', last_id)
.order(created_at: :desc)
.limit(count)
.uniq
2016-02-12 23:52:43 +08:00
end
2016-07-21 19:11:15 +08:00
def protocol
# Temporary function (until we fully support
# multiple protocols per module)
protocols.count > 0 ? protocols.first : nil
2016-02-12 23:52:43 +08:00
end
def first_n_samples(count = Constants::SEARCH_LIMIT)
2016-07-21 19:11:15 +08:00
samples.order(name: :asc).limit(count)
2016-02-12 23:52:43 +08:00
end
def number_of_samples
samples.count
end
def is_overdue?(datetime = DateTime.current)
due_date.present? && datetime.utc > due_date.utc
2016-02-12 23:52:43 +08:00
end
def overdue_for_days(datetime = DateTime.current)
if due_date.blank? || due_date.utc > datetime.utc
0
2016-02-12 23:52:43 +08:00
else
((datetime.utc.to_i - due_date.utc.to_i) / 1.day.to_f).ceil
2016-02-12 23:52:43 +08:00
end
end
def is_one_day_prior?(datetime = DateTime.current)
is_due_in?(datetime, 1.day)
end
def is_due_in?(datetime, diff)
due_date.present? &&
datetime.utc < due_date.utc &&
datetime.utc > (due_date.utc - diff)
2016-02-12 23:52:43 +08:00
end
def space_taken
st = 0
2016-07-21 19:11:15 +08:00
protocol.steps.find_each do |step|
2016-02-12 23:52:43 +08:00
st += step.space_taken
end
results
.includes(:result_asset)
.find_each do |result|
st += result.space_taken
end
st
end
def archived_results
results
.select('results.*')
.select('ra.id AS result_asset_id')
.select('rt.id AS result_table_id')
.select('rx.id AS result_text_id')
.joins('LEFT JOIN result_assets AS ra ON ra.result_id = results.id')
.joins('LEFT JOIN result_tables AS rt ON rt.result_id = results.id')
.joins('LEFT JOIN result_texts AS rx ON rx.result_id = results.id')
.where(:archived => true)
end
# Treat this module as root, get all modules of that subtree
def downstream_modules
2016-02-12 23:52:43 +08:00
final = []
modules = [self]
until modules.empty?
2016-02-12 23:52:43 +08:00
my_module = modules.shift
final << my_module unless final.include?(my_module)
modules.push(*my_module.my_modules)
2016-02-12 23:52:43 +08:00
end
final
end
# Treat this module as inversed root, get all modules of that inversed subtree
def upstream_modules
2016-02-12 23:52:43 +08:00
final = []
modules = [self]
until modules.empty?
2016-02-12 23:52:43 +08:00
my_module = modules.shift
final << my_module unless final.include?(my_module)
modules.push(*my_module.my_module_antecessors)
2016-02-12 23:52:43 +08:00
end
final
end
# Generate the samples belonging to this module
# in JSON form, suitable for display in handsontable.js
def samples_json_hot(order)
data = []
samples.order(created_at: order).each do |sample|
sample_json = []
sample_json << sample.name
if sample.sample_type.present?
sample_json << sample.sample_type.name
else
sample_json << I18n.t("samples.table.no_type")
end
if sample.sample_group.present?
sample_json << sample.sample_group.name
else
sample_json << I18n.t("samples.table.no_group")
end
sample_json << I18n.l(sample.created_at, format: :full)
sample_json << sample.user.full_name
data << sample_json
end
# Prepare column headers
headers = [
I18n.t("samples.table.sample_name"),
I18n.t("samples.table.sample_type"),
I18n.t("samples.table.sample_group"),
I18n.t("samples.table.added_on"),
I18n.t("samples.table.added_by")
]
{ data: data, headers: headers }
end
# Generate the repository rows belonging to this module
# in JSON form, suitable for display in handsontable.js
2020-05-18 18:56:10 +08:00
def repository_json_hot(repository, order)
data = []
2020-05-18 18:56:10 +08:00
rows = repository.assigned_rows(self).includes(:created_by).order(created_at: order)
2020-05-15 19:15:53 +08:00
rows.find_each do |row|
row_json = []
row_json << row.id
row_json << row.name
row_json << I18n.l(row.created_at, format: :full)
row_json << row.created_by.full_name
data << row_json
end
# Prepare column headers
headers = [
I18n.t('repositories.table.id'),
2017-06-08 19:36:37 +08:00
I18n.t('repositories.table.row_name'),
I18n.t('repositories.table.added_on'),
I18n.t('repositories.table.added_by')
]
{ data: data, headers: headers }
end
2020-05-18 18:56:10 +08:00
def repository_docx_json(repository)
headers = [
I18n.t('repositories.table.id'),
I18n.t('repositories.table.row_name'),
I18n.t('repositories.table.added_on'),
I18n.t('repositories.table.added_by')
]
custom_columns = []
return false unless repository
repository.repository_columns.order(:id).each do |column|
headers.push(column.name)
custom_columns.push(column.id)
end
2020-05-18 18:56:10 +08:00
records = repository.assigned_rows(self).select(:id, :name, :created_at, :created_by_id)
{ headers: headers, rows: records, custom_columns: custom_columns }
end
2016-02-12 23:52:43 +08:00
def deep_clone(current_user)
2016-08-16 15:10:45 +08:00
deep_clone_to_experiment(current_user, experiment)
end
2019-12-02 22:34:42 +08:00
def deep_clone_to_experiment(current_user, experiment_dest)
2016-02-12 23:52:43 +08:00
# Copy the module
2019-12-02 22:34:42 +08:00
clone = MyModule.new(name: name, experiment: experiment_dest, description: description, x: x, y: y)
# set new position if cloning in the same experiment
clone.attributes = get_new_position if clone.experiment == experiment
clone.save!
2017-06-28 21:29:10 +08:00
2016-07-21 19:11:15 +08:00
# Remove the automatically generated protocol,
# & clone the protocol instead
clone.protocol.destroy
clone.reload
2016-02-12 23:52:43 +08:00
2016-07-21 19:11:15 +08:00
# Update the cloned protocol if neccesary
2019-09-23 22:33:57 +08:00
clone_tinymce_assets(clone, clone.experiment.project.team)
clone.protocols << protocol.deep_clone_my_module(self, current_user)
2016-02-12 23:52:43 +08:00
clone.reload
# fixes linked protocols
clone.protocols.each do |protocol|
next unless protocol.linked?
protocol.updated_at = protocol.parent_updated_at
protocol.save
end
clone
2016-02-12 23:52:43 +08:00
end
# Find an empty position for the restored module. It's
# basically a first empty row with empty space inside x=[0, 32).
2016-02-12 23:52:43 +08:00
def get_new_position
2016-08-23 17:08:10 +08:00
return { x: 0, y: 0 } if experiment.blank?
2016-02-12 23:52:43 +08:00
# Get all modules position that overlap with first column, [0, WIDTH) and
# sort them by y coordinate.
2016-08-23 17:08:10 +08:00
positions = experiment.active_modules.collect { |m| [m.x, m.y] }
.select { |x, _| x >= 0 && x < WIDTH }
.sort_by { |_, y| y }
return { x: 0, y: 0 } if positions.empty? || positions.first[1] >= HEIGHT
# It looks we'll have to find a gap between the modules if it exists (at
# least 2*HEIGHT wide
2016-08-23 17:08:10 +08:00
ind = positions.each_cons(2).map { |f, s| s[1] - f[1] }
.index { |y| y >= 2 * HEIGHT }
return { x: 0, y: positions[ind][1] + HEIGHT } if ind
# We lucked out, no gaps, therefore we need to add it after the last element
2016-08-23 17:08:10 +08:00
{ x: 0, y: positions.last[1] + HEIGHT }
end
2016-02-12 23:52:43 +08:00
def completed?
state == 'completed'
end
# Check if my_module is ready to become completed
def check_completness_status
if protocol && protocol.steps.count > 0
completed = true
protocol.steps.find_each do |step|
completed = false unless step.completed
end
return true if completed
end
false
end
def complete
self.state = 'completed'
self.completed_on = DateTime.now
end
def uncomplete
self.state = 'uncompleted'
self.completed_on = nil
end
2020-03-30 21:16:14 +08:00
def assign_user(user, assigned_by = nil)
user_my_modules.create(
assigned_by: assigned_by || user,
user: user
)
Activities::CreateActivityService
.call(activity_type: :assign_user_to_module,
owner: assigned_by || user,
team: experiment.project.team,
project: experiment.project,
subject: self,
message_items: { my_module: id,
user_target: user.id })
end
private
def create_blank_protocol
protocols << Protocol.new_blank_for_module(self)
2016-02-12 23:52:43 +08:00
end
2019-10-22 15:03:33 +08:00
def coordinates_uniqueness_check
if experiment && experiment.my_modules.active.where(x: x, y: y).where.not(id: id).any?
errors.add(:position, I18n.t('activerecord.errors.models.my_module.attributes.position.not_unique'))
end
end
2016-02-12 23:52:43 +08:00
end