initial commit

This commit is contained in:
2018-01-20 13:02:44 -03:00
commit 203138a969
650 changed files with 19523 additions and 0 deletions
+48
View File
@@ -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
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
### v1.2.0
* rename dunlop to dunlop-core
* drop draper dependency
+38
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
Copyright 2018 Mozolutions
+76
View File
@@ -0,0 +1,76 @@
dunlop
============
<!--[![Build Status](https://travis-ci.org/mozolutions/dunlop-core.svg?branch=master)](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
```
+33
View File
@@ -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
View File
@@ -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
+63
View File
@@ -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'))
+43
View File
@@ -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
+11
View File
@@ -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
+3
View File
@@ -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
+125
View File
@@ -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
+37
View File
@@ -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
+270
View File
@@ -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&nbsp;" : ''
window_to_display = options[:window_to].present? ? "&nbsp;#{options[:window_to]}:00" : ''
window = "&nbsp;#{content_tag(:span, '', class: 'fa fa-clock-o')}&nbsp;&nbsp;#{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
+31
View File
@@ -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
+55
View File
@@ -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]
+26
View File
@@ -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
+6
View File
@@ -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
+25
View File
@@ -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
+216
View File
@@ -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
+21
View File
@@ -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
+222
View File
@@ -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
+21
View File
@@ -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
+33
View File
@@ -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
+71
View File
@@ -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