Merge branch 'ur-SCI-2924' (Refactor generate_workflow_image into service)

This commit is contained in:
Urban Rotnik 2019-01-18 17:01:23 +01:00
commit 30d0fcdf00
18 changed files with 215 additions and 211 deletions

5
.overcommit.yml Normal file
View file

@ -0,0 +1,5 @@
PreCommit:
RuboCop:
enabled: true
on_warn: fail
problem_on_unmodified_line: ignore

View file

@ -109,6 +109,7 @@ group :development, :test do
gem 'byebug'
gem 'factory_bot_rails'
gem 'listen', '~> 3.0'
gem 'overcommit'
gem 'pry'
gem 'pry-byebug'
gem 'pry-rails'

View file

@ -156,6 +156,8 @@ GEM
mail
case_transform (0.2)
activesupport
childprocess (0.9.0)
ffi (~> 1.0, >= 1.0.11)
climate_control (0.2.0)
cliver (0.3.2)
coderay (1.1.2)
@ -241,6 +243,7 @@ GEM
concurrent-ruby (~> 1.0)
i18n-js (3.0.3)
i18n (~> 0.6, >= 0.6.6)
iniparse (1.4.4)
jaro_winkler (1.5.1)
jbuilder (2.7.0)
activesupport (>= 4.2.0)
@ -330,6 +333,9 @@ GEM
oauth2 (~> 1.1)
omniauth (~> 1.2)
orm_adapter (0.5.0)
overcommit (0.46.0)
childprocess (~> 0.6, >= 0.6.3)
iniparse (~> 1.4)
paperclip (5.3.0)
activemodel (>= 4.2.0)
activesupport (>= 4.2.0)
@ -590,6 +596,7 @@ DEPENDENCIES
nokogiri (~> 1.8.1)
omniauth
omniauth-linkedin-oauth2
overcommit
paperclip (~> 5.3)
pg (~> 0.18)
phantomjs
@ -637,4 +644,4 @@ RUBY VERSION
ruby 2.4.4p296
BUNDLED WITH
1.16.6
1.17.1

View file

@ -98,12 +98,6 @@ class Experiment < ApplicationRecord
end
end
def modules_without_group
MyModule.where(experiment_id: id)
.where(my_module_group: nil)
.where(archived: false)
end
def active_module_groups
my_module_groups.joins(:my_modules)
.where('my_modules.archived = ?', false)
@ -227,125 +221,8 @@ class Experiment < ApplicationRecord
true
end
# This method generate the workflow image and saves it as
# experiment attachment
def generate_workflow_img
require 'graphviz'
graph = GraphViz.new(:G,
type: :digraph,
use: :neato)
graph[:size] = '4,4'
graph.node[color: Constants::COLOR_ALTO,
style: :filled,
fontcolor: Constants::COLOR_EMPEROR,
shape: 'circle',
fontname: 'Arial',
fontsize: '16.0']
graph.edge[color: Constants::COLOR_ALTO]
label = ''
subg = {}
# Draw orphan modules
if modules_without_group
modules_without_group.each do |my_module|
graph
.subgraph(rank: 'same')
.add_nodes("Orphan-#{my_module.id}",
label: label,
pos: "#{my_module.x / 10},-#{my_module.y / 10}!")
end
end
# Draw grouped modules
if my_module_groups.many?
my_module_groups.each_with_index do |group, gindex|
subgraph_name = "cluster-#{gindex}"
subg[subgraph_name] = graph.subgraph(rank: 'same')
group.ordered_modules.each_with_index do |my_module, index|
if my_module.outputs.any?
parent = subg[subgraph_name]
.add_nodes("#{subgraph_name}-#{index}",
label: label,
pos: "#{my_module.x / 10},-#{my_module.y / 10}!")
my_module.outputs.each_with_index do |output, i|
child_mod = MyModule.find_by_id(output.input_id)
child_node = subg[subgraph_name]
.add_nodes("#{subgraph_name}-O#{child_mod.id}-#{i}",
label: label,
pos: "#{child_mod.x / 10},-#{child_mod.y / 10}!")
subg[subgraph_name].add_edges(parent, child_node)
end
elsif my_module.inputs.any?
parent = subg[subgraph_name]
.add_nodes("#{subgraph_name}-#{index}",
label: label,
pos: "#{my_module.x / 10},-#{my_module.y / 10}!")
my_module.inputs.each_with_index do |input, i|
child_mod = MyModule.find_by_id(input.output_id)
child_node = subg[subgraph_name]
.add_nodes("#{subgraph_name}-I#{child_mod.id}-#{i}",
label: label,
pos: "#{child_mod.x / 10},-#{child_mod.y / 10}!")
subg[subgraph_name].add_edges(child_node, parent)
end
end
end
end
else
my_module_groups.each do |group|
group.ordered_modules.each_with_index do |my_module, index|
if my_module.outputs.any?
parent = graph.add_nodes("N-#{index}",
label: label,
pos: "#{my_module.x / 10},-#{ my_module.y / 10}!")
my_module.outputs.each_with_index do |output, i|
child_mod = MyModule.find_by_id(output.input_id)
child_node = graph
.add_nodes("N-O#{child_mod.id}-#{i}",
label: label,
pos: "#{child_mod.x / 10},-#{child_mod.y / 10}!")
graph.add_edges(parent, child_node)
end
elsif my_module.inputs.any?
parent = graph.add_nodes("N-#{index}",
label: label,
pos: "#{my_module.x / 10},-#{my_module.y / 10}!")
my_module.inputs.each_with_index do |input, i|
child_mod = MyModule.find_by_id(input.output_id)
child_node = graph
.add_nodes("N-I#{child_mod.id}-#{i}",
label: label,
pos: "#{child_mod.x / 10},-#{child_mod.y / 10}!")
graph.add_edges(child_node, parent)
end
end
end
end
end
file_location = Tempfile.open(['wimg', '.png'],
Rails.root.join('tmp'))
graph.output(png: file_location.path)
begin
file = File.open(file_location)
self.workflowimg = file
file.close
save
touch(:workflowimg_updated_at)
rescue => ex
logger.error ex.message
end
Experiments::GenerateWorkflowImageService.call(experiment_id: id)
end
# Clone this experiment to given project
@ -371,7 +248,7 @@ class Experiment < ApplicationRecord
end
# Copy modules without group
clone.my_modules << modules_without_group.map do |m|
clone.my_modules << my_modules.without_group.map do |m|
m.deep_clone_to_experiment(current_user, clone)
end
clone.save

View file

@ -68,11 +68,13 @@ class MyModule < ApplicationRecord
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) }
# A module takes this much space in canvas (x, y) in database
WIDTH = 30

View file

@ -10,10 +10,6 @@ class MyModuleGroup < ApplicationRecord
optional: true
has_many :my_modules, inverse_of: :my_module_group, dependent: :nullify
def ordered_modules
my_modules.order(workflow_order: :asc)
end
def deep_clone_to_experiment(current_user, experiment)
clone = MyModuleGroup.new(
created_by: created_by,
@ -21,12 +17,12 @@ class MyModuleGroup < ApplicationRecord
)
# Get clones of modules from this group, save them as hash
cloned_modules = ordered_modules.each_with_object({}) do |m, hash|
hash[m.id] = m.deep_clone_to_experiment(current_user, experiment)
hash
cloned_modules = my_modules.workflow_ordered.each_with_object({}) do |m, h|
h[m.id] = m.deep_clone_to_experiment(current_user, experiment)
h
end
ordered_modules.each do |m|
my_modules.workflow_ordered.each do |m|
# Copy connections
m.inputs.each do |inp|
Connection.create(

View file

@ -0,0 +1,87 @@
# frozen_string_literal: true
module Experiments
class GenerateWorkflowImageService
extend Service
require 'graphviz'
attr_reader :errors
def initialize(experiment_id:)
@exp = Experiment.find experiment_id
@graph = GraphViz.new(:G, type: :digraph, use: :neato)
@graph[:size] = '4,4'
@graph.node[color: Constants::COLOR_ALTO,
style: :filled,
fontcolor: Constants::COLOR_EMPEROR,
shape: 'circle',
fontname: 'Arial',
fontsize: '16.0']
@graph.edge[color: Constants::COLOR_ALTO]
@errors = []
end
def call
draw_diagram
save_file
self
end
def succeed?
@errors.none?
end
private
def draw_diagram
# Draw orphan nodes
@exp.my_modules.without_group.each do |my_module|
@graph.subgraph(rank: 'same').add_nodes(
"Orphan-#{my_module.id}",
label: '',
pos: "#{my_module.x / 10},-#{my_module.y / 10}!"
)
end
# Draw grouped modules
subg = {}
@exp.my_module_groups.each_with_index do |group, gindex|
subgraph_id = "cluster-#{gindex}"
subg[subgraph_id] = @graph.subgraph(rank: 'same')
nodes = {}
group.my_modules.workflow_ordered.each_with_index do |my_module, index|
# draw nodes
node = subg[subgraph_id].add_nodes(
"#{subgraph_id}-#{index}",
label: '',
pos: "#{my_module.x / 10},-#{my_module.y / 10}!"
)
nodes[my_module.id] = node
end
# draw edges
group.my_modules.workflow_ordered.each do |m|
m.outputs.each do |output|
parent_node = nodes[m.id]
child_node = nodes[output.input_id]
subg[subgraph_id].add_edges(parent_node, child_node)
end
end
end
end
def save_file
file = Tempfile.open(%w(wimg .png), Rails.root.join('tmp'))
@graph.output(png: file.path)
@exp.workflowimg = file
file.close
@exp.save
@exp.touch(:workflowimg_updated_at)
rescue StandardError => ex
logger.error ex.message
end
end
end

7
app/services/service.rb Normal file
View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
module Service
def call(*args)
new(*args).call
end
end

View file

@ -13,7 +13,7 @@
<% experiment.my_module_groups.each do |my_module_group| %>
<% if my_module_group.my_modules.exists? then %>
<% my_module_group.ordered_modules.each do |my_module| %>
<% my_module_group.my_modules.workflow_ordered.each do |my_module| %>
<li>
<%= form.check_box "modules[#{my_module.id}]", label: my_module.name %>
</li>
@ -22,13 +22,10 @@
<% end %>
<!-- Tasks without groups -->
<% modules_without_group = experiment.modules_without_group %>
<% if modules_without_group.exists? %>
<% modules_without_group.each do |my_module| %>
<li>
<%= form.check_box "modules[#{my_module.id}]", label: my_module.name %>
</li>
<% end %>
<% experiment.my_modules.without_group.each do |my_module| %>
<li>
<%= form.check_box "modules[#{my_module.id}]", label: my_module.name %>
</li>
<% end %>
</ul>

View file

@ -19,7 +19,7 @@
<% experiment.my_module_groups.each do |my_module_group| %>
<% next unless my_module_group.my_modules.is_archived(false).exists? %>
<% my_module_group.ordered_modules.is_archived(false).each do |my_module| %>
<% my_module_group.my_modules.workflow_ordered.is_archived(false).each do |my_module| %>
<li>
<%= form.check_box "modules[#{my_module.id}]", label: my_module.name %>
</li>
@ -27,13 +27,10 @@
<% end %>
<!-- Tasks without groups -->
<% modules_without_group = experiment.modules_without_group %>
<% if modules_without_group.exists? %>
<% modules_without_group.each do |my_module| %>
<li>
<%= form.check_box "modules[#{my_module.id}]", label: my_module.name %>
</li>
<% end %>
<% experiment.my_modules.without_group.each do |my_module| %>
<li>
<%= form.check_box "modules[#{my_module.id}]", label: my_module.name %>
</li>
<% end %>
</ul>

View file

@ -1,26 +1,19 @@
# frozen_string_literal: true
FactoryBot.define do
factory :experiment do
name { Faker::Name.unique.name }
description 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.'
end
factory :experiment_one, class: Experiment do
name 'My Experiment One'
description 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.'
association :project, factory: :project
association :created_by, factory: :user, email: Faker::Internet.email
association :last_modified_by,
factory: :user,
email: Faker::Internet.email
end
factory :experiment_two, class: Experiment do
name Faker::Name.name
description 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.'
association :project, factory: :project
association :created_by, factory: :user, email: Faker::Internet.email
association :last_modified_by,
factory: :user,
email: Faker::Internet.email
factory :experiment, class: Experiment do
transient do
user { create :user }
end
sequence(:name) { |n| "Experiment-#{n}" }
description { Faker::Lorem.sentence }
created_by { user }
last_modified_by { user }
project { create :project, created_by: user }
factory :experiment_with_tasks do
after(:create) do |e|
create_list :my_module, 3, experiment: e
end
end
end
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
factory :my_module_group do
experiment { Experiment.first || create(:experiment_two) }
experiment
end
end

View file

@ -1,10 +1,12 @@
# frozen_string_literal: true
FactoryBot.define do
factory :my_module do
name 'My first module'
x 0
y 0
workflow_order 0
experiment { Experiment.first || create(:experiment_one) }
my_module_group { MyModuleGroup.first || create(:my_module_group) }
sequence(:name) { |n| "Task-#{n}" }
x { Faker::Number.between(1, 100) }
y { Faker::Number.between(1, 100) }
workflow_order { MyModule.where(experiment_id: experiment.id).count + 1 }
experiment
my_module_group { create :my_module_group, experiment: experiment }
end
end

View file

@ -1,10 +1,12 @@
# frozen_string_literal: true
FactoryBot.define do
factory :project do
created_by { User.first || association(:project_user) }
team { Team.first || association(:project_team) }
archived false
name 'My project'
visibility 'hidden'
sequence(:name) { |n| "My project-#{n}" }
association :created_by, factory: :user
team { create :team, created_by: created_by }
archived { false }
visibility { 'hidden' }
end
factory :project_user, class: User do
@ -14,11 +16,4 @@ FactoryBot.define do
password 'asdf1243'
password_confirmation 'asdf1243'
end
factory :project_team, class: Team do
created_by { User.first || association(:project_user) }
name 'My team'
description 'Lorem ipsum dolor sit amet, consectetuer adipiscing eli.'
space_taken 1048576
end
end

View file

@ -1,8 +1,10 @@
# frozen_string_literal: true
FactoryBot.define do
factory :team do
created_by { User.first || create(:user) }
name 'My team'
description 'Lorem ipsum dolor sit amet, consectetuer adipiscing eli.'
space_taken 1048576
association :created_by, factory: :user
sequence(:name) { |n| "My team-#{n}" }
description { Faker::Lorem.sentence }
space_taken { 1048576 }
end
end

View file

@ -1,8 +1,10 @@
# frozen_string_literal: true
FactoryBot.define do
factory :user do
full_name 'admin'
initials 'AD'
email Faker::Internet.unique.email
sequence(:email) { |n| "user-#{n}@example.com" }
password 'asdf1243'
password_confirmation 'asdf1243'
current_sign_in_at DateTime.now

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'rails_helper'
describe Project, type: :model do
@ -39,22 +41,30 @@ describe Project, type: :model do
it { should have_many :report_elements }
end
describe 'Should be a valid object' do
let(:user) { create :user }
let(:team) { create :team, created_by: user }
it { should validate_presence_of :visibility }
it { should validate_presence_of :team }
it do
should validate_length_of(:name).is_at_least(Constants::NAME_MIN_LENGTH)
.is_at_most(Constants::NAME_MAX_LENGTH)
describe 'Validations' do
describe '#visibility' do
it { should validate_presence_of :visibility }
end
it 'should have a unique name scoped to team' do
create :project, created_by: user, last_modified_by: user, team: team
project_two = build :project, created_by: user,
last_modified_by: user,
team: team
expect(project_two).to_not be_valid
describe '#team' do
it { should validate_presence_of :team }
end
describe '#name' do
it 'should be at least 2 long and max 255 long' do
should validate_length_of(:name)
.is_at_least(Constants::NAME_MIN_LENGTH)
.is_at_most(Constants::NAME_MAX_LENGTH)
end
it 'should be uniq per project and team' do
first_project = create :project
second_project = build :project,
name: first_project.name,
team: first_project.team
expect(second_project).to_not be_valid
end
end
end
end

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
require 'rails_helper'
describe Experiments::GenerateWorkflowImageService do
let(:experiment) { create :experiment_with_tasks }
let(:params) { { experiment_id: experiment.id } }
context 'when succeed' do
it 'succeed? returns true' do
expect(described_class.call(params).succeed?).to be_truthy
end
it 'worklfow image of experiment is updated' do
old_filename = experiment.workflowimg_file_name
described_class.call(params)
experiment.reload
expect(experiment.workflowimg_file_name).not_to be == old_filename
end
end
end