initial commit
This commit is contained in:
+48
@@ -0,0 +1,48 @@
|
|||||||
|
*.rbc
|
||||||
|
capybara-*.html
|
||||||
|
.rspec
|
||||||
|
/log
|
||||||
|
/spec/dummy/log
|
||||||
|
/Gemfile.lock
|
||||||
|
/tmp
|
||||||
|
/spec/dummy/tmp
|
||||||
|
/spec/dummy/files
|
||||||
|
/spec/dummy/public/uploads
|
||||||
|
/spec/dummy/db/*.sqlite3
|
||||||
|
/spec/dummy/db/*.sqlite3-journal
|
||||||
|
/db/*.sqlite3
|
||||||
|
/db/*.sqlite3-journal
|
||||||
|
/public/system
|
||||||
|
/coverage/
|
||||||
|
/spec/tmp
|
||||||
|
**.orig
|
||||||
|
rerun.txt
|
||||||
|
pickle-email-*.html
|
||||||
|
|
||||||
|
# TODO Comment out these rules if you are OK with secrets being uploaded to the repo
|
||||||
|
config/initializers/secret_token.rb
|
||||||
|
config/secrets.yml
|
||||||
|
|
||||||
|
## Environment normalization:
|
||||||
|
/.bundle
|
||||||
|
/vendor/bundle
|
||||||
|
|
||||||
|
# these should all be checked in to normalize the environment:
|
||||||
|
# Gemfile.lock, .ruby-version, .ruby-gemset
|
||||||
|
|
||||||
|
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
||||||
|
.rvmrc
|
||||||
|
.ruby-version
|
||||||
|
|
||||||
|
# if using bower-rails ignore default bower_components path bower.json files
|
||||||
|
/vendor/assets/bower_components
|
||||||
|
*.bowerrc
|
||||||
|
bower.json
|
||||||
|
|
||||||
|
# Ignore pow environment settings
|
||||||
|
.powenv
|
||||||
|
*.gem
|
||||||
|
|
||||||
|
# ignore rails generated namespace object already handled by lib/dunlop.rb
|
||||||
|
/app/models/dunlop/dunlop.rb
|
||||||
|
/spec/dummy/routes.txt
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
language: ruby
|
||||||
|
gemfile:
|
||||||
|
- gemfiles/Gemfile.rails-4.2.x
|
||||||
|
- gemfiles/Gemfile.rails-5.1.x
|
||||||
|
rvm:
|
||||||
|
- 2.2.7
|
||||||
|
- 2.4.1
|
||||||
|
- 2.4.1-clang
|
||||||
|
#matrix:
|
||||||
|
# exclude:
|
||||||
|
# rvm: 2.1.9
|
||||||
|
# gemfile: gemfiles/Gemfile.rails-5.0.x
|
||||||
|
before_install:
|
||||||
|
- sudo apt-get install -y dos2unix
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
### v1.2.0
|
||||||
|
* rename dunlop to dunlop-core
|
||||||
|
* drop draper dependency
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
|
# Declare your gem's dependencies in dunlop.gemspec.
|
||||||
|
# Bundler will treat runtime dependencies like base dependencies, and
|
||||||
|
# development dependencies will be added by default to the :development group.
|
||||||
|
#
|
||||||
|
gemspec
|
||||||
|
|
||||||
|
gem "rails", "~> 5.1.3"
|
||||||
|
|
||||||
|
group :development, :test do
|
||||||
|
gem 'rspec-rails'
|
||||||
|
gem 'sqlite3'
|
||||||
|
gem 'pry-rails'
|
||||||
|
gem 'pry-doc'
|
||||||
|
gem 'kaminari'
|
||||||
|
gem 'spring'
|
||||||
|
gem 'spring-commands-rspec'
|
||||||
|
gem 'semantic-ui-sass'
|
||||||
|
gem "select2-rails"
|
||||||
|
#gem 'carrierwave'
|
||||||
|
#gem 'fog-aws'
|
||||||
|
gem 'uuidtools'
|
||||||
|
end
|
||||||
|
|
||||||
|
gem 'jquery-rails'
|
||||||
|
gem 'high_voltage'
|
||||||
|
|
||||||
|
group :test do
|
||||||
|
gem 'timecop'
|
||||||
|
gem "generator_spec"
|
||||||
|
gem "capybara"
|
||||||
|
gem 'capybara-screenshot'
|
||||||
|
gem 'poltergeist'
|
||||||
|
gem 'factory_bot_rails'
|
||||||
|
gem 'database_cleaner'
|
||||||
|
gem 'rspec-its'
|
||||||
|
end
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
dunlop
|
||||||
|
============
|
||||||
|
<!--[](https://travis-ci.org/mozolutions/dunlop-core)-->
|
||||||
|
|
||||||
|
Installation
|
||||||
|
-----------------------
|
||||||
|
```
|
||||||
|
gem 'dunlop-core', github: 'mozolutions/dunlop-core', branch: :master
|
||||||
|
gem 'devise'
|
||||||
|
```
|
||||||
|
Then
|
||||||
|
```
|
||||||
|
bundle update
|
||||||
|
```
|
||||||
|
Then
|
||||||
|
```
|
||||||
|
bundle exec rails generate devise:install
|
||||||
|
bundle exec rails generate devise User
|
||||||
|
bundle exec rails generate dunlop:install # for only the basics: rails g dunlop:install:base
|
||||||
|
bundle exec rake db:migrate
|
||||||
|
bundle exec rake db:seed
|
||||||
|
```
|
||||||
|
|
||||||
|
ISSUES
|
||||||
|
----------------
|
||||||
|
* js-routes not updated after generator command `rake tmp:clear` needed
|
||||||
|
|
||||||
|
|
||||||
|
TODO
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
* ChangeEvent .. change_event
|
||||||
|
* Make it behaves like a workflow step specs pass if only one workflow
|
||||||
|
step is associated
|
||||||
|
* Workflow step filtering: q%5Bworkflow_instance_batch_id_eq%5D=1&q%5Bworkflow_step%5D%5Bowner_initialization%5D=overdue
|
||||||
|
* Add reverse overdue handling
|
||||||
|
* Implement whenever:install on base install and runner "SourceFile.load_scheduled!" for source_file install generator
|
||||||
|
* Separate Real (user) notes from log notes
|
||||||
|
|
||||||
|
* Franky: Workflow step precedence
|
||||||
|
* Workflow step generation tool
|
||||||
|
* Single sign on/user management combined for projects
|
||||||
|
* Reporting
|
||||||
|
* Mijlpalen functionaliteit:
|
||||||
|
* filters
|
||||||
|
* source file file size
|
||||||
|
|
||||||
|
* Alles in de batch behalve twee
|
||||||
|
* Test bundler semantic tag locking
|
||||||
|
|
||||||
|
### Generators
|
||||||
|
* WorkflowInstanceScrubber in dunlop:install:workflow
|
||||||
|
* spec/rails_helper.rb as complete drop in replacement
|
||||||
|
* replace parent workflow instance with first generated scenario if
|
||||||
|
defined:
|
||||||
|
* factory :workflow_instance_for_workflow_step_test, parent: :workflow_instance do
|
||||||
|
* with
|
||||||
|
* factory :workflow_instance_for_workflow_step_test, parent: :workflow_instance_#{scenario} do
|
||||||
|
* Also check for this by scenario_rename action/generator
|
||||||
|
|
||||||
|
|
||||||
|
## Presentatie
|
||||||
|
* visie
|
||||||
|
* Wat is er al
|
||||||
|
* waar willen we naartoe
|
||||||
|
* samenvoegen van projecten? One user
|
||||||
|
* Source file portal?
|
||||||
|
|
||||||
|
Application creation log
|
||||||
|
---------------------------------------
|
||||||
|
```
|
||||||
|
rails g dunlop:scenario scenario1 --workflow_steps=all
|
||||||
|
rails g dunlop:workflow_step owner_initialization \
|
||||||
|
--scenarios=all \
|
||||||
|
--attributes=checked:boolean plan_date:date my_identifier window_from:integer window_to:integer number_of_visits:integer my_fraction:float my_explanation:text
|
||||||
|
```
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
begin
|
||||||
|
require 'bundler/setup'
|
||||||
|
rescue LoadError
|
||||||
|
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
||||||
|
end
|
||||||
|
|
||||||
|
require 'rdoc/task'
|
||||||
|
|
||||||
|
RDoc::Task.new(:rdoc) do |rdoc|
|
||||||
|
rdoc.rdoc_dir = 'rdoc'
|
||||||
|
rdoc.title = 'Dunlop'
|
||||||
|
rdoc.options << '--line-numbers'
|
||||||
|
rdoc.rdoc_files.include('README.rdoc')
|
||||||
|
rdoc.rdoc_files.include('lib/**/*.rb')
|
||||||
|
end
|
||||||
|
|
||||||
|
APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
|
||||||
|
load 'rails/tasks/engine.rake'
|
||||||
|
|
||||||
|
|
||||||
|
load 'rails/tasks/statistics.rake'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Bundler::GemHelper.install_tasks
|
||||||
|
|
||||||
|
require 'rspec/core'
|
||||||
|
require 'rspec/core/rake_task'
|
||||||
|
|
||||||
|
desc "Run all specs in spec directory (excluding plugin specs)"
|
||||||
|
RSpec::Core::RakeTask.new(spec: 'app:db:test:prepare')
|
||||||
|
|
||||||
|
task default: :spec
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#= require ./dunlop/base
|
||||||
|
#= require ./dunlop-semantic/ui-dropdown
|
||||||
|
#= require ./dunlop-semantic/ui-checkbox
|
||||||
|
#= require ./dunlop-semantic/calendar
|
||||||
|
#= require dunlop/time-display
|
||||||
|
#= require moment
|
||||||
|
#= require moment/nl
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
|||||||
|
#= require ./calendar-vendor
|
||||||
|
Dunlop.register_setup 'calendar', (target) ->
|
||||||
|
target.find('.datepicker').calendar
|
||||||
|
firstDayOfWeek: 1
|
||||||
|
type: 'date'
|
||||||
|
formatter:
|
||||||
|
date: (date, settings) ->
|
||||||
|
return '' unless date
|
||||||
|
moment(date).format().substr(0, 10)
|
||||||
|
datetime: (date, settings) ->
|
||||||
|
return '' unless date
|
||||||
|
moment(date).format().substr(0, 10)
|
||||||
|
|
||||||
|
target.find('.timepicker').calendar
|
||||||
|
ampm: false
|
||||||
|
firstDayOfWeek: 1
|
||||||
|
type: 'time'
|
||||||
|
formatter:
|
||||||
|
time: (date, settings) ->
|
||||||
|
return '' unless date
|
||||||
|
date.toISOString()
|
||||||
|
moment(date).format()
|
||||||
|
datetime: (date, settings) ->
|
||||||
|
return '' unless date
|
||||||
|
moment(date).format()
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Dunlop.register_setup 'ui-checkbox', (target) ->
|
||||||
|
target.find('.ui.checkbox').checkbox()
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
Dunlop.register_setup 'ui-dropdown', (target) ->
|
||||||
|
target.find('.ui.dropdown').each ->
|
||||||
|
el = $(@)
|
||||||
|
settings = el.data() || {}
|
||||||
|
can_clear = not el.find('option:first').val()
|
||||||
|
settings.fireOnInit = true
|
||||||
|
settings.onChange = (value, text, item) ->
|
||||||
|
return unless can_clear
|
||||||
|
dropdown = $(@)
|
||||||
|
if value
|
||||||
|
clear_icon = $('<i class="delete icon"></i>').click ->
|
||||||
|
dropdown.dropdown('clear')
|
||||||
|
$(@).remove()
|
||||||
|
el.after clear_icon
|
||||||
|
el.dropdown settings
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#= require ./dunlop/base
|
||||||
|
#= require dunlop/collapsible
|
||||||
|
#= require dunlop/categorize-as-button
|
||||||
|
#= require dunlop/collection-button
|
||||||
|
#= require dunlop/translations
|
||||||
|
#= require dunlop/display-badges
|
||||||
|
#= require dunlop/time-display
|
||||||
|
#= require pickadate/picker
|
||||||
|
#= require pickadate/picker.date
|
||||||
|
#= require pickadate/picker.time
|
||||||
|
#= require inflection
|
||||||
|
#= require js-routes
|
||||||
|
#= require moment
|
||||||
|
#= require moment/nl
|
||||||
|
#= require record_collection/all
|
||||||
|
Dunlop.register_setup (target)->
|
||||||
|
target.find('input.datepicker').each ->
|
||||||
|
options =
|
||||||
|
format: 'yyyy-mm-dd'
|
||||||
|
editable: true
|
||||||
|
selectMonths: true
|
||||||
|
if selectYears = $(@).data('selectYears')
|
||||||
|
options.selectYears = selectYears
|
||||||
|
$(@).pickadate(options)
|
||||||
|
|
||||||
|
##DEPRICATED use data-bytes instead
|
||||||
|
#$('[data-size]').each (i)->
|
||||||
|
# bytes = $(@).data("size")
|
||||||
|
# $(@).text human_size(bytes)
|
||||||
|
|
||||||
|
|
||||||
|
target.optionals()
|
||||||
|
|
||||||
|
$ ->
|
||||||
|
try
|
||||||
|
HawkEye.add_general_configuration
|
||||||
|
facet:
|
||||||
|
state:
|
||||||
|
colors:
|
||||||
|
uncategorized: "#eee"
|
||||||
|
unplanned: "#aaa"
|
||||||
|
planned: "#99b"
|
||||||
|
active: "#55f"
|
||||||
|
overdue: "#ffa500"
|
||||||
|
any_rejected: "#e44"
|
||||||
|
all_completed: "#5b5"
|
||||||
|
# workflow_step - duplicates
|
||||||
|
pending: "#eee"
|
||||||
|
processing: "#55f"
|
||||||
|
rejected: "#e44"
|
||||||
|
completed: "#5b5"
|
||||||
|
|
||||||
|
selector = $(document).multi_select
|
||||||
|
changed: ->
|
||||||
|
selection_text = "#{@selected_count()} van de #{@total_count()} orders geselecteerd"
|
||||||
|
if totalCount = @get('table').data('recordCount')
|
||||||
|
selection_text = "#{selection_text} (#{totalCount} totaal)"
|
||||||
|
$('.multi-select-statistics').html selection_text
|
||||||
|
return unless selector
|
||||||
|
# Find all buttons in the table footer and attach the action given their action data attribute
|
||||||
|
return unless table = selector.get('table')
|
||||||
|
selector.trigger 'changed'
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
@human_size = (bytes)->
|
||||||
|
exp = Math.log(bytes) / Math.log(1024) | 0
|
||||||
|
result = (bytes / Math.pow(1024, exp))
|
||||||
|
precision = if result < 0 then 2 else 1
|
||||||
|
result = result.toFixed(precision)
|
||||||
|
addition = if exp is 0 then "bytes" else "KMGTPEZY"[exp - 1] + "B"
|
||||||
|
"#{result} #{addition}"
|
||||||
|
Dunlop.register_setup 'human-size', (target) ->
|
||||||
|
target.find('[data-bytes]').each ->
|
||||||
|
display = $(@)
|
||||||
|
display.text human_size(display.data('bytes'))
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#= require_self
|
||||||
|
#= require ./helpers
|
||||||
|
#= require_tree ./active_support
|
||||||
|
class Dunlop
|
||||||
|
constructor: ->
|
||||||
|
@setup_callbacks = {}
|
||||||
|
@action_in_progress = 0
|
||||||
|
|
||||||
|
setup: (target, callback)->
|
||||||
|
@action_in_progress = 1
|
||||||
|
target ||= $(document)
|
||||||
|
for setup_label, callbacks_of_label of @setup_callbacks
|
||||||
|
callbacks_of_label.forEach (callback)=>
|
||||||
|
callback.call(@, target)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Set all optial boolean labels to the maximum label. NOTE: this is a hack
|
||||||
|
delay 300, ->
|
||||||
|
labels = $('.optional-boolean-label, .optional-input-activator-label')
|
||||||
|
label_minimum_length = if labels.filter( -> $(@).parents('.optional-attribute-container').data('one') ).length then 356 else 320
|
||||||
|
if labels.length
|
||||||
|
labels.each -> label_minimum_length = $(@).width() if $(@).width() > label_minimum_length
|
||||||
|
#max_optional_label_width = Math.max.apply(null, labels.map((i, el) -> $(el).width()).toArray())
|
||||||
|
#max_optional_label_width = Math.max(max_optional_label_width, 356) # form display width match for big screens = 366 - 10
|
||||||
|
labels.css 'width', "#{label_minimum_length + 10}px" if label_minimum_length
|
||||||
|
|
||||||
|
if callback
|
||||||
|
callback.call(@)
|
||||||
|
@action_in_progress = 0
|
||||||
|
else
|
||||||
|
@action_in_progress = 0
|
||||||
|
|
||||||
|
selected_ids: -> MultiSelect.selected_ids()
|
||||||
|
|
||||||
|
register_setup: (label_or_callback, labelled_callback)->
|
||||||
|
unless labelled_callback?
|
||||||
|
labelled_callback = label_or_callback
|
||||||
|
label_or_callback = 'anonymous'
|
||||||
|
(@setup_callbacks[label_or_callback] ||= []).push labelled_callback
|
||||||
|
|
||||||
|
|
||||||
|
@Dunlop = new Dunlop()
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
Dunlop.register_setup 'categorize-as-button', (target)->
|
||||||
|
button = $('#categorize-as-button')
|
||||||
|
return unless button.length
|
||||||
|
button.click ->
|
||||||
|
selected_ids = Dunlop.selected_ids() # proxy for MultiSelect.selected_ids()
|
||||||
|
return alert("Nothing selected") unless selected_ids.length
|
||||||
|
|
||||||
|
selector = $("#categorize-as-scenario-selector")
|
||||||
|
return unless target_scenario = selector.val()
|
||||||
|
if confirmation_t = button.data("confirmationT")
|
||||||
|
message = t_substitute_params confirmation_t,
|
||||||
|
target_scenario: selector.text() || target_scenario
|
||||||
|
return unless confirm(message)
|
||||||
|
$.post Routes.dunlop_categorize_as_workflow_instances_path(),
|
||||||
|
ids: selected_ids.join("~")
|
||||||
|
scenario: target_scenario
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
Dunlop.register_setup 'collapsible', (target)->
|
||||||
|
collapsible_containers = target.find('.collapsible-container')
|
||||||
|
collapsible_containers.each (i, el) ->
|
||||||
|
$(el).children('.collapsible-title').click -> $(el).toggleClass('collapsed')
|
||||||
|
|
||||||
|
target.find('.collapsible-expand-all').dblclick ->
|
||||||
|
if $(@).hasClass('opened')
|
||||||
|
collapsible_containers.addClass('collapsed')
|
||||||
|
$(@).removeClass('opened')
|
||||||
|
else
|
||||||
|
collapsible_containers.removeClass('collapsed')
|
||||||
|
$(@).addClass('opened')
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
collection_button_click = (target, options = {}) ->
|
||||||
|
return if target.data('confirmation') and not confirm(target.data('confirmation'))
|
||||||
|
action_name = target.data('action')
|
||||||
|
[resource, scenario] = full_resource.split('/') if full_resource = target.data('resource')
|
||||||
|
if not full_resource and target.attr('href') # direct collection_edit links
|
||||||
|
url = "#{target.attr('href')}&reveal=yes"
|
||||||
|
else
|
||||||
|
request_params = options.request_params || target.data('requestParams')
|
||||||
|
return unless request_params
|
||||||
|
# important to make a copy of options.request_params, otherwize the ids will be persisted in there
|
||||||
|
request_params = $.extend({}, request_params)
|
||||||
|
|
||||||
|
# See if multiselect ids is requested
|
||||||
|
for k, v of request_params
|
||||||
|
if v is 'selected_ids' # Implement multi select by inserting ids here
|
||||||
|
selected_ids = Dunlop.selected_ids()
|
||||||
|
return alert("Nothing selected") unless selected_ids.length
|
||||||
|
request_params[k] = selected_ids.join('~')
|
||||||
|
break
|
||||||
|
|
||||||
|
url_path ="collection_edit_#{resource}_#{scenario.pluralize()}_path"
|
||||||
|
request_params['reveal'] = 'yes'
|
||||||
|
return alert("The route #{url_path} does not exist") unless Routes[url_path]
|
||||||
|
url = Routes[url_path](request_params)
|
||||||
|
Dunlop.action_in_progress = 1
|
||||||
|
$.get url, (result) ->
|
||||||
|
$('#workflow-step-modal .content').html result
|
||||||
|
$('#workflow-step-modal').foundation('reveal', 'open')
|
||||||
|
Dunlop.setup $('#workflow-step-modal'), ->
|
||||||
|
$('#workflow-step-modal .workflow-step-back-button').click (e) ->
|
||||||
|
e.preventDefault()
|
||||||
|
$('#workflow-step-modal').foundation('reveal', 'close')
|
||||||
|
false
|
||||||
|
false
|
||||||
|
|
||||||
|
Dunlop.register_setup 'collection-button', (target) ->
|
||||||
|
# setup collection edit buttons
|
||||||
|
target.find('.collection-edit').click (evt) ->
|
||||||
|
evt.preventDefault()
|
||||||
|
collection_button_click $(this)
|
||||||
|
false
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
Dunlop.register_setup 'display_badges', (target)->
|
||||||
|
target.find('.display-badge').click ->
|
||||||
|
el = $(this)
|
||||||
|
return if el.find('a').length # there is a link given in the badge
|
||||||
|
content = $('.display-badge-info-content')
|
||||||
|
popup = $('#display-badge-info-popup')
|
||||||
|
data =
|
||||||
|
resource: el.data('resource')
|
||||||
|
id: el.data('id')
|
||||||
|
state: el.data('state')
|
||||||
|
workflow_instance_batch_id: el.data('workflowInstanceBatchId')
|
||||||
|
data.target = el.data('target') if el.data('target')
|
||||||
|
|
||||||
|
$.get "/dunlop/badge_info?#{$.param(data)}", (result)->
|
||||||
|
content.html result
|
||||||
|
popup.foundation 'reveal', 'open'
|
||||||
|
Dunlop.setup content
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# This is the same as setTimeout but then
|
||||||
|
# with switched arguments for coffeescript friendlyness.
|
||||||
|
# @a = 3
|
||||||
|
# delay =>
|
||||||
|
# console.log "The value of a is #{@a}"
|
||||||
|
# or to delay not a second but a self defined amount of time in milliseconds:
|
||||||
|
# delay 10000, =>
|
||||||
|
# console.log "The value of a is #{@a}"
|
||||||
|
@delay = (time = 1000, fn, args...) ->
|
||||||
|
setTimeout fn, time, args...
|
||||||
|
|
||||||
|
# based on http://www.thecodeship.com/web-development/alternative-to-javascript-evil-setinterval/
|
||||||
|
# can be used as:
|
||||||
|
# to keep scope and execute every second:
|
||||||
|
# @a = 3
|
||||||
|
# interval =>
|
||||||
|
# console.log "Hi there with a is: #{@a}"
|
||||||
|
# to log every 200 milliseconds without conservation of this scope:
|
||||||
|
# interval 200, -> console.log "Hi there"
|
||||||
|
# to execute a maximum number (30) of times:
|
||||||
|
# interval 200, 30, -> console.log "Hi there"
|
||||||
|
@interval = (wait = 1000, extra_args..., func)->
|
||||||
|
times = extra_args[0]
|
||||||
|
interv = do (wait, times)->
|
||||||
|
->
|
||||||
|
return if times? and times-- <= 0
|
||||||
|
setTimeout interv, wait
|
||||||
|
try
|
||||||
|
func.call null
|
||||||
|
catch e
|
||||||
|
times = 0
|
||||||
|
throw e.toString()
|
||||||
|
setTimeout interv, wait
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
Dunlop.register_setup 'time-display', (target) ->
|
||||||
|
# Display load time
|
||||||
|
target.find('[data-time]').each (i) ->
|
||||||
|
time = moment($(@).data('time'))
|
||||||
|
moment_format = $(@).data('format') || 'LLL' # LL means only date: TODO: Make default LLL since there is a data-date option -> done!
|
||||||
|
switch moment_format
|
||||||
|
when 'relative'
|
||||||
|
$(@).text time.fromNow()
|
||||||
|
interval 10000, =>
|
||||||
|
$(@).text time.fromNow()
|
||||||
|
else
|
||||||
|
$(@).text time.format(moment_format)
|
||||||
|
|
||||||
|
# Display date
|
||||||
|
target.find('[data-date]').each (i) ->
|
||||||
|
time = moment($(@).data('date'))
|
||||||
|
moment_format = $(@).data('format') || 'LL' # LL means only date
|
||||||
|
switch moment_format
|
||||||
|
when 'relative'
|
||||||
|
$(@).text time.fromNow()
|
||||||
|
interval 10000, =>
|
||||||
|
$(@).text time.fromNow()
|
||||||
|
else
|
||||||
|
$(@).text time.format(moment_format)
|
||||||
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
@$translations ||= {}
|
||||||
|
|
||||||
|
# dummy implementation of real client side translations
|
||||||
|
@t ||= (path, vars={}) ->
|
||||||
|
parts = path.split('.')
|
||||||
|
parts[parts.length - 1]
|
||||||
|
|
||||||
|
@t_substitute_params ||= (msg, params)->
|
||||||
|
for variable, value of params
|
||||||
|
msg = msg.replace("%{#{variable}}", value)
|
||||||
|
msg
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
@import ./dunlop/mixins
|
||||||
|
|
||||||
|
@import ./dunlop-semantic/calendar
|
||||||
|
@import ./dunlop-semantic/ui-calendar-additions
|
||||||
|
@import ./dunlop/components/environment-ribbon
|
||||||
|
//@import ./dunlop/components/user_management
|
||||||
|
|
||||||
|
.ui.selection.dropdown > .delete.icon
|
||||||
|
margin-right: 6px
|
||||||
|
|
||||||
|
.clearfix
|
||||||
|
clear: both
|
||||||
|
|
||||||
|
.hide
|
||||||
|
display: none !important
|
||||||
|
|
||||||
|
.form-actions
|
||||||
|
clear: both
|
||||||
|
margin-top: 12px
|
||||||
|
padding-top: 8px
|
||||||
|
border-top: 1px solid #ddd
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
/*!
|
||||||
|
* # Semantic UI 0.0.8 - Calendar
|
||||||
|
* http://github.com/semantic-org/semantic-ui/
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Released under the MIT license
|
||||||
|
* http://opensource.org/licenses/MIT
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************
|
||||||
|
Popup
|
||||||
|
*******************************/
|
||||||
|
|
||||||
|
.ui.calendar .ui.popup {
|
||||||
|
max-width: none;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************
|
||||||
|
Calendar
|
||||||
|
*******************************/
|
||||||
|
|
||||||
|
.ui.calendar .calendar:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************
|
||||||
|
Grid
|
||||||
|
*******************************/
|
||||||
|
|
||||||
|
.ui.calendar .ui.popup .ui.grid {
|
||||||
|
display: block;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.ui.calendar .ui.popup .ui.grid > .column {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************
|
||||||
|
Table
|
||||||
|
*******************************/
|
||||||
|
|
||||||
|
.ui.calendar .ui.table.year,
|
||||||
|
.ui.calendar .ui.table.month,
|
||||||
|
.ui.calendar .ui.table.minute {
|
||||||
|
min-width: 15em;
|
||||||
|
}
|
||||||
|
.ui.calendar .ui.table.day {
|
||||||
|
min-width: 18em;
|
||||||
|
}
|
||||||
|
.ui.calendar .ui.table.hour {
|
||||||
|
min-width: 20em;
|
||||||
|
}
|
||||||
|
.ui.calendar .ui.table tr th,
|
||||||
|
.ui.calendar .ui.table tr td {
|
||||||
|
padding: 0.5em;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.ui.calendar .ui.table tr th {
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
.ui.calendar .ui.table tr th .icon {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.ui.calendar .ui.table tr th .icon {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.ui.calendar .ui.table tr:first-child th {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
.ui.calendar .ui.table.day tr:first-child th {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.ui.calendar .ui.table.day tr:nth-child(2) th {
|
||||||
|
padding-top: 0.2em;
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
}
|
||||||
|
.ui.calendar .ui.table tr td {
|
||||||
|
padding-left: 0.1em;
|
||||||
|
padding-right: 0.1em;
|
||||||
|
}
|
||||||
|
.ui.calendar .ui.table tr .link {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.ui.calendar .ui.table tr .prev.link {
|
||||||
|
width: 14.28571429%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.ui.calendar .ui.table tr .next.link {
|
||||||
|
width: 14.28571429%;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.ui.calendar .ui.table tr .disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
color: rgba(40, 40, 40, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*--------------
|
||||||
|
States
|
||||||
|
---------------*/
|
||||||
|
|
||||||
|
.ui.calendar .ui.table tr td.today {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.ui.calendar .ui.table tr td.range {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
color: rgba(0, 0, 0, 0.95);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.ui.calendar .ui.table.inverted tr td.range {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.ui.calendar .calendar:focus .ui.table tbody tr td.focus,
|
||||||
|
.ui.calendar .calendar.active .ui.table tbody tr td.focus {
|
||||||
|
box-shadow: inset 0 0 0 1px #85B7D9;
|
||||||
|
}
|
||||||
|
.ui.calendar .calendar:focus .ui.table.inverted tbody tr td.focus,
|
||||||
|
.ui.calendar .calendar.active .ui.table.inverted tbody tr td.focus {
|
||||||
|
box-shadow: inset 0 0 0 1px #85B7D9;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************
|
||||||
|
Theme Overrides
|
||||||
|
*******************************/
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// This fixes the issue of ember-semantic-ui-calendar not functioning inside a definition table
|
||||||
|
// taken from ember-cli-dunlop
|
||||||
|
.ui.definition.table .ui.calendar
|
||||||
|
padding: 0
|
||||||
|
table
|
||||||
|
//border: 1px solid rgba(34,36,38,.15)
|
||||||
|
border-radius: .28571429rem
|
||||||
|
box-shadow: none
|
||||||
|
color: rgba(0,0,0,.87)
|
||||||
|
tr
|
||||||
|
th
|
||||||
|
pointer-events: all
|
||||||
|
font-weight: bold
|
||||||
|
color: rgba(0,0,0,.87)
|
||||||
|
background-color: #F9FAFB
|
||||||
|
&:nth-child(2)
|
||||||
|
border-left: 0
|
||||||
|
td
|
||||||
|
pointer-events: all
|
||||||
|
&:first-child:not(.ignored)
|
||||||
|
background-color: white
|
||||||
|
font-weight: normal
|
||||||
|
&.disabled
|
||||||
|
color: rgba(40, 40, 40, 0.3)
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
// import the CSS framework
|
||||||
|
@import foundation/functions
|
||||||
|
$row-width: 100%
|
||||||
|
@import foundation
|
||||||
|
.hide
|
||||||
|
display: none !important
|
||||||
|
|
||||||
|
// override for the 'Home' navigation link
|
||||||
|
.top-bar .name
|
||||||
|
font-size: rem-calc(13)
|
||||||
|
line-height: 45px
|
||||||
|
|
||||||
|
.top-bar .name a
|
||||||
|
font-weight: normal
|
||||||
|
color: white
|
||||||
|
padding: 0 15px
|
||||||
|
|
||||||
|
// THESE ARE EXAMPLES YOU CAN MODIFY
|
||||||
|
// create mixins using Foundation classes
|
||||||
|
=twelve-columns
|
||||||
|
@extend .small-12
|
||||||
|
@extend .columns
|
||||||
|
|
||||||
|
=six-columns-centered
|
||||||
|
@extend .small-6
|
||||||
|
@extend .columns
|
||||||
|
@extend .text-center
|
||||||
|
|
||||||
|
.inline-block
|
||||||
|
display: inline-block
|
||||||
|
|
||||||
|
.progress
|
||||||
|
.meter
|
||||||
|
text-align: center
|
||||||
|
color: white
|
||||||
|
// create your own classes
|
||||||
|
// to make views framework-neutral
|
||||||
|
.column
|
||||||
|
+six-columns-centered
|
||||||
|
|
||||||
|
.alert-box
|
||||||
|
&.alert
|
||||||
|
h1, h2, h3, h4
|
||||||
|
color: white
|
||||||
|
.form
|
||||||
|
+grid-column(6)
|
||||||
|
|
||||||
|
.form-centered
|
||||||
|
+six-columns-centered
|
||||||
|
|
||||||
|
.submit
|
||||||
|
@extend .button
|
||||||
|
@extend .radius
|
||||||
|
|
||||||
|
// apply styles to HTML elements
|
||||||
|
// to make views framework-neutral
|
||||||
|
main
|
||||||
|
+twelve-columns
|
||||||
|
background-color: #eee
|
||||||
|
min-height: calc(100% - 70px)
|
||||||
|
|
||||||
|
section
|
||||||
|
@extend .row
|
||||||
|
margin-top: 20px
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
//= require pickadate/default
|
||||||
|
//= require pickadate/default.date
|
||||||
|
//= require pickadate/default.time
|
||||||
|
//= require record_collection/all
|
||||||
|
|
||||||
|
$export-color: #626
|
||||||
|
@import ./1st_load_framework
|
||||||
|
@import font-awesome
|
||||||
|
@import ./mixins
|
||||||
|
@import ./components/*
|
||||||
|
@import ./resources/*
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
$archived-color: #FFDB9D
|
||||||
|
body.archived-mode
|
||||||
|
main
|
||||||
|
background-color: $archived-color
|
||||||
|
table
|
||||||
|
tr.archived
|
||||||
|
&:nth-of-type(even)
|
||||||
|
background-color: darken($archived-color, 12%)
|
||||||
|
&:nth-of-type(odd)
|
||||||
|
background-color: $archived-color
|
||||||
|
div.record-archived
|
||||||
|
background-color: $archived-color
|
||||||
|
.top-bar
|
||||||
|
.switch-to-archived-mode
|
||||||
|
background-color: inherit
|
||||||
|
.switch-to-active-mode
|
||||||
|
background-color: #f92 !important
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
= default-subprocess($pending-background:#aaa, $pending-color:#555, $processing-background:#55f, $processing-color:white, $completed-background:#5b5, $completed-color:black, $rejected-background:#f55, $rejected-color:black)
|
||||||
|
&.pending
|
||||||
|
background-color: $pending-background
|
||||||
|
color: $pending-color
|
||||||
|
a
|
||||||
|
color: $pending-color
|
||||||
|
&.processing, &.active
|
||||||
|
background-color: $processing-background
|
||||||
|
color: $processing-color
|
||||||
|
a
|
||||||
|
color: $processing-color
|
||||||
|
&.completed, &.all_completed
|
||||||
|
background-color: $completed-background
|
||||||
|
color: $completed-color
|
||||||
|
a
|
||||||
|
color: $completed-color
|
||||||
|
&.rejected, &.any_rejected
|
||||||
|
background-color: $rejected-background
|
||||||
|
color: $rejected-color
|
||||||
|
a
|
||||||
|
color: $rejected-color
|
||||||
|
&.overdue
|
||||||
|
background-color: orange
|
||||||
|
color: #fa0
|
||||||
|
color: black
|
||||||
|
.display-badge
|
||||||
|
+radius($global-rounded)
|
||||||
|
padding: 3px 8px
|
||||||
|
cursor: pointer
|
||||||
|
display: inline-block
|
||||||
|
margin-left: 5px
|
||||||
|
margin-bottom: 3px
|
||||||
|
.badge-name
|
||||||
|
.human-state
|
||||||
|
padding-left: 4px
|
||||||
|
display: none
|
||||||
|
+default-subprocess
|
||||||
|
|
||||||
|
&.workflow_instance
|
||||||
|
&.unplanned
|
||||||
|
background-color: #aaa
|
||||||
|
color: #555
|
||||||
|
&.planned
|
||||||
|
background-color: #aaa
|
||||||
|
color: #555
|
||||||
|
|
||||||
|
// Show the state name in stead of scenario badge for badges prefixed in
|
||||||
|
// the dom with class state
|
||||||
|
.state
|
||||||
|
.display-badge
|
||||||
|
.badge-name
|
||||||
|
display: none
|
||||||
|
.human-state
|
||||||
|
display: inline-block
|
||||||
|
pre.badge-info-text
|
||||||
|
+panel
|
||||||
|
max-height: 200px
|
||||||
|
overflow-x: scroll
|
||||||
|
white-space: pre-wrap
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.boolean-show
|
||||||
|
@extend .fa, .fa-lg, .fa-square-o
|
||||||
|
&.yes
|
||||||
|
color: $success-color
|
||||||
|
@extend .fa-check-square-o
|
||||||
|
td.boolean-show-cell
|
||||||
|
text-align: center
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
.collapsible-container
|
||||||
|
.collapsible-title
|
||||||
|
font-size: 1.4em
|
||||||
|
font-weight: bold
|
||||||
|
cursor: pointer
|
||||||
|
span
|
||||||
|
@extend .fa, .fa-arrow-down
|
||||||
|
&.collapsed
|
||||||
|
.collapsible-title
|
||||||
|
span
|
||||||
|
@extend .fa, .fa-arrow-right
|
||||||
|
.collapsible-content
|
||||||
|
display: none
|
||||||
|
.migration-comments
|
||||||
|
.thread_header
|
||||||
|
display: none
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
Pickadate has some styling compatibility issues with foundation at the moment
|
||||||
|
therefore this css file is to fix the most important issues untill and
|
||||||
|
'official' fix is published
|
||||||
|
.picker--opened
|
||||||
|
select
|
||||||
|
// Foundation fix
|
||||||
|
margin: 0
|
||||||
|
padding: 0
|
||||||
|
.picker__button--today, .picker__button--clear, .picker__button--close
|
||||||
|
color: #444
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
.display-row
|
||||||
|
+grid-row
|
||||||
|
.display-label
|
||||||
|
font-weight: bold
|
||||||
|
@media #{$small-only}
|
||||||
|
+grid-column($columns:5)
|
||||||
|
@media #{$medium-only}
|
||||||
|
+grid-column($columns:4, $offset:1)
|
||||||
|
@media #{$large-up}
|
||||||
|
+grid-column(3)
|
||||||
|
.display-field
|
||||||
|
@media #{$small-only}
|
||||||
|
+grid-column($columns: 5, $last-column:true)
|
||||||
|
@media #{$medium-only}
|
||||||
|
+grid-column($columns:4, $last-column:true)
|
||||||
|
@media #{$large-up}
|
||||||
|
+grid-column($columns: 3, $last-column:true)
|
||||||
|
&.full
|
||||||
|
@media #{$small-only}
|
||||||
|
+grid-column($columns: 7, $last-column:true)
|
||||||
|
@media #{$medium-only}
|
||||||
|
+grid-column($columns:7, $last-column:true)
|
||||||
|
@media #{$large-up}
|
||||||
|
+grid-column($columns: 9, $last-column:true)
|
||||||
|
pre.timewax-description
|
||||||
|
+panel
|
||||||
|
max-height: 200px
|
||||||
|
overflow-x: scroll
|
||||||
|
white-space: pre-wrap
|
||||||
|
|
||||||
|
pre.show-collection-edit-notes
|
||||||
|
+panel
|
||||||
|
max-height: 200px
|
||||||
|
overflow-x: scroll
|
||||||
|
white-space: pre-wrap
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// inspired by http://www.cssportal.com/css-ribbon-generator/
|
||||||
|
.environment-ribbon
|
||||||
|
position: fixed
|
||||||
|
left: -10px
|
||||||
|
bottom: 0
|
||||||
|
z-index: 1
|
||||||
|
overflow: hidden
|
||||||
|
width: 75px
|
||||||
|
height: 75px
|
||||||
|
text-align: right
|
||||||
|
&.top-right
|
||||||
|
margin-top: 45px
|
||||||
|
left: auto
|
||||||
|
right: -5px
|
||||||
|
top: -5px
|
||||||
|
|
||||||
|
span
|
||||||
|
font-size: 8px
|
||||||
|
font-weight: bold
|
||||||
|
text-transform: uppercase
|
||||||
|
text-align: center
|
||||||
|
line-height: 20px
|
||||||
|
transform: rotate(45deg)
|
||||||
|
-webkit-transform: rotate(45deg)
|
||||||
|
width: 100px
|
||||||
|
display: block
|
||||||
|
box-shadow: 0 3px 10px -5px rgba(0, 0, 0, 1)
|
||||||
|
position: absolute
|
||||||
|
top: 19px
|
||||||
|
right: -21px
|
||||||
|
|
||||||
|
&::before
|
||||||
|
content: ""
|
||||||
|
position: absolute
|
||||||
|
left: 0px
|
||||||
|
top: 100%
|
||||||
|
z-index: -1
|
||||||
|
|
||||||
|
&::after
|
||||||
|
content: ""
|
||||||
|
position: absolute
|
||||||
|
right: 0px
|
||||||
|
top: 100%
|
||||||
|
z-index: -1
|
||||||
|
border-left: 3px solid transparent
|
||||||
|
border-bottom: 3px solid transparent
|
||||||
|
&.development
|
||||||
|
+env-ribbon(#1e5799)
|
||||||
|
&.production
|
||||||
|
+env-ribbon(#808080)
|
||||||
|
display: none
|
||||||
|
&.test
|
||||||
|
+env-ribbon(#66ff66)
|
||||||
|
display: none
|
||||||
|
&.staging
|
||||||
|
+env-ribbon(#8F5408)
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
form
|
||||||
|
input.datepicker
|
||||||
|
width: 100px
|
||||||
|
display: inline-block
|
||||||
|
&.inline
|
||||||
|
display: inline-block
|
||||||
|
.form-actions
|
||||||
|
[type="submit"]
|
||||||
|
+button
|
||||||
|
.workflow-step-assign-button
|
||||||
|
+button($bg: $primary-color, $padding: $button-sml)
|
||||||
|
margin-right: 8px
|
||||||
|
.workflow-step-back-button
|
||||||
|
+button($bg: $secondary-color, $padding: $button-sml)
|
||||||
|
margin-right: 8px
|
||||||
|
.workflow-step-process-button
|
||||||
|
+button($bg: $primary-color, $padding: $button-sml)
|
||||||
|
margin-right: 8px
|
||||||
|
.workflow-step-overdue-button, .workflow-instance-archive-button, .workflow-instance-revive-button
|
||||||
|
+button($bg: $warning-color, $padding: $button-sml)
|
||||||
|
margin-right: 8px
|
||||||
|
.workflow-step-complete-button
|
||||||
|
+button($bg: $success-color, $padding: $button-sml)
|
||||||
|
margin-right: 8px
|
||||||
|
.workflow-step-reject-button, .workflow-instance-reset-button
|
||||||
|
+button($bg: $alert-color, $padding: $button-sml)
|
||||||
|
margin-right: 8px
|
||||||
|
.form-inputs
|
||||||
|
&.notes
|
||||||
|
textarea
|
||||||
|
min-height: 180px
|
||||||
|
&.notes-to-append
|
||||||
|
textarea
|
||||||
|
min-height: 80px
|
||||||
|
.field_with_hint
|
||||||
|
textarea
|
||||||
|
margin-bottom: 0
|
||||||
|
.hint
|
||||||
|
font-style: italic
|
||||||
|
color: #777
|
||||||
|
.notes-display
|
||||||
|
@extend .panel
|
||||||
|
height: 140px
|
||||||
|
overflow-y: scroll
|
||||||
|
.plan_date-placeholder
|
||||||
|
color: #777
|
||||||
|
font-size: 0.7em
|
||||||
|
|
||||||
|
// Add space to check_box collection list
|
||||||
|
div.checkbox label input
|
||||||
|
margin-right: 8px
|
||||||
|
.plan_date-placeholder
|
||||||
|
color: #777
|
||||||
|
font-size: 0.7em
|
||||||
|
|
||||||
|
body
|
||||||
|
label
|
||||||
|
&.number
|
||||||
|
display: inline-block
|
||||||
|
padding: 4px 10px
|
||||||
|
input
|
||||||
|
&.number
|
||||||
|
width: 60px
|
||||||
|
display: inline-block
|
||||||
|
.apply-filter
|
||||||
|
+button($bg: $primary-color, $padding: $button-tny)
|
||||||
|
.error
|
||||||
|
input, textarea
|
||||||
|
border-color: $alert-color
|
||||||
|
input[type="number"]
|
||||||
|
text-align: right
|
||||||
|
|
||||||
|
.form-row
|
||||||
|
+grid-row
|
||||||
|
.form-label
|
||||||
|
@media #{$small-only}
|
||||||
|
+grid-column($columns:10, $center:true)
|
||||||
|
@media #{$medium-only}
|
||||||
|
+grid-column($columns:4, $offset:1)
|
||||||
|
@media #{$large-up}
|
||||||
|
+grid-column(3)
|
||||||
|
&.half
|
||||||
|
+grid-column(6)
|
||||||
|
.form-field
|
||||||
|
@media #{$small-only}
|
||||||
|
+grid-column($columns:10, $center:true, $last-column:true)
|
||||||
|
@media #{$medium-only}
|
||||||
|
+grid-column($columns:4, $last-column:false)
|
||||||
|
@media #{$large-up}
|
||||||
|
+grid-column($columns: 4, $last-column:false)
|
||||||
|
&.full
|
||||||
|
@media #{$small-only}
|
||||||
|
+grid-column($columns:10, $center:true, $last-column:true)
|
||||||
|
@media #{$medium-only}
|
||||||
|
+grid-column($columns:7, $last-column:true)
|
||||||
|
@media #{$large-up}
|
||||||
|
+grid-column($columns: 9, $last-column:true)
|
||||||
|
&.half
|
||||||
|
+grid-column(6)
|
||||||
|
.form-info
|
||||||
|
@media #{$small-only}
|
||||||
|
+grid-column($columns:10, $center:true)
|
||||||
|
@media #{$medium-only}
|
||||||
|
+grid-column($columns:4, $offset:1)
|
||||||
|
@media #{$large-up}
|
||||||
|
+grid-column(3)
|
||||||
|
.error-message
|
||||||
|
color: $alert-color
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
.edit-icon
|
||||||
|
background-color: $warning-color
|
||||||
|
color: white
|
||||||
|
@extend .fa
|
||||||
|
@extend .fa-pencil
|
||||||
|
&.spacious
|
||||||
|
padding: 0.25em
|
||||||
|
h3
|
||||||
|
.edit-icon
|
||||||
|
@extend .fa-lg
|
||||||
|
font-size: 0.8em
|
||||||
|
padding: 5px
|
||||||
|
vertical-align: middle
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pre.log-entry-backtrace
|
||||||
|
margin-top: 4px
|
||||||
|
max-height: 222px
|
||||||
|
overflow-y: scroll
|
||||||
|
background-color: rgba(255,255,92, 0.2)
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
.optional-input, .optional-text-field
|
||||||
|
&.active
|
||||||
|
&.window_from, &.window_to, &.bucket, &.plan_date, &.window_date,
|
||||||
|
&.service_window_request_date,
|
||||||
|
&.service_window_number,
|
||||||
|
&.exceedance_minutes,
|
||||||
|
&.phone_number,
|
||||||
|
&.number_of_visits,
|
||||||
|
&.exceedance_couse_domain
|
||||||
|
display: inline-block
|
||||||
|
margin-left: 10px
|
||||||
|
select
|
||||||
|
width: 90px
|
||||||
|
input
|
||||||
|
width: 155px
|
||||||
|
|
||||||
|
&.workflow_instance_manager_id,
|
||||||
|
&.workflow_instance_batch_id,
|
||||||
|
&.customer_premise_equipment_id,
|
||||||
|
&.customer_premise_equipment_number,
|
||||||
|
&.new_xdf_service_id,
|
||||||
|
&.customer_order_id,
|
||||||
|
&.service_provider_phone_number
|
||||||
|
display: inline-block
|
||||||
|
margin-left: 10px
|
||||||
|
select
|
||||||
|
width: 272px
|
||||||
|
input
|
||||||
|
width: 272px
|
||||||
|
//&.inactive
|
||||||
|
//display: none
|
||||||
|
|
||||||
|
//TODO: should these rules be part of record_collection core?
|
||||||
|
.optional-input-activator-container
|
||||||
|
vertical-align: top
|
||||||
|
.optional-input-activator-label
|
||||||
|
display: inline-block
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
.page-actions, tfoot, .form-actions, .selection-container, .workflow-instance-batch-actions
|
||||||
|
clear: left
|
||||||
|
margin-top: 8px
|
||||||
|
border-top: 1px solid #ddd
|
||||||
|
padding-top: 8px
|
||||||
|
.new
|
||||||
|
+button
|
||||||
|
.back
|
||||||
|
+button($bg: $secondary-color)
|
||||||
|
margin-right: 8px
|
||||||
|
.collection-edit, .edit
|
||||||
|
+button($bg: $warning-color)
|
||||||
|
margin-right: 8px
|
||||||
|
.destroy
|
||||||
|
+button($bg: $alert-color)
|
||||||
|
margin-right: 8px
|
||||||
|
.workflow-step-back-button
|
||||||
|
.workflow-step-assign-button
|
||||||
|
.workflow-step-reset-button, .workflow-instance-reset-button
|
||||||
|
+button($bg: $alert-color)
|
||||||
|
.workflow-instance-archive-button, .workflow-instance-revive-button
|
||||||
|
+button($bg: $warning-color)
|
||||||
|
margin-right: 8px
|
||||||
|
|
||||||
|
.export-button
|
||||||
|
+button($bg: $export-color)
|
||||||
|
.show-workflow_instance_batch-workflow_instances
|
||||||
|
+button
|
||||||
|
margin-right: 8px
|
||||||
|
.title-back-link
|
||||||
|
@extend .fa
|
||||||
|
@extend .fa-arrow-left
|
||||||
|
margin-right: 8px
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
.role-icon
|
||||||
|
@extend .fa
|
||||||
|
&.checkmark
|
||||||
|
@extend .fa-check
|
||||||
|
&.read
|
||||||
|
@extend .fa-book
|
||||||
|
&.manage
|
||||||
|
@extend .fa-pencil
|
||||||
|
&.update
|
||||||
|
@extend .fa-pencil
|
||||||
|
&.download
|
||||||
|
@extend .fa-download
|
||||||
|
&.reset
|
||||||
|
@extend .fa-undo
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// Based on CU request from Kitty
|
||||||
|
.select2-container
|
||||||
|
.select2-results__option
|
||||||
|
padding: 0 3px
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.statistics-title
|
||||||
|
font-size: 1.3em
|
||||||
|
font-weight: bold
|
||||||
|
border-bottom: 1px solid #999
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.clearfix
|
||||||
|
+clearfix
|
||||||
|
clear: both
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
.subprocess-modal-button
|
||||||
|
+button
|
||||||
|
&.mark-complete
|
||||||
|
+button($bg: $success-color)
|
||||||
|
&.mark-reject
|
||||||
|
+button($bg: $alert-color)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
table
|
||||||
|
&.full-width
|
||||||
|
width: 100%
|
||||||
|
.actions
|
||||||
|
a
|
||||||
|
display: inline-block
|
||||||
|
margin-left: 10px
|
||||||
|
&.column-filter
|
||||||
|
white-space: nowrap
|
||||||
|
.table-link
|
||||||
|
&.show
|
||||||
|
i
|
||||||
|
@extend .fa, .fa-lg, .fa-folder-open
|
||||||
|
&.edit
|
||||||
|
color: $warning-color
|
||||||
|
i
|
||||||
|
@extend .fa, .fa-lg, .fa-edit
|
||||||
|
&.destroy
|
||||||
|
color: $alert-color
|
||||||
|
i
|
||||||
|
@extend .fa, .fa-lg, .fa-trash
|
||||||
|
&.download
|
||||||
|
color: $export-color
|
||||||
|
i
|
||||||
|
@extend .fa, .fa-lg, .fa-download
|
||||||
|
.clear-filters-button
|
||||||
|
+button($bg: $warning-color, $padding: $button-tny)
|
||||||
|
margin: 0
|
||||||
|
i
|
||||||
|
font-family: FontAwesome
|
||||||
|
.submit-filters-button
|
||||||
|
+button($bg: $primary-color, $padding: $button-tny)
|
||||||
|
margin: 0
|
||||||
|
//-webkit-appearance: button
|
||||||
|
font-family: FontAwesome
|
||||||
|
//span
|
||||||
|
@extend .fa, .fa-lg, .fa-filter
|
||||||
|
tr.search
|
||||||
|
.clear-filters-button
|
||||||
|
display: none
|
||||||
|
&.conditions-present
|
||||||
|
background-color: #7af
|
||||||
|
.clear-filters-button
|
||||||
|
display: inline-block
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/* Based on the bootstrap 3+ panel, of course under a different name.
|
||||||
|
The conversion is done based on:
|
||||||
|
http://stackoverflow.com/questions/19220027/can-you-use-panels-in-bootstrap-2-3
|
||||||
|
.title-box
|
||||||
|
padding: 15px
|
||||||
|
margin-bottom: 20px
|
||||||
|
background-color: #ffffff
|
||||||
|
border: 1px solid #dddddd
|
||||||
|
border-radius: 4px
|
||||||
|
-webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05)
|
||||||
|
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05)
|
||||||
|
|
||||||
|
.title-box-heading
|
||||||
|
padding: 10px 15px
|
||||||
|
margin: -15px -15px 15px
|
||||||
|
font-size: 17.5px
|
||||||
|
font-weight: 500
|
||||||
|
background-color: #f5f5f5
|
||||||
|
border-bottom: 1px solid #dddddd
|
||||||
|
border-top-right-radius: 3px
|
||||||
|
border-top-left-radius: 3px
|
||||||
|
|
||||||
|
.title-box-footer
|
||||||
|
padding: 10px 15px
|
||||||
|
margin: 15px -15px -15px
|
||||||
|
background-color: #f5f5f5
|
||||||
|
border-top: 1px solid #dddddd
|
||||||
|
border-bottom-right-radius: 3px
|
||||||
|
border-bottom-left-radius: 3px
|
||||||
|
|
||||||
|
.title-box-primary
|
||||||
|
border-color: #428bca
|
||||||
|
.title-box-heading
|
||||||
|
color: #ffffff
|
||||||
|
background-color: #428bca
|
||||||
|
border-color: #428bca
|
||||||
|
|
||||||
|
.title-box-success
|
||||||
|
border-color: #d6e9c6
|
||||||
|
|
||||||
|
.title-box-heading
|
||||||
|
color: #468847
|
||||||
|
background-color: #dff0d8
|
||||||
|
border-color: #d6e9c6
|
||||||
|
|
||||||
|
.title-box-warning
|
||||||
|
border-color: #fbeed5
|
||||||
|
|
||||||
|
.title-box-heading
|
||||||
|
color: #c09853
|
||||||
|
background-color: #fcf8e3
|
||||||
|
border-color: #fbeed5
|
||||||
|
|
||||||
|
.title-box-danger
|
||||||
|
border-color: #eed3d7
|
||||||
|
|
||||||
|
.title-box-heading
|
||||||
|
color: #b94a48
|
||||||
|
background-color: #f2dede
|
||||||
|
border-color: #eed3d7
|
||||||
|
|
||||||
|
.title-box-info
|
||||||
|
border-color: #bce8f1
|
||||||
|
|
||||||
|
.title-box-heading
|
||||||
|
color: #3a87ad
|
||||||
|
background-color: #d9edf7
|
||||||
|
border-color: #bce8f1
|
||||||
|
|
||||||
|
//State specific additions
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
table.roles
|
||||||
|
td
|
||||||
|
text-align: center
|
||||||
|
|
||||||
|
.user-roles-tables
|
||||||
|
display: flex
|
||||||
|
flex-direction: row
|
||||||
|
flex-wrap: wrap
|
||||||
|
align-items: flex-start
|
||||||
|
justify-content: space-around
|
||||||
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
= env-ribbon($color)
|
||||||
|
span
|
||||||
|
@if lightness($color) > 50
|
||||||
|
background: linear-gradient(darken($color, 15) 0%, $color 100%)
|
||||||
|
color: black
|
||||||
|
@else
|
||||||
|
background: linear-gradient(lighten($color, 15) 0%, $color 100%)
|
||||||
|
color: white
|
||||||
|
&::before
|
||||||
|
border-left: 3px solid $color
|
||||||
|
border-top: 3px solid $color
|
||||||
|
&::after
|
||||||
|
border-right: 3px solid $color
|
||||||
|
border-top: 3px solid $color
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@import ./mixin_parts/*
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#manual-container
|
||||||
|
img
|
||||||
|
background-color: white
|
||||||
|
border: 1px solid #aaa
|
||||||
|
padding: 5px
|
||||||
|
margin: 4px
|
||||||
|
h3
|
||||||
|
font-weight: bold
|
||||||
|
border-bottom: 1px solid #999
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
.show-workflow_instance_batch-workflow_instances
|
||||||
|
span
|
||||||
|
@extend .fa, .fa-lg, .fa-bars
|
||||||
|
td.workflow_instance_batch-index-progress-container
|
||||||
|
.workflow_instance_batch-index-workflow_step
|
||||||
|
display: none
|
||||||
|
|
||||||
|
|
||||||
|
.batch-states-collection-container
|
||||||
|
+panel
|
||||||
|
margin: 0
|
||||||
|
padding: 8px 12px 5px 12px
|
||||||
|
.workflow-instance-batch-actions
|
||||||
|
.workflow-step-container
|
||||||
|
padding-bottom: 5px
|
||||||
|
h4
|
||||||
|
margin-bottom: 0
|
||||||
|
button
|
||||||
|
padding: 5px
|
||||||
|
margin-bottom: 4px
|
||||||
|
.text
|
||||||
|
display: none
|
||||||
|
.icon
|
||||||
|
@extend .fa
|
||||||
|
@extend .fa-lg
|
||||||
|
@extend .fa-pencil
|
||||||
|
border-bottom: 1px solid #aaa
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
.selection-container
|
||||||
|
#workflow-instance-selection-input-submit
|
||||||
|
+button
|
||||||
|
.workflow-instance-workflow-steps-container
|
||||||
|
display: flex
|
||||||
|
flex-direction: row
|
||||||
|
flex-wrap: wrap
|
||||||
|
align-items: flex-start
|
||||||
|
justify-content: space-around
|
||||||
|
|
||||||
|
.workflow-instance-workflow-step-container
|
||||||
|
@extend .title-box
|
||||||
|
//float: left
|
||||||
|
width: 444px
|
||||||
|
.workflow-instance-workflow-step-title
|
||||||
|
@extend .title-box-heading
|
||||||
|
.workflow-instance-workflow-step-body
|
||||||
|
.display-row
|
||||||
|
&.attribute-notes
|
||||||
|
display: none
|
||||||
|
.display-label
|
||||||
|
width: 65%
|
||||||
|
.display-field
|
||||||
|
width: 35%
|
||||||
|
.page-actions
|
||||||
|
//display: none
|
||||||
|
text-align: center
|
||||||
|
.collection-edit
|
||||||
|
margin: 0
|
||||||
|
padding: 5px 12px
|
||||||
|
&.processing
|
||||||
|
@extend .title-box-primary
|
||||||
|
&.rejected
|
||||||
|
@extend .title-box-danger
|
||||||
|
&.completed
|
||||||
|
@extend .title-box-success
|
||||||
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
.workflow-step-workfow-instances-collection-container
|
||||||
|
+panel
|
||||||
|
padding: 10px
|
||||||
|
margin-bottom: 10px
|
||||||
|
.pending, .unplanned, .planned
|
||||||
|
color: #555
|
||||||
|
.processing
|
||||||
|
color: $primary-color
|
||||||
|
.rejected, .any_rejected
|
||||||
|
color: $alert-color
|
||||||
|
.completed, .all_completed
|
||||||
|
color: $success-color
|
||||||
|
h2
|
||||||
|
.workflow-step-actions
|
||||||
|
margin: 0
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
//= require pickadate/default
|
||||||
|
//= require pickadate/default.date
|
||||||
|
//= require pickadate/default.time
|
||||||
|
//= require record_collection/all
|
||||||
|
|
||||||
|
$export-color: #626
|
||||||
|
@import ./mixins
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
module Dunlop
|
||||||
|
class ApplicationController < ::ApplicationController
|
||||||
|
layout 'application'
|
||||||
|
|
||||||
|
before_action :store_or_reset_params_q, only: :index
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def query
|
||||||
|
params[:q]
|
||||||
|
end
|
||||||
|
|
||||||
|
def store_or_reset_params_q
|
||||||
|
session_key = "#{controller_path}_q"
|
||||||
|
session[session_key] = nil if params[:reset_q].present?
|
||||||
|
params[:q] = session[session_key] if params[:q].blank?
|
||||||
|
current_query = (params[:q] || {}).select{|k,v | v.present? }
|
||||||
|
session[session_key] = current_query if params[:q].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
if Rails::VERSION::MAJOR < 5
|
||||||
|
def redirect_back(fallback_location:, **args)
|
||||||
|
redirect_to :back, args
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
class Dunlop::DashboardController < Dunlop::ApplicationController
|
||||||
|
def home
|
||||||
|
end
|
||||||
|
|
||||||
|
def badge_info
|
||||||
|
@process_class = params[:resource].to_s.classify.safe_constantize
|
||||||
|
return head(:not_found) unless @process_class.present?
|
||||||
|
if params[:id].present?
|
||||||
|
setup_badge_info_for_record
|
||||||
|
else
|
||||||
|
if @process_class.ancestors.include?(WorkflowInstance)
|
||||||
|
setup_badge_info_for_scenario
|
||||||
|
else
|
||||||
|
setup_badge_info_for_subprocess
|
||||||
|
end
|
||||||
|
end
|
||||||
|
render layout: false
|
||||||
|
end
|
||||||
|
|
||||||
|
def changelog
|
||||||
|
markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML)
|
||||||
|
changelog_path = Rails.root.join('CHANGELOG.md')
|
||||||
|
render text: "No changelog present" unless File.exist?(changelog_path)
|
||||||
|
source = File.read(changelog_path)
|
||||||
|
source.gsub!(/(?<!\\)_/, '\_') # underscores are preserved
|
||||||
|
html = markdown.render source
|
||||||
|
html = "<div class='ui container'>#{html}</div>"
|
||||||
|
render html: html.html_safe, layout: true
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /archived_mode
|
||||||
|
def archived_mode
|
||||||
|
session[:archived_mode] = true
|
||||||
|
redirect_to main_app.root_path
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /active_mode
|
||||||
|
def active_mode
|
||||||
|
session[:archived_mode] = false
|
||||||
|
redirect_to main_app.root_path
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def workflow_instances_scope(scope_options = {})
|
||||||
|
scope_options[:archived] = session[:archived_mode] if can?(:archive, @record_class) and not scope_options[:all].present?
|
||||||
|
@record_class.scope_for(current_user, scope_options)
|
||||||
|
end
|
||||||
|
|
||||||
|
def setup_badge_info_for_record
|
||||||
|
@record = @process_class.find(params[:id])
|
||||||
|
authorize! :read, @record
|
||||||
|
end
|
||||||
|
|
||||||
|
def setup_badge_info_for_scenario
|
||||||
|
@record_class = @process_class
|
||||||
|
@record_batch_class = @record_class.batch_class
|
||||||
|
@workflow_instances = workflow_instances_scope.where(state: params[:state])
|
||||||
|
end
|
||||||
|
|
||||||
|
def setup_badge_info_for_subprocess
|
||||||
|
# @process_class is a subprocess, so only show workflow_instances
|
||||||
|
# belonging to this subprocess
|
||||||
|
@record_class = WorkflowInstance # no type preference, depends on subprocess
|
||||||
|
@workflow_instances = workflow_instances_scope.joins(@process_class.process_name.to_sym).where(@process_class.table_name => {sti_type: @process_class.name, state: params[:state]})
|
||||||
|
if params[:workflow_instance_batch_id].present?
|
||||||
|
# Limit query for to specific batch
|
||||||
|
@workflow_instance_batch = WorkflowInstanceBatch.find(params[:workflow_instance_batch_id])
|
||||||
|
@workflow_instances = @workflow_instances.where(workflow_instance_batch_id: @workflow_instance_batch.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
class Dunlop::ExecutionBatchesController < Dunlop::ApplicationController
|
||||||
|
respond_to :html
|
||||||
|
|
||||||
|
def index
|
||||||
|
authorize! :index, ExecutionBatch
|
||||||
|
@q = ExecutionBatch.search(query)
|
||||||
|
@q.sorts = 'created_at desc' if @q.sorts.empty?
|
||||||
|
@batches = @q.result.page(params[:page])
|
||||||
|
respond_with(@batches)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
@record = ExecutionBatch.find(params[:id])
|
||||||
|
authorize! :show, @record
|
||||||
|
respond_with(@record)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
module Dunlop::Reports
|
||||||
|
|
||||||
|
end
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
class Dunlop::Reports::SystemController < Dunlop::ApplicationController
|
||||||
|
def index
|
||||||
|
authorize! :report, :index
|
||||||
|
end
|
||||||
|
|
||||||
|
def workflow_instance_fields
|
||||||
|
authorize! :report, :system
|
||||||
|
end
|
||||||
|
|
||||||
|
def workflow_instance_workflow_steps
|
||||||
|
authorize! :report, :system
|
||||||
|
end
|
||||||
|
|
||||||
|
def workflow_step_scenarios
|
||||||
|
authorize! :report, :system
|
||||||
|
end
|
||||||
|
|
||||||
|
def workflow_steps_in_batch_finished
|
||||||
|
authorize! :report, :system
|
||||||
|
end
|
||||||
|
|
||||||
|
def workflow_steps_overdue
|
||||||
|
authorize! :report, :system
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
class Dunlop::SourceFilesController < Dunlop::ApplicationController
|
||||||
|
respond_to :html
|
||||||
|
|
||||||
|
def index
|
||||||
|
authorize! :index, SourceFile
|
||||||
|
@q = SourceFile.where(sti_type: SourceFile.classes.select{|cls| can? :read, cls}.map(&:name)).search(query)
|
||||||
|
@q.sorts = "created_at desc" if @q.sorts.empty?
|
||||||
|
@source_files = @q.result.page(params[:page]).per(100)
|
||||||
|
respond_with(@source_files)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
@source_file = SourceFile.find(params[:id])
|
||||||
|
authorize! :read, @source_file
|
||||||
|
respond_with(@source_file)
|
||||||
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
@source_file = SourceFile.new
|
||||||
|
authorize! :manage, @source_file
|
||||||
|
respond_with(@source_file)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@source_file = SourceFile.factory(source_file_params)
|
||||||
|
redirect_to source_files_path, alert: "No file given" and return unless params[:source_file][:original_file].present?
|
||||||
|
redirect_to source_files_path, alert: "No type specified" and return unless params[:source_file][:sti_type].present?
|
||||||
|
authorize! :manage, @source_file
|
||||||
|
@source_file.creator = current_user
|
||||||
|
@source_file.save && @source_file.autoload!
|
||||||
|
redirect_to source_files_path
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@source_file = SourceFile.find(params[:id])
|
||||||
|
authorize! :manage, @source_file
|
||||||
|
@source_file.destroy
|
||||||
|
redirect_to source_files_path
|
||||||
|
end
|
||||||
|
|
||||||
|
def download
|
||||||
|
@source_file = SourceFile.find(params[:id])
|
||||||
|
authorize! :download, @source_file
|
||||||
|
return redirect_back(fallback_location: source_files_path, alert: "File is no longer present") unless @source_file.original_file.present?
|
||||||
|
Dunlop::FileDownload.create user: current_user, file: @source_file
|
||||||
|
send_file @source_file.original_file.path
|
||||||
|
end
|
||||||
|
|
||||||
|
def schedule
|
||||||
|
@source_file = SourceFile.find(params[:id])
|
||||||
|
authorize! :manage, @source_file
|
||||||
|
@source_file.scheduled!
|
||||||
|
flash[:notice] = "Successfully scheduled loading of source file"
|
||||||
|
redirect_to source_files_path
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def source_file_params
|
||||||
|
params.require(:source_file).permit(:sti_type, :original_file)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
class Dunlop::TargetFilesController < Dunlop::ApplicationController
|
||||||
|
skip_authorize_resource if respond_to?(:skip_authorize_resource)
|
||||||
|
before_action :find_target_file, except: [:index, :download_latest]
|
||||||
|
|
||||||
|
def index
|
||||||
|
authorize! :index, TargetFile
|
||||||
|
@q = TargetFile.where(sti_type: TargetFile.classes.select{|cls| can? :read, cls}.map(&:name)).search(query)
|
||||||
|
@q.sorts = "created_at desc" if @q.sorts.empty?
|
||||||
|
@target_files = @q.result.page(params[:page]).per(100)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
authorize! :read, @target_file
|
||||||
|
end
|
||||||
|
|
||||||
|
def download
|
||||||
|
authorize! :download, @target_file
|
||||||
|
Dunlop::FileDownload.create user: current_user, file: @target_file
|
||||||
|
send_file @target_file.original_file.path
|
||||||
|
end
|
||||||
|
|
||||||
|
def download_latest
|
||||||
|
target_file_class = "TargetFile::#{params[:target_file_type].to_s.camelize}".safe_constantize
|
||||||
|
@target_file = target_file_class.latest
|
||||||
|
return head(:not_found) unless @target_file.present?
|
||||||
|
authorize! :download, @target_file
|
||||||
|
Dunlop::FileDownload.create user: current_user, file: @target_file
|
||||||
|
send_file @target_file.original_file.path
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
authorize! :destroy, @target_file
|
||||||
|
@target_file.destroy
|
||||||
|
redirect_to dunlop.target_files_path, notice: t('action.destroy.successfull', model: @target_file.class.model_name.human)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def find_target_file
|
||||||
|
@target_file = TargetFile.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
class Dunlop::UsersController < Dunlop::ApplicationController
|
||||||
|
skip_authorize_resource if respond_to?(:skip_authorize_resource)
|
||||||
|
skip_authorization_check if respond_to?(:skip_authorization_check)
|
||||||
|
respond_to :html
|
||||||
|
before_action :find_record, only: %i[show edit update destroy]
|
||||||
|
before_action :set_current_user_as_user, only: %i[profile edit_profile update_profile]
|
||||||
|
before_action :log_current_user, on: [:create, :update, :destroy, :become_user]
|
||||||
|
|
||||||
|
def profile
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit_profile
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_profile
|
||||||
|
user_params = params.require(:user).permit(:password, :confirmation_password)
|
||||||
|
if @user.update_attributes(user_params)
|
||||||
|
sign_in @user, bypass_sign_in: true
|
||||||
|
redirect_to dunlop.profile_users_path
|
||||||
|
else
|
||||||
|
render action: :edit_profile
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def index
|
||||||
|
authorize! :index, User
|
||||||
|
@users = User.active.page(params[:page])
|
||||||
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
@user = User.new
|
||||||
|
authorize! :create, @user
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@user = User.new user_params
|
||||||
|
authorize! :create, @user
|
||||||
|
if @user.save
|
||||||
|
redirect_to dunlop.users_path
|
||||||
|
else
|
||||||
|
render action: :edit
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
authorize! :show, @user
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
authorize! :update, @user
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
authorize! :update, @user
|
||||||
|
if user_params[:password].blank?
|
||||||
|
@user.update_without_password(user_params)
|
||||||
|
else
|
||||||
|
@user.update_attributes(user_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
if @user.errors.present?
|
||||||
|
render action: :edit
|
||||||
|
else
|
||||||
|
redirect_to dunlop.users_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def become_user
|
||||||
|
@user = User.find(params[:id])
|
||||||
|
authorize! :become_user, @user
|
||||||
|
raise CanCan::AccessDenied if @user.admin? and not current_user.admin? # Do not allow non admin users with user management authorization to become an admin user
|
||||||
|
sign_in @user, bypass_sign_in: true
|
||||||
|
respond_with(@user, location: user_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
authorize! :destroy, @user
|
||||||
|
@user.destroy
|
||||||
|
redirect_to dunlop.users_path
|
||||||
|
end
|
||||||
|
|
||||||
|
def permissions_table
|
||||||
|
authorize! :permissions_table, User
|
||||||
|
@table = Hash.new{|h,k| h[k] = Hash.new{|h2, k2| h2[k2] = []} }
|
||||||
|
@users = User.active
|
||||||
|
@users.each do |user|
|
||||||
|
user.role_names.each do |role|
|
||||||
|
if match = role.match(/(^[a-z0-9]+)-class-(.*)/)
|
||||||
|
role_class = match[2].safe_constantize
|
||||||
|
if role_class.respond_to?(:model_name)
|
||||||
|
role = role_class.model_name.human
|
||||||
|
else
|
||||||
|
role = role_class.try(:name)
|
||||||
|
end
|
||||||
|
@table[role][user] << match[1]
|
||||||
|
else
|
||||||
|
@table[role][user] << "check"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def find_record
|
||||||
|
@user = User.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_current_user_as_user
|
||||||
|
@user = current_user
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_params
|
||||||
|
permitted_params = [:ruisnaam, :email, :password, :password_confirmation, {role_names: []}]
|
||||||
|
if current_user.admin?
|
||||||
|
permitted_params.push(:admin, role_names_admin: [])
|
||||||
|
end
|
||||||
|
params.require(:user).permit(*permitted_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def log_current_user
|
||||||
|
logger.info "#{current_user.try(:email)} having id #{current_user.try(:id)} did user change"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
class Dunlop::WorkflowInstancesController < Dunlop::ApplicationController
|
||||||
|
respond_to :html, :csv
|
||||||
|
|
||||||
|
def reset
|
||||||
|
authorize! :reset, WorkflowInstance
|
||||||
|
@records = WorkflowInstance.find ids_param
|
||||||
|
@records.each(&:reset!)
|
||||||
|
redirect_path = @records.size == 1 ? [main_app, @records.first] : polymorphic_path([main_app, :selection, @records.first.try(:class) || WorkflowInstance], ids: ids_param.join(RecordCollection.ids_separator))
|
||||||
|
redirect_back(fallback_location: redirect_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def categorize_as
|
||||||
|
scenario_name = params[:scenario].to_s
|
||||||
|
target_scenario_class = "WorkflowInstance::#{scenario_name.classify}".safe_constantize
|
||||||
|
authorize! :categorize, target_scenario_class # general target authorization
|
||||||
|
@records = WorkflowInstance.find ids_param
|
||||||
|
@records.select{|r| can? :categorize, r }.each { |record| record.categorize_as scenario_name } # individual from authorization
|
||||||
|
redirect_path = polymorphic_path([main_app, :selection, target_scenario_class], ids: ids_param.join(RecordCollection.ids_separator))
|
||||||
|
respond_to do |format|
|
||||||
|
format.js { render js: "window.location = '#{redirect_path}'" }
|
||||||
|
format.html { redirect_to redirect_path }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /dunlop/workflow_instances/archive?ids=1~2~3~....
|
||||||
|
def archive
|
||||||
|
authorize! :archive, WorkflowInstance
|
||||||
|
WorkflowInstance.where(id: ids_param).archive!
|
||||||
|
session[:archived_mode] = true
|
||||||
|
if ids_param and ids_param.size == 1
|
||||||
|
redirect_to [main_app, WorkflowInstance.find_by(id: ids_param.first)]
|
||||||
|
else
|
||||||
|
redirect_to main_app.root_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /dunlop/workflow_instances/revive?ids=1~2~3~....
|
||||||
|
def revive
|
||||||
|
authorize! :archive, WorkflowInstance
|
||||||
|
WorkflowInstance.where(id: ids_param).revive!
|
||||||
|
session[:archived_mode] = false
|
||||||
|
if ids_param and ids_param.size == 1
|
||||||
|
redirect_to [main_app, WorkflowInstance.find_by(id: ids_param.first)]
|
||||||
|
else
|
||||||
|
redirect_to main_app.root_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def find_record
|
||||||
|
@record = WorkflowInstance.find(params[:id])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
module Dunlop::ActiveClassHelper
|
||||||
|
# Return active or nil based on the given route spec
|
||||||
|
# %li{ class: active_class('users') # true if controller is users, false otherwise
|
||||||
|
# %li{ class: active_class('pages#about') # true if controller is pages and action is about
|
||||||
|
def active_class(*route_specs)
|
||||||
|
options = route_specs.extract_options!
|
||||||
|
return nil if Array.wrap(options[:except]).any?{|exception| current_route_spec?(exception) }
|
||||||
|
return 'active' if route_specs.any?{|rs| current_route_spec?(rs, options) }
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check if the current route matches the route given as argument.
|
||||||
|
# The syntax is meant to be a bit similar to specifying routes in
|
||||||
|
# `config/routes.rb`.
|
||||||
|
# current_route_spec?('products') #=> true if controller name is products, false otherwise
|
||||||
|
# current_route_spec?('products#show') #=> true if controller_name is products AND action_name is show
|
||||||
|
# current_route_spec?('#show') #=> true if action_name is show, false otherwise
|
||||||
|
#NOTE: this helper is tested through the active_class helper
|
||||||
|
def current_route_spec?(route_spec, options = {})
|
||||||
|
return route_spec.match([controller_path, action_name].join('#')) if route_spec.is_a?(Regexp)
|
||||||
|
controller, action = route_spec.split('#')
|
||||||
|
return action == params[:id] if controller_path == 'high_voltage/pages'
|
||||||
|
actual_controller_parts = controller_path.split('/')
|
||||||
|
if controller #and controller_path == controller
|
||||||
|
tested_controller_parts = controller.split('/')
|
||||||
|
return if tested_controller_parts.size > actual_controller_parts.size
|
||||||
|
if actual_controller_parts[0...tested_controller_parts.size] == tested_controller_parts
|
||||||
|
# controller spec matches
|
||||||
|
return true unless action
|
||||||
|
action_name == action
|
||||||
|
end
|
||||||
|
else
|
||||||
|
action_name == action
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
module Dunlop
|
||||||
|
module ApplicationHelper
|
||||||
|
include Dunlop::ActiveClassHelper
|
||||||
|
include Dunlop::AuthorizationHelper
|
||||||
|
include Dunlop::TableHelper
|
||||||
|
include Dunlop::StateBadgeHelper
|
||||||
|
include Dunlop::InterpretedTextHelper
|
||||||
|
delegate :mathjax_path, to: :main_app
|
||||||
|
|
||||||
|
def application_name
|
||||||
|
Rails.application.config.application_name || Rails.application.class.name.deconstantize
|
||||||
|
end
|
||||||
|
|
||||||
|
def search_result_info(records)
|
||||||
|
from_item = (records.current_page - 1) * records.limit_value + 1
|
||||||
|
to_item = [from_item + records.size - 1, records.total_count].min
|
||||||
|
from_item = 0 if records.total_count.zero?
|
||||||
|
"#{from_item} - #{to_item} / #{records.total_count}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def body_classes
|
||||||
|
classes = []
|
||||||
|
classes << 'archived-mode' if session[:archived_mode]
|
||||||
|
classes
|
||||||
|
end
|
||||||
|
|
||||||
|
# Use this helper to show a boolean. Then a unifor way of displaying can be implemented.
|
||||||
|
def boolean_show(value)
|
||||||
|
content_tag(:i, nil, class: ['boolean-show', (value.present? ? 'yes checkmark box icon' : 'no square outline icon')])
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_address(record)
|
||||||
|
[record.zipcode, "#{record.housenumber}#{record.housenumber_ext}"].map(&:presence).compact.join(' ')
|
||||||
|
end
|
||||||
|
|
||||||
|
# https://coderwall.com/p/7gqmog/display-flash-messages-with-semantic-ui-in-rails
|
||||||
|
def flash_class(level)
|
||||||
|
case level.to_sym
|
||||||
|
when :success then "ui positive message"
|
||||||
|
when :error, :alert then "ui negative message"
|
||||||
|
when :notice then "ui info message"
|
||||||
|
else "ui #{level} message"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
#def render_cascaded(*args)
|
||||||
|
# options = args.extract_options!
|
||||||
|
# controller_parts = controller_path.split('/')
|
||||||
|
# if args.first.is_a?(String) or options[:partial].present?
|
||||||
|
# partial_name = args.first.presence || options[:partial]
|
||||||
|
# lookup_name = "_#{partial_name}"
|
||||||
|
# controller_parts.pop until lookup_context.template_exists?(File.join(*controller_parts, lookup_name))
|
||||||
|
# options[:partial] = File.join(*controller_parts, partial_name)
|
||||||
|
# render options
|
||||||
|
# elsif options[:template].present?
|
||||||
|
# lookup_name = options[:template]
|
||||||
|
# controller_parts.pop until lookup_context.template_exists?(File.join(*controller_parts, lookup_name))
|
||||||
|
# options[:template] = File.join(*controller_parts, lookup_name)
|
||||||
|
# render options
|
||||||
|
# else
|
||||||
|
# render *args
|
||||||
|
# end
|
||||||
|
#end
|
||||||
|
|
||||||
|
# Try to find a template containing the workflow step info specification of the record
|
||||||
|
# and render nothing if it cannot be found
|
||||||
|
# an OwnerServiceWindow1Preparation::Scenario1 record will search for a template
|
||||||
|
# named "info_fields" in the following locations:
|
||||||
|
# app/views/owner_service_window1_preparations/scenario1s/info_fields.html.haml
|
||||||
|
# app/views/owner_service_window1_preparations/info_fields.html.haml
|
||||||
|
def record_info_fields(record, options = {})
|
||||||
|
lookup_cascade = record.class.name.underscore.split('/').map(&:pluralize)
|
||||||
|
lookup_name = 'info_fields'
|
||||||
|
template_path = File.join(*lookup_cascade, lookup_name)
|
||||||
|
until lookup_context.template_exists?(template_path) or lookup_cascade.empty?
|
||||||
|
return if lookup_cascade.empty? # not template for record found
|
||||||
|
lookup_cascade.pop
|
||||||
|
template_path = File.join(*lookup_cascade, lookup_name)
|
||||||
|
end
|
||||||
|
render template: template_path, locals: options.merge(record: record) if lookup_context.template_exists?(template_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def scope_model(model = nil)
|
||||||
|
@scope_model = model if model
|
||||||
|
@scope_model
|
||||||
|
end
|
||||||
|
|
||||||
|
def at(attribute_name, scope_model=nil)
|
||||||
|
scope_model ||= @scope_model
|
||||||
|
scope_model.human_attribute_name(attribute_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def page_title(*args)
|
||||||
|
res = content_tag :h3, class: 'page-title ui header' do
|
||||||
|
if resource_title?(args)
|
||||||
|
@scope_model = args[1].is_a?(ActiveRecord::Base) ? args[1].class : args[1]
|
||||||
|
page_title_for_resource(args)
|
||||||
|
else
|
||||||
|
page_title_for_string(args)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
content_for :page_title, res
|
||||||
|
res
|
||||||
|
end
|
||||||
|
|
||||||
|
def resource_title?(args)
|
||||||
|
args.first.is_a?(Symbol) &&
|
||||||
|
(args[1].respond_to?(:model_name) || args[1].class.respond_to?(:model_name))
|
||||||
|
end
|
||||||
|
|
||||||
|
def page_title_for_string(args)
|
||||||
|
args.first
|
||||||
|
end
|
||||||
|
|
||||||
|
def page_title_for_resource(args)
|
||||||
|
options = args.extract_options!
|
||||||
|
model = args[1].respond_to?(:model_name) ? args[1] : args[1].class
|
||||||
|
if args.first == :index
|
||||||
|
title = t('action.index.label', models: model.model_name.human_plural)
|
||||||
|
else
|
||||||
|
title = t("action.#{args.first}.label", model: model.model_name.human)
|
||||||
|
end
|
||||||
|
if back_options = options[:back]
|
||||||
|
url =
|
||||||
|
case back_options
|
||||||
|
when Array then polymorphic_path(back_options)
|
||||||
|
when true then :back
|
||||||
|
else back_options
|
||||||
|
end
|
||||||
|
if url
|
||||||
|
back_link = link_to content_tag(:i, nil, class: 'left arrow icon'), url, class: 'title-back-link'
|
||||||
|
title = [back_link, title].join.html_safe
|
||||||
|
end
|
||||||
|
end
|
||||||
|
title
|
||||||
|
end
|
||||||
|
|
||||||
|
# It returns a proper array to select the hour of the day
|
||||||
|
def hours_for_select
|
||||||
|
(0..23).map{|v| DayTimeMinutes.new 60 * v }
|
||||||
|
end
|
||||||
|
|
||||||
|
# This is a wrapper to create collapsible content.
|
||||||
|
def collapsible_content(title, options = {}, &blk)
|
||||||
|
content = capture(&blk) if blk.present?
|
||||||
|
content ||= options.delete(:content)
|
||||||
|
collapsed = options.has_key?(:collapsed) ? options.delete(:collapsed) : true
|
||||||
|
|
||||||
|
classes = Array.wrap(options[:class]) | ["collapsible-container", collapsed ? 'collapsed' : nil]
|
||||||
|
title_tag = content_tag(:div, "<span></span>#{title}".html_safe, class: 'collapsible-title')
|
||||||
|
content_tag(:div, title_tag + content_tag(:div, content, class: 'collapsible-content'), options.merge(class: classes))
|
||||||
|
end
|
||||||
|
|
||||||
|
# Need a custom one, since rails still does not support non present dates
|
||||||
|
def dunlop_localize(date, options = {})
|
||||||
|
return '' unless date.present?
|
||||||
|
l(date, options)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Return a page relative time tag. The current time is meant to be replaced by relative time from
|
||||||
|
# the moment javascript library
|
||||||
|
def page_time(time = Time.current)
|
||||||
|
content_tag(:span, time, id: 'page-time', data: {time: time.utc.iso8601, format: 'relative'})
|
||||||
|
end
|
||||||
|
|
||||||
|
# Tested through the migration decorator
|
||||||
|
def plan_date(options = {})
|
||||||
|
return '' unless options[:date] or options[:window_from] or options[:window_to]
|
||||||
|
date = options[:date].try(:strftime, '%d-%m-%Y')
|
||||||
|
window = ''
|
||||||
|
if options[:window_from].present? or options[:window_to].present?
|
||||||
|
window_from_display = options[:window_from].present? ? "#{options[:window_from]}:00 " : ''
|
||||||
|
window_to_display = options[:window_to].present? ? " #{options[:window_to]}:00" : ''
|
||||||
|
window = " #{content_tag(:span, '', class: 'fa fa-clock-o')} #{window_from_display}-#{window_to_display}"
|
||||||
|
end
|
||||||
|
"#{date} #{window}".strip.html_safe
|
||||||
|
end
|
||||||
|
|
||||||
|
def plan_date_for_record(record)
|
||||||
|
return '' unless record.present?
|
||||||
|
plan_date(
|
||||||
|
date: record.plan_date,
|
||||||
|
window_from: record.window_from,
|
||||||
|
window_to: record.window_to
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def empty_collection(model)
|
||||||
|
content_tag(:div, t('collection.empty', models: model.model_name.human_plural), class: 'empty-collection')
|
||||||
|
end
|
||||||
|
|
||||||
|
def workflow_step_buttons_for(workflow_instance_class)
|
||||||
|
#buttons = []
|
||||||
|
#record_class.workflow_step_classes.each do |workflow_step|
|
||||||
|
# next unless can?(:manage, workflow_step)
|
||||||
|
# base_name = workflow_step.name.underscore.split('/').first
|
||||||
|
# buttons << content_tag(:button, t('perform_batch_actions_button', scope: base_name), class: "#{base_name.pluralize}-actions", data: {action: 'workflow_step', workflow_step: base_name})
|
||||||
|
#end
|
||||||
|
#buttons.join.html_safe
|
||||||
|
workflow_instance_class.workflow_step_classes.map{|workflow_step| workflow_step_button_for workflow_step }.compact.join.html_safe
|
||||||
|
end
|
||||||
|
|
||||||
|
def workflow_step_button_for(workflow_step, data_attributes = {})
|
||||||
|
return unless can?(:manage, workflow_step)
|
||||||
|
base_name = workflow_step.name.underscore.split('/').first
|
||||||
|
data_attributes[:action] ||= 'collection_edit'
|
||||||
|
data_attributes[:resource] ||= workflow_step.name.underscore
|
||||||
|
data_attributes[:request_params] = data_attributes[:request_params].to_json if data_attributes[:request_params].is_a?(Hash)
|
||||||
|
data_attributes[:request_params] ||= {workflow_instance_ids: 'selected_ids'}.to_json
|
||||||
|
text = t('workflow_step.update_button_text', models: workflow_step.model_name.human_plural)
|
||||||
|
content = content_tag(:span, text, class: 'text') + content_tag(:span, '', class: 'icon')
|
||||||
|
content_tag(:button, content, class: "#{base_name.pluralize}-actions workflow-step-actions collection-edit", data: data_attributes)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def human_duration(duration)
|
||||||
|
return duration.inspect unless duration.is_a?(ActiveSupport::Duration)
|
||||||
|
|
||||||
|
# taken and modified from ActiveSupport::Duration#inspect version 4.2.6
|
||||||
|
duration.parts.
|
||||||
|
reduce(::Hash.new(0)) { |h,(l,r)| h[l] += r; h }.
|
||||||
|
sort_by {|unit, _ | [:years, :months, :days, :minutes, :seconds].index(unit)}.
|
||||||
|
map {|unit, val| "#{val} #{val == 1 ? t(unit, scope: 'dunlop.duration', default: unit.to_s.chop) : t(unit, scope: 'dunlop.duration.plural', default: unit.to_s)}"}.
|
||||||
|
to_sentence(locale: ::I18n.locale)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def render_as_dunlop_modal(partial_name, options={})
|
||||||
|
render "dunlop/workflow_step/modal_renderer", partial_name: partial_name, options: options
|
||||||
|
end
|
||||||
|
|
||||||
|
def dunlop_class_ability(permission, klass, options={})
|
||||||
|
user = options[:user] || @user
|
||||||
|
check_box_tag "user[role_names][]", "#{permission}-class-#{klass.name}", user.role_names.include?("#{permission}-class-#{klass.name}"), id: "role-#{klass.name.underscore.parameterize}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def dunlop_ability(permission, type, target, options={})
|
||||||
|
user = options[:user] || @user
|
||||||
|
authorization = [permission, type, target].join('-')
|
||||||
|
check_box_tag "user[role_names][]", authorization, user.role_names.include?(authorization), id: "role-#{authorization}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def tarray(array, options={})
|
||||||
|
array.map do |entry|
|
||||||
|
if entry.respond_to?(:model_name)
|
||||||
|
if can?(:read, entry)
|
||||||
|
[entry.model_name.human, entry]
|
||||||
|
end
|
||||||
|
else
|
||||||
|
[entry, entry]
|
||||||
|
end
|
||||||
|
end.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
def link_to_model(model)
|
||||||
|
return nil unless model.present?
|
||||||
|
return nil unless can?(:read, model)
|
||||||
|
class_base = model.class.name.deconstantize
|
||||||
|
name = model.try(:presentation_name) || model.try(:model_name).try(:human) || model.class.name
|
||||||
|
if %w[SourceFile ExecutionBatch].include?(class_base)
|
||||||
|
path = dunlop.polymorphic_path(model.becomes(class_base.constantize)) rescue nil
|
||||||
|
else
|
||||||
|
path = main_app.polymorphic_path(model) rescue nil
|
||||||
|
#TODO: handle becomes?
|
||||||
|
end
|
||||||
|
return model.try(:presentation_name) unless path.present?
|
||||||
|
link_to name, path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
module Dunlop::AuthorizationHelper
|
||||||
|
def has_permission_of_type(type, user = current_user)
|
||||||
|
Dunlop::Ability.has_permission_of_type(type, user)
|
||||||
|
end
|
||||||
|
|
||||||
|
def any_permission_starts_with(expression, user = current_user)
|
||||||
|
Dunlop::Ability.any_permission_starts_with(expression, user)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
module Dunlop::InterpretedTextHelper
|
||||||
|
# taken from the g2l manual text helper
|
||||||
|
def interpreted_text(text, options={})
|
||||||
|
|
||||||
|
# internal references to other translation strings:
|
||||||
|
# ${models.plural.workflow_instance}
|
||||||
|
# gets replaced by the value of the actual model
|
||||||
|
text.scan(/\${[\w\.\/\-]+}/).each do |t_ref|
|
||||||
|
if t_ref == '${application.name}'
|
||||||
|
text.sub! t_ref, application_name
|
||||||
|
else
|
||||||
|
t_path = t_ref[2..-2]
|
||||||
|
t_path.prepend 'activerecord.' if t_path.starts_with?('models.') or t_path.starts_with?('attributes.')
|
||||||
|
t_path.prepend 'activerecord.state_machines.' if t_path.starts_with?('states.') # state_machine to simple notation
|
||||||
|
text.sub! t_ref, t(t_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Use shorthand notation for images:
|
||||||
|
# $i{iom-capacity} => image_tag("manual/iom-capacity.png")
|
||||||
|
# $i{image1|right;300px} => <div style="float:right;width:300px">#{image_tag("manual/image1.png")}</div>
|
||||||
|
# $i{image2|200px} => <div style="width:200px;display:inline-block">#{image_tag("manual/image2.png")}</div>
|
||||||
|
text.scan(/\$i{[\w\/\.\-;0-9|]+}/).each do |image_ref|
|
||||||
|
image_name, style_spec = image_ref[3..-2].split('|')
|
||||||
|
image_name.prepend "manual/"
|
||||||
|
image_name << ".png"
|
||||||
|
image_html = image_tag(image_name)
|
||||||
|
if style_spec.present?
|
||||||
|
style_spec.gsub!(/(left|right)/, 'float:\1')
|
||||||
|
style_spec.gsub!(/(\d+px)/, 'width:\1')
|
||||||
|
style_spec += ";display:inline-block" unless style_spec =~ /float/
|
||||||
|
image_html = "<div class='manual-image' style='#{style_spec}'>#{image_html}</div>"
|
||||||
|
end
|
||||||
|
text.sub! image_ref, image_html
|
||||||
|
end
|
||||||
|
text.html_safe
|
||||||
|
end
|
||||||
|
|
||||||
|
def interpreted_translation(path, options={})
|
||||||
|
options[:default] ||= "missing manual entry #{path} for locale #{I18n.locale}"
|
||||||
|
text = t(path, options)
|
||||||
|
interpreted_text(text, options)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
module Dunlop::LastAttributeChangeHelper
|
||||||
|
def last_changed_info(target, attr)
|
||||||
|
if target.is_a?(RecordCollection::Base)
|
||||||
|
changes = target.map{|r| r.last_changed[attr] }.compact.uniq
|
||||||
|
else # must be a ActiveRecord::Base
|
||||||
|
changes = [target.last_changed[attr]].compact
|
||||||
|
end
|
||||||
|
return nil if changes.empty?
|
||||||
|
return t('collection.last_changed.mixed_values') if changes.size > 1
|
||||||
|
changes.map{|c| I18n.localize(c, format: '%-e %b %-k:%M') }.join(', ')
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
module Dunlop::StateBadgeHelper
|
||||||
|
# Display the state badge html for a specific record.
|
||||||
|
def state_badge(target, options = {})
|
||||||
|
state_badge = Dunlop::StateBadge.new(self, target, options)
|
||||||
|
return ''.html_safe unless state_badge.valid?
|
||||||
|
state_badge.html
|
||||||
|
end
|
||||||
|
|
||||||
|
def workflow_instance_state_badges_for(workflow_instance_class)
|
||||||
|
output = ""
|
||||||
|
workflow_instance_class.scope_for(current_user, archived: session[:archived_mode]).sorted_state_count.each do |state, count|
|
||||||
|
badge = state_badge(workflow_instance_class.new(state: state), state_append_text: " (#{count})", link_to: main_app.polymorphic_path(workflow_instance_class, q: {state_eq: state}))
|
||||||
|
output << badge
|
||||||
|
end
|
||||||
|
output.html_safe
|
||||||
|
end
|
||||||
|
|
||||||
|
def workflow_step_state_badges_for(workflow_step_class)
|
||||||
|
output = ""
|
||||||
|
workflow_step_class.scope_for(current_user, archived: session[:archived_mode]).sorted_state_count.each do |state, count|
|
||||||
|
badge = state_badge workflow_step_class.new(state: state), state_append_text: " (#{count})"
|
||||||
|
output << badge
|
||||||
|
end
|
||||||
|
output.html_safe
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_workflow_progress_of(record)
|
||||||
|
workflow_steps = record.workflow_steps.select{|workflow_step| can? :read, workflow_step }
|
||||||
|
workflow_steps.map{|workflow_step| state_badge(workflow_step) }.join.html_safe
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
module Dunlop::TableHelper
|
||||||
|
def search_row(options = {}, &blk)
|
||||||
|
classes = Array.wrap(options[:class])
|
||||||
|
classes |= ['search']
|
||||||
|
classes << 'conditions-present' if @q.conditions.present?
|
||||||
|
content = capture(&blk)
|
||||||
|
content_tag(:tr, content, class: classes )
|
||||||
|
end
|
||||||
|
|
||||||
|
# This helper returns the link for showing a record inside a table
|
||||||
|
def table_show_link(record, path = nil, options = {})
|
||||||
|
if options.has_key?(:authorized)
|
||||||
|
return unless options[:authorized]
|
||||||
|
else
|
||||||
|
return unless can? :show, record
|
||||||
|
end
|
||||||
|
link_to(content_tag(:i,nil, class: 'folder open icon'), path || record, class: 'table-link show ui mini basic primary icon button')
|
||||||
|
end
|
||||||
|
|
||||||
|
# This helper returns the link for showing a record inside a table
|
||||||
|
def table_download_link(record, path = nil, options = {})
|
||||||
|
if options.has_key?(:authorized)
|
||||||
|
return unless options[:authorized]
|
||||||
|
else
|
||||||
|
return unless can? :download, record
|
||||||
|
end
|
||||||
|
link_to(content_tag(:i,nil, class: 'download icon'), path || [:download, record], class: 'table-link download ui mini violet icon button')
|
||||||
|
end
|
||||||
|
|
||||||
|
# This helper returns the link for editing a record inside a table
|
||||||
|
def table_edit_link(record, path = nil, options = {})
|
||||||
|
if options.has_key?(:authorized)
|
||||||
|
return unless options[:authorized]
|
||||||
|
else
|
||||||
|
return unless can? :update, record
|
||||||
|
end
|
||||||
|
link_to(content_tag(:i, nil, class: 'write icon'), path || [:edit, record], class: 'table-link edit ui mini basic yellow icon button')
|
||||||
|
end
|
||||||
|
|
||||||
|
def table_destroy_link(record, path = nil, options = {})
|
||||||
|
if options.has_key?(:authorized)
|
||||||
|
return unless options[:authorized]
|
||||||
|
else
|
||||||
|
return unless can? :destroy, record
|
||||||
|
end
|
||||||
|
confirm_text = "Are you sure you want to delete #{record.class.model_name.human}"
|
||||||
|
record_name = nil
|
||||||
|
record_name = record.presentation_name if record.respond_to?(:presentation_name)
|
||||||
|
record_name ||= record.name if record.respond_to?(:name)
|
||||||
|
record_name ||= record.title if record.respond_to?(:title)
|
||||||
|
confirm_text << " #{record_name}" if record_name.present?
|
||||||
|
confirm_text << "?"
|
||||||
|
link_to(content_tag(:i, nil, class: 'trash icon'), path || record, method: :delete, data: { confirm: confirm_text }, class: 'table-link destroy ui mini negative icon button')
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
module HasUuidIdentifier
|
||||||
|
UUID_MATCH = /^[0-9A-F]{8}-[0-9A-F]{4}-(?<version>[1-5])[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
before_save :ensure_identifier
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
def find(*args)
|
||||||
|
finder = args.first
|
||||||
|
if finder.is_a?(String) and finder.length == 36 and finder =~ UUID_MATCH
|
||||||
|
find_by(identifier: finder)
|
||||||
|
else
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def ensure_identifier
|
||||||
|
self.identifier ||= UUIDTools::UUID.timestamp_create.to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
module Dunlop
|
||||||
|
module ApplicationRecordAdditions
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
include Dunlop::ApplicationRecordAdditions::EpochWeeks
|
||||||
|
|
||||||
|
included do
|
||||||
|
self.abstract_class = true
|
||||||
|
self.inheritance_column = :sti_type
|
||||||
|
end
|
||||||
|
|
||||||
|
def decorate
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
if Rails::VERSION::MAJOR < 5
|
||||||
|
def saved_changes
|
||||||
|
changes
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
def truncate
|
||||||
|
#connection_pool.with_connection { |c| c.truncate table_name }
|
||||||
|
delete_all
|
||||||
|
end
|
||||||
|
|
||||||
|
def decorate
|
||||||
|
self
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
module Dunlop::ApplicationRecordAdditions::EpochWeeks
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
def set_epoch_weeks!(date_column: :date, epoch_week_column: :epoch_week)
|
||||||
|
case ActiveRecord::Base.connection.instance_values["config"][:adapter]
|
||||||
|
when 'sqlite3' then
|
||||||
|
# something
|
||||||
|
where.not(date_column => nil).update_all "#{epoch_week_column} = CAST((strftime('%J', date(#{date_column})) - 2440584.5) / 7 AS int)"
|
||||||
|
when 'postgesql' then
|
||||||
|
# something
|
||||||
|
# subtract 262800 to go to first second of epoch week, divide by 604800 seconds in a week and floor
|
||||||
|
where.not(date_column => nil).update_all "#{epoch_week_column} = FLOOR((TO_CHAR(#{date_column})::int - 2440585) / 7)"
|
||||||
|
else
|
||||||
|
where.not(date_column => nil).find_in_batches do |records|
|
||||||
|
transaction do
|
||||||
|
records.each do |record|
|
||||||
|
record.update epoch_week_column => record.public_send(date_column).epoch_week
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_epoch_weeks_from_iso_week!(week_column: :week, year_column: :year, epoch_week_column: :epoch_week)
|
||||||
|
case ActiveRecord::Base.connection.instance_values["config"][:adapter]
|
||||||
|
when 'sqlite3-disabled' then
|
||||||
|
# something
|
||||||
|
#update_all "#{epoch_week_column} = CAST((strftime('%J', date(#{date_column})) - 2440584.5) / 7 AS int)"
|
||||||
|
#NOTE todo
|
||||||
|
when 'postgesql' then
|
||||||
|
# something
|
||||||
|
# subtract 262800 to go to first second of epoch week, divide by 604800 seconds in a week and floor
|
||||||
|
where.not(week_column => nil, year_column => nil).update_all "#{epoch_week_column} = FLOOR((TO_CHAR(TO_DATE(CONCAT(#{year_column}, #{week_column}), 'iyyyiw'))::int - 2440585) / 7)"
|
||||||
|
else
|
||||||
|
where.not(week_column => nil, year_column => nil).find_in_batches do |records|
|
||||||
|
transaction do
|
||||||
|
records.each do |record|
|
||||||
|
record.update epoch_week_column => Date.commercial(record.public_send(year_column), record.public_send(week_column)).epoch_week
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Add tracking of last changed attributes. Models including this module will be able to track attribute changes like:
|
||||||
|
# record.last_changed[:backup_created] #=> timestamp or nil
|
||||||
|
# record.last_changed.backup_created #=> timestamp or nil
|
||||||
|
module Dunlop::AttributeChangesTracking
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
attr_accessor :attribute_change_time # add ability to synchronize changed_at time for collections
|
||||||
|
has_many :last_attribute_changes, as: :target, class_name: 'Dunlop::LastAttributeChange'
|
||||||
|
after_update :update_last_attribute_changed_at
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_last_attribute_changed_at
|
||||||
|
changed_at = attribute_change_time || Time.now
|
||||||
|
(saved_changes.keys - self.class.ignore_attributes_for_last_change).each do |attr|
|
||||||
|
if existing = last_attribute_changes.find{|changed| changed.attribute_name == attr }
|
||||||
|
existing.update(changed_at: changed_at)
|
||||||
|
else
|
||||||
|
last_attribute_changes.create(changed_at: changed_at, attribute_name: attr)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def last_changed
|
||||||
|
@last_changed ||= LastChanged.new(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
def with_last_attribute_changes
|
||||||
|
includes(:last_attribute_changes)
|
||||||
|
end
|
||||||
|
|
||||||
|
def ignore_attributes_for_last_change
|
||||||
|
%w[id sti_type created_at updated_at notes]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class LastChanged
|
||||||
|
attr_reader :target
|
||||||
|
def initialize(target)
|
||||||
|
@target = target
|
||||||
|
end
|
||||||
|
|
||||||
|
def method_missing(m, *args)
|
||||||
|
raise "#{target.class.name} has no attribute #{m}" unless target.has_attribute?(m)
|
||||||
|
self[m]
|
||||||
|
end
|
||||||
|
|
||||||
|
def [](val)
|
||||||
|
target.last_attribute_changes.find{|changed| changed.attribute_name == val.to_s }.try(:changed_at)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# owner_initialization.last_changed[attr]
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# This module creates a formalized way to facilitate collection lookups.
|
||||||
|
# The default convention is that every workflow model has an associated
|
||||||
|
# collection with it:
|
||||||
|
# WorkflowInstance::Scenario1::Collection
|
||||||
|
# WorkflowInstance::Scenario2::Collection
|
||||||
|
# OwnerInitialization::Scenario1::Collection
|
||||||
|
# etc...
|
||||||
|
# But since we should be ready for unforseen circumstances a lookup method
|
||||||
|
# should be used in stead of a hard namespace lookup.
|
||||||
|
module Dunlop::CollectionLookup
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
# conveniance and proper code smell implementation to avoid self.class
|
||||||
|
# calls in places where it does not belong
|
||||||
|
def collection_class
|
||||||
|
self.class.collection
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
# conveniance and proper code smell implementation to avoid self::Collection
|
||||||
|
# calls in places where it does not belong
|
||||||
|
def collection
|
||||||
|
self::Collection
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
module Dunlop::ExecutionBatchModel
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
include Dunlop::Loggable
|
||||||
|
|
||||||
|
state_machine initial: :new do
|
||||||
|
after_transition { |o| o.log_state_transition }
|
||||||
|
|
||||||
|
event(:processing) { transition :new => :processing }
|
||||||
|
event(:completed) { transition :processing => :completed }
|
||||||
|
event(:aborted) { transition any => :aborted }
|
||||||
|
event(:failed) { transition any => :failed }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute_process(options={})
|
||||||
|
with_nested_logger_and_catch_failed do
|
||||||
|
processing!
|
||||||
|
logger.benchmark { process_steps }
|
||||||
|
completed!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def abort!(message = '')
|
||||||
|
with_nested_logger_and_catch_failed do
|
||||||
|
logger.info(message) if message.present?
|
||||||
|
aborted!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_steps
|
||||||
|
raise ImplementInSubclass.new(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
def batch_pid_id
|
||||||
|
:global
|
||||||
|
end
|
||||||
|
|
||||||
|
def latest
|
||||||
|
where(state: :completed).order(identifier: :desc).order(created_at: :desc).first
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute_if_required
|
||||||
|
execute! if execution_required?
|
||||||
|
end
|
||||||
|
|
||||||
|
def execution_required?
|
||||||
|
#updated_ats = [
|
||||||
|
# SourceFile.maximum(:updated_at),
|
||||||
|
# FunctionalConfiguration.maximum(:updated_at),
|
||||||
|
#]
|
||||||
|
|
||||||
|
#latest_batch_created_at = latest.try(:created_at) || 1.year.ago
|
||||||
|
|
||||||
|
#!!updated_ats.detect do |updated_at|
|
||||||
|
# updated_at >= latest_batch_created_at
|
||||||
|
#end
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute!
|
||||||
|
return if Dunlop::ExecutionBatchPid.new(batch_pid_id).locked?
|
||||||
|
batch = nil
|
||||||
|
Dunlop::ExecutionBatchPid.new(batch_pid_id).lock do
|
||||||
|
batch = create
|
||||||
|
batch.execute_process
|
||||||
|
end
|
||||||
|
batch
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute
|
||||||
|
execute!
|
||||||
|
end
|
||||||
|
deprecate execute: "Please use execute! in stead of execute. This will be removed in dunlop v0.2.0"
|
||||||
|
|
||||||
|
def cleanup!(older_than = 1.month.ago)
|
||||||
|
older_than = older_than.ago if older_than.is_a?(ActiveSupport::Duration)
|
||||||
|
where(arel_table[:created_at].lt older_than).destroy_all
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
module Dunlop
|
||||||
|
class FileDownload < ApplicationRecord
|
||||||
|
belongs_to :user
|
||||||
|
belongs_to :file, polymorphic: true
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# This model keeps track of the timestamp (changed_at) when
|
||||||
|
# attributes of other models are changed the last time
|
||||||
|
class Dunlop::LastAttributeChange < ApplicationRecord
|
||||||
|
belongs_to :target, polymorphic: true, optional: true
|
||||||
|
validates :attribute_name, presence: true
|
||||||
|
|
||||||
|
before_validation on: :create do
|
||||||
|
self.changed_at = Time.now unless attribute_present?(:changed_at)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
class Dunlop::LogEntry < ApplicationRecord
|
||||||
|
belongs_to :loggable, polymorphic: true, optional: true
|
||||||
|
|
||||||
|
MAX_BODY_SIZE = (2**16-1)
|
||||||
|
before_save :truncate_body
|
||||||
|
|
||||||
|
def truncate_body
|
||||||
|
self.body = body.to_s.truncate(MAX_BODY_SIZE)
|
||||||
|
end
|
||||||
|
|
||||||
|
class << self
|
||||||
|
#TODO: remove me if it is confirmed that it is not used in applications
|
||||||
|
def exception_to_body(e)
|
||||||
|
[
|
||||||
|
e.message,
|
||||||
|
Array.wrap(e.backtrace)[0..9],
|
||||||
|
].flatten.join("\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
def exception_to_backtrace(e)
|
||||||
|
Array.wrap(e.backtrace).first(30).map { |line| line.to_s.sub(/#{Gem.dir}\/gems\/|#{Rails.root}\//, '') }.join("\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
module Dunlop::ProcessIdentification
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
delegate :process_name, :scenario_name, to: :class
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
def process_name
|
||||||
|
name.split('::').first.underscore
|
||||||
|
end
|
||||||
|
|
||||||
|
def scenario_name
|
||||||
|
name.split('::')[1].try(:underscore)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
module Dunlop::SourceFileModel
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
include Dunlop::SourceFileModel::ConversionHelpers
|
||||||
|
|
||||||
|
included do
|
||||||
|
include Dunlop::Loggable
|
||||||
|
has_many :downloads, class_name: 'Dunlop::FileDownload', as: :file
|
||||||
|
attr_accessor :working_file
|
||||||
|
belongs_to :creator, polymorphic: true, optional: true
|
||||||
|
|
||||||
|
mount_uploader :original_file, Dunlop::SourceFileUploader
|
||||||
|
# used by dunlop-file_transfer
|
||||||
|
def default_file
|
||||||
|
original_file
|
||||||
|
end
|
||||||
|
|
||||||
|
state_machine initial: :scheduled do
|
||||||
|
event(:scheduled) { transition [:inactive,:active] => :scheduled }
|
||||||
|
event(:loading) { transition scheduled: :loading }
|
||||||
|
event(:active) { transition loading: :active }
|
||||||
|
event(:inactive) { transition active: :inactive }
|
||||||
|
event(:failed) { transition any => :failed }
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
def deactivate_loaded_source_file
|
||||||
|
self.class.find_each do |source_file|
|
||||||
|
source_file.inactive! if source_file.active?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_source_file
|
||||||
|
self.working_file = new_working_file
|
||||||
|
unzip_working_file
|
||||||
|
pre_process_working_file
|
||||||
|
load_source_records
|
||||||
|
ensure
|
||||||
|
working_file.try(:unlink)
|
||||||
|
end
|
||||||
|
|
||||||
|
def new_working_file
|
||||||
|
local_working_file = Tempfile.new("source_file", Rails.application.config.working_path)
|
||||||
|
FileUtils.chmod(0644, local_working_file.path)
|
||||||
|
local_working_file
|
||||||
|
end
|
||||||
|
|
||||||
|
def unzip_working_file
|
||||||
|
source_path = if original_file.try(:options).try(:[], :storage) == :s3
|
||||||
|
temp = Tempfile.new 'temp' # keep reference for tempfile till unzip completed
|
||||||
|
temp.binmode
|
||||||
|
open( original_file.expiring_url ) { |data| temp.write data.read }
|
||||||
|
temp.close
|
||||||
|
temp.path
|
||||||
|
else
|
||||||
|
original_file.path
|
||||||
|
end
|
||||||
|
Dunlop::FileZip.unzip(source_path, working_file.path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def pre_process_working_file(target_path = working_file.path)
|
||||||
|
Dunlop::FileSed.fix_line_endings(target_path)
|
||||||
|
Dunlop::FileSed.in_place(target_path, %Q{s/"//g})
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_source_records
|
||||||
|
raise 'override in subclass'
|
||||||
|
end
|
||||||
|
|
||||||
|
def autoload!
|
||||||
|
#optionally override in subclass
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_first_line(expected, target_path = working_file.path)
|
||||||
|
raise "Header of #{self.class.model_name.human} is not as expected:\n\n #{expected}\n\nActual value was:\n#{first_line}" unless first_line == expected
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_first_line_starts_with(expected, target_path = working_file.path)
|
||||||
|
first_line_start = first_line[0...expected.length]
|
||||||
|
raise "First header part of file #{self.class.model_name.human} is not as expected:\n\n #{expected}\n\nActual value was:\n#{first_line_start}" unless first_line_start == expected
|
||||||
|
end
|
||||||
|
|
||||||
|
def first_line(target_path = working_file.path)
|
||||||
|
#@first_line ||= File.open(target_path).each_line{|l| break l unless l.starts_with? '#'}.to_s.strip # skip comments
|
||||||
|
@first_line ||= File.open(target_path, &:readline).to_s.strip
|
||||||
|
end
|
||||||
|
|
||||||
|
def sql_template_path
|
||||||
|
Rails.root.join('app/models/source_file/sql_templates')
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute_loading_process
|
||||||
|
with_nested_logger_and_catch_failed(nil, benchmark: true) do
|
||||||
|
logger.info I18n.t('dunlop.source_file.start_loading')
|
||||||
|
loading!
|
||||||
|
deactivate_loaded_source_file
|
||||||
|
load_source_file
|
||||||
|
active!
|
||||||
|
after_activate_hook
|
||||||
|
logger.info I18n.t('dunlop.source_file.finished_loading')
|
||||||
|
self
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Empty hook placeholder to be overwritten by subclass
|
||||||
|
def after_activate_hook
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute_sql_erb_script(name, template_binding=binding)
|
||||||
|
sql_erb_filename = File.join(sql_template_path, "#{name}.sql.erb")
|
||||||
|
sql_erb_script = File.read(sql_erb_filename)
|
||||||
|
template = ERB.new(sql_erb_script)
|
||||||
|
sql_script = template.result(template_binding)
|
||||||
|
sql_script.split(";\n").each do |sql_statement|
|
||||||
|
ActiveRecord::Base.connection.execute(sql_statement) unless sql_statement.strip.empty?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
|
||||||
|
def factory(attrs)
|
||||||
|
attrs ||= {}
|
||||||
|
klass = classes.find { |c| c.to_s == attrs[:sti_type].to_s } || self
|
||||||
|
klass.new(attrs)
|
||||||
|
end
|
||||||
|
|
||||||
|
def classes
|
||||||
|
class_names.map(&:constantize)
|
||||||
|
end
|
||||||
|
|
||||||
|
def class_names
|
||||||
|
source_file_names.map{|sfn| sfn.is_a?(Class) ? sfn.name : "#{self.name}::#{sfn.to_s.camelize}"}
|
||||||
|
end
|
||||||
|
|
||||||
|
def setup_source_files(source_file_names)
|
||||||
|
@source_file_names = source_file_names
|
||||||
|
end
|
||||||
|
|
||||||
|
def source_file_names
|
||||||
|
@source_file_names
|
||||||
|
end
|
||||||
|
|
||||||
|
def for_select
|
||||||
|
classes.map{|cls| [cls.model_name.human, cls.name]}
|
||||||
|
end
|
||||||
|
|
||||||
|
def latest
|
||||||
|
active.last
|
||||||
|
end
|
||||||
|
|
||||||
|
def last_current_day
|
||||||
|
where.not(current_at_day: nil).order(current_at_day: :desc).where.not(state: 'failed').first.try(:current_at_day)
|
||||||
|
end
|
||||||
|
|
||||||
|
def active
|
||||||
|
where(state: 'active')
|
||||||
|
end
|
||||||
|
|
||||||
|
def marker_updated_at(source_file_type)
|
||||||
|
SourceFile.
|
||||||
|
where(sti_type: source_file_type).
|
||||||
|
where(state: 'active').
|
||||||
|
order('updated_at DESC').
|
||||||
|
pluck(:updated_at).
|
||||||
|
first
|
||||||
|
end
|
||||||
|
|
||||||
|
def recent_ids
|
||||||
|
group(:sti_type).map do |combination|
|
||||||
|
sti_type = combination.sti_type
|
||||||
|
|
||||||
|
if marker = marker_updated_at(sti_type)
|
||||||
|
SourceFile.
|
||||||
|
where(sti_type: sti_type).
|
||||||
|
where.not(state: 'inactive').
|
||||||
|
#where{ updated_at >= marker }. # squeel
|
||||||
|
where("updated_at >= ?", marker).
|
||||||
|
pluck(:id)
|
||||||
|
else
|
||||||
|
SourceFile.
|
||||||
|
where(sti_type: sti_type).
|
||||||
|
pluck(:id)
|
||||||
|
end
|
||||||
|
end.flatten.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
def recent
|
||||||
|
where(id: recent_ids)
|
||||||
|
end
|
||||||
|
|
||||||
|
def archive
|
||||||
|
#where('id not in (?)',recent_ids)
|
||||||
|
where.not(id: recent_ids)
|
||||||
|
end
|
||||||
|
|
||||||
|
def cleanup
|
||||||
|
archive.find_each(&:destroy)
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_scheduled_source_files
|
||||||
|
where(state: 'scheduled').find_each do |scheduled_source_file|
|
||||||
|
scheduled_source_file.execute_loading_process
|
||||||
|
end
|
||||||
|
end
|
||||||
|
alias_method :load_scheduled!, :load_scheduled_source_files
|
||||||
|
|
||||||
|
def for_select
|
||||||
|
classes.map{|klass| [klass.model_name.human, klass.name]}
|
||||||
|
end
|
||||||
|
|
||||||
|
def activate_dunlop!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
module Dunlop::SourceFileModel::ConversionHelpers
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
def true_or_false(string)
|
||||||
|
!!(string.to_s =~ /T|J|Y|1|(?<!ON)WAAR/i)
|
||||||
|
end
|
||||||
|
|
||||||
|
def percent_to_decimal(string)
|
||||||
|
string.to_f / 100.0
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# encoding: utf-8
|
||||||
|
|
||||||
|
class Dunlop::SourceFileUploader < CarrierWave::Uploader::Base
|
||||||
|
|
||||||
|
#storage :file
|
||||||
|
|
||||||
|
# Override the directory where uploaded files will be stored.
|
||||||
|
# This is a sensible default for uploaders that are meant to be mounted:
|
||||||
|
def store_dir
|
||||||
|
File.join(
|
||||||
|
Rails.application.config.file_storage_path,
|
||||||
|
model.class.to_s.underscore,
|
||||||
|
mounted_as.to_s,
|
||||||
|
model.id.to_s
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def original_filename
|
||||||
|
File.basename(path.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Used to track when a user downloads a target file
|
||||||
|
class Dunlop::TargetFileDownload < ApplicationRecord
|
||||||
|
belongs_to :user, optional: true
|
||||||
|
belongs_to :target_file
|
||||||
|
end
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
module Dunlop::TargetFileModel
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
extend Memoist
|
||||||
|
|
||||||
|
included do
|
||||||
|
include Dunlop::Loggable
|
||||||
|
has_many :downloads, class_name: 'Dunlop::FileDownload', as: :file
|
||||||
|
attr_accessor :working_file, :original_file
|
||||||
|
attr_accessor :resource
|
||||||
|
|
||||||
|
mount_uploader :original_file, Dunlop::TargetFileUploader
|
||||||
|
# used by dunlop-file_transfer
|
||||||
|
def default_file
|
||||||
|
original_file
|
||||||
|
end
|
||||||
|
|
||||||
|
state_machine initial: :new do
|
||||||
|
before_transition on: :generating, do: :set_start_time
|
||||||
|
before_transition on: [:completed,:failed], do: :set_end_time
|
||||||
|
|
||||||
|
event(:scheduled) { transition new: :scheduled }
|
||||||
|
event(:generating) { transition scheduled: :generating }
|
||||||
|
event(:completed) { transition generating: :completed }
|
||||||
|
event(:failed) { transition any => :failed }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_start_time
|
||||||
|
self.start_time = Time.zone.now
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_end_time
|
||||||
|
self.end_time = Time.zone.now
|
||||||
|
end
|
||||||
|
|
||||||
|
def duration
|
||||||
|
if start_time and end_time
|
||||||
|
end_time - start_time
|
||||||
|
elsif start_time
|
||||||
|
Time.zone.now - start_time
|
||||||
|
else
|
||||||
|
0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_and_post_process_file
|
||||||
|
self.working_file = new_working_file
|
||||||
|
generate_file
|
||||||
|
post_process_file
|
||||||
|
attach_target_file
|
||||||
|
post_generate_hook
|
||||||
|
ensure
|
||||||
|
working_file.close! rescue SystemCallError # container permission issue
|
||||||
|
end
|
||||||
|
|
||||||
|
def local_target_filename
|
||||||
|
result = File.join(Rails.application.config.working_path, target_basename)
|
||||||
|
gzip_file? ? result + ".gz" : result
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: consider builder.file_name if builder option is used by default.
|
||||||
|
# This will synchronize file names between regular controller downloads and
|
||||||
|
# target file creation.
|
||||||
|
def target_basename
|
||||||
|
"#{self.class.name.demodulize.underscore}-#{date_number}.csv"
|
||||||
|
end
|
||||||
|
|
||||||
|
def post_generate_hook
|
||||||
|
# hook to implement post generation actions like sending by email and/or to other systems by means of web call and/or to create a transfer instance
|
||||||
|
end
|
||||||
|
|
||||||
|
# Implement in Subclass. Example:
|
||||||
|
# CsvBuilder::BladiBla.new(resource).data
|
||||||
|
def generate_file
|
||||||
|
working_file.puts builder.data
|
||||||
|
end
|
||||||
|
|
||||||
|
def builder
|
||||||
|
@builder ||= self.class.builder_class.new(resource, builder_options)
|
||||||
|
end
|
||||||
|
|
||||||
|
# can be implemented by specific builder classes, for customized builder behaviour
|
||||||
|
def builder_options
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
|
||||||
|
def header_line_count
|
||||||
|
builder.headers.present? ? 1 : 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def post_process_file
|
||||||
|
self.working_file.close
|
||||||
|
self.number_of_records = line_count
|
||||||
|
self.number_of_records -= header_line_count
|
||||||
|
gzip_file if gzip_file?
|
||||||
|
self.size = working_file_size
|
||||||
|
self.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
def attach_target_file
|
||||||
|
%x[cp #{Shellwords.escape(working_file.path)} #{Shellwords.escape(local_target_filename)}]
|
||||||
|
self.original_file = File.open(local_target_filename, 'r')
|
||||||
|
self.save!
|
||||||
|
ensure
|
||||||
|
FileUtils.rm_f(local_target_filename)
|
||||||
|
end
|
||||||
|
|
||||||
|
def new_working_file
|
||||||
|
local_working_file = Tempfile.new("target_file", Rails.application.config.working_path)
|
||||||
|
FileUtils.chmod(0666, local_working_file.path)
|
||||||
|
local_working_file
|
||||||
|
end
|
||||||
|
|
||||||
|
def gzip_file?
|
||||||
|
false #override in subclass
|
||||||
|
end
|
||||||
|
|
||||||
|
def gzip_file
|
||||||
|
return if Dunlop::FileZip.mime_type(working_file.path) == "application/x-gzip" # no double action and working file replacement
|
||||||
|
local_working_file = new_working_file
|
||||||
|
Dunlop::FileZip.gzip(working_file.path,local_working_file.path)
|
||||||
|
working_file.unlink rescue SystemCallError # container permission issue
|
||||||
|
self.working_file = local_working_file
|
||||||
|
end
|
||||||
|
|
||||||
|
def line_count
|
||||||
|
%x[wc -l #{Shellwords.escape(working_file.path)}].to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
def sql_template_path
|
||||||
|
Rails.root.join("app/models/target_file/sql_templates")
|
||||||
|
end
|
||||||
|
|
||||||
|
def prepare_working_file_for_sql_generation
|
||||||
|
working_file.close
|
||||||
|
FileUtils.rm_f(working_file.path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute_generation_process
|
||||||
|
with_nested_logger_and_catch_failed do
|
||||||
|
scheduled! unless scheduled?
|
||||||
|
generating!
|
||||||
|
generate_and_post_process_file
|
||||||
|
completed!
|
||||||
|
self # return generated file instance
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def date_number
|
||||||
|
Date.current.to_s(:number)
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
def factory(attrs)
|
||||||
|
attrs ||= {}
|
||||||
|
klass = classes.find { |c| c.to_s == attrs[:sti_type].to_s } || self
|
||||||
|
klass.new(attrs)
|
||||||
|
end
|
||||||
|
|
||||||
|
def classes
|
||||||
|
@classes ||= class_names.map(&:constantize)
|
||||||
|
end
|
||||||
|
|
||||||
|
def class_names
|
||||||
|
target_file_names.map{|tfn| tfn.is_a?(Class) ? tfn.name : "#{self.name}::#{tfn.to_s.camelize}"}
|
||||||
|
end
|
||||||
|
|
||||||
|
def setup_target_files(target_file_names)
|
||||||
|
@target_file_names = target_file_names
|
||||||
|
end
|
||||||
|
|
||||||
|
def target_file_names
|
||||||
|
@target_file_names
|
||||||
|
end
|
||||||
|
|
||||||
|
def latest
|
||||||
|
completed.last
|
||||||
|
end
|
||||||
|
|
||||||
|
def completed
|
||||||
|
where(state: 'completed')
|
||||||
|
end
|
||||||
|
|
||||||
|
def activate_dunlop!
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate!(resource = nil)
|
||||||
|
new_record = create
|
||||||
|
new_record.resource = resource if resource.present?
|
||||||
|
new_record.execute_generation_process
|
||||||
|
new_record
|
||||||
|
end
|
||||||
|
|
||||||
|
def cleanup
|
||||||
|
#noop, can't make assumptions here
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_from_source_file!(source_file)
|
||||||
|
target_file = self.new(
|
||||||
|
working_file: File.open(source_file.original_file.path, 'r'),
|
||||||
|
state: 'generating',
|
||||||
|
start_time: Time.now,
|
||||||
|
number_of_records: source_file.number_of_records,
|
||||||
|
)
|
||||||
|
target_file.size = File.size(target_file.working_file.path)
|
||||||
|
target_file.attach_target_file
|
||||||
|
target_file.completed!
|
||||||
|
target_file
|
||||||
|
end
|
||||||
|
|
||||||
|
def builder_class
|
||||||
|
"#{name}CsvBuilder".safe_constantize || ::CsvBuilder
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def working_file_size
|
||||||
|
File.size(working_file.path)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# encoding: utf-8
|
||||||
|
|
||||||
|
class Dunlop::TargetFileUploader < CarrierWave::Uploader::Base
|
||||||
|
|
||||||
|
storage :file
|
||||||
|
|
||||||
|
# Override the directory where uploaded files will be stored.
|
||||||
|
# This is a sensible default for uploaders that are meant to be mounted:
|
||||||
|
def store_dir
|
||||||
|
File.join(
|
||||||
|
Rails.application.config.file_storage_path,
|
||||||
|
model.class.to_s.underscore,
|
||||||
|
mounted_as.to_s,
|
||||||
|
model.id.to_s
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def original_filename
|
||||||
|
File.basename(path.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
module Dunlop::UserModel
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
serialize :role_names, JSONColumnCoder.new(Array)
|
||||||
|
serialize :role_names_admin, JSONColumnCoder.new(Array)
|
||||||
|
serialize :settings_storage, JSONColumnCoder.new(Hash)
|
||||||
|
has_many :source_files, as: :creator
|
||||||
|
end
|
||||||
|
|
||||||
|
def settings
|
||||||
|
@settings ||= Dunlop::UserSettings.new(self, settings_storage)
|
||||||
|
end
|
||||||
|
|
||||||
|
def all_role_names
|
||||||
|
role_names + role_names_admin
|
||||||
|
end
|
||||||
|
|
||||||
|
def presentation_name
|
||||||
|
email
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
|
||||||
|
def current
|
||||||
|
RequestStore.store[:current_user]
|
||||||
|
end
|
||||||
|
|
||||||
|
def current=(user)
|
||||||
|
RequestStore.store[:current_user] = user
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
module Dunlop::WorkflowInstanceBatchModel
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
include Dunlop::ProcessIdentification
|
||||||
|
|
||||||
|
has_many :workflow_instances, inverse_of: :workflow_instance_batch
|
||||||
|
end
|
||||||
|
|
||||||
|
def workflow_instance_class
|
||||||
|
self.class.name.sub(/WorkflowInstanceBatch/, 'WorkflowInstance').constantize
|
||||||
|
end
|
||||||
|
|
||||||
|
def workflow_step_map(user)
|
||||||
|
workflow_instance_class.workflow_step_classes.map do |workflow_step_class|
|
||||||
|
#base_scope = workflow_step_class.joins(:workflow_instance).where{ workflow_instance.workflow_instance_batch_id == my{id} } #squeel
|
||||||
|
base_scope = workflow_step_class.joins(:workflow_instance).where(workflow_instances: {workflow_instance_batch_id: id})
|
||||||
|
#base_scope = base_scope.where.not(workflow_instances: {service_provider_id: nil}).where(workflow_instances: {service_provider_id: user.service_provider_id}) unless user.admin?
|
||||||
|
#base_scope = base_scope.where(workflow_instances: {service_provider_id: user.service_provider_id, archived: false}) if user.service_provider_id.present?
|
||||||
|
[workflow_step_class, base_scope]
|
||||||
|
end.to_h
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
def active
|
||||||
|
all
|
||||||
|
end
|
||||||
|
|
||||||
|
def for_select
|
||||||
|
active.map{|mb| [mb.presentation_name, mb.id]}
|
||||||
|
end
|
||||||
|
|
||||||
|
# return a mapping for each batch containing the state counts for all
|
||||||
|
# the workflow step states. This is a performance implementation heavily
|
||||||
|
# reducing the time (queries) for large amount of batches
|
||||||
|
def workflow_step_map(user, options = {})
|
||||||
|
#NOTE: do not cache this method!
|
||||||
|
batch_ids = pluck(:id)
|
||||||
|
workflow_instance_class = self.name.sub(/WorkflowInstanceBatch/, 'WorkflowInstance').constantize
|
||||||
|
workflow_instance_class.workflow_step_classes.map.with_object(Hash.new{|h, k| h[k] = {}}) do |klass, hash|
|
||||||
|
step_counts_scope = klass.joins(:workflow_instance).where(workflow_instances: {workflow_instance_batch_id: batch_ids})
|
||||||
|
if options[:archived].present?
|
||||||
|
step_counts_scope = step_counts_scope.where.not(workflow_instances: {archived_at: nil})
|
||||||
|
else
|
||||||
|
step_counts_scope = step_counts_scope.where(workflow_instances: {archived_at: nil})
|
||||||
|
end
|
||||||
|
step_counts = step_counts_scope.group("#{klass.table_name}.state", 'workflow_instances.workflow_instance_batch_id').count
|
||||||
|
step_counts.each do |(state, batch_id), count|
|
||||||
|
hash[batch_id][klass] ||= Hash.new{|h, k| h[k] = {}}
|
||||||
|
hash[batch_id][klass][state] = count
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
module Dunlop::WorkflowInstanceCollection
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
def categorize_as(type_indication)
|
||||||
|
raise "Can only categorize uncategorized instances" unless uncategorized?
|
||||||
|
raise "#{type_indication} is not a configured scenario" unless WorkflowInstance.scenario_names.include?(type_indication)
|
||||||
|
WorkflowInstance.where(id: ids).update_all(type: "WorkflowInstance::#{type_indication.classify}")
|
||||||
|
WorkflowInstance.where(id: ids).find_each do |record|
|
||||||
|
record.setup_subprocesses
|
||||||
|
record.is_categorized
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def uncategorize!
|
||||||
|
each(&:uncategorize!)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
module Dunlop::WorkflowInstanceModel
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
include Dunlop::WorkflowInstanceModel::StateMachineHandling
|
||||||
|
include Dunlop::WorkflowInstanceModel::WorkflowStepHandling
|
||||||
|
include Dunlop::WorkflowInstanceModel::ArchivedHandling
|
||||||
|
include Dunlop::WorkflowInstanceModel::CategorizationAndResetHandling
|
||||||
|
include Dunlop::ProcessIdentification
|
||||||
|
include Dunlop::CollectionLookup
|
||||||
|
@scenario_workflow_step_names = []
|
||||||
|
|
||||||
|
belongs_to :workflow_instance_batch, inverse_of: :workflow_instances, optional: true
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_planned?
|
||||||
|
respond_to?(:plan_date) ? plan_date.present? : false
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
attr_reader :scenario_names
|
||||||
|
def setup_scenarios(scenario_names)
|
||||||
|
@scenario_names = scenario_names
|
||||||
|
end
|
||||||
|
|
||||||
|
def activate_dunlop!
|
||||||
|
possible_workflow_step_names.each do |step_name|
|
||||||
|
has_one step_name, dependent: :destroy #, inverse_of: :workflow_instance
|
||||||
|
end
|
||||||
|
#raise "No scenarios defined!, define scenarios using: setup_scenarios :scenario1, :other_scenario" unless @scenario_names
|
||||||
|
#scenario_classes # this will raise if not all of them can be constantized
|
||||||
|
end
|
||||||
|
|
||||||
|
# Add include statements to the scope for including workflow_instance
|
||||||
|
# displays for the index action
|
||||||
|
def including_relations
|
||||||
|
includes(*scenario_workflow_step_names, :workflow_instance_batch)
|
||||||
|
end
|
||||||
|
|
||||||
|
def scenario_classes
|
||||||
|
scenario_names.map{|scenario_name| "WorkflowInstance::#{scenario_name.to_s.classify}".constantize }
|
||||||
|
end
|
||||||
|
|
||||||
|
def scope_for(user, options = {})
|
||||||
|
scope = options[:all] ? all : (options[:archived] ? archived : active)
|
||||||
|
#scope = scope.where(service_provider_id: user.service_provider_id) if user.service_provider_id.present?
|
||||||
|
scope
|
||||||
|
end
|
||||||
|
|
||||||
|
def available_bucket_names
|
||||||
|
distinct.pluck(:bucket).map(&:presence).compact
|
||||||
|
end
|
||||||
|
|
||||||
|
def batch_class
|
||||||
|
@batch_class ||= name.sub('WorkflowInstance', 'WorkflowInstanceBatch').safe_constantize
|
||||||
|
end
|
||||||
|
|
||||||
|
# Mainly used in development stage from console or as a post migration
|
||||||
|
# step. This adds workflow_steps that should be there based on the
|
||||||
|
# configuration but are missing in the database
|
||||||
|
def add_missing_workflow_steps
|
||||||
|
includes(*possible_workflow_step_names).find_in_batches do |workflow_instances|
|
||||||
|
transaction do
|
||||||
|
workflow_instances.each do |workflow_instance|
|
||||||
|
workflow_instance.setup_workflow_steps
|
||||||
|
workflow_instance.save
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
module Dunlop::WorkflowInstanceModel::ArchivedHandling
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
def archived?
|
||||||
|
archived_at.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
def active
|
||||||
|
where(archived_at: nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def archived
|
||||||
|
where.not(archived_at: nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def archive!
|
||||||
|
update_all(archived_at: Time.now)
|
||||||
|
end
|
||||||
|
|
||||||
|
def revive!
|
||||||
|
update_all(archived_at: nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
module Dunlop::WorkflowInstanceModel::CategorizationAndResetHandling
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
# Reset the attributes defined on the collection to the default value of a new record
|
||||||
|
# and set the state back to unplanned and activate the special_care_flag
|
||||||
|
def reset!
|
||||||
|
workflow_steps.each(&:reset!)
|
||||||
|
new_record = self.class.new
|
||||||
|
collection_class.attributes.except('notes').each do |attr, _collection_value|
|
||||||
|
self[attr] = new_record[attr]
|
||||||
|
end
|
||||||
|
unplanned
|
||||||
|
end
|
||||||
|
|
||||||
|
# change the scenario
|
||||||
|
def categorize_as(scenario_name)
|
||||||
|
scenario_name = scenario_name.to_sym
|
||||||
|
new_scenario_class = "WorkflowInstance::#{scenario_name.to_s.classify}".constantize
|
||||||
|
return if self.class == new_scenario_class
|
||||||
|
|
||||||
|
# remove obsolete
|
||||||
|
obsolete_workflow_steps = (self.class.scenario_workflow_step_names - new_scenario_class.scenario_workflow_step_names).map(&:to_s)
|
||||||
|
workflow_steps.select{|ws| obsolete_workflow_steps.include? ws.process_name}.each(&:destroy)
|
||||||
|
|
||||||
|
extra_workflow_steps = (new_scenario_class.scenario_workflow_step_names - self.class.scenario_workflow_step_names).map(&:to_s)
|
||||||
|
_overlap_workflow_steps = (new_scenario_class.scenario_workflow_step_names & self.class.scenario_workflow_step_names).map(&:to_s)
|
||||||
|
|
||||||
|
# transform
|
||||||
|
target = becomes(new_scenario_class)
|
||||||
|
target.sti_type = new_scenario_class.name
|
||||||
|
|
||||||
|
# add missing steps
|
||||||
|
extra_workflow_steps.each{|ws| target.public_send "build_#{ws}", sti_type: "#{ws.classify}::#{scenario_name.to_s.classify}"}
|
||||||
|
|
||||||
|
target.save and target
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
module Dunlop::WorkflowInstanceModel::StateMachineHandling
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
STATE_ORDER = %w[uncategorized unplanned planned overdue active any_rejected all_completed].freeze
|
||||||
|
|
||||||
|
def final_states
|
||||||
|
%w[all_completed any_rejected]
|
||||||
|
end
|
||||||
|
|
||||||
|
included do
|
||||||
|
state_machine initial: :uncategorized do
|
||||||
|
after_transition do: :after_transition_hook
|
||||||
|
before_transition do: :before_transition_hook
|
||||||
|
event(:uncategorized) { transition any => :uncategorized }
|
||||||
|
event(:unplanned) { transition any => :unplanned }
|
||||||
|
event(:planned) { transition %i[uncategorized unplanned planned overdue any_rejected] => :planned }
|
||||||
|
event(:activate) { transition %i[uncategorized unplanned planned overdue any_rejected all_completed] => :active } # Sub process monitoring
|
||||||
|
event(:overdue) { transition %i[uncategorized unplanned planned active] => :overdue } # Sub process monitoring
|
||||||
|
event(:all_completed) { transition any => :all_completed }
|
||||||
|
event(:any_rejected) { transition any => :any_rejected }
|
||||||
|
end
|
||||||
|
|
||||||
|
before_save :set_state_based_on_subprocess_states
|
||||||
|
end
|
||||||
|
|
||||||
|
# Gets triggered after a transition
|
||||||
|
def after_transition_hook(transition)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Gets triggered before a transition
|
||||||
|
def before_transition_hook(transition)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_state_based_on_subprocess_states
|
||||||
|
states = workflow_steps.compact.map(&:state).compact.uniq
|
||||||
|
if states == ['completed']
|
||||||
|
self.state = 'all_completed'
|
||||||
|
elsif states.include? 'rejected'
|
||||||
|
self.state = 'any_rejected'
|
||||||
|
elsif states.include? 'overdue'
|
||||||
|
self.state = 'overdue'
|
||||||
|
elsif (states & %w[processing rejected completed]).any?
|
||||||
|
self.state = 'active'
|
||||||
|
elsif is_planned?
|
||||||
|
self.state = 'planned'
|
||||||
|
else
|
||||||
|
self.state = 'unplanned'
|
||||||
|
end
|
||||||
|
true # do not halt propagation
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
def sorted_state_count
|
||||||
|
group(:state).count.sort_by{|state, count| STATE_ORDER.index(state) || 99 }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
module Dunlop::WorkflowInstanceModel::WorkflowStepHandling
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
def workflow_steps
|
||||||
|
self.class.scenario_workflow_step_names.map{|workflow_step| public_send workflow_step }.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
# Make sure in all views a call to workflow_instance and workflow_instance_id can be made
|
||||||
|
def workflow_instance
|
||||||
|
self #duh
|
||||||
|
end
|
||||||
|
|
||||||
|
# Make sure in all views a call to workflow_instance and workflow_instance_id can be made
|
||||||
|
def workflow_instance_id
|
||||||
|
id
|
||||||
|
end
|
||||||
|
|
||||||
|
# Instantiate workflow_step instances if not already present
|
||||||
|
def setup_workflow_steps
|
||||||
|
return if self.class == WorkflowInstance # can only be done on scenario class
|
||||||
|
self.class.scenario_workflow_step_names.each do |step_name|
|
||||||
|
step_class = "#{step_name.to_s.classify}::#{scenario_name.classify}"
|
||||||
|
public_send "build_#{step_name}", sti_type: step_class unless public_send(step_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def workflow_step_processing!(workflow_step)
|
||||||
|
unless workflow_steps.map(&:rejected?).any?
|
||||||
|
activate
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def workflow_step_completed!(workflow_step)
|
||||||
|
if workflow_steps.map(&:rejected?).any?
|
||||||
|
any_rejected
|
||||||
|
else
|
||||||
|
workflow_steps.map(&:completed?).all? ? all_completed : activate
|
||||||
|
end
|
||||||
|
trigger_if_all_workflow_steps_in_batch_are_in_final_state(workflow_step)
|
||||||
|
end
|
||||||
|
|
||||||
|
def workflow_step_rejected!(workflow_step)
|
||||||
|
any_rejected
|
||||||
|
trigger_if_all_workflow_steps_in_batch_are_in_final_state(workflow_step)
|
||||||
|
end
|
||||||
|
|
||||||
|
def workflow_step_overdue!(workflow_step)
|
||||||
|
overdue unless final_states.include?(state)
|
||||||
|
end
|
||||||
|
|
||||||
|
# This method checks if all workflow steps in the same steps
|
||||||
|
# are in a final state and triggers a hook if this is the case
|
||||||
|
def trigger_if_all_workflow_steps_in_batch_are_in_final_state(workflow_step)
|
||||||
|
return unless workflow_instance_batch.present?
|
||||||
|
batch_states = workflow_step.workflow_steps_in_batch_scope.distinct.pluck(:state)
|
||||||
|
#return unless workflow_step.changes[:state].present?
|
||||||
|
if batch_states.any? and (batch_states - workflow_step.class.final_states).empty?
|
||||||
|
workflow_step.all_of_batch_are_in_final_state(workflow_instance_batch)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
attr_reader :scenario_workflow_step_names
|
||||||
|
|
||||||
|
# used to setup the workflow step inside a scenario class
|
||||||
|
def setup_scenario_workflow_steps(step_names)
|
||||||
|
raise "setup_workflow_steps is meant to use inside a scenario class" unless self.name.include?('::')
|
||||||
|
if illegal_step_names = (step_names - possible_workflow_step_names).presence
|
||||||
|
raise "You cannot specify scenario workflow steps that are not given as possible workflow steps. Extra steps were: #{illegal_step_names.join(', ')}"
|
||||||
|
end
|
||||||
|
@scenario_workflow_step_names = step_names
|
||||||
|
end
|
||||||
|
|
||||||
|
def possible_workflow_step_names
|
||||||
|
@@possible_workflow_step_names
|
||||||
|
end
|
||||||
|
|
||||||
|
def setup_possible_workflow_steps(possible_workflow_step_names)
|
||||||
|
@@possible_workflow_step_names = possible_workflow_step_names
|
||||||
|
end
|
||||||
|
|
||||||
|
# Return the the workflow_step classes for the WorkflowInstance.
|
||||||
|
# At
|
||||||
|
def workflow_step_classes
|
||||||
|
scenario_class_name = name.demodulize
|
||||||
|
if scenario_class_name == name
|
||||||
|
# method invoked on base class
|
||||||
|
possible_workflow_step_names.map do |step_name|
|
||||||
|
step_name.to_s.classify.constantize
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# method invoked on scenario class
|
||||||
|
scenario_workflow_step_names.map do |step_name|
|
||||||
|
"#{step_name.to_s.classify}::#{scenario_class_name}".constantize
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
#def setup_possible_workflow_steps(step_names)
|
||||||
|
#raise "setup_possible_workflow_steps is meant to use inside the workflow_instance base class" if self.name.include?('::')
|
||||||
|
#@possible_workflow_step_names = step_names
|
||||||
|
#step_names.each do |step_name|
|
||||||
|
#has_one step_name, dependent: :destroy
|
||||||
|
#end
|
||||||
|
#end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# it once was just Collection, but that intervenes with the rails lookup chain
|
||||||
|
module Dunlop::WorkflowStep::GenericCollection
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
#TODO: should this be part of the record_collection gem?
|
||||||
|
def collection?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_attributes_on_record(record, options = {})
|
||||||
|
# Add the timestamp, user info if present and the notes to append
|
||||||
|
change_time = options[:change_time] || Time.current
|
||||||
|
if self.respond_to?(:notes) and record.respond_to?(:notes)
|
||||||
|
record.append_note "\n#{change_time.iso8601} #{User.current.try(:email)}\n#{notes}".rstrip
|
||||||
|
end
|
||||||
|
|
||||||
|
record.attribute_change_time = change_time if record.respond_to?(:attribute_change_time)
|
||||||
|
|
||||||
|
# Change the record's attributes and log the changes to its notes fields
|
||||||
|
record.assign_attributes changed_attributes.except('notes')
|
||||||
|
end
|
||||||
|
|
||||||
|
# Used by set_attributes_on_record
|
||||||
|
#def get_attribute_name_and_value_for(record, attr, value)
|
||||||
|
# if attr.end_with?('_id') && value.present?
|
||||||
|
# association_name = attr[0..-4]
|
||||||
|
# association_class = record.class.reflect_on_association(association_name).klass
|
||||||
|
# attribute_name = association_class.model_name.human
|
||||||
|
# if association = association_class.find_by(id: value)
|
||||||
|
# value = association.name if association.respond_to? :name
|
||||||
|
# value = association.title if association.respond_to? :title
|
||||||
|
# end
|
||||||
|
# else
|
||||||
|
# attribute_name = self.class.human_attribute_name(attr)
|
||||||
|
# end
|
||||||
|
# [attribute_name, value]
|
||||||
|
#end
|
||||||
|
|
||||||
|
def trigger_action!(action, attributes = nil)
|
||||||
|
raise "Unsupported action #{action}" unless %w[process complete pending overdue reject].include?(action.to_s)
|
||||||
|
assign_attributes attributes if attributes
|
||||||
|
return false unless valid?
|
||||||
|
change_time = Time.current
|
||||||
|
each do |record|
|
||||||
|
set_attributes_on_record record, change_time: change_time
|
||||||
|
record.public_send("#{action}!")
|
||||||
|
end
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
module Dunlop::WorkflowStepModel
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
include Dunlop::WorkflowStepModel::StateMachine
|
||||||
|
include Dunlop::ProcessIdentification
|
||||||
|
include Dunlop::AttributeChangesTracking
|
||||||
|
|
||||||
|
included do
|
||||||
|
include Dunlop::CollectionLookup
|
||||||
|
include Dunlop::WorkflowStepModel::NotesHandling
|
||||||
|
belongs_to :workflow_instance, optional: true
|
||||||
|
#include HasManyChangeEvents
|
||||||
|
end
|
||||||
|
|
||||||
|
#TODO: should this be part of the record_collection gem?
|
||||||
|
def collection?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
# Reset the attributes defined on the collection to the default value of a new record
|
||||||
|
# and set the state back to pending
|
||||||
|
def reset!
|
||||||
|
new_record = self.class.new
|
||||||
|
collection_class.attributes.except('notes').each do |attr, clean_value|
|
||||||
|
self[attr] = new_record[attr]
|
||||||
|
end
|
||||||
|
self.notes = "[RESET]"
|
||||||
|
pending
|
||||||
|
end
|
||||||
|
|
||||||
|
# returns the scope for all the workflow steps in the same batch.
|
||||||
|
# If the record has no batch an empty scope is returned.
|
||||||
|
# record = workflow_instance.contractor_service_window1_execution
|
||||||
|
# record.workflow_steps_in_batch_scope #=> All the ContractorServiceWindow1Execution workflow step records for the batch
|
||||||
|
# record.workflow_steps_in_batch_scope(:owner_initialization) #=> All the OwnerInitialization workflow step records for the batch
|
||||||
|
def workflow_steps_in_batch_scope(workflow_step = process_name)
|
||||||
|
klass = workflow_step.to_s.classify.constantize
|
||||||
|
batch_id = workflow_instance.workflow_instance_batch_id
|
||||||
|
return klass.none unless batch_id.present?
|
||||||
|
klass.joins(:workflow_instance).where(workflow_instances: {workflow_instance_batch_id: batch_id})
|
||||||
|
end
|
||||||
|
|
||||||
|
# This method will be triggered if all the workflow intances
|
||||||
|
# in the same batch are in a final state (completed, rejected).
|
||||||
|
# To use this functionality, implment in the appropriate class
|
||||||
|
def all_of_batch_are_in_final_state(workflow_instance_batch)
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
def activate_dunlop!
|
||||||
|
# for possible future use dealing with class settings using this module's methods
|
||||||
|
end
|
||||||
|
|
||||||
|
def scope_for(user, options = {})
|
||||||
|
archived = !!options[:archived]
|
||||||
|
#if user.service_provider_id.present?
|
||||||
|
#joins(:migration).where(migrations: {service_provider_id: user.service_provider_id, archived: archived})
|
||||||
|
#else
|
||||||
|
#joins(:migration).where(migrations: {archived: archived})
|
||||||
|
#end
|
||||||
|
if archived
|
||||||
|
joins(:workflow_instance).where.not(workflow_instances: {archived_at: nil})
|
||||||
|
else
|
||||||
|
joins(:workflow_instance).where(workflow_instances: {archived_at: nil})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def final_states
|
||||||
|
%w[completed rejected]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
module Dunlop::WorkflowStepModel::NotesHandling
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
def notes=(append_text)
|
||||||
|
change_time = attribute_change_time || Time.current
|
||||||
|
write_attribute :notes,
|
||||||
|
"#{notes}\n\n#{change_time.iso8601} #{User.current.try(:email)}\n#{append_text}".strip
|
||||||
|
end
|
||||||
|
|
||||||
|
def append_note(value)
|
||||||
|
write_attribute :notes, [notes, value].join("\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
def replace_notes(value)
|
||||||
|
write_attribute :notes, value
|
||||||
|
end
|
||||||
|
alias_method :replace_notes=, :replace_notes
|
||||||
|
|
||||||
|
def replace_notes!(value)
|
||||||
|
replace_notes value
|
||||||
|
save
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
module Dunlop::WorkflowStepModel::StateMachine
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
STATE_ORDER = %w[pending overdue processing rejected completed].freeze
|
||||||
|
|
||||||
|
included do
|
||||||
|
state_machine initial: :pending do
|
||||||
|
after_transition do: :after_transition_hook
|
||||||
|
before_transition do: :before_transition_hook
|
||||||
|
event(:pending) { transition any => :pending }
|
||||||
|
event(:processing) { transition any => :processing }
|
||||||
|
event(:overdue) { transition any => :overdue } # any could be [:pending, :processing]
|
||||||
|
event(:completed) { transition any => :completed }
|
||||||
|
event(:rejected) { transition any => :rejected }
|
||||||
|
end
|
||||||
|
private *%i[
|
||||||
|
pending pending!
|
||||||
|
processing processing!
|
||||||
|
overdue overdue!
|
||||||
|
completed completed!
|
||||||
|
rejected rejected!
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def before_transition_hook(transition)
|
||||||
|
return unless self.class.columns_hash['notes'].try(:type) == :text
|
||||||
|
changes.except('notes').each do |attr, (from, to)|
|
||||||
|
case self.class.columns_hash[attr].type
|
||||||
|
when :boolean
|
||||||
|
append_note "[CHECK_CHANGE] #{self.class.human_attribute_name(attr)} changed to #{to ? 'yes' : 'no'}"
|
||||||
|
else
|
||||||
|
attribute_name, value = get_attribute_name_and_value_for(attr, to)
|
||||||
|
value = "[EMPTY]" if value == '' or value.nil?
|
||||||
|
append_note "[CHANGE] #{attribute_name} changed to #{value}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
append_note "[STATE_CHANGE] #{transition.from} => #{transition.to}" if transition.from != transition.to
|
||||||
|
end
|
||||||
|
|
||||||
|
def after_transition_hook(transition)
|
||||||
|
end
|
||||||
|
|
||||||
|
def process!
|
||||||
|
return save if processing?
|
||||||
|
processing
|
||||||
|
workflow_instance.workflow_step_processing!(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
def complete!
|
||||||
|
return save if completed?
|
||||||
|
completed
|
||||||
|
workflow_instance.workflow_step_completed!(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
def reject!
|
||||||
|
return save if rejected?
|
||||||
|
rejected
|
||||||
|
workflow_instance.workflow_step_rejected!(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_overdue!
|
||||||
|
return save if overdue?
|
||||||
|
overdue
|
||||||
|
workflow_instance.workflow_step_overdue!(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
def sorted_state_count
|
||||||
|
group(arel_table[:state]).count.sort_by{|state, count| STATE_ORDER.index(state) || 99}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def get_attribute_name_and_value_for(attr, value)
|
||||||
|
# Try to give a nice name for associations
|
||||||
|
if attr.end_with?('_id')
|
||||||
|
association_name = attr[0..-4]
|
||||||
|
association = self.class.reflect_on_association(association_name)
|
||||||
|
#TODO: Take namespacing into account.
|
||||||
|
# OwnerInitialization::Scenario1.reflect_on_association(:workflow_instance).klass
|
||||||
|
# returns WorkflowInstance in stead of WorkflowInstance::Scenario1
|
||||||
|
attribute_name = association.klass.model_name.human
|
||||||
|
if association = public_send(association.name)
|
||||||
|
value = association.title.presence if association.respond_to? :title
|
||||||
|
value ||= association.name if association.respond_to? :name
|
||||||
|
end
|
||||||
|
else
|
||||||
|
attribute_name = self.class.human_attribute_name(attr)
|
||||||
|
end
|
||||||
|
[attribute_name, value]
|
||||||
|
end
|
||||||
|
end
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user