diff --git a/.gitignore b/.gitignore index 25aa411d..e49faf20 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ /test/tmp/ /test/version_tmp/ /tmp/ +.DS_Store ## Specific to RubyMotion: .dat* diff --git a/.travis.yml b/.travis.yml index c4164bba..4f75c1e3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,3 +4,9 @@ language: ruby bundler_args: --without deployment sudo: false cache: bundler +before_install: + - "export PATH=$PWD/travis_phantomjs/phantomjs-2.1.1-linux-x86_64/bin:$PATH" + - "if [ $(phantomjs --version) != '2.1.1' ]; then rm -rf $PWD/travis_phantomjs; mkdir -p $PWD/travis_phantomjs; fi" + - "if [ $(phantomjs --version) != '2.1.1' ]; then wget https://assets.membergetmember.co/software/phantomjs-2.1.1-linux-x86_64.tar.bz2 -O $PWD/travis_phantomjs/phantomjs-2.1.1-linux-x86_64.tar.bz2; fi" + - "if [ $(phantomjs --version) != '2.1.1' ]; then tar -xvf $PWD/travis_phantomjs/phantomjs-2.1.1-linux-x86_64.tar.bz2 -C $PWD/travis_phantomjs; fi" + - "phantomjs --version" diff --git a/Gemfile b/Gemfile index 75520628..73b68f3e 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,7 @@ gem "puma" gem "sinatra" gem "sinatra-activerecord" gem "activerecord" +gem "activesupport" gem "mysql2" gem "rake" gem "pry" @@ -17,4 +18,9 @@ gem "sinatra-assetpack" group :test do gem "rspec" gem "timecop" + gem "database_cleaner" + gem 'factory_girl' + gem "capybara" + gem "poltergeist" + gem 'launchy' end diff --git a/Gemfile.lock b/Gemfile.lock index 6d6a819e..2c8620f5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,37 +1,63 @@ GEM remote: https://rubygems.org/ specs: - activemodel (4.2.2) - activesupport (= 4.2.2) + activemodel (4.2.6) + activesupport (= 4.2.6) builder (~> 3.1) - activerecord (4.2.2) - activemodel (= 4.2.2) - activesupport (= 4.2.2) + activerecord (4.2.6) + activemodel (= 4.2.6) + activesupport (= 4.2.6) arel (~> 6.0) - activesupport (4.2.2) + activesupport (4.2.6) i18n (~> 0.7) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) + addressable (2.5.0) + public_suffix (~> 2.0, >= 2.0.2) arel (6.0.0) bootstrap-sass (3.1.1.1) sass (~> 3.2) builder (3.2.2) + capybara (2.12.1) + addressable + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + xpath (~> 2.0) + cliver (0.3.2) coderay (1.1.0) + database_cleaner (1.5.3) diff-lcs (1.2.5) + factory_girl (4.8.0) + activesupport (>= 3.0.0) i18n (0.7.0) jsmin (1.0.1) json (1.8.3) + launchy (2.4.3) + addressable (~> 2.3) method_source (0.8.2) + mime-types (3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0521) + mini_portile2 (2.1.0) minitest (5.7.0) - mysql2 (0.3.16) + mysql2 (0.3.17) + nokogiri (1.7.0.1) + mini_portile2 (~> 2.1.0) + poltergeist (1.13.0) + capybara (~> 2.1) + cliver (~> 0.3.1) + websocket-driver (>= 0.2.0) pry (0.9.12.6) coderay (~> 1.0) method_source (~> 0.8) slop (~> 3.4) pry-nav (0.2.3) pry (~> 0.9.10) + public_suffix (2.0.5) puma (2.8.2) rack (>= 1.1, < 2.0) rack (1.6.4) @@ -67,14 +93,25 @@ GEM timecop (0.7.4) tzinfo (1.2.2) thread_safe (~> 0.1) + websocket-driver (0.6.5) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.2) + xpath (2.0.0) + nokogiri (~> 1.3) PLATFORMS ruby DEPENDENCIES activerecord + activesupport bootstrap-sass + capybara + database_cleaner + factory_girl + launchy mysql2 + poltergeist pry pry-nav puma @@ -86,4 +123,4 @@ DEPENDENCIES timecop BUNDLED WITH - 1.10.5 + 1.14.5 diff --git a/README.md b/README.md index 17ef9c80..6b0be367 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ How to start it 1) Generate the web assets: -rake assetpack:build +rake build:assets 2) Start the web server and go to http://localhost:9292 diff --git a/Rakefile b/Rakefile index 3eee6c4a..27f84d4b 100644 --- a/Rakefile +++ b/Rakefile @@ -43,3 +43,4 @@ namespace :build do end end +require './lib/tasks/update_old_assets_and_workflows' \ No newline at end of file diff --git a/app.rb b/app.rb index d0758f40..0f299383 100644 --- a/app.rb +++ b/app.rb @@ -98,7 +98,6 @@ class SmWorkflowLims < Sinatra::Base get '/admin' do presenter = AdminController.new(params).get_index - erb :'admin/index', :locals => { :presenter => presenter } end diff --git a/app/controllers/assets_controller.rb b/app/controllers/assets_controller.rb index df82c29f..c64a0183 100644 --- a/app/controllers/assets_controller.rb +++ b/app/controllers/assets_controller.rb @@ -3,50 +3,32 @@ class AssetsController < Controller validate_parameters_for :update, :assets_provided, 'No assets selected' - validate_parameters_for :update, :single_action, 'You cannot complete and report assets at the same time' def update - (Asset::Completer.create!(assets:completed_assets,time:DateTime.now) if completed_assets?)|| - (Asset::Reporter.create!( assets:reported_assets, time:DateTime.now) if reported_assets?) + Asset::Updater.create!(assets: assets_to_be_updated, action: params[:action]) end def index assets = Asset.in_state(state).with_identifier(params[:identifier]) - Presenter::AssetPresenter::Index.new(assets,search,state) + Presenter::AssetPresenter::Index.new(assets, search, state) end private def state - params[:state]||'in_progress' + State.find_by(name: params[:state]) end def search params[:identifier] && "identifier matches '#{params[:identifier]}'" end - def completed_assets? - params[:complete].is_a?(Hash) && params[:complete].keys.present? - end - - def reported_assets? - params[:report].is_a?(Hash) && params[:report].keys.present? - end - def assets_provided - completed_assets? || reported_assets? - end - - def single_action - completed_assets? ^ reported_assets? - end - - def completed_assets - @assets||=Asset.find(params[:complete].keys) + params[:assets].is_a?(Hash) && params[:assets].keys.present? end - def reported_assets - @assets||=Asset.find(params[:report].keys) + def assets_to_be_updated + @assets||=Asset.find(params[:assets].keys) end end diff --git a/app/controllers/workflows_controller.rb b/app/controllers/workflows_controller.rb index 22c141eb..3b7dce6e 100644 --- a/app/controllers/workflows_controller.rb +++ b/app/controllers/workflows_controller.rb @@ -16,10 +16,11 @@ class WorkflowsController < Controller def create Workflow::Creator.create!( - :name => params[:name], - :has_comment => params[:hasComment] || false, - :reportable => params[:reportable] || false, - :turn_around_days => params[:turn_around_days] + name: params[:name], + has_comment: params[:hasComment] || false, + reportable: params[:reportable] || false, + initial_state_name: params[:initial_state_name], + turn_around_days: params[:turn_around_days] ) end @@ -29,11 +30,12 @@ def show def update Workflow::Updater.create!( - :workflow => workflow, - :name => params[:name], - :has_comment => params[:hasComment] || false, - :reportable => params[:reportable] || false, - :turn_around_days => turn_around_days + workflow: workflow, + name: params[:name], + has_comment: params[:hasComment] || false, + reportable: params[:reportable] || false, + initial_state_name: params[:initial_state_name], + turn_around_days: turn_around_days ) end diff --git a/app/manifest.rb b/app/manifest.rb index 7dff484a..6e0edb83 100644 --- a/app/manifest.rb +++ b/app/manifest.rb @@ -8,6 +8,8 @@ require './app/models/asset_type' require './app/models/batch' require './app/models/comment' +require './app/models/event' +require './app/models/state' require './app/models/workflow' require './app/models/pipeline_destination' require './app/models/cost_code' diff --git a/app/models/asset.rb b/app/models/asset.rb index 835851f9..f4a8dac7 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb @@ -1,10 +1,10 @@ require 'active_record' -require './lib/state_scoping' +require './app/models/concerns/state_machine' require './lib/client_side_validations' class Asset < ActiveRecord::Base - extend StateScoping + include StateMachine belongs_to :asset_type belongs_to :workflow @@ -13,8 +13,12 @@ class Asset < ActiveRecord::Base belongs_to :batch belongs_to :comment + has_many :events, dependent: :destroy + after_destroy :remove_comment, :if => :comment + after_create :create_initial_event + include ClientSideValidations validate_with_regexp :study, :with => /^\w+$/ @@ -22,12 +26,16 @@ def remove_comment comment.destroy end - def self.with_identifier(search_string) - search_string.nil? ? all : where(identifier:search_string) + def self.in_state(state) + if state.present? + joins(:events).where(events: {id: Event.latest_per_asset, state: state}) + else + all + end end - def self.in_state(state) - scope_for(state) + def self.with_identifier(search_string) + search_string.nil? ? all : where(identifier: search_string) end before_create :set_begun_at @@ -39,18 +47,15 @@ def set_begun_at validates_presence_of :workflow, :batch, :identifier, :asset_type - delegate :identifier_type, :to => :asset_type + delegate :identifier_type, to: :asset_type scope :in_progress, -> { where(completed_at: nil) } scope :completed, -> { where.not(completed_at: nil) } scope :reportable, -> { where(workflows:{reportable:true}) } scope :report_required, -> { reportable.completed.where(reported_at:nil) } + scope :reported, -> { reportable.completed.where.not(reported_at:nil) } scope :latest_first, -> { order('begun_at DESC') } - add_state('all', :all) - add_state('in_progress', :in_progress) - add_state('report_required', :report_required) - default_scope { includes(:workflow,:asset_type,:comment,:batch) } def reportable? @@ -61,6 +66,10 @@ def completed? completed_at.present? end + def completed_at + super || events.date('completed') + end + def age # DateTime#-(DateTime) Returns the difference in days as a rational (in Ruby 2.2.2) DateTime.now - begun_at.to_datetime @@ -71,17 +80,21 @@ def time_without_completion age end + def create_initial_event + events.create!(state: workflow.initial_state, created_at: begun_at) + end class AssetAction - attr_reader :time, :assets, :state + attr_reader :action, :assets, :state, :asset_state def self.create!(*args) self.new(*args).tap {|action| action.do! } end - def initialize(time:,assets:) - @time = time + def initialize(action:,assets:) + @action = action @assets = assets + @asset_state = assets.first.current_state @state = 'incomplete' end @@ -96,62 +109,22 @@ def identifiers end end - class Completer < AssetAction + class Updater < AssetAction def do! ActiveRecord::Base.transaction do - assets.each {|a| a.update_attributes!(completed_at:time) } + assets.each { |a| a.perform_action(action) } @state = 'success' end true end def message - done? ? "#{identifiers.to_sentence} #{identifiers.many? ? 'were' : 'was'} marked as completed." : - 'Assets have not been completed.' + done? ? "#{asset_state.humanize} is done for #{identifiers.to_sentence}" : + "#{asset_state.humanize} has not been finished for requested assets." end - def redirect_state; 'in_progress'; end - + def redirect_state; asset_state; end end - class Reporter < AssetAction - - def do! - return false unless valid? - ActiveRecord::Base.transaction do - assets.each {|a| a.update_attributes!(reported_at:time) } - @state = 'success' - end - true - end - - def valid? - assets.each do |asset| - asset.reportable? || log_error("#{asset.identifier} is in #{asset.workflow.name}, which does not need a report.") - asset.completed? || log_error("#{asset.identifier} can not be reported on before it is completed.") - end - errors.empty? - end - - def message - return errors.join("\n") if errors.present? - done? ? "#{identifiers.to_sentence} #{identifiers.many? ? 'were' : 'was'} marked as reported." : - 'Assets have not been reported.' - end - - def redirect_state; 'report_required'; end - - private - - def errors - @errors ||= [] - end - - def log_error(message) - @state = 'danger' - errors << message - end - - end end diff --git a/app/models/concerns/state_machine.rb b/app/models/concerns/state_machine.rb new file mode 100644 index 00000000..9e724d5e --- /dev/null +++ b/app/models/concerns/state_machine.rb @@ -0,0 +1,43 @@ +require 'active_support' +require 'active_support/core_ext' +require './app/models/state' + +module StateMachine + + extend ActiveSupport::Concern + + included do + delegate :in_progress?, :volume_check?, :quant?, :report_required?, :reported?, to: :current_state + end + + StateMachineError = Class.new(StandardError) + + VALID_ACTIONS = ['check_volume', 'complete', 'report'] + + def perform_action(action) + if VALID_ACTIONS.include? action + send(action) + else + raise StateMachineError, "#{action} is not a recognised action" + end + end + + def complete + events.create! state_name: 'completed' if (in_progress? || quant?) + events.create! state_name: 'report_required' if reportable? + end + + def check_volume + events.create! state_name: 'quant' if volume_check? + end + + def report + events.create! state_name: 'reported' if report_required? + end + + def current_state + events.last.state.name.inquiry + end + +end + diff --git a/app/models/event.rb b/app/models/event.rb new file mode 100644 index 00000000..8933761f --- /dev/null +++ b/app/models/event.rb @@ -0,0 +1,23 @@ +require 'active_record' + +class Event < ActiveRecord::Base + belongs_to :asset + belongs_to :state + + validates_presence_of :asset_id, :state_id + + attr_accessor :state_name + + def state_name=(state_name) + self.state = State.find_by(name: state_name) + end + + def self.date(state_name) + where(state: State.find_by(name: state_name)).first.try(:created_at) + end + + def self.latest_per_asset + Event.group(:asset_id).maximum(:id).values + end + +end \ No newline at end of file diff --git a/app/models/state.rb b/app/models/state.rb new file mode 100644 index 00000000..6ac527b6 --- /dev/null +++ b/app/models/state.rb @@ -0,0 +1,21 @@ +require 'active_record' + +class State < ActiveRecord::Base + has_many :events + has_many :workflows + + validates_presence_of :name + validates_uniqueness_of :name + + def default? + name == 'in_progress' + end + + # Multi-Team quant essential is hopefully a temporary + # situation, and should be replaced soon with something + # less hard-coded. 15/03/2017 + def multi_team_quant_essential? + !default? + end + +end diff --git a/app/models/workflow.rb b/app/models/workflow.rb index d18350fa..a4b368eb 100644 --- a/app/models/workflow.rb +++ b/app/models/workflow.rb @@ -3,52 +3,69 @@ class Workflow < ActiveRecord::Base has_many :assets + belongs_to :initial_state, class_name: 'State' - validates_presence_of :name + validates_presence_of :name, :initial_state validates_uniqueness_of :name validates_numericality_of :turn_around_days, :greater_than_or_equal_to => 0, :allow_nil => true, :only_integer => true - class Creator + attr_accessor :initial_state_name + + def initial_state_name=(initial_state_name) + self.initial_state = State.find_by(name: initial_state_name) + end + + def multi_team_quant_essential + initial_state.multi_team_quant_essential? + end - attr_reader :name, :has_comment, :reportable, :turn_around_days + class Creator + include + attr_reader :name, :has_comment, :reportable, :turn_around_days, :initial_state_name def self.create!(*args) self.new(*args).do! end - def initialize(name:,has_comment:,reportable:,turn_around_days:nil) + def initialize(name:, has_comment:, reportable:, initial_state_name:, turn_around_days:nil) @name = name @has_comment = has_comment @reportable = reportable + @initial_state_name = initial_state_name + @turn_around_days = turn_around_days end def do! ActiveRecord::Base.transaction do - Workflow.new(:name => name, :has_comment => has_comment, :reportable => reportable).save! + Workflow.new(name: name, has_comment: has_comment, reportable: reportable, initial_state_name: initial_state_name, turn_around_days: turn_around_days).save! end end + end class Updater - attr_reader :name, :has_comment, :reportable, :turn_around_days, :workflow, :turn_around_days + attr_reader :name, :has_comment, :reportable, :turn_around_days, :workflow, :initial_state_name def self.create!(*args) self.new(*args).do! end - def initialize(workflow:,name:,has_comment:,reportable:,turn_around_days:nil) + def initialize(workflow:, name:, has_comment:, reportable:, initial_state_name:, turn_around_days:nil) @workflow = workflow @name = name @has_comment = has_comment @reportable = reportable @turn_around_days = turn_around_days + @initial_state_name = initial_state_name end def do! ActiveRecord::Base.transaction do - workflow.update_attributes!(name:name, has_comment: has_comment, reportable: reportable, turn_around_days: turn_around_days) + workflow.update_attributes!(name:name, has_comment: has_comment, reportable: reportable, turn_around_days: turn_around_days, initial_state_name: initial_state_name) end end + end + end diff --git a/app/presenters/asset/index.rb b/app/presenters/asset/index.rb index cb00022c..6bf45588 100644 --- a/app/presenters/asset/index.rb +++ b/app/presenters/asset/index.rb @@ -10,7 +10,7 @@ def initialize(found_assets,search=nil,state=nil) @total = found_assets.count @assets = found_assets.group_by {|a| a.asset_type.name}.tap {|h| h.default = [] } @search = search - @state = state||'in_progress' + @state = state.name if state end def asset_identifiers @@ -55,17 +55,17 @@ def state end def action - { - 'in_progress' => 'complete', + {'in_progress' => 'complete', + 'volume_check' => 'check_volume', + 'quant' => 'complete', 'report_required' => 'report' - }[@state].tap do |action| - yield action if action.present? - end + }[@state] end def action_button - { - 'in_progress' => 'Completed selected', + {'in_progress' => 'Completed selected', + 'volume_check' => 'Volume checked selected', + 'quant' => 'Completed selected', 'report_required' => 'Reported selected' }[@state].tap do |button| yield button if button.present? diff --git a/app/presenters/presenter.rb b/app/presenters/presenter.rb index 94c22b3c..8993398e 100644 --- a/app/presenters/presenter.rb +++ b/app/presenters/presenter.rb @@ -38,7 +38,7 @@ def with_each_asset_type def each_workflow Workflow.all.each do |workflow| - yield(workflow.name,workflow.has_comment,workflow.id,workflow.reportable,workflow.turn_around_days) + yield(workflow.name, workflow.has_comment, workflow.id, workflow.reportable, workflow.multi_team_quant_essential, workflow.turn_around_days) end end diff --git a/app/presenters/workflow/show.rb b/app/presenters/workflow/show.rb index 0786d020..1849b5f3 100644 --- a/app/presenters/workflow/show.rb +++ b/app/presenters/workflow/show.rb @@ -10,7 +10,7 @@ def initialize(workflow) @workflow = workflow end - delegate :name, :has_comment, :reportable, :to => :workflow + delegate :name, :has_comment, :reportable, :multi_team_quant_essential, to: :workflow def turn_around workflow.turn_around_days diff --git a/app/views/_menu.erb b/app/views/_menu.erb index 5b080d77..a3cb7d3e 100644 --- a/app/views/_menu.erb +++ b/app/views/_menu.erb @@ -29,13 +29,14 @@
  • <%= asset_type.name %>
  • <% end %> -
  • In Progress
  • +
  • In Progress
  • +
  • Volume check
  • +
  • Quant
  • Report Required
  • Admin