2016-02-12 23:52:43 +08:00
|
|
|
class MyModule < ActiveRecord::Base
|
|
|
|
include ArchivableModel, SearchableModel
|
|
|
|
|
2016-07-21 19:11:15 +08:00
|
|
|
before_create :create_blank_protocol
|
2016-02-12 23:52:43 +08:00
|
|
|
|
2016-09-21 21:54:12 +08:00
|
|
|
auto_strip_attributes :name, :description, nullify: false
|
2016-02-12 23:52:43 +08:00
|
|
|
validates :name,
|
2016-09-16 21:41:31 +08:00
|
|
|
length: { minimum: NAME_MIN_LENGTH, maximum: NAME_MAX_LENGTH }
|
2016-09-21 21:54:12 +08:00
|
|
|
validates :description, length: { maximum: TEXT_MAX_LENGTH }
|
2016-02-12 23:52:43 +08:00
|
|
|
validates :x, :y, :workflow_order, presence: true
|
2016-07-22 16:43:55 +08:00
|
|
|
validates :experiment, presence: true
|
2016-02-12 23:52:43 +08:00
|
|
|
validates :my_module_group, presence: true, if: "!my_module_group_id.nil?"
|
|
|
|
|
|
|
|
belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User'
|
|
|
|
belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User'
|
|
|
|
belongs_to :archived_by, foreign_key: 'archived_by_id', class_name: 'User'
|
|
|
|
belongs_to :restored_by, foreign_key: 'restored_by_id', class_name: 'User'
|
2016-07-22 16:43:55 +08:00
|
|
|
belongs_to :experiment, inverse_of: :my_modules
|
2016-02-12 23:52:43 +08:00
|
|
|
belongs_to :my_module_group, inverse_of: :my_modules
|
|
|
|
has_many :results, inverse_of: :my_module, :dependent => :destroy
|
|
|
|
has_many :my_module_tags, inverse_of: :my_module, :dependent => :destroy
|
|
|
|
has_many :tags, through: :my_module_tags
|
|
|
|
has_many :my_module_comments, inverse_of: :my_module, :dependent => :destroy
|
|
|
|
has_many :comments, through: :my_module_comments
|
|
|
|
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
|
|
|
|
has_many :my_module_antecessors, through: :inputs, source: :from, class_name: 'MyModule'
|
|
|
|
has_many :sample_my_modules, inverse_of: :my_module, :dependent => :destroy
|
|
|
|
has_many :samples, through: :sample_my_modules
|
|
|
|
has_many :user_my_modules, inverse_of: :my_module, :dependent => :destroy
|
|
|
|
has_many :users, through: :user_my_modules
|
|
|
|
has_many :activities, inverse_of: :my_module
|
|
|
|
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
|
2016-02-12 23:52:43 +08:00
|
|
|
|
2016-08-09 19:19:32 +08:00
|
|
|
scope :is_archived, ->(is_archived) { where('archived = ?', is_archived) }
|
2016-08-09 17:17:25 +08:00
|
|
|
|
2016-08-19 19:42:28 +08:00
|
|
|
# A module takes this much space in canvas (x, y) in database
|
|
|
|
WIDTH = 30
|
|
|
|
HEIGHT = 14
|
|
|
|
|
2016-02-12 23:52:43 +08:00
|
|
|
def self.search(user, include_archived, query = nil, page = 1)
|
2016-07-25 22:30:59 +08:00
|
|
|
exp_ids =
|
|
|
|
Experiment
|
2016-02-12 23:52:43 +08:00
|
|
|
.search(user, include_archived, nil, SHOW_ALL_RESULTS)
|
|
|
|
.select("id")
|
|
|
|
|
2016-07-21 19:11:15 +08:00
|
|
|
if query
|
|
|
|
a_query = query.strip
|
|
|
|
.gsub("_","\\_")
|
|
|
|
.gsub("%","\\%")
|
|
|
|
.split(/\s+/)
|
|
|
|
.map {|t| "%" + t + "%" }
|
|
|
|
else
|
|
|
|
a_query = query
|
|
|
|
end
|
|
|
|
|
2016-02-12 23:52:43 +08:00
|
|
|
if include_archived
|
|
|
|
new_query = MyModule
|
|
|
|
.distinct
|
2016-07-25 22:30:59 +08:00
|
|
|
.where("my_modules.experiment_id IN (?)", exp_ids)
|
2016-07-21 19:11:15 +08:00
|
|
|
.where_attributes_like([:name, :description], a_query)
|
2016-02-12 23:52:43 +08:00
|
|
|
else
|
|
|
|
new_query = MyModule
|
|
|
|
.distinct
|
2016-07-25 22:30:59 +08:00
|
|
|
.where("my_modules.experiment_id IN (?)", exp_ids)
|
2016-07-25 17:25:10 +08:00
|
|
|
.where("my_modules.archived = ?", false)
|
|
|
|
.where_attributes_like([:name, :description], a_query)
|
2016-02-12 23:52:43 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
# Show all results if needed
|
|
|
|
if page == SHOW_ALL_RESULTS
|
|
|
|
new_query
|
|
|
|
else
|
|
|
|
new_query
|
|
|
|
.limit(SEARCH_LIMIT)
|
|
|
|
.offset((page - 1) * SEARCH_LIMIT)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Removes assigned samples from module and connections with other
|
|
|
|
# modules.
|
|
|
|
def archive (current_user)
|
|
|
|
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.
|
|
|
|
archived = SampleMyModule.destroy_all(:my_module => self) if archived
|
|
|
|
# Remove all connection between modules.
|
|
|
|
archived = Connection.delete_all(:input_id => id) if archived
|
|
|
|
archived = Connection.delete_all(:output_id => id) if archived
|
|
|
|
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
|
|
|
|
restored
|
|
|
|
end
|
|
|
|
|
|
|
|
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 " +
|
2016-07-25 17:25:10 +08:00
|
|
|
"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
|
2016-07-25 17:25:10 +08:00
|
|
|
Sample.where(organization_id: experiment.project.organization).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 " +
|
2016-07-25 17:25:10 +08:00
|
|
|
"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 = 20)
|
|
|
|
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 = 20)
|
|
|
|
last_id = 9999999999999 if last_id <= 1
|
|
|
|
Comment.joins(:my_module_comment)
|
|
|
|
.where(my_module_comments: {my_module_id: id})
|
|
|
|
.where('comments.id < ?', last_id)
|
|
|
|
.order(created_at: :desc)
|
|
|
|
.limit(per_page)
|
|
|
|
end
|
|
|
|
|
|
|
|
def last_activities(last_id = 1, count = 20)
|
|
|
|
last_id = 9999999999999 if last_id <= 1
|
|
|
|
Activity.joins(:my_module)
|
|
|
|
.where(my_module_id: id)
|
|
|
|
.where('activities.id < ?', last_id)
|
|
|
|
.order(created_at: :desc)
|
|
|
|
.limit(count)
|
|
|
|
.uniq
|
|
|
|
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
|
|
|
|
|
2016-07-21 19:11:15 +08:00
|
|
|
def first_n_samples(count = 20)
|
|
|
|
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? and datetime.utc > due_date.utc
|
|
|
|
end
|
|
|
|
|
|
|
|
def overdue_for_days(datetime = DateTime.current)
|
|
|
|
if due_date.blank? or due_date.utc > datetime.utc
|
|
|
|
return 0
|
|
|
|
else
|
|
|
|
return ((datetime.utc.to_i - due_date.utc.to_i) / (60*60*24).to_f).ceil
|
|
|
|
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? and datetime.utc < due_date.utc and datetime.utc > (due_date.utc - diff)
|
|
|
|
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 get_downstream_modules
|
|
|
|
final = []
|
|
|
|
modules = [self]
|
|
|
|
while !modules.empty?
|
|
|
|
my_module = modules.shift
|
|
|
|
if !final.include?(my_module)
|
|
|
|
final << my_module
|
|
|
|
end
|
|
|
|
modules.push(*my_module.my_modules.flatten)
|
|
|
|
end
|
|
|
|
final
|
|
|
|
end
|
|
|
|
|
|
|
|
# Treat this module as inversed root, get all modules of that inversed subtree
|
|
|
|
def get_upstream_modules
|
|
|
|
final = []
|
|
|
|
modules = [self]
|
|
|
|
while !modules.empty?
|
|
|
|
my_module = modules.shift
|
|
|
|
if !final.include?(my_module)
|
|
|
|
final << my_module
|
|
|
|
end
|
|
|
|
modules.push(*my_module.my_module_antecessors.flatten)
|
|
|
|
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
|
|
|
|
|
|
|
|
def deep_clone(current_user)
|
2016-08-16 15:10:45 +08:00
|
|
|
deep_clone_to_experiment(current_user, experiment)
|
2016-08-11 21:13:36 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def deep_clone_to_experiment(current_user, experiment)
|
2016-02-12 23:52:43 +08:00
|
|
|
# Copy the module
|
|
|
|
clone = MyModule.new(
|
|
|
|
name: self.name,
|
2016-08-11 21:13:36 +08:00
|
|
|
experiment: experiment,
|
2016-02-12 23:52:43 +08:00
|
|
|
description: self.description,
|
|
|
|
x: self.x,
|
|
|
|
y: self.y)
|
|
|
|
clone.save
|
|
|
|
|
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
|
|
|
|
clone.protocols << self.protocol.deep_clone_my_module(self, current_user)
|
2016-02-12 23:52:43 +08:00
|
|
|
clone.reload
|
|
|
|
|
2016-08-23 22:15:45 +08:00
|
|
|
# fixes linked protocols
|
|
|
|
clone.protocols.each do |protocol|
|
|
|
|
next unless protocol.linked?
|
|
|
|
protocol.updated_at = protocol.parent_updated_at
|
|
|
|
protocol.save
|
|
|
|
end
|
|
|
|
|
2016-02-12 23:52:43 +08:00
|
|
|
return clone
|
|
|
|
end
|
|
|
|
|
|
|
|
# Writes to user log.
|
|
|
|
def log(message)
|
|
|
|
final = "[%s] %s" % [name, message]
|
2016-07-25 17:25:10 +08:00
|
|
|
experiment.project.log(final)
|
2016-02-12 23:52:43 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
# Find an empty position for the restored module. It's
|
2016-08-19 19:42:28 +08:00
|
|
|
# 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
|
|
|
|
2016-08-19 19:42:28 +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 }
|
2016-08-19 19:42:28 +08:00
|
|
|
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 }
|
2016-08-19 19:42:28 +08:00
|
|
|
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 }
|
2016-08-19 19:42:28 +08:00
|
|
|
end
|
2016-02-12 23:52:43 +08:00
|
|
|
|
2016-08-19 19:42:28 +08:00
|
|
|
private
|
|
|
|
|
|
|
|
def create_blank_protocol
|
|
|
|
protocols << Protocol.new_blank_for_module(self)
|
2016-02-12 23:52:43 +08:00
|
|
|
end
|
|
|
|
end
|