commit 203138a96946ef048ecd2e558c058e217e2ffb0a Author: Benjamin ter Kuile Date: Sat Jan 20 13:02:44 2018 -0300 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..32365bf --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..de39e2f --- /dev/null +++ b/.travis.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..43a2a95 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +### v1.2.0 +* rename dunlop to dunlop-core +* drop draper dependency diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..94b28e5 --- /dev/null +++ b/Gemfile @@ -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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f10e4d9 --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +Copyright 2018 Mozolutions diff --git a/README.md b/README.md new file mode 100644 index 0000000..b4abcb5 --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +dunlop +============ + + +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 +``` diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..833bd43 --- /dev/null +++ b/Rakefile @@ -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 diff --git a/app/assets/images/dunlop/.keep b/app/assets/images/dunlop/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/assets/javascripts/dunlop-semantic.coffee b/app/assets/javascripts/dunlop-semantic.coffee new file mode 100644 index 0000000..ccc6bef --- /dev/null +++ b/app/assets/javascripts/dunlop-semantic.coffee @@ -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 diff --git a/app/assets/javascripts/dunlop-semantic/calendar-vendor.js b/app/assets/javascripts/dunlop-semantic/calendar-vendor.js new file mode 100644 index 0000000..cefaeb6 --- /dev/null +++ b/app/assets/javascripts/dunlop-semantic/calendar-vendor.js @@ -0,0 +1,1381 @@ +/* + * # Semantic UI 0.0.8 - Calendar + * http://github.com/semantic-org/semantic-ui/ + * + * + * Released under the MIT license + * http://opensource.org/licenses/MIT + */ + +; +(function ($, window, document, undefined) { + + "use strict"; + + window = (typeof window != 'undefined' && window.Math == Math) + ? window + : (typeof self != 'undefined' && self.Math == Math) + ? self + : Function('return this')() + ; + + $.fn.calendar = function (parameters) { + + var + $allModules = $(this), + + moduleSelector = $allModules.selector || '', + + time = new Date().getTime(), + performance = [], + + query = arguments[0], + methodInvoked = (typeof query == 'string'), + queryArguments = [].slice.call(arguments, 1), + returnedValue + ; + + $allModules + .each(function () { + var + settings = ( $.isPlainObject(parameters) ) + ? $.extend(true, {}, $.fn.calendar.settings, parameters) + : $.extend({}, $.fn.calendar.settings), + + className = settings.className, + namespace = settings.namespace, + selector = settings.selector, + formatter = settings.formatter, + parser = settings.parser, + metadata = settings.metadata, + error = settings.error, + + eventNamespace = '.' + namespace, + moduleNamespace = 'module-' + namespace, + + $module = $(this), + $input = $module.find(selector.input), + $container = $module.find(selector.popup), + $activator = $module.find(selector.activator), + + element = this, + instance = $module.data(moduleNamespace), + + isTouch, + isTouchDown = false, + focusDateUsedForRange = false, + module + ; + + module = { + + initialize: function () { + module.debug('Initializing calendar for', element); + + isTouch = module.get.isTouch(); + module.setup.popup(); + module.setup.inline(); + module.setup.input(); + module.setup.date(); + module.create.calendar(); + + module.bind.events(); + module.instantiate(); + }, + + instantiate: function () { + module.verbose('Storing instance of calendar'); + instance = module; + $module.data(moduleNamespace, instance); + }, + + destroy: function () { + module.verbose('Destroying previous calendar for', element); + $module.removeData(moduleNamespace); + module.unbind.events(); + }, + + setup: { + popup: function () { + if (settings.inline) { + return; + } + if (!$activator.length) { + $activator = $module.children().first(); + if (!$activator.length) { + return; + } + } + if ($.fn.popup === undefined) { + module.error(error.popup); + return; + } + if (!$container.length) { + //prepend the popup element to the activator's parent so that it has less chance of messing with + //the styling (eg input action button needs to be the last child to have correct border radius) + $container = $('
').addClass(className.popup).prependTo($activator.parent()); + } + $container.addClass(className.calendar); + var onVisible = settings.onVisible; + var onHidden = settings.onHidden; + if (!$input.length) { + //no input, $container has to handle focus/blur + $container.attr('tabindex', '0'); + onVisible = function () { + module.focus(); + return settings.onVisible.apply($container, arguments); + }; + onHidden = function () { + module.blur(); + return settings.onHidden.apply($container, arguments); + }; + } + var onShow = function () { + //reset the focus date onShow + module.set.focusDate(module.get.date()); + module.set.mode(settings.startMode); + return settings.onShow.apply($container, arguments); + }; + var on = settings.on || ($input.length ? 'focus' : 'click'); + var options = $.extend({}, settings.popupOptions, { + popup: $container, + on: on, + hoverable: on === 'hover', + onShow: onShow, + onVisible: onVisible, + onHide: settings.onHide, + onHidden: onHidden + }); + module.popup(options); + }, + inline: function () { + if ($activator.length && !settings.inline) { + return; + } + $container = $('
').addClass(className.calendar).appendTo($module); + if (!$input.length) { + $container.attr('tabindex', '0'); + } + }, + input: function () { + if (settings.touchReadonly && $input.length && isTouch) { + $input.prop('readonly', true); + } + }, + date: function () { + if ($input.length) { + var val = $input.val(); + var date = parser.date(val, settings); + module.set.date(date, settings.formatInput, false); + } + } + }, + + create: { + calendar: function () { + var i, r, c, p, row, cell, pageGrid; + + var mode = module.get.mode(); + var today = new Date(); + var date = module.get.date(); + var focusDate = module.get.focusDate(); + var display = focusDate || date || settings.initialDate || today; + display = module.helper.dateInRange(display); + + if (!focusDate) { + focusDate = display; + module.set.focusDate(focusDate, false, false); + } + + var isYear = mode === 'year'; + var isMonth = mode === 'month'; + var isDay = mode === 'day'; + var isHour = mode === 'hour'; + var isMinute = mode === 'minute'; + var isTimeOnly = settings.type === 'time'; + + var multiMonth = Math.max(settings.multiMonth, 1); + var monthOffset = !isDay ? 0 : module.get.monthOffset(); + + var minute = display.getMinutes(); + var hour = display.getHours(); + var day = display.getDate(); + var startMonth = display.getMonth() + monthOffset; + var year = display.getFullYear(); + + var columns = isDay ? 7 : isHour ? 4 : 3; + var columnsString = columns === 7 ? 'seven' : columns === 4 ? 'four' : 'three'; + var rows = isDay || isHour ? 6 : 4; + var pages = isDay ? multiMonth : 1; + + var container = $container; + container.empty(); + if (pages > 1) { + pageGrid = $('
').addClass(className.grid).appendTo(container); + } + + for (p = 0; p < pages; p++) { + if (pages > 1) { + var pageColumn = $('
').addClass(className.column).appendTo(pageGrid); + container = pageColumn; + } + + var month = startMonth + p; + var firstMonthDayColumn = (new Date(year, month, 1).getDay() - settings.firstDayOfWeek % 7 + 7) % 7; + if (!settings.constantHeight && isDay) { + var requiredCells = new Date(year, month + 1, 0).getDate() + firstMonthDayColumn; + rows = Math.ceil(requiredCells / 7); + } + + var yearChange = isYear ? 10 : isMonth ? 1 : 0; + var monthChange = isDay ? 1 : 0; + var dayChange = isHour || isMinute ? 1 : 0; + var prevNextDay = isHour || isMinute ? day : 1; + var prevDate = new Date(year - yearChange, month - monthChange, prevNextDay - dayChange, hour); + var nextDate = new Date(year + yearChange, month + monthChange, prevNextDay + dayChange, hour); + + var prevLast = isYear ? new Date(Math.ceil(year / 10) * 10 - 9, 0, 0) : + isMonth ? new Date(year, 0, 0) : isDay ? new Date(year, month, 0) : new Date(year, month, day, -1); + var nextFirst = isYear ? new Date(Math.ceil(year / 10) * 10 + 1, 0, 1) : + isMonth ? new Date(year + 1, 0, 1) : isDay ? new Date(year, month + 1, 1) : new Date(year, month, day + 1); + + var table = $('').addClass(className.table).addClass(columnsString + ' column').addClass(mode).appendTo(container); + + //no header for time-only mode + if (!isTimeOnly) { + var thead = $('').appendTo(table); + + row = $('').appendTo(thead); + cell = $('').appendTo(thead); + for (i = 0; i < columns; i++) { + cell = $('').appendTo(table); + i = isYear ? Math.ceil(year / 10) * 10 - 9 : isDay ? 1 - firstMonthDayColumn : 0; + for (r = 0; r < rows; r++) { + row = $('').appendTo(tbody); + for (c = 0; c < columns; c++, i++) { + var cellDate = isYear ? new Date(i, month, 1, hour, minute) : + isMonth ? new Date(year, i, 1, hour, minute) : isDay ? new Date(year, month, i, hour, minute) : + isHour ? new Date(year, month, day, i) : new Date(year, month, day, hour, i * 5); + var cellText = isYear ? i : + isMonth ? settings.text.monthsShort[i] : isDay ? cellDate.getDate() : + formatter.time(cellDate, settings, true); + cell = $('').appendTo(tbody); + var todayButton = $('
').attr('colspan', '' + columns).appendTo(row); + + var headerDate = isYear || isMonth ? new Date(year, 0, 1) : + isDay ? new Date(year, month, 1) : new Date(year, month, day, hour, minute); + var headerText = $('').addClass(className.link).appendTo(cell); + headerText.text(formatter.header(headerDate, mode, settings)); + var newMode = isMonth ? (settings.disableYear ? 'day' : 'year') : + isDay ? (settings.disableMonth ? 'year' : 'month') : 'day'; + headerText.data(metadata.mode, newMode); + + if (p === 0) { + var prev = $('').addClass(className.prev).appendTo(cell); + prev.data(metadata.focusDate, prevDate); + prev.toggleClass(className.disabledCell, !module.helper.isDateInRange(prevLast, mode)); + $('').addClass(className.prevIcon).appendTo(prev); + } + + if (p === pages - 1) { + var next = $('').addClass(className.next).appendTo(cell); + next.data(metadata.focusDate, nextDate); + next.toggleClass(className.disabledCell, !module.helper.isDateInRange(nextFirst, mode)); + $('').addClass(className.nextIcon).appendTo(next); + } + + if (isDay) { + row = $('
').appendTo(row); + cell.text(formatter.dayColumnHeader((i + settings.firstDayOfWeek) % 7, settings)); + } + } + } + + var tbody = $('
').addClass(className.cell).appendTo(row); + cell.text(cellText); + cell.data(metadata.date, cellDate); + var adjacent = isDay && cellDate.getMonth() !== ((month + 12) % 12); + var disabled = adjacent || !module.helper.isDateInRange(cellDate, mode) || settings.isDisabled(cellDate, mode); + var active = module.helper.dateEqual(cellDate, date, mode); + var isToday = module.helper.dateEqual(cellDate, today, mode); + cell.toggleClass(className.adjacentCell, adjacent); + cell.toggleClass(className.disabledCell, disabled); + cell.toggleClass(className.activeCell, active && !adjacent); + if (!isHour && !isMinute) { + cell.toggleClass(className.todayCell, !adjacent && isToday); + } + + // Allow for external modifications of each cell + var cellOptions = { + mode: mode, + adjacent: adjacent, + disabled: disabled, + active: active, + today: isToday + }; + formatter.cell(cell, cellDate, cellOptions); + + if (module.helper.dateEqual(cellDate, focusDate, mode)) { + //ensure that the focus date is exactly equal to the cell date + //so that, if selected, the correct value is set + module.set.focusDate(cellDate, false, false); + } + } + } + + if (settings.today) { + var todayRow = $('
').attr('colspan', '' + columns).addClass(className.today).appendTo(todayRow); + todayButton.text(formatter.today(settings)); + todayButton.data(metadata.date, today); + } + + module.update.focus(false, table); + } + } + }, + + update: { + focus: function (updateRange, container) { + container = container || $container; + var mode = module.get.mode(); + var date = module.get.date(); + var focusDate = module.get.focusDate(); + var startDate = module.get.startDate(); + var endDate = module.get.endDate(); + var rangeDate = (updateRange ? focusDate : null) || date || (!isTouch ? focusDate : null); + + container.find('td').each(function () { + var cell = $(this); + var cellDate = cell.data(metadata.date); + if (!cellDate) { + return; + } + var disabled = cell.hasClass(className.disabledCell); + var active = cell.hasClass(className.activeCell); + var adjacent = cell.hasClass(className.adjacentCell); + var focused = module.helper.dateEqual(cellDate, focusDate, mode); + var inRange = !rangeDate ? false : + ((!!startDate && module.helper.isDateInRange(cellDate, mode, startDate, rangeDate)) || + (!!endDate && module.helper.isDateInRange(cellDate, mode, rangeDate, endDate))); + cell.toggleClass(className.focusCell, focused && (!isTouch || isTouchDown) && !adjacent); + cell.toggleClass(className.rangeCell, inRange && !active && !disabled); + }); + } + }, + + refresh: function () { + module.create.calendar(); + }, + + bind: { + events: function () { + $container.on('mousedown' + eventNamespace, module.event.mousedown); + $container.on('touchstart' + eventNamespace, module.event.mousedown); + $container.on('mouseup' + eventNamespace, module.event.mouseup); + $container.on('touchend' + eventNamespace, module.event.mouseup); + $container.on('mouseover' + eventNamespace, module.event.mouseover); + if ($input.length) { + $input.on('input' + eventNamespace, module.event.inputChange); + $input.on('focus' + eventNamespace, module.event.inputFocus); + $input.on('blur' + eventNamespace, module.event.inputBlur); + $input.on('click' + eventNamespace, module.event.inputClick); + $input.on('keydown' + eventNamespace, module.event.keydown); + } else { + $container.on('keydown' + eventNamespace, module.event.keydown); + } + } + }, + + unbind: { + events: function () { + $container.off(eventNamespace); + if ($input.length) { + $input.off(eventNamespace); + } + } + }, + + event: { + mouseover: function (event) { + var target = $(event.target); + var date = target.data(metadata.date); + var mousedown = event.buttons === 1; + if (date) { + module.set.focusDate(date, false, true, mousedown); + } + }, + mousedown: function (event) { + if ($input.length) { + //prevent the mousedown on the calendar causing the input to lose focus + event.preventDefault(); + } + isTouchDown = event.type.indexOf('touch') >= 0; + var target = $(event.target); + var date = target.data(metadata.date); + if (date) { + module.set.focusDate(date, false, true, true); + } + }, + mouseup: function (event) { + //ensure input has focus so that it receives keydown events for calendar navigation + module.focus(); + event.preventDefault(); + event.stopPropagation(); + isTouchDown = false; + var target = $(event.target); + var parent = target.parent(); + if (parent.data(metadata.date) || parent.data(metadata.focusDate) || parent.data(metadata.mode)) { + //clicked on a child element, switch to parent (used when clicking directly on prev/next icon element) + target = parent; + } + var date = target.data(metadata.date); + var focusDate = target.data(metadata.focusDate); + var mode = target.data(metadata.mode); + if (date) { + var forceSet = target.hasClass(className.today); + module.selectDate(date, forceSet); + } + else if (focusDate) { + module.set.focusDate(focusDate); + } + else if (mode) { + module.set.mode(mode); + } + }, + keydown: function (event) { + if (event.keyCode === 27 || event.keyCode === 9) { + //esc || tab + module.popup('hide'); + } + + if (module.popup('is visible')) { + if (event.keyCode === 37 || event.keyCode === 38 || event.keyCode === 39 || event.keyCode === 40) { + //arrow keys + var mode = module.get.mode(); + var bigIncrement = mode === 'day' ? 7 : mode === 'hour' ? 4 : 3; + var increment = event.keyCode === 37 ? -1 : event.keyCode === 38 ? -bigIncrement : event.keyCode == 39 ? 1 : bigIncrement; + increment *= mode === 'minute' ? 5 : 1; + var focusDate = module.get.focusDate() || module.get.date() || new Date(); + var year = focusDate.getFullYear() + (mode === 'year' ? increment : 0); + var month = focusDate.getMonth() + (mode === 'month' ? increment : 0); + var day = focusDate.getDate() + (mode === 'day' ? increment : 0); + var hour = focusDate.getHours() + (mode === 'hour' ? increment : 0); + var minute = focusDate.getMinutes() + (mode === 'minute' ? increment : 0); + var newFocusDate = new Date(year, month, day, hour, minute); + if (settings.type === 'time') { + newFocusDate = module.helper.mergeDateTime(focusDate, newFocusDate); + } + if (module.helper.isDateInRange(newFocusDate, mode)) { + module.set.focusDate(newFocusDate); + } + } else if (event.keyCode === 13) { + //enter + var mode = module.get.mode(); + var date = module.get.focusDate(); + if (date && !settings.isDisabled(date, mode)) { + module.selectDate(date); + } + //disable form submission: + event.preventDefault(); + event.stopPropagation(); + } + } + + if (event.keyCode === 38 || event.keyCode === 40) { + //arrow-up || arrow-down + event.preventDefault(); //don't scroll + module.popup('show'); + } + }, + inputChange: function () { + var val = $input.val(); + var date = parser.date(val, settings); + module.set.date(date, false); + }, + inputFocus: function () { + $container.addClass(className.active); + }, + inputBlur: function () { + $container.removeClass(className.active); + if (settings.formatInput) { + var date = module.get.date(); + var text = formatter.datetime(date, settings); + $input.val(text); + } + }, + inputClick: function () { + module.popup('show'); + } + }, + + get: { + date: function () { + return $module.data(metadata.date) || null; + }, + focusDate: function () { + return $module.data(metadata.focusDate) || null; + }, + startDate: function () { + var startModule = module.get.calendarModule(settings.startCalendar); + return (startModule ? startModule.get.date() : $module.data(metadata.startDate)) || null; + }, + endDate: function () { + var endModule = module.get.calendarModule(settings.endCalendar); + return (endModule ? endModule.get.date() : $module.data(metadata.endDate)) || null; + }, + monthOffset: function () { + return $module.data(metadata.monthOffset) || 0; + }, + mode: function () { + //only returns valid modes for the current settings + var mode = $module.data(metadata.mode) || settings.startMode; + var validModes = module.get.validModes(); + if ($.inArray(mode, validModes) >= 0) { + return mode; + } + return settings.type === 'time' ? 'hour' : + settings.type === 'month' ? 'month' : + settings.type === 'year' ? 'year' : 'day'; + }, + validModes: function () { + var validModes = []; + if (settings.type !== 'time') { + if (!settings.disableYear || settings.type === 'year') { + validModes.push('year'); + } + if (!(settings.disableMonth || settings.type === 'year') || settings.type === 'month') { + validModes.push('month'); + } + if (settings.type.indexOf('date') >= 0) { + validModes.push('day'); + } + } + if (settings.type.indexOf('time') >= 0) { + validModes.push('hour'); + if (!settings.disableMinute) { + validModes.push('minute'); + } + } + return validModes; + }, + isTouch: function () { + try { + document.createEvent('TouchEvent'); + return true; + } + catch (e) { + return false; + } + }, + calendarModule: function (selector) { + if (!selector) { + return null; + } + if (!(selector instanceof $)) { + selector = $module.parent().children(selector).first(); + } + //assume range related calendars are using the same namespace + return selector.data(moduleNamespace); + } + }, + + set: { + date: function (date, updateInput, fireChange) { + updateInput = updateInput !== false; + fireChange = fireChange !== false; + date = module.helper.sanitiseDate(date); + date = module.helper.dateInRange(date); + + var mode = module.get.mode(); + var text = formatter.datetime(date, settings); + if (fireChange && settings.onChange.call(element, date, text, mode) === false) { + return false; + } + + module.set.focusDate(date); + + if (settings.isDisabled(date, mode)) { + return false; + } + + var endDate = module.get.endDate(); + if (!!endDate && !!date && date > endDate) { + //selected date is greater than end date in range, so clear end date + module.set.endDate(undefined); + } + module.set.dataKeyValue(metadata.date, date); + + if (updateInput && $input.length) { + $input.val(text); + } + }, + startDate: function (date, refreshCalendar) { + date = module.helper.sanitiseDate(date); + var startModule = module.get.calendarModule(settings.startCalendar); + if (startModule) { + startModule.set.date(date); + } + module.set.dataKeyValue(metadata.startDate, date, refreshCalendar); + }, + endDate: function (date, refreshCalendar) { + date = module.helper.sanitiseDate(date); + var endModule = module.get.calendarModule(settings.endCalendar); + if (endModule) { + endModule.set.date(date); + } + module.set.dataKeyValue(metadata.endDate, date, refreshCalendar); + }, + focusDate: function (date, refreshCalendar, updateFocus, updateRange) { + date = module.helper.sanitiseDate(date); + date = module.helper.dateInRange(date); + var isDay = module.get.mode() === 'day'; + var oldFocusDate = module.get.focusDate(); + if (isDay && date && oldFocusDate) { + var yearDelta = date.getFullYear() - oldFocusDate.getFullYear(); + var monthDelta = yearDelta * 12 + date.getMonth() - oldFocusDate.getMonth(); + if (monthDelta) { + var monthOffset = module.get.monthOffset() - monthDelta; + module.set.monthOffset(monthOffset, false); + } + } + var changed = module.set.dataKeyValue(metadata.focusDate, date, refreshCalendar); + updateFocus = (updateFocus !== false && changed && refreshCalendar === false) || focusDateUsedForRange != updateRange; + focusDateUsedForRange = updateRange; + if (updateFocus) { + module.update.focus(updateRange); + } + }, + monthOffset: function (monthOffset, refreshCalendar) { + var multiMonth = Math.max(settings.multiMonth, 1); + monthOffset = Math.max(1 - multiMonth, Math.min(0, monthOffset)); + module.set.dataKeyValue(metadata.monthOffset, monthOffset, refreshCalendar); + }, + mode: function (mode, refreshCalendar) { + module.set.dataKeyValue(metadata.mode, mode, refreshCalendar); + }, + dataKeyValue: function (key, value, refreshCalendar) { + var oldValue = $module.data(key); + var equal = oldValue === value || (oldValue <= value && oldValue >= value); //equality test for dates and string objects + if (value) { + $module.data(key, value); + } else { + $module.removeData(key); + } + refreshCalendar = refreshCalendar !== false && !equal; + if (refreshCalendar) { + module.create.calendar(); + } + return !equal; + } + }, + + selectDate: function (date, forceSet) { + var mode = module.get.mode(); + var complete = forceSet || mode === 'minute' || + (settings.disableMinute && mode === 'hour') || + (settings.type === 'date' && mode === 'day') || + (settings.type === 'month' && mode === 'month') || + (settings.type === 'year' && mode === 'year'); + if (complete) { + var canceled = module.set.date(date) === false; + if (!canceled && settings.closable) { + module.popup('hide'); + //if this is a range calendar, show the end date calendar popup and focus the input + var endModule = module.get.calendarModule(settings.endCalendar); + if (endModule) { + endModule.popup('show'); + endModule.focus(); + } + } + } else { + var newMode = mode === 'year' ? (!settings.disableMonth ? 'month' : 'day') : + mode === 'month' ? 'day' : mode === 'day' ? 'hour' : 'minute'; + module.set.mode(newMode); + if (mode === 'hour' || (mode === 'day' && module.get.date())) { + //the user has chosen enough to consider a valid date/time has been chosen + module.set.date(date); + } else { + module.set.focusDate(date); + } + } + }, + + changeDate: function (date) { + module.set.date(date); + }, + + clear: function () { + module.set.date(undefined); + }, + + popup: function () { + return $activator.popup.apply($activator, arguments); + }, + + focus: function () { + if ($input.length) { + $input.focus(); + } else { + $container.focus(); + } + }, + blur: function () { + if ($input.length) { + $input.blur(); + } else { + $container.blur(); + } + }, + + helper: { + sanitiseDate: function (date) { + if (!date) { + return undefined; + } + if (!(date instanceof Date)) { + date = parser.date('' + date, settings); + } + if (isNaN(date.getTime())) { + return undefined; + } + return date; + }, + dateDiff: function (date1, date2, mode) { + mode = mode || 'day'; + var isTimeOnly = settings.type === 'time'; + var isYear = mode === 'year'; + var isYearOrMonth = isYear || mode === 'month'; + var isMinute = mode === 'minute'; + var isHourOrMinute = isMinute || mode === 'hour'; + //only care about a minute accuracy of 5 + date1 = new Date( + isTimeOnly ? 2000 : date1.getFullYear(), + isTimeOnly ? 0 : isYear ? 0 : date1.getMonth(), + isTimeOnly ? 1 : isYearOrMonth ? 1 : date1.getDate(), + !isHourOrMinute ? 0 : date1.getHours(), + !isMinute ? 0 : 5 * Math.floor(date1.getMinutes() / 5)); + date2 = new Date( + isTimeOnly ? 2000 : date2.getFullYear(), + isTimeOnly ? 0 : isYear ? 0 : date2.getMonth(), + isTimeOnly ? 1 : isYearOrMonth ? 1 : date2.getDate(), + !isHourOrMinute ? 0 : date2.getHours(), + !isMinute ? 0 : 5 * Math.floor(date2.getMinutes() / 5)); + return date2.getTime() - date1.getTime(); + }, + dateEqual: function (date1, date2, mode) { + return !!date1 && !!date2 && module.helper.dateDiff(date1, date2, mode) === 0; + }, + isDateInRange: function (date, mode, minDate, maxDate) { + if (!minDate && !maxDate) { + var startDate = module.get.startDate(); + minDate = startDate && settings.minDate ? new Date(Math.max(startDate, settings.minDate)) : startDate || settings.minDate; + maxDate = settings.maxDate; + } + minDate = minDate && new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate(), minDate.getHours(), 5 * Math.ceil(minDate.getMinutes() / 5)); + return !(!date || + (minDate && module.helper.dateDiff(date, minDate, mode) > 0) || + (maxDate && module.helper.dateDiff(maxDate, date, mode) > 0)); + }, + dateInRange: function (date, minDate, maxDate) { + if (!minDate && !maxDate) { + var startDate = module.get.startDate(); + minDate = startDate && settings.minDate ? new Date(Math.max(startDate, settings.minDate)) : startDate || settings.minDate; + maxDate = settings.maxDate; + } + minDate = minDate && new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate(), minDate.getHours(), 5 * Math.ceil(minDate.getMinutes() / 5)); + var isTimeOnly = settings.type === 'time'; + return !date ? date : + (minDate && module.helper.dateDiff(date, minDate, 'minute') > 0) ? + (isTimeOnly ? module.helper.mergeDateTime(date, minDate) : minDate) : + (maxDate && module.helper.dateDiff(maxDate, date, 'minute') > 0) ? + (isTimeOnly ? module.helper.mergeDateTime(date, maxDate) : maxDate) : + date; + }, + mergeDateTime: function (date, time) { + return (!date || !time) ? time : + new Date(date.getFullYear(), date.getMonth(), date.getDate(), time.getHours(), time.getMinutes()); + } + }, + + setting: function (name, value) { + module.debug('Changing setting', name, value); + if ($.isPlainObject(name)) { + $.extend(true, settings, name); + } + else if (value !== undefined) { + if ($.isPlainObject(settings[name])) { + $.extend(true, settings[name], value); + } + else { + settings[name] = value; + } + } + else { + return settings[name]; + } + }, + internal: function (name, value) { + module.debug('Changing internal', name, value); + if (value !== undefined) { + if ($.isPlainObject(name)) { + $.extend(true, module, name); + } + else { + module[name] = value; + } + } + else { + return module[name]; + } + }, + debug: function () { + if (!settings.silent && settings.debug) { + if (settings.performance) { + module.performance.log(arguments); + } + else { + module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.debug.apply(console, arguments); + } + } + }, + verbose: function () { + if (!settings.silent && settings.verbose && settings.debug) { + if (settings.performance) { + module.performance.log(arguments); + } + else { + module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.verbose.apply(console, arguments); + } + } + }, + error: function () { + if (!settings.silent) { + module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); + module.error.apply(console, arguments); + } + }, + performance: { + log: function (message) { + var + currentTime, + executionTime, + previousTime + ; + if (settings.performance) { + currentTime = new Date().getTime(); + previousTime = time || currentTime; + executionTime = currentTime - previousTime; + time = currentTime; + performance.push({ + 'Name': message[0], + 'Arguments': [].slice.call(message, 1) || '', + 'Element': element, + 'Execution Time': executionTime + }); + } + clearTimeout(module.performance.timer); + module.performance.timer = setTimeout(module.performance.display, 500); + }, + display: function () { + var + title = settings.name + ':', + totalTime = 0 + ; + time = false; + clearTimeout(module.performance.timer); + $.each(performance, function (index, data) { + totalTime += data['Execution Time']; + }); + title += ' ' + totalTime + 'ms'; + if (moduleSelector) { + title += ' \'' + moduleSelector + '\''; + } + if ((console.group !== undefined || console.table !== undefined) && performance.length > 0) { + console.groupCollapsed(title); + if (console.table) { + console.table(performance); + } + else { + $.each(performance, function (index, data) { + console.log(data['Name'] + ': ' + data['Execution Time'] + 'ms'); + }); + } + console.groupEnd(); + } + performance = []; + } + }, + invoke: function (query, passedArguments, context) { + var + object = instance, + maxDepth, + found, + response + ; + passedArguments = passedArguments || queryArguments; + context = element || context; + if (typeof query == 'string' && object !== undefined) { + query = query.split(/[\. ]/); + maxDepth = query.length - 1; + $.each(query, function (depth, value) { + var camelCaseValue = (depth != maxDepth) + ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) + : query + ; + if ($.isPlainObject(object[camelCaseValue]) && (depth != maxDepth)) { + object = object[camelCaseValue]; + } + else if (object[camelCaseValue] !== undefined) { + found = object[camelCaseValue]; + return false; + } + else if ($.isPlainObject(object[value]) && (depth != maxDepth)) { + object = object[value]; + } + else if (object[value] !== undefined) { + found = object[value]; + return false; + } + else { + module.error(error.method, query); + return false; + } + }); + } + if ($.isFunction(found)) { + response = found.apply(context, passedArguments); + } + else if (found !== undefined) { + response = found; + } + if ($.isArray(returnedValue)) { + returnedValue.push(response); + } + else if (returnedValue !== undefined) { + returnedValue = [returnedValue, response]; + } + else if (response !== undefined) { + returnedValue = response; + } + return found; + } + }; + + if (methodInvoked) { + if (instance === undefined) { + module.initialize(); + } + module.invoke(query); + } + else { + if (instance !== undefined) { + instance.invoke('destroy'); + } + module.initialize(); + } + }) + ; + return (returnedValue !== undefined) + ? returnedValue + : this + ; + }; + + $.fn.calendar.settings = { + + name: 'Calendar', + namespace: 'calendar', + + silent: false, + debug: false, + verbose: false, + performance: false, + + type: 'datetime', // picker type, can be 'datetime', 'date', 'time', 'month', or 'year' + firstDayOfWeek: 0, // day for first day column (0 = Sunday) + constantHeight: true, // add rows to shorter months to keep day calendar height consistent (6 rows) + today: false, // show a 'today/now' button at the bottom of the calendar + closable: true, // close the popup after selecting a date/time + monthFirst: true, // month before day when parsing/converting date from/to text + touchReadonly: true, // set input to readonly on touch devices + inline: false, // create the calendar inline instead of inside a popup + on: null, // when to show the popup (defaults to 'focus' for input, 'click' for others) + initialDate: null, // date to display initially when no date is selected (null = now) + startMode: false, // display mode to start in, can be 'year', 'month', 'day', 'hour', 'minute' (false = 'day') + minDate: null, // minimum date/time that can be selected, dates/times before are disabled + maxDate: null, // maximum date/time that can be selected, dates/times after are disabled + ampm: true, // show am/pm in time mode + disableYear: false, // disable year selection mode + disableMonth: false, // disable month selection mode + disableMinute: false, // disable minute selection mode + formatInput: true, // format the input text upon input blur and module creation + startCalendar: null, // jquery object or selector for another calendar that represents the start date of a date range + endCalendar: null, // jquery object or selector for another calendar that represents the end date of a date range + multiMonth: 1, // show multiple months when in 'day' mode + + // popup options ('popup', 'on', 'hoverable', and show/hide callbacks are overridden) + popupOptions: { + position: 'bottom left', + lastResort: 'bottom left', + prefer: 'opposite', + hideOnScroll: false + }, + + text: { + days: ['S', 'M', 'T', 'W', 'T', 'F', 'S'], + months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], + monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + today: 'Today', + now: 'Now', + am: 'AM', + pm: 'PM' + }, + + formatter: { + header: function (date, mode, settings) { + return mode === 'year' ? settings.formatter.yearHeader(date, settings) : + mode === 'month' ? settings.formatter.monthHeader(date, settings) : + mode === 'day' ? settings.formatter.dayHeader(date, settings) : + mode === 'hour' ? settings.formatter.hourHeader(date, settings) : + settings.formatter.minuteHeader(date, settings); + }, + yearHeader: function (date, settings) { + var decadeYear = Math.ceil(date.getFullYear() / 10) * 10; + return (decadeYear - 9) + ' - ' + (decadeYear + 2); + }, + monthHeader: function (date, settings) { + return date.getFullYear(); + }, + dayHeader: function (date, settings) { + var month = settings.text.months[date.getMonth()]; + var year = date.getFullYear(); + return month + ' ' + year; + }, + hourHeader: function (date, settings) { + return settings.formatter.date(date, settings); + }, + minuteHeader: function (date, settings) { + return settings.formatter.date(date, settings); + }, + dayColumnHeader: function (day, settings) { + return settings.text.days[day]; + }, + datetime: function (date, settings) { + if (!date) { + return ''; + } + var day = settings.type === 'time' ? '' : settings.formatter.date(date, settings); + var time = settings.type.indexOf('time') < 0 ? '' : settings.formatter.time(date, settings, false); + var separator = settings.type === 'datetime' ? ' ' : ''; + return day + separator + time; + }, + date: function (date, settings) { + if (!date) { + return ''; + } + var day = date.getDate(); + var month = settings.text.months[date.getMonth()]; + var year = date.getFullYear(); + return settings.type === 'year' ? year : + settings.type === 'month' ? month + ' ' + year : + (settings.monthFirst ? month + ' ' + day : day + ' ' + month) + ', ' + year; + }, + time: function (date, settings, forCalendar) { + if (!date) { + return ''; + } + var hour = date.getHours(); + var minute = date.getMinutes(); + var ampm = ''; + if (settings.ampm) { + ampm = ' ' + (hour < 12 ? settings.text.am : settings.text.pm); + hour = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour; + } + return hour + ':' + (minute < 10 ? '0' : '') + minute + ampm; + }, + today: function (settings) { + return settings.type === 'date' ? settings.text.today : settings.text.now; + }, + cell: function (cell, date, cellOptions) { + } + }, + + parser: { + date: function (text, settings) { + if (!text) { + return null; + } + text = ('' + text).trim().toLowerCase(); + if (text.length === 0) { + return null; + } + + var i, j, k; + var minute = -1, hour = -1, day = -1, month = -1, year = -1; + var isAm = undefined; + + var isTimeOnly = settings.type === 'time'; + var isDateOnly = settings.type.indexOf('time') < 0; + + var words = text.split(settings.regExp.dateWords); + var numbers = text.split(settings.regExp.dateNumbers); + + if (!isDateOnly) { + //am/pm + isAm = $.inArray(settings.text.am.toLowerCase(), words) >= 0 ? true : + $.inArray(settings.text.pm.toLowerCase(), words) >= 0 ? false : undefined; + + //time with ':' + for (i = 0; i < numbers.length; i++) { + var number = numbers[i]; + if (number.indexOf(':') >= 0) { + if (hour < 0 || minute < 0) { + var parts = number.split(':'); + for (k = 0; k < Math.min(2, parts.length); k++) { + j = parseInt(parts[k]); + if (isNaN(j)) { + j = 0; + } + if (k === 0) { + hour = j % 24; + } else { + minute = j % 60; + } + } + } + numbers.splice(i, 1); + } + } + } + /*else { + return moment(text).toDate(); + }*/ + + if (!isTimeOnly) { + //textual month + for (i = 0; i < words.length; i++) { + var word = words[i]; + if (word.length <= 0) { + continue; + } + word = word.substring(0, Math.min(word.length, 3)); + for (j = 0; j < settings.text.months.length; j++) { + var monthString = settings.text.months[j]; + monthString = monthString.substring(0, Math.min(word.length, Math.min(monthString.length, 3))).toLowerCase(); + if (monthString === word) { + month = j + 1; + break; + } + } + if (month >= 0) { + break; + } + } + + //year > 59 + for (i = 0; i < numbers.length; i++) { + j = parseInt(numbers[i]); + if (isNaN(j)) { + continue; + } + if (j > 59) { + year = j; + numbers.splice(i, 1); + break; + } + } + + //numeric month + if (month < 0) { + for (i = 0; i < numbers.length; i++) { + k = i > 1 || settings.monthFirst ? i : i === 1 ? 0 : 1; + j = parseInt(numbers[k]); + if (isNaN(j)) { + continue; + } + if (1 <= j && j <= 12) { + month = j; + numbers.splice(k, 1); + break; + } + } + } + + //day + for (i = 0; i < numbers.length; i++) { + j = parseInt(numbers[i]); + if (isNaN(j)) { + continue; + } + if (1 <= j && j <= 31) { + day = j; + numbers.splice(i, 1); + break; + } + } + + //year <= 59 + if (year < 0) { + for (i = numbers.length - 1; i >= 0; i--) { + j = parseInt(numbers[i]); + if (isNaN(j)) { + continue; + } + if (j < 99) { + j += 2000; + } + year = j; + numbers.splice(i, 1); + break; + } + } + } + + if (!isDateOnly) { + //hour + if (hour < 0) { + for (i = 0; i < numbers.length; i++) { + j = parseInt(numbers[i]); + if (isNaN(j)) { + continue; + } + if (0 <= j && j <= 23) { + hour = j; + numbers.splice(i, 1); + break; + } + } + } + + //minute + if (minute < 0) { + for (i = 0; i < numbers.length; i++) { + j = parseInt(numbers[i]); + if (isNaN(j)) { + continue; + } + if (0 <= j && j <= 59) { + minute = j; + numbers.splice(i, 1); + break; + } + } + } + } + + if (minute < 0 && hour < 0 && day < 0 && month < 0 && year < 0) { + return null; + } + + if (minute < 0) { + minute = 0; + } + if (hour < 0) { + hour = 0; + } + if (day < 0) { + day = 1; + } + if (month < 0) { + month = 1; + } + if (year < 0) { + year = new Date().getFullYear(); + } + + if (isAm !== undefined) { + if (isAm) { + if (hour === 12) { + hour = 0; + } + } else if (hour < 12) { + hour += 12; + } + } + + var date = new Date(year, month - 1, day, hour, minute); + if (date.getMonth() !== month - 1 || date.getFullYear() !== year) { + //month or year don't match up, switch to last day of the month + date = new Date(year, month, 0, hour, minute); + } + return isNaN(date.getTime()) ? null : date; + } + }, + + // callback when date changes, return false to cancel the change + onChange: function (date, text, mode) { + return true; + }, + + // callback before show animation, return false to prevent show + onShow: function () { + }, + + // callback after show animation + onVisible: function () { + }, + + // callback before hide animation, return false to prevent hide + onHide: function () { + }, + + // callback after hide animation + onHidden: function () { + }, + + // is the given date disabled? + isDisabled: function (date, mode) { + return false; + }, + + selector: { + popup: '.ui.popup', + input: 'input', + activator: 'input' + }, + + regExp: { + dateWords: /[^A-Za-z\u00C0-\u024F]+/g, + dateNumbers: /[^\d:]+/g + }, + + error: { + popup: 'UI Popup, a required component is not included in this page', + method: 'The method you called is not defined.' + }, + + className: { + calendar: 'calendar', + active: 'active', + popup: 'ui popup', + grid: 'ui equal width grid', + column: 'column', + table: 'ui celled center aligned unstackable table', + prev: 'prev link', + next: 'next link', + prevIcon: 'chevron left icon', + nextIcon: 'chevron right icon', + link: 'link', + cell: 'link', + disabledCell: 'disabled', + adjacentCell: 'adjacent', + activeCell: 'active', + rangeCell: 'range', + focusCell: 'focus', + todayCell: 'today', + today: 'today link' + }, + + metadata: { + date: 'date', + focusDate: 'focusDate', + startDate: 'startDate', + endDate: 'endDate', + mode: 'mode', + monthOffset: 'monthOffset' + } + }; + +})(jQuery, window, document); diff --git a/app/assets/javascripts/dunlop-semantic/calendar.coffee b/app/assets/javascripts/dunlop-semantic/calendar.coffee new file mode 100644 index 0000000..c52a971 --- /dev/null +++ b/app/assets/javascripts/dunlop-semantic/calendar.coffee @@ -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() diff --git a/app/assets/javascripts/dunlop-semantic/ui-checkbox.coffee b/app/assets/javascripts/dunlop-semantic/ui-checkbox.coffee new file mode 100644 index 0000000..f74ff73 --- /dev/null +++ b/app/assets/javascripts/dunlop-semantic/ui-checkbox.coffee @@ -0,0 +1,2 @@ +Dunlop.register_setup 'ui-checkbox', (target) -> + target.find('.ui.checkbox').checkbox() diff --git a/app/assets/javascripts/dunlop-semantic/ui-dropdown.coffee b/app/assets/javascripts/dunlop-semantic/ui-dropdown.coffee new file mode 100644 index 0000000..21aa996 --- /dev/null +++ b/app/assets/javascripts/dunlop-semantic/ui-dropdown.coffee @@ -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 = $('').click -> + dropdown.dropdown('clear') + $(@).remove() + el.after clear_icon + el.dropdown settings diff --git a/app/assets/javascripts/dunlop.coffee b/app/assets/javascripts/dunlop.coffee new file mode 100644 index 0000000..3a471e2 --- /dev/null +++ b/app/assets/javascripts/dunlop.coffee @@ -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' + diff --git a/app/assets/javascripts/dunlop/active_support/human_size.coffee b/app/assets/javascripts/dunlop/active_support/human_size.coffee new file mode 100644 index 0000000..860c6ff --- /dev/null +++ b/app/assets/javascripts/dunlop/active_support/human_size.coffee @@ -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')) diff --git a/app/assets/javascripts/dunlop/base.coffee b/app/assets/javascripts/dunlop/base.coffee new file mode 100644 index 0000000..746b645 --- /dev/null +++ b/app/assets/javascripts/dunlop/base.coffee @@ -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() diff --git a/app/assets/javascripts/dunlop/categorize-as-button.coffee b/app/assets/javascripts/dunlop/categorize-as-button.coffee new file mode 100644 index 0000000..c4a25bc --- /dev/null +++ b/app/assets/javascripts/dunlop/categorize-as-button.coffee @@ -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 diff --git a/app/assets/javascripts/dunlop/collapsible.coffee b/app/assets/javascripts/dunlop/collapsible.coffee new file mode 100644 index 0000000..79c707d --- /dev/null +++ b/app/assets/javascripts/dunlop/collapsible.coffee @@ -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') diff --git a/app/assets/javascripts/dunlop/collection-button.coffee b/app/assets/javascripts/dunlop/collection-button.coffee new file mode 100644 index 0000000..26570f3 --- /dev/null +++ b/app/assets/javascripts/dunlop/collection-button.coffee @@ -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 + diff --git a/app/assets/javascripts/dunlop/display-badges.coffee b/app/assets/javascripts/dunlop/display-badges.coffee new file mode 100644 index 0000000..5b336b7 --- /dev/null +++ b/app/assets/javascripts/dunlop/display-badges.coffee @@ -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 diff --git a/app/assets/javascripts/dunlop/helpers.coffee b/app/assets/javascripts/dunlop/helpers.coffee new file mode 100644 index 0000000..89defc3 --- /dev/null +++ b/app/assets/javascripts/dunlop/helpers.coffee @@ -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 diff --git a/app/assets/javascripts/dunlop/time-display.coffee b/app/assets/javascripts/dunlop/time-display.coffee new file mode 100644 index 0000000..a52aff3 --- /dev/null +++ b/app/assets/javascripts/dunlop/time-display.coffee @@ -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) + diff --git a/app/assets/javascripts/dunlop/translations.coffee b/app/assets/javascripts/dunlop/translations.coffee new file mode 100644 index 0000000..46f4d91 --- /dev/null +++ b/app/assets/javascripts/dunlop/translations.coffee @@ -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 + diff --git a/app/assets/stylesheets/dunlop-semantic-ui.sass b/app/assets/stylesheets/dunlop-semantic-ui.sass new file mode 100644 index 0000000..1a7a3a1 --- /dev/null +++ b/app/assets/stylesheets/dunlop-semantic-ui.sass @@ -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 diff --git a/app/assets/stylesheets/dunlop-semantic/calendar.css b/app/assets/stylesheets/dunlop-semantic/calendar.css new file mode 100644 index 0000000..623685d --- /dev/null +++ b/app/assets/stylesheets/dunlop-semantic/calendar.css @@ -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 +*******************************/ + diff --git a/app/assets/stylesheets/dunlop-semantic/ui-calendar-additions.sass b/app/assets/stylesheets/dunlop-semantic/ui-calendar-additions.sass new file mode 100644 index 0000000..38e6077 --- /dev/null +++ b/app/assets/stylesheets/dunlop-semantic/ui-calendar-additions.sass @@ -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) + + diff --git a/app/assets/stylesheets/dunlop/1st_load_framework.sass b/app/assets/stylesheets/dunlop/1st_load_framework.sass new file mode 100644 index 0000000..ff0fe0f --- /dev/null +++ b/app/assets/stylesheets/dunlop/1st_load_framework.sass @@ -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 diff --git a/app/assets/stylesheets/dunlop/all.sass b/app/assets/stylesheets/dunlop/all.sass new file mode 100644 index 0000000..db49e01 --- /dev/null +++ b/app/assets/stylesheets/dunlop/all.sass @@ -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/* diff --git a/app/assets/stylesheets/dunlop/components/_archived_mode.sass b/app/assets/stylesheets/dunlop/components/_archived_mode.sass new file mode 100644 index 0000000..a4dd376 --- /dev/null +++ b/app/assets/stylesheets/dunlop/components/_archived_mode.sass @@ -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 diff --git a/app/assets/stylesheets/dunlop/components/_badges.sass b/app/assets/stylesheets/dunlop/components/_badges.sass new file mode 100644 index 0000000..c26a4be --- /dev/null +++ b/app/assets/stylesheets/dunlop/components/_badges.sass @@ -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 diff --git a/app/assets/stylesheets/dunlop/components/_boolean_show.sass b/app/assets/stylesheets/dunlop/components/_boolean_show.sass new file mode 100644 index 0000000..aa44a5f --- /dev/null +++ b/app/assets/stylesheets/dunlop/components/_boolean_show.sass @@ -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 diff --git a/app/assets/stylesheets/dunlop/components/_collapsible.sass b/app/assets/stylesheets/dunlop/components/_collapsible.sass new file mode 100644 index 0000000..144e4b9 --- /dev/null +++ b/app/assets/stylesheets/dunlop/components/_collapsible.sass @@ -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 diff --git a/app/assets/stylesheets/dunlop/components/_collections.sass b/app/assets/stylesheets/dunlop/components/_collections.sass new file mode 100644 index 0000000..e69de29 diff --git a/app/assets/stylesheets/dunlop/components/_datepicker.sass b/app/assets/stylesheets/dunlop/components/_datepicker.sass new file mode 100644 index 0000000..3d8291a --- /dev/null +++ b/app/assets/stylesheets/dunlop/components/_datepicker.sass @@ -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 diff --git a/app/assets/stylesheets/dunlop/components/_display_fields.sass b/app/assets/stylesheets/dunlop/components/_display_fields.sass new file mode 100644 index 0000000..b30421a --- /dev/null +++ b/app/assets/stylesheets/dunlop/components/_display_fields.sass @@ -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 diff --git a/app/assets/stylesheets/dunlop/components/_environment-ribbon.sass b/app/assets/stylesheets/dunlop/components/_environment-ribbon.sass new file mode 100644 index 0000000..2bcae00 --- /dev/null +++ b/app/assets/stylesheets/dunlop/components/_environment-ribbon.sass @@ -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) diff --git a/app/assets/stylesheets/dunlop/components/_forms.sass b/app/assets/stylesheets/dunlop/components/_forms.sass new file mode 100644 index 0000000..2b8301a --- /dev/null +++ b/app/assets/stylesheets/dunlop/components/_forms.sass @@ -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 diff --git a/app/assets/stylesheets/dunlop/components/_icon_buttons.sass b/app/assets/stylesheets/dunlop/components/_icon_buttons.sass new file mode 100644 index 0000000..383c564 --- /dev/null +++ b/app/assets/stylesheets/dunlop/components/_icon_buttons.sass @@ -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 diff --git a/app/assets/stylesheets/dunlop/components/_log_entries.sass b/app/assets/stylesheets/dunlop/components/_log_entries.sass new file mode 100644 index 0000000..3422dd6 --- /dev/null +++ b/app/assets/stylesheets/dunlop/components/_log_entries.sass @@ -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) diff --git a/app/assets/stylesheets/dunlop/components/_optional_input.sass b/app/assets/stylesheets/dunlop/components/_optional_input.sass new file mode 100644 index 0000000..8408210 --- /dev/null +++ b/app/assets/stylesheets/dunlop/components/_optional_input.sass @@ -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 diff --git a/app/assets/stylesheets/dunlop/components/_page_actions.sass b/app/assets/stylesheets/dunlop/components/_page_actions.sass new file mode 100644 index 0000000..e2f5302 --- /dev/null +++ b/app/assets/stylesheets/dunlop/components/_page_actions.sass @@ -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 diff --git a/app/assets/stylesheets/dunlop/components/_role-icons.sass b/app/assets/stylesheets/dunlop/components/_role-icons.sass new file mode 100644 index 0000000..e899aaa --- /dev/null +++ b/app/assets/stylesheets/dunlop/components/_role-icons.sass @@ -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 diff --git a/app/assets/stylesheets/dunlop/components/_select2-overrides.sass b/app/assets/stylesheets/dunlop/components/_select2-overrides.sass new file mode 100644 index 0000000..3790f59 --- /dev/null +++ b/app/assets/stylesheets/dunlop/components/_select2-overrides.sass @@ -0,0 +1,5 @@ +// Based on CU request from Kitty +.select2-container + .select2-results__option + padding: 0 3px + diff --git a/app/assets/stylesheets/dunlop/components/_statistics.sass b/app/assets/stylesheets/dunlop/components/_statistics.sass new file mode 100644 index 0000000..640101e --- /dev/null +++ b/app/assets/stylesheets/dunlop/components/_statistics.sass @@ -0,0 +1,4 @@ +.statistics-title + font-size: 1.3em + font-weight: bold + border-bottom: 1px solid #999 diff --git a/app/assets/stylesheets/dunlop/components/_structure.sass b/app/assets/stylesheets/dunlop/components/_structure.sass new file mode 100644 index 0000000..0d2e2e0 --- /dev/null +++ b/app/assets/stylesheets/dunlop/components/_structure.sass @@ -0,0 +1,3 @@ +.clearfix + +clearfix + clear: both diff --git a/app/assets/stylesheets/dunlop/components/_subprocess_modal.sass b/app/assets/stylesheets/dunlop/components/_subprocess_modal.sass new file mode 100644 index 0000000..12f7d2e --- /dev/null +++ b/app/assets/stylesheets/dunlop/components/_subprocess_modal.sass @@ -0,0 +1,6 @@ +.subprocess-modal-button + +button + &.mark-complete + +button($bg: $success-color) + &.mark-reject + +button($bg: $alert-color) diff --git a/app/assets/stylesheets/dunlop/components/_tables.sass b/app/assets/stylesheets/dunlop/components/_tables.sass new file mode 100644 index 0000000..0ee5b66 --- /dev/null +++ b/app/assets/stylesheets/dunlop/components/_tables.sass @@ -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 diff --git a/app/assets/stylesheets/dunlop/components/_title-box.sass b/app/assets/stylesheets/dunlop/components/_title-box.sass new file mode 100644 index 0000000..872a331 --- /dev/null +++ b/app/assets/stylesheets/dunlop/components/_title-box.sass @@ -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 diff --git a/app/assets/stylesheets/dunlop/components/_user_management.sass b/app/assets/stylesheets/dunlop/components/_user_management.sass new file mode 100644 index 0000000..7f777c4 --- /dev/null +++ b/app/assets/stylesheets/dunlop/components/_user_management.sass @@ -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 + diff --git a/app/assets/stylesheets/dunlop/mixin_parts/_environment_ribbon.sass b/app/assets/stylesheets/dunlop/mixin_parts/_environment_ribbon.sass new file mode 100644 index 0000000..1f3ca09 --- /dev/null +++ b/app/assets/stylesheets/dunlop/mixin_parts/_environment_ribbon.sass @@ -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 + diff --git a/app/assets/stylesheets/dunlop/mixins.sass b/app/assets/stylesheets/dunlop/mixins.sass new file mode 100644 index 0000000..edbb68b --- /dev/null +++ b/app/assets/stylesheets/dunlop/mixins.sass @@ -0,0 +1 @@ +@import ./mixin_parts/* diff --git a/app/assets/stylesheets/dunlop/resources/_manual.sass b/app/assets/stylesheets/dunlop/resources/_manual.sass new file mode 100644 index 0000000..621319d --- /dev/null +++ b/app/assets/stylesheets/dunlop/resources/_manual.sass @@ -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 diff --git a/app/assets/stylesheets/dunlop/resources/_workflow_instance_batches.sass b/app/assets/stylesheets/dunlop/resources/_workflow_instance_batches.sass new file mode 100644 index 0000000..e1fe73d --- /dev/null +++ b/app/assets/stylesheets/dunlop/resources/_workflow_instance_batches.sass @@ -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 diff --git a/app/assets/stylesheets/dunlop/resources/_workflow_instances.sass b/app/assets/stylesheets/dunlop/resources/_workflow_instances.sass new file mode 100644 index 0000000..860ce2d --- /dev/null +++ b/app/assets/stylesheets/dunlop/resources/_workflow_instances.sass @@ -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 + diff --git a/app/assets/stylesheets/dunlop/resources/_workflow_steps.sass b/app/assets/stylesheets/dunlop/resources/_workflow_steps.sass new file mode 100644 index 0000000..89d5e6f --- /dev/null +++ b/app/assets/stylesheets/dunlop/resources/_workflow_steps.sass @@ -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 diff --git a/app/assets/stylesheets/dunlop/semantic.sass b/app/assets/stylesheets/dunlop/semantic.sass new file mode 100644 index 0000000..98500df --- /dev/null +++ b/app/assets/stylesheets/dunlop/semantic.sass @@ -0,0 +1,7 @@ +//= require pickadate/default +//= require pickadate/default.date +//= require pickadate/default.time +//= require record_collection/all + +$export-color: #626 +@import ./mixins diff --git a/app/controllers/dunlop/application_controller.rb b/app/controllers/dunlop/application_controller.rb new file mode 100644 index 0000000..806f457 --- /dev/null +++ b/app/controllers/dunlop/application_controller.rb @@ -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 diff --git a/app/controllers/dunlop/dashboard_controller.rb b/app/controllers/dunlop/dashboard_controller.rb new file mode 100644 index 0000000..359f38f --- /dev/null +++ b/app/controllers/dunlop/dashboard_controller.rb @@ -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!(/(?#{html}" + 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 diff --git a/app/controllers/dunlop/execution_batches_controller.rb b/app/controllers/dunlop/execution_batches_controller.rb new file mode 100644 index 0000000..1736ae1 --- /dev/null +++ b/app/controllers/dunlop/execution_batches_controller.rb @@ -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 diff --git a/app/controllers/dunlop/reports.rb b/app/controllers/dunlop/reports.rb new file mode 100644 index 0000000..b0e6ad1 --- /dev/null +++ b/app/controllers/dunlop/reports.rb @@ -0,0 +1,3 @@ +module Dunlop::Reports + +end diff --git a/app/controllers/dunlop/reports/system_controller.rb b/app/controllers/dunlop/reports/system_controller.rb new file mode 100644 index 0000000..7f9bb3a --- /dev/null +++ b/app/controllers/dunlop/reports/system_controller.rb @@ -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 diff --git a/app/controllers/dunlop/source_files_controller.rb b/app/controllers/dunlop/source_files_controller.rb new file mode 100644 index 0000000..fcea42d --- /dev/null +++ b/app/controllers/dunlop/source_files_controller.rb @@ -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 diff --git a/app/controllers/dunlop/target_files_controller.rb b/app/controllers/dunlop/target_files_controller.rb new file mode 100644 index 0000000..82aae61 --- /dev/null +++ b/app/controllers/dunlop/target_files_controller.rb @@ -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 diff --git a/app/controllers/dunlop/users_controller.rb b/app/controllers/dunlop/users_controller.rb new file mode 100644 index 0000000..374995a --- /dev/null +++ b/app/controllers/dunlop/users_controller.rb @@ -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 + diff --git a/app/controllers/dunlop/workflow_instances_controller.rb b/app/controllers/dunlop/workflow_instances_controller.rb new file mode 100644 index 0000000..357f088 --- /dev/null +++ b/app/controllers/dunlop/workflow_instances_controller.rb @@ -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 diff --git a/app/helpers/dunlop/active_class_helper.rb b/app/helpers/dunlop/active_class_helper.rb new file mode 100644 index 0000000..15f73e7 --- /dev/null +++ b/app/helpers/dunlop/active_class_helper.rb @@ -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 diff --git a/app/helpers/dunlop/application_helper.rb b/app/helpers/dunlop/application_helper.rb new file mode 100644 index 0000000..5b92cf9 --- /dev/null +++ b/app/helpers/dunlop/application_helper.rb @@ -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, "#{title}".html_safe, class: 'collapsible-title') + content_tag(:div, title_tag + content_tag(:div, content, class: 'collapsible-content'), options.merge(class: classes)) + end + + # Need a custom one, since rails still does not support non present dates + def dunlop_localize(date, options = {}) + return '' unless date.present? + l(date, options) + end + + # Return a page relative time tag. The current time is meant to be replaced by relative time from + # the moment javascript library + def page_time(time = Time.current) + content_tag(:span, time, id: 'page-time', data: {time: time.utc.iso8601, format: 'relative'}) + end + + # Tested through the migration decorator + def plan_date(options = {}) + return '' unless options[:date] or options[:window_from] or options[:window_to] + date = options[:date].try(:strftime, '%d-%m-%Y') + window = '' + if options[:window_from].present? or options[:window_to].present? + window_from_display = options[:window_from].present? ? "#{options[:window_from]}:00 " : '' + window_to_display = options[:window_to].present? ? " #{options[:window_to]}:00" : '' + window = " #{content_tag(:span, '', class: 'fa fa-clock-o')}  #{window_from_display}-#{window_to_display}" + end + "#{date} #{window}".strip.html_safe + end + + def plan_date_for_record(record) + return '' unless record.present? + plan_date( + date: record.plan_date, + window_from: record.window_from, + window_to: record.window_to + ) + end + + def empty_collection(model) + content_tag(:div, t('collection.empty', models: model.model_name.human_plural), class: 'empty-collection') + end + + def workflow_step_buttons_for(workflow_instance_class) + #buttons = [] + #record_class.workflow_step_classes.each do |workflow_step| + # next unless can?(:manage, workflow_step) + # base_name = workflow_step.name.underscore.split('/').first + # buttons << content_tag(:button, t('perform_batch_actions_button', scope: base_name), class: "#{base_name.pluralize}-actions", data: {action: 'workflow_step', workflow_step: base_name}) + #end + #buttons.join.html_safe + workflow_instance_class.workflow_step_classes.map{|workflow_step| workflow_step_button_for workflow_step }.compact.join.html_safe + end + + def workflow_step_button_for(workflow_step, data_attributes = {}) + return unless can?(:manage, workflow_step) + base_name = workflow_step.name.underscore.split('/').first + data_attributes[:action] ||= 'collection_edit' + data_attributes[:resource] ||= workflow_step.name.underscore + data_attributes[:request_params] = data_attributes[:request_params].to_json if data_attributes[:request_params].is_a?(Hash) + data_attributes[:request_params] ||= {workflow_instance_ids: 'selected_ids'}.to_json + text = t('workflow_step.update_button_text', models: workflow_step.model_name.human_plural) + content = content_tag(:span, text, class: 'text') + content_tag(:span, '', class: 'icon') + content_tag(:button, content, class: "#{base_name.pluralize}-actions workflow-step-actions collection-edit", data: data_attributes) + end + + + def human_duration(duration) + return duration.inspect unless duration.is_a?(ActiveSupport::Duration) + + # taken and modified from ActiveSupport::Duration#inspect version 4.2.6 + duration.parts. + reduce(::Hash.new(0)) { |h,(l,r)| h[l] += r; h }. + sort_by {|unit, _ | [:years, :months, :days, :minutes, :seconds].index(unit)}. + map {|unit, val| "#{val} #{val == 1 ? t(unit, scope: 'dunlop.duration', default: unit.to_s.chop) : t(unit, scope: 'dunlop.duration.plural', default: unit.to_s)}"}. + to_sentence(locale: ::I18n.locale) + end + + + def render_as_dunlop_modal(partial_name, options={}) + render "dunlop/workflow_step/modal_renderer", partial_name: partial_name, options: options + end + + def dunlop_class_ability(permission, klass, options={}) + user = options[:user] || @user + check_box_tag "user[role_names][]", "#{permission}-class-#{klass.name}", user.role_names.include?("#{permission}-class-#{klass.name}"), id: "role-#{klass.name.underscore.parameterize}" + end + + def dunlop_ability(permission, type, target, options={}) + user = options[:user] || @user + authorization = [permission, type, target].join('-') + check_box_tag "user[role_names][]", authorization, user.role_names.include?(authorization), id: "role-#{authorization}" + end + + def tarray(array, options={}) + array.map do |entry| + if entry.respond_to?(:model_name) + if can?(:read, entry) + [entry.model_name.human, entry] + end + else + [entry, entry] + end + end.compact + end + + def link_to_model(model) + return nil unless model.present? + return nil unless can?(:read, model) + class_base = model.class.name.deconstantize + name = model.try(:presentation_name) || model.try(:model_name).try(:human) || model.class.name + if %w[SourceFile ExecutionBatch].include?(class_base) + path = dunlop.polymorphic_path(model.becomes(class_base.constantize)) rescue nil + else + path = main_app.polymorphic_path(model) rescue nil + #TODO: handle becomes? + end + return model.try(:presentation_name) unless path.present? + link_to name, path + end + end +end diff --git a/app/helpers/dunlop/authorization_helper.rb b/app/helpers/dunlop/authorization_helper.rb new file mode 100644 index 0000000..a817130 --- /dev/null +++ b/app/helpers/dunlop/authorization_helper.rb @@ -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 diff --git a/app/helpers/dunlop/interpreted_text_helper.rb b/app/helpers/dunlop/interpreted_text_helper.rb new file mode 100644 index 0000000..2bdf9bf --- /dev/null +++ b/app/helpers/dunlop/interpreted_text_helper.rb @@ -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} =>
#{image_tag("manual/image1.png")}
+ # $i{image2|200px} =>
#{image_tag("manual/image2.png")}
+ 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 = "
#{image_html}
" + 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 diff --git a/app/helpers/dunlop/last_attribute_change_helper.rb b/app/helpers/dunlop/last_attribute_change_helper.rb new file mode 100644 index 0000000..1b7db69 --- /dev/null +++ b/app/helpers/dunlop/last_attribute_change_helper.rb @@ -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 diff --git a/app/helpers/dunlop/state_badge_helper.rb b/app/helpers/dunlop/state_badge_helper.rb new file mode 100644 index 0000000..29a4aa6 --- /dev/null +++ b/app/helpers/dunlop/state_badge_helper.rb @@ -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 diff --git a/app/helpers/dunlop/table_helper.rb b/app/helpers/dunlop/table_helper.rb new file mode 100644 index 0000000..e28100c --- /dev/null +++ b/app/helpers/dunlop/table_helper.rb @@ -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 diff --git a/app/models/concerns/has_uuid_identifier.rb b/app/models/concerns/has_uuid_identifier.rb new file mode 100644 index 0000000..2ad5091 --- /dev/null +++ b/app/models/concerns/has_uuid_identifier.rb @@ -0,0 +1,25 @@ +module HasUuidIdentifier + UUID_MATCH = /^[0-9A-F]{8}-[0-9A-F]{4}-(?[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 diff --git a/app/models/dunlop/application_record_additions.rb b/app/models/dunlop/application_record_additions.rb new file mode 100644 index 0000000..9316818 --- /dev/null +++ b/app/models/dunlop/application_record_additions.rb @@ -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 diff --git a/app/models/dunlop/application_record_additions/epoch_weeks.rb b/app/models/dunlop/application_record_additions/epoch_weeks.rb new file mode 100644 index 0000000..53ce91d --- /dev/null +++ b/app/models/dunlop/application_record_additions/epoch_weeks.rb @@ -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 diff --git a/app/models/dunlop/attribute_changes_tracking.rb b/app/models/dunlop/attribute_changes_tracking.rb new file mode 100644 index 0000000..faa7f3c --- /dev/null +++ b/app/models/dunlop/attribute_changes_tracking.rb @@ -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] diff --git a/app/models/dunlop/collection_lookup.rb b/app/models/dunlop/collection_lookup.rb new file mode 100644 index 0000000..9f74f4e --- /dev/null +++ b/app/models/dunlop/collection_lookup.rb @@ -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 diff --git a/app/models/dunlop/execution_batch_model.rb b/app/models/dunlop/execution_batch_model.rb new file mode 100644 index 0000000..e4cba8d --- /dev/null +++ b/app/models/dunlop/execution_batch_model.rb @@ -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 diff --git a/app/models/dunlop/file_download.rb b/app/models/dunlop/file_download.rb new file mode 100644 index 0000000..a1327ad --- /dev/null +++ b/app/models/dunlop/file_download.rb @@ -0,0 +1,6 @@ +module Dunlop + class FileDownload < ApplicationRecord + belongs_to :user + belongs_to :file, polymorphic: true + end +end diff --git a/app/models/dunlop/last_attribute_change.rb b/app/models/dunlop/last_attribute_change.rb new file mode 100644 index 0000000..5554962 --- /dev/null +++ b/app/models/dunlop/last_attribute_change.rb @@ -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 diff --git a/app/models/dunlop/log_entry.rb b/app/models/dunlop/log_entry.rb new file mode 100644 index 0000000..b4abb90 --- /dev/null +++ b/app/models/dunlop/log_entry.rb @@ -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 + diff --git a/app/models/dunlop/process_identification.rb b/app/models/dunlop/process_identification.rb new file mode 100644 index 0000000..7dce4ee --- /dev/null +++ b/app/models/dunlop/process_identification.rb @@ -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 diff --git a/app/models/dunlop/source_file_model.rb b/app/models/dunlop/source_file_model.rb new file mode 100644 index 0000000..3f6406e --- /dev/null +++ b/app/models/dunlop/source_file_model.rb @@ -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 diff --git a/app/models/dunlop/source_file_model/conversion_helpers.rb b/app/models/dunlop/source_file_model/conversion_helpers.rb new file mode 100644 index 0000000..f0a4f44 --- /dev/null +++ b/app/models/dunlop/source_file_model/conversion_helpers.rb @@ -0,0 +1,11 @@ +module Dunlop::SourceFileModel::ConversionHelpers + extend ActiveSupport::Concern + + def true_or_false(string) + !!(string.to_s =~ /T|J|Y|1|(? :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 diff --git a/app/models/dunlop/target_file_uploader.rb b/app/models/dunlop/target_file_uploader.rb new file mode 100644 index 0000000..e6fcd17 --- /dev/null +++ b/app/models/dunlop/target_file_uploader.rb @@ -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 diff --git a/app/models/dunlop/user_model.rb b/app/models/dunlop/user_model.rb new file mode 100644 index 0000000..accab5f --- /dev/null +++ b/app/models/dunlop/user_model.rb @@ -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 diff --git a/app/models/dunlop/workflow_instance_batch_model.rb b/app/models/dunlop/workflow_instance_batch_model.rb new file mode 100644 index 0000000..deb14e7 --- /dev/null +++ b/app/models/dunlop/workflow_instance_batch_model.rb @@ -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 diff --git a/app/models/dunlop/workflow_instance_collection.rb b/app/models/dunlop/workflow_instance_collection.rb new file mode 100644 index 0000000..9aa1597 --- /dev/null +++ b/app/models/dunlop/workflow_instance_collection.rb @@ -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 diff --git a/app/models/dunlop/workflow_instance_model.rb b/app/models/dunlop/workflow_instance_model.rb new file mode 100644 index 0000000..2e4cffd --- /dev/null +++ b/app/models/dunlop/workflow_instance_model.rb @@ -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 diff --git a/app/models/dunlop/workflow_instance_model/archived_handling.rb b/app/models/dunlop/workflow_instance_model/archived_handling.rb new file mode 100644 index 0000000..83d9400 --- /dev/null +++ b/app/models/dunlop/workflow_instance_model/archived_handling.rb @@ -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 diff --git a/app/models/dunlop/workflow_instance_model/categorization_and_reset_handling.rb b/app/models/dunlop/workflow_instance_model/categorization_and_reset_handling.rb new file mode 100644 index 0000000..bbc5d14 --- /dev/null +++ b/app/models/dunlop/workflow_instance_model/categorization_and_reset_handling.rb @@ -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 diff --git a/app/models/dunlop/workflow_instance_model/state_machine_handling.rb b/app/models/dunlop/workflow_instance_model/state_machine_handling.rb new file mode 100644 index 0000000..97094b3 --- /dev/null +++ b/app/models/dunlop/workflow_instance_model/state_machine_handling.rb @@ -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 diff --git a/app/models/dunlop/workflow_instance_model/workflow_step_handling.rb b/app/models/dunlop/workflow_instance_model/workflow_step_handling.rb new file mode 100644 index 0000000..d48a6bc --- /dev/null +++ b/app/models/dunlop/workflow_instance_model/workflow_step_handling.rb @@ -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 diff --git a/app/models/dunlop/workflow_step/generic_collection.rb b/app/models/dunlop/workflow_step/generic_collection.rb new file mode 100644 index 0000000..40757fc --- /dev/null +++ b/app/models/dunlop/workflow_step/generic_collection.rb @@ -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 diff --git a/app/models/dunlop/workflow_step_model.rb b/app/models/dunlop/workflow_step_model.rb new file mode 100644 index 0000000..f3837a3 --- /dev/null +++ b/app/models/dunlop/workflow_step_model.rb @@ -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 diff --git a/app/models/dunlop/workflow_step_model/notes_handling.rb b/app/models/dunlop/workflow_step_model/notes_handling.rb new file mode 100644 index 0000000..f8c9eef --- /dev/null +++ b/app/models/dunlop/workflow_step_model/notes_handling.rb @@ -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 + diff --git a/app/models/dunlop/workflow_step_model/state_machine.rb b/app/models/dunlop/workflow_step_model/state_machine.rb new file mode 100644 index 0000000..e481625 --- /dev/null +++ b/app/models/dunlop/workflow_step_model/state_machine.rb @@ -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 diff --git a/app/services/day_time_base.rb b/app/services/day_time_base.rb new file mode 100644 index 0000000..2ef481d --- /dev/null +++ b/app/services/day_time_base.rb @@ -0,0 +1,59 @@ +module DayTimeBase + extend ActiveSupport::Concern + attr_reader :number + delegate :blank?, :to_i, :to_f, to: :number + + def initialize(number_or_time) + if number_or_time.present? + @number = number_or_time.is_a?(String) ? self.class.string_to_number(number_or_time) : number_or_time + else + @number = nil + end + end + + def inspect + to_s + end + + def coerce(value) + [value, number] + end + + # ActiveRecord does a check on the existenc of the method quoted_id to quote for sql. Since this is an integer representation, + # the number is used directly. + # active_record/connection_adapters/abstract/quoting.rb|8 col 7| def quote(value, column = nil) + def quoted_id + number || 'NULL' + end + + [:==, :>, :<, :!=, :>=, :<=, :equal?, :<=>].each do |compare_method| + define_method compare_method do |compare_value| + compare_operation compare_method, compare_value + end + end + + def compare_operation(operation, number_or_time) + case number_or_time + when String + number.public_send(operation, self.class.string_to_number(number_or_time)) + when DayTimeBase + seconds.public_send(operation, number_or_time.seconds) + else + number.public_send(operation, number_or_time) + end + end + + module ClassMethods + def load(value) + new(value) + end + + def dump(number_or_time) + case number_or_time + when DayTimeBase then number_or_time.number + when String then string_to_number(number_or_time) + else number_or_time + end + end + end +end diff --git a/app/services/day_time_minutes.rb b/app/services/day_time_minutes.rb new file mode 100644 index 0000000..4587692 --- /dev/null +++ b/app/services/day_time_minutes.rb @@ -0,0 +1,30 @@ +# Helper class for time of day storage in database as integer value +class DayTimeMinutes + include DayTimeBase + + def to_s + return '' unless number + hours, minutes_part = number.divmod(60) + [hours, minutes_part.round].map{|v| v.to_s.rjust(2, '0')}.join(':') + end + + def seconds + number.to_i * 60 + end + + def minutes + number.to_i + end + + def hours + number.to_f / 60.0 + end + + class << self + def string_to_number(string) + return nil unless string.present? + hours, minutes = string.split(':') + hours.to_i * 60 + minutes.to_i + end + end +end diff --git a/app/services/day_time_seconds.rb b/app/services/day_time_seconds.rb new file mode 100644 index 0000000..4103767 --- /dev/null +++ b/app/services/day_time_seconds.rb @@ -0,0 +1,31 @@ +# Helper class for time of day storage in database as integer value +class DayTimeSeconds + include DayTimeBase + + def to_s + return '' unless number + hours, minutes_part = number.divmod(3600) + minutes, seconds_part = minutes_part.divmod(60) + [hours, minutes, seconds_part.round].map{|v| v.to_s.rjust(2, '0')}.join(':') + end + + def seconds + number.to_i + end + + def minutes + number.to_f / 60.0 + end + + def hours + number.to_f / 3600.0 + end + + class << self + def string_to_number(string) + return nil unless string.present? + hours, minutes, seconds = string.split(':') + hours.to_i * 3600 + minutes.to_i * 60 + seconds.to_i + end + end +end diff --git a/app/services/dunlop/ability.rb b/app/services/dunlop/ability.rb new file mode 100644 index 0000000..fc3a726 --- /dev/null +++ b/app/services/dunlop/ability.rb @@ -0,0 +1,166 @@ +module Dunlop::Ability + extend ActiveSupport::Concern + include CanCan::Ability + + included do + attr_reader :user + @allowed_authorization_classes ||= [] + @subject_aliases = {} + end + + def self.has_permission_of_type(type, user) + return true if user.admin? + user.role_names.any?{|role| role =~ /^\w+-#{type}-/} + end + + def self.any_permission_starts_with(expression, user) + return true if user.admin? + expression = "(#{expression.join('|')})" if expression.is_a?(Array) + user.role_names.any?{|role| role =~ /^\w+-\w+-#{expression}/ } + end + + def initialize(user) + return unless user.present? + @user = user + setup_abilities + end + + def has_permission_of_type(type) + Dunlop::Ability.has_permission_of_type(type, user) + end + + def any_permission_starts_with(expression) + Dunlop::Ability.any_permission_starts_with(expression, user) + end + + def setup_dunlop_abilities + if user.admin? + can :manage, :all + # Extra permissions, not assigned + # can :backtrace, Dunlop::LogEntry + # can :report, :index + # can :report, :system + else + setup_dunlop_class_based_abilities + setup_subject_aliases + can :do, :shit if rules.empty? # cancancan 1.15.0 bug, cannot handle empty rules + end + end + + def user_roles + @user_roles ||= all_roles.select(&:user_role?) + end + + def admin_roles + @admin_roles ||= all_roles.select(&:admin_role?) + end + + def all_roles + return @all_roles if @all_roles + roles = [] + user.role_names.each do |role_name| + roles.push get_role_for_role_name(role_name) + end + user.role_names_admin.each do |role_name| + roles.push get_role_for_role_name(role_name, role: Dunlop::Ability::AdminRole, verify_class: false) + end + @all_roles = roles.compact + end + + def get_role_for_role_name(role_name, role: Dunlop::Ability::UserRole, verify_class: true) + return unless role_name =~ /\w+-\w+-[\w:\/-]+/ + ability, type, class_name = role_name.split('-', 3) + case type + when 'app', 'adapter' + type = 'class' + class_name = "#{class_name.camelize}::Engine" + when 'model' + type = 'class' + class_name = class_name.gsub('-', '_').camelize + end + return if verify_class and not self.class.allowed_authorization_classes.include?(class_name) + role.new(ability, type, class_name) + end + + def all_class_roles + @all_class_roles ||= all_roles.select(&:class_role?) + end + + def setup_dunlop_class_based_abilities + all_class_roles.each do |role| + can role.ability, role.target_class + end + + # One usecase is to build a topmenu having the right menu structure based on the permissions. If there + # is a permission on class A::B::C::D then there will be no permission on the A class, but I still have + # to show the A toplevel menu, otherwise the user never arrives at the A::B::C::D class to perform it's + # action through the UI. The current solution is to set an :index ability on the top level class A, so + # in the _navigation_links.html.slim you can use + # can? :index, A + # or even: + # can? :index, A::B + # since a top level ability gives it to all subclasses (CanCanCan) + all_class_roles.map{|role| role.class_name.split('::').first }.uniq.each do |top_level_class_name| + can :index, top_level_class_name.constantize + end + end + + # This method should be implemented in the application app/models/ability.rb to implement custom authorization logic + def setup_abilities + setup_dunlop_abilities + end + + def alternative_subjects(subject) + result = super + if subject.is_a?(Class) and subject_alias = self.class.subject_aliases.find{ |alias_from, alias_target| subject.ancestors.include?(alias_from) }.try(:last) # target + result << subject_alias unless result.include?(subject_alias) + end + result + end + + # Add the aliases to the rule's subjects + def setup_subject_aliases + rules.each do |rule| + self.class.subject_aliases.each do |alias_from, alias_target| + rule.subjects << alias_from if rule.subjects.include?(alias_target) and not rule.subjects.include?(alias_from) + end + end + end + + module ClassMethods + + def alias_subject(*subjects) + options = subjects.extract_options! + subject_to = options[:to] or raise "alias_subject has no: ', to: TakePermissionsFrom' statement" + subjects.each do |subject| + subject_aliases[subject] = subject_to # shape of hash is to quick find the subject, subject_to less important + end + end + + # This method is a default implementation and meant to be overridden in de app/models/ability.rb class + # for custom behaviour + def allowed_authorization_classes + @allowed_authorization_classes + end + + def add_allowed_authorization_classes(classes) + class_names = Array.wrap(classes).map{|c| c.is_a?(String) ? c : (c.try(:name) || c.to_s)} + @allowed_authorization_classes |= class_names + end + + def subject_aliases + @subject_aliases + end + + def add_dunlop_allowed_authorization_classes! + result = ['Dunlop::Engine'] + if Dunlop.has_workflow? + result += ['WorkflowInstance', 'WorkflowInstanceBatch'] + result += WorkflowInstance.possible_workflow_step_names.map{|name| name.to_s.classify } + end + result += SourceFile.classes.map(&:name) if Dunlop.has_source_files? + result += TargetFile.classes.map(&:name) if Dunlop.has_target_files? + @allowed_authorization_classes |= result + end + end +end diff --git a/app/services/dunlop/ability/admin_role.rb b/app/services/dunlop/ability/admin_role.rb new file mode 100644 index 0000000..9fa8fa4 --- /dev/null +++ b/app/services/dunlop/ability/admin_role.rb @@ -0,0 +1,5 @@ +class Dunlop::Ability::AdminRole < Dunlop::Ability::BaseRole + def admin_role? + true + end +end diff --git a/app/services/dunlop/ability/base_role.rb b/app/services/dunlop/ability/base_role.rb new file mode 100644 index 0000000..e75448d --- /dev/null +++ b/app/services/dunlop/ability/base_role.rb @@ -0,0 +1,25 @@ +Dunlop::Ability::BaseRole = Struct.new(:ability, :type, :class_name) do + def class_role? + type == 'class' + end + + def app_role? + %w[app adapter].include? type + end + + def target_class + @target_class ||= class_name.constantize + end + + def user_role? + false + end + + def admin_role? + false + end + + def ability + self[:ability].to_sym + end +end diff --git a/app/services/dunlop/ability/user_role.rb b/app/services/dunlop/ability/user_role.rb new file mode 100644 index 0000000..b9bba86 --- /dev/null +++ b/app/services/dunlop/ability/user_role.rb @@ -0,0 +1,5 @@ +class Dunlop::Ability::UserRole < Dunlop::Ability::BaseRole + def user_role? + true + end +end diff --git a/app/services/dunlop/execution_batch_pid.rb b/app/services/dunlop/execution_batch_pid.rb new file mode 100644 index 0000000..30c9c14 --- /dev/null +++ b/app/services/dunlop/execution_batch_pid.rb @@ -0,0 +1,47 @@ +class Dunlop::ExecutionBatchPid + + PIDDIR = Rails.root.join("tmp/pids") + + def initialize(id=nil) + @id = id + end + + def lock + raise "lock already taken" if locked? + begin + claim_lock + yield + ensure + release_lock + end + end + + def locked? + File.exist?(pidfile) + end + + private + + def pidfile + filename = if @id.blank? + "rake_fab_run_batch.pid" + else + "rake_fab_run_batch_#{Shellwords.escape(@id)}.pid" + end + File.join(PIDDIR, filename) + end + + + def claim_lock + FileUtils.mkdir_p(PIDDIR) + File.open( pidfile, "w" ) do |file| + file << Process.pid || 'dummy' + end + return true + end + + def release_lock + FileUtils.rm_f(pidfile) + end + +end diff --git a/app/services/dunlop/file_grep.rb b/app/services/dunlop/file_grep.rb new file mode 100644 index 0000000..3ab7973 --- /dev/null +++ b/app/services/dunlop/file_grep.rb @@ -0,0 +1,35 @@ +module Dunlop::FileGrep + + def self.egrep( input_filename, output_filename, regexp ) + %x[egrep '#{regexp}' #{input_filename} > #{output_filename}] + end + + def self.egrep_v( input_filename, output_filename, regexp ) + %x[egrep -v '#{regexp}' #{input_filename} > #{output_filename}] + end + + def self.select(input_filename, regexp_string) + filter input_filename, regexp_string, filter_method: :egrep + end + + def self.filter(input_filename, regexp_string, filter_method: :egrep) + local_working_file = Tempfile.new("file_grep", WORKING_PATH) + #egrep(input_filename, local_working_file.path, regexp_string) + public_send(filter_method, input_filename, local_working_file.path, regexp_string) + FileUtils.cp(local_working_file.path, input_filename) + FileUtils.chmod(0644, input_filename) + ensure + local_working_file.unlink + end + + def self.reject(input_filename, regexp_string) + filter input_filename, regexp_string, filter_method: :egrep_v + #local_working_file = Tempfile.new("file_grep", WORKING_PATH) + #egrep_v(input_filename, local_working_file.path, regexp_string) + #FileUtils.cp(local_working_file.path, input_filename) + #FileUtils.chmod(0644, input_filename) + #ensure + #local_working_file.unlink + end + +end diff --git a/app/services/dunlop/file_sed.rb b/app/services/dunlop/file_sed.rb new file mode 100644 index 0000000..de70003 --- /dev/null +++ b/app/services/dunlop/file_sed.rb @@ -0,0 +1,23 @@ +module Dunlop::FileSed + + def self.in_place(filename, sed_expressions) + expression_list = Array.wrap(sed_expressions).map { |exp| "-e '#{exp}'" }.join(' ') + if /Darwin/i =~ %x[uname] + #BSD + %x[ LC_CTYPE=C LANG=C gsed -i'' -r #{expression_list} #{filename} ] + raise 'error in sed command line' unless $?.exitstatus == 0 + else + #GNU + %x[ LC_CTYPE=C LANG=C sed -i'' -r #{expression_list} #{filename} ] + raise 'error in sed command line' unless $?.exitstatus == 0 + end + end + + def self.fix_line_endings(filename) + %x[dos2unix -f #{filename} > /dev/null 2>&1] + raise 'error in dos2unix command line' unless $?.exitstatus == 0 + %x[dos2unix -f -c mac #{filename} > /dev/null 2>&1] + raise 'error in dos2unix command line' unless $?.exitstatus == 0 + end + +end diff --git a/app/services/dunlop/file_zip.rb b/app/services/dunlop/file_zip.rb new file mode 100644 index 0000000..194d643 --- /dev/null +++ b/app/services/dunlop/file_zip.rb @@ -0,0 +1,47 @@ +module Dunlop::FileZip + + def self.unzip(zipped_filename, unzipped_filename) + case self.mime_type(zipped_filename) + when "application/zip" + %x[ unzip -p #{zipped_filename} > #{unzipped_filename} ] + when "application/x-gzip" + %x[ gzip -c -d #{zipped_filename} > #{unzipped_filename} ] + when "text/plain" + FileUtils.cp(zipped_filename, unzipped_filename) + else + raise "unsupported mime-type" + end + end + + def self.unzip_to_directory_and_junk_paths(zipped_filename, destination_directory) + case self.mime_type(zipped_filename) + when "application/zip" + %x[ unzip -j #{zipped_filename} -d #{destination_directory} ] + else + raise "unsupported mime-type" + end + end + + def self.mime_type(filename) + command = if /Darwin/i =~ %x[uname] + "file -bI #{filename}" + else + "file -bi #{filename}" + end + + case %x[#{command}] + when /gzip/i + "application/x-gzip" + when /zip/i + "application/zip" + else + "text/plain" + end + end + + def self.gzip(unzipped_filename, gzipped_filename) + return if mime_type(unzipped_filename) == "application/x-gzip" #TODO: maybe the application using is is expecti a copy at the target location in this case? + %x[cat #{unzipped_filename} | gzip -c > #{gzipped_filename}] + end + +end diff --git a/app/services/dunlop/log_entry_creator.rb b/app/services/dunlop/log_entry_creator.rb new file mode 100644 index 0000000..8d9d182 --- /dev/null +++ b/app/services/dunlop/log_entry_creator.rb @@ -0,0 +1,13 @@ +class Dunlop::LogEntryCreator + attr_reader :record + def initialize(record) + @record = record + end + + + [:debug, :info, :warn, :error].each do |severity| + define_method(severity) do |msg| + record.log_entries.create(body: record.logger.formatter.call(severity, Time.current, nil, msg)) + end + end +end diff --git a/app/services/dunlop/loggable.rb b/app/services/dunlop/loggable.rb new file mode 100644 index 0000000..727e6a7 --- /dev/null +++ b/app/services/dunlop/loggable.rb @@ -0,0 +1,59 @@ +module Dunlop::Loggable + extend ActiveSupport::Concern + + included do + has_many :log_entries, as: :loggable, dependent: :destroy, class_name: 'Dunlop::LogEntry' + end + + def with_nested_logger_and_catch_failed(scope=nil, options={}) + scope ||= caller[0][/`.*'/][1..-2] + options[:exception_notifier] = true unless options.has_key?(:exception_notifier) + backtrace = nil + Dunlop::NestedLogger.scope('empty sandbox', benchmark: false) do + result = Dunlop::NestedLogger.scope(scope, options) do + begin + yield + rescue => e + failed! + ExceptionNotifier.notify_exception(e) if options[:exception_notifier] + backtrace = Dunlop::LogEntry.exception_to_backtrace(e) + Dunlop::NestedLogger.error(e.message) + end + end + log_yaml = Dunlop::NestedLogger.flush_as_yaml + log_entries.create(body: log_yaml, backtrace: backtrace) if log_yaml.present? + result # return block result + end + end + + def with_nested_logger(scope=nil, options={}) + scope ||= caller[0][/`.*'/][1..-2] + e = nil + backtrace = nil + Dunlop::NestedLogger.scope('empty sandbox', benchmark: false) do + Dunlop::NestedLogger.scope(scope, options) do + begin + yield + rescue => e + backtrace = Dunlop::LogEntry.exception_to_backtrace(e) + Dunlop::NestedLogger.error(e.message) + end + end + log_yaml = Dunlop::NestedLogger.flush_as_yaml + log_entries.create(body: log_yaml, backtrace: backtrace) if log_yaml.present? + end + raise e if e + end + + def logger + Dunlop::NestedLogger + end + + def log_state_transition + log_entries.create(body: "transition to #{state}") + end + + def log_entry + Dunlop::LogEntryCreator.new(self) + end +end diff --git a/app/services/dunlop/nested_logger.rb b/app/services/dunlop/nested_logger.rb new file mode 100644 index 0000000..39aeb6b --- /dev/null +++ b/app/services/dunlop/nested_logger.rb @@ -0,0 +1,158 @@ +# thread safe logger +module Dunlop::NestedLogger + + class << self + + def formatter=(formatter) + Thread.current[:nested_logger_formatter] = formatter + end + + def options + @options ||= { + yaml_options: {} + } + end + + def formatter + Thread.current[:nested_logger_formatter] ||= begin + proc do |severity, datetime, progname, msg| + "#{severity == :warn ? 'WARNING' : severity.upcase} - #{msg}" + end + end + end + + def reset_formatter + Thread.current[:nested_logger_formatter] = nil + end + + def benchmark + Thread.current[:nested_logger_benchmark] = true + yield + ensure + Thread.current[:nested_logger_benchmark] = false + end + + def benchmark? + Thread.current[:nested_logger_benchmark] == true + end + + def scope(label, options={}) + scope_stack << Scope.new(label) + r0 = Time.now + return_value = yield + duration = Time.now - r0 + info("duration = #{duration}s") if options.fetch(:benchmark, benchmark?) + + if parent_scope? + closing_scope = scope_stack.pop + current_scope.add_scope(closing_scope) unless closing_scope.empty? + end + return_value + end + + def scope_stack + Thread.current[:nested_logger_scope_stack] ||= [BaseScope.new] + end + + def empty_scope_stack + Thread.current[:nested_logger_scope_stack] = nil + end + alias_method :reset, :empty_scope_stack + + def current_scope + scope_stack.last + end + + def parent_scope? + scope_stack.size > 1 + end + + def merge_threads(threads) + threads.map do |thread| + if top_thread_scope = Array.wrap(thread[:nested_logger_scope_stack]).pop + current_scope.entries += top_thread_scope.entries + end + end + end + + def copy_scope_stack_for_merge + Marshal.load(Marshal.dump(scope_stack)) + end + + def merge_scope_stack(stack) + current_scope.entries += stack.pop.entries + end + + def flush + current_scope.pop_entries.map(&:to_entry) || [] + end + + def flush_as_yaml + (f = flush.presence) ? YAML.dump(f, Dunlop::NestedLogger.options[:yaml_options]).sub("---\n", '') : '' + end + + def log(options={}) + current_scope.log(options) if current_scope + end + + [:debug, :info, :warn, :error].each do |severity| + define_method(severity) do |msg| + current_scope.log(msg: msg, severity: severity) if current_scope + end + end + end + + class Scope + attr_accessor :label, :entries + def initialize(label, entries=[]) + @label = label.to_s + @entries = entries + end + + def empty? + entries.empty? + end + + def log(options={}) + entries << Entry.new(options) + end + + def add_scope(scope) + entries << scope + end + + def to_entry + { label => entries.map(&:to_entry).compact } unless empty? + end + + def pop_entries + entries.tap { self.entries = [] } + end + end + + class BaseScope < Scope + def initialize + @entries = [] + end + def to_entry + entries.map(&:to_entry).compact unless empty? + end + end + + class Entry + attr_reader :severity, :datetime, :progname, :msg + + def initialize(options) + @severity = options[:severity] + @datetime = options[:datetime] || Time.current + @progname = options[:progname] + @msg = options[:msg] + end + + def to_entry + Dunlop::NestedLogger.formatter.call(severity, datetime, progname, msg) + end + end + +end + diff --git a/app/services/dunlop/overdue_handling.rb b/app/services/dunlop/overdue_handling.rb new file mode 100644 index 0000000..bb865e1 --- /dev/null +++ b/app/services/dunlop/overdue_handling.rb @@ -0,0 +1,124 @@ +module Dunlop::OverdueHandling + @registry = {} + @final_states = %w[overdue completed rejected] + + def self.registry + @registry + end + + def self.final_states + @final_states + end + + def self.final_states=(states) + @final_states = states + end + + def self.define(&block) + @registry = {} # empty the registry + definition_proxy = DefinitionProxy.new + definition_proxy.instance_eval(&block) + end + + def self.handle_overdue! + registry.each do |workflow_step_name, config| + base_scope = WorkflowInstance.all #where.not(state: final_states) + base_scope = base_scope.where(sti_type: config.scenarios.map{|s| "WorkflowInstance::#{s.to_s.classify}"}) if config.scenarios.any? + base_scope = base_scope.joins(workflow_step_name).where.not("#{workflow_step_name.to_s.pluralize}.state" => final_states) #.includes(workflow_step_name) + if config.from.is_nested? + base_scope = base_scope.joins(config.from.key) if config.from.key != workflow_step_name + end + base_scope = base_scope.where("#{config.date_target_class.table_name}.#{config.date_target_column} <= ?", Date.current - config.time) + + base_scope.find_each do |workflow_instance| + next unless workflow_step = workflow_instance.public_send(workflow_step_name) + workflow_step.is_overdue! + end + end + end + + class DefinitionProxy + def when_workflow_step(workflow_step, options = {}, &block) + raise "Workflow step #{workflow_step} does not exist" unless WorkflowInstance.possible_workflow_step_names.include?(workflow_step) + workflow_step_proxy = WorkflowStepProxy.new(workflow_step, options) + workflow_step_proxy.instance_eval(&block) if block + workflow_step_proxy.register_callbacks! + Dunlop::OverdueHandling.registry[workflow_step] = workflow_step_proxy + end + + def the_final_states_are(states) + Dunlop::OverdueHandling.final_states = states + end + end + + class WorkflowStepProxy + attr_reader :workflow_step, :options, :overdue_target_class + def initialize(workflow_step, options) + @workflow_step, @options = workflow_step, options + @overdue_target_class = workflow_step.to_s.classify.constantize + end + + def date_target_class + @date_target_class ||= from.is_nested? ? from.key.to_s.classify.constantize : WorkflowInstance + end + + def date_target_column + @date_target_column ||= from.field + end + + def scenarios + options[:scenarios] || [] + end + + def from + WorkflowStepBaseDate.new(workflow_step, options[:from]) + end + + def time + options[:is] + end + + #TODO: find a simple way to do this!!! + def register_callbacks! + date_target_classes = [date_target_class] #TODO: only implement on specific scenarios if given as argument + date_column = date_target_column + workflow_step_name = workflow_step # make local scope for after_save definition + overdue_time = time + date_target_classes.each do |target_class| + target_class.after_save do |record| + if saved_changes[date_column] + target_record = record.workflow_instance.public_send(workflow_step_name) + if date = record.public_send(date_column) + if date <= Date.current - overdue_time + target_record.is_overdue! if target_record.pending? or target_record.processing? + else + target_record.process! if target_record.overdue? + end + else + target_record.process! if target_record.overdue? + end + end + end + end + end + end + + class WorkflowStepBaseDate + attr_reader :workflow_step, :config + def initialize(workflow_step, config) + @workflow_step, @config = workflow_step, config + end + + def is_nested? + config.is_a?(Hash) + end + + def key + is_nested? ? config.keys.first : config + end + + def field + is_nested? ? config.values.first : config + end + end +end diff --git a/app/services/dunlop/state_badge.rb b/app/services/dunlop/state_badge.rb new file mode 100644 index 0000000..e2a63a4 --- /dev/null +++ b/app/services/dunlop/state_badge.rb @@ -0,0 +1,49 @@ +class Dunlop::StateBadge + attr_reader :context, :target, :state, :options + def initialize(context, target, options = {}) + @context = context + @target = target + @target = target.object if target && target.respond_to?(:decorated?) && target.decorated? + @options = options + @state = options[:state] || target.try(:state) # allow hard setting of state, take current state otherwise + end + + def link_to + options[:link_to] + end + + def valid? + target.present? && state.present? + end + + def underscored + @underscored = target.class.name.underscore + end + + # Code climate extraction + def human_state_name + state_name = target.class.human_state_name(state) + state_name + end + + def badge_data + result = (options[:data] || {}) + result = result.merge(id: target.id, resource: underscored, state: state) + result.merge!(target: link_to) if link_to.present? + result + end + + # Generate html as string in stead of a partial. This made the workflow_instances index page render a factor 4 faster + def html + #content = options[:link_to] ? context.link_to(inner_badge_html, options[:link_to]) : inner_badge_html + content = inner_badge_html + context.content_tag :span, content, + class: "display-badge #{underscored.gsub(/\W/, ' ')} #{state}", + data: badge_data + end + + def inner_badge_html + badge_name = context.t("display_badge", scope: underscored, default: Proc.new{ I18n.t('display_badge', scope: underscored.split('/').first, default: target.class.model_name.human) }) + %|#{badge_name}#{human_state_name}#{options[:state_append_text]}|.html_safe + end +end diff --git a/app/services/dunlop/user_settings.rb b/app/services/dunlop/user_settings.rb new file mode 100644 index 0000000..44b2090 --- /dev/null +++ b/app/services/dunlop/user_settings.rb @@ -0,0 +1,40 @@ +class Dunlop::UserSettings + attr_reader :record, :storage + def initialize(record, storage) + @record, @storage = record, storage + end + + def get(*path_spec) + current_settings_level = storage + Array.wrap(path_spec).each do |level| + current_settings_level = current_settings_level[level.to_s] + return nil unless current_settings_level + end + current_settings_level + end + + def set(*args) + value = args.pop + if value.is_a?(Hash) and args.empty? # settings.set(a: 4) assignment + value.each do |k, v| + set(k, v) + end + else + value = value.deep_stringify_keys if value.respond_to?(:deep_stringify_keys) + args = args.first if args.first.is_a?(Array) + current_settings_level = storage + Array.wrap(args)[0..-2].each do |level| + current_settings_level[level.to_s] ||= {} + current_settings_level = current_settings_level[level.to_s] + end + current_settings_level[args.last.to_s] = value + record.settings_storage = storage + end + value + end + + def set!(*args) + set(*args) + record.save + end +end diff --git a/app/services/dunlop/workflow.rb b/app/services/dunlop/workflow.rb new file mode 100644 index 0000000..318d247 --- /dev/null +++ b/app/services/dunlop/workflow.rb @@ -0,0 +1,13 @@ +# https://robots.thoughtbot.com/mygem-configure-block +module Dunlop::Workflow + class << self + attr_accessor :configuration + def configuration + @configuration ||= Dunlop::Workflow::Configuration.new + end + end + + def self.configure + yield(configuration) + end +end diff --git a/app/services/dunlop/workflow/configuration.rb b/app/services/dunlop/workflow/configuration.rb new file mode 100644 index 0000000..eff17bf --- /dev/null +++ b/app/services/dunlop/workflow/configuration.rb @@ -0,0 +1,7 @@ +class Dunlop::Workflow::Configuration + attr_accessor :max_number_of_records_in_display_badge + + def initialize + @max_number_of_records_in_display_badge = 100 + end +end diff --git a/app/services/dunlop/workflow_steps_in_batch_finished.rb b/app/services/dunlop/workflow_steps_in_batch_finished.rb new file mode 100644 index 0000000..5d8a602 --- /dev/null +++ b/app/services/dunlop/workflow_steps_in_batch_finished.rb @@ -0,0 +1,126 @@ +#Dunlop::WorkflowStepsInBatchFinished.define do +# the_final_states_are %w[rejected completed] + +# on_finishing_of :owner_service_window1_preparation do +# description "Send notification mail to WBA" do |workflow_instance_batch| +# KpnCoreDeliveryMailer.wba_notification(workflow_instance_batch.id, self.model_name.human).deliver_later +# end +# end + +# on_finishing_of :contractor_service_window1_execution do +# description <<-DESCRIPTION +# Check ${attributes.owner_service_window2_preparation.migration_file_for_bvt_ready} +# for ${models.owner_service_window2_preparation} workflow steps in the batch +# DESCRIPTION +# action do |workflow_instance_batch| +# workflow_steps_in_batch_scope(:owner_service_window2_preparation).update_all migration_file_for_bvt_ready: true +# end + +# description "Send an email to Core Delivery informing them of batch completion" do |workflow_instance_batch| +# KpnCoreDeliveryMailer.contractor_service_window1_handled_for_batch(workflow_instance_batch.id).deliver_later +# end + +# description "Send an email to Schuuring informing them of batch completion" do |workflow_instance_batch| +# SchuuringMailer.workflow_instance_batch_workflow_step_finished(self.class.name, workflow_instance_batch.id).deliver_later +# end +# end + +#end +module Dunlop::WorkflowStepsInBatchFinished + @registry = {} + @final_states = %w[completed rejected] + + def self.registry + @registry + end + + def self.final_states + @final_states + end + + def self.final_states=(states) + @final_states = states + end + + def self.define(&block) + definition_proxy = DefinitionProxy.new + definition_proxy.instance_eval(&block) + end + + def self.handle_workflow_step(workflow_instance_batch, workflow_step) + return unless definition_proxy = registry[workflow_step.process_name.to_sym] + batch_states = workflow_step.workflow_steps_in_batch_scope.uniq.pluck(:state) + #return unless workflow_step.changes[:state].present? + if batch_states.any? and (batch_states - final_states).empty? + definition_proxy.actions.each do |action_definition| + workflow_step.instance_exec(workflow_instance_batch, &action_definition.action) + end + end + end + + class DefinitionProxy + def on_finishing_of(workflow_step, options = {}, &block) + workflow_step_proxy = WorkflowStepProxy.new + workflow_step_proxy.instance_eval(&block) + Dunlop::WorkflowStepsInBatchFinished.registry[workflow_step] = workflow_step_proxy + end + + def the_final_states_are(states) + Dunlop::WorkflowStepsInBatchFinished.final_states = states + end + end + + class WorkflowStepProxy + attr_reader :actions + def initialize + @actions = [] + @action_descriptions = [] + @scenarios_definition = nil + end + + # Actions should be given a discription. This can be done by giving a + # description "Hi there, my function is to describe what the following action will do" + # followed by an + # action { do_something } + # statement. The action can also be added directly on the description by supplying the + # action Proc: + # description "This description will be the action at the same time", scenarios: [:scenario1] do + # do_something + # end + def description(description, options = {}, &block) + @action_descriptions << description + if block.present? # Action direct specified on description + action(options, &block) + end + end + + def action(options = {}, &block) + @actions << WorkflowStepAction.new(action: block, descriptions: @action_descriptions.map(&:strip_heredoc), scenarios_definition: @scenarios_definition) + @action_descriptions = [] + end + + # Allow setting another scope for scenarios + def scenarios(*scenarios_definition, &block) + @previous_scenarios_definition = @scenarios_definition + @scenarios_definition = scenarios_definition + instance_eval(&block) + @scenarios_definition = @previous_scenarios_definition + end + end + + class WorkflowStepAction + attr_reader :settings + def initialize(settings) + @settings = settings + end + + def action + @settings[:action] + end + + def descriptions + @settings[:descriptions] || [] + end + end +end +#require_dependency Rails.root.join("config/initializers/workflow_steps_in_batch_finished_config") diff --git a/app/services/implement_in_subclass.rb b/app/services/implement_in_subclass.rb new file mode 100644 index 0000000..cebc60e --- /dev/null +++ b/app/services/implement_in_subclass.rb @@ -0,0 +1,18 @@ +# To force an interface raise this error: +# raise ImplementInSubclass.new(self) +# to raise an error with a nice message +class ImplementInSubclass < StandardError + def initialize(klass) + @calling_method = caller_locations(2,1)[0] + @concerning_class = klass.is_a?(Class) ? klass : klass.class # can be called from instance or class + super + end + + def method_to_be_implemented + @calling_method.label + end + + def message + "Implement method: #{method_to_be_implemented} in a subclass of #{@concerning_class.name}" + end +end diff --git a/app/services/json_column_coder.rb b/app/services/json_column_coder.rb new file mode 100644 index 0000000..7520897 --- /dev/null +++ b/app/services/json_column_coder.rb @@ -0,0 +1,34 @@ +require 'json' +class JSONColumnCoder + + attr_accessor :object_class + + def initialize(object_class = Object) + @object_class = object_class + end + + def dump(obj) + return if obj.nil? + + unless obj.is_a?(object_class) + raise ActiveRecord::SerializationTypeMismatch, + "Attribute was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}" + end + JSON.dump obj + end + + def load(json) + return object_class.new if object_class != Object && json.nil? + return json unless json.is_a?(String) + obj = JSON.load(json) + + unless obj.is_a?(object_class) || obj.nil? + raise ActiveRecord::SerializationTypeMismatch, + "Attribute was supposed to be a #{object_class}, but was a #{obj.class}" + end + obj ||= object_class.new if object_class != Object + + obj + end +end + diff --git a/app/services/week.rb b/app/services/week.rb new file mode 100644 index 0000000..1908078 --- /dev/null +++ b/app/services/week.rb @@ -0,0 +1,9 @@ +class Week < Range + attr_reader :number, :year + def initialize(number, options = {}) + @number = number + @year = options[:year] || Date.current.year + beginning_of_week = Date.commercial(year, number, 1) + super(beginning_of_week, beginning_of_week.end_of_week) + end +end diff --git a/app/views/application/403.csv b/app/views/application/403.csv new file mode 100644 index 0000000..e69de29 diff --git a/app/views/application/403.html.slim b/app/views/application/403.html.slim new file mode 100644 index 0000000..e222cbd --- /dev/null +++ b/app/views/application/403.html.slim @@ -0,0 +1,2 @@ +br +.alert-box.alert Unauthorized action diff --git a/app/views/application/_admin_user_roles.html.slim b/app/views/application/_admin_user_roles.html.slim new file mode 100644 index 0000000..7d44469 --- /dev/null +++ b/app/views/application/_admin_user_roles.html.slim @@ -0,0 +1,5 @@ +.ui.horizontal.divider Authorizations +.user-roles-tables.ui.two.column.grid + .column= render "dunlop/users/scenario_permissions" + .column= render "dunlop/users/source_files_permissions" + .column= render "dunlop/users/target_files_permissions" diff --git a/app/views/dunlop/_badge_info_collection_attributes.html.slim b/app/views/dunlop/_badge_info_collection_attributes.html.slim new file mode 100644 index 0000000..71f1851 --- /dev/null +++ b/app/views/dunlop/_badge_info_collection_attributes.html.slim @@ -0,0 +1,2 @@ += render "dunlop/badge_info/show_attributes", local_assigns += render "dunlop/badge_info/footer_actions", local_assigns diff --git a/app/views/dunlop/_log_entries.html.slim b/app/views/dunlop/_log_entries.html.slim new file mode 100644 index 0000000..caac143 --- /dev/null +++ b/app/views/dunlop/_log_entries.html.slim @@ -0,0 +1,15 @@ +.ui.horizontal.divider Log Entries +table.log-entries.ui.table + thead + tr + th Timestamp + th Body + tbody + - loggable.log_entries.each do |log_entry| + tr + td data-time=log_entry.created_at.iso8601 data-format="LLL" + td + pre.log-entry-body= log_entry.body + - if can? :backtrace, log_entry + pre.log-entry-backtrace= log_entry.backtrace + diff --git a/app/views/dunlop/badge_info/_footer_actions.html.slim b/app/views/dunlop/badge_info/_footer_actions.html.slim new file mode 100644 index 0000000..9a63f22 --- /dev/null +++ b/app/views/dunlop/badge_info/_footer_actions.html.slim @@ -0,0 +1,3 @@ +.page-actions + - if can? :manage, record + = link_to t('action.edit.label', model: record.class.model_name.human), [main_app, :edit, record], class: ['collection-edit', record.process_name], data: {resource: record.class.name.underscore, action: 'collection_edit', request_params: {ids: record.id}} diff --git a/app/views/dunlop/badge_info/_show_attributes.html.slim b/app/views/dunlop/badge_info/_show_attributes.html.slim new file mode 100644 index 0000000..db75c2e --- /dev/null +++ b/app/views/dunlop/badge_info/_show_attributes.html.slim @@ -0,0 +1,30 @@ +/ This view renders the attributes defined on the record's class collection +/ This is meant to be a good default when no specific logic is required +- record.collection_class.attributes.each do |attribute, options| + - column = record.class.columns_hash[attribute] + - if attribute.ends_with?('_id') && association_options = record.class.reflect_on_association(attribute.sub(/_id$/, '')) #UGLY BUT EFFECTIVE + - if association = record.public_send(association_options.name) + .display-row class="attribute-#{association_options.name}" + .display-label= association.class.model_name.human + .display-field + - display_value = association.attributes.slice('name', 'title').values.first || attribute.sub(/_id$/, '') + - if path = polymorphic_path([main_app, association]) rescue nil # might not have a show path + = link_to display_value, path + - else + = display_value + - elsif column.type == :text + .display-row class="attribute-#{attribute}" + .display-label= record.class.human_attribute_name(attribute) + pre.badge-info-text.display-field= record.public_send(attribute) + - elsif column.type == :boolean + .display-row class="attribute-#{attribute}" + .display-label= record.class.human_attribute_name(attribute) + .display-field= boolean_show record.public_send(attribute) + - elsif column.type == :date + .display-row class="attribute-#{attribute}" + .display-label= record.class.human_attribute_name(attribute) + .display-field= dunlop_localize record.public_send(attribute) + - else + .display-row class="attribute-#{attribute}" + .display-label= record.class.human_attribute_name(attribute) + .display-field= record.public_send(attribute) diff --git a/app/views/dunlop/dashboard/badge_info.html.slim b/app/views/dunlop/dashboard/badge_info.html.slim new file mode 100644 index 0000000..5598054 --- /dev/null +++ b/app/views/dunlop/dashboard/badge_info.html.slim @@ -0,0 +1,21 @@ +- if @record + h3= @record.model_name.human + .display-row + .display-label= @record.class.human_attribute_name(:state) + .display-field= @record.human_state_name + /= render 'migration_attribute', collection: @record.migration_attributes + = record_info_fields @record +- else + .display-row + .display-label= @process_class.model_name.human + .display-field= @process_class.human_state_name params[:state] + - if @migration_batch + .display-row + .display-label= @migration_batch.class.model_name.human + .display-field= link_to @migration_batch.presentation_name, [main_app, @migration_batch] + - if @workflow_instances.size > Dunlop::Workflow.configuration.max_number_of_records_in_display_badge + .panel.service_groups-list= t('dunlop.display_badge.more_records_than_maximum', count: Dunlop::Workflow.configuration.max_number_of_records_in_display_badge) + - else + .panel.service_groups-list== @workflow_instances.map{ |workflow_instance| link_to workflow_instance.presentation_name, [main_app, workflow_instance] }.join(', ') + - if params[:target].present? + = link_to t('dunlop.display_badge.collection_target_link'), params[:target] diff --git a/app/views/dunlop/dashboard/home.html.slim b/app/views/dunlop/dashboard/home.html.slim new file mode 100644 index 0000000..3c06f9b --- /dev/null +++ b/app/views/dunlop/dashboard/home.html.slim @@ -0,0 +1 @@ += page_title "Dunlop" diff --git a/app/views/dunlop/execution_batches/index.html.slim b/app/views/dunlop/execution_batches/index.html.slim new file mode 100644 index 0000000..002ca80 --- /dev/null +++ b/app/views/dunlop/execution_batches/index.html.slim @@ -0,0 +1,72 @@ +- if defined?(Foundation) + = page_title :index, ExecutionBatch + = search_form_for @q do |f| + table.execution-batches.ui.compact.striped.table + thead + tr + th= sort_link @q, :sti_type + th= sort_link @q, :state + th= sort_link @q, :created_at + th= at :info_message + th.actions + i.icon-search + = search_result_info @batches + = search_row do + th= f.select :sti_type_eq, ExecutionBatch.distinct.pluck(:sti_type), {include_blank: ' - '}, {class: 'input-small'} + th= f.select :state_eq, ExecutionBatch.distinct.pluck(:state), {include_blank: ' - '}, {class: 'input-small'} + th + = f.text_field "created_at_gteq", placeholder: 'min', class: 'datepicker input-small' + = f.text_field "created_at_lteq", placeholder: 'max', class: 'datepicker input-small' + th= f.text_field :info_message_cont, placeholder: '~' + th.column-filter.actions + = f.submit t('dunlop.filter.apply').html_safe, class: 'submit-filters-button' + = link_to dunlop.execution_batches_path(reset_q: true), class: 'clear-filters-button' do + span== t('dunlop.filter.reset') + tbody + - @batches.each do |batch| + tr + td= link_to batch.model_name.human, dunlop.execution_batch_path(batch) + td= batch.human_state_name + td data-time=batch.created_at.iso8601 data-format="LLL" + td= batch.info_message + td= table_show_link batch, dunlop.execution_batch_path(batch) + + br + = paginate @batches +- else + .ui.container + = page_title :index, ExecutionBatch + = search_form_for(@q, html: {class: 'ui form'}) do |f| + table.execution-batches.ui.compact.striped.table + thead + tr + th= sort_link @q, :sti_type + th= sort_link @q, :state + th= sort_link @q, :created_at + th= at :info_message + th.actions + i.icon-search + = search_result_info @batches + = search_row do + th= f.select :sti_type_eq, ExecutionBatch.distinct.pluck(:sti_type), {include_blank: ' - '}, {class: 'input-small'} + th= f.select :state_eq, ExecutionBatch.distinct.pluck(:state), {include_blank: ' - '}, {class: 'input-small'} + th + .ui.mini.input= f.text_field "created_at_gteq", placeholder: 'min', class: 'datepicker input-small' + .ui.mini.input= f.text_field "created_at_lteq", placeholder: 'max', class: 'datepicker input-small' + th= f.text_field :info_message_cont, placeholder: '~' + th.column-filter.actions + button.ui.small.primary.icon.button.submit-filters-button + i.filter.icon + = link_to dunlop.source_files_path(reset_q: true), class: 'clear-filters-button ui basic yellow small icon button' + i.undo.icon + tbody + - @batches.each do |batch| + tr + td= link_to batch.model_name.human, dunlop.execution_batch_path(batch) + td= batch.human_state_name + td data-time=batch.created_at.iso8601 data-format="LLL" + td= batch.info_message + td= table_show_link batch, dunlop.execution_batch_path(batch) + + br + = paginate @batches diff --git a/app/views/dunlop/execution_batches/show.html.slim b/app/views/dunlop/execution_batches/show.html.slim new file mode 100644 index 0000000..53ade18 --- /dev/null +++ b/app/views/dunlop/execution_batches/show.html.slim @@ -0,0 +1,41 @@ +- if defined?(Foundation) + = page_title :show, @record, back: dunlop.execution_batches_path + .display-row + .display-label= at :sti_type + .display-field= @record.model_name.human + .display-row + .display-label= at :state + .display-field= @record.human_state_name + .display-row + .display-label= at :created_at + .display-field data-time=@record.created_at.iso8601 data-format="LLL" + .display-row + .display-label= at :updated_at + .display-field data-time=@record.updated_at.iso8601 data-format="LLL" + .display-row + .display-label= at :info_message + .display-field= @record.info_message + = render 'dunlop/log_entries', loggable: @record +- else + .ui.container + = page_title :show, @record, back: dunlop.execution_batches_path + table.ui.definition.table + tr + td= at :sti_type + td= @record.model_name.human + tr + td= at :state + td= @record.human_state_name + tr + td= at :created_at + td: span data-time=@record.created_at.iso8601 data-format="LLL" + tr + td= at :updated_at + td: span data-time=@record.updated_at.iso8601 data-format="LLL" + tr + td= at :info_message + td= @record.info_message + = render 'dunlop/log_entries', loggable: @record +- if @record.processing? + javascript: + setTimeout(function(){window.location.reload()}, 40000) diff --git a/app/views/dunlop/reports/system/index.html.slim b/app/views/dunlop/reports/system/index.html.slim new file mode 100644 index 0000000..089eeb2 --- /dev/null +++ b/app/views/dunlop/reports/system/index.html.slim @@ -0,0 +1,30 @@ +h1= t 'dunlop.reports.title' +ul + - if can? :report, :system + li + h4= t('dunlop.reports.system.title') + ul + li= link_to t('dunlop.reports.system.workflow_instance_workflow_steps.title', workflow_instance: WorkflowInstance.model_name.human), %i[reports system workflow_instance_workflow_steps] + li= link_to t('dunlop.reports.system.workflow_instance_fields.title'), %i[reports system workflow_instance_fields] + li= link_to t('dunlop.reports.system.workflow_steps_in_batch_finished.title'), %i[reports system workflow_steps_in_batch_finished] + - if Dunlop::OverdueHandling.registry.present? + li= link_to t('dunlop.reports.system.workflow_steps_overdue.title'), %i[reports system workflow_steps_overdue] + - if can? :manage, User + li= link_to t('dunlop.reports.users_permissions_table.title'), dunlop.permissions_table_users_path + /li= link_to 'WorkflowInstance workflow_steps', %i[reports system workflow_instance_workflow_steps] + /li= link_to 'WorkflowStep scenarios', %i[reports system workflow_step_scenarios] +hr +.row + .small-12.columns + table + tbody + tr + th= t 'dunlop.statistics.overall.total_count' + td= WorkflowInstance.count + tr + th= t 'dunlop.statistics.overall.active_count' + td= WorkflowInstance.active.count + tr + th= t 'dunlop.statistics.overall.archived_count' + td= WorkflowInstance.archived.count + diff --git a/app/views/dunlop/reports/system/workflow_instance_fields.html.slim b/app/views/dunlop/reports/system/workflow_instance_fields.html.slim new file mode 100644 index 0000000..aafb7f6 --- /dev/null +++ b/app/views/dunlop/reports/system/workflow_instance_fields.html.slim @@ -0,0 +1,48 @@ +h1= "#{WorkflowInstance.model_name.human} fields" +- attribute_counter = 0 +- WorkflowInstance.scenario_classes.each do |scenario_class| + .report-scenario-container + h2= scenario_class.model_name.human + /- scenario_class.new.migration_attributes.each do |attr| + - scenario_class.collection.attributes.each do |attr, attr_options| + .report-attribute-name + - case scenario_class.columns_hash[attr].type + - when :integer + - if attr.ends_with? '_id' + span.fa.fa-long-arrow-right + - else + span.fa.fa-sort-numeric + - when :boolean + = boolean_show true + - when :date + span.fa.fa-calendar + - when :text + span.fa.fa-sticky-note-o + = scenario_class.human_attribute_name(attr) + - attribute_counter += 1 + - scenario_class.workflow_step_classes.each do |workflow_step_class| + .report-subprocess-container + h3= workflow_step_class.model_name.human + - workflow_step_class.collection.attributes.each do |attr, attr_options| + .report-attribute-name + - case workflow_step_class.columns_hash[attr].type + - when :integer + - if attr.ends_with? '_id' + span.fa.fa-long-arrow-right + - else + span.fa.fa-sort-numeric + - when :boolean + = boolean_show true + - when :date + span.fa.fa-calendar + - when :text + span.fa.fa-sticky-note-o + = workflow_step_class.human_attribute_name(attr) + - attribute_counter += 1 + +.report-statistics.hide + .report-total-attributes= attribute_counter + + +.page-actions + = link_to 'Back', reports_root_path, class: 'back' diff --git a/app/views/dunlop/reports/system/workflow_instance_workflow_steps.html.slim b/app/views/dunlop/reports/system/workflow_instance_workflow_steps.html.slim new file mode 100644 index 0000000..79c393f --- /dev/null +++ b/app/views/dunlop/reports/system/workflow_instance_workflow_steps.html.slim @@ -0,0 +1,19 @@ +- t_scope = 'dunlop.reports.system.workflow_instance_workflow_steps' += page_title t('title', scope: t_scope, workflow_instance: WorkflowInstance.model_name.human) +table + thead + th + - WorkflowInstance.scenario_classes.each do |scenario_class| + th= scenario_class.model_name.human + tbody + - WorkflowInstance.workflow_step_classes.each do |workflow_step_class| + tr + th= workflow_step_class.model_name.human + - WorkflowInstance.scenario_classes.each do |scenario_class| + td + - if scenario_class.scenario_workflow_step_names.map(&:to_s).include? workflow_step_class.process_name + span.fa.fa-check + - else + span +.page-actions + = link_to 'Back', reports_root_path, class: 'back' diff --git a/app/views/dunlop/reports/system/workflow_step_scenarios.html.slim b/app/views/dunlop/reports/system/workflow_step_scenarios.html.slim new file mode 100644 index 0000000..6f3b2d2 --- /dev/null +++ b/app/views/dunlop/reports/system/workflow_step_scenarios.html.slim @@ -0,0 +1,8 @@ +h1 Subprocess scenarios +.panel + - WorkflowInstance.workflow_step_classes.each do |subprocess_class| + .subprocess-container + h3.scenario-name= subprocess_class.model_name.human + - subprocess_class.descendants.each do |subprocess_scenario| + .subprocess-name= "- #{subprocess_scenario.name.demodulize}" + hr diff --git a/app/views/dunlop/reports/system/workflow_steps_in_batch_finished.html.slim b/app/views/dunlop/reports/system/workflow_steps_in_batch_finished.html.slim new file mode 100644 index 0000000..79bfc41 --- /dev/null +++ b/app/views/dunlop/reports/system/workflow_steps_in_batch_finished.html.slim @@ -0,0 +1,11 @@ +- t_scope = 'dunlop.reports.system.workflow_steps_in_batch_finished' += page_title t('title', scope: t_scope) +- Dunlop::WorkflowStepsInBatchFinished.registry.each do |workflow_step_name, workflow_step_definition| + .row + h3=t 'all_in_finished_state_title', scope: t_scope, workflow_steps: workflow_step_name.to_s.classify.constantize.model_name.human_plural + ul + - workflow_step_definition.actions.each do |workflow_step_action| + - workflow_step_action.descriptions.each do |action_description| + li= interpreted_text action_description +.page-actions + = link_to 'Back', reports_root_path, class: 'back' diff --git a/app/views/dunlop/reports/system/workflow_steps_overdue.html.slim b/app/views/dunlop/reports/system/workflow_steps_overdue.html.slim new file mode 100644 index 0000000..dfedcc3 --- /dev/null +++ b/app/views/dunlop/reports/system/workflow_steps_overdue.html.slim @@ -0,0 +1,17 @@ +- t_scope = 'dunlop.reports.system.workflow_steps_overdue' += page_title t('title', scope: t_scope) +ul +- Dunlop::OverdueHandling.registry.each do |workflow_step_name, config| + - workflow_step = workflow_step_name.to_s.classify.constantize + - date = config.date_target_class.human_attribute_name(config.date_target_column) + - if config.from.is_nested? + - date = [config.date_target_class.model_name.human, date].join(' → ') + - t_args = {scope: t_scope, workflow_step: workflow_step.model_name.human, date: date} + - if config.time < 0 + li== t('before_date', t_args.merge(time: human_duration(config.time).gsub('-', ''))) + - elsif config.time.zero? + li== t('on_date', t_args) + - else + li== t('after_date', t_args.merge(time: human_duration(config.time))) +.page-actions + = link_to 'Back', reports_root_path, class: 'back' diff --git a/app/views/dunlop/source_files/index.html.slim b/app/views/dunlop/source_files/index.html.slim new file mode 100644 index 0000000..1d52252 --- /dev/null +++ b/app/views/dunlop/source_files/index.html.slim @@ -0,0 +1,101 @@ +- if defined?(Foundation) + = page_title :index, SourceFile + .row + = simple_form_for(SourceFile.new) do |f| + .small-6.medium-4.columns + .form-inputs= f.input :sti_type, collection: SourceFile.classes.select{|cls| can? :manage, cls}.map{|cls| [cls.model_name.human, cls.name]}, include_blank: '', input_html: {class: 'smart-select'} + .small-6.medium-4.columns= f.input :original_file, as: :file + .small-6.medium-4.columns= f.submit t('action.new.label', model: SourceFile.model_name.human), class: 'button' + hr + table.source_files + thead + = search_form_for @q do |f| + = hidden_field_tag :per_page, params[:per_page], id: 'q_per_page' + tr + th= at :sti_type + th= at :name + th= at :state + th= sort_link @q, :created_at + th= at :creator + th.actions + i.icon-search + = search_result_info @source_files + = search_row do + th= f.select :sti_type_eq, SourceFile.classes.select{|cls| can? :read, cls}.map{|cls| [cls.model_name.human, cls.name]}, {include_blank: ' - '}, class: 'smart-select' + th= f.text_field :original_file_cont, placeholder: 'filename contains' + th= f.select :state_eq, SourceFile.distinct.pluck(:state), {include_blank: ' - '}, {class: 'input-small'} + th + = f.text_field "created_at_gteq", placeholder: 'min', class: 'datepicker input-small' + = f.text_field "created_at_lteq", placeholder: 'max', class: 'datepicker input-small' + th + th.column-filter.actions + = f.submit t('dunlop.filter.apply').html_safe, class: 'submit-filters-button' + = link_to dunlop.source_files_path(reset_q: true), class: 'clear-filters-button' do + span== t('dunlop.filter.reset') + + tbody + - @source_files.each do |source_file| + - next unless can? :read, source_file + tr + td= link_to source_file.model_name.human, dunlop.source_file_path(source_file) + td= source_file.original_file.file.try :identifier + td= source_file.human_state_name + td data-time=source_file.created_at.iso8601 data-format="LLL" + td= link_to_model source_file.creator + td.actions + = table_show_link source_file, dunlop.source_file_path(source_file) + = table_download_link source_file, dunlop.download_source_file_path(source_file) + = table_destroy_link source_file, dunlop.source_file_path(source_file) + + = paginate @source_files +- else + .ui.container + = page_title :index, SourceFile + .ui.horizontal.divider Nieuwe toevoegen + = simple_form_for SourceFile.new, class: 'form' do |f| + .ui.three.column.grid + .column= f.input :sti_type, collection: SourceFile.classes.select{|cls| can? :manage, cls}.map{|cls| [cls.model_name.human, cls.name]}, include_blank: '', input_html: {class: 'smart-select'} + .column= f.input :original_file, as: :file + .column= f.submit t('action.new.label', model: SourceFile.model_name.human), class: 'ui button' + .ui.horizontal.divider= SourceFile.model_name.human_plural + = search_form_for(@q, html: {class: 'ui form'}) do |f| + table.source_files.ui.compact.striped.table + thead + = hidden_field_tag :per_page, params[:per_page], id: 'q_per_page' + tr + th= at :sti_type + th= at :name + th= at :state + th= sort_link @q, :created_at + th= at :creator + th.actions + i.icon-search + = search_result_info @source_files + = search_row do + th= f.select :sti_type_eq, SourceFile.classes.select{|cls| can? :read, cls}.map{|cls| [cls.model_name.human, cls.name]}, {include_blank: ' - '}, class: 'smart-select' + th= f.text_field :original_file_cont, placeholder: 'filename contains' + th= f.select :state_eq, SourceFile.distinct.pluck(:state), {include_blank: ' - '}, {class: 'input-small'} + th + .ui.mini.datepicker.input= f.text_field "created_at_gteq", placeholder: 'min' + .ui.mini.datepicker.input= f.text_field "created_at_lteq", placeholder: 'max' + th + th.column-filter.actions + button.ui.small.primary.icon.button.submit-filters-button + i.filter.icon + = link_to dunlop.source_files_path(reset_q: true), class: 'clear-filters-button ui basic yellow small icon button' + i.undo.icon + tbody + - @source_files.each do |source_file| + - next unless can? :read, source_file + tr + td= link_to source_file.model_name.human, dunlop.source_file_path(source_file) + td= source_file.original_file.file.try :identifier + td= source_file.human_state_name + td data-time=source_file.created_at.iso8601 data-format="LLL" + td= link_to_model source_file.creator + td.actions + = table_show_link source_file, dunlop.source_file_path(source_file) + = table_download_link source_file, dunlop.download_source_file_path(source_file) + = table_destroy_link source_file, dunlop.source_file_path(source_file) + + = paginate @source_files diff --git a/app/views/dunlop/source_files/show.html.slim b/app/views/dunlop/source_files/show.html.slim new file mode 100644 index 0000000..aeedcbb --- /dev/null +++ b/app/views/dunlop/source_files/show.html.slim @@ -0,0 +1,40 @@ +- if defined?(Foundation) + = page_title :show, @source_file, back: dunlop.source_files_path + + .display-row + .display-label= at :state + .display-field.state= @source_file.human_state_name + .display-row + .display-label= at :name + .display-field.state= @source_file.original_file.file.try :identifier + .display-row + .display-label= at :created_at + .display-field data-time=@source_file.created_at.iso8601 data-format="LLL" + .display-row + .display-label= at :current_at_day + .display-field data-date=@source_file.current_at_day.try(:iso8601) + + = render 'dunlop/log_entries', loggable: @source_file + - if can? :download, @source_file + .page-actions + = link_to "Download", dunlop.download_source_file_path(@source_file), class: 'export-button' +- else + .ui.container + = page_title :show, @source_file, back: dunlop.source_files_path + table.ui.top.attached.compact.definition.table + tr + td= at :state + td= @source_file.human_state_name + tr + td= at :name + td.state= @source_file.original_file.file.try :identifier + tr + td= at :created_at + td: span data-time=@source_file.created_at.iso8601 data-format="LLL" + tr + td= at :current_at_day + td: span data-date=@source_file.current_at_day.try(:iso8601) + - if can? :download, @source_file + .ui.bottom.attached.segment.page-actions + = link_to "Download", dunlop.download_source_file_path(@source_file), class: 'export-button ui violet button' + = render 'dunlop/log_entries', loggable: @source_file diff --git a/app/views/dunlop/target_files/index.html.slim b/app/views/dunlop/target_files/index.html.slim new file mode 100644 index 0000000..d56287d --- /dev/null +++ b/app/views/dunlop/target_files/index.html.slim @@ -0,0 +1,88 @@ +- if defined?(Foundation) + = page_title :index, TargetFile + table.target_files + thead + = search_form_for @q do |f| + tr + th= at :sti_type + th= at :state + th= at :size + th= at :number_of_records + th= sort_link @q, :created_at + th= at :duration + th.actions + i.icon-search + = search_result_info @target_files + = search_row do + th= f.select :sti_type_eq, tarray(TargetFile.classes.select{|klass| can? :read, klass}), {include_blank: ' - '}, class: 'smart-select', data: {select2_width: '400px'} + th= f.select :state_eq, TargetFile.distinct.pluck(:state), {include_blank: ' - '}, {class: 'input-small'} + th   + th   + th + = f.text_field "created_at_gteq", placeholder: 'min', class: 'datepicker input-small' + = f.text_field "created_at_lteq", placeholder: 'max', class: 'datepicker input-small' + th   + th.actions + = f.submit t('dunlop.filter.apply').html_safe, class: 'submit-filters-button' + = link_to dunlop.target_files_path(reset_q: true), class: 'clear-filters-button' do + span== t('dunlop.filter.reset') + tbody + - @target_files.each do |target_file| + - if can? :read, target_file + tr + td= link_to target_file.class.model_name.human, dunlop.target_file_path(target_file) + td= target_file.state + td data-size=target_file.size + td= target_file.number_of_records + td data-time=target_file.created_at.iso8601 + td= target_file.duration.to_f.round + td.actions + = table_show_link target_file, dunlop.target_file_path(target_file) + = table_download_link target_file, dunlop.download_target_file_path(target_file) + = table_destroy_link target_file, dunlop.target_file_path(target_file) + = paginate @target_files +- else + .ui.container + = page_title :index, TargetFile + = search_form_for(@q, html: {class: 'ui form'}) do |f| + table.target_files.ui.compact.striped.table + thead + tr + th= at :sti_type + th= at :state + th= at :size + th= at :number_of_records + th= sort_link @q, :created_at + th= at :duration + th.actions + i.icon-search + = search_result_info @target_files + = search_row do + th= f.select :sti_type_eq, tarray(TargetFile.classes.select{|klass| can? :read, klass}), {include_blank: ' - '}, class: 'smart-select', data: {select2_width: '400px'} + th= f.select :state_eq, TargetFile.distinct.pluck(:state), {include_blank: ' - '}, {class: 'input-small'} + th   + th   + th + .ui.mini.input= f.text_field "created_at_gteq", placeholder: 'min', class: 'datepicker input-small' + .ui.mini.input= f.text_field "created_at_lteq", placeholder: 'max', class: 'datepicker input-small' + th   + th.column-filter.actions + button.ui.small.primary.icon.button.submit-filters-button + i.filter.icon + = link_to dunlop.source_files_path(reset_q: true), class: 'clear-filters-button ui basic yellow small icon button' + i.undo.icon + tbody + - @target_files.each do |target_file| + - if can? :read, target_file + tr + td= link_to target_file.class.model_name.human, dunlop.target_file_path(target_file) + td= target_file.state + td data-size=target_file.size + td= target_file.number_of_records + td data-time=target_file.created_at.iso8601 + td= target_file.duration.to_f.round + td.actions + = table_show_link target_file, dunlop.target_file_path(target_file) + = table_download_link target_file, dunlop.download_target_file_path(target_file) + = table_destroy_link target_file, dunlop.target_file_path(target_file) + = paginate @target_files diff --git a/app/views/dunlop/target_files/show.html.slim b/app/views/dunlop/target_files/show.html.slim new file mode 100644 index 0000000..d39485f --- /dev/null +++ b/app/views/dunlop/target_files/show.html.slim @@ -0,0 +1,56 @@ +- if defined?(Foundation) + = page_title :show, @target_file, back: dunlop.target_files_path + .display-row + .display-label= at :sti_type + .display-field= @target_file.model_name.human + .display-row + .display-label= at :state + .display-field.state= @target_file.human_state_name + .display-row + .display-label= at :created_at + .display-field data-time=@target_file.created_at.iso8601 data-format="LLL" + .display-row + .display-label= at :start_time + .display-field data-time=@target_file.start_time.try(:iso8601) data-format="LLL" + .display-row + .display-label= at :end_time + .display-field data-time=@target_file.end_time.try(:iso8601) data-format="LLL" + .display-row + .display-label= at :size + .display-field= number_to_human_size @target_file.size + .display-row + .display-label= at :number_of_records + .display-field= @target_file.number_of_records + = render 'dunlop/log_entries', loggable: @target_file + - if can? :download, @target_file and @target_file.completed? + .page-actions + = link_to "Download", dunlop.download_target_file_path(@target_file), class: 'export-button' +- else + .ui.container + = page_title :show, @target_file, back: dunlop.target_files_path + table.ui.top.attached.definition.table + tr + td= at :sti_type + td= @target_file.model_name.human + tr + td= at :state + td= @target_file.human_state_name + tr + td= at :created_at + td: span data-time=@target_file.created_at.iso8601 data-format="LLL" + tr + td= at :start_time + td: span data-time=@target_file.start_time.try(:iso8601) data-format="LLL" + tr + td= at :end_time + td: span data-time=@target_file.end_time.try(:iso8601) data-format="LLL" + tr + td= at :size + td= number_to_human_size @target_file.size + tr + td= at :number_of_records + td= @target_file.number_of_records + - if can? :download, @target_file and @target_file.completed? + .ui.bottom.attached.segment.page-actions + = link_to "Download", dunlop.download_target_file_path(@target_file), class: 'export-button ui violet button' + = render 'dunlop/log_entries', loggable: @target_file diff --git a/app/views/dunlop/users/_form.html.slim b/app/views/dunlop/users/_form.html.slim new file mode 100644 index 0000000..03a7d81 --- /dev/null +++ b/app/views/dunlop/users/_form.html.slim @@ -0,0 +1,11 @@ += simple_form_for [dunlop, @user], html: {autocomplete: 'off', class: 'ui form'} do |f| + = f.error_notification + .form-inputs= f.input :email, input_html: {autocomplete: 'off'} + .form-inputs= f.input :password, input_html: {autocomplete: 'off'} + - if current_user.admin? + .form-inputs.ui.checkbox= f.input :admin + = hidden_field_tag "user[role_names][]", nil + = render "admin_user_roles", f: f + .form-actions.clearfix + = link_to 'Cancel', dunlop.users_path, class: 'ui basic button' + = f.button :submit, class: 'ui secondary button' diff --git a/app/views/dunlop/users/_scenario_permissions.html.slim b/app/views/dunlop/users/_scenario_permissions.html.slim new file mode 100644 index 0000000..ff62e98 --- /dev/null +++ b/app/views/dunlop/users/_scenario_permissions.html.slim @@ -0,0 +1,40 @@ +- if Dunlop.has_workflow? + .ui.top.attached.block.header= WorkflowInstance.model_name.human_plural + table.roles.workflow-instance-roles.ui.bottom.attached.compact.celled.table + caption= WorkflowInstance.model_name.human + tbody + tr + th Download + td= dunlop_class_ability :download, WorkflowInstance + tr + th Categorize + td= dunlop_class_ability :categorize, WorkflowInstance + tr + th Update + td= dunlop_class_ability :update, WorkflowInstance + tr + th Archive / Revive + td= dunlop_class_ability :archive, WorkflowInstance + tr + th Reset + td= dunlop_class_ability :reset, WorkflowInstance + + table.roles.workflow-step-roles.ui.bottom.attached.compact.celled.table + caption Workflow + thead + tr + th + th Read + th Manage + tbody + = hidden_field_tag "user[role_names][]", "read-class-WorkflowInstance" + tr + th= WorkflowInstanceBatch.model_name.human + td= dunlop_class_ability :read, WorkflowInstanceBatch + td= dunlop_class_ability :manage, WorkflowInstanceBatch + - WorkflowInstance.possible_workflow_step_names.each do |step_name| + - step_class = step_name.to_s.classify.constantize + tr + th= step_class.model_name.human + td= dunlop_class_ability :read, step_class + td= dunlop_class_ability :manage, step_class diff --git a/app/views/dunlop/users/_source_files_permissions.html.slim b/app/views/dunlop/users/_source_files_permissions.html.slim new file mode 100644 index 0000000..3c8cbb0 --- /dev/null +++ b/app/views/dunlop/users/_source_files_permissions.html.slim @@ -0,0 +1,15 @@ +- if Dunlop.has_source_files? + .ui.top.attached.block.header= SourceFile.model_name.human_plural + table.roles.ui.bottom.attached.compact.celled.table + thead + th= SourceFile.model_name.human + th Read + th Download + th Manage + tbody + - SourceFile.classes.each do |source_file| + tr + td= source_file.model_name.human + td= dunlop_class_ability :read, source_file + td= dunlop_class_ability :download, source_file + td= dunlop_class_ability :manage, source_file diff --git a/app/views/dunlop/users/_target_files_permissions.html.slim b/app/views/dunlop/users/_target_files_permissions.html.slim new file mode 100644 index 0000000..7e730e4 --- /dev/null +++ b/app/views/dunlop/users/_target_files_permissions.html.slim @@ -0,0 +1,15 @@ +- if Dunlop.has_target_files? + .ui.top.attached.block.header= TargetFile.model_name.human_plural + table.roles.ui.bottom.attached.compact.celled.table + thead + th= TargetFile.model_name.human + th Read + th Download + th Manage + tbody + - TargetFile.classes.each do |target_file| + tr + td= target_file.model_name.human + td= dunlop_class_ability :read, target_file + td= dunlop_class_ability :download, target_file + td= dunlop_class_ability :manage, target_file diff --git a/app/views/dunlop/users/edit.html.slim b/app/views/dunlop/users/edit.html.slim new file mode 100644 index 0000000..7e59191 --- /dev/null +++ b/app/views/dunlop/users/edit.html.slim @@ -0,0 +1,4 @@ +.ui.container + = page_title :edit, User, back: [dunlop, User] + = render 'form' + diff --git a/app/views/dunlop/users/edit_profile.html.slim b/app/views/dunlop/users/edit_profile.html.slim new file mode 100644 index 0000000..ee91b4b --- /dev/null +++ b/app/views/dunlop/users/edit_profile.html.slim @@ -0,0 +1,16 @@ +.ui.container + = page_title "Change your password" + + = simple_form_for(@user, url: dunlop.update_profile_users_path, html: { method: :patch, class: 'ui form' }) do |f| + = f.error_notification + + = f.input :reset_password_token, as: :hidden + = f.full_error :reset_password_token + + .form-inputs + = f.input :password, label: "New password", required: true + = f.input :password_confirmation, label: "Confirm your new password", required: true + + .form-actions + = link_to 'Back', :back, class: 'back ui button' + = f.button :submit, "Change my password", class: 'ui secondary button' diff --git a/app/views/dunlop/users/index.html.slim b/app/views/dunlop/users/index.html.slim new file mode 100644 index 0000000..b2849df --- /dev/null +++ b/app/views/dunlop/users/index.html.slim @@ -0,0 +1,26 @@ += page_title :index, User +p= paginate @users +table.ui.table.table-striped.celled + thead + tr + th= at :email + th= at :admin + -#%th Roles + th.actions + tbody + - @users.each do |user| + tr id="user-#{user.id}" + td= link_to user.email, [dunlop, :edit, user] + td= boolean_show user.admin + -#%td= user.roles + td.actions + - if can? :edit, user + = table_edit_link user, [dunlop, :edit, user] + - if can? :destroy, user + = table_destroy_link user, [dunlop, user] + /= link_to 'Edit', edit_admin_user_path(user), class: 'button small secondary' + - if can? :become_user, user + = link_to 'Become User', dunlop.become_user_user_path(user), method: :post, class: 'ui small button' + /= link_to 'Destroy', [:admin, user], data: { confirm: 'Are you sure?' }, method: :delete, class: 'button small alert' +.page-actions + = link_to t('action.new.label', model: User.model_name.human), dunlop.new_user_path, class: 'new ui primary button' diff --git a/app/views/dunlop/users/new.html.slim b/app/views/dunlop/users/new.html.slim new file mode 100644 index 0000000..1705f99 --- /dev/null +++ b/app/views/dunlop/users/new.html.slim @@ -0,0 +1,4 @@ +.ui.container + = page_title :new, User, back: [dunlop, User] + = render 'form' + diff --git a/app/views/dunlop/users/permissions_table.html.slim b/app/views/dunlop/users/permissions_table.html.slim new file mode 100644 index 0000000..4a3d99b --- /dev/null +++ b/app/views/dunlop/users/permissions_table.html.slim @@ -0,0 +1,48 @@ +- t_scope = 'dunlop.reports.users_permissions_table' +- permission_class = {read: 'book', manage: 'write', reset: 'undo'} += page_title t('title', scope: t_scope) +table.ui.compact.celled.table + thead + tr + th Role + - @users.each do |user| + th= link_to user.email.to_s.sub(/\@.*/, ''), [dunlop, :edit, user], title: user.email + tbody + tr + td Admin? + - @users.each do |user| + td= boolean_show user.admin? + - @table.each do |role, users| + tr + td= role + - @users.each do |user| + - if permissions = users[user] + td + - permissions.each do |p| + i class="role-icon #{p} icon #{permission_class[p]}" + - else + td= boolean_show false + +table + caption= t 'legend_title', scope: t_scope + tr + th + i.role-icon.checkmark.icon + td=t 'permission.general', scope: t_scope + tr + th + i.role-icon.read.book.icon + td=t 'permission.read', scope: t_scope + tr + th + i.role-icon.download.icon + td=t 'permission.download', scope: t_scope + tr + th + i.role-icon.manage.write.icon + td=t 'permission.manage', scope: t_scope + tr + th + i.role-icon.reset.undo.icon + td=t 'permission.reset', scope: t_scope + diff --git a/app/views/dunlop/users/profile.html.slim b/app/views/dunlop/users/profile.html.slim new file mode 100644 index 0000000..22e383f --- /dev/null +++ b/app/views/dunlop/users/profile.html.slim @@ -0,0 +1,14 @@ +.ui.container + = page_title "Profile" + table.table.table-bordered.ui.definition + tr + td Email + td= @user.email + /tr + th Roles + td= @user.role_names.join(', ') + + .page-actions + = link_to 'Back', :back, class: 'back ui button' + = link_to 'Change password', dunlop.edit_profile_users_path, class: 'ui basic yellow warning button' + diff --git a/app/views/dunlop/users/show.html.slim b/app/views/dunlop/users/show.html.slim new file mode 100644 index 0000000..4cfc624 --- /dev/null +++ b/app/views/dunlop/users/show.html.slim @@ -0,0 +1,2 @@ += page_title :show, User +div Empty diff --git a/app/views/dunlop/workflow_instance/_categorize_as_button.html.slim b/app/views/dunlop/workflow_instance/_categorize_as_button.html.slim new file mode 100644 index 0000000..e208c83 --- /dev/null +++ b/app/views/dunlop/workflow_instance/_categorize_as_button.html.slim @@ -0,0 +1,10 @@ +- if can? :categorize, record_class + .row.categorize-as-button-container + .small-6.medium-3.columns + select#categorize-as-scenario-selector + - WorkflowInstance.scenario_classes.each do |scenario| + - next if scenario == record_class + option value=scenario.scenario_name = scenario.model_name.human + .small-6.medium-3.columns.end + button#categorize-as-button data-confirmation-t=t("workflow_instance.categorize.confirmation_message") = t("workflow_instance.categorize.categorize_as") + diff --git a/app/views/dunlop/workflow_instance/_scenario_statistics.html.slim b/app/views/dunlop/workflow_instance/_scenario_statistics.html.slim new file mode 100644 index 0000000..f1b5312 --- /dev/null +++ b/app/views/dunlop/workflow_instance/_scenario_statistics.html.slim @@ -0,0 +1,18 @@ +.row + .small-12.medium-6.columns + .row + .small-12.columns.statistics-title + - if session[:archived_mode] + = link_to t('dunlop.statistics.archived_title', model: workflow_instance_class.model_name.human, count: workflow_instance_class.scope_for(current_user, archived: true).size), workflow_instance_class + - else + = link_to t('dunlop.statistics.title', model: workflow_instance_class.model_name.human, count: workflow_instance_class.scope_for(current_user).size), workflow_instance_class + .row + .small-12.columns.state= workflow_instance_state_badges_for(workflow_instance_class) + .small-12.medium-6.columns + - workflow_instance_class.workflow_step_classes.each do |workflow_step_class| + - if can? :read, workflow_step_class + .row + .small-12.columns.statistics-title= t 'dunlop.statistics.workflow_step_title', model: workflow_step_class.model_name.human + .row + .small-12.columns.state= workflow_step_state_badges_for(workflow_step_class) +br diff --git a/app/views/dunlop/workflow_instance/_statistics.html.slim b/app/views/dunlop/workflow_instance/_statistics.html.slim new file mode 100644 index 0000000..a20bb6e --- /dev/null +++ b/app/views/dunlop/workflow_instance/_statistics.html.slim @@ -0,0 +1,4 @@ +- WorkflowInstance.scenario_classes.each do |scenario_class| + - if can? :read, scenario_class + = render "dunlop/workflow_instance/scenario_statistics", workflow_instance_class: scenario_class + diff --git a/app/views/dunlop/workflow_step/_modal_renderer.js.erb b/app/views/dunlop/workflow_step/_modal_renderer.js.erb new file mode 100644 index 0000000..17f85b2 --- /dev/null +++ b/app/views/dunlop/workflow_step/_modal_renderer.js.erb @@ -0,0 +1,3 @@ +$('#workflow-step-modal .content').html('<%=j render partial_name, options %>'); +$('#workflow-step-modal').foundation('reveal', 'open'); +Dunlop.setup($('#workflow-step-modal')); diff --git a/app/views/layouts/dunlop/application.html.erb b/app/views/layouts/dunlop/application.html.erb new file mode 100644 index 0000000..0d951d8 --- /dev/null +++ b/app/views/layouts/dunlop/application.html.erb @@ -0,0 +1,14 @@ + + + + Dunlop + <%= stylesheet_link_tag "dunlop/application", media: "all" %> + <%= javascript_include_tag "dunlop/application" %> + <%= csrf_meta_tags %> + + + +<%= yield %> + + + diff --git a/bin/rails b/bin/rails new file mode 100755 index 0000000..e22bfcc --- /dev/null +++ b/bin/rails @@ -0,0 +1,12 @@ +#!/usr/bin/env ruby +# This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application. + +ENGINE_ROOT = File.expand_path('../..', __FILE__) +ENGINE_PATH = File.expand_path('../../lib/dunlop/engine', __FILE__) + +# Set up gems listed in the Gemfile. +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) + +require 'rails/all' +require 'rails/engine/commands' diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 0000000..0ae9219 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1 @@ +#NOTE: this file is only for vim-rails to recognize this as a rails project diff --git a/config/initializers/human_plural.rb b/config/initializers/human_plural.rb new file mode 100644 index 0000000..436d8c7 --- /dev/null +++ b/config/initializers/human_plural.rb @@ -0,0 +1,16 @@ +module ActiveModel + class Name + def human_plural + # Try to find the plural name of a model. If none can be found try to find a non namespaced version and return that one + default = Proc.new do |path| + if path.present? and path['/'] # non namespaced model a/b/c => a + unnamespaced_path = path.sub(/\.(\w+)\/[\w\\]+/, '.\1') + I18n.t(unnamespaced_path, default: Proc.new{ human.pluralize }) + else + human.pluralize + end + end + I18n.t("#{@klass.i18n_scope}.models.plural.#{i18n_key}", default: default ) + end + end +end diff --git a/config/locales/display-badge.en.yml b/config/locales/display-badge.en.yml new file mode 100644 index 0000000..c9ffeb1 --- /dev/null +++ b/config/locales/display-badge.en.yml @@ -0,0 +1,5 @@ +en: + dunlop: + display_badge: + collection_target_link: Show list + more_records_than_maximum: More than %{count} records will not be displayed diff --git a/config/locales/display-badge.nl.yml b/config/locales/display-badge.nl.yml new file mode 100644 index 0000000..ba47e6b --- /dev/null +++ b/config/locales/display-badge.nl.yml @@ -0,0 +1,5 @@ +nl: + dunlop: + display_badge: + collection_target_link: Toon lijst + more_records_than_maximum: Meer dan %{count} wordt niet getoond diff --git a/config/locales/dunlop.en.yml b/config/locales/dunlop.en.yml new file mode 100644 index 0000000..232b35e --- /dev/null +++ b/config/locales/dunlop.en.yml @@ -0,0 +1,5 @@ +en: + dunlop: + filter: + apply: '' + reset: '' diff --git a/config/locales/dunlop.nl.yml b/config/locales/dunlop.nl.yml new file mode 100644 index 0000000..42eccd4 --- /dev/null +++ b/config/locales/dunlop.nl.yml @@ -0,0 +1,5 @@ +nl: + dunlop: + filter: + apply: '' + reset: '' diff --git a/config/locales/duration.en.yml b/config/locales/duration.en.yml new file mode 100644 index 0000000..15c8d64 --- /dev/null +++ b/config/locales/duration.en.yml @@ -0,0 +1,19 @@ +en: + dunlop: + duration: + seconds: Second + minutes: Minute + hours: Hour + days: Day + weeks: Week + months: Month + years: Year + plural: + seconds: Seconds + minutes: Minutes + hours: Hours + days: Days + weeks: Weeks + months: Months + years: Years + diff --git a/config/locales/duration.nl.yml b/config/locales/duration.nl.yml new file mode 100644 index 0000000..e693752 --- /dev/null +++ b/config/locales/duration.nl.yml @@ -0,0 +1,19 @@ +nl: + dunlop: + duration: + seconds: seconde + minutes: minuut + hours: uur + days: dag + weeks: week + months: maand + years: jaar + plural: + seconds: secondes + minutes: minuten + hours: uren + days: dagen + weeks: weken + months: maanden + years: jaar + diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 0000000..7effb9d --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,29 @@ +en: + general: + back: Back + boolean_yes: 'Yes' + boolean_no: 'No' + collection: + empty: "There are no %{models}" + edit_button: 'Edit %{models}' + last_changed: + mixed_values: (mixed) + action: + index: + label: "Listing %{models}" + new: + label: "Add %{model}" + show: + label: "%{model}" + edit: + label: "Edit %{model}" + create: + successfull: '%{model} is successfully created' + update: + successfull: '%{model} is successfully updated' + destroy: + label: "Delete %{model}" + successfull: '%{model} is successfully deleted' + download: + label: Download + diff --git a/config/locales/models.en.yml b/config/locales/models.en.yml new file mode 100644 index 0000000..4c2dc90 --- /dev/null +++ b/config/locales/models.en.yml @@ -0,0 +1,21 @@ +en: + activerecord: + models: + user: User + execution_batch: Execution batch + workflow_instance_batch: Batch + plural: + user: Users + execution_batch: Execution batches + workflow_instance_batch: Batches + attributes: + user: + admin: Admin? + execution_batch: + sti_type: Type + state: State + created_at: Created + updated_at: Updated + info_message: Info + workflow_instance_batch: + state: State diff --git a/config/locales/models.nl.yml b/config/locales/models.nl.yml new file mode 100644 index 0000000..0b826a0 --- /dev/null +++ b/config/locales/models.nl.yml @@ -0,0 +1,21 @@ +nl: + activerecord: + models: + user: Gebruiker + execution_batch: Uitvoerings batch + workflow_instance_batch: Batch + plural: + user: Gebruiker + execution_batch: Uitvoerings batches + workflow_instance_batch: Batches + attributes: + user: + admin: Admin? + execution_batch: + sti_type: Type + state: Status + created_at: Aangemaakt + updated_at: Aangepast + info_message: Info + workflow_instance_batch: + state: Status diff --git a/config/locales/nl.yml b/config/locales/nl.yml new file mode 100644 index 0000000..6968530 --- /dev/null +++ b/config/locales/nl.yml @@ -0,0 +1,28 @@ +nl: + general: + back: Terug + boolean_yes: 'Ja' + boolean_no: 'Nee' + collection: + empty: "There are no %{models}" + edit_button: 'Bewerk %{models}' + last_changed: + mixed_values: (mixed) + action: + index: + label: "%{models} overzicht" + new: + label: "%{model} toevoegen" + show: + label: "%{model}" + edit: + label: "%{model} bewerken" + create: + successfull: '%{model} is successfully created' + update: + successfull: '%{model} is successfully updated' + destroy: + label: "%{model} verwijderen" + successfull: '%{model} is successfully destroyed' + download: + label: Download diff --git a/config/locales/reports.en.yml b/config/locales/reports.en.yml new file mode 100644 index 0000000..090a1d4 --- /dev/null +++ b/config/locales/reports.en.yml @@ -0,0 +1,37 @@ +en: + dunlop: + reports: + title: Rapportages + system: + title: Basisrapportages + workflow_instance_fields: + title: Workflow instance fields + workflow_steps_in_batch_finished: + title: Report listing actions performed when all workflow steps in a batch are in a final state + all_in_finished_state_title: When all %{workflow_steps} are in a final state + workflow_instance_workflow_steps: + title: "%{workflow_instance} workflow steps" + workflow_steps_overdue: + title: Workflow steps overdue configuration + before_date: "%{workflow_step} is overdue when not finished %{time} before %{date}" + after_date: "%{workflow_step} is overdue when not finished %{time} after %{date}" + on_date: "%{workflow_step} is overdue when not finished on %{date}" + users_permissions_table: + title: Users permissions overview + legend_title: Legend + permission: + general: General permission + read: Read + download: Download + manage: Update / Manage + reset: Reset + + statistics: + #title: "%{model} (%{count} active, %{archived_count} archived)" + title: "%{model} (%{count})" + archived_title: "%{model} (%{count})" + workflow_step_title: "%{model}" + overall: + total_count: Total + active_count: '# active' + archived_count: '# archived' diff --git a/config/locales/reports.nl.yml b/config/locales/reports.nl.yml new file mode 100644 index 0000000..0302805 --- /dev/null +++ b/config/locales/reports.nl.yml @@ -0,0 +1,37 @@ +nl: + dunlop: + reports: + title: Rapportages + system: + title: Basisrapportages + workflow_instance_fields: + title: Workflow instance fields + workflow_steps_in_batch_finished: + title: Report listing actions performed when all workflow steps in a batch are in a final state + all_in_finished_state_title: When all %{workflow_steps} are in a final state + workflow_instance_workflow_steps: + title: "%{workflow_instance} workflow steps" + workflow_steps_overdue: + title: Workflow steps overdue configuration + before_date: "%{workflow_step} is overdue als hij %{time} voor %{date} nog niet afgerond is" + after_date: "%{workflow_step} is overdue als hij %{time} na %{date} nog niet afgerond is" + on_date: "%{workflow_step} is overdue als hij na %{date} nog niet afgerond is" + users_permissions_table: + title: Users permissions overview + legend_title: Legend + permission: + general: General permission + read: Read + download: Download + manage: Update / Manage + reset: Reset + + statistics: + #title: "%{model} (%{count} active, %{archived_count} archived)" + title: "%{model} (%{count})" + archived_title: "%{model} (%{count})" + workflow_step_title: "%{model}" + overall: + total_count: Totaal + active_count: Aantal actief + archived_count: Aantal gearchiveerd diff --git a/config/locales/source_file.en.yml b/config/locales/source_file.en.yml new file mode 100644 index 0000000..978b708 --- /dev/null +++ b/config/locales/source_file.en.yml @@ -0,0 +1,18 @@ +en: + dunlop: + source_file: + start_loading: Start loading + finished_loading: Finished loading + activerecord: + models: + source_file: Source File + plural: + source_file: Source File + attributes: + source_file: + sti_type: Type + name: Name + state: State + source: Source + created_at: Created + original_file: File diff --git a/config/locales/source_file.nl.yml b/config/locales/source_file.nl.yml new file mode 100644 index 0000000..857ba62 --- /dev/null +++ b/config/locales/source_file.nl.yml @@ -0,0 +1,18 @@ +nl: + dunlop: + source_file: + start_loading: Start inladen + finished_loading: Inladen gereed + activerecord: + models: + source_file: Bronbestand + plural: + source_file: Bronbestanden + attributes: + source_file: + sti_type: Type + name: Naam + state: Status + source: Bron + created_at: Aangemaakt + original_file: File diff --git a/config/locales/state_machines.en.yml b/config/locales/state_machines.en.yml new file mode 100644 index 0000000..a9d5754 --- /dev/null +++ b/config/locales/state_machines.en.yml @@ -0,0 +1,42 @@ +en: + activerecord: + state_machines: + # Possible lookup precedence + # workflow_instance/scenario1 + # state: + # states: + # active: Actief + # workflow_instance: + # state: + # states: + # active: Actief + # workflow_instance/scenario1: + # states: + # active: Actief + # workflow_instance: + # states: + # active: Actief + # state: + # states: + # active: Actief + # states: + # active: Actief + source_file: + states: + scheduled: Scheduled + active: Active + failed: Failed + states: + uncategorized: Uncategorized + unplanned: Unplanned + planned: Planned + active: Active + overdue: Overdue + any_rejected: Any rejected + all_completed: All completed + + pending: Pending + processing: Active + rejected: Rejected + completed: Completed + diff --git a/config/locales/state_machines.nl.yml b/config/locales/state_machines.nl.yml new file mode 100644 index 0000000..0a52113 --- /dev/null +++ b/config/locales/state_machines.nl.yml @@ -0,0 +1,42 @@ +nl: + activerecord: + state_machines: + # Possible lookup precedence + # workflow_instance/scenario1 + # state: + # states: + # active: Actief + # workflow_instance: + # state: + # states: + # active: Actief + # workflow_instance/scenario1: + # states: + # active: Actief + # workflow_instance: + # states: + # active: Actief + # state: + # states: + # active: Actief + # states: + # active: Actief + source_file: + states: + scheduled: Scheduled + active: Actief + failed: Failed + states: + uncategorized: Uncategorized + unplanned: Unplanned + planned: Gepland + active: Actief + overdue: Overdue + any_rejected: Any rejected + all_completed: All completed + + pending: Pending + processing: Actief + rejected: Rejected + completed: Completed + diff --git a/config/locales/workflow-instance.en.yml b/config/locales/workflow-instance.en.yml new file mode 100644 index 0000000..d56793f --- /dev/null +++ b/config/locales/workflow-instance.en.yml @@ -0,0 +1,19 @@ +en: + workflow_instance: + update: + update: Update + categorize: + categorize_as: Categorize + confirmation_message: "Are you sure? Non present steps in %{target_scenario} will be removed." + archive: + archive: Archive + confirmation_message: Are you sure you want to archive the selection? + revive: + revive: Revive + confirmation_message: Are you sure you want to revive the selection? + reset: + reset: Reset + confirmation_message: | + Are you sure you want to reset the %{workflow_instance}? + + All the progress will be lost. The notes will remain diff --git a/config/locales/workflow-instance.nl.yml b/config/locales/workflow-instance.nl.yml new file mode 100644 index 0000000..b3781e0 --- /dev/null +++ b/config/locales/workflow-instance.nl.yml @@ -0,0 +1,20 @@ +nl: + workflow_instance: + update: + update: Aanpassen + categorize: + categorize_as: Categoriseer + confirmation_message: "Weet je dit zeker? Stappen die niet aanwezig zijn in %{target_scenario} zullen verdwijnen." + archive: + archive: Archiveer + confirmation_message: Weet je zeker dat je de selectie wil archiveren? + revive: + revive: Activeer + confirmation_message: Weet je zeker dat je de selectie wil activeren? + reset: + reset: Reset + confirmation_message: | + Are you sure you want to reset the %{workflow_instance}? + + All the progress will be lost. The notes will remain + diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000..edbae50 --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,53 @@ +Dunlop::Engine.routes.draw do + root to: 'dashboard#home' + get 'badge_info' => 'dashboard#badge_info' + get 'changelog' => 'dashboard#changelog' + post 'active_mode' => 'dashboard#active_mode', as: :active_mode + post 'archived_mode' => 'dashboard#archived_mode', as: :archived_mode + + resources :users do + member do + post :become_user + end + + collection do + get :permissions_table + get :profile + get :edit_profile + patch :update_profile + end + end + + resources :source_files, except: [:edit, :update] do + get :download, on: :member + post :schedule, on: :member + end + + resources :target_files, only: [:index, :show, :destroy] do + member do + get :download + end + + get "/download_latest/:target_file_type", action: :download_latest, on: :collection + end + + resources :execution_batches + + resources :workflow_instances do + collection do + post :categorize_as + post :archive + post :revive + post :reset + end + end + + namespace :reports do + get 'system/workflow_instance_fields' + get 'system/workflow_instance_workflow_steps' + get 'system/workflow_step_scenarios' + get 'system/workflow_steps_in_batch_finished' + get 'system/workflow_steps_overdue' + root to: 'system#index', as: 'root' + end +end diff --git a/config/spring.rb b/config/spring.rb new file mode 100644 index 0000000..6573206 --- /dev/null +++ b/config/spring.rb @@ -0,0 +1 @@ +Spring.application_root = 'spec/dummy' diff --git a/db/migrate/20171024132015_create_dunlop_file_downloads.rb b/db/migrate/20171024132015_create_dunlop_file_downloads.rb new file mode 100644 index 0000000..beb0d41 --- /dev/null +++ b/db/migrate/20171024132015_create_dunlop_file_downloads.rb @@ -0,0 +1,10 @@ +class CreateDunlopFileDownloads < ActiveRecord::Migration[5.1] + def change + create_table :dunlop_file_downloads do |t| + t.belongs_to :user + t.belongs_to :file, polymorphic: true + + t.timestamps + end + end +end diff --git a/db/migrate/20171024142354_drop_dunlop_target_file_downloads_if_exist.rb b/db/migrate/20171024142354_drop_dunlop_target_file_downloads_if_exist.rb new file mode 100644 index 0000000..ba7fb54 --- /dev/null +++ b/db/migrate/20171024142354_drop_dunlop_target_file_downloads_if_exist.rb @@ -0,0 +1,5 @@ +class DropDunlopTargetFileDownloadsIfExist < ActiveRecord::Migration[5.1] + def change + drop_table :dunlop_target_file_downloads, if_exists: true + end +end diff --git a/doc/screenshots/Selection_092.png b/doc/screenshots/Selection_092.png new file mode 100644 index 0000000..595fded Binary files /dev/null and b/doc/screenshots/Selection_092.png differ diff --git a/doc/screenshots/Selection_093.png b/doc/screenshots/Selection_093.png new file mode 100644 index 0000000..ff43517 Binary files /dev/null and b/doc/screenshots/Selection_093.png differ diff --git a/doc/screenshots/Selection_094 (copy).png b/doc/screenshots/Selection_094 (copy).png new file mode 100644 index 0000000..9d21bba Binary files /dev/null and b/doc/screenshots/Selection_094 (copy).png differ diff --git a/doc/screenshots/Selection_094.png b/doc/screenshots/Selection_094.png new file mode 100644 index 0000000..9d21bba Binary files /dev/null and b/doc/screenshots/Selection_094.png differ diff --git a/doc/screenshots/Selection_095 (copy).png b/doc/screenshots/Selection_095 (copy).png new file mode 100644 index 0000000..363208c Binary files /dev/null and b/doc/screenshots/Selection_095 (copy).png differ diff --git a/doc/screenshots/Selection_095.png b/doc/screenshots/Selection_095.png new file mode 100644 index 0000000..9e0ab6e Binary files /dev/null and b/doc/screenshots/Selection_095.png differ diff --git a/doc/screenshots/Selection_096.png b/doc/screenshots/Selection_096.png new file mode 100644 index 0000000..16d2a17 Binary files /dev/null and b/doc/screenshots/Selection_096.png differ diff --git a/doc/screenshots/Selection_097.png b/doc/screenshots/Selection_097.png new file mode 100644 index 0000000..9d0e86f Binary files /dev/null and b/doc/screenshots/Selection_097.png differ diff --git a/doc/screenshots/Selection_098.png b/doc/screenshots/Selection_098.png new file mode 100644 index 0000000..e7f24f2 Binary files /dev/null and b/doc/screenshots/Selection_098.png differ diff --git a/doc/screenshots/Selection_104 (copy).png b/doc/screenshots/Selection_104 (copy).png new file mode 100644 index 0000000..bbc7b3a Binary files /dev/null and b/doc/screenshots/Selection_104 (copy).png differ diff --git a/doc/screenshots/Selection_104.png b/doc/screenshots/Selection_104.png new file mode 100644 index 0000000..425b754 Binary files /dev/null and b/doc/screenshots/Selection_104.png differ diff --git a/doc/screenshots/Selection_105-orig.png b/doc/screenshots/Selection_105-orig.png new file mode 100644 index 0000000..93c8784 Binary files /dev/null and b/doc/screenshots/Selection_105-orig.png differ diff --git a/doc/screenshots/Selection_105.png b/doc/screenshots/Selection_105.png new file mode 100644 index 0000000..f9d0241 Binary files /dev/null and b/doc/screenshots/Selection_105.png differ diff --git a/dunlop-core.gemspec b/dunlop-core.gemspec new file mode 100644 index 0000000..0ced135 --- /dev/null +++ b/dunlop-core.gemspec @@ -0,0 +1,50 @@ +$:.push File.expand_path("../lib", __FILE__) + +# Maintain your gem's version: +require "dunlop/version" + +# Describe your gem and declare its dependencies: +Gem::Specification.new do |s| + s.name = "dunlop-core" + s.version = Dunlop::VERSION + s.authors = ["Benjamin ter Kuile"] + s.email = ["bterkuile@gmail.com"] + s.homepage = "http://mozolutions.com" + s.summary = "Workflow management and data ananylis" + s.description = "Organize business processes, do data analysis and guard your goals" + s.test_files = Dir.glob("spec/**/*") + + s.files = Dir["{app,config,db,lib,vendor}/**/*", "LICENSE", "Rakefile", "README.md"] + + s.add_dependency "rails", ">= 4.2.6" + s.add_dependency 'slim-rails' + s.add_dependency 'record_collection', '>= 0.10.4' + s.add_dependency 'request_store' + s.add_dependency 'ransack' + s.add_dependency 'responders' + s.add_dependency 'redcarpet' + s.add_dependency 'state_machines-activerecord' + s.add_dependency 'carrierwave' + s.add_dependency 'simple_form' + s.add_dependency 'devise' + s.add_dependency 'devise-token_authenticatable' + s.add_dependency 'exception_notification' + s.add_dependency 'facets' + s.add_dependency 'memoist' + s.add_dependency 'cancancan' + + # assets + s.add_dependency 'momentjs-rails' + s.add_dependency 'pickadate-rails', '~> 3.5.5.0' # 3.5.6.0 does not work with editable: true + s.add_dependency 'coffee-rails', '~> 4.1' + s.add_dependency 'js-routes' + s.add_dependency "font-awesome-rails" + s.add_dependency "sass-rails", ">= 5.0.4" + + # End user development dependencies + #s.add_dependency 'rspec-rails' + #s.add_dependency 'capybara-webkit' + #s.add_dependency 'capybara-screenshot' + #s.add_dependency 'factory_bot_rails' + #s.add_dependency 'database_cleaner' +end diff --git a/gemfiles/Gemfile.rails-4.2.x b/gemfiles/Gemfile.rails-4.2.x new file mode 100644 index 0000000..3d68806 --- /dev/null +++ b/gemfiles/Gemfile.rails-4.2.x @@ -0,0 +1,35 @@ +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 path: '../' + +gem "rails", "~> 4.2.7" + +group :development, :test do + gem 'rspec-rails' + gem 'sqlite3' + gem 'pry-rails' + gem 'pry-doc' + gem 'cancancan' + gem 'kaminari' + gem 'spring' + gem 'spring-commands-rspec' + gem 'semantic-ui-sass' +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 diff --git a/gemfiles/Gemfile.rails-5.1.x b/gemfiles/Gemfile.rails-5.1.x new file mode 100644 index 0000000..6a82ece --- /dev/null +++ b/gemfiles/Gemfile.rails-5.1.x @@ -0,0 +1,35 @@ +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 path: '../' + +gem "rails", "~> 5.1.3" + +group :development, :test do + gem 'rspec-rails' + gem 'sqlite3' + gem 'pry-rails' + gem 'pry-doc' + gem 'cancancan' + gem 'kaminari' + gem 'spring' + gem 'spring-commands-rspec' + gem 'semantic-ui-sass' +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 diff --git a/gemfiles/Gemfile.rails-master b/gemfiles/Gemfile.rails-master new file mode 100644 index 0000000..9a77459 --- /dev/null +++ b/gemfiles/Gemfile.rails-master @@ -0,0 +1,11 @@ +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 path: '../' + +gem 'rails', github: 'rails/rails' +gem 'arel', github: 'rails/arel' +gem 'rack', github: 'rack/rack' diff --git a/lib/core_extensions/activerecord/find_each_ordered.rb b/lib/core_extensions/activerecord/find_each_ordered.rb new file mode 100644 index 0000000..d1b783e --- /dev/null +++ b/lib/core_extensions/activerecord/find_each_ordered.rb @@ -0,0 +1,38 @@ +# rails' find_each and find_in_batches do not respect order on a scope +# this implementation is inspired by: +# https://github.com/telent/ar-as-batches +require 'active_record' +module ActiveRecord + module FindEachOrdered + def find_each_ordered(args={},&blk) + batch_size=args[:batch_size] || 1000 + offset=arel.offset || 0 + limit=arel.limit + if limit && (limit < batch_size) then batch_size = limit end + records = self.offset(offset).limit(batch_size).all + while records.any? + offset += records.size + records.each { |r| yield r } + if limit then + limit -= records.size + if (limit < batch_size) then batch_size = limit end + if limit.zero? then + return + end + end + records = self.offset(offset).limit(batch_size).all + end + end + end + + class Relation + include FindEachOrdered + end + + class Base + class << self + # use find_each since order is not important if called directly on class + alias_method :find_each_ordered, :find_each + end + end +end diff --git a/lib/core_extensions/date/epoch_week.rb b/lib/core_extensions/date/epoch_week.rb new file mode 100644 index 0000000..36b5f4e --- /dev/null +++ b/lib/core_extensions/date/epoch_week.rb @@ -0,0 +1,21 @@ +module CoreExtensions + module Date + module EpochWeek + EPOCH_BASE = ::Date.new(1969, 12, 29) # MONDAY!!!!! + # we're goot until 2038 + def epoch_week + return ((self - EPOCH_BASE).to_i / 7).floor + iso_string = iso8601 + result = 52 * cwyear + cweek + return result if iso_string < '2021-01-04' + result += 1 + return result if iso_string < '2027-01-04' + result += 1 + return result if iso_string < '2031-12-29' + result + 1 + end + end + end +end + +Date.include CoreExtensions::Date::EpochWeek diff --git a/lib/core_extensions/time/epoch_week.rb b/lib/core_extensions/time/epoch_week.rb new file mode 100644 index 0000000..8fa2b16 --- /dev/null +++ b/lib/core_extensions/time/epoch_week.rb @@ -0,0 +1,12 @@ +module CoreExtensions + module Time + module EpochWeek + EPOCH_BASE = -262800 # ::Time.at(0).utc.beginning_of_week # MONDAY!!!!! + def epoch_week + ((to_i + EPOCH_BASE) / 604800).floor + end + end + end +end + +Time.include CoreExtensions::Time::EpochWeek diff --git a/lib/dunlop.rb b/lib/dunlop.rb new file mode 100644 index 0000000..83578df --- /dev/null +++ b/lib/dunlop.rb @@ -0,0 +1,23 @@ +require "dunlop/engine" +require "dunlop/csv_builder" +require 'core_extensions/activerecord/find_each_ordered' +require 'core_extensions/date/epoch_week' +require 'core_extensions/time/epoch_week' + +module Dunlop + def self.table_name_prefix + 'dunlop_' + end + + def self.has_workflow? + File.exist?(Rails.root.join("app/models/workflow_instance/data_setup.rb")) + end + + def self.has_source_files? + 'SourceFile'.safe_constantize + end + + def self.has_target_files? + 'TargetFile'.safe_constantize + end +end diff --git a/lib/dunlop/README.md b/lib/dunlop/README.md new file mode 100644 index 0000000..0bfbd22 --- /dev/null +++ b/lib/dunlop/README.md @@ -0,0 +1,45 @@ +Dunlop workflow tool system +================================ + +Installation +--------------------------------------- +To use Dunlop in your project add it to your Gemfile +```ruby +gem 'dunlop' +``` +and create the project scaffold: +``` +rails generate dunlop:install +``` + +Generators +--------------------------------------- + +### source_file +To add a source file to your project use the source_file +generator: +``` +rails generate source_file planned_dslams +``` +NOTE: do a git commit before you use a generator! +Possible options for the generator are: + +* `--sql` to add scaffold sql templates to load data using sql +* `--source_record` if the data is loaded into a connected SourceRecord + model associated with the latest loaded source file. +* `--attributes="Attr 1" "Attr 2" attr_3` to list the header columns of + the source file. The attributes will be assumed to have an underscored + column name form with spaces replaced by underscores. Default column + type will be string. It is meant as a fast and good start, not a final + result. + +So to generate a full featured source file the following will work: +``` +rails generate source_file planned_dslams --sql --source_record --attributes="Attr 1" attr_etc +``` + +### dunlop:scenario +To add a scenario to your project: +``` +rails generate dunlop:scenario VulaMigration +``` diff --git a/lib/dunlop/core.rb b/lib/dunlop/core.rb new file mode 100644 index 0000000..6d56da9 --- /dev/null +++ b/lib/dunlop/core.rb @@ -0,0 +1 @@ +require "dunlop" diff --git a/lib/dunlop/csv_builder.rb b/lib/dunlop/csv_builder.rb new file mode 100644 index 0000000..1687bd7 --- /dev/null +++ b/lib/dunlop/csv_builder.rb @@ -0,0 +1,123 @@ +require "csv" +module Dunlop::CsvBuilder + extend ActiveSupport::Concern + + included do + attr_reader :resource, :options, :csv_options, :controller + class_attribute :default_csv_options + self.default_csv_options = {} + end + + def initialize(resource, options = {}) + @resource, @options = resource, options + @csv_options = default_csv_options.merge(options.slice(:col_sep, :row_sep, :encoding, :field_size_limit, :quote_char, :force_quotes)) + @controller = options[:controller] + end + + def file_name + options[:file_name] || "#{Rails.application.config.application_name}-#{date_number}-data.csv" + end + + def headers + [] + end + + def for_each_serialized_row(&block) + iterate_over = iteration_resource + iterate_over = Array.wrap(iterate_over) unless iterate_over.respond_to?(:find_each_ordered) || iterate_over.respond_to?(:each) # allow resource to be a record using the index's csv builder + iteration_method = iterate_over.respond_to?(:find_each_ordered) ? :find_each_ordered : :each + iterate_over.public_send(iteration_method) do |*args| + block.call serialize_row(*args) + end + end + + def serialize_row(record) + [record.id] + end + + def data + # use #to_csv for base_class builders if present + # in custom class implement" + # def data + # resource.to_csv + # end + # since Arrays implement #to_csv, but we do not want to use it in this case we explicitly exclude it + return resource.to_csv if self.class == CsvBuilder and resource.present? and resource.respond_to?(:to_csv) and !resource.is_a?(Array) + + # try iteration otherwise + str = CSV.generate csv_options do |csv| + csv << headers if headers.present? + for_each_serialized_row do |serialized_row| + csv << serialized_row + end + end + str + end + + def date_number + Date.current.to_s(:number) + end + + # This method can be overridden in order to use another iteration source than the resource eg: + # resouce = batch + # def iteration_resource + # resource.workflow_instances + # end + def iteration_resource + resource + end + + #TODO: this is a separate include, not part of the core CsvBuilder object + def workflow_instances_in_batch + resource.workflow_instances.for_filtered_export + end + + # Do a lookup on a record of an attribute and map the value to the + # csv required version if appliccable. At the moment booleans are + # converted to zeros and ones + def record_attribute(record, attribute_name) + result = record.public_send(attribute_name) + case result + when TrueClass then 1 + when FalseClass then 0 + when String then result.strip.gsub(/\r\n?|\n/, " ") + when DayTimeBase then result.number + else result + end + end + + # Make some options available as methods + %i[action_name controller_path].each do |option_method| + define_method option_method do + options[option_method] + end + end + + module ClassMethods + # find the csv builder for the given route spec + def find_for_route(route_spec) + find_for_route_lookup_order(route_spec).lazy.map(&:safe_constantize).find(&:itself) + end + + # separate method since this is the important method to test, so extracted from the main method + def find_for_route_lookup_order(route_spec) + controller_path, action_name = route_spec.split('#') + + controller_parts = controller_path.split('/') + if controller_parts.size < 2 + parts = controller_parts.map(&:classify) + else + parts = controller_parts[0..-2].map(&:camelize) + parts << controller_parts.last.classify + end + + # Create a lookup tree that uses all the namespaces, followed by the actoin name + # and ending with CsvBuilder for consitancy. Aka: if the route is: + # workflow_instances/scenario1s#show + # the lookup_order should be: + # ["WorkflowInstanceBatch::Scenario1::ShowCsvBuilder", "WorkflowInstanceBatch::ShowCsvBuilder"] + action_postfix = "::#{action_name.camelize}CsvBuilder" + parts.map.with_index{|p, i| parts[0..(i - 1)].join('::') + action_postfix} + end + end +end diff --git a/lib/dunlop/engine.rb b/lib/dunlop/engine.rb new file mode 100644 index 0000000..07faff2 --- /dev/null +++ b/lib/dunlop/engine.rb @@ -0,0 +1,61 @@ + +# Asset groups +if (Rails.groups.map(&:to_s) & %w[development assets test]).any? + require "font-awesome-rails" + require "sass-rails" + require "js-routes" + require "momentjs-rails" + require 'coffee-rails' + require "pickadate-rails" +end + +require "slim-rails" +require 'record_collection' +require 'request_store' +require 'ransack' +require 'devise' +require 'devise-token_authenticatable' +require 'state_machines-activerecord' +require 'carrierwave' +require 'simple_form' +require 'redcarpet' +require 'exception_notification' +require 'facets/enumerable/map_send' +require 'memoist' +require 'cancancan' + +if Rails::VERSION::MAJOR < 5 + ActiveRecord::Associations::Builder::Association.valid_options |= [:optional] + class ActiveRecord::Migration + # rails5 compatibility + def self.[](*) + self + end + end +end + +module Dunlop + class Engine < ::Rails::Engine + isolate_namespace Dunlop + initializer 'dunlop.action_controller' do |app| + ActiveSupport.on_load :action_controller do + helper Dunlop::ApplicationHelper if respond_to?(:helper) + end + end + + initializer "dunlop.ability", group: 'dunlop' do |app| + if ability_class = "Ability".safe_constantize and ability_class.respond_to?(:add_dunlop_allowed_authorization_classes!) + ability_class.add_dunlop_allowed_authorization_classes! + end + end + + # add migrations to containing application + initializer :append_migrations do |app| + unless app.root.to_s.match root.to_s + config.paths["db/migrate"].expanded.each do |expanded_path| + app.config.paths["db/migrate"] << expanded_path + end + end + end + end +end diff --git a/lib/dunlop/generators.rb b/lib/dunlop/generators.rb new file mode 100644 index 0000000..4f47963 --- /dev/null +++ b/lib/dunlop/generators.rb @@ -0,0 +1,8 @@ +module Dunlop::Generators + +end + +module Dunlop::Install +end + +require "dunlop/generators/generator_helpers" diff --git a/lib/dunlop/generators/generator_helpers.rb b/lib/dunlop/generators/generator_helpers.rb new file mode 100644 index 0000000..c7b6d88 --- /dev/null +++ b/lib/dunlop/generators/generator_helpers.rb @@ -0,0 +1,104 @@ +module Dunlop::Generators::GeneratorHelpers + extend ActiveSupport::Concern + + def inject_before_last_end(file_path, code) + code << "\n" unless code.last == "\n" + code = " #{code}" unless code.first =~ /^\s/ + inject_into_file file_path, code, before: /^end/ + end + + module ClassMethods + def next_migration_number(path) + ActiveRecord::Generators::Base.next_migration_number(path) + end + end + + class DunlopGeneratorAttribute + attr_reader :name + def initialize(spec) + @name, @type = spec.to_s.strip.split(':') + end + + def type + @type || 'string' + end + + def column_name + name.to_s.underscore.gsub(/\W/, '_') + end + + def to_s + name + end + + def form_type + case type + when 'boolean' then 'boolean' + else 'input' + end + end + + def boolean? + type == 'boolean' + end + + def integer? + type == 'integer' + end + + def float? + %w[decimal float].include?(type) + end + + def numeric? + integer? or float? + end + + def date? + type == 'date' + end + end + + private + + def attributes + options.attributes.map{|a| DunlopGeneratorAttribute.new(a) } + end + + # Taken from base rails and modified for gsub block support + def gsub_file(path, flag, *args, &block) + config = args.last.is_a?(Hash) ? args.pop : {} + + path = File.expand_path(path, destination_root) + if not File.exist? path + say_status :gsub, set_color("The file #{path} cannot be found", :yellow) + return + end + say_status :gsub, relative_to_original_destination_root(path), config.fetch(:verbose, true) + + unless options[:pretend] + content = File.binread(path) + if block.present? # modification from rails standard to support match data in supplied blocks + content.gsub!(flag, *args){ block.call( Regexp.last_match ) } + else + content.gsub!(flag, *args) + end + File.open(path, "wb") { |file| file.write(content) } + end + end + + ## + # Add a name to a definition list. eg: + # setup_secnarios %i[ + # existing_scenario + # ] + # at the bottom. + def add_to_symbol_list(file_path, setup_method, name) + if behavior == :revoke + gsub_file file_path, /(#{setup_method} %i\[.*)\n\s+#{name}(.*)\]/m, '\1\2]' + else + return if File.read(file_path) =~ /^ #{setup_method} %i\[[^\]]*\W#{name}\W[^\]]*\]/m # only add once + gsub_file file_path, /(^ #{setup_method} %i\[\n[^\]]*)]/m, "\\1 #{name}\n ]" + end + end +end diff --git a/lib/dunlop/models.rb b/lib/dunlop/models.rb new file mode 100644 index 0000000..0cde931 --- /dev/null +++ b/lib/dunlop/models.rb @@ -0,0 +1,2 @@ +module Dunlop +end diff --git a/lib/dunlop/version.rb b/lib/dunlop/version.rb new file mode 100644 index 0000000..729074c --- /dev/null +++ b/lib/dunlop/version.rb @@ -0,0 +1,3 @@ +module Dunlop + VERSION = "1.4.0" +end diff --git a/lib/generators/dunlop/execution_batch/USAGE b/lib/generators/dunlop/execution_batch/USAGE new file mode 100644 index 0000000..9b36327 --- /dev/null +++ b/lib/generators/dunlop/execution_batch/USAGE @@ -0,0 +1,8 @@ +Description: + Explain the generator + +Example: + rails generate execution_batch Thing + + This will create: + what/will/it/create diff --git a/lib/generators/dunlop/execution_batch/execution_batch_generator.rb b/lib/generators/dunlop/execution_batch/execution_batch_generator.rb new file mode 100644 index 0000000..5468022 --- /dev/null +++ b/lib/generators/dunlop/execution_batch/execution_batch_generator.rb @@ -0,0 +1,11 @@ +require 'dunlop/generators' +class Dunlop::ExecutionBatchGenerator < Rails::Generators::NamedBase + include Dunlop::Generators::GeneratorHelpers + source_root File.expand_path('../templates', __FILE__) + + def generate_model + template "model.rb.erb", "app/models/execution_batch/#{file_name}.rb" + template "model_spec.rb.erb", "spec/models/execution_batch/#{file_name}_spec.rb" + #TODO: add and revoke behavior for factories at: spec/factories/execution_batch.rb + end +end diff --git a/lib/generators/dunlop/execution_batch/templates/model.rb.erb b/lib/generators/dunlop/execution_batch/templates/model.rb.erb new file mode 100644 index 0000000..9c87743 --- /dev/null +++ b/lib/generators/dunlop/execution_batch/templates/model.rb.erb @@ -0,0 +1,6 @@ +class ExecutionBatch::<%= class_name %> < ExecutionBatch + def process_steps + raise "Please implement me" + end +end + diff --git a/lib/generators/dunlop/execution_batch/templates/model_spec.rb.erb b/lib/generators/dunlop/execution_batch/templates/model_spec.rb.erb new file mode 100644 index 0000000..6ddb3df --- /dev/null +++ b/lib/generators/dunlop/execution_batch/templates/model_spec.rb.erb @@ -0,0 +1,8 @@ +require "rails_helper" + +describe ExecutionBatch::<%= class_name %> do + it "executes without errors" do + described_class.execute + described_class.last.should_not have_log_entries /ERROR/ + end +end diff --git a/lib/generators/dunlop/install/USAGE b/lib/generators/dunlop/install/USAGE new file mode 100644 index 0000000..93211d8 --- /dev/null +++ b/lib/generators/dunlop/install/USAGE @@ -0,0 +1,8 @@ +Description: + This generator adds Dunlop logic to your project + +Example: + rails generate dunlop:install + + This will create: + All the scaffold files needed to customize your Dunlop logic diff --git a/lib/generators/dunlop/install/base/USAGE b/lib/generators/dunlop/install/base/USAGE new file mode 100644 index 0000000..2c04b60 --- /dev/null +++ b/lib/generators/dunlop/install/base/USAGE @@ -0,0 +1,8 @@ +Description: + Explain the generator + +Example: + rails generate base Thing + + This will create: + what/will/it/create diff --git a/lib/generators/dunlop/install/base/base_generator.rb b/lib/generators/dunlop/install/base/base_generator.rb new file mode 100644 index 0000000..c164d85 --- /dev/null +++ b/lib/generators/dunlop/install/base/base_generator.rb @@ -0,0 +1,139 @@ +require 'rails/generators/active_record' +require 'dunlop/generators' +class Dunlop::Install::BaseGenerator < Rails::Generators::Base + include Rails::Generators::Migration + include Dunlop::Generators::GeneratorHelpers + source_root File.expand_path('../templates', __FILE__) + + def generate_all_dependencies + generate "devise User" unless Rails.root.join('app/models/user.rb').exist? + end + + def setup_user_model + copy_file "CHANGELOG.md", "CHANGELOG.md" + generate "devise:install" unless File.exist?(Rails.root.join("config/initializers/devise.rb")) + generate "devise User" unless File.exist?(Rails.root.join("app/models/user.rb")) + #generate "simple_form:install --foundation" + generate "rspec:install" unless File.exist?(Rails.root.join('spec/rails_helper.rb')) + migration_template "migrations/add_admin_and_role_names_to_users.rb", "db/migrate/add_admin_and_role_names_to_users.rb" + user_model = Rails.root.join('app/models/user.rb') + if Rails.env != 'test' and File.exist?(user_model) # Test unlink does not work with special test destination target + File.unlink user_model + end + copy_file "models/user.rb", "app/models/user.rb" + end + + def create_basics + template "controllers/application_controller.rb", "app/controllers/application_controller.rb" + copy_file "models/application_record.rb", "app/models/application_record.rb" + copy_file "models/ability.rb", "app/models/ability.rb" + copy_file "models/execution_batch.rb", "app/models/execution_batch.rb" + directory "views/layouts", "app/views/layouts" + route 'mount Dunlop::Engine => "/dunlop"' + + inject_into_file "config/application.rb", after: "class Application < Rails::Application\n" do <<-RUBY + + config.application_name = "#{Rails.application.class.name.deconstantize}" + + config.git_version = Rails.env + config.git_version = File.read('REVISION').strip if File.exists?('REVISION') + config.git_version = File.read('OFFLINE_REVISION').strip if File.exists?('OFFLINE_REVISION') + + RUBY + end + + inject_into_file "config/environments/development.rb", after: "Rails.application.configure do\n" do <<-RUBY + + config.time_zone = 'Moscow' + config.git_version = 'develop' + + RUBY + end + + inject_into_file "config/environments/test.rb", after: "Rails.application.configure do\n" do <<-RUBY + + config.time_zone = 'Moscow' + config.git_version = 'test' + + RUBY + end + + original_application_layout = Rails.root.join('app/views/layouts/application.html.erb') + File.unlink original_application_layout if File.exist?(original_application_layout) + end + + def add_required_gems + return unless Rails.root.join('Gemfile').exist? + gem 'cancancan' + gem 'kaminari' + gem 'exception_notification' + gem_group :assets do + gem 'semantic-ui-sass' + end + + gem_group :development, :test do + gem 'pry-rails' + gem 'pry-doc' + gem 'rspec-rails' + gem 'spring' + gem 'spring-commands-rspec' + gem 'factory_bot_rails' + end + + gem_group :test do + gem 'timecop' + gem 'poltergeist' + gem 'capybara-screenshot' + gem 'database_cleaner' + end + end + + def create_migrations + migration_template "migrations/create_dunlop_log_entries.rb", "db/migrate/create_dunlop_log_entries.rb" + migration_template "migrations/add_admin_and_role_names_to_users.rb", "db/migrate/add_admin_and_role_names_to_users.rb" + migration_template "migrations/create_dunlop_execution_batches.rb", "db/migrate/create_dunlop_execution_batches.rb" + copy_file "migrations/seeds.rb", "db/seeds.rb" + end + + def setup_assets + js_manifest = Rails.root.join("app/assets/javascripts/application.js") + if File.exist?(js_manifest) + File.unlink js_manifest + copy_file "assets/javascripts/application.coffee", "app/assets/javascripts/application.coffee" + end + + css_manifest = Rails.root.join("app/assets/stylesheets/application.css") + if File.exist?(css_manifest) + File.unlink css_manifest + copy_file "assets/stylesheets/application.sass", "app/assets/stylesheets/application.sass" + end + end + + def setup_dashboard_with_home + return if File.directory?(Rails.root.join("app/views/dashboard")) + return unless yes? "Setup dashboard controller with homepage?" + copy_file "controllers/dashboard_controller.rb", "app/controllers/dashboard_controller.rb" + route "root to: 'dashboard#home'" + directory "views/dashboard", "app/views/dashboard" + copy_file "rspec/home_spec.rb", "spec/features/home_spec.rb" + end + + def create_rspec_base + copy_file "rspec/dunlop_integrity_spec.rb", "spec/dunlop_integrity_spec.rb" + template "rspec/rails_helper.rb", "spec/rails_helper.rb", force: true + copy_file "rspec/users_factory.rb", "spec/factories/users.rb" + end + + def add_exception_notification + #template "initializers/exception_notification.rb.erb", "config/initializers/exception_notification.rb" + directory 'initializers', 'config/initializers' + end + + def email + return @email if @email + default = %x[git config user.email].strip + return @email = "test@example.com" if Rails.env.test? + @email = ask("What is the developer's email?", default: default) + end + +end diff --git a/lib/generators/dunlop/install/base/templates/CHANGELOG.md b/lib/generators/dunlop/install/base/templates/CHANGELOG.md new file mode 100644 index 0000000..f3cc3a8 --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/CHANGELOG.md @@ -0,0 +1,6 @@ + +Changelog +========= + +All notable changes to this project will be documented in this file. + diff --git a/lib/generators/dunlop/install/base/templates/assets/javascripts/application.coffee b/lib/generators/dunlop/install/base/templates/assets/javascripts/application.coffee new file mode 100644 index 0000000..b36e083 --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/assets/javascripts/application.coffee @@ -0,0 +1,9 @@ +#= require jquery +#= require jquery_ujs +#= require semantic-ui +#= require dunlop-semantic +# require_tree . +$ -> + Dunlop.setup() + $('.message .close').on 'click', -> $(@).closest('.message').transition('fade') + diff --git a/lib/generators/dunlop/install/base/templates/assets/stylesheets/application.sass b/lib/generators/dunlop/install/base/templates/assets/stylesheets/application.sass new file mode 100644 index 0000000..40e538d --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/assets/stylesheets/application.sass @@ -0,0 +1,18 @@ +//= require_tree . +//= require_self +@import semantic-ui +@import dunlop-semantic-ui + +main + min-height: calc(100% - 34px) // 34px for footer inclusion + padding-top: 40px +.page-title + &.ui.header + margin-top: 12px +.footer + border-top: 1px solid #aaa + background-color: #eee + > .right.aligned.column + padding-right: 1.5rem + + diff --git a/lib/generators/dunlop/install/base/templates/controllers/application_controller.rb b/lib/generators/dunlop/install/base/templates/controllers/application_controller.rb new file mode 100644 index 0000000..8f5c146 --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/controllers/application_controller.rb @@ -0,0 +1,32 @@ +class ApplicationController < ActionController::Base + # Prevent CSRF attacks by raising an exception. + # For APIs, you may want to use :null_session instead. + protect_from_forgery with: :exception + + before_action :authenticate_user! + + #http://rails-bestpractices.com/posts/2010/08/23/fetch-current-user-in-models + before_action :set_current_user + + rescue_from CanCan::AccessDenied do + render 'application/403', status: 403 + end + + + private + + def set_current_user + request.env["exception_notifier.exception_data"] = { + current_user: current_user.try(:inspect) + } + User.current = current_user + end + + # Return an array of ids given as a param or nil if nothing is supplied + def ids_param + return nil unless ids = params[:ids].presence + @ids_param ||= (ids.is_a?(String) ? ids.split(RecordCollection.ids_separator) : Array.wrap(ids)) + end + helper_method :ids_param + +end diff --git a/lib/generators/dunlop/install/base/templates/controllers/dashboard_controller.rb b/lib/generators/dunlop/install/base/templates/controllers/dashboard_controller.rb new file mode 100644 index 0000000..7e0cfa1 --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/controllers/dashboard_controller.rb @@ -0,0 +1,7 @@ +class DashboardController < ApplicationController + + # GET / + def home + end + +end diff --git a/lib/generators/dunlop/install/base/templates/initializers/exception_notification.rb.erb b/lib/generators/dunlop/install/base/templates/initializers/exception_notification.rb.erb new file mode 100644 index 0000000..17ce00b --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/initializers/exception_notification.rb.erb @@ -0,0 +1,42 @@ +require 'exception_notification/rails' + +ExceptionNotification.configure do |config| + # Ignore additional exception types. + # ActiveRecord::RecordNotFound, AbstractController::ActionNotFound and ActionController::RoutingError are already added. + # config.ignored_exceptions += %w{ActionView::TemplateError CustomError} + + # Adds a condition to decide when an exception must be ignored or not. + # The ignore_if method can be invoked multiple times to add extra conditions. + config.ignore_if do |exception, options| + Rails.env.development? + end + + # Notifiers ================================================================= + + # Email notifier sends notifications by email. + config.add_notifier :email, { + email_prefix: "[ERROR] ", + sender_address: %{support@fourstack.nl}, + exception_recipients: %w[ <%= email %> ] + } + + # Campfire notifier sends notifications to your Campfire room. Requires 'tinder' gem. + # config.add_notifier :campfire, { + # subdomain: 'my_subdomain', + # token: 'my_token', + # room_name: 'my_room' + # } + + # HipChat notifier sends notifications to your HipChat room. Requires 'hipchat' gem. + # config.add_notifier :hipchat, { + # api_token: 'my_token', + # room_name: 'my_room' + # } + + # Webhook notifier sends notifications over HTTP protocol. Requires 'httparty' gem. + # config.add_notifier :webhook, { + # url: 'http://example.com:5555/hubot/path', + # http_method: :post + # } + +end diff --git a/lib/generators/dunlop/install/base/templates/initializers/simple_form.rb b/lib/generators/dunlop/install/base/templates/initializers/simple_form.rb new file mode 100644 index 0000000..a0d3327 --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/initializers/simple_form.rb @@ -0,0 +1,202 @@ +# Use this setup block to configure all options available in SimpleForm. +SimpleForm.setup do |config| + # Wrappers are used by the form builder to generate a + # complete input. You can remove any component from the + # wrapper, change the order or even add your own to the + # stack. The options given below are used to wrap the + # whole input. + config.wrappers :default, class: :input, hint_class: :field_with_hint, error_class: :field_with_errors do |b| + ## Extensions enabled by default + # Any of these extensions can be disabled for a + # given input by passing: `f.input EXTENSION_NAME => false`. + # You can make any of these extensions optional by + # renaming `b.use` to `b.optional`. + + # Determines whether to use HTML5 (:email, :url, ...) + # and required attributes + b.use :html5 + + # Calculates placeholders automatically from I18n + # You can also pass a string as f.input placeholder: "Placeholder" + b.use :placeholder + + ## Optional extensions + # They are disabled unless you pass `f.input EXTENSION_NAME => true` + # to the input. If so, they will retrieve the values from the model + # if any exists. If you want to enable any of those + # extensions by default, you can change `b.optional` to `b.use`. + + # Calculates maxlength from length validations for string inputs + # and/or database column lengths + b.optional :maxlength + + # Calculate minlength from length validations for string inputs + b.optional :minlength + + # Calculates pattern from format validations for string inputs + b.optional :pattern + + # Calculates min and max from length validations for numeric inputs + b.optional :min_max + + # Calculates readonly automatically from readonly attributes + b.optional :readonly + + ## Inputs + b.use :label_input + b.use :hint, wrap_with: { tag: :span, class: :hint } + b.use :error, wrap_with: { tag: :span, class: :error } + end + + # Custom Semantic Wrapper + # Values are similar to the default wrapper above, with different classes + config.wrappers :semantic, tag: 'div', class: "field", error_class: 'error', hint_class: 'with_hint' do |b| + b.use :html5 + b.use :placeholder + b.optional :maxlength + b.optional :pattern + b.optional :min_max + b.use :label_input + b.use :hint, wrap_with: { tag: 'div', class: 'hint' } + b.use :error, wrap_with: { tag: 'div', class: 'ui red pointing above label error' } + end + + config.wrappers :ui_checkbox, tag: 'div', class: "field", error_class: 'error', hint_class: 'with_hint' do |b| + b.use :html5 + b.wrapper tag: 'div', class: 'ui checkbox' do |input| + input.use :label_input + input.use :hint, wrap_with: { tag: 'div', class: 'hint' } + end + end + + config.wrappers :ui_slider_checkbox, tag: 'div', class: "field", error_class: 'error', hint_class: 'with_hint' do |b| + b.use :html5 + b.wrapper tag: 'div', class: 'ui slider checkbox' do |input| + input.use :label_input + input.use :hint, wrap_with: { tag: 'div', class: 'hint' } + end + end + + config.wrappers :ui_toggle_checkbox, tag: 'div', class: "field", error_class: 'error', hint_class: 'with_hint' do |b| + b.use :html5 + b.wrapper tag: 'div', class: 'ui toggle checkbox' do |input| + input.use :label_input + input.use :hint, wrap_with: { tag: 'div', class: 'hint' } + end + end + + # The default wrapper to be used by the FormBuilder. + # config.default_wrapper = :default + config.default_wrapper = :semantic + + # Define the way to render check boxes / radio buttons with labels. + # Defaults to :nested for bootstrap config. + # inline: input + label + # nested: label > input + config.boolean_style = :inline + + # Default class for buttons + config.button_class = 'ui primary submit button' + + # Method used to tidy up errors. Specify any Rails Array method. + # :first lists the first message for each field. + # Use :to_sentence to list all errors for each field. + config.error_method = :first + + # Default tag used for error notification helper. + config.error_notification_tag = :div + + # CSS class to add for error notification helper. + config.error_notification_class = 'alert alert-error' + + # ID to add for error notification helper. + # config.error_notification_id = nil + + # Series of attempts to detect a default label method for collection. + # config.collection_label_methods = [ :to_label, :name, :title, :to_s ] + + # Series of attempts to detect a default value method for collection. + # config.collection_value_methods = [ :id, :to_s ] + + # You can wrap a collection of radio/check boxes in a pre-defined tag, defaulting to none. + # config.collection_wrapper_tag = :div + + # You can define the class to use on all collection wrappers. Defaulting to none. + # config.collection_wrapper_class = "field" + + # You can wrap each item in a collection of radio/check boxes with a tag, + # defaulting to :span. Please note that when using :boolean_style = :nested, + # SimpleForm will force this option to be a label. + config.item_wrapper_tag = :div + + # You can define a class to use in all item wrappers. Defaulting to none. + config.item_wrapper_class = 'ui checkbox' + + # How the label text should be generated altogether with the required text. + # config.label_text = lambda { |label, required, explicit_label| "#{required} #{label}" } + config.label_text = lambda { |label, required, explicit_label| "#{label}" } + # Semantic UI has its own astrick + + # You can define the class to use on all labels. Default is nil. + # config.label_class = nil + + # You can define the class to use on all forms. Default is simple_form. + config.default_form_class = 'ui form' + + # You can define which elements should obtain additional classes + # config.generate_additional_classes_for = [:wrapper, :label, :input] + + # Whether attributes are required by default (or not). Default is true. + # config.required_by_default = true + + # Tell browsers whether to use the native HTML5 validations (novalidate form option). + # These validations are enabled in SimpleForm's internal config but disabled by default + # in this configuration, which is recommended due to some quirks from different browsers. + # To stop SimpleForm from generating the novalidate option, enabling the HTML5 validations, + # change this configuration to true. + config.browser_validations = false + + # Collection of methods to detect if a file type was given. + # config.file_methods = [ :mounted_as, :file?, :public_filename ] + + # Custom mappings for input types. This should be a hash containing a regexp + # to match as key, and the input type that will be used when the field name + # matches the regexp as value. + # config.input_mappings = { /count/ => :integer } + + # Custom wrappers for input types. This should be a hash containing an input + # type as key and the wrapper that will be used for all inputs with specified type. + # config.wrapper_mappings = { string: :prepend } + + # Namespaces where SimpleForm should look for custom input classes that + # override default inputs. + # config.custom_inputs_namespaces << "CustomInputs" + + # Default priority for time_zone inputs. + # config.time_zone_priority = nil + + # Default priority for country inputs. + # config.country_priority = nil + + # When false, do not use translations for labels. + # config.translate_labels = true + + # Automatically discover new inputs in Rails' autoload path. + # config.inputs_discovery = true + + # Cache SimpleForm inputs discovery + # config.cache_discovery = !Rails.env.development? + + # Default class for inputs + # config.input_class = nil + + # Define the default class of the input wrapper of the boolean input. + config.boolean_label_class = 'checkbox' + + # Defines if the default input wrapper class should be included in radio + # collection wrappers. + # config.include_default_input_wrapper_class = true + + # Defines which i18n scope will be used in Simple Form. + # config.i18n_scope = 'simple_form' +end diff --git a/lib/generators/dunlop/install/base/templates/migrations/add_admin_and_role_names_to_users.rb b/lib/generators/dunlop/install/base/templates/migrations/add_admin_and_role_names_to_users.rb new file mode 100644 index 0000000..1af97ac --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/migrations/add_admin_and_role_names_to_users.rb @@ -0,0 +1,13 @@ +class AddAdminAndRoleNamesToUsers < ActiveRecord::Migration[5.1] + def change + add_column :users, :admin, :boolean, default: false, null: false + add_column :users, :role_names, :text + add_column :users, :role_names_admin, :text + add_column :users, :settings_storage, :text + add_column :users, :authentication_token, :string + add_column :users, :authentication_token_created_at, :datetime + add_column :users, :nickname, :string + add_index :users, :authentication_token + end +end + diff --git a/lib/generators/dunlop/install/base/templates/migrations/create_dunlop_execution_batches.rb b/lib/generators/dunlop/install/base/templates/migrations/create_dunlop_execution_batches.rb new file mode 100644 index 0000000..ab650ca --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/migrations/create_dunlop_execution_batches.rb @@ -0,0 +1,13 @@ +class CreateDunlopExecutionBatches < ActiveRecord::Migration[5.1] + def change + create_table :execution_batches do |t| + t.string :state + t.string :sti_type + t.string :identifier + t.string :info_message + + t.timestamps null: false + end + add_index :execution_batches, :sti_type + end +end diff --git a/lib/generators/dunlop/install/base/templates/migrations/create_dunlop_log_entries.rb b/lib/generators/dunlop/install/base/templates/migrations/create_dunlop_log_entries.rb new file mode 100644 index 0000000..1ca3d6c --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/migrations/create_dunlop_log_entries.rb @@ -0,0 +1,14 @@ +class CreateDunlopLogEntries < ActiveRecord::Migration[5.1] + def change + create_table :dunlop_log_entries do |t| + t.integer :loggable_id + t.string :loggable_type + t.text :body + t.text :backtrace + t.datetime :created_at + t.datetime :updated_at + end + + add_index :dunlop_log_entries, [:loggable_id, :loggable_type] + end +end diff --git a/lib/generators/dunlop/install/base/templates/migrations/seeds.rb b/lib/generators/dunlop/install/base/templates/migrations/seeds.rb new file mode 100644 index 0000000..b1df734 --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/migrations/seeds.rb @@ -0,0 +1,5 @@ +unless User.find_by(email: 'admin@example.com').present? + puts "Creating admin" + User.create!(email: "admin@example.com", password: "admin123", admin: true) +end + diff --git a/lib/generators/dunlop/install/base/templates/models/ability.rb b/lib/generators/dunlop/install/base/templates/models/ability.rb new file mode 100644 index 0000000..3351628 --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/models/ability.rb @@ -0,0 +1,7 @@ +class Ability + include Dunlop::Ability + + def setup_abilities + setup_dunlop_abilities + end +end diff --git a/lib/generators/dunlop/install/base/templates/models/application_record.rb b/lib/generators/dunlop/install/base/templates/models/application_record.rb new file mode 100644 index 0000000..d4f6b82 --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/models/application_record.rb @@ -0,0 +1,4 @@ +class ApplicationRecord < ActiveRecord::Base + include Dunlop::ApplicationRecordAdditions +end + diff --git a/lib/generators/dunlop/install/base/templates/models/execution_batch.rb b/lib/generators/dunlop/install/base/templates/models/execution_batch.rb new file mode 100644 index 0000000..6805c19 --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/models/execution_batch.rb @@ -0,0 +1,3 @@ +class ExecutionBatch < ApplicationRecord + include Dunlop::ExecutionBatchModel +end diff --git a/lib/generators/dunlop/install/base/templates/models/user.rb b/lib/generators/dunlop/install/base/templates/models/user.rb new file mode 100644 index 0000000..7006d87 --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/models/user.rb @@ -0,0 +1,11 @@ +class User < ApplicationRecord + include Dunlop::UserModel + + # Include default devise modules. Others available are: + # :confirmable, :lockable, :timeoutable and :omniauthable + devise :database_authenticatable, #:token_authenticatable, #:registerable, + :recoverable, :rememberable, :trackable, :validatable + + # To be overridden, with application logic + scope :active, ->{ all } +end diff --git a/lib/generators/dunlop/install/base/templates/rspec/dunlop_integrity_spec.rb b/lib/generators/dunlop/install/base/templates/rspec/dunlop_integrity_spec.rb new file mode 100644 index 0000000..f252278 --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/rspec/dunlop_integrity_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +describe Rails.application do + it_behaves_like "a dunlop application" +end diff --git a/lib/generators/dunlop/install/base/templates/rspec/home_spec.rb b/lib/generators/dunlop/install/base/templates/rspec/home_spec.rb new file mode 100644 index 0000000..0429cda --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/rspec/home_spec.rb @@ -0,0 +1,16 @@ +require 'rails_helper' + +RSpec.feature 'Home page' do + background { i_am_logged_in_as_admin } + scenario "there are no javascript error messages", js: true do + visit '/' + case page.driver.class.name.underscore + when /webkit/ + expect( page.driver.error_messages ).to be_empty + when /poltergeist/ + # nothing, js_errors: true will raise ruby error + end + page.should have_content Rails.application.config.application_name + end +end + diff --git a/lib/generators/dunlop/install/base/templates/rspec/rails_helper.rb b/lib/generators/dunlop/install/base/templates/rspec/rails_helper.rb new file mode 100644 index 0000000..3bd4cec --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/rspec/rails_helper.rb @@ -0,0 +1,85 @@ +# This file is copied to spec/ when you run 'rails generate rspec:install' +ENV['RAILS_ENV'] ||= 'test' +require File.expand_path('../../config/environment', __FILE__) +# Prevent database truncation if the environment is production +abort("The Rails environment is running in production mode!") if Rails.env.production? +require 'spec_helper' +require 'rspec/rails' +require 'rspec/dunlop' +require 'capybara/poltergeist' + +# Add additional requires below this line. Rails is not loaded until this point! + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } + +# Checks for pending migration and applies them before tests are run. +# If you are not using ActiveRecord, you can remove this line. +ActiveRecord::Migration.maintain_test_schema! + +Capybara.javascript_driver = :poltergeist +Capybara::Screenshot.webkit_options = { width: 1024, height: 768 } +#Capybara::Webkit.configure do |config| +# config.allow_url("www.example.com") +#end + +RSpec.configure do |config| + config.expect_with(:rspec) { |c| c.syntax = [:should, :expect] } + + config.include FactoryBot::Syntax::Methods + config.include Rspec::Dunlop::GeneralHelpers + config.include Rspec::Dunlop::FeatureHelpers, type: :feature + config.include Warden::Test::Helpers + + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_path = "#{::Rails.root}/spec/fixtures" + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = false + + config.infer_spec_type_from_file_location! + + config.before :suite do + DatabaseCleaner.clean_with(:truncation) + Dunlop::NestedLogger.options[:yaml_options][:line_width] = 2000 + Warden.test_mode! + end + + config.before :each do |example| + ActionMailer::Base.deliveries.clear + Dunlop::NestedLogger.reset + #Delayed::Worker.delay_jobs = false + + if example.metadata.slice(:non_transactional, :js).values.any? + # DatabaseCleaner.strategy = :deletion + # DatabaseCleaner.strategy = :truncation, {pre_count: true, reset_ids: false} + DatabaseCleaner.strategy = :truncation + else + DatabaseCleaner.strategy = :transaction + end + DatabaseCleaner.start + end + + config.append_after :each do + DatabaseCleaner.clean + end + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") +end diff --git a/lib/generators/dunlop/install/base/templates/rspec/users_factory.rb b/lib/generators/dunlop/install/base/templates/rspec/users_factory.rb new file mode 100644 index 0000000..03f05e6 --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/rspec/users_factory.rb @@ -0,0 +1,20 @@ +FactoryBot.define do + + sequence :email do |n| + "user#{n}@example.com" + end + + factory :user do + email + password 'password' + role_names %w[read-class-WorkflowInstance] + end + + factory :admin, class: User do + email 'admin@example.com' + password 'password' + admin true + end + +end + diff --git a/lib/generators/dunlop/install/base/templates/views/dashboard/home.html.slim b/lib/generators/dunlop/install/base/templates/views/dashboard/home.html.slim new file mode 100644 index 0000000..d6481f6 --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/views/dashboard/home.html.slim @@ -0,0 +1,4 @@ += page_title application_name +/- WorkflowInstance.scenario_classes.each do |scenario_class| + - if can? :read, scenario_class + = render "workflow_instance/statistics", workflow_instance_class: scenario_class diff --git a/lib/generators/dunlop/install/base/templates/views/devise/confirmations/new.html.haml b/lib/generators/dunlop/install/base/templates/views/devise/confirmations/new.html.haml new file mode 100644 index 0000000..3c03002 --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/views/devise/confirmations/new.html.haml @@ -0,0 +1,8 @@ +.ui.grid.two.column.page + .column + %h1 Resend confirmation instructions + = simple_form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: {method: :post}) do |f| + = f.input :email, required: true, autofocus: true + = f.button :submit, 'Resend confirmation instructions' + .column + = render "devise/shared/links" \ No newline at end of file diff --git a/lib/generators/dunlop/install/base/templates/views/devise/passwords/edit.html.slim b/lib/generators/dunlop/install/base/templates/views/devise/passwords/edit.html.slim new file mode 100644 index 0000000..fbea4c4 --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/views/devise/passwords/edit.html.slim @@ -0,0 +1,11 @@ +.ui.grid.two.column.page + .column + h1.page-title.ui.header Change your password + = simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: {method: :put}) do |f| + = f.hidden_field :reset_password_token + = f.input :password, required: true, autofocus: true + = f.input :password_confirmation, required: true + = f.button :submit, 'Change my password' + + .column + = render "devise/shared/links" diff --git a/lib/generators/dunlop/install/base/templates/views/devise/passwords/new.html.slim b/lib/generators/dunlop/install/base/templates/views/devise/passwords/new.html.slim new file mode 100644 index 0000000..de3cbb6 --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/views/devise/passwords/new.html.slim @@ -0,0 +1,8 @@ +.ui.grid.two.column.page + .column + h1.page-title.ui.header Forgot your password? + = simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| + = f.input :email, required: true, autofocus: true + = f.button :submit, 'Send me reset password instructions' + .column + = render "devise/shared/links" diff --git a/lib/generators/dunlop/install/base/templates/views/devise/registrations/_form.html.haml b/lib/generators/dunlop/install/base/templates/views/devise/registrations/_form.html.haml new file mode 100644 index 0000000..e2e55e5 --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/views/devise/registrations/_form.html.haml @@ -0,0 +1,19 @@ +%h1= t('account.edit') + += simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| + = f.input :first_name, :autofocus => true, :required => true + = f.input :last_name, :required => true + + - if current_user.unconfirmed_email? + %p + We've sent an email to #{current_user[:unconfirmed_email]} please check your inbox and spam folders and confirm your email address + - else + = f.input :email, :required => true + + - if current_user.password_required? + = f.input :password, :autocomplete => "off", :hint => "leave it blank if you don't want to change it", :required => false + = f.input :password_confirmation, :required => false + = f.input :current_password, :hint => "we need your current password to confirm your changes", :required => true + + .field + = f.button :submit, 'Update' \ No newline at end of file diff --git a/lib/generators/dunlop/install/base/templates/views/devise/registrations/edit.html.haml b/lib/generators/dunlop/install/base/templates/views/devise/registrations/edit.html.haml new file mode 100644 index 0000000..4d0d737 --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/views/devise/registrations/edit.html.haml @@ -0,0 +1,18 @@ +.ui.grid.two.column.page + .column + = render 'devise/registrations/form' + .column + %h2 Quick Links + + .ui.bulleted.list + - if devise_mapping.confirmable? && controller_name != 'confirmations' + = link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name), class: 'item' + - if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' + = link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name), class: 'item' + - if devise_mapping.omniauthable? + - resource_class.omniauth_providers.each do |provider| + = link_to "Connect your #{provider.to_s.titleize} account", omniauth_authorize_path(resource_name, provider), class: 'item' + + %h2 Cancel my account + %p + Unhappy? #{link_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete}. \ No newline at end of file diff --git a/lib/generators/dunlop/install/base/templates/views/devise/registrations/new.html.haml b/lib/generators/dunlop/install/base/templates/views/devise/registrations/new.html.haml new file mode 100644 index 0000000..2689073 --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/views/devise/registrations/new.html.haml @@ -0,0 +1,12 @@ +.ui.grid.two.column.page + .column + %h1 Sign Up + = simple_form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| + = f.input :first_name, autofocus: true + = f.input :last_name + = f.input :email, required: true + = f.input :password, required: true + = f.input :password_confirmation, required: true + = f.button :submit, 'Sign up' + .column + = render "devise/shared/links" \ No newline at end of file diff --git a/lib/generators/dunlop/install/base/templates/views/devise/sessions/new.html.slim b/lib/generators/dunlop/install/base/templates/views/devise/sessions/new.html.slim new file mode 100644 index 0000000..18234c4 --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/views/devise/sessions/new.html.slim @@ -0,0 +1,10 @@ +.ui.grid.two.column.page + .column + h1.page-title.ui.header Sign in + = simple_form_for(resource, as: resource_name, url: session_path(resource_name), html: { id: 'login-user' }) do |f| + = f.input :email, autofocus: true + = f.input :password + = f.input :remember_me, as: :boolean, wrapper: :ui_checkbox if devise_mapping.rememberable? + = f.button :submit, "Sign in" + .column + = render "devise/shared/links" diff --git a/lib/generators/dunlop/install/base/templates/views/devise/shared/_links.html.slim b/lib/generators/dunlop/install/base/templates/views/devise/shared/_links.html.slim new file mode 100644 index 0000000..90a01eb --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/views/devise/shared/_links.html.slim @@ -0,0 +1,16 @@ +h2.page-title.ui.header Quick Links + +.ui.bulleted.list + - if controller_name != 'sessions' + = link_to "Sign in", new_session_path(resource_name), class: 'item' + - if devise_mapping.registerable? && controller_name != 'registrations' + = link_to "Sign up", new_registration_path(resource_name), class: 'item' + - if devise_mapping.recoverable? && controller_name != 'passwords' + = link_to "Forgot your password?", new_password_path(resource_name), class: 'item' + - if devise_mapping.confirmable? && controller_name != 'confirmations' + = link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name), class: 'item' + - if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' + = link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name), class: 'item' + - if devise_mapping.omniauthable? + - resource_class.omniauth_providers.each do |provider| + = link_to "Sign in with #{provider.to_s.titleize}", omniauth_authorize_path(resource_name, provider), class: 'item' diff --git a/lib/generators/dunlop/install/base/templates/views/devise/unlocks/new.html.haml b/lib/generators/dunlop/install/base/templates/views/devise/unlocks/new.html.haml new file mode 100644 index 0000000..9e6570b --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/views/devise/unlocks/new.html.haml @@ -0,0 +1,5 @@ +%h2 Resend unlock instructions += simple_form_for(resource, :as => resource_name, :url => unlock_path(resource_name), :html => {:method => :post , :class => 'form-vertical' }) do |f| + = f.input :email, :required => true, :autofocus => true + = f.button :submit, 'Resend unlock instructions', :class => 'btn-primary' += render "devise/shared/links" diff --git a/lib/generators/dunlop/install/base/templates/views/layouts/_messages.html.slim b/lib/generators/dunlop/install/base/templates/views/layouts/_messages.html.slim new file mode 100644 index 0000000..0bec9b1 --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/views/layouts/_messages.html.slim @@ -0,0 +1,9 @@ +/ foundation - flash.each do |name, msg| + - if msg.is_a?(String) + div.alert-box.round class="#{name.to_s == 'notice' ? 'success' : 'alert'}" data-alert="" + = content_tag :div, msg + a.close href="#" × +- flash.each do |key, value| + .closable class=flash_class(key) + i.close.icon + = value diff --git a/lib/generators/dunlop/install/base/templates/views/layouts/_navigation.html.slim b/lib/generators/dunlop/install/base/templates/views/layouts/_navigation.html.slim new file mode 100644 index 0000000..e546680 --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/views/layouts/_navigation.html.slim @@ -0,0 +1,40 @@ +/ foundation nav.top-bar data-topbar=true + ul.title-area + li.name= link_to application_name, main_app.root_path + li.toggle-topbar.menu-icon: a href="#": span Menu + .top-bar-section= render 'layouts/navigation_links' +.ui.inverted.menu.fixed + /.ui.container + = link_to application_name, main_app.root_path, class: 'header item' + - if current_user.present? + = link_to 'App', '/panda/app/', class: 'item' + = link_to SourceFile.model_name.human_plural, main_app.source_files_path, class: ['item', active_class('source_files')] + /= link-to 'work-order' class='item' + span= t 'work_order.title' + /= link-to 'wholesale-list' class='item' + span= t 'wholesale_list.title' + .ui.dropdown.item + span Adapters + i.dropdown.icon + .menu + - Panda.adapters.each do |adapter| + = link_to adapter.title, adapter.root_path, class: 'item' + + .right.menu + - if current_user.present? + .ui.dropdown.item + span= current_user.nickname + i.dropdown.icon + .menu + /= link-to 'user.profile' class='item' + i.user.icon + span.text= t 'user.profile' + /= link-to 'manual' class='item' + i.book.icon + span.text= t 'manual.title' + = link_to main_app.destroy_user_session_path, method: :delete, class: 'item' + i.toggle.right.icon + span.text= t 'user.sign_out' + - else + = link_to 'Login', main_app.new_user_session_path, class: 'item' + diff --git a/lib/generators/dunlop/install/base/templates/views/layouts/_navigation_links.html.slim b/lib/generators/dunlop/install/base/templates/views/layouts/_navigation_links.html.slim new file mode 100644 index 0000000..5a91447 --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/views/layouts/_navigation_links.html.slim @@ -0,0 +1,39 @@ +- if signed_in? + ul.right + li class=active_class('dunlop#changelog') + = link_to Rails.application.config.git_version, dunlop.changelog_path, title: 'Current version, click for changelog' + li class=active_class('dunlop/users#profile', 'dunlop/users#edit_profile') = link_to current_user.email.to_s.sub(/\@.*/, ''), dunlop.profile_users_path + li= link_to 'Sign out', main_app.destroy_user_session_path, method: :delete + ul.left + - if can? :index, WorkflowInstance + li.has-dropdown class=active_class('workflow_instance') + = link_to WorkflowInstance.model_name.human_plural, '#' + ul.dropdown + - WorkflowInstance.scenario_classes.each do |scenario| + - if can? :read, scenario + li class=active_class("workflow_instance/#{scenario.process_name.pluralize}") = link_to scenario.model_name.human_plural, [main_app, scenario] + - if can? :index, WorkflowInstanceBatch + li.has-dropdown class=active_class('workflow_instance_batch') + = link_to WorkflowInstanceBatch.model_name.human_plural, '#' + ul.dropdown + - WorkflowInstance.scenario_classes.each do |scenario| + - if can? :read, scenario.batch_class + li class=active_class("workflow_instance_batch/#{scenario.batch_class.process_name.pluralize}") = link_to scenario.batch_class.model_name.human_plural, [main_app, scenario.batch_class] + /- if can? :index, SourceFile or can? :index, TargetFile + li.has-dropdown class=active_class('dunlop/source_files', 'dunlop/target_files') + = link_to 'Files', '#' + ul.dropdown + - if can? :index, SourceFile + li class=active_class('dunlop/source_files') = link_to SourceFile.model_name.human_plural, dunlop.source_files_path + - if can? :index, TargetFile + li class=active_class('dunlop/target_files') = link_to TargetFile.model_name.human_plural, dunlop.target_files_path + - if can? :index, User + li.has-dropdown class=active_class("dunlop/users", except: /profile$/) + = link_to 'Admin', '#' + ul.dropdown + li class=active_class('dunlop/users', except: /profile$/) = link_to User.model_name.human_plural, dunlop.users_path + /li{ class: active_class('manual') }= link_to 'Manual', manual_root_path +- else + ul.right + li= link_to 'Sign In', main_app.new_user_session_path + diff --git a/lib/generators/dunlop/install/base/templates/views/layouts/application.html.slim b/lib/generators/dunlop/install/base/templates/views/layouts/application.html.slim new file mode 100644 index 0000000..ed00dee --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/views/layouts/application.html.slim @@ -0,0 +1,21 @@ +doctype html +html + head + meta name="viewport" content="width=device-width, initial-scale=1.0" + title= content_for?(:title) ? yield(:title) : application_name + meta name="description" content="#{content_for?(:description) ? yield(:description) : 'Application'}" + = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true + = javascript_include_tag 'application', 'data-turbolinks-track' => true + = csrf_meta_tags + body class=body_classes + header= render 'layouts/navigation' + .environment-ribbon class=Rails.env + span= Rails.env + main role="main" + = render 'layouts/messages' + = yield + .ui.two.column.footer.grid + .column + .right.aligned.column + span Powered by + = link_to 'FourStack B.V.', 'http://fourstack.nl/', target: :_blank diff --git a/lib/generators/dunlop/install/base/templates/views/pages/changelog.html.slim b/lib/generators/dunlop/install/base/templates/views/pages/changelog.html.slim new file mode 100644 index 0000000..dfca766 --- /dev/null +++ b/lib/generators/dunlop/install/base/templates/views/pages/changelog.html.slim @@ -0,0 +1,3 @@ +.ui.container + markdown: + #{File.read Rails.root.join('CHANGELOG.md')} diff --git a/lib/generators/dunlop/install/csv/USAGE b/lib/generators/dunlop/install/csv/USAGE new file mode 100644 index 0000000..e7a63f5 --- /dev/null +++ b/lib/generators/dunlop/install/csv/USAGE @@ -0,0 +1,8 @@ +Description: + Explain the generator + +Example: + rails generate csv Thing + + This will create: + what/will/it/create diff --git a/lib/generators/dunlop/install/csv/csv_generator.rb b/lib/generators/dunlop/install/csv/csv_generator.rb new file mode 100644 index 0000000..e32427d --- /dev/null +++ b/lib/generators/dunlop/install/csv/csv_generator.rb @@ -0,0 +1,10 @@ +require 'dunlop/generators' +class Dunlop::Install::CsvGenerator < Rails::Generators::Base + include Dunlop::Generators::GeneratorHelpers + source_root File.expand_path('../templates', __FILE__) + + def setup_csv_builders + copy_file "initializers/csv.rb", "config/initializers/csv.rb" + directory "csv_builders", "app/csv_builders" + end +end diff --git a/lib/generators/dunlop/install/csv/templates/csv_builders/csv_builder.rb b/lib/generators/dunlop/install/csv/templates/csv_builders/csv_builder.rb new file mode 100644 index 0000000..b4c427c --- /dev/null +++ b/lib/generators/dunlop/install/csv/templates/csv_builders/csv_builder.rb @@ -0,0 +1,3 @@ +class CsvBuilder + include Dunlop::CsvBuilder +end diff --git a/lib/generators/dunlop/install/csv/templates/initializers/csv.rb b/lib/generators/dunlop/install/csv/templates/initializers/csv.rb new file mode 100644 index 0000000..c8f1704 --- /dev/null +++ b/lib/generators/dunlop/install/csv/templates/initializers/csv.rb @@ -0,0 +1,8 @@ +ActionController::Renderers.add :csv do |resource, options, *rest| + builder_class = options[:builder] || CsvBuilder.find_for_route("#{controller_path}##{action_name}") + + raise "Cannot find the Csv builder for controller: #{controller_name} and action #{action_name}" unless builder_class.present? + + builder = builder_class.new(resource, options.merge(action_name: action_name, controller_path: controller_path, controller: self)) + send_data builder.data, type: :csv, disposition: "attachment; filename=#{builder.file_name}" +end diff --git a/lib/generators/dunlop/install/install_generator.rb b/lib/generators/dunlop/install/install_generator.rb new file mode 100644 index 0000000..1e6478e --- /dev/null +++ b/lib/generators/dunlop/install/install_generator.rb @@ -0,0 +1,15 @@ +require 'rails/generators/active_record' +require 'dunlop/generators' +class Dunlop::InstallGenerator < Rails::Generators::Base + include Rails::Generators::Migration + include Dunlop::Generators::GeneratorHelpers + source_root File.expand_path('../templates', __FILE__) + + def generate_all_dunlop_modules + generate "dunlop:install:base" + generate "dunlop:install:source_files" + generate "dunlop:install:csv" + generate "dunlop:install:target_files" # requires source_file (configuration installer) + generate "dunlop:install:workflow" + end +end diff --git a/lib/generators/dunlop/install/source_files/USAGE b/lib/generators/dunlop/install/source_files/USAGE new file mode 100644 index 0000000..3b24531 --- /dev/null +++ b/lib/generators/dunlop/install/source_files/USAGE @@ -0,0 +1,8 @@ +Description: + Explain the generator + +Example: + rails generate source_files Thing + + This will create: + what/will/it/create diff --git a/lib/generators/dunlop/install/source_files/source_files_generator.rb b/lib/generators/dunlop/install/source_files/source_files_generator.rb new file mode 100644 index 0000000..d711b79 --- /dev/null +++ b/lib/generators/dunlop/install/source_files/source_files_generator.rb @@ -0,0 +1,38 @@ +require 'rails/generators/active_record' +require 'dunlop/generators' +class Dunlop::Install::SourceFilesGenerator < Rails::Generators::Base + include Rails::Generators::Migration + include Dunlop::Generators::GeneratorHelpers + source_root File.expand_path('../templates', __FILE__) + + def setup_source_files_with_other_name + copy_file "rspec/source_files_factory.rb", "spec/factories/source_files.rb" + copy_file "models/source_file.rb", "app/models/source_file.rb" + + FileUtils.mkdir_p Rails.root.join("storage") + inject_into_file "config/environments/development.rb", after: "class Application < Rails::Application\n" do <<-RUBY + config.file_storage_path = Rails.root.join('storage') # source file storage + config.working_path = '/tmp' # source file processing directory + + RUBY + end + + FileUtils.mkdir_p Rails.root.join("tmp/storage") + inject_into_file "config/environments/test.rb", after: "class Application < Rails::Application\n" do <<-RUBY + config.file_storage_path = Rails.root.join('tmp/storage') # source file storage + config.working_path = '/tmp' # source file processing directory + + RUBY + end + + inject_into_file "config/environments/production.rb", after: "class Application < Rails::Application\n" do <<-RUBY + config.file_storage_path = File.readlink(Rails.root.join('storage')) # source file storage + config.working_path = '/tmp' # source file processing directory + + RUBY + end + + migration_template "migrations/create_source_files.rb", "db/migrate/create_source_files.rb" + directory "locales", "config/locales" + end +end diff --git a/lib/generators/dunlop/install/source_files/templates/locales/source_files.yml b/lib/generators/dunlop/install/source_files/templates/locales/source_files.yml new file mode 100644 index 0000000..3e73005 --- /dev/null +++ b/lib/generators/dunlop/install/source_files/templates/locales/source_files.yml @@ -0,0 +1,10 @@ +en: + activerecord: + models: + source_file: Source file + plural: + source_file: Source files + attributes: + source_file: + sti_type: Type + original_file: File diff --git a/lib/generators/dunlop/install/source_files/templates/migrations/create_source_files.rb b/lib/generators/dunlop/install/source_files/templates/migrations/create_source_files.rb new file mode 100644 index 0000000..17fae27 --- /dev/null +++ b/lib/generators/dunlop/install/source_files/templates/migrations/create_source_files.rb @@ -0,0 +1,23 @@ +class CreateSourceFiles < ActiveRecord::Migration[5.1] + def change + create_table :source_files do |t| + t.belongs_to :creator, polymorphic: true + t.string :sti_type + t.string :state + t.string :description + t.integer :number_of_records + t.integer :duration + t.string :original_file + t.date :current_at_day + t.string :original_file_file_name + t.string :original_file_content_type + t.bigint :original_file_file_size + t.datetime :original_file_updated_at + + t.timestamps null: false + end + + add_index :source_files, :sti_type + end +end + diff --git a/lib/generators/dunlop/install/source_files/templates/models/source_file.rb b/lib/generators/dunlop/install/source_files/templates/models/source_file.rb new file mode 100644 index 0000000..a961d73 --- /dev/null +++ b/lib/generators/dunlop/install/source_files/templates/models/source_file.rb @@ -0,0 +1,8 @@ +class SourceFile < ApplicationRecord + include Dunlop::SourceFileModel + + setup_source_files %i[ + ] + + activate_dunlop! +end diff --git a/lib/generators/dunlop/install/source_files/templates/rspec/source_files_factory.rb b/lib/generators/dunlop/install/source_files/templates/rspec/source_files_factory.rb new file mode 100644 index 0000000..aa504b5 --- /dev/null +++ b/lib/generators/dunlop/install/source_files/templates/rspec/source_files_factory.rb @@ -0,0 +1,3 @@ +FactoryBot.define do + factory :source_file +end diff --git a/lib/generators/dunlop/install/target_files/USAGE b/lib/generators/dunlop/install/target_files/USAGE new file mode 100644 index 0000000..9bec34a --- /dev/null +++ b/lib/generators/dunlop/install/target_files/USAGE @@ -0,0 +1,8 @@ +Description: + Explain the generator + +Example: + rails generate dunlop:install:target_files + + This will create: + what/will/it/create diff --git a/lib/generators/dunlop/install/target_files/target_files_generator.rb b/lib/generators/dunlop/install/target_files/target_files_generator.rb new file mode 100644 index 0000000..0d28f69 --- /dev/null +++ b/lib/generators/dunlop/install/target_files/target_files_generator.rb @@ -0,0 +1,14 @@ +require 'rails/generators/active_record' +require 'dunlop/generators' +class Dunlop::Install::TargetFilesGenerator < Rails::Generators::Base + include Rails::Generators::Migration + include Dunlop::Generators::GeneratorHelpers + source_root File.expand_path('../templates', __FILE__) + + def setup_target_files + copy_file "rspec/target_files_factory.rb", "spec/factories/target_files.rb" + copy_file "models/target_file.rb", "app/models/target_file.rb" + migration_template "migrations/create_target_files.rb", "db/migrate/create_target_files.rb" + directory "locales", "config/locales" + end +end diff --git a/lib/generators/dunlop/install/target_files/templates/locales/target_files.yml b/lib/generators/dunlop/install/target_files/templates/locales/target_files.yml new file mode 100644 index 0000000..1272c05 --- /dev/null +++ b/lib/generators/dunlop/install/target_files/templates/locales/target_files.yml @@ -0,0 +1,15 @@ +en: + activerecord: + models: + target_file: Export + plural: + target_file: Exports + attributes: + target_file: + sti_type: Type + state: state + original_file: File + number_of_records: '# records' + start_time: start + end_time: end + size: size diff --git a/lib/generators/dunlop/install/target_files/templates/migrations/create_target_files.rb b/lib/generators/dunlop/install/target_files/templates/migrations/create_target_files.rb new file mode 100644 index 0000000..5404c13 --- /dev/null +++ b/lib/generators/dunlop/install/target_files/templates/migrations/create_target_files.rb @@ -0,0 +1,28 @@ +class CreateTargetFiles < ActiveRecord::Migration[5.1] + def change + create_table :target_files do |t| + t.string :sti_type + t.string :state + t.integer :size + t.integer :number_of_records + t.datetime :start_time + t.datetime :end_time + t.string :original_file # carrierwave + # paperclip + t.string :original_file_file_name + t.string :original_file_content_type + t.integer :original_file_file_size + t.datetime :original_file_updated_at + + t.timestamps null: false + end + add_index :target_files, :sti_type + + create_table :dunlop_target_file_downloads do |t| + t.integer :user_id + t.integer :target_file_id + + t.timestamps null: false + end + end +end diff --git a/lib/generators/dunlop/install/target_files/templates/models/target_file.rb b/lib/generators/dunlop/install/target_files/templates/models/target_file.rb new file mode 100644 index 0000000..83d54ee --- /dev/null +++ b/lib/generators/dunlop/install/target_files/templates/models/target_file.rb @@ -0,0 +1,8 @@ +class TargetFile < ApplicationRecord + include Dunlop::TargetFileModel + + setup_target_files %i[ + ] + + activate_dunlop! +end diff --git a/lib/generators/dunlop/install/target_files/templates/rspec/target_files_factory.rb b/lib/generators/dunlop/install/target_files/templates/rspec/target_files_factory.rb new file mode 100644 index 0000000..3a71162 --- /dev/null +++ b/lib/generators/dunlop/install/target_files/templates/rspec/target_files_factory.rb @@ -0,0 +1,3 @@ +FactoryBot.define do + factory :target_file +end diff --git a/lib/generators/dunlop/install/workflow/USAGE b/lib/generators/dunlop/install/workflow/USAGE new file mode 100644 index 0000000..bd2aedc --- /dev/null +++ b/lib/generators/dunlop/install/workflow/USAGE @@ -0,0 +1,8 @@ +Description: + Explain the generator + +Example: + rails generate workflow Thing + + This will create: + what/will/it/create diff --git a/lib/generators/dunlop/install/workflow/templates/controllers/concerns/workflow_step_controller.rb b/lib/generators/dunlop/install/workflow/templates/controllers/concerns/workflow_step_controller.rb new file mode 100644 index 0000000..1189e74 --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/controllers/concerns/workflow_step_controller.rb @@ -0,0 +1,107 @@ +module WorkflowStepController + extend ActiveSupport::Concern + + included do + helper_method :record_class + helper_method :workflow_instance_class + before_action :set_record, only: [:edit, :update, :destroy] + end + + def edit + end + + def record_class + @record_class ||= self.class.name.sub(/Controller$/, '').split('::').map(&:singularize).join('::').constantize + end + + def workflow_instance_class + @workflow_instance_class ||= "WorkflowInstance::#{self.class.name.split('::').last.sub(/Controller$/, '').singularize}".constantize + end + + # PATCH/PUT /owner_decommission/migrate_line_onnet_without_cpes/1 + def update + authorize! :update, record_class + @record.assign_attributes(record_params) + if @record.valid? + @record.public_send "#{record_commit_action}!" + redirect_to polymorphic_path([@record.workflow_instance]) + else + render :edit + end + end + + def destroy + + end + + # GET /contractor_completion/parallel_onnet_nls1_with_cpes/collection_edit + def collection_edit + authorize! :edit, record_class + if params[:workflow_instance_ids].present? + params[:workflow_instance_ids] = params[:workflow_instance_ids].split(RecordCollection.ids_separator) if params[:workflow_instance_ids].is_a?(String) + @collection = record_class.collection.includes(:workflow_instance).joins(:workflow_instance).where(workflow_instances: {id: params[:workflow_instance_ids]}) + elsif params[:workflow_instance_batch_id].present? + @workflow_instance_batch = WorkflowInstanceBatch.find(params[:workflow_instance_batch_id]) + @collection = record_class.collection.includes(:workflow_instance).joins(:workflow_instance) + .where(workflow_instances: {workflow_instance_batch_id: @workflow_instance_batch.id}) + .where.not(workflow_instances: {state: 'any_rejected'}) + .refine_relation{ with_last_attribute_changes } + else + @collection = record_class.collection.includes(:workflow_instance).find(ids_param) + end + render layout: params[:reveal] != 'yes' + end + + # GET /contractor_completion/parallel_onnet_nls1_with_cpes/collection_edit + def collection_update + authorize! :update, record_class + @collection = record_class.collection.find(ids_param) # automatic security of find on specific sti class + + # Batch reject security + if params[:workflow_instance_batch_id].present? and record_commit_action == 'reject' + return redirect_to :back, alert: "You cannot reject an entire batch" + end + + #TODO: the following code could be much nicer! + if @collection.trigger_action!(record_commit_action, params[:collection]) + if params[:workflow_instance_batch_id].present? + @workflow_instance_batch = WorkflowInstanceBatch.find(params[:workflow_instance_batch_id]) + redirect_to @workflow_instance_batch + elsif @collection.one? + redirect_to @collection.first.workflow_instance + else + redirect_to polymorphic_path([:selection, workflow_instance_class], ids: @collection.map(&:workflow_instance_id).join(RecordCollection.ids_separator)) + end + else + render :collection_edit + end + end + + private + + + # Translate the form commit action to a consistent useable and safe + # processable name + def record_commit_action + case params[:commit] + when t('workflow_step.process_button') then 'process' + when t('workflow_step.complete_button') then 'complete' + when t('workflow_step.reject_button') then 'reject' + else raise "Unsupported record commit type given: #{params[:commit]}" + end + end + + # Use callbacks to share common setup or constraints between actions. + def set_record + @record = record_class.find(params[:id]) + end + + def record_params + record_param = record_class.name.underscore.gsub('/', '_') + return {} unless params[record_param].present? # Allow empty params, empty form just triggering state change is also an option + #params.require( record_param ).permit( *permitted_attributes ) + params.require( record_param ).permit! #TODO: make secure!!! + end + +end + diff --git a/lib/generators/dunlop/install/workflow/templates/controllers/workflow_instance_batches_controller.rb b/lib/generators/dunlop/install/workflow/templates/controllers/workflow_instance_batches_controller.rb new file mode 100644 index 0000000..aaf98d1 --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/controllers/workflow_instance_batches_controller.rb @@ -0,0 +1,79 @@ +class WorkflowInstanceBatchesController < ApplicationController + before_action :find_record, only: [:show, :edit, :update, :destroy] + delegate :workflow_instance_batch_class, :workflow_instance_class, to: :class + respond_to :html, :csv + + def index + authorize! :read, workflow_instance_batch_class + @q = workflow_instance_batch_class.search(query) + @q.sorts = "plan_date ASC" if @q.sorts.empty? + @records = @q.result.page(params[:page]).per(params[:per_page]) + end + + def new + authorize! :manage, workflow_instance_batch_class + @record = workflow_instance_batch_class.new + end + + def with_mixed_bas_completeness + @records = workflow_instance_batch_class.with_mixed_bas_load_states + end + + def create + @record = workflow_instance_batch_class.new(record_params) + authorize! :manage, @record + if @record.save + redirect_to action: :index + else + render action: "new" + end + end + + def show + authorize! :read, @record + respond_with @record, record_class: workflow_instance_class, include_notes: params[:include_notes] + end + + def edit + authorize! :manage, @record + end + + def update + authorize! :manage, @record + if @record.update record_params + redirect_to action: :index + else + render action: :edit + end + end + + class << self + def workflow_instance_batch_class + return @workflow_instance_batch_class if @workflow_instance_batch_class + parts = controller_path.split('/').map(&:classify) + parts.pop until klass = parts.join('::').safe_constantize + @workflow_instance_batch_class = klass + end + + def workflow_instance_class + return @workflow_instance_class if @workflow_instance_class + @workflow_instance_class = workflow_instance_batch_class.name.sub('Batch', '').constantize + end + end + helper_method :workflow_instance_batch_class + helper_method :workflow_instance_class + + private + + def record_params + params.require(workflow_instance_batch_class.name.underscore.gsub('/', '_')).permit(:name, :plan_date, :workflow_instance_manager_id) + end + + def find_record + @record = workflow_instance_batch_class.find(params[:id]) + end + + def query + params[:q] + end +end diff --git a/lib/generators/dunlop/install/workflow/templates/controllers/workflow_instances_controller.rb b/lib/generators/dunlop/install/workflow/templates/controllers/workflow_instances_controller.rb new file mode 100644 index 0000000..de4a921 --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/controllers/workflow_instances_controller.rb @@ -0,0 +1,175 @@ +class WorkflowInstancesController < ApplicationController + before_action :set_record, only: [:show, :edit, :update, :destroy, :reset] + before_action :set_collection, only: [:collection_edit, :collection_update, :uncategorize] + delegate :record_class, :record_batch_class, to: :class + alias_method :workflow_instance_class, :record_class + + respond_to :html, :csv + + def index + authorize! :read, record_class + set_records_for_index + respond_to do |format| + format.html do + @records = @records.page(params[:page]).per(params[:per_page]).decorate + end + format.csv { respond_with @records, record_class: record_class, include_notes: params[:include_notes] } + end + end + + def show + authorize! :read, @record + @record = @record.decorate + respond_with(@record) + end + + def collection_edit + authorize! :read, record_class # not update, since this read can have multiple update authorizations + redirect_to record_class and return unless @collection.any? + if params[:reveal].present? + render layout: false # popup + end + end + + # This is a custom reporting action where an extra filter + # is applied to the full index scope. + # reports + def with_bas_correction + authorize! :read, record_class + set_records_for_index + @records = @records.where(data_corrected_by_bas_data: true).decorate + respond_to do |format| + format.html { render action: :selection } + format.csv { respond_with @records, record_class: record_class, include_notes: params[:include_notes] } + end + end + + # reports + def with_incomplete_bas_records + authorize! :read, record_class + set_records_for_index + @records = @records.with_incomplete_bas_records.decorate + respond_to do |format| + format.html do + redirect_to :back, notice: "There are no incompletely enriched DSLAMs" and return unless @records.any? + render action: :selection + end + format.csv { respond_with @records, record_class: record_class, include_notes: params[:include_notes] } + end + end + + def collection_update + authorize! :update, record_class + if @collection.update params[:collection] + redirect_to polymorphic_path([:selection, record_class], ids: @collection.ids) + else + render "collection_edit" + end + end + + def reset + authorize! :reset, @record + @record.reset! + redirect_to @record + end + + # POST /records/selection input=EAP/123; ...ETC.... + def selection + authorize! :read, record_class unless record_class == WorkflowInstance + respond_to do |format| + format.html do + @records = find_records_by_ids_or_input decorate: true + end + format.csv do + @records = find_records_by_ids_or_input + respond_with @records, record_class: record_class, include_notes: params[:include_notes] + end + end + end + + #def edit + # authorize! :edit, @record + # #@record = @record.decorate + # @collection = record_class.collection.new([@record]) + # render template: 'workflow_instances/collection_edit' + # #respond_with(@record) + #end + + #def update + # authorize! :update, @record + # @record.update(record_params) + # respond_with(@record) + #end + + #def destroy + # authorize! :destroy, @record + # @record.destroy + # respond_with(@record) + #end + + private + + def set_records_for_index + @q = records_scope.search(query) + @q.sorts = "state asc" if @q.sorts.empty? + @records = @q.result.including_relations + end + + def set_record + scope = record_class.scope_for(current_user, all: can?(:archive, record_class)) + @record = scope.find(params[:id]) + end + + def set_collection + if params[:workflow_instance_batch_id].present? + @workflow_instance_batch = WorkflowInstanceBatch.find(params[:workflow_instance_batch_id]) + @collection = record_class.collection.new(@workflow_instance_batch.workflow_instances) + else + @collection = record_class.collection.new(records_scope(all: can?(:read, :archived_records)).includes(:workflow_instance_batch).where(id: ids_param)) + end + end + + def records_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 + + # Used for the dslam_name actions + # The search is used for sorting, not for actual filtering + def find_records_by_ids_or_input(options = {}) + @q = records_scope(all: can?(:archive, record_class)).search(query) + @q.sorts = "state asc" if @q.sorts.empty? + records = apply_record_input_scope.including_relations + options[:decorate] ? records.decorate : records + end + + # Apply scope to query by means of given ids or eap input + def apply_record_input_scope + return @q.result.where(id: ids_param) if ids_param.present? # use ids param if supplied + return @q.result.where(workflow_instance_batch_id: params[:workflow_instance_batch_id]) if params[:workflow_instance_batch_id].present? + input = WorkflowInstanceInputScrubber.new( params[:input] ) + selection = @q.result.where dslam_name: input.dslam_names + params[:ids] = selection.pluck(:id) + selection + end + + def query + params[:q] + end + + class << self + def record_class + return @record_class if @record_class + parts = controller_path.split('/').map(&:classify) + parts.pop until klass = parts.join('::').safe_constantize + @record_class = klass + end + + def record_batch_class + return @record_batch_class if @record_batch_class + @record_batch_class = record_class.try(:batch_class) + end + end + helper_method :record_class + helper_method :record_batch_class +end diff --git a/lib/generators/dunlop/install/workflow/templates/csv_builders/workflow_instance/index_csv_builder.rb b/lib/generators/dunlop/install/workflow/templates/csv_builders/workflow_instance/index_csv_builder.rb new file mode 100644 index 0000000..50fbb18 --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/csv_builders/workflow_instance/index_csv_builder.rb @@ -0,0 +1,69 @@ +class WorkflowInstance::IndexCsvBuilder < CsvBuilder + BASE_COLUMNS = %i[] + + def headers + columns = [workflow_instance_class.model_name.human] + columns += BASE_COLUMNS.map{|c| workflow_instance_class.human_attribute_name(c)} + # Workflow steps + workflow_step_mapping.each do |klass, attributes| + columns << klass.model_name.human # state column + columns += attributes.map{|attr| klass.human_attribute_name(attr)} + end + columns + end + + def workflow_instances + resource + end + + # It could all be done using a .map instead of a find_each with fallback each into + # an array, but for large exports this does not blow up the queries that may have many + # association includes. Therefore the .find_each can be useful for exports of over + # 5000 records + def serialized_rows + rows = [] + iteration_method = workflow_instances.respond_to?(:find_each) ? :find_each : :each + workflow_instances.public_send(iteration_method) do |workflow_instance| + rows.push serialize_row(workflow_instance) + end + rows + end + + def serialize_row(record) + columns = [record.try(:presentation_name)] + columns += BASE_COLUMNS.map{|c| record_attribute(record, c) } + # Workflow steps + workflow_step_mapping.each do |workflow_step_class, attributes| + unless workflow_step = record.public_send(workflow_step_class.process_name) + columns += Array.new(1 + attributes.size) # state + attributes Empty fill + next + end + columns << workflow_step.state + columns += attributes.map{|attr| record_attribute(workflow_step, attr) } + end + columns + end + + # Create a mapping of (klass => attributes): + # { + # WorkflowStepClass => ['notes', 'workflow_step_action_executed', ...], + # .... + # } + def workflow_step_mapping + @workflow_step_mapping ||= begin + workflow_instance_class.workflow_step_classes.each.with_object({}) do |klass, h| + attributes = klass.collection.attributes.keys + attributes -= ['notes'] unless options[:include_notes].present? + h[klass] = attributes + end + end + end + + def workflow_instance_class + controller.workflow_instance_class + end + + def file_name + "#{Rails.application.config.application_name}-export-#{date_number}.csv" + end +end diff --git a/lib/generators/dunlop/install/workflow/templates/csv_builders/workflow_instance/selection_csv_builder.rb b/lib/generators/dunlop/install/workflow/templates/csv_builders/workflow_instance/selection_csv_builder.rb new file mode 100644 index 0000000..8a78b62 --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/csv_builders/workflow_instance/selection_csv_builder.rb @@ -0,0 +1,3 @@ +class WorkflowInstance::SelectionCsvBuilder < WorkflowInstance::IndexCsvBuilder +end + diff --git a/lib/generators/dunlop/install/workflow/templates/csv_builders/workflow_instance_batch/show_csv_builder.rb b/lib/generators/dunlop/install/workflow/templates/csv_builders/workflow_instance_batch/show_csv_builder.rb new file mode 100644 index 0000000..70651b6 --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/csv_builders/workflow_instance_batch/show_csv_builder.rb @@ -0,0 +1,10 @@ +class WorkflowInstanceBatch::ShowCsvBuilder < WorkflowInstance::IndexCsvBuilder + + def workflow_instances + resource.workflow_instances.including_relations + end + + def file_name + "G2l-dslams-batch-#{resource.presentation_name}-export.csv" + end +end diff --git a/lib/generators/dunlop/install/workflow/templates/factories/workflow_instance.rb b/lib/generators/dunlop/install/workflow/templates/factories/workflow_instance.rb new file mode 100644 index 0000000..ebdca30 --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/factories/workflow_instance.rb @@ -0,0 +1,49 @@ +FactoryBot.define do + factory :workflow_instance do + after(:build){|instance| instance.setup_workflow_steps } + state "unplanned" # default for Vula + + trait :uncategorized do + state 'uncategorized' + end + + trait :unplanned do + state 'unplanned' + end + + trait :planned do + state 'planned' + end + + trait :active do + state 'active' + after :build do |instance| + instance.workflow_steps.first.state = 'processing' + end + end + + trait :overdue do + state 'overdue' + end + + trait :all_completed do + state 'all_completed' + after :build do |instance| + instance.workflow_steps.each{|workflow_step| workflow_step.state = 'completed' } + end + end + + trait :any_rejected do + state 'any_rejected' + after :build do |instance| + instance.workflow_steps.first.state = 'rejected' + end + end + + trait :with_values do + #TODO: fill with reasonable values + + end + end +end + diff --git a/lib/generators/dunlop/install/workflow/templates/factories/workflow_instance_batch.rb b/lib/generators/dunlop/install/workflow/templates/factories/workflow_instance_batch.rb new file mode 100644 index 0000000..dac18bc --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/factories/workflow_instance_batch.rb @@ -0,0 +1,4 @@ +FactoryBot.define do + factory :workflow_instance_batch +end + diff --git a/lib/generators/dunlop/install/workflow/templates/initializers/dunlop_overdue_config.rb b/lib/generators/dunlop/install/workflow/templates/initializers/dunlop_overdue_config.rb new file mode 100644 index 0000000..78cb045 --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/initializers/dunlop_overdue_config.rb @@ -0,0 +1,9 @@ +#Dunlop::OverdueHandling.define do +# when_workflow_step :owner_preparation, is: -20.days, from: {contractor_preparation: :plan_date} +# when_workflow_step :wba_submission, is: -18.days, from: {contractor_preparation: :plan_date} +# when_workflow_step :wba_completion, is: -10.days, from: {contractor_preparation: :plan_date} +# when_workflow_step :contractor_preparation, is: -3.days, from: {contractor_preparation: :plan_date} +# when_workflow_step :contractor_completion, is: 1.day, from: {contractor_preparation: :plan_date} +# when_workflow_step :service_provider_completion, is: 1.day, from: {contractor_preparation: :plan_date} +# when_workflow_step :service_provider_decommission, is: 5.days, from: {contractor_preparation: :plan_date} +#end diff --git a/lib/generators/dunlop/install/workflow/templates/initializers/dunlop_workflow_steps_in_batch_finished_config.rb b/lib/generators/dunlop/install/workflow/templates/initializers/dunlop_workflow_steps_in_batch_finished_config.rb new file mode 100644 index 0000000..54884bc --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/initializers/dunlop_workflow_steps_in_batch_finished_config.rb @@ -0,0 +1,28 @@ +Dunlop::WorkflowStepsInBatchFinished.define do + the_final_states_are %w[rejected completed] + + #on_finishing_of :owner_service_window1_preparation do + # description "Send notification mail to WBA" do |workflow_instance_batch| + # KpnCoreDeliveryMailer.wba_notification(workflow_instance_batch.id, self.model_name.human).deliver_later + # end + #end + + #on_finishing_of :contractor_service_window1_execution do + # description <<-DESCRIPTION + # Check ${attributes.owner_service_window2_preparation.migration_file_for_bvt_ready} + # for ${models.owner_service_window2_preparation} workflow steps in the batch + # DESCRIPTION + # action do |workflow_instance_batch| + # workflow_steps_in_batch_scope(:owner_service_window2_preparation).update_all migration_file_for_bvt_ready: true + # end + + # description "Send an email to Core Delivery informing them of batch completion" do |workflow_instance_batch| + # KpnCoreDeliveryMailer.contractor_service_window1_handled_for_batch(workflow_instance_batch.id).deliver_later + # end + + # description "Send an email to Schuuring informing them of batch completion" do |workflow_instance_batch| + # SchuuringMailer.workflow_instance_batch_workflow_step_finished(self.class.name, workflow_instance_batch.id).deliver_later + # end + #end + +end diff --git a/lib/generators/dunlop/install/workflow/templates/locales/workflow_instance_batch.yml b/lib/generators/dunlop/install/workflow/templates/locales/workflow_instance_batch.yml new file mode 100644 index 0000000..c36b81d --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/locales/workflow_instance_batch.yml @@ -0,0 +1,9 @@ +en: + workflow_instance_batch: + cannot_reject_message: | + Het is niet mogelijk om een Batch in zijn geheel te rejecten. + Ga naar een individuele DSLAM en voer de reject actie daar uit. + to_index_button: Naar Batches overzicht + edit_dslams_button: Bewerk alle records in de batch + show_dslams_button: Records tonen in lijst + diff --git a/lib/generators/dunlop/install/workflow/templates/locales/workflow_instances.yml b/lib/generators/dunlop/install/workflow/templates/locales/workflow_instances.yml new file mode 100644 index 0000000..96524bf --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/locales/workflow_instances.yml @@ -0,0 +1,20 @@ +en: + activerecord: + models: + workflow_instance: Migration + plural: + workflow_instance: Migrations + workflow_instance: + display_badge: WI + workflow_steps_header: Stappen + show_more_attributes: Meer attributen tonen + reset_confirmation: | + Are you sure you want to reset? + + All the progress will be lost. The notes will remain + selection: + submit_button: Select by DSLAM name + progress_display: + title: Voortgang + export_button: Download Records + diff --git a/lib/generators/dunlop/install/workflow/templates/locales/workflow_steps.yml b/lib/generators/dunlop/install/workflow/templates/locales/workflow_steps.yml new file mode 100644 index 0000000..dc5a65b --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/locales/workflow_steps.yml @@ -0,0 +1,14 @@ +en: + workflow_step: + name: Workflow step + plural_name: Workflow steps + previous_step_not_finished_warning: | + Nog niet alle acties van %{step_name} zijn afgerond + Weet je zeker dat je wilt doorgaan? + actions_modal: + title: '%{model} actions' + process_button: Process / Update + complete_button: Complete + reject_button: Reject + edit_single_title: 'Edit %{workflow_step} of %{workflow_instance} %{presentation_name}' + update_button_text: Update %{models} diff --git a/lib/generators/dunlop/install/workflow/templates/migrations/create_dunlop_last_attribute_changes.rb b/lib/generators/dunlop/install/workflow/templates/migrations/create_dunlop_last_attribute_changes.rb new file mode 100644 index 0000000..ebb9979 --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/migrations/create_dunlop_last_attribute_changes.rb @@ -0,0 +1,13 @@ +class CreateDunlopLastAttributeChanges < ActiveRecord::Migration + def change + create_table :dunlop_last_attribute_changes do |t| + t.integer :target_id + t.string :target_type + t.datetime :changed_at + t.string :attribute_name + + t.timestamps null: false + end + end +end + diff --git a/lib/generators/dunlop/install/workflow/templates/migrations/create_workflow_instance_batches.rb b/lib/generators/dunlop/install/workflow/templates/migrations/create_workflow_instance_batches.rb new file mode 100644 index 0000000..a16a7bf --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/migrations/create_workflow_instance_batches.rb @@ -0,0 +1,13 @@ +class CreateWorkflowInstanceBatches < ActiveRecord::Migration + def change + create_table :workflow_instance_batches do |t| + t.string :sti_type + t.string :state + t.date :plan_date + t.string :name + t.timestamps null: false + end + add_index :workflow_instance_batches, :sti_type + end +end + diff --git a/lib/generators/dunlop/install/workflow/templates/migrations/create_workflow_instances.rb b/lib/generators/dunlop/install/workflow/templates/migrations/create_workflow_instances.rb new file mode 100644 index 0000000..4f957fe --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/migrations/create_workflow_instances.rb @@ -0,0 +1,15 @@ +class CreateWorkflowInstances < ActiveRecord::Migration + def change + create_table :workflow_instances do |t| + t.string :sti_type + t.string :state + t.datetime :archived_at + t.string :bucket + t.integer :workflow_instance_batch_id + t.date :plan_date + t.timestamps null: false + end + add_index :workflow_instances, :sti_type + end +end + diff --git a/lib/generators/dunlop/install/workflow/templates/models/workflow_instance.rb b/lib/generators/dunlop/install/workflow/templates/models/workflow_instance.rb new file mode 100644 index 0000000..402a910 --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/models/workflow_instance.rb @@ -0,0 +1,17 @@ +class WorkflowInstance < ApplicationRecord + include Dunlop::WorkflowInstanceModel + include WorkflowInstance::DataSetup + + # The name by which the workflow instance can be recognized + def presentation_name + [id, state].join('-') + end + + setup_scenarios %i[ + ] + + setup_possible_workflow_steps %i[ + ] + + activate_dunlop! +end diff --git a/lib/generators/dunlop/install/workflow/templates/models/workflow_instance_batch.rb b/lib/generators/dunlop/install/workflow/templates/models/workflow_instance_batch.rb new file mode 100644 index 0000000..352f473 --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/models/workflow_instance_batch.rb @@ -0,0 +1,8 @@ +class WorkflowInstanceBatch < ApplicationRecord + include Dunlop::WorkflowInstanceBatchModel + + def presentation_name + name + end + +end diff --git a/lib/generators/dunlop/install/workflow/templates/models/workflow_instance_data_setup.rb b/lib/generators/dunlop/install/workflow/templates/models/workflow_instance_data_setup.rb new file mode 100644 index 0000000..6f5f175 --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/models/workflow_instance_data_setup.rb @@ -0,0 +1,13 @@ +module WorkflowInstance::DataSetup + extend ActiveSupport::Concern + + included do + # put extra source record relations here + end + + module ClassMethods + # add load logig based on source records here + # def create_or_update_from_source_record_dslams + # end + end +end diff --git a/lib/generators/dunlop/install/workflow/templates/models/workflow_step_collection.rb b/lib/generators/dunlop/install/workflow/templates/models/workflow_step_collection.rb new file mode 100644 index 0000000..0cba95a --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/models/workflow_step_collection.rb @@ -0,0 +1,8 @@ +module WorkflowStepCollection + extend ActiveSupport::Concern + include Dunlop::WorkflowStep::GenericCollection + + included do + attribute :notes + end +end diff --git a/lib/generators/dunlop/install/workflow/templates/models/workflow_step_model.rb b/lib/generators/dunlop/install/workflow/templates/models/workflow_step_model.rb new file mode 100644 index 0000000..a962d9f --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/models/workflow_step_model.rb @@ -0,0 +1,7 @@ +module WorkflowStepModel + extend ActiveSupport::Concern + + included do + include Dunlop::WorkflowStepModel + end +end diff --git a/lib/generators/dunlop/install/workflow/templates/services/workflow_instance_input_scrubber.rb b/lib/generators/dunlop/install/workflow/templates/services/workflow_instance_input_scrubber.rb new file mode 100644 index 0000000..e1c4440 --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/services/workflow_instance_input_scrubber.rb @@ -0,0 +1,26 @@ +class WorkflowInstanceInputScrubber + attr_reader :input + DSLAM_NAME_REGEXP = /[a-zA-Z0-9\-]+[\/-]DSLA8?[\/-]\d+/ + EAP_FORMAT = /EAP\/\d+/i + ZIPCODE_FORMAT = /\d{4}[a-zA-Z]{2}/ + XDF_SERVICE_ID_FORMAT = /[a-zA-Z]{3}\d{5}/ + + def initialize(input) + @input = input.to_s + end + + def dslam_names + input.scan(DSLAM_NAME_REGEXP).map do |input_name| + # schuuring to kpn aanpassing + input_name.sub('-DSLA-', '/DSLA/').sub('-DSLA8-', '/DSLA8/') + end + end + + def xdf_service_ids + input.scan(XDF_SERVICE_ID_FORMAT) + end + + def empty? + dslam_names.empty? + end +end diff --git a/lib/generators/dunlop/install/workflow/templates/views/workflow_instance_batches/_form.html.slim b/lib/generators/dunlop/install/workflow/templates/views/workflow_instance_batches/_form.html.slim new file mode 100644 index 0000000..0a657a6 --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/views/workflow_instance_batches/_form.html.slim @@ -0,0 +1,7 @@ += simple_form_for(@record) do |f| + = f.error_notification + - unless @record.new_record? + .form-inputs= f.input :id, disabled: true + /.form-inputs= f.input :name + .form-inputs= f.input :plan_date, as: :string, input_html: {class: 'datepicker'} + .form-actions= f.button :submit diff --git a/lib/generators/dunlop/install/workflow/templates/views/workflow_instance_batches/edit.html.slim b/lib/generators/dunlop/install/workflow/templates/views/workflow_instance_batches/edit.html.slim new file mode 100644 index 0000000..056c1cf --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/views/workflow_instance_batches/edit.html.slim @@ -0,0 +1,4 @@ += page_title :edit, workflow_instance_batch_class += render 'workflow_instance_batches/form' +.page-actions + = link_to 'Back', workflow_instance_batch_class, class: 'back' diff --git a/lib/generators/dunlop/install/workflow/templates/views/workflow_instance_batches/index.html.slim b/lib/generators/dunlop/install/workflow/templates/views/workflow_instance_batches/index.html.slim new file mode 100644 index 0000000..1e7409d --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/views/workflow_instance_batches/index.html.slim @@ -0,0 +1,41 @@ += page_title :index, workflow_instance_batch_class += paginate @records +table.full-width + thead + tr + th= sort_link @q, :id + th= sort_link @q, :plan_date + th= "# #{workflow_instance_class.model_name.human_plural}" + th + th.actions + = search_form_for @q do |f| + tr + th.column-filter.name= f.search_field :id_eq, placeholder: '==' + th.column-filter.name= f.search_field :plan_date_eq, placeholder: 'date', class: 'datepicker' + th + th + th.column-filter.actions + = f.submit t('dunlop.filter.apply').html_safe, class: 'submit-filters-button' + = link_to workflow_instance_batch_class, class: 'clear-filters-button' do + span== t('dunlop.filter.reset') + tbody + - workflow_step_map = @records.workflow_step_map(current_user, archived: session[:archived_mode]) + - @records.each do |workflow_instance_batch| + tr + td= link_to workflow_instance_batch.presentation_name, workflow_instance_batch + td.plan_date data-date=workflow_instance_batch.plan_date.try(:iso8601) + td= workflow_instance_batch.workflow_instances.scope_for(current_user, archived: session[:archived_mode]).size + td.workflow_instance_batch-index-progress-container + - workflow_step_map[workflow_instance_batch.id].each do |workflow_step, state_counts| + span.workflow_instance_batch-index-workflow_step= workflow_step.model_name.human + - state_counts.each do |state, count| + span.state-container= state_badge workflow_step.new(state: state), state_append_text: " (#{count})", data: {workflow_instance_batch_id: workflow_instance_batch.id} + td.actions + = link_to polymorphic_path(workflow_instance_class, q: {workflow_instance_batch_id_eq: workflow_instance_batch.id}, per_page: 10_000), class: 'show-workflow_instance_batch-workflow_instances' do + span + = table_show_link workflow_instance_batch + - if can? :edit, workflow_instance_batch + = table_edit_link workflow_instance_batch +.page-actions + - if can? :create, workflow_instance_batch_class + = link_to t('action.new.label', model: workflow_instance_batch_class.model_name.human), new_polymorphic_path(workflow_instance_batch_class), class: 'new' diff --git a/lib/generators/dunlop/install/workflow/templates/views/workflow_instance_batches/new.html.slim b/lib/generators/dunlop/install/workflow/templates/views/workflow_instance_batches/new.html.slim new file mode 100644 index 0000000..9c4fb09 --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/views/workflow_instance_batches/new.html.slim @@ -0,0 +1,4 @@ += page_title :new, workflow_instance_batch_class += render 'workflow_instance_batches/form' +.page-actions + = link_to 'Back', workflow_instance_batch_class, class: 'back' diff --git a/lib/generators/dunlop/install/workflow/templates/views/workflow_instance_batches/show.html.slim b/lib/generators/dunlop/install/workflow/templates/views/workflow_instance_batches/show.html.slim new file mode 100644 index 0000000..46197f4 --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/views/workflow_instance_batches/show.html.slim @@ -0,0 +1,38 @@ +- workflow_instances_scope = @record.workflow_instances.scope_for(current_user, archived: session[:archived_mode]) +.display-row + .display-label= page_title :show, workflow_instance_batch_class + .display-field + h3 + = @record.presentation_name + = "(#{workflow_instances_scope.size})" + - if can? :edit, @record + = link_to '', [:edit, @record], class: 'edit-icon' +.display-row + .display-label= workflow_instance_batch_class.human_attribute_name(:plan_date) + .display-field= @record.plan_date + +.workflow-step-workfow-instances-collection-container + = safe_join workflow_instances_scope.map{|workflow_instance| link_to workflow_instance.presentation_name, workflow_instance, class: workflow_instance.state}, ', ' +.batch-states-collection-container + - workflow_instances_scope.group(:state).count.each do |state, count| + span.state= state_badge workflow_instance_class.new(state: state), state_append_text: " (#{count})", link_to: polymorphic_path(workflow_instance_class, q: {state_eq: state, workflow_instance_batch_id_eq: @record.id}) +.workflow-instance-batch-actions + - previous_step_unfinished = nil + - @record.workflow_step_map(current_user).each do |workflow_step, records| + .row.workflow-step-container + .small-12.columns + h4 + = workflow_step.model_name.human + = workflow_step_button_for workflow_step, request_params: {workflow_instance_batch_id: @record.id}, confirmation: (previous_step_unfinished.present? ? t('workflow_step.previous_step_not_finished_warning', step_name: previous_step_unfinished.model_name.human) : nil) + - state_counts = records.group("#{workflow_step.table_name}.state").count + - state_counts.each do |state, count| + span.state= state_badge workflow_step.new(state: state), state_append_text: " (#{count})", data: {workflow_instance_batch_id: @record.id} + - previous_step_unfinished = (state_counts.keys - workflow_step.final_states).any? ? workflow_step : nil +.page-actions + = link_to t('workflow_instance_batch.to_index_button'), workflow_instance_batch_class, class: 'back' + - if can? :manage, workflow_instance_class + button.collection-edit data-action="collection_edit" data-resource=workflow_instance_class.name.underscore data-request-params={workflow_instance_batch_id: @record.id}.to_json + = t 'workflow_instance_batch.edit_dslams_button', models: workflow_instance_class.model_name.human_plural + = link_to t('workflow_instance_batch.show_dslams_button'), polymorphic_path([:selection, workflow_instance_class], workflow_instance_batch_id: @record.id), class: 'show-workflow_instance_batch-workflow_instances' + - if can? :download, workflow_instance_class + = link_to t('workflow_instance.export_button'), {format: :csv}, class: 'export-button' diff --git a/lib/generators/dunlop/install/workflow/templates/views/workflow_instances/_index_table.html.slim b/lib/generators/dunlop/install/workflow/templates/views/workflow_instances/_index_table.html.slim new file mode 100644 index 0000000..7aa6e33 --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/views/workflow_instances/_index_table.html.slim @@ -0,0 +1,50 @@ +table.with-selection data-resource=record_class.name.underscore data-record-count=(@records.total_count rescue @records.size) data-preselected=local_assigns[:preselected] + thead + tr + th= sort_link @q, :id + - if local_assigns[:show_scenario] + th= at :scenario + th= at :state + th= at :plan_date + th= sort_link @q, :bucket + th= record_batch_class.model_name.human + th= t 'workflow_instance.progress_display.title' + /th.actions + - unless local_assigns[:filters] == false + = search_form_for @q do |f| + = hidden_field_tag :per_page, params[:per_page], id: 'q_per_page' + = search_row do + th.column-filter + - if local_assigns[:show_scenario] + th.column-filter.scenario= f.select :sti_type_eq, WorkflowInstance.workflow_step_classes.map{|step| [step.model_name.human, step.name]}, include_blank: 'all' + th.column-filter.state= f.select :state_eq, WorkflowInstance.state_machine.states.map{|state| [state.human_name, state.name]}, include_blank: 'all' + th.column-filter.plan_date + th.column-filter.bucket= f.search_field :bucket_cont, placeholder: 'contains', class: 'autocomplete', data: {source: record_class.available_bucket_names} + th.column-filter.workflow_instance_batch= f.select :workflow_instance_batch_id_eq, record_batch_class.for_select, {include_blank: 'all'}, class: 'search-select' + th.column-filter.actions + = f.submit t('dunlop.filter.apply').html_safe, class: 'submit-filters-button' + = link_to record_class, class: 'clear-filters-button' do + span== t('dunlop.filter.reset') + tbody + - @records.each do |record| + tr data-record={ id: record.id }.to_json class=(record.archived? ? 'archived' : 'active') + td= link_to record.presentation_name, record + - if local_assigns[:show_scenario] + td.scenario= record.model_name.human + td.state= state_badge record + td.plan_date data-date=record.plan_date.try(:iso8601) + td.bucket= record.bucket + td.workflow_instance_batch + - if record.workflow_instance_batch.present? + = link_to record.workflow_instance_batch.presentation_name, record.workflow_instance_batch + td.progress-display= show_workflow_progress_of record + /td.actions= table_show_link record + - unless local_assigns[:hide_buttons] + tfoot + tr + td colspan=99 + - if can?(:edit, record_class) or can?(:archive, record_class) or can?(:reset, record_class) + button.collection-edit.workflow_instance-actions data-resource=record_class.name.underscore data-request-params='{"ids": "selected_ids"}' = t 'collection.edit_button', models: record_class.model_name.human_plural + = workflow_step_buttons_for record_class + - if can? :download, record_class + = link_to t('workflow_instance.export_button'), {q: params[:q].try(:permit!), ids: params[:ids], workflow_instance_batch_id: params[:workflow_instance_batch_id], format: :csv}, class: 'export-button' diff --git a/lib/generators/dunlop/install/workflow/templates/views/workflow_instances/_show_fields.html.slim b/lib/generators/dunlop/install/workflow/templates/views/workflow_instances/_show_fields.html.slim new file mode 100644 index 0000000..004af79 --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/views/workflow_instances/_show_fields.html.slim @@ -0,0 +1,16 @@ +.workflow-instance-progress-display-container.panel= show_workflow_progress_of @record +.display-row + .display-label= at :state + .display-field.state= state_badge @record +.display-row + .display-label= record_class.batch_class.model_name.human + .display-field + - if batch = @record.workflow_instance_batch + = link_to batch.presentation_name, batch +.display-row + .display-label= at :plan_date + .display-field= @record.plan_date += collapsible_content t('workflow_instance.show_more_attributes') do + .display-row + .display-label= at :id + .display-field= @record.id diff --git a/lib/generators/dunlop/install/workflow/templates/views/workflow_instances/_workflow_step.html.slim b/lib/generators/dunlop/install/workflow/templates/views/workflow_instances/_workflow_step.html.slim new file mode 100644 index 0000000..a8c07c9 --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/views/workflow_instances/_workflow_step.html.slim @@ -0,0 +1,11 @@ +.workflow-instance-workflow-step-container class="#{workflow_step.state} #{workflow_step.class.name.underscore.split('/')}" + .workflow-instance-workflow-step-title + - if params[:show_changes].present? + = link_to workflow_step.class.model_name.human, show_record_changes_path(workflow_step.process_name, workflow_step.id, show_changes: 'yes'), data: {remote: true} + - else + = workflow_step.class.model_name.human + .workflow-instance-workflow-step-body + .display-row + .display-label= workflow_step.class.human_attribute_name(:state) + .display-field= workflow_step.human_state_name + = record_info_fields workflow_step diff --git a/lib/generators/dunlop/install/workflow/templates/views/workflow_instances/collection_edit.html.slim b/lib/generators/dunlop/install/workflow/templates/views/workflow_instances/collection_edit.html.slim new file mode 100644 index 0000000..967c6e6 --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/views/workflow_instances/collection_edit.html.slim @@ -0,0 +1,28 @@ +h2=t "workflow_step.actions_modal.title", model: record_class.model_name.human +- if batch = @workflow_instance_batch + .display-row + .display-label= batch.class.model_name.human + .display-field + = batch.presentation_name + = "(#{@collection.size})" +.panel.workflow-step-workfow-instances-collection-container + = safe_join @collection.map{|r| link_to(r.presentation_name, r, class: r.state) }, ', ' += simple_form_for @collection do |f| + = f.collection_ids + = f.error_notification + - if can? :update, record_class + .form-inputs= f.optional_input :bucket, input_html: {class: 'autocomplete', data: {source: record_class.available_bucket_names.to_json }} + - if record_batch_class.present? and batch.blank? # do not offer to change batch if the collection is the batch itself + .form-inputs= f.optional_input :workflow_instance_batch_id, collection: record_batch_class.for_select, label: record_batch_class.model_name.human + .form-inputs= f.optional_input :plan_date, input_html: {class: 'datepicker'} + .form-actions + = link_to 'Back', @collection.one? ? @collection.first : record_class, class: 'workflow-step-back-button' + - if can? :update, record_class + = f.button :submit, 'Assign', class: 'workflow-step-assign-button' + - if can?(:reset, record_class) + = f.submit t('workflow_instance.reset.reset'), formaction: dunlop.reset_workflow_instances_path, class: "workflow-instance-reset-button right", data: {confirm: t('workflow_instance.reset.confirmation_message', workflow_instance: record_class.model_name.human)} + - if can? :archive, record_class + - if session[:archived_mode] + = f.submit t('workflow_instance.revive.revive'), formaction: dunlop.revive_workflow_instances_path, class: "workflow-instance-revive-button right", data: {confirm: t('workflow_instance.revive.confirmation_message')} + - else + = f.submit t('workflow_instance.archive.archive'), formaction: dunlop.archive_workflow_instances_path, class: "workflow-instance-archive-button right", data: {confirm: t('workflow_instance.archive.confirmation_message')} diff --git a/lib/generators/dunlop/install/workflow/templates/views/workflow_instances/index.html.slim b/lib/generators/dunlop/install/workflow/templates/views/workflow_instances/index.html.slim new file mode 100644 index 0000000..799f8ab --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/views/workflow_instances/index.html.slim @@ -0,0 +1,12 @@ +.right= page_time += page_title :index, record_class +/.selection-container + = form_tag [:selection, record_class] do + = text_area_tag :input, '', id: 'workflow-instance-selection-input', placeholder: 'copy/paste DSLAM names for selection' + = submit_tag t('workflow_instance.selection.submit_button'), id: 'workflow-instance-selection-input-submit' + /=link_to 'Export Benny (Schuuring)', {q: params[:q].try(:permit!), stakeholder: 'schuuring', format: :csv}, class: 'export-button' + /=submit_tag t('record.select.find_fuzzy'), id: 'dslam_name-input-fuzzy-submit' + /=link_to 'Export records', {q: params[:q].try(:permit!), format: :csv}, class: 'export-button' +.multi-select-statistics += paginate @records += render "index_table" diff --git a/lib/generators/dunlop/install/workflow/templates/views/workflow_instances/info_fields.html.slim b/lib/generators/dunlop/install/workflow/templates/views/workflow_instances/info_fields.html.slim new file mode 100644 index 0000000..b77d78c --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/views/workflow_instances/info_fields.html.slim @@ -0,0 +1 @@ += render 'dunlop/badge_info_collection_attributes', local_assigns diff --git a/lib/generators/dunlop/install/workflow/templates/views/workflow_instances/selection.html.slim b/lib/generators/dunlop/install/workflow/templates/views/workflow_instances/selection.html.slim new file mode 100644 index 0000000..dd481ab --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/views/workflow_instances/selection.html.slim @@ -0,0 +1,9 @@ +.right= page_time += page_title :index, record_class +.multi-select-statistics += render "index_table", filters: false, preselected: 'yes', hide_buttons: local_assigns[:hide_buttons], show_scenario: local_assigns[:show_scenario] +.page-actions + /= link_to t('action.new.label', model: Migration::MigrateLineOnnetWithoutCpe.model_name.human), new_migration_migrate_line_onnet_without_cpe_path, class: 'new' + = link_to 'Back', record_class, class: 'back' + / unless local_assigns[:hide_buttons] + = link_to 'Export', {input: params[:input], ids: params[:ids], format: :csv}, class: 'export-button' diff --git a/lib/generators/dunlop/install/workflow/templates/views/workflow_instances/show.html.slim b/lib/generators/dunlop/install/workflow/templates/views/workflow_instances/show.html.slim new file mode 100644 index 0000000..9601aa2 --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/views/workflow_instances/show.html.slim @@ -0,0 +1,17 @@ += page_title :show, record_class +div class=(@record.archived? ? 'record-archived' : 'record-active') + = render 'show_fields' + h2.collapsible-expand-all= t 'workflow_instance.workflow_steps_header' + .workflow-instance-workflow-steps-container + - @record.workflow_steps.each do |workflow_step| + = render 'workflow_step', workflow_step: workflow_step + .page-actions + = link_to 'Back', record_class, class: 'back' + - if can? :manage, @record + = link_to t('collection.edit_button', models: record_class.model_name.human), polymorphic_path([:collection_edit, record_class], ids: [@record.id]), class: 'collection-edit' + - if can? :reset, @record + = link_to t('workflow_instance.reset.reset'), [:reset, @record], class: 'workflow-instance-reset-button right', method: :post, data: {confirm: t('workflow_instance.reset.confirmation_message', workflow_instance: record_class.model_name.human)} +/- if can? :show_changes, @record + = collapsible_content "Record changes" do + = render 'change_events/show_record_changes', target: @record + diff --git a/lib/generators/dunlop/install/workflow/templates/views/workflow_steps/_collection_edit.html.slim b/lib/generators/dunlop/install/workflow/templates/views/workflow_steps/_collection_edit.html.slim new file mode 100644 index 0000000..d0d978f --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/views/workflow_steps/_collection_edit.html.slim @@ -0,0 +1,17 @@ +h2=t "workflow_step.actions_modal.title", model: record_class.model_name.human +- if batch = @workflow_instance_batch + .display-row + .display-label= batch.class.model_name.human + .display-field + = batch.presentation_name + = "(#{@collection.size})" +.panel.workflow-step-workfow-instances-collection-container + = safe_join @collection.map{|r| link_to(r.workflow_instance.presentation_name, r.workflow_instance, class: r.state) }, ', ' += simple_form_for @collection do |f| + = f.error_notification + = f.collection_ids + - if batch + = hidden_field_tag :workflow_instance_batch_id, batch.id + = render 'collection_form', f: f + .form-actions= render 'workflow_steps/form_actions', f: f, back: workflow_instance_class + diff --git a/lib/generators/dunlop/install/workflow/templates/views/workflow_steps/_edit.html.slim b/lib/generators/dunlop/install/workflow/templates/views/workflow_steps/_edit.html.slim new file mode 100644 index 0000000..0cec278 --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/views/workflow_steps/_edit.html.slim @@ -0,0 +1,11 @@ += page_title t('workflow_step.edit_single_title', @record.workflow_instance.attributes.symbolize_keys.merge(workflow_step: record_class.model_name.human, presentation_name: @record.workflow_instance.presentation_name, workflow_instance: @record.workflow_instance.model_name.human)) +- scope_model record_class +.display-row + .display-label= at :state + .display-field.state= state_badge @record += simple_form_for @record do |f| + = f.error_notification + = f.collection_ids + = render 'collection_form', f: f + .form-actions= render 'workflow_steps/form_actions', f: f, back: @record.workflow_instance + diff --git a/lib/generators/dunlop/install/workflow/templates/views/workflow_steps/_form_actions.html.slim b/lib/generators/dunlop/install/workflow/templates/views/workflow_steps/_form_actions.html.slim new file mode 100644 index 0000000..77bbe93 --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/views/workflow_steps/_form_actions.html.slim @@ -0,0 +1,7 @@ += link_to 'Back', back, class: 'workflow-step-back-button' += f.submit t('workflow_step.process_button'), class: 'workflow-step-process-button' += f.submit t('workflow_step.complete_button'), class: 'workflow-step-complete-button' +- if @workflow_instance_batch.present? + = link_to t('workflow_step.reject_button'), '#', onclick: %|alert('#{escape_javascript t('workflow_instance_batch.cannot_reject_message')}'); return false|, class: 'workflow-step-reject-button' +- else + = f.submit t('workflow_step.reject_button'), class: 'workflow-step-reject-button' diff --git a/lib/generators/dunlop/install/workflow/templates/views/workflow_steps/_notes.html.slim b/lib/generators/dunlop/install/workflow/templates/views/workflow_steps/_notes.html.slim new file mode 100644 index 0000000..5fb5589 --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/views/workflow_steps/_notes.html.slim @@ -0,0 +1,11 @@ +- if f.object.collection? + - if f.object.one? + pre.notes-display= f.object.uniform_collection_value(:notes) + .form-inputs.notes-to-append= f.input :notes, as: :text, hint: "Notes will be appended" + - else + .form-inputs.notes-to-append= f.input :notes, as: :text, hint: "Notes will be appended to the individual notes of the collection" +- else + .form-inputs.notes + pre.notes-display= f.object.notes + = f.input :notes, as: :text, input_html: {value: ''} + diff --git a/lib/generators/dunlop/install/workflow/templates/views/workflow_steps/_window_slot.html.slim b/lib/generators/dunlop/install/workflow/templates/views/workflow_steps/_window_slot.html.slim new file mode 100644 index 0000000..2709be6 --- /dev/null +++ b/lib/generators/dunlop/install/workflow/templates/views/workflow_steps/_window_slot.html.slim @@ -0,0 +1,11 @@ +- if f.object.is_a?(RecordCollection::Base) # Optionals + .form-inputs= f.optional_input :window_date, as: :string, input_html: {class: 'datepicker'} + - if local_assigns[:time_slot] + .form-inputs= f.optional_input :window_from, collection: hours_for_select, include_blank: '--' + .form-inputs= f.optional_input :window_to, collection: hours_for_select, include_blank: '--' +- else + .form-inputs= f.input :window_date, as: :string, input_html: {class: 'datepicker'} + - if local_assigns[:time_slot] + .form-inputs= f.input :window_from, collection: hours_for_select, include_blank: '--' + .form-inputs= f.input :window_to, collection: hours_for_select, include_blank: '--' + diff --git a/lib/generators/dunlop/install/workflow/workflow_generator.rb b/lib/generators/dunlop/install/workflow/workflow_generator.rb new file mode 100644 index 0000000..d7c2387 --- /dev/null +++ b/lib/generators/dunlop/install/workflow/workflow_generator.rb @@ -0,0 +1,94 @@ +require 'rails/generators/active_record' +require 'dunlop/generators' +class Dunlop::Install::WorkflowGenerator < Rails::Generators::Base + include Rails::Generators::Migration + include Dunlop::Generators::GeneratorHelpers + source_root File.expand_path('../templates', __FILE__) + + def create_models + copy_file "models/workflow_step_collection.rb", "app/models/concerns/workflow_step_collection.rb" + copy_file "models/workflow_step_model.rb", "app/models/concerns/workflow_step_model.rb" + end + + def setup_workflow_basics + template "models/workflow_instance_batch.rb", "app/models/workflow_instance_batch.rb" + setup_workflow_decorators + setup_workflow_instances + setup_workflow_steps + #TODO: call workflow related generators here + end + + def generate_views + directory "views/workflow_steps", "app/views/workflow_steps" + end + + def create_controllers + template "controllers/workflow_instances_controller.rb", "app/controllers/workflow_instances_controller.rb" + template "controllers/workflow_instance_batches_controller.rb", "app/controllers/workflow_instance_batches_controller.rb" + directory "controllers/concerns", "app/controllers/concerns" + end + + def add_csv_builders + directory "csv_builders", "app/csv_builders" + end + + def create_locales + directory "locales", "config/locales" + end + + def setup_rspec_files + directory "factories", "spec/factories" + end + + def setup_initializers + directory "initializers", "config/initializers" + end + + private + + def setup_workflow_decorators + template "decorators/workflow_instance_decorator.rb", "app/decorators/workflow_instance_decorator.rb" + end + + def setup_workflow_instances + migration_template "migrations/create_workflow_instances.rb", "db/migrate/create_workflow_instances.rb" + migration_template "migrations/create_workflow_instance_batches.rb", "db/migrate/create_workflow_instance_batches.rb" + migration_template "migrations/create_dunlop_last_attribute_changes.rb", "db/migrate/create_dunlop_last_attribute_changes.rb" + copy_file "models/workflow_instance.rb", "app/models/workflow_instance.rb" + copy_file "models/workflow_instance_data_setup.rb", "app/models/workflow_instance/data_setup.rb" + directory "views/workflow_instances", "app/views/workflow_instances" + directory "views/workflow_instance_batches", "app/views/workflow_instance_batches" + + route <<-ROUTE.strip_heredoc + namespace :workflow_instance do + WorkflowInstance.scenario_names.each do |scenario_name| + collection_resources scenario_name.to_s.pluralize do + collection do + match :selection, via: [:get, :post] + end + post :reset, on: :member + end + end + end + + namespace :workflow_instance_batch do + WorkflowInstance.scenario_names.each do |scenario_name| + resources scenario_name.to_s.pluralize + end + end + ROUTE + end + + def setup_workflow_steps + route <<-ROUTE.strip_heredoc + WorkflowInstance.possible_workflow_step_names.each do |workflow_step| + namespace workflow_step do + WorkflowInstance.scenario_names.each do |scenario_name| + collection_resources scenario_name.to_s.pluralize, only: [:edit, :update] + end + end + end + ROUTE + end + +end diff --git a/lib/generators/dunlop/rename_source_file/USAGE b/lib/generators/dunlop/rename_source_file/USAGE new file mode 100644 index 0000000..9219b60 --- /dev/null +++ b/lib/generators/dunlop/rename_source_file/USAGE @@ -0,0 +1,8 @@ +Description: + Explain the generator + +Example: + rails generate rename_source_file Thing + + This will create: + what/will/it/create diff --git a/lib/generators/dunlop/rename_source_file/rename_source_file_generator.rb b/lib/generators/dunlop/rename_source_file/rename_source_file_generator.rb new file mode 100644 index 0000000..abf5d81 --- /dev/null +++ b/lib/generators/dunlop/rename_source_file/rename_source_file_generator.rb @@ -0,0 +1,43 @@ +require 'fileutils' +class Dunlop::RenameSourceFileGenerator < Rails::Generators::NamedBase + include Rails::Generators::Migration + include Dunlop::Generators::GeneratorHelpers + source_root File.expand_path('../templates', __FILE__) + class_option :new_name, required: true + + def do_rename + return if behavior == :revoke + + #new_source_file_path = path('app/models') + #rename_model "source_file/#{file_name}", "source_file/#{new_name}" + #rename_model "source_record/#{file_name}", "source_record/#{new_name}" + from_table_name = "source_record_#{file_name.pluralize}" + if connection.table_exists?(from_table_name) + migration_template "migrations/rename_source_record.rb.erb", "db/migrate/rename_source_record_#{file_name}_to_#{new_name}.rb" + end + end + + private + + def connection + ActiveRecord::Base.connection + end + + def rename_model(from_name, to_name) + from_path = Rails.root.join('app/models', from_name) + to_path = Rails.root.join('app/models', to_name) + if File.exist? to_path + puts "There already exists a model at the path #{to_path}, skipping model rename for #{from_path}" + FileUtils.mv from_path, to_path + gsub_file to_path, from_name.classify, to_name.classify + end + end + + def new_name + @new_name = options.new_name.underscore.singularize + end + + def new_class_name + new_name.classify + end +end diff --git a/lib/generators/dunlop/rename_source_file/templates/migrations/rename_source_record.rb.erb b/lib/generators/dunlop/rename_source_file/templates/migrations/rename_source_record.rb.erb new file mode 100644 index 0000000..96ecc4a --- /dev/null +++ b/lib/generators/dunlop/rename_source_file/templates/migrations/rename_source_record.rb.erb @@ -0,0 +1,5 @@ +class RenameSourceRecord<%= class_name %>To<%= new_class_name %> < ActiveRecord::Migration + def change + rename_table :source_record_<%= file_name.pluralize %>, :source_record_<%= new_name.pluralize %> + end +end diff --git a/lib/generators/dunlop/scenario/USAGE b/lib/generators/dunlop/scenario/USAGE new file mode 100644 index 0000000..b1ae336 --- /dev/null +++ b/lib/generators/dunlop/scenario/USAGE @@ -0,0 +1,10 @@ +Description: + Explain the generator + +Example: + rails g dunlop:scenario wba_without_cpe --workflow_steps=contractor_initialization + rails g dunlop:scenario wba_without_cpe --workflow_steps=all + rails g dunlop:scenario uncategorized --workflow_steps= + + This will create: + an extra scenario diff --git a/lib/generators/dunlop/scenario/scenario_generator.rb b/lib/generators/dunlop/scenario/scenario_generator.rb new file mode 100644 index 0000000..ac99585 --- /dev/null +++ b/lib/generators/dunlop/scenario/scenario_generator.rb @@ -0,0 +1,79 @@ +require 'dunlop/generators' +class Dunlop::ScenarioGenerator < Rails::Generators::NamedBase + include Dunlop::Generators::GeneratorHelpers + + source_root File.expand_path('../templates', __FILE__) + class_option :columns, type: :array, default: [] + class_option :workflow_steps, type: :array, default: [] + + def setup_workflow_steps + unless options.workflow_steps.any? + say "you have not specified the workflow steps for the scenario. Define using\n --workflow_steps=#{WorkflowInstance.possible_workflow_step_names.join(' ')}\nor\n --workflow_steps=all" + abort + end + + wi_factory_code = "factory 'workflow_instance/#{file_name}', aliases: [:workflow_instance_#{file_name}], class: 'WorkflowInstance::#{class_name}', parent: :workflow_instance" + inject_before_last_end "spec/factories/workflow_instance.rb", wi_factory_code + + wi_batch_factory_code = "factory 'workflow_instance_batch/#{file_name}', aliases: [:workflow_instance_batch_#{file_name}], class: 'WorkflowInstanceBatch::#{class_name}', parent: :workflow_instance_batch" + inject_before_last_end "spec/factories/workflow_instance_batch.rb", wi_batch_factory_code + + workflow_steps.each do |ws| + @workflow_step = ws + template "workflow_step_model.rb.erb", "app/models/#{workflow_step}/#{file_name}.rb" + template "workflow_step_collection.rb.erb", "app/models/#{workflow_step}/#{file_name}/collection.rb" + + template "workflow_step_controller.rb.erb", "app/controllers/#{workflow_step}/#{file_name.pluralize}_controller.rb" + + wi_step_factory_code = "factory '#{workflow_step}/#{file_name}', aliases: [:#{workflow_step}_#{file_name}], class: '#{workflow_step.classify}::#{class_name}', parent: :#{workflow_step}" + inject_before_last_end "spec/factories/#{workflow_step}.rb", wi_step_factory_code + end + + if behavior == :revoke and (WorkflowInstance.possible_workflow_step_names - workflow_step.map(&:to_sym)).any? + say "There are still workflow steps associated with the scenario, so the base models (and controllers) will remain", :blue + abort + end + end + + def workflow_step + @workflow_step + end + + def workflow_steps + return @workflow_steps if @workflow_steps + all_possible = WorkflowInstance.possible_workflow_step_names.map(&:to_s) + selection = options.workflow_steps.select(&:present?) + abort "Not all specified workflow steps are actually present" unless selection.all?{|step_name| all_possible.include? step_name} + @workflow_steps = options.workflow_steps == ['all'] ? all_possible : selection + end + + def create_controllers + template "workflow_instance_controller.rb.erb", "app/controllers/workflow_instance/#{file_name.pluralize}_controller.rb" + template "workflow_instance_batch_controller.rb.erb", "app/controllers/workflow_instance_batch/#{file_name.pluralize}_controller.rb" + end + + def create_migration + return unless options.columns.any? + extra_columns = options.columns.select{|column_spec| WorkflowInstance.column_names.exclude?(column_spec.split(':').first) } + return unless extra_columns.any? + say "rails generate migration add_columns_to_workflow_instances #{extra_columns.join(' ')}" if behavior == :invoke + end + + def create_model_and_collection + template "model.rb.erb", "app/models/workflow_instance/#{file_name}.rb" + template "collection.rb.erb", "app/models/workflow_instance/#{file_name}/collection.rb" + template "batch_model.rb.erb", "app/models/workflow_instance_batch/#{file_name}.rb" + end + + def create_views + template "translations.yml.erb", "config/locales/scenario-#{file_name}.yml" + end + + def setup_spec_support + end + + def setup_scenario_on_base + return if behavior == :invoke and WorkflowInstance.scenario_names.map(&:to_s).include?(file_name) + add_to_symbol_list "app/models/workflow_instance.rb", 'setup_scenarios', file_name + end +end diff --git a/lib/generators/dunlop/scenario/templates/batch_model.rb.erb b/lib/generators/dunlop/scenario/templates/batch_model.rb.erb new file mode 100644 index 0000000..bd9595a --- /dev/null +++ b/lib/generators/dunlop/scenario/templates/batch_model.rb.erb @@ -0,0 +1,2 @@ +class WorkflowInstanceBatch::<%= class_name %> < WorkflowInstanceBatch +end diff --git a/lib/generators/dunlop/scenario/templates/collection.rb.erb b/lib/generators/dunlop/scenario/templates/collection.rb.erb new file mode 100644 index 0000000..e447c59 --- /dev/null +++ b/lib/generators/dunlop/scenario/templates/collection.rb.erb @@ -0,0 +1,7 @@ +class WorkflowInstance::<%= class_name %>::Collection < RecordCollection::Base + include Dunlop::WorkflowInstanceCollection + + attribute :bucket + attribute :workflow_instance_batch_id + attribute :plan_date +end diff --git a/lib/generators/dunlop/scenario/templates/model.rb.erb b/lib/generators/dunlop/scenario/templates/model.rb.erb new file mode 100644 index 0000000..534f04b --- /dev/null +++ b/lib/generators/dunlop/scenario/templates/model.rb.erb @@ -0,0 +1,3 @@ +class WorkflowInstance::<%= class_name %> < WorkflowInstance + setup_scenario_workflow_steps <%= "%i[ #{workflow_steps.join(' ')} ]" %> +end diff --git a/lib/generators/dunlop/scenario/templates/translations.yml.erb b/lib/generators/dunlop/scenario/templates/translations.yml.erb new file mode 100644 index 0000000..33119b3 --- /dev/null +++ b/lib/generators/dunlop/scenario/templates/translations.yml.erb @@ -0,0 +1,8 @@ +en: + activerecord: + models: + workflow_instance/<%= file_name %>: <%= file_name.humanize %> + workflow_instance_batch/<%= file_name %>: <%= file_name.humanize %> Batch + plural: + workflow_instance/<%= file_name %>: <%= file_name.humanize.pluralize %> + workflow_instance_batch/<%= file_name %>: <%= file_name.humanize.pluralize %> Batches diff --git a/lib/generators/dunlop/scenario/templates/workflow_instance_batch_controller.rb.erb b/lib/generators/dunlop/scenario/templates/workflow_instance_batch_controller.rb.erb new file mode 100644 index 0000000..4abf6ba --- /dev/null +++ b/lib/generators/dunlop/scenario/templates/workflow_instance_batch_controller.rb.erb @@ -0,0 +1,2 @@ +class WorkflowInstanceBatch::<%= class_name.pluralize %>Controller < WorkflowInstanceBatchesController +end diff --git a/lib/generators/dunlop/scenario/templates/workflow_instance_controller.rb.erb b/lib/generators/dunlop/scenario/templates/workflow_instance_controller.rb.erb new file mode 100644 index 0000000..27c0f64 --- /dev/null +++ b/lib/generators/dunlop/scenario/templates/workflow_instance_controller.rb.erb @@ -0,0 +1,2 @@ +class WorkflowInstance::<%= class_name.pluralize %>Controller < WorkflowInstancesController +end diff --git a/lib/generators/dunlop/scenario/templates/workflow_step_collection.rb.erb b/lib/generators/dunlop/scenario/templates/workflow_step_collection.rb.erb new file mode 100644 index 0000000..2c49cdb --- /dev/null +++ b/lib/generators/dunlop/scenario/templates/workflow_step_collection.rb.erb @@ -0,0 +1,3 @@ +class <%= workflow_step.classify %>::<%= class_name %>::Collection < RecordCollection::Base + include WorkflowStepCollection +end diff --git a/lib/generators/dunlop/scenario/templates/workflow_step_controller.rb.erb b/lib/generators/dunlop/scenario/templates/workflow_step_controller.rb.erb new file mode 100644 index 0000000..16d8196 --- /dev/null +++ b/lib/generators/dunlop/scenario/templates/workflow_step_controller.rb.erb @@ -0,0 +1,3 @@ +class <%= workflow_step.classify %>::<%= class_name.pluralize %>Controller < <%= workflow_step.classify.pluralize %>Controller +end + diff --git a/lib/generators/dunlop/scenario/templates/workflow_step_model.rb.erb b/lib/generators/dunlop/scenario/templates/workflow_step_model.rb.erb new file mode 100644 index 0000000..2cabb93 --- /dev/null +++ b/lib/generators/dunlop/scenario/templates/workflow_step_model.rb.erb @@ -0,0 +1,2 @@ +class <%= workflow_step.classify %>::<%= class_name %> < <%= workflow_step.classify %> +end diff --git a/lib/generators/dunlop/source_file/USAGE b/lib/generators/dunlop/source_file/USAGE new file mode 100644 index 0000000..df480fb --- /dev/null +++ b/lib/generators/dunlop/source_file/USAGE @@ -0,0 +1,8 @@ +Description: + Explain the generator + +Example: + rails generate dunlop:source_file my_source --sql --source_record --atributes=cu_id my_date:date + + This will create: + what/will/it/create diff --git a/lib/generators/dunlop/source_file/source_file_generator.rb b/lib/generators/dunlop/source_file/source_file_generator.rb new file mode 100644 index 0000000..d5f71be --- /dev/null +++ b/lib/generators/dunlop/source_file/source_file_generator.rb @@ -0,0 +1,121 @@ +require "dunlop/generators" +require "dunlop/generators/generator_helpers" +require "csv_builder" + + +class Dunlop::SourceFileGenerator < Rails::Generators::NamedBase + include Dunlop::Generators::GeneratorHelpers + source_root File.expand_path('../templates', __FILE__) + class_option :sql, type: :boolean + class_option :source_record, type: :boolean + class_option :attributes, type: :array, default: [] + + def create_source_file + add_to_symbol_list "app/models/source_file.rb", 'setup_source_files', file_name + + template "model.rb.erb", "app/models/source_file/#{file_name}.rb" + template "model_spec.rb.erb", "spec/models/source_file/#{file_name}_spec.rb" + if options.sql + create_file "app/models/source_file/sql_templates/load_#{file_name}.sql.erb" do + columns = '@dummy' + custom_set_rulings = [] + if attributes.present? + columns_ary = [] + attributes.each do |attr| + if attr.column_name == 'zipcode' # strip whitespace + columns_ary.push "@#{attr.column_name}" + custom_set_rulings.push "#{attr.column_name}=REPLACE(@#{attr.column_name}, ' ', '')" + elsif attr.boolean? + columns_ary.push "@#{attr.column_name}" + custom_set_rulings.push "#{attr.column_name}=IF(@#{attr.column_name}='true' OR @#{attr.column_name}='1', 1, 0)" + elsif attr.date? + columns_ary.push "@#{attr.column_name}" + # NULLIF(STR_TO_DATE(@due_date, '%d-%m-%Y'), '0000-00-00') + custom_set_rulings.push "#{attr.column_name}=NULLIF(@#{attr.column_name}, '')" + else + # no custom sql conversion + columns_ary.push attr.column_name + end + end + columns = columns_ary.in_groups_of(6).map{|g| g.compact.join(', ')}.join(",\n ") + end + output = <<-SQL.strip_heredoc + LOAD DATA LOCAL INFILE '<%= working_file.path %>' IGNORE + INTO TABLE source_record_#{file_name.pluralize} + FIELDS TERMINATED BY '#{CsvBuilder.default_csv_options[:col_sep] || ','}' OPTIONALLY ENCLOSED BY '"' + IGNORE 1 LINES ( + #{columns} + ) + SQL + if custom_set_rulings.any? + output += <<-SQL.strip_heredoc + SET + #{custom_set_rulings.join(",\n ")} + SQL + end + output + end + create_file "app/models/source_file/sql_templates/post_process_#{file_name}.sql.erb" + end + create_file "spec/fixtures/source_files/#{file_name}_1.csv" do + headers = '' + if attributes.present? + headers = attributes.map(&:name).join(CsvBuilder.default_csv_options[:col_sep] || ',') + end + _csv = <<-CSV.strip_heredoc.strip + #{headers} + CSV + end + end + + def add_factory + if behavior == :revoke + gsub_file "spec/factories/source_files.rb", /^s+factory 'source_file/#{file_name}'.*$/, '' + else + gsub_file "spec/factories/source_files.rb", /end\s*\z/m do |match_data| + " factory 'source_file/#{file_name}', class: 'SourceFile::#{class_name}', parent: :source_file\nend" + end + end + end + + def add_translations + gsub_file "config/locales/source_files.yml", /^ plural:\s*$/, " source_file/#{file_name}: #{file_name.humanize}\n plural:" + gsub_file "config/locales/source_files.yml", /^ attributes:\s*$/, " source_file/#{file_name}: #{file_name.humanize.pluralize}\n attributes:" + return unless options.source_record + # Preserve column mapping by I18N + append_to_file "config/locales/source_files.yml", <<-YML + source_record/#{file_name}: + #{attributes.map{|a| "#{a.column_name}: #{a.name}"}.join("\n ")} + YML + end + + def generate_source_record_additions + return unless options.source_record + return unless has_workflow? + inject_into_file "app/models/workflow_instance/data_setup.rb", after: "module ClassMethods\n" do <<-RUBY + # create_or_update callback from app/models/source_file/#{file_name} + def create_or_update_from_#{file_name} + end + + RUBY + end + end + + def has_workflow? + Dunlop.has_workflow? + end + + def output_helper_messages + if options.source_record + if attributes.present? + attrs = attributes.map{|a| [a.column_name, a.type].join(':')}.join(' ') + else + attrs = '' + end + if behavior == :invoke + puts "Source record itself is not created. Hint for creation is:" + puts "rails generate model SourceRecord::#{class_name} #{attrs}" + end + end + end +end diff --git a/lib/generators/dunlop/source_file/templates/model.rb.erb b/lib/generators/dunlop/source_file/templates/model.rb.erb new file mode 100644 index 0000000..d6629e8 --- /dev/null +++ b/lib/generators/dunlop/source_file/templates/model.rb.erb @@ -0,0 +1,26 @@ +class SourceFile::<%= class_name %> < SourceFile + def load_source_records + <%- if attributes.present? -%> + assert_first_line "<%= attributes.map(&:name).join(CsvBuilder.default_csv_options[:col_sep] || ',') %>" + <%- end -%> + <%- if options.source_record -%> + SourceRecord::<%= class_name %>.truncate + <%- end -%> + <%- if options.sql -%> + + execute_sql_erb_script(:load_<%= file_name %>) + execute_sql_erb_script(:post_process_<%= file_name %>) + <%- end -%> + <%- if options.source_record -%> + + self.number_of_records = SourceRecord::<%= class_name %>.count + <%- end -%> + end + +<%- if has_workflow? -%> + def after_activate_hook + # call app/models/workflow_instance/data_setup create_or_update method + WorkflowInstance.create_or_update_from_<%= file_name %> + end +<%- end -%> +end diff --git a/lib/generators/dunlop/source_file/templates/model_spec.rb.erb b/lib/generators/dunlop/source_file/templates/model_spec.rb.erb new file mode 100644 index 0000000..3e23f97 --- /dev/null +++ b/lib/generators/dunlop/source_file/templates/model_spec.rb.erb @@ -0,0 +1,38 @@ +require 'rails_helper' + +describe SourceFile::<%= class_name %> do + subject { create('source_file/<%= file_name %>', original_file: fixture_file("source_files/<%= file_name %>_1.csv") ) } + <%- if options.source_record -%> + let(:model) { SourceRecord::<%= class_name %> } + <%- end -%> + + describe "#execute_loading_process" do + it "loads" do + subject.execute_loading_process + subject.should_not have_log_entries /ERROR/ + subject.should have_log_entries 'Inladen gereed' + + model.count.should eq 99999 + record = model.first + + <%- attributes.each do |attr| -%> + <%- if attr.type == 'date' -%> + record.<%= attr.column_name %>.should eq "1981-03-09".to_date + <%- elsif attr.numeric? -%> + record.<%= attr.column_name %>.should eq 99999 + <%- else -%> + record.<%= attr.column_name %>.should eq "99999" + <%- end -%> + <%- end -%> + end + end + + <%- if has_workflow? -%> + describe '#after_activate_hook' do + it 'calls the create or update hook of workflow instance (app/models/workflow_instance/data_setup)' do + expect(WorkflowInstance).to receive(:create_or_update_from_<%= file_name %>) + subject.execute_loading_process + end + end + <%- end -%> +end diff --git a/lib/generators/dunlop/target_file/USAGE b/lib/generators/dunlop/target_file/USAGE new file mode 100644 index 0000000..1e94449 --- /dev/null +++ b/lib/generators/dunlop/target_file/USAGE @@ -0,0 +1,8 @@ +Description: + Explain the generator + +Example: + rails generate dunlop:target_file my_target + + This will create: + what/will/it/create diff --git a/lib/generators/dunlop/target_file/target_file_generator.rb b/lib/generators/dunlop/target_file/target_file_generator.rb new file mode 100644 index 0000000..222354e --- /dev/null +++ b/lib/generators/dunlop/target_file/target_file_generator.rb @@ -0,0 +1,31 @@ +require "dunlop/generators" +require "dunlop/generators/generator_helpers" +require "csv_builder" + +class Dunlop::TargetFileGenerator < Rails::Generators::NamedBase + include Dunlop::Generators::GeneratorHelpers + source_root File.expand_path('../templates', __FILE__) + + def create_target_file + add_to_symbol_list "app/models/target_file.rb", 'setup_target_files', file_name + + template "model.rb.erb", "app/models/target_file/#{file_name}.rb" + template "model_spec.rb.erb", "spec/models/target_file/#{file_name}_spec.rb" + template "csv_builder.rb.erb", "app/csv_builders/target_file/#{file_name}_csv_builder.rb" + end + + def add_factory + if behavior == :revoke + gsub_file "spec/factories/target_files.rb", /^s+factory 'target_file/#{file_name}'.*$/, '' + else + gsub_file "spec/factories/target_files.rb", /end\s*\z/m do |match_data| + " factory 'target_file/#{file_name}', class: 'TargetFile::#{class_name}', parent: :target_file\nend" + end + end + end + + def add_translations + gsub_file "config/locales/target_files.yml", /^ plural:\s*$/, " target_file/#{file_name}: #{file_name.humanize}\n plural:" + gsub_file "config/locales/target_files.yml", /^ attributes:\s*$/, " target_file/#{file_name}: #{file_name.humanize.pluralize}\n attributes:" + end +end diff --git a/lib/generators/dunlop/target_file/templates/csv_builder.rb.erb b/lib/generators/dunlop/target_file/templates/csv_builder.rb.erb new file mode 100644 index 0000000..f50c9c8 --- /dev/null +++ b/lib/generators/dunlop/target_file/templates/csv_builder.rb.erb @@ -0,0 +1,2 @@ +class TargetFile::<%= class_name %>CsvBuilder < CsvBuilder +end diff --git a/lib/generators/dunlop/target_file/templates/model.rb.erb b/lib/generators/dunlop/target_file/templates/model.rb.erb new file mode 100644 index 0000000..4ec50f1 --- /dev/null +++ b/lib/generators/dunlop/target_file/templates/model.rb.erb @@ -0,0 +1,13 @@ +class TargetFile::<%= class_name %> < TargetFile + # self.builder_class = WorkflowInstance::IndexCsvBuilder + + # Generate file + def generate_file + working_file.puts "My Custom Data" + end + + ## Comment out generate_file and set builder_class and resource results to use Dunlop's CsvBuilder structure + #def resource + # WorkflowInstance.for_target_file_<%= file_name %> + #end +end diff --git a/lib/generators/dunlop/target_file/templates/model_spec.rb.erb b/lib/generators/dunlop/target_file/templates/model_spec.rb.erb new file mode 100644 index 0000000..46c1412 --- /dev/null +++ b/lib/generators/dunlop/target_file/templates/model_spec.rb.erb @@ -0,0 +1,23 @@ +require 'rails_helper' + +describe TargetFile::<%= class_name %> do + subject { create 'target_file/<%= file_name %>' } + + describe "#execute_generation_process" do + + it "generates the file" do + subject.execute_generation_process + + subject.reload.should_not have_log_entries /ERROR/ + subject.should be_completed + + File.basename(subject.original_file.path).should == "<%= file_name %>-#{Date.current.to_s(:number)}.csv" + + actual_string = read_zipfile(subject.original_file.path) + + actual_string.should eq <<-EXP.strip_heredoc + Expected content for taget file <%= file_name %> + EXP + end + end +end diff --git a/lib/generators/dunlop/workflow_step/USAGE b/lib/generators/dunlop/workflow_step/USAGE new file mode 100644 index 0000000..4fded0d --- /dev/null +++ b/lib/generators/dunlop/workflow_step/USAGE @@ -0,0 +1,8 @@ +Description: + Explain the generator + +Example: + rails generate workflow_step Thing + + This will create: + what/will/it/create diff --git a/lib/generators/dunlop/workflow_step/templates/factories.rb.erb b/lib/generators/dunlop/workflow_step/templates/factories.rb.erb new file mode 100644 index 0000000..4f6a3ce --- /dev/null +++ b/lib/generators/dunlop/workflow_step/templates/factories.rb.erb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :<%= file_name%> +<%- scenarios.each do |scenario| -%> + factory '<%= file_name %>/<%= scenario %>', aliases: [:<%= file_name %>_<%= scenario %>], class: '<%= class_name %>::<%= scenario.classify %>', parent: :<%= file_name %> +<%- end -%> +end diff --git a/lib/generators/dunlop/workflow_step/templates/feature_spec.rb.erb b/lib/generators/dunlop/workflow_step/templates/feature_spec.rb.erb new file mode 100644 index 0000000..484a16d --- /dev/null +++ b/lib/generators/dunlop/workflow_step/templates/feature_spec.rb.erb @@ -0,0 +1,21 @@ +require 'rails_helper' + +RSpec.feature "<%= class_name %> for <%= scenario.classify %>" do + it_behaves_like "a workflow step" do + let(:workflow_step){ workflow_instance.<%= file_name %> } + let :attributes do + { + <%- attributes.each do |attribute| -%> + <%- if attribute.boolean? -%> + <%= attribute.name %>: { type: :boolean }, + <%- elsif attribute.name =~ /window_(from|to)/ -%> + <%= attribute.name %>: { type: :<%= attribute.name %>}, + <%- else -%> + <%= attribute.name %>: { type: :<%= attribute.type %>}, + <%- end -%> + <%- end -%> + } + end + end +end + diff --git a/lib/generators/dunlop/workflow_step/templates/migration.rb.erb b/lib/generators/dunlop/workflow_step/templates/migration.rb.erb new file mode 100644 index 0000000..4a7e27d --- /dev/null +++ b/lib/generators/dunlop/workflow_step/templates/migration.rb.erb @@ -0,0 +1,24 @@ +class Create<%= class_name.pluralize %> < ActiveRecord::Migration + def change + create_table :<%= file_name.pluralize %> do |t| + t.belongs_to :workflow_instance, index: true + t.string :state + t.string :sti_type + t.text :notes + + <%- attributes.each do |attribute| -%> + <%- if attribute.boolean? -%> + t.boolean :<%= attribute.name %>, default: false, null: false + <%- else -%> + t.<%= attribute.type %> :<%= attribute.name %> + <%- end -%> + <%- end -%> + + t.timestamps null: false + end + + <%- scenarios.map(&:classify).each do |scenario| -%> + WorkflowInstance::<%= scenario %>.pluck(:id).each{|id| <%= class_name %>::<%= scenario %>.create!(workflow_instance_id: id) } + <%- end -%> + end +end diff --git a/lib/generators/dunlop/workflow_step/templates/scenario_collection_model.rb.erb b/lib/generators/dunlop/workflow_step/templates/scenario_collection_model.rb.erb new file mode 100644 index 0000000..9a6c82d --- /dev/null +++ b/lib/generators/dunlop/workflow_step/templates/scenario_collection_model.rb.erb @@ -0,0 +1,18 @@ +class <%= class_name %>::<%= scenario.classify %>::Collection < RecordCollection::Base + include WorkflowStepCollection + + <%- attributes.each do |attribute| -%> + <%- if attribute.boolean? -%> + attribute :<%= attribute %>, type: Boolean + <%- elsif %w[window_from window_to].include?(attribute.name) -%> + attribute :<%= attribute %> + <%- elsif attribute.integer? -%> + attribute :<%= attribute %>, type: Integer + <%- elsif attribute.float? -%> + attribute :<%= attribute %>, type: Float + <%- else -%> + attribute :<%= attribute %> + <%- end -%> + <%- end -%> +end + diff --git a/lib/generators/dunlop/workflow_step/templates/scenario_controller.rb.erb b/lib/generators/dunlop/workflow_step/templates/scenario_controller.rb.erb new file mode 100644 index 0000000..a2e8e50 --- /dev/null +++ b/lib/generators/dunlop/workflow_step/templates/scenario_controller.rb.erb @@ -0,0 +1,3 @@ +class <%= class_name %>::<%= scenario.classify.pluralize %>Controller < <%= class_name.pluralize %>Controller +end + diff --git a/lib/generators/dunlop/workflow_step/templates/scenario_model.rb.erb b/lib/generators/dunlop/workflow_step/templates/scenario_model.rb.erb new file mode 100644 index 0000000..87a0afb --- /dev/null +++ b/lib/generators/dunlop/workflow_step/templates/scenario_model.rb.erb @@ -0,0 +1,2 @@ +class <%= class_name %>::<%= scenario.classify %> < <%= class_name %> +end diff --git a/lib/generators/dunlop/workflow_step/templates/translations.yml.erb b/lib/generators/dunlop/workflow_step/templates/translations.yml.erb new file mode 100644 index 0000000..2e09301 --- /dev/null +++ b/lib/generators/dunlop/workflow_step/templates/translations.yml.erb @@ -0,0 +1,14 @@ +en: + activerecord: + models: + <%= file_name %>: <%= file_name.humanize %> + plural: + <%= file_name %>: <%= file_name.humanize.pluralize %> + attributes: + <%= file_name %>: + <%- attributes.each do |attribute| -%> + <%= attribute.name %>: <%= attribute.name.humanize %> + <%- end -%> + + <%= file_name %>: + display_badge: <%= file_name.humanize %> diff --git a/lib/generators/dunlop/workflow_step/templates/views/collection_form.html.slim.erb b/lib/generators/dunlop/workflow_step/templates/views/collection_form.html.slim.erb new file mode 100644 index 0000000..7b03bfc --- /dev/null +++ b/lib/generators/dunlop/workflow_step/templates/views/collection_form.html.slim.erb @@ -0,0 +1,13 @@ += render "workflow_steps/notes", f: f +<%- attributes.each do |attribute| -%> + <%- if %w[window_from window_to].include? attribute.name -%> +.form-inputs= f.optional_input :<%= attribute.name%>, collection: hours_for_select, include_blank: '--' + <%- elsif %w[integer float].include? attribute.type -%> +.form-inputs= f.optional_input :<%= attribute.name%>, as: :<%= attribute.type %> + <%- elsif attribute.type == 'date' -%> +.form-inputs= f.optional_input :<%= attribute.name%>, as: :string, input_html: {class: 'datepicker'} + <%- else -%> +.form-inputs= f.optional_<%= attribute.form_type%> :<%= attribute.name %> + <%- end -%> +<%- end -%> + diff --git a/lib/generators/dunlop/workflow_step/templates/views/general_views/collection_edit.html.slim b/lib/generators/dunlop/workflow_step/templates/views/general_views/collection_edit.html.slim new file mode 100644 index 0000000..cd5aa05 --- /dev/null +++ b/lib/generators/dunlop/workflow_step/templates/views/general_views/collection_edit.html.slim @@ -0,0 +1,2 @@ += render 'workflow_steps/collection_edit' + diff --git a/lib/generators/dunlop/workflow_step/templates/views/general_views/edit.html.slim b/lib/generators/dunlop/workflow_step/templates/views/general_views/edit.html.slim new file mode 100644 index 0000000..8a03adc --- /dev/null +++ b/lib/generators/dunlop/workflow_step/templates/views/general_views/edit.html.slim @@ -0,0 +1,2 @@ += render "workflow_steps/edit" + diff --git a/lib/generators/dunlop/workflow_step/templates/views/general_views/info_fields.html.slim b/lib/generators/dunlop/workflow_step/templates/views/general_views/info_fields.html.slim new file mode 100644 index 0000000..d9090eb --- /dev/null +++ b/lib/generators/dunlop/workflow_step/templates/views/general_views/info_fields.html.slim @@ -0,0 +1,2 @@ += render 'dunlop/badge_info_collection_attributes', local_assigns + diff --git a/lib/generators/dunlop/workflow_step/templates/workflow_step_controller.rb.erb b/lib/generators/dunlop/workflow_step/templates/workflow_step_controller.rb.erb new file mode 100644 index 0000000..ef9eded --- /dev/null +++ b/lib/generators/dunlop/workflow_step/templates/workflow_step_controller.rb.erb @@ -0,0 +1,4 @@ +class <%= class_name.pluralize %>Controller < ApplicationController + include WorkflowStepController +end + diff --git a/lib/generators/dunlop/workflow_step/templates/workflow_step_model.rb.erb b/lib/generators/dunlop/workflow_step/templates/workflow_step_model.rb.erb new file mode 100644 index 0000000..d0f550a --- /dev/null +++ b/lib/generators/dunlop/workflow_step/templates/workflow_step_model.rb.erb @@ -0,0 +1,6 @@ +class <%= class_name %> < ApplicationRecord + include WorkflowStepModel + + activate_dunlop! +end + diff --git a/lib/generators/dunlop/workflow_step/workflow_step_generator.rb b/lib/generators/dunlop/workflow_step/workflow_step_generator.rb new file mode 100644 index 0000000..6a84419 --- /dev/null +++ b/lib/generators/dunlop/workflow_step/workflow_step_generator.rb @@ -0,0 +1,70 @@ +require 'fileutils' +require 'rails/generators/active_record' +require 'dunlop/generators' +class Dunlop::WorkflowStepGenerator < Rails::Generators::NamedBase + include Rails::Generators::Migration + include Dunlop::Generators::GeneratorHelpers + source_root File.expand_path('../templates', __FILE__) + class_option :scenarios, type: :array, default: [] + class_option :attributes, type: :array, default: [] + + def setup_scenarios + unless scenarios.any? + abort "you have not specified the scenarios for the workflow step. Define using\n --scenarios=#{WorkflowInstance.scenario_names.join(' ')}\nor\n --scenarios=all" + end + scenarios.each do |scenario| + @scenario = scenario + #template "workflow_step_model.rb.erb", "app/models/#{workflow_step}/#{file_name}.rb" + #template "workflow_step_collection.rb.erb", "app/models/#{workflow_step}/#{file_name}/collection.rb" + + template "scenario_model.rb.erb", "app/models/#{file_name}/#{scenario}.rb" + template "scenario_collection_model.rb.erb", "app/models/#{file_name}/#{scenario}/collection.rb" + template "scenario_controller.rb.erb", "app/controllers/#{file_name}/#{scenario.pluralize}_controller.rb" + template "feature_spec.rb.erb", "spec/features/workflow_steps/#{file_name}_spec.rb" + end + if behavior == :revoke and (WorkflowInstance.scenario_names - workflow_step.map(&:to_sym)).any? + abort "There are still scenarios associated with the workflow step, so the base models will remain" + end + end + + def add_creation_migration + migration_template "migration.rb.erb", "db/migrate/create_#{file_name.pluralize}.rb" + end + + def create_controller + template "workflow_step_controller.rb.erb", "app/controllers/#{file_name.pluralize}_controller.rb" + if behavior == :revoke + FileUtils.rm_rf Rails.root.join("app/controllers/#{file_name.pluralize}/") + end + end + + def create_model + template "workflow_step_model.rb.erb", "app/models/#{file_name}.rb" + if behavior == :revoke + FileUtils.rm_rf Rails.root.join("app/models/#{file_name}/") + FileUtils.rm_rf Rails.root.join("app/models/#{file_name}.rb") + end + end + + def generate_views + template "translations.yml.erb", "config/locales/workflow_step-#{file_name}.yml" + directory "views/general_views", "app/views/#{file_name.pluralize}" + template "views/collection_form.html.slim.erb", "app/views/#{file_name.pluralize}/_collection_form.html.slim" + + add_to_symbol_list "app/models/workflow_instance.rb", 'setup_possible_workflow_steps', file_name + end + + def generate_spec_helpers + template "factories.rb.erb", "spec/factories/#{file_name}.rb" + end + + private + + def scenario + @scenario + end + + def scenarios + options.scenarios == ['all'] ? WorkflowInstance.scenario_names.map(&:to_s) : options.scenarios + end +end diff --git a/lib/rspec/dunlop.rb b/lib/rspec/dunlop.rb new file mode 100644 index 0000000..8d13b91 --- /dev/null +++ b/lib/rspec/dunlop.rb @@ -0,0 +1,23 @@ +module Rspec + module Dunlop + end +end + +# ACTUAL SPECS THAT CAN/SHOULD BE PERFORMED BY THE APPLICATION +require "rspec/dunlop/self_test" + +# SUPPORT +require "rspec/dunlop/general_helpers" +require "rspec/dunlop/js_helpers" +require "rspec/dunlop/authentication_support" +require "rspec/dunlop/feature_helpers" + +# MATCHERS +require "rspec/dunlop/matchers/be_retried_by_delayed_job" +require "rspec/dunlop/matchers/exceed_query_limit" +require "rspec/dunlop/matchers/have_log_entries" +require "rspec/dunlop/matchers/have_translation" + +# SHARED EXAMPLES +require "rspec/dunlop/shared_examples/workflow_step_behaviour" +require "rspec/dunlop/shared_examples/a_dunlop_application" diff --git a/lib/rspec/dunlop/authentication_support.rb b/lib/rspec/dunlop/authentication_support.rb new file mode 100644 index 0000000..f65ec62 --- /dev/null +++ b/lib/rspec/dunlop/authentication_support.rb @@ -0,0 +1,59 @@ +module Rspec::Dunlop::AuthenticationSupport + + # Add support for: + # visit WorkflowInstance.last + def visit(*args) + case args[0] + when ActiveRecord::Base, Array, Class + args[0] = polymorphic_path(args[0]) + end + super *args + end + + def i_am_logged_in_as_user(*user_args) + if user_args.last.is_a?(Hash) + user_args[-1][:role_names] = Array.wrap(user_args[-1][:role_names]) | ['read-class-WorkflowInstance'] if user_args[-1][:role_names].present? + end + @user = FactoryBot.create(:user, *user_args) + i_am_logged_in_as(@user) + end + + def i_am_logged_in_as_user_of_service_provider + @service_provider ||= FactoryBot.create :service_provider, :all_scenarios + @user = FactoryBot.create(:user, service_provider: @service_provider) + i_am_logged_in_as(@user) + end + + def i_am_logged_in_as_admin + @user = FactoryBot.create(:admin) + i_am_logged_in_as(@user) + end + + def current_user + @user + end + + def i_am_logged_in_as(user) + visit new_user_session_path + fill_in 'Email', with: user.email + fill_in 'Password', with: user.password + click_button 'Log in' + #expect(current_path).to eq root_path + end + + def screenshot + screenshot_and_open_image + end + + def open_page + save_and_open_page + end + + def submit_form + find('input[type="submit"]').click + end + + def submit_collection_form + find('#new_collection input[type="submit"]').click + end +end diff --git a/lib/rspec/dunlop/feature_helpers.rb b/lib/rspec/dunlop/feature_helpers.rb new file mode 100644 index 0000000..25651ea --- /dev/null +++ b/lib/rspec/dunlop/feature_helpers.rb @@ -0,0 +1,5 @@ +module Rspec::Dunlop::FeatureHelpers + include Rspec::Dunlop::JsHelpers + include Rspec::Dunlop::AuthenticationSupport + +end diff --git a/lib/rspec/dunlop/general_helpers.rb b/lib/rspec/dunlop/general_helpers.rb new file mode 100644 index 0000000..0f785b2 --- /dev/null +++ b/lib/rspec/dunlop/general_helpers.rb @@ -0,0 +1,51 @@ +module Rspec::Dunlop::GeneralHelpers + def fixture_file(filename) + File.open fixture_path.join(filename) + end + + def read_zipfile(filename) + case %x[file #{filename}] + when /gzip/i + output = "" + Zlib::GzipReader.open(filename) do |gz| + output << gz.read + end + output + when /zip/i + output = "" + Zip::InputStream::open(filename) do |io| + while (entry = io.get_next_entry) + output << io.read + end + end + output + else + File.read(filename) + end + end + + # Wait for the database to have finished the update stuff, then reload + # the models. Threading might give inconsistent data + def reload_model(*models) + if respond_to?(:page) and page.respond_to?(:driver) + case page.driver.class.name.underscore + when /poltergeist/ + # I don't know, but there seems to be a chronoligical misbehavior overhere + sleep 0.3 + end + end + models.each &:reload + end + alias_method :reload_models, :reload_model + + def click_on_translation(path, options = {}) + translated_value = I18n.t(path, options.merge(default: 'TRANSLATION-NOT-FOUND')) + raise "Translation #{path} not found" if translated_value.blank? or translated_value == 'TRANSLATION-NOT-FOUND' + click_on translated_value + end + + def click_on_selector(selector) + find(selector).click + end + +end diff --git a/lib/rspec/dunlop/js_helpers.rb b/lib/rspec/dunlop/js_helpers.rb new file mode 100644 index 0000000..e4789e5 --- /dev/null +++ b/lib/rspec/dunlop/js_helpers.rb @@ -0,0 +1,85 @@ +module Rspec::Dunlop::JsHelpers + def js_set_date(selector, value = '') + js_set_field selector, value + end + + def js_set_field(selector, value = '') + find selector + page.execute_script("$('#{selector}').val('#{value}').trigger('change')") + end + + + def toggle_optional_input_activator_for(attr, options={}) + js_click "#{options[:selector_prefix]} .optional-input-activator-container.#{attr} .optional-input-activator-toggle" + end + + def toggle_optional_text_field_for(attr) + js_click ".optional-text-field-activator-container.#{attr} .optional-input-activator-toggle" + end + + def set_optional_value(attr, value='', options = {}) + selector = "#{options[:selector_prefix]} .optional-attribute-container.#{attr} input, .optional-attribute-container.#{attr} select, .optional-attribute-container.#{attr} textarea" + find selector + js_set_field selector, value + end + + # Activate the boolean checkbox + def toggle_optional_boolean_activator_for(attr) + js_click ".optional-attribute-container.optional-boolean.#{attr} .optional-boolean-activator-toggle" + end + + # Check the boolean checkbox + def toggle_optional_boolean_for(attr, options={}) + selector = "#{options[:selector_prefix]} .optional-attribute-container.optional-boolean.#{attr} .optional-boolean-toggle" + js_click selector + end + + # Trigger a click event through javascript. Use this to avoid waiting for + # javascript events issues + def js_click(selector) + find(selector) # wait for the page to contain the selector + page.execute_script("$('#{selector}').click()") + + # wait for triggered ajax requests to be finished + steps = 0 + while page.evaluate_script('Dunlop.action_in_progress + $.active').to_i > 0 and steps < 25 + sleep 0.1 + steps += 1 + end + end + + def select_all_workflow_instances + js_click '.selection-toggle-all' + + steps = 0 + #until all('table.with-selection td.selection .checker').all?{|e| e['class'].include? 'checked'} or steps > 15 + until page.evaluate_script("$('table.with-selection td.selection .checker:not(.checked)').length === 0") or steps > 15 + sleep 0.1 + steps += 1 + end + end + + def select_workflow_instance(id) + #page.execute_script %|$("tr[data-record='{\"id\":#{id}}'] .checker").click()| + page.execute_script %|if( found_element = $('table.with-selection tbody tr').filter(function(i, e){ return $(e).data('record').id == #{id}})[0]){ $(found_element).find('.checker:not(.checked)').click()}| + sleep 0.1 + end + + def deselect_workflow_instance(id) + page.execute_script %|if( found_element = $('table.with-selection tbody tr').filter(function(i, e){ return $(e).data('record').id == #{id}})[0]){ $(found_element).find('.checker.checked').click()}| + sleep 0.1 + end + + def expect_path_bo_be(path) + steps = 0 + while page.current_path != path and steps < 15 + sleep 0.1 + steps += 1 + end + expect( page.current_path ).to eq path + end + + def disable_the_datepicker + page.driver.execute_script %|$('.datepicker').pickadate('stop');$.fn.pickadate = function(){}| rescue nil + end +end diff --git a/lib/rspec/dunlop/matchers/be_retried_by_delayed_job.rb b/lib/rspec/dunlop/matchers/be_retried_by_delayed_job.rb new file mode 100644 index 0000000..d7feb30 --- /dev/null +++ b/lib/rspec/dunlop/matchers/be_retried_by_delayed_job.rb @@ -0,0 +1,24 @@ +module Matchers + class BeRetriedByDelayedJob + def matches?(expected) + @failure_message = "This expectation only works with Delayed::Worker.delay_jobs = true" and return false unless Delayed::Worker.delay_jobs + begin + expected.reload + rescue => e + @failure_message = "The job no longer exists in the database" and return false + end + + @failure_message = "The job has a failed_at date so will not be retried" and return false unless expected.failed_at.blank? + true + end + + def failure_message + @failure_message || "Job will not be retried" + end + + end + + def be_retried_by_delayed_job(*args, &block) + BeRetriedByDelayedJob.new(*args, &block) + end +end diff --git a/lib/rspec/dunlop/matchers/exceed_query_limit.rb b/lib/rspec/dunlop/matchers/exceed_query_limit.rb new file mode 100644 index 0000000..4ed97cd --- /dev/null +++ b/lib/rspec/dunlop/matchers/exceed_query_limit.rb @@ -0,0 +1,78 @@ +# taken from: http://stackoverflow.com/questions/5490411/counting-the-number-of-queries-performed +module ActiveRecord + class QueryCounter + + attr_reader :query_count, :performed_queries + + def initialize + @performed_queries = [] + @query_count = 0 + end + + def to_proc + lambda(&method(:callback)) + end + + def callback(name, start, finish, message_id, values) + count_query = true + count_query = false if %w[ CACHE SCHEMA SAVEPOINT ].include?(values[:name]) or values[:sql]['SAVEPOINT'] + count_query = false if %w[ BEGIN COMMIT ].include?(values[:sql]) + if count_query + @query_count += 1 + @performed_queries << values[:sql] + end + end + + end +end + +RSpec::Matchers.define :exceed_query_limit do |expected| + + match do |block| + query_count(&block) > expected + end + + failure_message_when_negated do |actual| + "Expected to run maximum #{expected} queries, got #{@counter.query_count}\n#{@counter.performed_queries.join("\n")}" + end + + failure_message do |actual| + "Expected to run minimum #{expected} queries, got #{@counter.query_count}\n#{@counter.performed_queries.join("\n")}" + end + + def query_count(&block) + @counter = ActiveRecord::QueryCounter.new + ActiveSupport::Notifications.subscribed(@counter.to_proc, 'sql.active_record', &block) + @counter.query_count + end + + def supports_block_expectations? + true + end + +end + +RSpec::Matchers.define :have_query_count do |expected| + match do |block| + query_count(&block) == expected + end + + failure_message_when_negated do |actual| + "Expected to run #{expected} queries, got #{@counter.query_count}\n#{@counter.performed_queries.join("\n")}" + end + + failure_message do |actual| + "Expected to not run #{expected} queries, got #{@counter.query_count}\n#{@counter.performed_queries.join("\n")}" + end + + def query_count(&block) + @counter = ActiveRecord::QueryCounter.new + ActiveSupport::Notifications.subscribed(@counter.to_proc, 'sql.active_record', &block) + @counter.query_count + end + + def supports_block_expectations? + true + end + +end diff --git a/lib/rspec/dunlop/matchers/have_log_entries.rb b/lib/rspec/dunlop/matchers/have_log_entries.rb new file mode 100644 index 0000000..ea06e73 --- /dev/null +++ b/lib/rspec/dunlop/matchers/have_log_entries.rb @@ -0,0 +1,35 @@ +### +# record.should_not have_log_entries +# record.should have_log_entries /blablabla/ +# record.should have_log_entries "blablabla" +### +RSpec::Matchers.define :have_log_entries do |expected| + + match do |record| + if expected.present? + record.log_entries.select{|log_entry| log_entry.body.to_s[expected]}.any? + else + record.log_entries.any? + end + end + + failure_message_when_negated do |record| + log_entry_separator = "\n\n-------------------------------------------------------------------------\n\n" + entry_messages = record.log_entries.map{|l| "#{l.body}\n#{l.backtrace.to_s.split("\n")[0..10].join("\n")}"} + message = <<-ERROR_MESSAGE + No log entries were expected for the record, but #{record.log_entries.size} found" + #{entry_messages.join(log_entry_separator)} + ERROR_MESSAGE + message + end + + failure_message do |record| + "Expected to find log entry #{expected} in:\n\n#{record.log_entries.map(&:body).join("\n")}" + end + + def supports_block_expectations? + false + end + +end +RSpec::Matchers.alias_matcher :have_log_entry, :have_log_entries diff --git a/lib/rspec/dunlop/matchers/have_translation.rb b/lib/rspec/dunlop/matchers/have_translation.rb new file mode 100644 index 0000000..0048e68 --- /dev/null +++ b/lib/rspec/dunlop/matchers/have_translation.rb @@ -0,0 +1,21 @@ +RSpec::Matchers.define :have_translation do |expected| + + match do |page| + translated_value = I18n.t(expected, default: 'no_translation_found') + if translated_value == 'no_translation_found' + @failure_message = "there is no expected translation for #{expected}" + false + else + page.has_content? translated_value + end + end + + failure_message do |page| + @failure_message || "Expected to find translation for #{expected} in:#{page.current_path}" + end + + def supports_block_expectations? + false + end +end +RSpec::Matchers.alias_matcher :have_a_translation_for, :have_translation diff --git a/lib/rspec/dunlop/self_test.rb b/lib/rspec/dunlop/self_test.rb new file mode 100644 index 0000000..f8172fb --- /dev/null +++ b/lib/rspec/dunlop/self_test.rb @@ -0,0 +1,22 @@ +class Rspec::Dunlop::SelfTest + def fail_with(message) + Failure.new(message) + end + + class Failure < Struct.new(:message) + def blank? + true + end + def present? + false + end + def falsy? + true + end + end +end + +require "rspec/dunlop/self_test/integrity" +class Rspec::Dunlop::SelfTest + include Rspec::Dunlop::SelfTest::Integrity +end diff --git a/lib/rspec/dunlop/self_test/integrity.rb b/lib/rspec/dunlop/self_test/integrity.rb new file mode 100644 index 0000000..89f4d64 --- /dev/null +++ b/lib/rspec/dunlop/self_test/integrity.rb @@ -0,0 +1,44 @@ +module Rspec::Dunlop::SelfTest::Integrity + extend ActiveSupport::Concern + Pair = Struct.new(:key, :value) + + # it{ should be_properly_installed_and_configured } + def properly_installed_and_configured? + if defined?(TargetFile) + target_file_valid? + end + if defined?(SourceFile) + source_file_valid? + end + true + end + + def target_file_valid? + # Naming uniqueness + test_duplicate_model_names TargetFile.classes + end + + def source_file_valid? + test_duplicate_model_names SourceFile.classes + end + + def test_duplicate_model_names(models) + singular_names = models.map{|cls| Pair.new(cls.name, cls.model_name.human) } + duplicate_singular_names = singular_names.group_by(&:value).select { |k, vs| vs.size > 1 } + if duplicate_singular_names.present? + message = duplicate_singular_names.map{|name, pairs| "The models #{pairs.map(&:key).join(', ')} have a duplicate singular name #{name}"}.join("\n") + raise DunlopValidationError.new(message) + end + + plural_names = models.map{|cls| Pair.new(cls.name, cls.model_name.human_plural) } + duplicate_plural_names = plural_names.group_by(&:value).select { |k, vs| vs.size > 1 } + if duplicate_plural_names.present? + message = duplicate_plural_names.map{|name, pairs| "The models #{pairs.map(&:key).join(', ')} have a duplicate plural name #{name}"}.join("\n") + raise DunlopValidationError.new(message) + end + end + + class DunlopValidationError < StandardError + + end +end diff --git a/lib/rspec/dunlop/shared_examples/a_dunlop_application.rb b/lib/rspec/dunlop/shared_examples/a_dunlop_application.rb new file mode 100644 index 0000000..9ad5434 --- /dev/null +++ b/lib/rspec/dunlop/shared_examples/a_dunlop_application.rb @@ -0,0 +1,94 @@ +RSpec.shared_examples_for "a dunlop application" do |application_specific_options| + DunlopTestKeyValuePair = Struct.new(:key, :value) + let(:default_dunlop_test_options) {{ + allow_duplicate_model_names: false + }} + let(:dunlop_test_options) { default_dunlop_test_options.merge(application_specific_options || {}) } + + def test_duplicate_model_names(models) + return if dunlop_test_options[:allow_duplicate_model_names] + singular_names = models.map{|cls| DunlopTestKeyValuePair.new(cls.name, cls.model_name.human) } + duplicate_singular_names = singular_names.group_by(&:value).select { |k, vs| vs.size > 1 } + if duplicate_singular_names.present? + message = duplicate_singular_names.map{|name, pairs| "The models #{pairs.map(&:key).join(', ')} have a duplicate singular name #{name}"}.join("\n") + duplicate_singular_names.should be_empty, message + end + + plural_names = models.map{|cls| DunlopTestKeyValuePair.new(cls.name, cls.model_name.human_plural) } + duplicate_plural_names = plural_names.group_by(&:value).select { |k, vs| vs.size > 1 } + if duplicate_plural_names.present? + message = duplicate_plural_names.map{|name, pairs| "The models #{pairs.map(&:key).join(', ')} have a duplicate plural name #{name}"}.join("\n") + duplicate_plural_names.should be_empty, message + end + end + + def check_model_columns(model, columns, options = {}) + invalid_columns = [] + columns.each do |name, spec| + spec = {type: spec} if spec.is_a?(Symbol) + invalid_columns.push "column #{name} with #{spec.map{|k,v| "#{k}: #{v}"}.join(', ')} is missing" and next unless column = model.columns_hash[name.to_s] + invalid_columns.push "column #{name} has type #{column.type} but type #{spec[:type]} was expected" unless column.type == spec[:type] + end + invalid_columns.should(be_empty, "The model #{model.name} is invalid\n #{invalid_columns.join("\n ")}\n#{options[:extra_message]}") + end + + it "has an application name" do + Rails.application.config.application_name.should be_present + end + + it "has a user admin column" do + check_model_columns User, { + admin: :boolean + }, extra_message: <<-MIGRATION + + create a migration: bundle exec rails generate migration add_admin_to_users admin:boolean + + add_column :users, :admin, :boolean, null: false, default: false + MIGRATION + end + + it "has token-authentication columns" do + check_model_columns User, { + authentication_token: :string, + authentication_token_created_at: :datetime + }, extra_message: <<-MIGRATION + + create a migration: bundle exec rails generate migration add_token_authenticatable_columns_to_users + + add_column :users, :authentication_token, :string + add_column :users, :authentication_token_created_at, :datetime + add_index :users, :authentication_token + + and don't forget to add: + devise :database_authenticatable, :token_authenticatable, ... + to the User's model to activate it + MIGRATION + end + + if Dunlop.has_source_files? + it "has proper source_file columns" do + check_model_columns(SourceFile, + creator_id: :integer, + creator_type: :string, + ) + end + + it "has a unique translation for all source files" do + test_duplicate_model_names SourceFile.classes + end + end + + if Dunlop.has_target_files? + it "has a unique translation for all target files" do + test_duplicate_model_names TargetFile.classes + end + end + + it "has proper log_entry columns" do + check_model_columns(Dunlop::LogEntry, + body: :text, + backtrace: :text, + ) + end +end + diff --git a/lib/rspec/dunlop/shared_examples/workflow_step_behaviour.rb b/lib/rspec/dunlop/shared_examples/workflow_step_behaviour.rb new file mode 100644 index 0000000..23943e2 --- /dev/null +++ b/lib/rspec/dunlop/shared_examples/workflow_step_behaviour.rb @@ -0,0 +1,236 @@ +RSpec.shared_examples_for "a workflow step" do |workflow_step_name| + background { i_am_logged_in_as_admin } + let(:attributes) { Hash.new } + let(:time) { Time.current } + let(:workflow_step){ workflow_instance.public_send(workflow_step_name) } + let(:workflow_instance_factory) { :workflow_instance } + before { Timecop.freeze time } + after { Timecop.return } + + context "selecting workflow_instance from workflow instances page", js: true do + let(:workflow_instance) { create workflow_instance_factory } + scenario "sunny" do + return unless workflow_step_setup_is_valid? + workflow_step.replace_notes! 'Previous notes' + + visit polymorphic_path([workflow_instance.class]) + + select_all_workflow_instances + + find(".#{workflow_step.process_name.pluralize}-actions").click + within '#workflow-step-modal' do + # Has contractor info + expect( page ).to have_content "#{workflow_step.class.model_name.human} actions" + end + + set_all_attributes selector_prefix: '#workflow-step-modal' + js_click('.workflow-step-process-button') + + reload_models workflow_instance, workflow_step + + expect( workflow_instance ).to be_active + + expect( workflow_step ).to be_processing + expect( workflow_step.notes ).to start_with <<-NOTES.strip_heredoc.strip + Previous notes + + #{time.iso8601} #{@user.email} + NOTES + + expect( workflow_step.notes ).to end_with <<-NOTES.strip_heredoc.strip + [STATE_CHANGE] pending => processing + NOTES + + validate_attributes_on_model + end + end + + context "use the non collection resource edit path for editing", js: true do + let(:workflow_instance) { create workflow_instance_factory, :unplanned } + scenario 'sunny' do + workflow_step.replace_notes! 'Previous notes' + + visit polymorphic_path([:edit, workflow_step]) + + # Set a value for all attributes + set_all_attributes + find('.workflow-step-complete-button').click + + sleep 0.3 # thread issue + reload_models workflow_instance, workflow_step + + expect( workflow_step ).to be_completed + expect( workflow_step.notes ).to start_with "Previous notes" + expect( workflow_step.notes ).to end_with "[STATE_CHANGE] pending => completed" + + #change_event = ChangeEvent.last + #expect( change_event.user ).to eq @user + #expect( change_event.changes_log['state'] ).to eq %w[pending completed] + + validate_attributes_on_model + end + end + + context "edit from the batch page", js: true do + let(:workflow_instance_batch) { create workflow_instance_factory.to_s.sub('workflow_instance', 'workflow_instance_batch') } + let(:workflow_instance) { create workflow_instance_factory, workflow_instance_batch: workflow_instance_batch } + scenario 'sunny' do + workflow_step.replace_notes! 'Previous notes' + workflow_instance_not_in_batch = create :workflow_instance + + visit workflow_instance_batch + find(".#{workflow_step.process_name.pluralize}-actions").click + + # Set a value for all attributes + set_all_attributes selector_prefix: '#workflow-step-modal' + js_click('.workflow-step-process-button') + + expect( page.current_path ).to eq polymorphic_path(workflow_instance_batch) + + reload_models workflow_instance, workflow_instance_not_in_batch, workflow_step + + expect( workflow_instance ).to be_active + expect( workflow_instance_not_in_batch ).to be_unplanned + + expect( workflow_step ).to be_processing + expect( workflow_step.notes ).to start_with <<-NOTES.strip_heredoc.strip + Previous notes + + #{time.iso8601} #{@user.email} + NOTES + expect( workflow_step.notes ).to end_with "[STATE_CHANGE] pending => processing" + + validate_attributes_on_model + end + end + + context "edit from the workflow instance show page", js: true do + let(:workflow_instance) { create workflow_instance_factory, :planned } + scenario 'sunny' do + workflow_step.replace_notes! 'Previous notes' + + visit workflow_instance + js_click ".#{workflow_step.process_name}.collection-edit" + + # Set a value for all attributes + set_all_attributes + find('.workflow-step-complete-button').click + + expect( page.current_path ).to eq polymorphic_path(workflow_instance) + reload_models workflow_instance, workflow_step + + expect( workflow_instance ).to be_active + + expect( workflow_step ).to be_completed + expect( workflow_step.notes ).to start_with <<-NOTES.strip_heredoc.strip + Previous notes + + #{time.iso8601} #{@user.email} + NOTES + expect( workflow_step.notes ).to end_with "[STATE_CHANGE] pending => completed" + + validate_attributes_on_model + end + end + + context "edit from a state badge show", js: true do + let(:workflow_instance) { create workflow_instance_factory, :planned } + scenario 'sunny' do + workflow_step.replace_notes! 'Previous notes' + + visit polymorphic_path([workflow_instance]) + js_click ".#{workflow_step.process_name}.display-badge" + js_click "#display-badge-info-popup .#{workflow_step.process_name}.collection-edit" + + # Set a value for all attributes + set_all_attributes + find('.workflow-step-complete-button').click + + expect( page.current_path ).to eq polymorphic_path(workflow_instance) + reload_models workflow_instance, workflow_step + + expect( workflow_instance ).to be_active + + expect( workflow_step ).to be_completed + expect( workflow_step.notes ).to start_with <<-NOTES.strip_heredoc.strip + Previous notes + + #{time.iso8601} #{@user.email} + NOTES + expect( workflow_step.notes ).to end_with "[STATE_CHANGE] pending => completed" + + validate_attributes_on_model + end + end + + def set_all_attributes(options = {}) + # Set a value for all attributes + attributes.each do |attr, attr_options| + test_value = attr_options[:test_value] + case attr_options[:type] + when :boolean + toggle_optional_boolean_for attr, options + when :date + disable_the_datepicker + # Problem with uniform_attribute if all are nil at the moment + #toggle_optional_input_activator_for(attr) + set_optional_value(attr, test_date.iso8601, options) + when :string + set_optional_value attr, test_value || 'teststr', options + when :window_from + # Give an existing id, then make it empty + workflow_step.class.where(id: workflow_step.id).update_all(attr => 8 * 60) + # Empty value must be possible, this test is important to find situations where empty string is casted to 0 in stead of nil + set_optional_value attr, '', options + when :window_to + set_optional_value attr, '13:00' + when :integer + set_optional_value attr, '92', options + when :float + set_optional_value attr, '99.9', options + end + end + end + + def validate_attributes_on_model + # Check if the values are actually set and added to the notes field + attributes.each do |attr, attr_options| + human_name = workflow_step.class.human_attribute_name(attr) + case attr_options[:type] + when :boolean + expect( workflow_step[attr] ).to be(true), "attribute :#{attr} is not set" + expect( workflow_step.notes ).to include "[CHECK_CHANGE] #{human_name} changed to yes" + when :date + expect( workflow_step[attr] ).to eq(test_date), "attribute :#{attr} is not set" + expect( workflow_step.notes ).to include "[CHANGE] #{human_name} changed to #{test_date.iso8601}" + when :string + test_value = attr_options[:test_value] || 'teststr' + expect( workflow_step[attr] ).to eq(test_value), "attribute :#{attr} is not set" + expect( workflow_step.notes ).to include "[CHANGE] #{human_name} changed to #{test_value}" + when :window_from + expect( workflow_step[attr] ).to eq(nil), "attribute :#{attr} is not set" + expect( workflow_step.notes ).to include "[CHANGE] #{human_name} changed to [EMPTY]" + when :window_to + expect( workflow_step[attr] ).to eq('13:00'), "attribute :#{attr} is not set" + expect( workflow_step.notes ).to include "[CHANGE] #{human_name} changed to 13" + when :integer + expect( workflow_step[attr] ).to eq(92), "attribute :#{attr} is not set" + expect( workflow_step.notes ).to include "[CHANGE] #{human_name} changed to 92" + when :float + expect( workflow_step[attr] ).to eq(99.9), "attribute :#{attr} is not set" + expect( workflow_step.notes ).to include "[CHANGE] #{human_name} changed to 99.9" + end + end + end + + + def test_date + 1.year.from_now.to_date + end + + def workflow_step_setup_is_valid? + raise "Workflow instance #{workflow_instance.class.name} has no workflow step #{workflow_step_name}" unless workflow_step.present? + true + end + +end diff --git a/lib/tasks/dunlop_tasks.rake b/lib/tasks/dunlop_tasks.rake new file mode 100644 index 0000000..2341b4b --- /dev/null +++ b/lib/tasks/dunlop_tasks.rake @@ -0,0 +1,4 @@ +# desc "Explaining what the task does" +# task :dunlop do +# # Task goes here +# end diff --git a/spec/config/initializers/human_plural_spec.rb b/spec/config/initializers/human_plural_spec.rb new file mode 100644 index 0000000..1902028 --- /dev/null +++ b/spec/config/initializers/human_plural_spec.rb @@ -0,0 +1,55 @@ +require 'rails_helper' +class ModelNameTest + extend ActiveModel::Naming + def self.i18n_scope + 'activerecord' + end +end +module ModelNameTestRootModel + class NestedModelNameTest + extend ActiveModel::Naming + def self.i18n_scope + 'activerecord' + end + end +end + +describe '.human_plural' do + + def with_translation_modifications(&block) + original = I18n.backend.instance_variable_get('@translations').deep_dup + block.call + I18n.backend.instance_variable_set('@translations', original) + end + let(:translations){ I18n.backend.instance_variable_get('@translations') } + it "works when translation is specified in locales" do + User.model_name.human_plural.should eq 'Gebruikers' + end + + it "Falls back to reasonable human.pluralize when no translation can be found" do + ModelNameTest.model_name.human_plural.should eq 'Model name tests' + end + + describe 'nested' do + subject { ModelNameTestRootModel::NestedModelNameTest.model_name } + + it "falls back to unnested path if nested path is not given" do + with_translation_modifications do + translations[:nl][:activerecord][:models][:plural][:model_name_test_root_model] = "Non sti version human plural" + subject.human_plural.should eq "Non sti version human plural" + end + end + + it "chooses the most specific translation if available" do + with_translation_modifications do + translations[:nl][:activerecord][:models][:plural][:model_name_test_root_model] = "Non sti version human plural" + translations[:nl][:activerecord][:models][:plural][:"model_name_test_root_model/nested_model_name_test"] = "Sti version human plural" + subject.human_plural.should eq "Sti version human plural" + end + end + + it "returns a resonable default when not given" do + subject.human_plural.should eq 'Nested model name tests' + end + end +end diff --git a/spec/controllers/dunlop/source_files_controller_spec.rb b/spec/controllers/dunlop/source_files_controller_spec.rb new file mode 100644 index 0000000..0c1ec78 --- /dev/null +++ b/spec/controllers/dunlop/source_files_controller_spec.rb @@ -0,0 +1,36 @@ +require 'rails_helper' + +RSpec.describe Dunlop::SourceFilesController do + routes { Dunlop::Engine.routes } + before { sign_in user} + describe 'authorized' do + let(:user) { create :user, role_names: ['manage-model-source-file/my-source'] } + describe '#download' do + it "downloads the model and logs it" do + model = create 'source_file/my_source' + get :download, params: {id: model.id} + response.body.should eq <<-CSV.strip_heredoc + my_date,my_var,My count,my_num,my_truth + 2015-05-09,abc D,19,4.8,true + 2015-11-09,def G,19,7.8,false + CSV + + download = Dunlop::FileDownload.last + download.user.should eq user + download.file.should eq model + end + end + + end + + describe 'unauthorized' do + let(:user) { create :user, role_names: [] } + describe '#download' do + it "403" do + model = create 'source_file/my_source' + get :download, params: {id: model.id} + response.status.should eq 403 + end + end + end +end diff --git a/spec/controllers/dunlop/target_files_controller_spec.rb b/spec/controllers/dunlop/target_files_controller_spec.rb new file mode 100644 index 0000000..047a2ad --- /dev/null +++ b/spec/controllers/dunlop/target_files_controller_spec.rb @@ -0,0 +1,59 @@ +require 'rails_helper' + +RSpec.describe Dunlop::TargetFilesController do + routes { Dunlop::Engine.routes } + before { sign_in user} + describe 'authorized' do + let(:user) { create :user, role_names: ['manage-model-target-file/my-target'] } + describe '#download' do + it "downloads the model and logs it" do + model = create 'target_file/my_target' + model.execute_generation_process + get :download, params: {id: model.id} + response.should be_ok + response.headers['Content-Disposition'].should start_with 'attachment; filename="my_target-' + response.headers['Content-Disposition'].should end_with '.csv.gz"' + + download = Dunlop::FileDownload.last + download.user.should eq user + download.file.should eq model + end + end + + describe '#download_latest' do + it "downloads the latest model and logs it" do + model = create 'target_file/my_target' + model.execute_generation_process + get :download_latest, params: {target_file_type: 'my_target'} + response.should be_ok + response.headers['Content-Disposition'].should start_with 'attachment; filename="my_target-' + response.headers['Content-Disposition'].should end_with '.csv.gz"' + + download = Dunlop::FileDownload.last + download.user.should eq user + download.file.should eq model + end + end + end + + describe 'unauthorized' do + let(:user) { create :user, role_names: [] } + describe '#download' do + it "403" do + model = create 'target_file/my_target' + model.execute_generation_process + get :download, params: {id: model.id} + response.status.should eq 403 + end + end + + describe '#download_latest' do + it "403" do + model = create 'target_file/my_target' + model.execute_generation_process + get :download_latest, params: {target_file_type: 'my_target'} + response.status.should eq 403 + end + end + end +end diff --git a/spec/controllers/users/update_roles_spec.rb b/spec/controllers/users/update_roles_spec.rb new file mode 100644 index 0000000..f916210 --- /dev/null +++ b/spec/controllers/users/update_roles_spec.rb @@ -0,0 +1,55 @@ +require 'rails_helper' + +describe Dunlop::UsersController, type: :controller, broken: Rails.version.starts_with?('4') do + routes { Dunlop::Engine.routes } + before { sign_in user } + + context "without abilities" do + let(:user) { create :user, role_names: [], role_names_admin: [] } + + it "cannot add abilities through update_profile" do + patch :update_profile, params: {user: {role_names: ['my-hackery-UserRole'], role_names_admin: ['my-hackery-UserAdminRole']}} + user.reload.role_names.should be_empty + user.role_names_admin.should be_empty + end + + it "cannot add abilities through personal route by id" do + patch :update, params: {id: user.id, user: {role_names: ['my-hackery-UserRole'], role_names_admin: ['my-hackery-UserAdminRole']}} + user.reload.role_names.should be_empty + user.role_names_admin.should be_empty + end + + it "cannot list users" do + get :index + response.status.should eq 403 + end + + it "cannot show/edit other user" do + other_user = create :user, role_names: [], role_names_admin: [] + get :show, params: {id: other_user.id} + response.status.should eq 403 + get :edit, params: {id: other_user.id} + response.status.should eq 403 + end + end + + context "with user manage abilities (try to add admin rights mwha mwha mwha)" do + let(:user) { create :user, role_names_admin: ['manage-class-User'] } + + it "cannot set other admin roles" do + patch :update, params: {id: user.id, user: {role_names: ['my-added-UserRole'], role_names_admin: ['my-hackery-UserAdminRole']}} + user.reload.role_names.should eq ['my-added-UserRole'] + user.role_names_admin.should eq ['manage-class-User'] # set in other way + end + + it "can edit other user's role_names, but not role_names_admin" do + other_user = create :user, role_names: [], role_names_admin: [] + patch :update, params: {id: other_user.id, user: {role_names: ['my-added-UserRole'], role_names_admin: ['my-hackery-UserAdminRole']}} + other_user.reload.role_names.should eq ['my-added-UserRole'] + other_user.role_names_admin.should be_empty + end + end + +end + + diff --git a/spec/csv_builders/workflow_instance/index_csv_builder_spec.rb b/spec/csv_builders/workflow_instance/index_csv_builder_spec.rb new file mode 100644 index 0000000..85ff635 --- /dev/null +++ b/spec/csv_builders/workflow_instance/index_csv_builder_spec.rb @@ -0,0 +1,19 @@ +require 'rails_helper' + +describe WorkflowInstance::IndexCsvBuilder do + it "handles windows carriage returns" do + wi = create :workflow_instance_scenario1 + wi.owner_initialization.update(my_explanation: "My explanation\r\n having multiple\rlines\n with extra") + expected_csv = <<-CSV.strip_heredoc + SC1,Core initialisatie,HazBeenChecked,Dattum,My identifier,Window from,Window to,Number of visits,My fraction,My explanation,Contr. initialisatie + ,pending,0,,,,,,,My explanation having multiple lines with extra,pending + CSV + + result = described_class.new([wi], controller: double(record_class: WorkflowInstance::Scenario1)).data + result.should eq expected_csv + + # works with single resource as well + result = described_class.new(wi, controller: double(record_class: WorkflowInstance::Scenario1)).data + result.should eq expected_csv + end +end diff --git a/spec/dummy/CHANGELOG.md b/spec/dummy/CHANGELOG.md new file mode 100644 index 0000000..e8c3dff --- /dev/null +++ b/spec/dummy/CHANGELOG.md @@ -0,0 +1,13 @@ + +Changelog +========= + +All notable changes to this project will be documented in this file. + +v1.2.7 2016-06-17 +------------------- + +### Added +* These un_der_scores are not replaced by italic +* Already escaped\_under\_scores are not double escaped + diff --git a/spec/dummy/Procfile b/spec/dummy/Procfile new file mode 100644 index 0000000..7ff5abc --- /dev/null +++ b/spec/dummy/Procfile @@ -0,0 +1 @@ +web: bundle exec rails server -p 4001 diff --git a/spec/dummy/README.rdoc b/spec/dummy/README.rdoc new file mode 100644 index 0000000..dd4e97e --- /dev/null +++ b/spec/dummy/README.rdoc @@ -0,0 +1,28 @@ +== README + +This README would normally document whatever steps are necessary to get the +application up and running. + +Things you may want to cover: + +* Ruby version + +* System dependencies + +* Configuration + +* Database creation + +* Database initialization + +* How to run the test suite + +* Services (job queues, cache servers, search engines, etc.) + +* Deployment instructions + +* ... + + +Please feel free to use a different markup language if you do not plan to run +rake doc:app. diff --git a/spec/dummy/Rakefile b/spec/dummy/Rakefile new file mode 100644 index 0000000..ba6b733 --- /dev/null +++ b/spec/dummy/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require File.expand_path('../config/application', __FILE__) + +Rails.application.load_tasks diff --git a/spec/dummy/app/assets/images/.keep b/spec/dummy/app/assets/images/.keep new file mode 100644 index 0000000..e69de29 diff --git a/spec/dummy/app/assets/javascripts/application.js.coffee b/spec/dummy/app/assets/javascripts/application.js.coffee new file mode 100644 index 0000000..10d76e9 --- /dev/null +++ b/spec/dummy/app/assets/javascripts/application.js.coffee @@ -0,0 +1,11 @@ +#= require jquery +#= require jquery_ujs +#= require dunlop +#= require_directory . +# require foundation +# require select2 +$ -> + $(document).foundation() + $(document).find('select.smart-select').select2() + Dunlop.setup() + diff --git a/spec/dummy/app/assets/stylesheets/application.sass b/spec/dummy/app/assets/stylesheets/application.sass new file mode 100644 index 0000000..22634b7 --- /dev/null +++ b/spec/dummy/app/assets/stylesheets/application.sass @@ -0,0 +1,7 @@ +//= require dunlop/all +//= require select2 +@import "dunlop/mixins" + +.environment-ribbon + &.development + +env-ribbon(#aaffaa) diff --git a/spec/dummy/app/controllers/application_controller.rb b/spec/dummy/app/controllers/application_controller.rb new file mode 100644 index 0000000..c69514d --- /dev/null +++ b/spec/dummy/app/controllers/application_controller.rb @@ -0,0 +1,31 @@ +class ApplicationController < ActionController::Base + # Prevent CSRF attacks by raising an exception. + # For APIs, you may want to use :null_session instead. + protect_from_forgery with: :exception + + before_action :authenticate_user! + + #http://rails-bestpractices.com/posts/2010/08/23/fetch-current-user-in-models + before_action :set_current_user + + rescue_from CanCan::AccessDenied do + render 'application/403', status: 403 + end + + private + + def set_current_user + request.env["exception_notifier.exception_data"] = { + current_user: current_user.try(:inspect) + } + User.current = current_user + end + + # Return an array of ids given as a param or nil if nothing is supplied + def ids_param + return nil unless ids = params[:ids].presence + @ids_param ||= (ids.is_a?(String) ? ids.split(RecordCollection.ids_separator) : Array.wrap(ids)) + end + helper_method :ids_param + +end diff --git a/spec/dummy/app/controllers/concerns/.keep b/spec/dummy/app/controllers/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/spec/dummy/app/controllers/concerns/workflow_step_controller.rb b/spec/dummy/app/controllers/concerns/workflow_step_controller.rb new file mode 100644 index 0000000..c34be97 --- /dev/null +++ b/spec/dummy/app/controllers/concerns/workflow_step_controller.rb @@ -0,0 +1,107 @@ +module WorkflowStepController + extend ActiveSupport::Concern + + included do + helper_method :record_class + helper_method :workflow_instance_class + before_action :set_record, only: [:edit, :update, :destroy] + end + + def edit + end + + def record_class + @record_class ||= self.class.name.sub(/Controller$/, '').split('::').map(&:singularize).join('::').constantize + end + + def workflow_instance_class + @workflow_instance_class ||= "WorkflowInstance::#{self.class.name.split('::').last.sub(/Controller$/, '').singularize}".constantize + end + + # PATCH/PUT /owner_decommission/migrate_line_onnet_without_cpes/1 + def update + authorize! :update, record_class + @record.assign_attributes(record_params) + if @record.valid? + @record.public_send "#{record_commit_action}!" + redirect_to polymorphic_path([@record.workflow_instance]) + else + render :edit + end + end + + def destroy + + end + + # GET /contractor_completion/parallel_onnet_nls1_with_cpes/collection_edit + def collection_edit + authorize! :edit, record_class + if params[:workflow_instance_ids].present? + params[:workflow_instance_ids] = params[:workflow_instance_ids].split(RecordCollection.ids_separator) if params[:workflow_instance_ids].is_a?(String) + @collection = record_class.collection.includes(:workflow_instance).joins(:workflow_instance).where(workflow_instances: {id: params[:workflow_instance_ids]}) + elsif params[:workflow_instance_batch_id].present? + @workflow_instance_batch = WorkflowInstanceBatch.find(params[:workflow_instance_batch_id]) + @collection = record_class.collection.includes(:workflow_instance).joins(:workflow_instance) + .where(workflow_instances: {workflow_instance_batch_id: @workflow_instance_batch.id}) + .where.not(workflow_instances: {state: 'any_rejected'}) + .refine_relation{ with_last_attribute_changes } + else + @collection = record_class.collection.includes(:workflow_instance).find(ids_param) + end + render layout: params[:reveal] != 'yes' + end + + # GET /contractor_completion/parallel_onnet_nls1_with_cpes/collection_edit + def collection_update + authorize! :update, record_class + @collection = record_class.collection.find(ids_param) # automatic security of find on specific sti class + + # Batch reject security + if params[:workflow_instance_batch_id].present? and record_commit_action == 'reject' + return redirect_to :back, alert: "You cannot reject an entire batch" + end + + #TODO: the following code could be much nicer! + if @collection.trigger_action!(record_commit_action, params[:collection]) + if params[:workflow_instance_batch_id].present? + @workflow_instance_batch = WorkflowInstanceBatch.find(params[:workflow_instance_batch_id]) + redirect_to @workflow_instance_batch + elsif @collection.one? + redirect_to @collection.first.workflow_instance + else + redirect_to polymorphic_path([:selection, workflow_instance_class], ids: @collection.map(&:workflow_instance_id)) + end + else + render :collection_edit + end + end + + private + + + # Translate the form commit action to a consistent useable and safe + # processable name + def record_commit_action + case params[:commit] + when t('workflow_step.process_button') then 'process' + when t('workflow_step.complete_button') then 'complete' + when t('workflow_step.reject_button') then 'reject' + else raise "Unsupported record commit type given: #{params[:commit]}" + end + end + + # Use callbacks to share common setup or constraints between actions. + def set_record + @record = record_class.find(params[:id]) + end + + def record_params + record_param = record_class.name.underscore.gsub('/', '_') + return {} unless params[record_param].present? # Allow empty params, empty form just triggering state change is also an option + #params.require( record_param ).permit( *permitted_attributes ) + params.require( record_param ).permit! #TODO: make secure!!! + end + +end + diff --git a/spec/dummy/app/controllers/contractor_initialization/scenario1s_controller.rb b/spec/dummy/app/controllers/contractor_initialization/scenario1s_controller.rb new file mode 100644 index 0000000..6584719 --- /dev/null +++ b/spec/dummy/app/controllers/contractor_initialization/scenario1s_controller.rb @@ -0,0 +1,3 @@ +class ContractorInitialization::Scenario1sController < ContractorInitializationsController +end + diff --git a/spec/dummy/app/controllers/contractor_initialization/scenario2s_controller.rb b/spec/dummy/app/controllers/contractor_initialization/scenario2s_controller.rb new file mode 100644 index 0000000..09e3a91 --- /dev/null +++ b/spec/dummy/app/controllers/contractor_initialization/scenario2s_controller.rb @@ -0,0 +1,3 @@ +class ContractorInitialization::Scenario2sController < ContractorInitializationsController +end + diff --git a/spec/dummy/app/controllers/contractor_initializations_controller.rb b/spec/dummy/app/controllers/contractor_initializations_controller.rb new file mode 100644 index 0000000..c02b367 --- /dev/null +++ b/spec/dummy/app/controllers/contractor_initializations_controller.rb @@ -0,0 +1,4 @@ +class ContractorInitializationsController < ApplicationController + include WorkflowStepController +end + diff --git a/spec/dummy/app/controllers/dashboard_controller.rb b/spec/dummy/app/controllers/dashboard_controller.rb new file mode 100644 index 0000000..653b0f3 --- /dev/null +++ b/spec/dummy/app/controllers/dashboard_controller.rb @@ -0,0 +1,9 @@ +class DashboardController < ApplicationController + + # GET / + def home + end + + def render_as_dunlop_modal_test + end +end diff --git a/spec/dummy/app/controllers/owner_initialization/scenario1s_controller.rb b/spec/dummy/app/controllers/owner_initialization/scenario1s_controller.rb new file mode 100644 index 0000000..f35ef3a --- /dev/null +++ b/spec/dummy/app/controllers/owner_initialization/scenario1s_controller.rb @@ -0,0 +1,3 @@ +class OwnerInitialization::Scenario1sController < OwnerInitializationsController +end + diff --git a/spec/dummy/app/controllers/owner_initializations_controller.rb b/spec/dummy/app/controllers/owner_initializations_controller.rb new file mode 100644 index 0000000..f8de515 --- /dev/null +++ b/spec/dummy/app/controllers/owner_initializations_controller.rb @@ -0,0 +1,4 @@ +class OwnerInitializationsController < ApplicationController + include WorkflowStepController +end + diff --git a/spec/dummy/app/controllers/wba_submission/scenario2s_controller.rb b/spec/dummy/app/controllers/wba_submission/scenario2s_controller.rb new file mode 100644 index 0000000..8eb2db8 --- /dev/null +++ b/spec/dummy/app/controllers/wba_submission/scenario2s_controller.rb @@ -0,0 +1,3 @@ +class WbaSubmission::Scenario2sController < WbaSubmissionsController +end + diff --git a/spec/dummy/app/controllers/wba_submissions_controller.rb b/spec/dummy/app/controllers/wba_submissions_controller.rb new file mode 100644 index 0000000..59d1166 --- /dev/null +++ b/spec/dummy/app/controllers/wba_submissions_controller.rb @@ -0,0 +1,4 @@ +class WbaSubmissionsController < ApplicationController + include WorkflowStepController +end + diff --git a/spec/dummy/app/controllers/workflow_instance/scenario1s_controller.rb b/spec/dummy/app/controllers/workflow_instance/scenario1s_controller.rb new file mode 100644 index 0000000..2686fac --- /dev/null +++ b/spec/dummy/app/controllers/workflow_instance/scenario1s_controller.rb @@ -0,0 +1,2 @@ +class WorkflowInstance::Scenario1sController < WorkflowInstancesController +end diff --git a/spec/dummy/app/controllers/workflow_instance/scenario2s_controller.rb b/spec/dummy/app/controllers/workflow_instance/scenario2s_controller.rb new file mode 100644 index 0000000..641a6d9 --- /dev/null +++ b/spec/dummy/app/controllers/workflow_instance/scenario2s_controller.rb @@ -0,0 +1,2 @@ +class WorkflowInstance::Scenario2sController < WorkflowInstancesController +end diff --git a/spec/dummy/app/controllers/workflow_instance_batch/scenario1s_controller.rb b/spec/dummy/app/controllers/workflow_instance_batch/scenario1s_controller.rb new file mode 100644 index 0000000..4ebf83e --- /dev/null +++ b/spec/dummy/app/controllers/workflow_instance_batch/scenario1s_controller.rb @@ -0,0 +1,2 @@ +class WorkflowInstanceBatch::Scenario1sController < WorkflowInstanceBatchesController +end diff --git a/spec/dummy/app/controllers/workflow_instance_batch/scenario2s_controller.rb b/spec/dummy/app/controllers/workflow_instance_batch/scenario2s_controller.rb new file mode 100644 index 0000000..4b87111 --- /dev/null +++ b/spec/dummy/app/controllers/workflow_instance_batch/scenario2s_controller.rb @@ -0,0 +1,2 @@ +class WorkflowInstanceBatch::Scenario2sController < WorkflowInstanceBatchesController +end diff --git a/spec/dummy/app/controllers/workflow_instance_batches_controller.rb b/spec/dummy/app/controllers/workflow_instance_batches_controller.rb new file mode 100644 index 0000000..aaf98d1 --- /dev/null +++ b/spec/dummy/app/controllers/workflow_instance_batches_controller.rb @@ -0,0 +1,79 @@ +class WorkflowInstanceBatchesController < ApplicationController + before_action :find_record, only: [:show, :edit, :update, :destroy] + delegate :workflow_instance_batch_class, :workflow_instance_class, to: :class + respond_to :html, :csv + + def index + authorize! :read, workflow_instance_batch_class + @q = workflow_instance_batch_class.search(query) + @q.sorts = "plan_date ASC" if @q.sorts.empty? + @records = @q.result.page(params[:page]).per(params[:per_page]) + end + + def new + authorize! :manage, workflow_instance_batch_class + @record = workflow_instance_batch_class.new + end + + def with_mixed_bas_completeness + @records = workflow_instance_batch_class.with_mixed_bas_load_states + end + + def create + @record = workflow_instance_batch_class.new(record_params) + authorize! :manage, @record + if @record.save + redirect_to action: :index + else + render action: "new" + end + end + + def show + authorize! :read, @record + respond_with @record, record_class: workflow_instance_class, include_notes: params[:include_notes] + end + + def edit + authorize! :manage, @record + end + + def update + authorize! :manage, @record + if @record.update record_params + redirect_to action: :index + else + render action: :edit + end + end + + class << self + def workflow_instance_batch_class + return @workflow_instance_batch_class if @workflow_instance_batch_class + parts = controller_path.split('/').map(&:classify) + parts.pop until klass = parts.join('::').safe_constantize + @workflow_instance_batch_class = klass + end + + def workflow_instance_class + return @workflow_instance_class if @workflow_instance_class + @workflow_instance_class = workflow_instance_batch_class.name.sub('Batch', '').constantize + end + end + helper_method :workflow_instance_batch_class + helper_method :workflow_instance_class + + private + + def record_params + params.require(workflow_instance_batch_class.name.underscore.gsub('/', '_')).permit(:name, :plan_date, :workflow_instance_manager_id) + end + + def find_record + @record = workflow_instance_batch_class.find(params[:id]) + end + + def query + params[:q] + end +end diff --git a/spec/dummy/app/controllers/workflow_instances_controller.rb b/spec/dummy/app/controllers/workflow_instances_controller.rb new file mode 100644 index 0000000..18f8a48 --- /dev/null +++ b/spec/dummy/app/controllers/workflow_instances_controller.rb @@ -0,0 +1,159 @@ +class WorkflowInstancesController < ApplicationController + before_action :set_record, only: [:show, :edit, :update, :destroy, :reset] + before_action :set_collection, only: [:collection_edit, :collection_update, :uncategorize] + delegate :record_class, :record_batch_class, to: :class + + respond_to :html, :csv + + def index + authorize! :read, record_class + set_records_for_index + respond_to do |format| + format.html do + @records = @records.page(params[:page]).per(params[:per_page]) + end + format.csv { respond_with @records, record_class: record_class, include_notes: params[:include_notes] } + end + end + + def show + authorize! :read, @record + @record = @record + respond_with(@record) + end + + def collection_edit + authorize! :read, record_class # not update, since this read can have multiple update authorizations + redirect_to record_class and return unless @collection.any? + if params[:reveal].present? + render layout: false # popup + end + end + + # This is a custom reporting action where an extra filter + # is applied to the full index scope. + # reports + def with_bas_correction + authorize! :read, record_class + set_records_for_index + @records = @records.where(data_corrected_by_bas_data: true) + respond_to do |format| + format.html { render action: :selection } + format.csv { respond_with @records, record_class: record_class, include_notes: params[:include_notes] } + end + end + + # reports + def with_incomplete_bas_records + authorize! :read, record_class + set_records_for_index + @records = @records.with_incomplete_bas_records + respond_to do |format| + format.html do + redirect_to :back, notice: "There are no incompletely enriched DSLAMs" and return unless @records.any? + render action: :selection + end + format.csv { respond_with @records, record_class: record_class, include_notes: params[:include_notes] } + end + end + + def collection_update + authorize! :update, record_class + if @collection.update params[:collection] + redirect_to polymorphic_path([:selection, record_class], ids: @collection.ids) + else + render "collection_edit" + end + end + + def reset + authorize! :reset, @record + @record.reset! + redirect_to @record + end + + # POST /records/selection input=EAP/123; ...ETC.... + def selection + authorize! :read, record_class unless record_class == WorkflowInstance + respond_to do |format| + format.html do + @records = find_records_by_ids_or_input + end + format.csv do + @records = find_records_by_ids_or_input + respond_with @records, record_class: record_class, include_notes: params[:include_notes] + end + end + end + + #def destroy + # authorize! :destroy, @record + # @record.destroy + # respond_with(@record) + #end + + private + + def set_records_for_index + @q = records_scope.search(query) + @q.sorts = "state asc" if @q.sorts.empty? + @records = @q.result.including_relations + end + + def set_record + scope = record_class.scope_for(current_user, all: can?(:archive, record_class)) + @record = scope.find(params[:id]) + end + + def set_collection + if params[:workflow_instance_batch_id].present? + @workflow_instance_batch = WorkflowInstanceBatch.find(params[:workflow_instance_batch_id]) + @collection = record_class.collection.new(@workflow_instance_batch.workflow_instances) + else + @collection = record_class.collection.new(records_scope(all: can?(:read, :archived_records)).includes(:workflow_instance_batch).where(id: ids_param)) + end + end + + def records_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 + + # Used for the dslam_name actions + # The search is used for sorting, not for actual filtering + def find_records_by_ids_or_input(options = {}) + @q = records_scope(all: can?(:archive, record_class)).search(query) + @q.sorts = "state asc" if @q.sorts.empty? + apply_record_input_scope.including_relations + end + + # Apply scope to query by means of given ids or eap input + def apply_record_input_scope + return @q.result.where(id: ids_param) if ids_param.present? # use ids param if supplied + return @q.result.where(workflow_instance_batch_id: params[:workflow_instance_batch_id]) if params[:workflow_instance_batch_id].present? + input = WorkflowInstanceInputScrubber.new( params[:input] ) + selection = @q.result.where dslam_name: input.dslam_names + params[:ids] = selection.pluck(:id) + selection + end + + def query + params[:q] + end + + class << self + def record_class + return @record_class if @record_class + parts = controller_path.split('/').map(&:classify) + parts.pop until klass = parts.join('::').safe_constantize + @record_class = klass + end + + def record_batch_class + return @record_batch_class if @record_batch_class + @record_batch_class = record_class.try(:batch_class) + end + end + helper_method :record_class + helper_method :record_batch_class +end diff --git a/spec/dummy/app/csv_builders/csv_builder.rb b/spec/dummy/app/csv_builders/csv_builder.rb new file mode 100644 index 0000000..b4c427c --- /dev/null +++ b/spec/dummy/app/csv_builders/csv_builder.rb @@ -0,0 +1,3 @@ +class CsvBuilder + include Dunlop::CsvBuilder +end diff --git a/spec/dummy/app/csv_builders/target_file/file_with_builder_csv_builder.rb b/spec/dummy/app/csv_builders/target_file/file_with_builder_csv_builder.rb new file mode 100644 index 0000000..f0690cd --- /dev/null +++ b/spec/dummy/app/csv_builders/target_file/file_with_builder_csv_builder.rb @@ -0,0 +1,13 @@ +class TargetFile::FileWithBuilderCsvBuilder < CsvBuilder + def headers + %w[one two] + end + + def serialize_row(record) + [record.class.name, record.id] + end + + def file_name + "File-With-builder-2017-09-34.csv" + end +end diff --git a/spec/dummy/app/csv_builders/target_file/my_target_csv_builder.rb b/spec/dummy/app/csv_builders/target_file/my_target_csv_builder.rb new file mode 100644 index 0000000..ee16d1e --- /dev/null +++ b/spec/dummy/app/csv_builders/target_file/my_target_csv_builder.rb @@ -0,0 +1,2 @@ +class TargetFile::MyTargetCsvBuilder < CsvBuilder +end diff --git a/spec/dummy/app/csv_builders/workflow_instance/index_csv_builder.rb b/spec/dummy/app/csv_builders/workflow_instance/index_csv_builder.rb new file mode 100644 index 0000000..84b1013 --- /dev/null +++ b/spec/dummy/app/csv_builders/workflow_instance/index_csv_builder.rb @@ -0,0 +1,48 @@ +class WorkflowInstance::IndexCsvBuilder < CsvBuilder + BASE_COLUMNS = %i[] + + def headers + columns = [controller.record_class.model_name.human] + columns += BASE_COLUMNS.map{|c| controller.record_class.human_attribute_name(c)} + # Workflow steps + workflow_step_mapping.each do |klass, attributes| + columns << klass.model_name.human # state column + columns += attributes.map{|attr| klass.human_attribute_name(attr)} + end + columns + end + + def serialize_row(record) + columns = [record.try(:presentation_name)] + columns += BASE_COLUMNS.map{|c| record_attribute(record, c) } + # Workflow steps + workflow_step_mapping.each do |workflow_step_class, attributes| + unless workflow_step = record.public_send(workflow_step_class.process_name) + columns += Array.new(1 + attributes.size) # state + attributes Empty fill + next + end + columns << workflow_step.state + columns += attributes.map{|attr| record_attribute(workflow_step, attr) } + end + columns + end + + # Create a mapping of (klass => attributes): + # { + # WorkflowStepClass => ['notes', 'workflow_step_action_executed', ...], + # .... + # } + def workflow_step_mapping + @workflow_step_mapping ||= begin + controller.record_class.workflow_step_classes.each.with_object({}) do |klass, h| + attributes = klass.collection.attributes.keys + attributes -= ['notes'] unless options[:include_notes].present? + h[klass] = attributes + end + end + end + + def file_name + "#{Rails.application.config.application_name}-export-#{date_number}.csv" + end +end diff --git a/spec/dummy/app/csv_builders/workflow_instance/scenario2/index_csv_builder.rb b/spec/dummy/app/csv_builders/workflow_instance/scenario2/index_csv_builder.rb new file mode 100644 index 0000000..cc3ab39 --- /dev/null +++ b/spec/dummy/app/csv_builders/workflow_instance/scenario2/index_csv_builder.rb @@ -0,0 +1,3 @@ +# implemented to test lookup +class WorkflowInstance::Scenario2::IndexCsvBuilder < WorkflowInstance::IndexCsvBuilder +end diff --git a/spec/dummy/app/csv_builders/workflow_instance/selection_csv_builder.rb b/spec/dummy/app/csv_builders/workflow_instance/selection_csv_builder.rb new file mode 100644 index 0000000..8a78b62 --- /dev/null +++ b/spec/dummy/app/csv_builders/workflow_instance/selection_csv_builder.rb @@ -0,0 +1,3 @@ +class WorkflowInstance::SelectionCsvBuilder < WorkflowInstance::IndexCsvBuilder +end + diff --git a/spec/dummy/app/helpers/application_helper.rb b/spec/dummy/app/helpers/application_helper.rb new file mode 100644 index 0000000..de6be79 --- /dev/null +++ b/spec/dummy/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/spec/dummy/app/mailers/.keep b/spec/dummy/app/mailers/.keep new file mode 100644 index 0000000..e69de29 diff --git a/spec/dummy/app/mailers/application_mailer.rb b/spec/dummy/app/mailers/application_mailer.rb new file mode 100644 index 0000000..d25d889 --- /dev/null +++ b/spec/dummy/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: "from@example.com" + layout 'mailer' +end diff --git a/spec/dummy/app/mailers/owner_mailer.rb b/spec/dummy/app/mailers/owner_mailer.rb new file mode 100644 index 0000000..1ffd7b0 --- /dev/null +++ b/spec/dummy/app/mailers/owner_mailer.rb @@ -0,0 +1,13 @@ +class OwnerMailer < ApplicationMailer + + # Subject can be set in your I18n file at config/locales/en.yml + # with the following lookup: + # + # en.owner_mailer.owner_initialization_finished.subject + # + def owner_initialization_finished(batch, last_finalized_workflow_step) + @greeting = "Hi" + + mail to: "to@example.org" + end +end diff --git a/spec/dummy/app/models/.keep b/spec/dummy/app/models/.keep new file mode 100644 index 0000000..e69de29 diff --git a/spec/dummy/app/models/ability.rb b/spec/dummy/app/models/ability.rb new file mode 100644 index 0000000..8e94694 --- /dev/null +++ b/spec/dummy/app/models/ability.rb @@ -0,0 +1,13 @@ +class Ability + include Dunlop::Ability + add_allowed_authorization_classes 'SourceFile::MySource' + + def setup_abilities + setup_dunlop_abilities + if user.my_domain.present? + can :manage, User, User.for_domain(user.my_domain) do |domain_user| + user.my_domain == domain_user.my_domain and user.role_names_admin.include?('manage-domain-User') + end + end + end +end diff --git a/spec/dummy/app/models/application_record.rb b/spec/dummy/app/models/application_record.rb new file mode 100644 index 0000000..ce22459 --- /dev/null +++ b/spec/dummy/app/models/application_record.rb @@ -0,0 +1,5 @@ +class ApplicationRecord < ActiveRecord::Base + include Dunlop::ApplicationRecordAdditions + +end + diff --git a/spec/dummy/app/models/concerns/.keep b/spec/dummy/app/models/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/spec/dummy/app/models/concerns/workflow_step_collection.rb b/spec/dummy/app/models/concerns/workflow_step_collection.rb new file mode 100644 index 0000000..0cba95a --- /dev/null +++ b/spec/dummy/app/models/concerns/workflow_step_collection.rb @@ -0,0 +1,8 @@ +module WorkflowStepCollection + extend ActiveSupport::Concern + include Dunlop::WorkflowStep::GenericCollection + + included do + attribute :notes + end +end diff --git a/spec/dummy/app/models/concerns/workflow_step_model.rb b/spec/dummy/app/models/concerns/workflow_step_model.rb new file mode 100644 index 0000000..a962d9f --- /dev/null +++ b/spec/dummy/app/models/concerns/workflow_step_model.rb @@ -0,0 +1,7 @@ +module WorkflowStepModel + extend ActiveSupport::Concern + + included do + include Dunlop::WorkflowStepModel + end +end diff --git a/spec/dummy/app/models/contractor_initialization.rb b/spec/dummy/app/models/contractor_initialization.rb new file mode 100644 index 0000000..41d7715 --- /dev/null +++ b/spec/dummy/app/models/contractor_initialization.rb @@ -0,0 +1,6 @@ +class ContractorInitialization < ApplicationRecord + include WorkflowStepModel + + activate_dunlop! +end + diff --git a/spec/dummy/app/models/contractor_initialization/scenario1.rb b/spec/dummy/app/models/contractor_initialization/scenario1.rb new file mode 100644 index 0000000..904fcdc --- /dev/null +++ b/spec/dummy/app/models/contractor_initialization/scenario1.rb @@ -0,0 +1,2 @@ +class ContractorInitialization::Scenario1 < ContractorInitialization +end diff --git a/spec/dummy/app/models/contractor_initialization/scenario1/collection.rb b/spec/dummy/app/models/contractor_initialization/scenario1/collection.rb new file mode 100644 index 0000000..f4344dc --- /dev/null +++ b/spec/dummy/app/models/contractor_initialization/scenario1/collection.rb @@ -0,0 +1,5 @@ +class ContractorInitialization::Scenario1::Collection < RecordCollection::Base + include WorkflowStepCollection + +end + diff --git a/spec/dummy/app/models/contractor_initialization/scenario2.rb b/spec/dummy/app/models/contractor_initialization/scenario2.rb new file mode 100644 index 0000000..bf0cde7 --- /dev/null +++ b/spec/dummy/app/models/contractor_initialization/scenario2.rb @@ -0,0 +1,2 @@ +class ContractorInitialization::Scenario2 < ContractorInitialization +end diff --git a/spec/dummy/app/models/contractor_initialization/scenario2/collection.rb b/spec/dummy/app/models/contractor_initialization/scenario2/collection.rb new file mode 100644 index 0000000..e14bdc9 --- /dev/null +++ b/spec/dummy/app/models/contractor_initialization/scenario2/collection.rb @@ -0,0 +1,3 @@ +class ContractorInitialization::Scenario2::Collection < RecordCollection::Base + include WorkflowStepCollection +end diff --git a/spec/dummy/app/models/execution_batch.rb b/spec/dummy/app/models/execution_batch.rb new file mode 100644 index 0000000..6805c19 --- /dev/null +++ b/spec/dummy/app/models/execution_batch.rb @@ -0,0 +1,3 @@ +class ExecutionBatch < ApplicationRecord + include Dunlop::ExecutionBatchModel +end diff --git a/spec/dummy/app/models/execution_batch/my_batch.rb b/spec/dummy/app/models/execution_batch/my_batch.rb new file mode 100644 index 0000000..3ef3b77 --- /dev/null +++ b/spec/dummy/app/models/execution_batch/my_batch.rb @@ -0,0 +1,9 @@ +class ExecutionBatch::MyBatch < ExecutionBatch + + def process_steps + log_entry.info "Actual info in long running process" + logger.info "Finished 30 imaginary tasks" + assign_attributes info_message: "I migth have processed 47 records" + end +end + diff --git a/spec/dummy/app/models/non_workflow_project.rb b/spec/dummy/app/models/non_workflow_project.rb new file mode 100644 index 0000000..a07880e --- /dev/null +++ b/spec/dummy/app/models/non_workflow_project.rb @@ -0,0 +1,3 @@ +class NonWorkflowProject < ApplicationRecord + include HasUuidIdentifier +end diff --git a/spec/dummy/app/models/owner_initialization.rb b/spec/dummy/app/models/owner_initialization.rb new file mode 100644 index 0000000..0ede1be --- /dev/null +++ b/spec/dummy/app/models/owner_initialization.rb @@ -0,0 +1,9 @@ +class OwnerInitialization < ApplicationRecord + include WorkflowStepModel + + serialize :window_from, DayTimeMinutes + serialize :window_to, DayTimeMinutes + + activate_dunlop! +end + diff --git a/spec/dummy/app/models/owner_initialization/scenario1.rb b/spec/dummy/app/models/owner_initialization/scenario1.rb new file mode 100644 index 0000000..0f7388d --- /dev/null +++ b/spec/dummy/app/models/owner_initialization/scenario1.rb @@ -0,0 +1,2 @@ +class OwnerInitialization::Scenario1 < OwnerInitialization +end diff --git a/spec/dummy/app/models/owner_initialization/scenario1/collection.rb b/spec/dummy/app/models/owner_initialization/scenario1/collection.rb new file mode 100644 index 0000000..f18c183 --- /dev/null +++ b/spec/dummy/app/models/owner_initialization/scenario1/collection.rb @@ -0,0 +1,13 @@ +class OwnerInitialization::Scenario1::Collection < RecordCollection::Base + include WorkflowStepCollection + + attribute :initialization_check_done, type: Boolean + attribute :plan_date, type: Date + attribute :my_identifier + attribute :window_from + attribute :window_to + attribute :number_of_visits, type: Integer + attribute :my_fraction, type: Float + attribute :my_explanation +end + diff --git a/spec/dummy/app/models/source_file.rb b/spec/dummy/app/models/source_file.rb new file mode 100644 index 0000000..2a7049e --- /dev/null +++ b/spec/dummy/app/models/source_file.rb @@ -0,0 +1,9 @@ +class SourceFile < ApplicationRecord + include Dunlop::SourceFileModel + + setup_source_files %i[ + my_source + ] + + activate_dunlop! +end diff --git a/spec/dummy/app/models/source_file/my_source.rb b/spec/dummy/app/models/source_file/my_source.rb new file mode 100644 index 0000000..e124d4b --- /dev/null +++ b/spec/dummy/app/models/source_file/my_source.rb @@ -0,0 +1,23 @@ +class SourceFile::MySource < SourceFile + def load_source_records + assert_first_line "my_date,my_var,My count,my_num,my_truth" + SourceRecord::MySource.truncate + + #execute_sql_erb_script(:load_my_source) + #execute_sql_erb_script(:post_process_my_source) + CSV.foreach working_file.path, headers: :first_row do |row| + SourceRecord::MySource.create \ + my_date: row['my_date'], + my_var: row['my_var'], + my_count: row['My count'], + my_num: row['my_num'], + my_truth: row['my_truth'] + end + + self.number_of_records = SourceRecord::MySource.count + end + + def after_activate_hook + WorkflowInstance.create_or_update_from_my_source + end +end diff --git a/spec/dummy/app/models/source_file/sql_templates/load_my_source.sql.erb b/spec/dummy/app/models/source_file/sql_templates/load_my_source.sql.erb new file mode 100644 index 0000000..b3f1f45 --- /dev/null +++ b/spec/dummy/app/models/source_file/sql_templates/load_my_source.sql.erb @@ -0,0 +1,9 @@ +LOAD DATA LOCAL INFILE '<%= working_file.path %>' IGNORE + INTO TABLE source_record_my_sources + FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' + IGNORE 1 LINES ( + @my_date, my_var, my_count, my_num, @my_truth + ) +SET + my_date=NULLIF(@my_date, ''), + my_truth=IF(@my_thruth='true' OR @my_truth='1', 1, 0) diff --git a/spec/dummy/app/models/source_file/sql_templates/post_process_my_source.sql.erb b/spec/dummy/app/models/source_file/sql_templates/post_process_my_source.sql.erb new file mode 100644 index 0000000..e69de29 diff --git a/spec/dummy/app/models/source_record.rb b/spec/dummy/app/models/source_record.rb new file mode 100644 index 0000000..07697ee --- /dev/null +++ b/spec/dummy/app/models/source_record.rb @@ -0,0 +1,5 @@ +module SourceRecord + def self.table_name_prefix + 'source_record_' + end +end diff --git a/spec/dummy/app/models/source_record/my_source.rb b/spec/dummy/app/models/source_record/my_source.rb new file mode 100644 index 0000000..7389742 --- /dev/null +++ b/spec/dummy/app/models/source_record/my_source.rb @@ -0,0 +1,2 @@ +class SourceRecord::MySource < ApplicationRecord +end diff --git a/spec/dummy/app/models/target_file.rb b/spec/dummy/app/models/target_file.rb new file mode 100644 index 0000000..e8cad50 --- /dev/null +++ b/spec/dummy/app/models/target_file.rb @@ -0,0 +1,11 @@ +class TargetFile < ApplicationRecord + include Dunlop::TargetFileModel + + setup_target_files %i[ + my_target + file_with_builder + file_without_builder + ] + + activate_dunlop! +end diff --git a/spec/dummy/app/models/target_file/file_with_builder.rb b/spec/dummy/app/models/target_file/file_with_builder.rb new file mode 100644 index 0000000..d509259 --- /dev/null +++ b/spec/dummy/app/models/target_file/file_with_builder.rb @@ -0,0 +1,2 @@ +class TargetFile::FileWithBuilder < TargetFile +end diff --git a/spec/dummy/app/models/target_file/file_without_builder.rb b/spec/dummy/app/models/target_file/file_without_builder.rb new file mode 100644 index 0000000..9f67605 --- /dev/null +++ b/spec/dummy/app/models/target_file/file_without_builder.rb @@ -0,0 +1,11 @@ +class TargetFile::FileWithoutBuilder < TargetFile + class CustomResource + def to_csv + "my;custom;csv\n1;2;3\n" + end + end + + def resource + CustomResource.new + end +end diff --git a/spec/dummy/app/models/target_file/my_target.rb b/spec/dummy/app/models/target_file/my_target.rb new file mode 100644 index 0000000..76eb142 --- /dev/null +++ b/spec/dummy/app/models/target_file/my_target.rb @@ -0,0 +1,14 @@ +class TargetFile::MyTarget < TargetFile + def header_line_count + 1 + end + + # Generate file + def generate_file + working_file.puts "My;Content;From;Dummy;App" + end + + def gzip_file? + true + end +end diff --git a/spec/dummy/app/models/user.rb b/spec/dummy/app/models/user.rb new file mode 100644 index 0000000..c563ce4 --- /dev/null +++ b/spec/dummy/app/models/user.rb @@ -0,0 +1,17 @@ +class User < ApplicationRecord + include Dunlop::UserModel + attr_accessor :my_domain # used for advanced authorization testing + + # Include default devise modules. Others available are: + # :confirmable, :lockable, :timeoutable and :omniauthable + devise :database_authenticatable, #:registerable, :trackable, + :recoverable, :rememberable, :validatable + + scope :active, ->{ all } + + class << self + def for_domain(domain) + where.not(email: 'outside-domain@example.com') + end + end +end diff --git a/spec/dummy/app/models/wba_submission.rb b/spec/dummy/app/models/wba_submission.rb new file mode 100644 index 0000000..4efd8a7 --- /dev/null +++ b/spec/dummy/app/models/wba_submission.rb @@ -0,0 +1,6 @@ +class WbaSubmission < ApplicationRecord + include WorkflowStepModel + + activate_dunlop! +end + diff --git a/spec/dummy/app/models/wba_submission/scenario2.rb b/spec/dummy/app/models/wba_submission/scenario2.rb new file mode 100644 index 0000000..0c2e564 --- /dev/null +++ b/spec/dummy/app/models/wba_submission/scenario2.rb @@ -0,0 +1,2 @@ +class WbaSubmission::Scenario2 < WbaSubmission +end diff --git a/spec/dummy/app/models/wba_submission/scenario2/collection.rb b/spec/dummy/app/models/wba_submission/scenario2/collection.rb new file mode 100644 index 0000000..21a63e7 --- /dev/null +++ b/spec/dummy/app/models/wba_submission/scenario2/collection.rb @@ -0,0 +1,7 @@ +class WbaSubmission::Scenario2::Collection < RecordCollection::Base + include WorkflowStepCollection + + attribute :all_clear, type: Boolean + attribute :new_service_group +end + diff --git a/spec/dummy/app/models/workflow_instance.rb b/spec/dummy/app/models/workflow_instance.rb new file mode 100644 index 0000000..2e847df --- /dev/null +++ b/spec/dummy/app/models/workflow_instance.rb @@ -0,0 +1,22 @@ +class WorkflowInstance < ApplicationRecord + include Dunlop::WorkflowInstanceModel + include WorkflowInstance::DataSetup + + # The name by which the workflow instance can be recognized + def presentation_name + my_name + end + + setup_scenarios %i[ + scenario1 + scenario2 + ] + + setup_possible_workflow_steps %i[ + owner_initialization + contractor_initialization + wba_submission + ] + + activate_dunlop! +end diff --git a/spec/dummy/app/models/workflow_instance/data_setup.rb b/spec/dummy/app/models/workflow_instance/data_setup.rb new file mode 100644 index 0000000..f79c391 --- /dev/null +++ b/spec/dummy/app/models/workflow_instance/data_setup.rb @@ -0,0 +1,16 @@ +module WorkflowInstance::DataSetup + extend ActiveSupport::Concern + + included do + # put extra source record relations here + end + + module ClassMethods + def create_or_update_from_my_source + end + + # add load logig based on source records here + # def create_or_update_from_source_record_dslams + # end + end +end diff --git a/spec/dummy/app/models/workflow_instance/scenario1.rb b/spec/dummy/app/models/workflow_instance/scenario1.rb new file mode 100644 index 0000000..9c36f1b --- /dev/null +++ b/spec/dummy/app/models/workflow_instance/scenario1.rb @@ -0,0 +1,3 @@ +class WorkflowInstance::Scenario1 < WorkflowInstance + setup_scenario_workflow_steps %i[ owner_initialization contractor_initialization ] +end diff --git a/spec/dummy/app/models/workflow_instance/scenario1/collection.rb b/spec/dummy/app/models/workflow_instance/scenario1/collection.rb new file mode 100644 index 0000000..82f5994 --- /dev/null +++ b/spec/dummy/app/models/workflow_instance/scenario1/collection.rb @@ -0,0 +1,5 @@ +class WorkflowInstance::Scenario1::Collection < RecordCollection::Base + attribute :bucket + attribute :workflow_instance_batch_id + attribute :plan_date +end diff --git a/spec/dummy/app/models/workflow_instance/scenario2.rb b/spec/dummy/app/models/workflow_instance/scenario2.rb new file mode 100644 index 0000000..235aaeb --- /dev/null +++ b/spec/dummy/app/models/workflow_instance/scenario2.rb @@ -0,0 +1,3 @@ +class WorkflowInstance::Scenario2 < WorkflowInstance + setup_scenario_workflow_steps %i[ contractor_initialization wba_submission ] +end diff --git a/spec/dummy/app/models/workflow_instance/scenario2/collection.rb b/spec/dummy/app/models/workflow_instance/scenario2/collection.rb new file mode 100644 index 0000000..2d7b0cf --- /dev/null +++ b/spec/dummy/app/models/workflow_instance/scenario2/collection.rb @@ -0,0 +1,5 @@ +class WorkflowInstance::Scenario2::Collection < RecordCollection::Base + attribute :bucket + attribute :workflow_instance_batch_id + attribute :plan_date +end diff --git a/spec/dummy/app/models/workflow_instance_batch.rb b/spec/dummy/app/models/workflow_instance_batch.rb new file mode 100644 index 0000000..352f473 --- /dev/null +++ b/spec/dummy/app/models/workflow_instance_batch.rb @@ -0,0 +1,8 @@ +class WorkflowInstanceBatch < ApplicationRecord + include Dunlop::WorkflowInstanceBatchModel + + def presentation_name + name + end + +end diff --git a/spec/dummy/app/models/workflow_instance_batch/scenario1.rb b/spec/dummy/app/models/workflow_instance_batch/scenario1.rb new file mode 100644 index 0000000..6700de7 --- /dev/null +++ b/spec/dummy/app/models/workflow_instance_batch/scenario1.rb @@ -0,0 +1,2 @@ +class WorkflowInstanceBatch::Scenario1 < WorkflowInstanceBatch +end diff --git a/spec/dummy/app/models/workflow_instance_batch/scenario2.rb b/spec/dummy/app/models/workflow_instance_batch/scenario2.rb new file mode 100644 index 0000000..78ead5c --- /dev/null +++ b/spec/dummy/app/models/workflow_instance_batch/scenario2.rb @@ -0,0 +1,2 @@ +class WorkflowInstanceBatch::Scenario2 < WorkflowInstanceBatch +end diff --git a/spec/dummy/app/services/workflow_instance_input_scrubber.rb b/spec/dummy/app/services/workflow_instance_input_scrubber.rb new file mode 100644 index 0000000..e54aab5 --- /dev/null +++ b/spec/dummy/app/services/workflow_instance_input_scrubber.rb @@ -0,0 +1,20 @@ +class WorkflowInstanceInputScrubber + attr_reader :input + DSLAM_NAME_REGEXP = /[a-zA-Z0-9\-]+[\/-]DSLA8?[\/-]\d+/ + + def initialize(input) + @input = input.to_s + end + + def dslam_names + input.scan(DSLAM_NAME_REGEXP).map do |input_name| + # schuuring to kpn aanpassing + input_name.sub('-DSLA-', '/DSLA/').sub('-DSLA8-', '/DSLA8/') + end + end + + + def empty? + dslam_names.empty? + end +end diff --git a/spec/dummy/app/views/contractor_initializations/_collection_form.html.slim b/spec/dummy/app/views/contractor_initializations/_collection_form.html.slim new file mode 100644 index 0000000..0185956 --- /dev/null +++ b/spec/dummy/app/views/contractor_initializations/_collection_form.html.slim @@ -0,0 +1,2 @@ += render "workflow_steps/notes", f: f + diff --git a/spec/dummy/app/views/contractor_initializations/collection_edit.html.slim b/spec/dummy/app/views/contractor_initializations/collection_edit.html.slim new file mode 100644 index 0000000..cd5aa05 --- /dev/null +++ b/spec/dummy/app/views/contractor_initializations/collection_edit.html.slim @@ -0,0 +1,2 @@ += render 'workflow_steps/collection_edit' + diff --git a/spec/dummy/app/views/contractor_initializations/edit.html.slim b/spec/dummy/app/views/contractor_initializations/edit.html.slim new file mode 100644 index 0000000..8a03adc --- /dev/null +++ b/spec/dummy/app/views/contractor_initializations/edit.html.slim @@ -0,0 +1,2 @@ += render "workflow_steps/edit" + diff --git a/spec/dummy/app/views/contractor_initializations/info_fields.html.slim b/spec/dummy/app/views/contractor_initializations/info_fields.html.slim new file mode 100644 index 0000000..d9090eb --- /dev/null +++ b/spec/dummy/app/views/contractor_initializations/info_fields.html.slim @@ -0,0 +1,2 @@ += render 'dunlop/badge_info_collection_attributes', local_assigns + diff --git a/spec/dummy/app/views/dashboard/_special_partial_for_test_reasons.html.slim b/spec/dummy/app/views/dashboard/_special_partial_for_test_reasons.html.slim new file mode 100644 index 0000000..d3ef1c4 --- /dev/null +++ b/spec/dummy/app/views/dashboard/_special_partial_for_test_reasons.html.slim @@ -0,0 +1 @@ +h4= "Special partial for testing, passed value is #{my_value}" diff --git a/spec/dummy/app/views/dashboard/home.html.slim b/spec/dummy/app/views/dashboard/home.html.slim new file mode 100644 index 0000000..800977b --- /dev/null +++ b/spec/dummy/app/views/dashboard/home.html.slim @@ -0,0 +1,7 @@ +.row + .small-12.columns + br.show-for-medium-up + br.show-for-medium-up + h1= application_name + br.show-for-medium-up += render "dunlop/workflow_instance/statistics" diff --git a/spec/dummy/app/views/dashboard/render_as_dunlop_modal_test.html.slim b/spec/dummy/app/views/dashboard/render_as_dunlop_modal_test.html.slim new file mode 100644 index 0000000..bb3d7ec --- /dev/null +++ b/spec/dummy/app/views/dashboard/render_as_dunlop_modal_test.html.slim @@ -0,0 +1,4 @@ +javascript: + $(function(){ + $.get('/dashboard/render_as_dunlop_modal_test.js') + }); diff --git a/spec/dummy/app/views/dashboard/render_as_dunlop_modal_test.js.slim b/spec/dummy/app/views/dashboard/render_as_dunlop_modal_test.js.slim new file mode 100644 index 0000000..f78808f --- /dev/null +++ b/spec/dummy/app/views/dashboard/render_as_dunlop_modal_test.js.slim @@ -0,0 +1 @@ += render_as_dunlop_modal 'special_partial_for_test_reasons', my_value: 78 diff --git a/spec/dummy/app/views/layouts/_messages.html.slim b/spec/dummy/app/views/layouts/_messages.html.slim new file mode 100644 index 0000000..5a6c1ae --- /dev/null +++ b/spec/dummy/app/views/layouts/_messages.html.slim @@ -0,0 +1,5 @@ +- flash.each do |name, msg| + - if msg.is_a?(String) + div.alert-box.round class="#{name.to_s == 'notice' ? 'success' : 'alert'}" data-alert="" + = content_tag :div, msg + a.close href="#" × diff --git a/spec/dummy/app/views/layouts/_navigation.html.slim b/spec/dummy/app/views/layouts/_navigation.html.slim new file mode 100644 index 0000000..8567dfd --- /dev/null +++ b/spec/dummy/app/views/layouts/_navigation.html.slim @@ -0,0 +1,7 @@ +nav.top-bar data-topbar=true + ul.title-area + li.name= link_to application_name, main_app.root_path + li.toggle-topbar.menu-icon + a href="#" Menu + .top-bar-section + ul= render 'layouts/navigation_links' diff --git a/spec/dummy/app/views/layouts/_navigation_links.html.slim b/spec/dummy/app/views/layouts/_navigation_links.html.slim new file mode 100644 index 0000000..dc4ebc6 --- /dev/null +++ b/spec/dummy/app/views/layouts/_navigation_links.html.slim @@ -0,0 +1,36 @@ +- if can? :index, WorkflowInstance + li.has-dropdown class=active_class('workflow_instance') + = link_to WorkflowInstance.model_name.human_plural, '#' + ul.dropdown + - WorkflowInstance.scenario_classes.each do |scenario| + - if can? :read, scenario + li class=active_class("workflow_instance/#{scenario.process_name.pluralize}") = link_to scenario.model_name.human_plural, [main_app, scenario] +- if can? :index, WorkflowInstanceBatch + li.has-dropdown class=active_class('workflow_instance_batch') + = link_to WorkflowInstanceBatch.model_name.human_plural, '#' + ul.dropdown + - WorkflowInstance.scenario_classes.each do |scenario| + - if can? :read, scenario.batch_class + li class=active_class("workflow_instance_batch/#{scenario.batch_class.process_name.pluralize}") = link_to scenario.batch_class.model_name.human_plural, [main_app, scenario.batch_class] +/- if can? :index, WorkflowInstanceBatch + li class=active_class('workflow_instance_batches') = link_to WorkflowInstanceBatch::Scenario1.model_name.human_plural, WorkflowInstanceBatch::Scenario1 +- if can? :index, SourceFile or can? :index, TargetFile + li.has-dropdown class=active_class('dunlop/source_files', 'dunlop/target_files') + = link_to 'Files', '#' + ul.dropdown + li class=active_class('dunlop/source_files') = link_to SourceFile.model_name.human_plural, dunlop.source_files_path + li class=active_class('dunlop/target_files') = link_to TargetFile.model_name.human_plural, dunlop.target_files_path +- if can? :index, :reports + li class=active_class('dunlop/reports') = link_to t('dunlop.reports.title'), dunlop.reports_root_path +- if can? :index, User + li class=active_class("dunlop/users") = link_to User.model_name.human_plural, dunlop.users_path +/li{ class: active_class('manual') }= link_to 'Manual', manual_root_path +.right + li class=active_class('dunlop#changelog') + = link_to Rails.application.config.git_version, dunlop.changelog_path, title: 'Current version, click for changelog' + - if signed_in? + li class=active_class('dunlop/users#profile', 'dunlop/users#edit_profile') = link_to 'Profile', dunlop.profile_users_path + li= link_to 'Sign out', main_app.destroy_user_session_path, method: :delete + - else + li= link_to 'Sign In', main_app.new_user_session_path + diff --git a/spec/dummy/app/views/layouts/application.html.slim b/spec/dummy/app/views/layouts/application.html.slim new file mode 100644 index 0000000..834f567 --- /dev/null +++ b/spec/dummy/app/views/layouts/application.html.slim @@ -0,0 +1,26 @@ +doctype html +html + head + meta name="viewport" content="width=device-width, initial-scale=1.0" + title= content_for?(:title) ? yield(:title) : application_name + meta name="description" content="#{content_for?(:description) ? yield(:description) : 'Migration Dashboard'}" + = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true + = javascript_include_tag 'vendor/modernizr' + = javascript_include_tag 'application', 'data-turbolinks-track' => true + = csrf_meta_tags + body class=body_classes + #workflow-step-modal.reveal-modal data-reveal="" + .content + a.close-reveal-modal × + header= render 'layouts/navigation' + main role="main" + .environment-ribbon class=Rails.env : span= Rails.env + = render 'layouts/messages' + = yield + footer + .right + Powered by + = link_to 'FourStack B.V.', 'http://fourstack.nl/', target: :_blank + #display-badge-info-popup.reveal-modal data-reveal="" + .display-badge-info-content + a.close-reveal-modal x diff --git a/spec/dummy/app/views/layouts/mailer.text.erb b/spec/dummy/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000..37f0bdd --- /dev/null +++ b/spec/dummy/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/spec/dummy/app/views/owner_initializations/_collection_form.html.slim b/spec/dummy/app/views/owner_initializations/_collection_form.html.slim new file mode 100644 index 0000000..207957e --- /dev/null +++ b/spec/dummy/app/views/owner_initializations/_collection_form.html.slim @@ -0,0 +1,10 @@ += render "workflow_steps/notes", f: f +.form-inputs= f.optional_boolean :initialization_check_done +.form-inputs= f.optional_input :plan_date, as: :string, input_html: {class: 'datepicker'} +.form-inputs= f.optional_input :my_identifier +.form-inputs= f.optional_input :window_from, collection: hours_for_select, include_blank: '--' +.form-inputs= f.optional_input :window_to, collection: hours_for_select, include_blank: '--' +.form-inputs= f.optional_input :number_of_visits +.form-inputs= f.optional_input :my_fraction +.form-inputs= f.optional_input :my_explanation + diff --git a/spec/dummy/app/views/owner_initializations/collection_edit.html.slim b/spec/dummy/app/views/owner_initializations/collection_edit.html.slim new file mode 100644 index 0000000..cd5aa05 --- /dev/null +++ b/spec/dummy/app/views/owner_initializations/collection_edit.html.slim @@ -0,0 +1,2 @@ += render 'workflow_steps/collection_edit' + diff --git a/spec/dummy/app/views/owner_initializations/edit.html.slim b/spec/dummy/app/views/owner_initializations/edit.html.slim new file mode 100644 index 0000000..8a03adc --- /dev/null +++ b/spec/dummy/app/views/owner_initializations/edit.html.slim @@ -0,0 +1,2 @@ += render "workflow_steps/edit" + diff --git a/spec/dummy/app/views/owner_initializations/info_fields.html.slim b/spec/dummy/app/views/owner_initializations/info_fields.html.slim new file mode 100644 index 0000000..d9090eb --- /dev/null +++ b/spec/dummy/app/views/owner_initializations/info_fields.html.slim @@ -0,0 +1,2 @@ += render 'dunlop/badge_info_collection_attributes', local_assigns + diff --git a/spec/dummy/app/views/owner_mailer/owner_initialization_finished.text.slim b/spec/dummy/app/views/owner_mailer/owner_initialization_finished.text.slim new file mode 100644 index 0000000..1a9d1ab --- /dev/null +++ b/spec/dummy/app/views/owner_mailer/owner_initialization_finished.text.slim @@ -0,0 +1,3 @@ +OwnerMailer#owner_initialization_finished + += @greeting + ", find me in app/views/owner_mailer/owner_initialization_finished.text.slim" diff --git a/spec/dummy/app/views/wba_submissions/_collection_form.html.slim b/spec/dummy/app/views/wba_submissions/_collection_form.html.slim new file mode 100644 index 0000000..7fd1252 --- /dev/null +++ b/spec/dummy/app/views/wba_submissions/_collection_form.html.slim @@ -0,0 +1,4 @@ += render "workflow_steps/notes", f: f +.form-inputs= f.optional_boolean :all_clear +.form-inputs= f.optional_input :new_service_group + diff --git a/spec/dummy/app/views/wba_submissions/collection_edit.html.slim b/spec/dummy/app/views/wba_submissions/collection_edit.html.slim new file mode 100644 index 0000000..cd5aa05 --- /dev/null +++ b/spec/dummy/app/views/wba_submissions/collection_edit.html.slim @@ -0,0 +1,2 @@ += render 'workflow_steps/collection_edit' + diff --git a/spec/dummy/app/views/wba_submissions/edit.html.slim b/spec/dummy/app/views/wba_submissions/edit.html.slim new file mode 100644 index 0000000..8a03adc --- /dev/null +++ b/spec/dummy/app/views/wba_submissions/edit.html.slim @@ -0,0 +1,2 @@ += render "workflow_steps/edit" + diff --git a/spec/dummy/app/views/wba_submissions/info_fields.html.slim b/spec/dummy/app/views/wba_submissions/info_fields.html.slim new file mode 100644 index 0000000..d9090eb --- /dev/null +++ b/spec/dummy/app/views/wba_submissions/info_fields.html.slim @@ -0,0 +1,2 @@ += render 'dunlop/badge_info_collection_attributes', local_assigns + diff --git a/spec/dummy/app/views/workflow_instance_batches/_form.html.slim b/spec/dummy/app/views/workflow_instance_batches/_form.html.slim new file mode 100644 index 0000000..fd9bef8 --- /dev/null +++ b/spec/dummy/app/views/workflow_instance_batches/_form.html.slim @@ -0,0 +1,7 @@ += simple_form_for(@record) do |f| + = f.error_notification + - unless @record.new_record? + .form-inputs= f.input :id, disabled: true + .form-inputs= f.input :name + .form-inputs= f.input :plan_date, as: :string, input_html: {class: 'datepicker'} + .form-actions= f.button :submit diff --git a/spec/dummy/app/views/workflow_instance_batches/edit.html.slim b/spec/dummy/app/views/workflow_instance_batches/edit.html.slim new file mode 100644 index 0000000..056c1cf --- /dev/null +++ b/spec/dummy/app/views/workflow_instance_batches/edit.html.slim @@ -0,0 +1,4 @@ += page_title :edit, workflow_instance_batch_class += render 'workflow_instance_batches/form' +.page-actions + = link_to 'Back', workflow_instance_batch_class, class: 'back' diff --git a/spec/dummy/app/views/workflow_instance_batches/index.html.slim b/spec/dummy/app/views/workflow_instance_batches/index.html.slim new file mode 100644 index 0000000..ff51606 --- /dev/null +++ b/spec/dummy/app/views/workflow_instance_batches/index.html.slim @@ -0,0 +1,41 @@ += page_title :index, workflow_instance_batch_class += paginate @records +table.full-width + thead + tr + th= sort_link @q, :id + th= sort_link @q, :plan_date + th= "# #{workflow_instance_class.model_name.human_plural}" + th + th.actions + = search_form_for @q do |f| + tr + th.column-filter.name= f.search_field :id_eq, placeholder: '==' + th.column-filter.name= f.search_field :plan_date_eq, placeholder: 'date', class: 'datepicker' + th + th + th.column-filter.actions + = f.submit t('dunlop.filter.apply').html_safe, class: 'submit-filters-button' + = link_to workflow_instance_batch_class, class: 'clear-filters-button' do + span== t('dunlop.filter.reset') + tbody + - workflow_step_map = @records.workflow_step_map(current_user, archived: session[:archived_mode]) + - @records.each do |workflow_instance_batch| + tr + td= link_to workflow_instance_batch.presentation_name, workflow_instance_batch + td.plan_date data-date=workflow_instance_batch.plan_date.try(:iso8601) + td= workflow_instance_batch.workflow_instances.scope_for(current_user, archived: session[:archived_mode]).size + td.workflow_instance_batch-index-progress-container + - workflow_step_map[workflow_instance_batch.id].each do |workflow_step, state_counts| + span.workflow_instance_batch-index-workflow_step= workflow_step.model_name.human + - state_counts.each do |state, count| + span.state-container= state_badge workflow_step.new(state: state), state_append_text: " (#{count})", data: {workflow_instance_batch_id: workflow_instance_batch.id} + /TODO workflow step filtering #, link_to: main_app.polymorphic_path(workflow_instance_class, q: {workflow_instance_batch_id_eq: workflow_instance_batch.id, workflow_step: {workflow_step.process_name => {state: state}}}) + td.actions + = link_to polymorphic_path(workflow_instance_class, q: {workflow_instance_batch_id_eq: workflow_instance_batch.id}, per_page: 10_000), class: 'show-workflow_instance_batch-workflow_instances' do + span + = table_show_link workflow_instance_batch + - if can? :edit, workflow_instance_batch + = table_edit_link workflow_instance_batch +.page-actions + = link_to t('action.new.label', model: workflow_instance_batch_class.model_name.human), new_polymorphic_path(workflow_instance_batch_class), class: 'new' diff --git a/spec/dummy/app/views/workflow_instance_batches/new.html.slim b/spec/dummy/app/views/workflow_instance_batches/new.html.slim new file mode 100644 index 0000000..9c4fb09 --- /dev/null +++ b/spec/dummy/app/views/workflow_instance_batches/new.html.slim @@ -0,0 +1,4 @@ += page_title :new, workflow_instance_batch_class += render 'workflow_instance_batches/form' +.page-actions + = link_to 'Back', workflow_instance_batch_class, class: 'back' diff --git a/spec/dummy/app/views/workflow_instance_batches/show.html.slim b/spec/dummy/app/views/workflow_instance_batches/show.html.slim new file mode 100644 index 0000000..46197f4 --- /dev/null +++ b/spec/dummy/app/views/workflow_instance_batches/show.html.slim @@ -0,0 +1,38 @@ +- workflow_instances_scope = @record.workflow_instances.scope_for(current_user, archived: session[:archived_mode]) +.display-row + .display-label= page_title :show, workflow_instance_batch_class + .display-field + h3 + = @record.presentation_name + = "(#{workflow_instances_scope.size})" + - if can? :edit, @record + = link_to '', [:edit, @record], class: 'edit-icon' +.display-row + .display-label= workflow_instance_batch_class.human_attribute_name(:plan_date) + .display-field= @record.plan_date + +.workflow-step-workfow-instances-collection-container + = safe_join workflow_instances_scope.map{|workflow_instance| link_to workflow_instance.presentation_name, workflow_instance, class: workflow_instance.state}, ', ' +.batch-states-collection-container + - workflow_instances_scope.group(:state).count.each do |state, count| + span.state= state_badge workflow_instance_class.new(state: state), state_append_text: " (#{count})", link_to: polymorphic_path(workflow_instance_class, q: {state_eq: state, workflow_instance_batch_id_eq: @record.id}) +.workflow-instance-batch-actions + - previous_step_unfinished = nil + - @record.workflow_step_map(current_user).each do |workflow_step, records| + .row.workflow-step-container + .small-12.columns + h4 + = workflow_step.model_name.human + = workflow_step_button_for workflow_step, request_params: {workflow_instance_batch_id: @record.id}, confirmation: (previous_step_unfinished.present? ? t('workflow_step.previous_step_not_finished_warning', step_name: previous_step_unfinished.model_name.human) : nil) + - state_counts = records.group("#{workflow_step.table_name}.state").count + - state_counts.each do |state, count| + span.state= state_badge workflow_step.new(state: state), state_append_text: " (#{count})", data: {workflow_instance_batch_id: @record.id} + - previous_step_unfinished = (state_counts.keys - workflow_step.final_states).any? ? workflow_step : nil +.page-actions + = link_to t('workflow_instance_batch.to_index_button'), workflow_instance_batch_class, class: 'back' + - if can? :manage, workflow_instance_class + button.collection-edit data-action="collection_edit" data-resource=workflow_instance_class.name.underscore data-request-params={workflow_instance_batch_id: @record.id}.to_json + = t 'workflow_instance_batch.edit_dslams_button', models: workflow_instance_class.model_name.human_plural + = link_to t('workflow_instance_batch.show_dslams_button'), polymorphic_path([:selection, workflow_instance_class], workflow_instance_batch_id: @record.id), class: 'show-workflow_instance_batch-workflow_instances' + - if can? :download, workflow_instance_class + = link_to t('workflow_instance.export_button'), {format: :csv}, class: 'export-button' diff --git a/spec/dummy/app/views/workflow_instances/_index_table.html.slim b/spec/dummy/app/views/workflow_instances/_index_table.html.slim new file mode 100644 index 0000000..dd02bdc --- /dev/null +++ b/spec/dummy/app/views/workflow_instances/_index_table.html.slim @@ -0,0 +1,51 @@ +table.with-selection data-resource=record_class.name.underscore data-record-count=(@records.total_count rescue @records.size) data-preselected=local_assigns[:preselected] + thead + tr + th= sort_link @q, :id + - if local_assigns[:show_scenario] + th= at :scenario + th= at :state + th= at :plan_date + th= sort_link @q, :bucket + th= WorkflowInstanceBatch.model_name.human + th= t 'workflow_instance.progress_display.title' + /th.actions + - unless local_assigns[:filters] == false + = search_form_for @q do |f| + = hidden_field_tag :per_page, params[:per_page], id: 'q_per_page' + = search_row do + th.column-filter + - if local_assigns[:show_scenario] + th.column-filter.scenario= f.select :sti_type_eq, WorkflowInstance.workflow_step_classes.map{|step| [step.model_name.human, step.name]}, include_blank: 'all' + th.column-filter.state= f.select :state_eq, WorkflowInstance.state_machine.states.map{|state| [state.human_name, state.name]}, include_blank: 'all' + th.column-filter.plan_date + th.column-filter.bucket= f.search_field :bucket_cont, placeholder: 'contains', class: 'autocomplete', data: {source: record_class.available_bucket_names} + th.column-filter.workflow_instance_batch= f.select :workflow_instance_batch_id_eq, record_batch_class.for_select, {include_blank: 'all'}, class: 'search-select' + th.column-filter.actions + = f.submit t('dunlop.filter.apply').html_safe, class: 'submit-filters-button' + = link_to record_class, class: 'clear-filters-button' do + span== t('dunlop.filter.reset') + tbody + - @records.each do |record| + tr data-record={ id: record.id }.to_json class=(record.archived? ? 'archived' : 'active') + td= link_to record.presentation_name, record + - if local_assigns[:show_scenario] + td.scenario= record.model_name.human + td.state= state_badge record + td.plan_date data-date=record.plan_date.try(:iso8601) + td.bucket= record.bucket + td.workflow_instance_batch + - if record.workflow_instance_batch.present? + = link_to record.workflow_instance_batch.presentation_name, record.workflow_instance_batch + td.progress-display= show_workflow_progress_of record + /td.actions= table_show_link record + - unless local_assigns[:hide_buttons] + tfoot + tr + td colspan=99 + - if can?(:edit, record_class) or can?(:archive, record_class) or can?(:reset, record_class) + button.collection-edit.workflow_instance-actions data-resource=record_class.name.underscore data-request-params='{"ids": "selected_ids"}' = t 'collection.edit_button', models: record_class.model_name.human_plural + = workflow_step_buttons_for record_class + - if can? :download, record_class + = link_to t('workflow_instance.export_button'), {q: params[:q].try(:permit!), ids: params[:ids], format: :csv}, class: 'export-button' += render "dunlop/workflow_instance/categorize_as_button" diff --git a/spec/dummy/app/views/workflow_instances/_show_fields.html.slim b/spec/dummy/app/views/workflow_instances/_show_fields.html.slim new file mode 100644 index 0000000..b0eba23 --- /dev/null +++ b/spec/dummy/app/views/workflow_instances/_show_fields.html.slim @@ -0,0 +1,16 @@ +.panel= show_workflow_progress_of @record +.display-row + .display-label= at :state + .display-field.state= state_badge @record +.display-row + .display-label= record_class.batch_class.model_name.human + .display-field + - if batch = @record.workflow_instance_batch + = link_to batch.presentation_name, batch +.display-row + .display-label= at :plan_date + .display-field= @record.plan_date += collapsible_content t('workflow_instance.show_more_attributes') do + .display-row + .display-label= at :id + .display-field= @record.id diff --git a/spec/dummy/app/views/workflow_instances/_workflow_step.html.slim b/spec/dummy/app/views/workflow_instances/_workflow_step.html.slim new file mode 100644 index 0000000..a8c07c9 --- /dev/null +++ b/spec/dummy/app/views/workflow_instances/_workflow_step.html.slim @@ -0,0 +1,11 @@ +.workflow-instance-workflow-step-container class="#{workflow_step.state} #{workflow_step.class.name.underscore.split('/')}" + .workflow-instance-workflow-step-title + - if params[:show_changes].present? + = link_to workflow_step.class.model_name.human, show_record_changes_path(workflow_step.process_name, workflow_step.id, show_changes: 'yes'), data: {remote: true} + - else + = workflow_step.class.model_name.human + .workflow-instance-workflow-step-body + .display-row + .display-label= workflow_step.class.human_attribute_name(:state) + .display-field= workflow_step.human_state_name + = record_info_fields workflow_step diff --git a/spec/dummy/app/views/workflow_instances/collection_edit.html.slim b/spec/dummy/app/views/workflow_instances/collection_edit.html.slim new file mode 100644 index 0000000..ad8e238 --- /dev/null +++ b/spec/dummy/app/views/workflow_instances/collection_edit.html.slim @@ -0,0 +1,28 @@ +h2=t "workflow_step.actions_modal.title", model: record_class.model_name.human +- if batch = @workflow_instance_batch + .display-row + .display-label= batch.class.model_name.human + .display-field + = batch.presentation_name + = "(#{@collection.size})" +.panel.workflow-step-workfow-instances-collection-container + = safe_join @collection.map{|r| link_to(r.presentation_name, r, class: r.state) }, ', ' += simple_form_for @collection do |f| + = f.collection_ids + = f.error_notification + - if can? :update, record_class + .form-inputs= f.optional_input :bucket, input_html: {class: 'autocomplete', data: {source: record_class.available_bucket_names.to_json }} + - if record_batch_class.present? and batch.blank? # do not offer to change batch if the collection is the batch itself + .form-inputs= f.optional_input :workflow_instance_batch_id, collection: record_batch_class.for_select, label: record_batch_class.model_name.human + .form-inputs= f.optional_input :plan_date, input_html: {class: 'datepicker'} + .form-actions + = link_to 'Back', @collection.one? ? @collection.first : record_class, class: 'workflow-step-back-button' + - if can? :update, record_class + = f.button :submit, t('workflow_instance.update.update'), class: 'workflow-step-assign-button' + - if can?(:reset, record_class) + = f.submit t('workflow_instance.reset.reset'), formaction: dunlop.reset_workflow_instances_path, class: "workflow-instance-reset-button right", data: {confirm: t('workflow_instance.reset.confirmation_message', workflow_instance: record_class.model_name.human)} + - if can? :archive, record_class + - if session[:archived_mode] + = f.submit t('workflow_instance.revive.revive'), formaction: dunlop.revive_workflow_instances_path, class: "workflow-instance-revive-button right", data: {confirm: t('workflow_instance.revive.confirmation_message')} + - else + = f.submit t('workflow_instance.archive.archive'), formaction: dunlop.archive_workflow_instances_path, class: "workflow-instance-archive-button right", data: {confirm: t('workflow_instance.archive.confirmation_message')} diff --git a/spec/dummy/app/views/workflow_instances/index.html.slim b/spec/dummy/app/views/workflow_instances/index.html.slim new file mode 100644 index 0000000..799f8ab --- /dev/null +++ b/spec/dummy/app/views/workflow_instances/index.html.slim @@ -0,0 +1,12 @@ +.right= page_time += page_title :index, record_class +/.selection-container + = form_tag [:selection, record_class] do + = text_area_tag :input, '', id: 'workflow-instance-selection-input', placeholder: 'copy/paste DSLAM names for selection' + = submit_tag t('workflow_instance.selection.submit_button'), id: 'workflow-instance-selection-input-submit' + /=link_to 'Export Benny (Schuuring)', {q: params[:q].try(:permit!), stakeholder: 'schuuring', format: :csv}, class: 'export-button' + /=submit_tag t('record.select.find_fuzzy'), id: 'dslam_name-input-fuzzy-submit' + /=link_to 'Export records', {q: params[:q].try(:permit!), format: :csv}, class: 'export-button' +.multi-select-statistics += paginate @records += render "index_table" diff --git a/spec/dummy/app/views/workflow_instances/info_fields.html.slim b/spec/dummy/app/views/workflow_instances/info_fields.html.slim new file mode 100644 index 0000000..b77d78c --- /dev/null +++ b/spec/dummy/app/views/workflow_instances/info_fields.html.slim @@ -0,0 +1 @@ += render 'dunlop/badge_info_collection_attributes', local_assigns diff --git a/spec/dummy/app/views/workflow_instances/selection.html.slim b/spec/dummy/app/views/workflow_instances/selection.html.slim new file mode 100644 index 0000000..dd481ab --- /dev/null +++ b/spec/dummy/app/views/workflow_instances/selection.html.slim @@ -0,0 +1,9 @@ +.right= page_time += page_title :index, record_class +.multi-select-statistics += render "index_table", filters: false, preselected: 'yes', hide_buttons: local_assigns[:hide_buttons], show_scenario: local_assigns[:show_scenario] +.page-actions + /= link_to t('action.new.label', model: Migration::MigrateLineOnnetWithoutCpe.model_name.human), new_migration_migrate_line_onnet_without_cpe_path, class: 'new' + = link_to 'Back', record_class, class: 'back' + / unless local_assigns[:hide_buttons] + = link_to 'Export', {input: params[:input], ids: params[:ids], format: :csv}, class: 'export-button' diff --git a/spec/dummy/app/views/workflow_instances/show.html.slim b/spec/dummy/app/views/workflow_instances/show.html.slim new file mode 100644 index 0000000..b6dac26 --- /dev/null +++ b/spec/dummy/app/views/workflow_instances/show.html.slim @@ -0,0 +1,13 @@ += page_title :show, record_class +div class=(@record.archived? ? 'record-archived' : 'record-active') + = render 'show_fields' + h2.collapsible-expand-all= t 'workflow_instance.workflow_steps_header' + .workflow-instance-workflow-steps-container + - @record.workflow_steps.each do |workflow_step| + = render 'workflow_step', workflow_step: workflow_step + .page-actions + = link_to 'Back', record_class, class: 'back' + - if can? :manage, @record + = link_to t('collection.edit_button', models: record_class.model_name.human), polymorphic_path([:collection_edit, record_class], ids: [@record.id]), class: 'collection-edit' +- if params[:show_changes].present? + /= render 'change_events/show_record_changes', target: @record diff --git a/spec/dummy/app/views/workflow_steps/_collection_edit.html.slim b/spec/dummy/app/views/workflow_steps/_collection_edit.html.slim new file mode 100644 index 0000000..d0d978f --- /dev/null +++ b/spec/dummy/app/views/workflow_steps/_collection_edit.html.slim @@ -0,0 +1,17 @@ +h2=t "workflow_step.actions_modal.title", model: record_class.model_name.human +- if batch = @workflow_instance_batch + .display-row + .display-label= batch.class.model_name.human + .display-field + = batch.presentation_name + = "(#{@collection.size})" +.panel.workflow-step-workfow-instances-collection-container + = safe_join @collection.map{|r| link_to(r.workflow_instance.presentation_name, r.workflow_instance, class: r.state) }, ', ' += simple_form_for @collection do |f| + = f.error_notification + = f.collection_ids + - if batch + = hidden_field_tag :workflow_instance_batch_id, batch.id + = render 'collection_form', f: f + .form-actions= render 'workflow_steps/form_actions', f: f, back: workflow_instance_class + diff --git a/spec/dummy/app/views/workflow_steps/_edit.html.slim b/spec/dummy/app/views/workflow_steps/_edit.html.slim new file mode 100644 index 0000000..0cec278 --- /dev/null +++ b/spec/dummy/app/views/workflow_steps/_edit.html.slim @@ -0,0 +1,11 @@ += page_title t('workflow_step.edit_single_title', @record.workflow_instance.attributes.symbolize_keys.merge(workflow_step: record_class.model_name.human, presentation_name: @record.workflow_instance.presentation_name, workflow_instance: @record.workflow_instance.model_name.human)) +- scope_model record_class +.display-row + .display-label= at :state + .display-field.state= state_badge @record += simple_form_for @record do |f| + = f.error_notification + = f.collection_ids + = render 'collection_form', f: f + .form-actions= render 'workflow_steps/form_actions', f: f, back: @record.workflow_instance + diff --git a/spec/dummy/app/views/workflow_steps/_form_actions.html.slim b/spec/dummy/app/views/workflow_steps/_form_actions.html.slim new file mode 100644 index 0000000..77bbe93 --- /dev/null +++ b/spec/dummy/app/views/workflow_steps/_form_actions.html.slim @@ -0,0 +1,7 @@ += link_to 'Back', back, class: 'workflow-step-back-button' += f.submit t('workflow_step.process_button'), class: 'workflow-step-process-button' += f.submit t('workflow_step.complete_button'), class: 'workflow-step-complete-button' +- if @workflow_instance_batch.present? + = link_to t('workflow_step.reject_button'), '#', onclick: %|alert('#{escape_javascript t('workflow_instance_batch.cannot_reject_message')}'); return false|, class: 'workflow-step-reject-button' +- else + = f.submit t('workflow_step.reject_button'), class: 'workflow-step-reject-button' diff --git a/spec/dummy/app/views/workflow_steps/_notes.html.slim b/spec/dummy/app/views/workflow_steps/_notes.html.slim new file mode 100644 index 0000000..5fb5589 --- /dev/null +++ b/spec/dummy/app/views/workflow_steps/_notes.html.slim @@ -0,0 +1,11 @@ +- if f.object.collection? + - if f.object.one? + pre.notes-display= f.object.uniform_collection_value(:notes) + .form-inputs.notes-to-append= f.input :notes, as: :text, hint: "Notes will be appended" + - else + .form-inputs.notes-to-append= f.input :notes, as: :text, hint: "Notes will be appended to the individual notes of the collection" +- else + .form-inputs.notes + pre.notes-display= f.object.notes + = f.input :notes, as: :text, input_html: {value: ''} + diff --git a/spec/dummy/app/views/workflow_steps/_window_slot.html.slim b/spec/dummy/app/views/workflow_steps/_window_slot.html.slim new file mode 100644 index 0000000..2709be6 --- /dev/null +++ b/spec/dummy/app/views/workflow_steps/_window_slot.html.slim @@ -0,0 +1,11 @@ +- if f.object.is_a?(RecordCollection::Base) # Optionals + .form-inputs= f.optional_input :window_date, as: :string, input_html: {class: 'datepicker'} + - if local_assigns[:time_slot] + .form-inputs= f.optional_input :window_from, collection: hours_for_select, include_blank: '--' + .form-inputs= f.optional_input :window_to, collection: hours_for_select, include_blank: '--' +- else + .form-inputs= f.input :window_date, as: :string, input_html: {class: 'datepicker'} + - if local_assigns[:time_slot] + .form-inputs= f.input :window_from, collection: hours_for_select, include_blank: '--' + .form-inputs= f.input :window_to, collection: hours_for_select, include_blank: '--' + diff --git a/spec/dummy/bin/bundle b/spec/dummy/bin/bundle new file mode 100755 index 0000000..66e9889 --- /dev/null +++ b/spec/dummy/bin/bundle @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +load Gem.bin_path('bundler', 'bundle') diff --git a/spec/dummy/bin/rails b/spec/dummy/bin/rails new file mode 100755 index 0000000..5191e69 --- /dev/null +++ b/spec/dummy/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path('../../config/application', __FILE__) +require_relative '../config/boot' +require 'rails/commands' diff --git a/spec/dummy/bin/rake b/spec/dummy/bin/rake new file mode 100755 index 0000000..1724048 --- /dev/null +++ b/spec/dummy/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative '../config/boot' +require 'rake' +Rake.application.run diff --git a/spec/dummy/bin/setup b/spec/dummy/bin/setup new file mode 100755 index 0000000..acdb2c1 --- /dev/null +++ b/spec/dummy/bin/setup @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +require 'pathname' + +# path to your application root. +APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) + +Dir.chdir APP_ROOT do + # This script is a starting point to setup your application. + # Add necessary setup steps to this file: + + puts "== Installing dependencies ==" + system "gem install bundler --conservative" + system "bundle check || bundle install" + + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # system "cp config/database.yml.sample config/database.yml" + # end + + puts "\n== Preparing database ==" + system "bin/rake db:setup" + + puts "\n== Removing old logs and tempfiles ==" + system "rm -f log/*" + system "rm -rf tmp/cache" + + puts "\n== Restarting application server ==" + system "touch tmp/restart.txt" +end diff --git a/spec/dummy/config.ru b/spec/dummy/config.ru new file mode 100644 index 0000000..bd83b25 --- /dev/null +++ b/spec/dummy/config.ru @@ -0,0 +1,4 @@ +# This file is used by Rack-based servers to start the application. + +require ::File.expand_path('../config/environment', __FILE__) +run Rails.application diff --git a/spec/dummy/config/application.rb b/spec/dummy/config/application.rb new file mode 100644 index 0000000..306382c --- /dev/null +++ b/spec/dummy/config/application.rb @@ -0,0 +1,42 @@ +require File.expand_path('../boot', __FILE__) + +# Pick the frameworks you want: +require "active_record/railtie" +require "action_controller/railtie" +require "action_mailer/railtie" +require "action_view/railtie" +require "sprockets/railtie" +# require "rails/test_unit/railtie" + +Bundler.require(*Rails.groups) +require "dunlop" + +module Dummy + class Application < Rails::Application + config.file_storage_path = Rails.root.join('files') # source file storage + config.working_path = '/tmp' # source file processing directory + + config.application_name = 'Dunlop' + + config.git_version = nil + config.git_version = File.read('REVISION').strip if File.exists?('REVISION') + config.git_version = File.read('OFFLINE_REVISION').strip if File.exists?('OFFLINE_REVISION') + + config.file_storage_path = Rails.root.join('files') # source file storage + config.working_path = '/tmp' # source file processing directory + + config.time_zone = 'Moscow' + # Settings in config/environments/* take precedence over those specified here. + # Application configuration should go into files in config/initializers + # -- all .rb files in that directory are automatically loaded. + + # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. + # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. + # config.time_zone = 'Central Time (US & Canada)' + + # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. + # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] + config.i18n.default_locale = :nl + end +end + diff --git a/spec/dummy/config/boot.rb b/spec/dummy/config/boot.rb new file mode 100644 index 0000000..6266cfc --- /dev/null +++ b/spec/dummy/config/boot.rb @@ -0,0 +1,5 @@ +# Set up gems listed in the Gemfile. +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) + +require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) +$LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) diff --git a/spec/dummy/config/database.yml b/spec/dummy/config/database.yml new file mode 100644 index 0000000..1c1a37c --- /dev/null +++ b/spec/dummy/config/database.yml @@ -0,0 +1,25 @@ +# SQLite version 3.x +# gem install sqlite3 +# +# Ensure the SQLite 3 gem is defined in your Gemfile +# gem 'sqlite3' +# +default: &default + adapter: sqlite3 + pool: 5 + timeout: 5000 + +development: + <<: *default + database: db/development.sqlite3 + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: db/test.sqlite3 + +production: + <<: *default + database: db/production.sqlite3 diff --git a/spec/dummy/config/environment.rb b/spec/dummy/config/environment.rb new file mode 100644 index 0000000..b8b71e6 --- /dev/null +++ b/spec/dummy/config/environment.rb @@ -0,0 +1,7 @@ +# Load the Rails application. +require File.expand_path('../application', __FILE__) + +require 'carrierwave/storage/fog' + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/spec/dummy/config/environments/development.rb b/spec/dummy/config/environments/development.rb new file mode 100644 index 0000000..aacbd15 --- /dev/null +++ b/spec/dummy/config/environments/development.rb @@ -0,0 +1,46 @@ +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded on + # every request. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.cache_classes = false + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Debug mode disables concatenation and preprocessing of assets. + # This option may cause significant delays in view rendering with a large + # number of complex assets. + config.assets.debug = true + + # Asset digests allow you to set far-future HTTP expiration dates on all assets, + # yet still be able to expire them through the digest params. + config.assets.digest = true + + config.to_prepare do + require Rails.root.join("config/initializers/dunlop_workflow_steps_in_batch_finished_config") + require Rails.root.join("config/initializers/dunlop_overdue_config") + end + + # Adds additional error checking when serving assets at runtime. + # Checks for improperly declared sprockets dependencies. + # Raises helpful error messages. + config.assets.raise_runtime_errors = true + + # Raises error for missing translations + config.action_view.raise_on_missing_translations = true +end diff --git a/spec/dummy/config/environments/production.rb b/spec/dummy/config/environments/production.rb new file mode 100644 index 0000000..5c1b32e --- /dev/null +++ b/spec/dummy/config/environments/production.rb @@ -0,0 +1,79 @@ +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.cache_classes = true + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Enable Rack::Cache to put a simple HTTP cache in front of your application + # Add `rack-cache` to your Gemfile before enabling this. + # For large-scale production use, consider using a caching reverse proxy like + # NGINX, varnish or squid. + # config.action_dispatch.rack_cache = true + + # Disable serving static files from the `/public` folder by default since + # Apache or NGINX already handles this. + config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? + + # Compress JavaScripts and CSS. + config.assets.js_compressor = :uglifier + # config.assets.css_compressor = :sass + + # Do not fallback to assets pipeline if a precompiled asset is missed. + config.assets.compile = false + + # Asset digests allow you to set far-future HTTP expiration dates on all assets, + # yet still be able to expire them through the digest params. + config.assets.digest = true + + # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb + + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # Use the lowest log level to ensure availability of diagnostic information + # when problems arise. + config.log_level = :debug + + # Prepend all log lines with the following tags. + # config.log_tags = [ :subdomain, :uuid ] + + # Use a different logger for distributed setups. + # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) + + # Use a different cache store in production. + # config.cache_store = :mem_cache_store + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.action_controller.asset_host = 'http://assets.example.com' + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Send deprecation notices to registered listeners. + config.active_support.deprecation = :notify + + # Use default logging formatter so that PID and timestamp are not suppressed. + config.log_formatter = ::Logger::Formatter.new + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false +end diff --git a/spec/dummy/config/environments/test.rb b/spec/dummy/config/environments/test.rb new file mode 100644 index 0000000..17b25b7 --- /dev/null +++ b/spec/dummy/config/environments/test.rb @@ -0,0 +1,54 @@ +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # The test environment is used exclusively to run your application's + # test suite. You never need to work with it otherwise. Remember that + # your test database is "scratch space" for the test suite and is wiped + # and recreated between test runs. Don't rely on the data there! + config.cache_classes = true + config.time_zone = 'Moscow' + config.git_version = 'test' + + # Do not eager load code on boot. This avoids loading your whole application + # just for the purpose of running a single test. If you are using a tool that + # preloads Rails for running tests, you may have to set it to true. + config.eager_load = false + + # Configure static file server for tests with Cache-Control for performance. + if Rails.version.starts_with?('4') + config.serve_static_files = true # rails < 5 + config.static_cache_control = 'public, max-age=3600' # rails < 5 + else + config.public_file_server.enabled = true + config.public_file_server.headers = { 'Cache-Control' => 'public, max-age=3600' } + end + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + + # Raise exceptions instead of rendering exception templates. + config.action_dispatch.show_exceptions = false + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + config.to_prepare do + require Rails.root.join("config/initializers/dunlop_workflow_steps_in_batch_finished_config") + require Rails.root.join("config/initializers/dunlop_overdue_config") + end + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Randomize the order test cases are executed. + config.active_support.test_order = :random + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raises error for missing translations + config.action_view.raise_on_missing_translations = true +end diff --git a/spec/dummy/config/initializers/assets.rb b/spec/dummy/config/initializers/assets.rb new file mode 100644 index 0000000..01ef3e6 --- /dev/null +++ b/spec/dummy/config/initializers/assets.rb @@ -0,0 +1,11 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = '1.0' + +# Add additional assets to the asset load path +# Rails.application.config.assets.paths << Emoji.images_path + +# Precompile additional assets. +# application.js, application.css, and all non-JS/CSS in app/assets folder are already added. +# Rails.application.config.assets.precompile += %w( search.js ) diff --git a/spec/dummy/config/initializers/backtrace_silencers.rb b/spec/dummy/config/initializers/backtrace_silencers.rb new file mode 100644 index 0000000..59385cd --- /dev/null +++ b/spec/dummy/config/initializers/backtrace_silencers.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. +# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } + +# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. +# Rails.backtrace_cleaner.remove_silencers! diff --git a/spec/dummy/config/initializers/cookies_serializer.rb b/spec/dummy/config/initializers/cookies_serializer.rb new file mode 100644 index 0000000..7f70458 --- /dev/null +++ b/spec/dummy/config/initializers/cookies_serializer.rb @@ -0,0 +1,3 @@ +# Be sure to restart your server when you modify this file. + +Rails.application.config.action_dispatch.cookies_serializer = :json diff --git a/spec/dummy/config/initializers/csv.rb b/spec/dummy/config/initializers/csv.rb new file mode 100644 index 0000000..4398101 --- /dev/null +++ b/spec/dummy/config/initializers/csv.rb @@ -0,0 +1,8 @@ +ActionController::Renderers.add :csv do |resource, options, *rest| + builder_class = options[:builder] || CsvBuilder.find_for_route("#{controller_path}##{action_name}") + + raise "Cannot find the Csv builder for: #{controller_path}##{action_name}" unless builder_class.present? + + builder = builder_class.new(resource, options.merge(action_name: action_name, controller_path: controller_path, controller: self)) + send_data builder.data, type: Mime::CSV, disposition: "attachment; filename=#{builder.file_name}" +end diff --git a/spec/dummy/config/initializers/devise.rb b/spec/dummy/config/initializers/devise.rb new file mode 100644 index 0000000..0247a6f --- /dev/null +++ b/spec/dummy/config/initializers/devise.rb @@ -0,0 +1,268 @@ +# Use this hook to configure devise mailer, warden hooks and so forth. +# Many of these configuration options can be set straight in your model. +Devise.setup do |config| + # The secret key used by Devise. Devise uses this key to generate + # random tokens. Changing this key will render invalid all existing + # confirmation, reset password and unlock tokens in the database. + # Devise will use the `secret_key_base` as its `secret_key` + # by default. You can change it below and use your own secret key. + # config.secret_key = '9f104774236dcfa1209adc8a9ae917b5615ce11e74437662e9b10e77cb09900d4578e90158146f3a8b8bbee7db6494b75c0fd0e7eed34f138cfd38c3a0fce6b2' + + # ==> Mailer Configuration + # Configure the e-mail address which will be shown in Devise::Mailer, + # note that it will be overwritten if you use your own mailer class + # with default "from" parameter. + config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' + + # Configure the class responsible to send e-mails. + # config.mailer = 'Devise::Mailer' + + # Configure the parent class responsible to send e-mails. + # config.parent_mailer = 'ActionMailer::Base' + + # ==> ORM configuration + # Load and configure the ORM. Supports :active_record (default) and + # :mongoid (bson_ext recommended) by default. Other ORMs may be + # available as additional gems. + require 'devise/orm/active_record' + + # ==> Configuration for any authentication mechanism + # Configure which keys are used when authenticating a user. The default is + # just :email. You can configure it to use [:username, :subdomain], so for + # authenticating a user, both parameters are required. Remember that those + # parameters are used only when authenticating and not when retrieving from + # session. If you need permissions, you should implement that in a before filter. + # You can also supply a hash where the value is a boolean determining whether + # or not authentication should be aborted when the value is not present. + # config.authentication_keys = [:email] + + # Configure parameters from the request object used for authentication. Each entry + # given should be a request method and it will automatically be passed to the + # find_for_authentication method and considered in your model lookup. For instance, + # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. + # The same considerations mentioned for authentication_keys also apply to request_keys. + # config.request_keys = [] + + # Configure which authentication keys should be case-insensitive. + # These keys will be downcased upon creating or modifying a user and when used + # to authenticate or find a user. Default is :email. + config.case_insensitive_keys = [:email] + + # Configure which authentication keys should have whitespace stripped. + # These keys will have whitespace before and after removed upon creating or + # modifying a user and when used to authenticate or find a user. Default is :email. + config.strip_whitespace_keys = [:email] + + # Tell if authentication through request.params is enabled. True by default. + # It can be set to an array that will enable params authentication only for the + # given strategies, for example, `config.params_authenticatable = [:database]` will + # enable it only for database (email + password) authentication. + # config.params_authenticatable = true + + # Tell if authentication through HTTP Auth is enabled. False by default. + # It can be set to an array that will enable http authentication only for the + # given strategies, for example, `config.http_authenticatable = [:database]` will + # enable it only for database authentication. The supported strategies are: + # :database = Support basic authentication with authentication key + password + # config.http_authenticatable = false + + # If 401 status code should be returned for AJAX requests. True by default. + # config.http_authenticatable_on_xhr = true + + # The realm used in Http Basic Authentication. 'Application' by default. + # config.http_authentication_realm = 'Application' + + # It will change confirmation, password recovery and other workflows + # to behave the same regardless if the e-mail provided was right or wrong. + # Does not affect registerable. + # config.paranoid = true + + # By default Devise will store the user in session. You can skip storage for + # particular strategies by setting this option. + # Notice that if you are skipping storage for all authentication paths, you + # may want to disable generating routes to Devise's sessions controller by + # passing skip: :sessions to `devise_for` in your config/routes.rb + config.skip_session_storage = [:http_auth] + + # By default, Devise cleans up the CSRF token on authentication to + # avoid CSRF token fixation attacks. This means that, when using AJAX + # requests for sign in and sign up, you need to get a new CSRF token + # from the server. You can disable this option at your own risk. + # config.clean_up_csrf_token_on_authentication = true + + # ==> Configuration for :database_authenticatable + # For bcrypt, this is the cost for hashing the password and defaults to 11. If + # using other algorithms, it sets how many times you want the password to be hashed. + # + # Limiting the stretches to just one in testing will increase the performance of + # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use + # a value less than 10 in other environments. Note that, for bcrypt (the default + # algorithm), the cost increases exponentially with the number of stretches (e.g. + # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). + config.stretches = Rails.env.test? ? 1 : 11 + + # Set up a pepper to generate the hashed password. + # config.pepper = '5b2eecc7b0fa0b91df27285f6707cadc979ab91a105ccd190c17c2dbd959cf0ebf4f766ae50564a56b1be5d53389b247cd44dc401839910f554e5a513bdcf0e2' + + # Send a notification email when the user's password is changed + # config.send_password_change_notification = false + + # ==> Configuration for :confirmable + # A period that the user is allowed to access the website even without + # confirming their account. For instance, if set to 2.days, the user will be + # able to access the website for two days without confirming their account, + # access will be blocked just in the third day. Default is 0.days, meaning + # the user cannot access the website without confirming their account. + # config.allow_unconfirmed_access_for = 2.days + + # A period that the user is allowed to confirm their account before their + # token becomes invalid. For example, if set to 3.days, the user can confirm + # their account within 3 days after the mail was sent, but on the fourth day + # their account can't be confirmed with the token any more. + # Default is nil, meaning there is no restriction on how long a user can take + # before confirming their account. + # config.confirm_within = 3.days + + # If true, requires any email changes to be confirmed (exactly the same way as + # initial account confirmation) to be applied. Requires additional unconfirmed_email + # db field (see migrations). Until confirmed, new email is stored in + # unconfirmed_email column, and copied to email column on successful confirmation. + config.reconfirmable = true + + # Defines which key will be used when confirming an account + # config.confirmation_keys = [:email] + + # ==> Configuration for :rememberable + # The time the user will be remembered without asking for credentials again. + # config.remember_for = 2.weeks + + # Invalidates all the remember me tokens when the user signs out. + config.expire_all_remember_me_on_sign_out = true + + # If true, extends the user's remember period when remembered via cookie. + # config.extend_remember_period = false + + # Options to be passed to the created cookie. For instance, you can set + # secure: true in order to force SSL only cookies. + # config.rememberable_options = {} + + # ==> Configuration for :validatable + # Range for password length. + config.password_length = 6..128 + + # Email regex used to validate email formats. It simply asserts that + # one (and only one) @ exists in the given string. This is mainly + # to give user feedback and not to assert the e-mail validity. + config.email_regexp = /\A[^@]+@[^@]+\z/ + + # ==> Configuration for :timeoutable + # The time you want to timeout the user session without activity. After this + # time the user will be asked for credentials again. Default is 30 minutes. + # config.timeout_in = 30.minutes + + # ==> Configuration for :lockable + # Defines which strategy will be used to lock an account. + # :failed_attempts = Locks an account after a number of failed attempts to sign in. + # :none = No lock strategy. You should handle locking by yourself. + # config.lock_strategy = :failed_attempts + + # Defines which key will be used when locking and unlocking an account + # config.unlock_keys = [:email] + + # Defines which strategy will be used to unlock an account. + # :email = Sends an unlock link to the user email + # :time = Re-enables login after a certain amount of time (see :unlock_in below) + # :both = Enables both strategies + # :none = No unlock strategy. You should handle unlocking by yourself. + # config.unlock_strategy = :both + + # Number of authentication tries before locking an account if lock_strategy + # is failed attempts. + # config.maximum_attempts = 20 + + # Time interval to unlock the account if :time is enabled as unlock_strategy. + # config.unlock_in = 1.hour + + # Warn on the last attempt before the account is locked. + # config.last_attempt_warning = true + + # ==> Configuration for :recoverable + # + # Defines which key will be used when recovering the password for an account + # config.reset_password_keys = [:email] + + # Time interval you can reset your password with a reset password key. + # Don't put a too small interval or your users won't have the time to + # change their passwords. + config.reset_password_within = 6.hours + + # When set to false, does not sign a user in automatically after their password is + # reset. Defaults to true, so a user is signed in automatically after a reset. + # config.sign_in_after_reset_password = true + + # ==> Configuration for :encryptable + # Allow you to use another hashing or encryption algorithm besides bcrypt (default). + # You can use :sha1, :sha512 or algorithms from others authentication tools as + # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20 + # for default behavior) and :restful_authentication_sha1 (then you should set + # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper). + # + # Require the `devise-encryptable` gem when using anything other than bcrypt + # config.encryptor = :sha512 + + # ==> Scopes configuration + # Turn scoped views on. Before rendering "sessions/new", it will first check for + # "users/sessions/new". It's turned off by default because it's slower if you + # are using only default views. + # config.scoped_views = false + + # Configure the default scope given to Warden. By default it's the first + # devise role declared in your routes (usually :user). + # config.default_scope = :user + + # Set this configuration to false if you want /users/sign_out to sign out + # only the current scope. By default, Devise signs out all scopes. + # config.sign_out_all_scopes = true + + # ==> Navigation configuration + # Lists the formats that should be treated as navigational. Formats like + # :html, should redirect to the sign in page when the user does not have + # access, but formats like :xml or :json, should return 401. + # + # If you have any extra navigational formats, like :iphone or :mobile, you + # should add them to the navigational formats lists. + # + # The "*/*" below is required to match Internet Explorer requests. + # config.navigational_formats = ['*/*', :html] + + # The default HTTP method used to sign out a resource. Default is :delete. + config.sign_out_via = :delete + + # ==> OmniAuth + # Add a new OmniAuth provider. Check the wiki for more information on setting + # up on your models and hooks. + # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' + + # ==> Warden configuration + # If you want to use other strategies, that are not supported by Devise, or + # change the failure app, you can configure them inside the config.warden block. + # + # config.warden do |manager| + # manager.intercept_401 = false + # manager.default_strategies(scope: :user).unshift :some_external_strategy + # end + + # ==> Mountable engine configurations + # When using Devise inside an engine, let's call it `MyEngine`, and this engine + # is mountable, there are some extra configurations to be taken into account. + # The following options are available, assuming the engine is mounted as: + # + # mount MyEngine, at: '/my_engine' + # + # The router that invoked `devise_for`, in the example above, would be: + # config.router_name = :my_engine + # + # When using OmniAuth, Devise cannot automatically set OmniAuth path, + # so you need to do it manually. For the users scope, it would be: + # config.omniauth_path_prefix = '/my_engine/users/auth' +end diff --git a/spec/dummy/config/initializers/dunlop_overdue_config.rb b/spec/dummy/config/initializers/dunlop_overdue_config.rb new file mode 100644 index 0000000..b6e4d49 --- /dev/null +++ b/spec/dummy/config/initializers/dunlop_overdue_config.rb @@ -0,0 +1,3 @@ +Dunlop::OverdueHandling.define do + when_workflow_step :contractor_initialization, is: 2.days, from: {owner_initialization: :plan_date} +end diff --git a/spec/dummy/config/initializers/dunlop_workflow_config.rb b/spec/dummy/config/initializers/dunlop_workflow_config.rb new file mode 100644 index 0000000..9e24c2a --- /dev/null +++ b/spec/dummy/config/initializers/dunlop_workflow_config.rb @@ -0,0 +1,3 @@ +Dunlop::Workflow.configure do |config| + config.max_number_of_records_in_display_badge = 72 +end diff --git a/spec/dummy/config/initializers/dunlop_workflow_steps_in_batch_finished_config.rb b/spec/dummy/config/initializers/dunlop_workflow_steps_in_batch_finished_config.rb new file mode 100644 index 0000000..273a0d0 --- /dev/null +++ b/spec/dummy/config/initializers/dunlop_workflow_steps_in_batch_finished_config.rb @@ -0,0 +1,28 @@ +Dunlop::WorkflowStepsInBatchFinished.define do + the_final_states_are %w[rejected completed] + + on_finishing_of :owner_initialization do + description "Send notification mail to Owner" do |workflow_instance_batch| + OwnerMailer.owner_initialization_finished(workflow_instance_batch, self).deliver_later + end + end + + #on_finishing_of :contractor_service_window1_execution do + # description <<-DESCRIPTION + # Check ${attributes.owner_service_window2_preparation.migration_file_for_bvt_ready} + # for ${models.owner_service_window2_preparation} workflow steps in the batch + # DESCRIPTION + # action do |workflow_instance_batch| + # workflow_steps_in_batch_scope(:owner_service_window2_preparation).update_all migration_file_for_bvt_ready: true + # end + + # description "Send an email to Core Delivery informing them of batch completion" do |workflow_instance_batch| + # KpnCoreDeliveryMailer.contractor_service_window1_handled_for_batch(workflow_instance_batch.id).deliver_later + # end + + # description "Send an email to Schuuring informing them of batch completion" do |workflow_instance_batch| + # SchuuringMailer.workflow_instance_batch_workflow_step_finished(self.class.name, workflow_instance_batch.id).deliver_later + # end + #end + +end diff --git a/spec/dummy/config/initializers/filter_parameter_logging.rb b/spec/dummy/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000..4a994e1 --- /dev/null +++ b/spec/dummy/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Configure sensitive parameters which will be filtered from the log file. +Rails.application.config.filter_parameters += [:password] diff --git a/spec/dummy/config/initializers/inflections.rb b/spec/dummy/config/initializers/inflections.rb new file mode 100644 index 0000000..ac033bf --- /dev/null +++ b/spec/dummy/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, '\1en' +# inflect.singular /^(ox)en/i, '\1' +# inflect.irregular 'person', 'people' +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym 'RESTful' +# end diff --git a/spec/dummy/config/initializers/mime_types.rb b/spec/dummy/config/initializers/mime_types.rb new file mode 100644 index 0000000..dc18996 --- /dev/null +++ b/spec/dummy/config/initializers/mime_types.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Add new mime types for use in respond_to blocks: +# Mime::Type.register "text/richtext", :rtf diff --git a/spec/dummy/config/initializers/session_store.rb b/spec/dummy/config/initializers/session_store.rb new file mode 100644 index 0000000..e766b67 --- /dev/null +++ b/spec/dummy/config/initializers/session_store.rb @@ -0,0 +1,3 @@ +# Be sure to restart your server when you modify this file. + +Rails.application.config.session_store :cookie_store, key: '_dummy_session' diff --git a/spec/dummy/config/initializers/simple_form.rb b/spec/dummy/config/initializers/simple_form.rb new file mode 100644 index 0000000..a0d3327 --- /dev/null +++ b/spec/dummy/config/initializers/simple_form.rb @@ -0,0 +1,202 @@ +# Use this setup block to configure all options available in SimpleForm. +SimpleForm.setup do |config| + # Wrappers are used by the form builder to generate a + # complete input. You can remove any component from the + # wrapper, change the order or even add your own to the + # stack. The options given below are used to wrap the + # whole input. + config.wrappers :default, class: :input, hint_class: :field_with_hint, error_class: :field_with_errors do |b| + ## Extensions enabled by default + # Any of these extensions can be disabled for a + # given input by passing: `f.input EXTENSION_NAME => false`. + # You can make any of these extensions optional by + # renaming `b.use` to `b.optional`. + + # Determines whether to use HTML5 (:email, :url, ...) + # and required attributes + b.use :html5 + + # Calculates placeholders automatically from I18n + # You can also pass a string as f.input placeholder: "Placeholder" + b.use :placeholder + + ## Optional extensions + # They are disabled unless you pass `f.input EXTENSION_NAME => true` + # to the input. If so, they will retrieve the values from the model + # if any exists. If you want to enable any of those + # extensions by default, you can change `b.optional` to `b.use`. + + # Calculates maxlength from length validations for string inputs + # and/or database column lengths + b.optional :maxlength + + # Calculate minlength from length validations for string inputs + b.optional :minlength + + # Calculates pattern from format validations for string inputs + b.optional :pattern + + # Calculates min and max from length validations for numeric inputs + b.optional :min_max + + # Calculates readonly automatically from readonly attributes + b.optional :readonly + + ## Inputs + b.use :label_input + b.use :hint, wrap_with: { tag: :span, class: :hint } + b.use :error, wrap_with: { tag: :span, class: :error } + end + + # Custom Semantic Wrapper + # Values are similar to the default wrapper above, with different classes + config.wrappers :semantic, tag: 'div', class: "field", error_class: 'error', hint_class: 'with_hint' do |b| + b.use :html5 + b.use :placeholder + b.optional :maxlength + b.optional :pattern + b.optional :min_max + b.use :label_input + b.use :hint, wrap_with: { tag: 'div', class: 'hint' } + b.use :error, wrap_with: { tag: 'div', class: 'ui red pointing above label error' } + end + + config.wrappers :ui_checkbox, tag: 'div', class: "field", error_class: 'error', hint_class: 'with_hint' do |b| + b.use :html5 + b.wrapper tag: 'div', class: 'ui checkbox' do |input| + input.use :label_input + input.use :hint, wrap_with: { tag: 'div', class: 'hint' } + end + end + + config.wrappers :ui_slider_checkbox, tag: 'div', class: "field", error_class: 'error', hint_class: 'with_hint' do |b| + b.use :html5 + b.wrapper tag: 'div', class: 'ui slider checkbox' do |input| + input.use :label_input + input.use :hint, wrap_with: { tag: 'div', class: 'hint' } + end + end + + config.wrappers :ui_toggle_checkbox, tag: 'div', class: "field", error_class: 'error', hint_class: 'with_hint' do |b| + b.use :html5 + b.wrapper tag: 'div', class: 'ui toggle checkbox' do |input| + input.use :label_input + input.use :hint, wrap_with: { tag: 'div', class: 'hint' } + end + end + + # The default wrapper to be used by the FormBuilder. + # config.default_wrapper = :default + config.default_wrapper = :semantic + + # Define the way to render check boxes / radio buttons with labels. + # Defaults to :nested for bootstrap config. + # inline: input + label + # nested: label > input + config.boolean_style = :inline + + # Default class for buttons + config.button_class = 'ui primary submit button' + + # Method used to tidy up errors. Specify any Rails Array method. + # :first lists the first message for each field. + # Use :to_sentence to list all errors for each field. + config.error_method = :first + + # Default tag used for error notification helper. + config.error_notification_tag = :div + + # CSS class to add for error notification helper. + config.error_notification_class = 'alert alert-error' + + # ID to add for error notification helper. + # config.error_notification_id = nil + + # Series of attempts to detect a default label method for collection. + # config.collection_label_methods = [ :to_label, :name, :title, :to_s ] + + # Series of attempts to detect a default value method for collection. + # config.collection_value_methods = [ :id, :to_s ] + + # You can wrap a collection of radio/check boxes in a pre-defined tag, defaulting to none. + # config.collection_wrapper_tag = :div + + # You can define the class to use on all collection wrappers. Defaulting to none. + # config.collection_wrapper_class = "field" + + # You can wrap each item in a collection of radio/check boxes with a tag, + # defaulting to :span. Please note that when using :boolean_style = :nested, + # SimpleForm will force this option to be a label. + config.item_wrapper_tag = :div + + # You can define a class to use in all item wrappers. Defaulting to none. + config.item_wrapper_class = 'ui checkbox' + + # How the label text should be generated altogether with the required text. + # config.label_text = lambda { |label, required, explicit_label| "#{required} #{label}" } + config.label_text = lambda { |label, required, explicit_label| "#{label}" } + # Semantic UI has its own astrick + + # You can define the class to use on all labels. Default is nil. + # config.label_class = nil + + # You can define the class to use on all forms. Default is simple_form. + config.default_form_class = 'ui form' + + # You can define which elements should obtain additional classes + # config.generate_additional_classes_for = [:wrapper, :label, :input] + + # Whether attributes are required by default (or not). Default is true. + # config.required_by_default = true + + # Tell browsers whether to use the native HTML5 validations (novalidate form option). + # These validations are enabled in SimpleForm's internal config but disabled by default + # in this configuration, which is recommended due to some quirks from different browsers. + # To stop SimpleForm from generating the novalidate option, enabling the HTML5 validations, + # change this configuration to true. + config.browser_validations = false + + # Collection of methods to detect if a file type was given. + # config.file_methods = [ :mounted_as, :file?, :public_filename ] + + # Custom mappings for input types. This should be a hash containing a regexp + # to match as key, and the input type that will be used when the field name + # matches the regexp as value. + # config.input_mappings = { /count/ => :integer } + + # Custom wrappers for input types. This should be a hash containing an input + # type as key and the wrapper that will be used for all inputs with specified type. + # config.wrapper_mappings = { string: :prepend } + + # Namespaces where SimpleForm should look for custom input classes that + # override default inputs. + # config.custom_inputs_namespaces << "CustomInputs" + + # Default priority for time_zone inputs. + # config.time_zone_priority = nil + + # Default priority for country inputs. + # config.country_priority = nil + + # When false, do not use translations for labels. + # config.translate_labels = true + + # Automatically discover new inputs in Rails' autoload path. + # config.inputs_discovery = true + + # Cache SimpleForm inputs discovery + # config.cache_discovery = !Rails.env.development? + + # Default class for inputs + # config.input_class = nil + + # Define the default class of the input wrapper of the boolean input. + config.boolean_label_class = 'checkbox' + + # Defines if the default input wrapper class should be included in radio + # collection wrappers. + # config.include_default_input_wrapper_class = true + + # Defines which i18n scope will be used in Simple Form. + # config.i18n_scope = 'simple_form' +end diff --git a/spec/dummy/config/initializers/to_prepare.rb b/spec/dummy/config/initializers/to_prepare.rb new file mode 100644 index 0000000..f92b0bf --- /dev/null +++ b/spec/dummy/config/initializers/to_prepare.rb @@ -0,0 +1,7 @@ +Rails.application.config.to_prepare do + require_dependency Rails.root.join("config/initializers/dunlop_overdue_config") + if ability_class = "Ability".safe_constantize and ability_class.respond_to?(:add_dunlop_allowed_authorization_classes!) + ability_class.add_dunlop_allowed_authorization_classes! + end +end + diff --git a/spec/dummy/config/initializers/wrap_parameters.rb b/spec/dummy/config/initializers/wrap_parameters.rb new file mode 100644 index 0000000..33725e9 --- /dev/null +++ b/spec/dummy/config/initializers/wrap_parameters.rb @@ -0,0 +1,14 @@ +# Be sure to restart your server when you modify this file. + +# This file contains settings for ActionController::ParamsWrapper which +# is enabled by default. + +# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. +ActiveSupport.on_load(:action_controller) do + wrap_parameters format: [:json] if respond_to?(:wrap_parameters) +end + +# To enable root element in JSON for ActiveRecord objects. +# ActiveSupport.on_load(:active_record) do +# self.include_root_in_json = true +# end diff --git a/spec/dummy/config/locales/devise.en.yml b/spec/dummy/config/locales/devise.en.yml new file mode 100644 index 0000000..bd4c3eb --- /dev/null +++ b/spec/dummy/config/locales/devise.en.yml @@ -0,0 +1,62 @@ +# Additional translations at https://github.com/plataformatec/devise/wiki/I18n + +en: + devise: + confirmations: + confirmed: "Your email address has been successfully confirmed." + send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." + failure: + already_authenticated: "You are already signed in." + inactive: "Your account is not activated yet." + invalid: "Invalid %{authentication_keys} or password." + locked: "Your account is locked." + last_attempt: "You have one more attempt before your account is locked." + not_found_in_database: "Invalid %{authentication_keys} or password." + timeout: "Your session expired. Please sign in again to continue." + unauthenticated: "You need to sign in or sign up before continuing." + unconfirmed: "You have to confirm your email address before continuing." + mailer: + confirmation_instructions: + subject: "Confirmation instructions" + reset_password_instructions: + subject: "Reset password instructions" + unlock_instructions: + subject: "Unlock instructions" + password_change: + subject: "Password Changed" + omniauth_callbacks: + failure: "Could not authenticate you from %{kind} because \"%{reason}\"." + success: "Successfully authenticated from %{kind} account." + passwords: + no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." + send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." + updated: "Your password has been changed successfully. You are now signed in." + updated_not_active: "Your password has been changed successfully." + registrations: + destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." + signed_up: "Welcome! You have signed up successfully." + signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." + signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." + signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." + update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address." + updated: "Your account has been updated successfully." + sessions: + signed_in: "Signed in successfully." + signed_out: "Signed out successfully." + already_signed_out: "Signed out successfully." + unlocks: + send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." + send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." + unlocked: "Your account has been unlocked successfully. Please sign in to continue." + errors: + messages: + already_confirmed: "was already confirmed, please try signing in" + confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" + expired: "has expired, please request a new one" + not_found: "not found" + not_locked: "was not locked" + not_saved: + one: "1 error prohibited this %{resource} from being saved:" + other: "%{count} errors prohibited this %{resource} from being saved:" diff --git a/spec/dummy/config/locales/en.yml b/spec/dummy/config/locales/en.yml new file mode 100644 index 0000000..0653957 --- /dev/null +++ b/spec/dummy/config/locales/en.yml @@ -0,0 +1,23 @@ +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t 'hello' +# +# In views, this is aliased to just `t`: +# +# <%= t('hello') %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# To learn more, please read the Rails Internationalization guide +# available at http://guides.rubyonrails.org/i18n.html. + +en: + hello: "Hello world" diff --git a/spec/dummy/config/locales/models.nl.yml b/spec/dummy/config/locales/models.nl.yml new file mode 100644 index 0000000..8bd4ba2 --- /dev/null +++ b/spec/dummy/config/locales/models.nl.yml @@ -0,0 +1,7 @@ +nl: + activerecord: + models: + user: Gebruiker + plural: + user: Gebruikers + nested_model_name_test: ABC diff --git a/spec/dummy/config/locales/rails-i18n.nl.yml b/spec/dummy/config/locales/rails-i18n.nl.yml new file mode 100644 index 0000000..e74ce94 --- /dev/null +++ b/spec/dummy/config/locales/rails-i18n.nl.yml @@ -0,0 +1,205 @@ +--- +nl: + date: + abbr_day_names: + - zo + - ma + - di + - wo + - do + - vr + - za + abbr_month_names: + - + - jan + - feb + - mrt + - apr + - mei + - jun + - jul + - aug + - sep + - okt + - nov + - dec + day_names: + - zondag + - maandag + - dinsdag + - woensdag + - donderdag + - vrijdag + - zaterdag + formats: + default: "%d-%m-%Y" + long: "%e %B %Y" + short: "%e %b" + month_names: + - + - januari + - februari + - maart + - april + - mei + - juni + - juli + - augustus + - september + - oktober + - november + - december + order: + - :day + - :month + - :year + datetime: + distance_in_words: + about_x_hours: + one: ongeveer een uur + other: ongeveer %{count} uur + about_x_months: + one: ongeveer een maand + other: ongeveer %{count} maanden + about_x_years: + one: ongeveer een jaar + other: ongeveer %{count} jaar + almost_x_years: + one: bijna een jaar + other: bijna %{count} jaar + half_a_minute: een halve minuut + less_than_x_minutes: + one: minder dan een minuut + other: minder dan %{count} minuten + less_than_x_seconds: + one: minder dan een seconde + other: minder dan %{count} seconden + over_x_years: + one: meer dan een jaar + other: meer dan %{count} jaar + x_days: + one: 1 dag + other: "%{count} dagen" + x_minutes: + one: 1 minuut + other: "%{count} minuten" + x_months: + one: 1 maand + other: "%{count} maanden" + x_seconds: + one: 1 seconde + other: "%{count} seconden" + prompts: + day: dag + hour: uur + minute: minuut + month: maand + second: seconde + year: jaar + errors: + format: "%{attribute} %{message}" + messages: + accepted: moet worden geaccepteerd + blank: moet opgegeven zijn + present: moet leeg zijn + confirmation: komt niet overeen met %{attribute} + empty: moet opgegeven zijn + equal_to: moet gelijk zijn aan %{count} + even: moet even zijn + exclusion: is gereserveerd + greater_than: moet groter zijn dan %{count} + greater_than_or_equal_to: moet groter dan of gelijk zijn aan %{count} + inclusion: is niet in de lijst opgenomen + invalid: is ongeldig + less_than: moet minder zijn dan %{count} + less_than_or_equal_to: moet minder dan of gelijk zijn aan %{count} + not_a_number: is geen getal + not_an_integer: moet een geheel getal zijn + odd: moet oneven zijn + record_invalid: 'Validatie mislukt: %{errors}' + restrict_dependent_destroy: + one: Kan item niet verwijderen omdat %{record} afhankelijk is + many: Kan item niet verwijderen omdat afhankelijke %{record} bestaan + taken: is al in gebruik + too_long: + one: is te lang (maximaal %{count} teken) + other: is te lang (maximaal %{count} tekens) + too_short: + one: is te kort (minimaal %{count} teken) + other: is te kort (minimaal %{count} tekens) + wrong_length: + one: heeft onjuiste lengte (moet 1 teken lang zijn) + other: heeft onjuiste lengte (moet %{count} tekens lang zijn) + other_than: moet anders zijn dan %{count} + template: + body: 'Er zijn problemen met de volgende velden:' + header: + one: "%{model} niet opgeslagen: 1 fout gevonden" + other: "%{model} niet opgeslagen: %{count} fouten gevonden" + helpers: + select: + prompt: Maak een keuze + submit: + create: "%{model} toevoegen" + submit: "%{model} opslaan" + update: "%{model} bijwerken" + number: + currency: + format: + delimiter: "." + format: "%u %n" + precision: 2 + separator: "," + significant: false + strip_insignificant_zeros: false + unit: "€" + format: + delimiter: "." + precision: 2 + separator: "," + significant: false + strip_insignificant_zeros: false + human: + decimal_units: + format: "%n %u" + units: + billion: miljard + million: miljoen + quadrillion: quadriljoen + thousand: duizend + trillion: triljoen + unit: '' + format: + delimiter: '' + precision: 3 + significant: true + strip_insignificant_zeros: true + storage_units: + format: "%n %u" + units: + byte: + one: byte + other: bytes + gb: GB + kb: KB + mb: MB + tb: TB + percentage: + format: + delimiter: '' + format: "%n%" + precision: + format: + delimiter: '' + support: + array: + last_word_connector: " en " + two_words_connector: " en " + words_connector: ", " + time: + am: "'s ochtends" + formats: + default: "%a %d %b %Y %H:%M:%S %Z" + long: "%d %B %Y %H:%M" + short: "%d %b %H:%M" + pm: "'s middags" diff --git a/spec/dummy/config/locales/scenario-scenario1.yml b/spec/dummy/config/locales/scenario-scenario1.yml new file mode 100644 index 0000000..0eb48a9 --- /dev/null +++ b/spec/dummy/config/locales/scenario-scenario1.yml @@ -0,0 +1,8 @@ +nl: + activerecord: + models: + workflow_instance/scenario1: SC1 + workflow_instance_batch/scenario1: SC1 Batch + plural: + workflow_instance/scenario1: SC1s + workflow_instance_batch/scenario1: SC1 Batches diff --git a/spec/dummy/config/locales/scenario-scenario2.yml b/spec/dummy/config/locales/scenario-scenario2.yml new file mode 100644 index 0000000..22a1b36 --- /dev/null +++ b/spec/dummy/config/locales/scenario-scenario2.yml @@ -0,0 +1,8 @@ +nl: + activerecord: + models: + workflow_instance/scenario2: SC2 + workflow_instance_batch/scenario2: SC2 Batch + plural: + workflow_instance/scenario2: SC2s + workflow_instance_batch/scenario2: SC2 Batches diff --git a/spec/dummy/config/locales/source_files.yml b/spec/dummy/config/locales/source_files.yml new file mode 100644 index 0000000..a7776e6 --- /dev/null +++ b/spec/dummy/config/locales/source_files.yml @@ -0,0 +1,6 @@ +nl: + activerecord: + models: + source_file/my_source: My source + plural: + source_file/my_source: My sources diff --git a/spec/dummy/config/locales/target_files.yml b/spec/dummy/config/locales/target_files.yml new file mode 100644 index 0000000..28c18dc --- /dev/null +++ b/spec/dummy/config/locales/target_files.yml @@ -0,0 +1,14 @@ +nl: + activerecord: + models: + target_file: Target file + target_file/my_target: My target + target_file/file_without_builder: File without builder + plural: + target_file: Target files + target_file/my_target: My targets + target_file/file_without_builder: File without builders + attributes: + target_file: + sti_type: Type + original_file: File diff --git a/spec/dummy/config/locales/workflow_instance_batch.yml b/spec/dummy/config/locales/workflow_instance_batch.yml new file mode 100644 index 0000000..598cb8f --- /dev/null +++ b/spec/dummy/config/locales/workflow_instance_batch.yml @@ -0,0 +1,9 @@ +nl: + workflow_instance_batch: + cannot_reject_message: | + Het is niet mogelijk om een Batch in zijn geheel te rejecten. + Ga naar een individuele DSLAM en voer de reject actie daar uit. + to_index_button: Naar Batches overzicht + edit_dslams_button: Bewerk alle records in de batch + show_dslams_button: Records tonen in lijst + diff --git a/spec/dummy/config/locales/workflow_instances.yml b/spec/dummy/config/locales/workflow_instances.yml new file mode 100644 index 0000000..2eae7c3 --- /dev/null +++ b/spec/dummy/config/locales/workflow_instances.yml @@ -0,0 +1,20 @@ +nl: + activerecord: + models: + workflow_instance: Migration + plural: + workflow_instance: Migrations + workflow_instance: + display_badge: WI + workflow_steps_header: Stappen + show_more_attributes: Meer attributen tonen + reset_confirmation: | + Are you sure you want to reset? + + All the progress will be lost. The notes will remain + selection: + submit_button: Select by DSLAM name + progress_display: + title: Voortgang + export_button: Download Records + diff --git a/spec/dummy/config/locales/workflow_step-contractor_initialization.yml b/spec/dummy/config/locales/workflow_step-contractor_initialization.yml new file mode 100644 index 0000000..24f8d77 --- /dev/null +++ b/spec/dummy/config/locales/workflow_step-contractor_initialization.yml @@ -0,0 +1,12 @@ +nl: + activerecord: + models: + contractor_initialization: Contr. initialisatie + plural: + contractor_initialization: Contr. initialisaties + attributes: + contractor_initialization: + plan_date: Contr Datum + + contractor_initialization: + display_badge: Contr. init diff --git a/spec/dummy/config/locales/workflow_step-owner_initialization.yml b/spec/dummy/config/locales/workflow_step-owner_initialization.yml new file mode 100644 index 0000000..474a6ae --- /dev/null +++ b/spec/dummy/config/locales/workflow_step-owner_initialization.yml @@ -0,0 +1,19 @@ +nl: + activerecord: + models: + owner_initialization: Core initialisatie + plural: + owner_initialization: Core initialisaties + attributes: + owner_initialization: + initialization_check_done: HazBeenChecked + plan_date: Dattum + my_identifier: My identifier + window_from: Window from + window_to: Window to + number_of_visits: Number of visits + my_fraction: My fraction + my_explanation: My explanation + + owner_initialization: + display_badge: Owner Init diff --git a/spec/dummy/config/locales/workflow_step-wba_submission.yml b/spec/dummy/config/locales/workflow_step-wba_submission.yml new file mode 100644 index 0000000..b394157 --- /dev/null +++ b/spec/dummy/config/locales/workflow_step-wba_submission.yml @@ -0,0 +1,13 @@ +nl: + activerecord: + models: + wba_submission: Wba submission + plural: + wba_submission: Wba submissions + attributes: + wba_submission: + all_clear: All clear + new_service_group: New service group + + wba_submission: + display_badge: Wba submission diff --git a/spec/dummy/config/locales/workflow_steps.yml b/spec/dummy/config/locales/workflow_steps.yml new file mode 100644 index 0000000..19804ad --- /dev/null +++ b/spec/dummy/config/locales/workflow_steps.yml @@ -0,0 +1,14 @@ +nl: + workflow_step: + name: Workflow step + plural_name: Workflow steps + previous_step_not_finished_warning: | + Nog niet alle acties van %{step_name} zijn afgerond + Weet je zeker dat je wilt doorgaan? + actions_modal: + title: '%{model} actions' + process_button: Process / Update + complete_button: Complete + reject_button: Reject + edit_single_title: 'Edit %{workflow_step} of %{workflow_instance} %{presentation_name}' + update_button_text: Update %{models} diff --git a/spec/dummy/config/routes.rb b/spec/dummy/config/routes.rb new file mode 100644 index 0000000..cec03f0 --- /dev/null +++ b/spec/dummy/config/routes.rb @@ -0,0 +1,34 @@ +Rails.application.routes.draw do + + WorkflowInstance.possible_workflow_step_names.each do |workflow_step| + namespace workflow_step do + WorkflowInstance.scenario_names.each do |scenario_name| + collection_resources scenario_name.to_s.pluralize, only: [:edit, :update] + end + end + end + + mount Dunlop::Engine => "/dunlop" + + namespace :workflow_instance do + WorkflowInstance.scenario_names.each do |scenario_name| + collection_resources scenario_name.to_s.pluralize do + collection do + match :selection, via: [:get, :post] + end + post :reset, on: :member + end + end + end + + namespace :workflow_instance_batch do + WorkflowInstance.scenario_names.each do |scenario_name| + resources scenario_name.to_s.pluralize + end + end + + root to: 'dashboard#home' + get 'dashboard/render_as_dunlop_modal_test' + + devise_for :users +end diff --git a/spec/dummy/config/secrets.yml b/spec/dummy/config/secrets.yml new file mode 100644 index 0000000..7c51907 --- /dev/null +++ b/spec/dummy/config/secrets.yml @@ -0,0 +1,22 @@ +# Be sure to restart your server when you modify this file. + +# Your secret key is used for verifying the integrity of signed cookies. +# If you change this key, all old signed cookies will become invalid! + +# Make sure the secret is at least 30 characters and all random, +# no regular words or you'll be exposed to dictionary attacks. +# You can use `rake secret` to generate a secure secret key. + +# Make sure the secrets in this file are kept private +# if you're sharing your code publicly. + +development: + secret_key_base: 8e72d16d3e1fae4f12515c6612869443fad9ea542985c4da9d44cde495a990f1616df0d7cb17610f0f083847433f1fe26ee7750efb35867ba3012ec2123d68ac + +test: + secret_key_base: 71968d13c1bdc15b491180c1873c8925105f38f43509c5a572c2fb2a92f0f902cda7b26d6e95ce20c595f6b1ae408f0a02a50adb99429dec177a89ba6e38d653 + +# Do not keep production secrets in the repository, +# instead read values from the environment. +production: + secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> diff --git a/spec/dummy/db/migrate/20160424102153_devise_create_users.rb b/spec/dummy/db/migrate/20160424102153_devise_create_users.rb new file mode 100644 index 0000000..9200e8b --- /dev/null +++ b/spec/dummy/db/migrate/20160424102153_devise_create_users.rb @@ -0,0 +1,42 @@ +class DeviseCreateUsers < ActiveRecord::Migration + def change + create_table :users do |t| + ## Database authenticatable + t.string :email, null: false, default: "" + t.string :encrypted_password, null: false, default: "" + + ## Recoverable + t.string :reset_password_token + t.datetime :reset_password_sent_at + + ## Rememberable + t.datetime :remember_created_at + + ## Trackable + t.integer :sign_in_count, default: 0, null: false + t.datetime :current_sign_in_at + t.datetime :last_sign_in_at + t.string :current_sign_in_ip + t.string :last_sign_in_ip + + ## Confirmable + # t.string :confirmation_token + # t.datetime :confirmed_at + # t.datetime :confirmation_sent_at + # t.string :unconfirmed_email # Only if using reconfirmable + + ## Lockable + # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts + # t.string :unlock_token # Only if unlock strategy is :email or :both + # t.datetime :locked_at + + + t.timestamps null: false + end + + add_index :users, :email, unique: true + add_index :users, :reset_password_token, unique: true + # add_index :users, :confirmation_token, unique: true + # add_index :users, :unlock_token, unique: true + end +end diff --git a/spec/dummy/db/migrate/20160424102154_add_admin_and_role_names_to_users.rb b/spec/dummy/db/migrate/20160424102154_add_admin_and_role_names_to_users.rb new file mode 100644 index 0000000..687ab25 --- /dev/null +++ b/spec/dummy/db/migrate/20160424102154_add_admin_and_role_names_to_users.rb @@ -0,0 +1,7 @@ +class AddAdminAndRoleNamesToUsers < ActiveRecord::Migration + def change + add_column :users, :admin, :boolean, default: false, null: false + add_column :users, :role_names, :text + end +end + diff --git a/spec/dummy/db/migrate/20160424102156_create_dunlop_log_entries.rb b/spec/dummy/db/migrate/20160424102156_create_dunlop_log_entries.rb new file mode 100644 index 0000000..b1205ff --- /dev/null +++ b/spec/dummy/db/migrate/20160424102156_create_dunlop_log_entries.rb @@ -0,0 +1,14 @@ +class CreateDunlopLogEntries < ActiveRecord::Migration + def change + create_table :dunlop_log_entries do |t| + t.integer :loggable_id + t.string :loggable_type + t.string :message + t.text :body + t.datetime :created_at + t.datetime :updated_at + end + + add_index :dunlop_log_entries, [:loggable_id, :loggable_type] + end +end diff --git a/spec/dummy/db/migrate/20160424102200_create_source_files.rb b/spec/dummy/db/migrate/20160424102200_create_source_files.rb new file mode 100644 index 0000000..d68cd96 --- /dev/null +++ b/spec/dummy/db/migrate/20160424102200_create_source_files.rb @@ -0,0 +1,14 @@ +class CreateSourceFiles < ActiveRecord::Migration + def change + create_table :source_files do |t| + t.string :type + t.string :state + t.string :description + t.integer :number_of_records + t.integer :user_id + t.integer :duration + t.string :original_file + t.timestamps null: false + end + end +end diff --git a/spec/dummy/db/migrate/20160424102202_create_workflow_instances.rb b/spec/dummy/db/migrate/20160424102202_create_workflow_instances.rb new file mode 100644 index 0000000..aa8fa19 --- /dev/null +++ b/spec/dummy/db/migrate/20160424102202_create_workflow_instances.rb @@ -0,0 +1,14 @@ +class CreateWorkflowInstances < ActiveRecord::Migration + def change + create_table :workflow_instances do |t| + t.string :type + t.string :state + t.datetime :archived_at + t.string :bucket + t.integer :workflow_instance_batch_id + t.date :plan_date + t.timestamps null: false + end + end +end + diff --git a/spec/dummy/db/migrate/20160424102203_create_workflow_instance_batches.rb b/spec/dummy/db/migrate/20160424102203_create_workflow_instance_batches.rb new file mode 100644 index 0000000..ee8a4e8 --- /dev/null +++ b/spec/dummy/db/migrate/20160424102203_create_workflow_instance_batches.rb @@ -0,0 +1,11 @@ +class CreateWorkflowInstanceBatches < ActiveRecord::Migration + def change + create_table :workflow_instance_batches do |t| + t.string :type + t.string :state + t.string :name + t.timestamps null: false + end + end +end + diff --git a/spec/dummy/db/migrate/20160424102204_create_dunlop_last_attribute_changes.rb b/spec/dummy/db/migrate/20160424102204_create_dunlop_last_attribute_changes.rb new file mode 100644 index 0000000..ebb9979 --- /dev/null +++ b/spec/dummy/db/migrate/20160424102204_create_dunlop_last_attribute_changes.rb @@ -0,0 +1,13 @@ +class CreateDunlopLastAttributeChanges < ActiveRecord::Migration + def change + create_table :dunlop_last_attribute_changes do |t| + t.integer :target_id + t.string :target_type + t.datetime :changed_at + t.string :attribute_name + + t.timestamps null: false + end + end +end + diff --git a/spec/dummy/db/migrate/20160425095433_add_plan_date_to_workflow_instance_batches.rb b/spec/dummy/db/migrate/20160425095433_add_plan_date_to_workflow_instance_batches.rb new file mode 100644 index 0000000..a02c72d --- /dev/null +++ b/spec/dummy/db/migrate/20160425095433_add_plan_date_to_workflow_instance_batches.rb @@ -0,0 +1,5 @@ +class AddPlanDateToWorkflowInstanceBatches < ActiveRecord::Migration + def change + add_column :workflow_instance_batches, :plan_date, :date + end +end diff --git a/spec/dummy/db/migrate/20160425111751_create_owner_initializations.rb b/spec/dummy/db/migrate/20160425111751_create_owner_initializations.rb new file mode 100644 index 0000000..c73fe46 --- /dev/null +++ b/spec/dummy/db/migrate/20160425111751_create_owner_initializations.rb @@ -0,0 +1,23 @@ +class CreateOwnerInitializations < ActiveRecord::Migration + def change + create_table :owner_initializations do |t| + t.belongs_to :workflow_instance, index: true + t.string :state + t.string :type + t.text :notes + + t.boolean :checked, default: false, null: false + t.date :plan_date + t.string :my_identifier + t.integer :window_from + t.integer :window_to + t.integer :number_of_visits + t.float :my_fraction + t.text :my_explanation + + t.timestamps null: false + end + + WorkflowInstance::Scenario1.pluck(:id).each{|id| OwnerInitialization::Scenario1.create!(workflow_instance_id: id) } + end +end diff --git a/spec/dummy/db/migrate/20160426104411_rename_checked_to_initialization_check_done.rb b/spec/dummy/db/migrate/20160426104411_rename_checked_to_initialization_check_done.rb new file mode 100644 index 0000000..3e2bdbc --- /dev/null +++ b/spec/dummy/db/migrate/20160426104411_rename_checked_to_initialization_check_done.rb @@ -0,0 +1,5 @@ +class RenameCheckedToInitializationCheckDone < ActiveRecord::Migration + def change + rename_column :owner_initializations, :checked, :initialization_check_done + end +end diff --git a/spec/dummy/db/migrate/20160426111524_create_contractor_initializations.rb b/spec/dummy/db/migrate/20160426111524_create_contractor_initializations.rb new file mode 100644 index 0000000..a40f9a1 --- /dev/null +++ b/spec/dummy/db/migrate/20160426111524_create_contractor_initializations.rb @@ -0,0 +1,15 @@ +class CreateContractorInitializations < ActiveRecord::Migration + def change + create_table :contractor_initializations do |t| + t.belongs_to :workflow_instance, index: true + t.string :state + t.string :type + t.text :notes + + + t.timestamps null: false + end + + WorkflowInstance::Scenario1.pluck(:id).each{|id| ContractorInitialization::Scenario1.create!(workflow_instance_id: id) } + end +end diff --git a/spec/dummy/db/migrate/20160428121513_add_address_info_to_workflow_instances.rb b/spec/dummy/db/migrate/20160428121513_add_address_info_to_workflow_instances.rb new file mode 100644 index 0000000..c845f43 --- /dev/null +++ b/spec/dummy/db/migrate/20160428121513_add_address_info_to_workflow_instances.rb @@ -0,0 +1,7 @@ +class AddAddressInfoToWorkflowInstances < ActiveRecord::Migration + def change + add_column :workflow_instances, :zipcode, :string + add_column :workflow_instances, :housenumber, :integer + add_column :workflow_instances, :housenumber_ext, :string + end +end diff --git a/spec/dummy/db/migrate/20160504160114_change_type_to_sti_type.rb b/spec/dummy/db/migrate/20160504160114_change_type_to_sti_type.rb new file mode 100644 index 0000000..41913fc --- /dev/null +++ b/spec/dummy/db/migrate/20160504160114_change_type_to_sti_type.rb @@ -0,0 +1,9 @@ +class ChangeTypeToStiType < ActiveRecord::Migration + def change + rename_column :contractor_initializations, :type, :sti_type + rename_column :owner_initializations, :type, :sti_type + rename_column :source_files, :type, :sti_type + rename_column :workflow_instance_batches, :type, :sti_type + rename_column :workflow_instances, :type, :sti_type + end +end diff --git a/spec/dummy/db/migrate/20160512103847_add_my_name_to_workflow_instances.rb b/spec/dummy/db/migrate/20160512103847_add_my_name_to_workflow_instances.rb new file mode 100644 index 0000000..7648e29 --- /dev/null +++ b/spec/dummy/db/migrate/20160512103847_add_my_name_to_workflow_instances.rb @@ -0,0 +1,5 @@ +class AddMyNameToWorkflowInstances < ActiveRecord::Migration + def change + add_column :workflow_instances, :my_name, :string + end +end diff --git a/spec/dummy/db/migrate/20160525093841_create_wba_submissions.rb b/spec/dummy/db/migrate/20160525093841_create_wba_submissions.rb new file mode 100644 index 0000000..ff4a647 --- /dev/null +++ b/spec/dummy/db/migrate/20160525093841_create_wba_submissions.rb @@ -0,0 +1,17 @@ +class CreateWbaSubmissions < ActiveRecord::Migration + def change + create_table :wba_submissions do |t| + t.belongs_to :workflow_instance, index: true + t.string :state + t.string :sti_type + t.text :notes + + t.boolean :all_clear, default: false, null: false + t.string :new_service_group + + t.timestamps null: false + end + + WorkflowInstance::Scenario2.pluck(:id).each{|id| WbaSubmission::Scenario2.create!(workflow_instance_id: id) } + end +end diff --git a/spec/dummy/db/migrate/20160623120618_create_source_record_my_sources.rb b/spec/dummy/db/migrate/20160623120618_create_source_record_my_sources.rb new file mode 100644 index 0000000..3710c80 --- /dev/null +++ b/spec/dummy/db/migrate/20160623120618_create_source_record_my_sources.rb @@ -0,0 +1,13 @@ +class CreateSourceRecordMySources < ActiveRecord::Migration + def change + create_table :source_record_my_sources do |t| + t.date :my_date + t.string :my_var + t.integer :my_count + t.float :my_num + t.boolean :my_truth + + t.timestamps null: false + end + end +end diff --git a/spec/dummy/db/migrate/20160628095423_create_dunlop_execution_batches.rb b/spec/dummy/db/migrate/20160628095423_create_dunlop_execution_batches.rb new file mode 100644 index 0000000..ff90c05 --- /dev/null +++ b/spec/dummy/db/migrate/20160628095423_create_dunlop_execution_batches.rb @@ -0,0 +1,11 @@ +class CreateDunlopExecutionBatches < ActiveRecord::Migration + def change + create_table :dunlop_execution_batches do |t| + t.string :state + t.string :sti_type + t.string :identifier + + t.timestamps null: false + end + end +end diff --git a/spec/dummy/db/migrate/20160705133851_add_settings_storage_to_users.rb b/spec/dummy/db/migrate/20160705133851_add_settings_storage_to_users.rb new file mode 100644 index 0000000..2e7d89a --- /dev/null +++ b/spec/dummy/db/migrate/20160705133851_add_settings_storage_to_users.rb @@ -0,0 +1,5 @@ +class AddSettingsStorageToUsers < ActiveRecord::Migration + def change + add_column :users, :settings_storage, :text + end +end diff --git a/spec/dummy/db/migrate/20160706141123_add_role_names_admin_to_users.rb b/spec/dummy/db/migrate/20160706141123_add_role_names_admin_to_users.rb new file mode 100644 index 0000000..b34a0ff --- /dev/null +++ b/spec/dummy/db/migrate/20160706141123_add_role_names_admin_to_users.rb @@ -0,0 +1,5 @@ +class AddRoleNamesAdminToUsers < ActiveRecord::Migration + def change + add_column :users, :role_names_admin, :text + end +end diff --git a/spec/dummy/db/migrate/20160713131047_create_target_files.rb b/spec/dummy/db/migrate/20160713131047_create_target_files.rb new file mode 100644 index 0000000..891fc92 --- /dev/null +++ b/spec/dummy/db/migrate/20160713131047_create_target_files.rb @@ -0,0 +1,21 @@ +class CreateTargetFiles < ActiveRecord::Migration + def change + create_table :target_files do |t| + t.string :sti_type + t.string :state + t.integer :size + t.integer :number_of_records + t.datetime :start_time + t.datetime :end_time + t.string :original_file + + t.timestamps null: false + end + + create_table :dunlop_target_file_downloads do |t| + t.integer :user_id + + t.timestamps null: false + end + end +end diff --git a/spec/dummy/db/migrate/20160713142449_add_target_file_id_to_dunlop_target_file_downloads.rb b/spec/dummy/db/migrate/20160713142449_add_target_file_id_to_dunlop_target_file_downloads.rb new file mode 100644 index 0000000..11f4ac7 --- /dev/null +++ b/spec/dummy/db/migrate/20160713142449_add_target_file_id_to_dunlop_target_file_downloads.rb @@ -0,0 +1,5 @@ +class AddTargetFileIdToDunlopTargetFileDownloads < ActiveRecord::Migration + def change + add_column :dunlop_target_file_downloads, :target_file_id, :integer + end +end diff --git a/spec/dummy/db/migrate/20160725070616_add_backtrace_to_dunlop_log_entries.rb b/spec/dummy/db/migrate/20160725070616_add_backtrace_to_dunlop_log_entries.rb new file mode 100644 index 0000000..bd4b6e1 --- /dev/null +++ b/spec/dummy/db/migrate/20160725070616_add_backtrace_to_dunlop_log_entries.rb @@ -0,0 +1,5 @@ +class AddBacktraceToDunlopLogEntries < ActiveRecord::Migration + def change + add_column :dunlop_log_entries, :backtrace, :text + end +end diff --git a/spec/dummy/db/migrate/20160725083239_remove_message_from_dunlop_log_entries.rb b/spec/dummy/db/migrate/20160725083239_remove_message_from_dunlop_log_entries.rb new file mode 100644 index 0000000..8d7a012 --- /dev/null +++ b/spec/dummy/db/migrate/20160725083239_remove_message_from_dunlop_log_entries.rb @@ -0,0 +1,5 @@ +class RemoveMessageFromDunlopLogEntries < ActiveRecord::Migration + def change + remove_column :dunlop_log_entries, :message, :string + end +end diff --git a/spec/dummy/db/migrate/20160803144659_rename_dunlop_execution_batches_to_execution_batches.rb b/spec/dummy/db/migrate/20160803144659_rename_dunlop_execution_batches_to_execution_batches.rb new file mode 100644 index 0000000..930d8e2 --- /dev/null +++ b/spec/dummy/db/migrate/20160803144659_rename_dunlop_execution_batches_to_execution_batches.rb @@ -0,0 +1,5 @@ +class RenameDunlopExecutionBatchesToExecutionBatches < ActiveRecord::Migration + def change + rename_table :dunlop_execution_batches, :execution_batches + end +end diff --git a/spec/dummy/db/migrate/20160817101540_add_current_at_day_to_source_files.rb b/spec/dummy/db/migrate/20160817101540_add_current_at_day_to_source_files.rb new file mode 100644 index 0000000..a860881 --- /dev/null +++ b/spec/dummy/db/migrate/20160817101540_add_current_at_day_to_source_files.rb @@ -0,0 +1,5 @@ +class AddCurrentAtDayToSourceFiles < ActiveRecord::Migration + def change + add_column :source_files, :current_at_day, :date + end +end diff --git a/spec/dummy/db/migrate/20160822085707_add_info_message_to_execution_batches.rb b/spec/dummy/db/migrate/20160822085707_add_info_message_to_execution_batches.rb new file mode 100644 index 0000000..16db89c --- /dev/null +++ b/spec/dummy/db/migrate/20160822085707_add_info_message_to_execution_batches.rb @@ -0,0 +1,5 @@ +class AddInfoMessageToExecutionBatches < ActiveRecord::Migration + def change + add_column :execution_batches, :info_message, :string + end +end diff --git a/spec/dummy/db/migrate/20160830063421_add_source_to_source_files.rb b/spec/dummy/db/migrate/20160830063421_add_source_to_source_files.rb new file mode 100644 index 0000000..f6cfe0c --- /dev/null +++ b/spec/dummy/db/migrate/20160830063421_add_source_to_source_files.rb @@ -0,0 +1,19 @@ +class AddSourceToSourceFiles < ActiveRecord::Migration[5.0] + def up + add_column :source_files, :creator_id, :integer + add_column :source_files, :creator_type, :string + connection.execute <<-SQL + UPDATE source_files SET creator_id=user_id, creator_type='User' WHERE user_id IS NOT NULL + SQL + remove_column :source_files, :user_id, :integer + end + + def down + add_column :source_files, :user_id, :integer + connection.execute <<-SQL + UPDATE source_files SET user_id=creator_id WHERE creator_id IS NOT NULL AND creator_type='User' + SQL + remove_column :source_files, :creator_id, :integer + remove_column :source_files, :creator_type, :string + end +end diff --git a/spec/dummy/db/migrate/20161008200847_add_token_authenticatable_columns_to_users.rb b/spec/dummy/db/migrate/20161008200847_add_token_authenticatable_columns_to_users.rb new file mode 100644 index 0000000..a17d775 --- /dev/null +++ b/spec/dummy/db/migrate/20161008200847_add_token_authenticatable_columns_to_users.rb @@ -0,0 +1,7 @@ +class AddTokenAuthenticatableColumnsToUsers < ActiveRecord::Migration + def change + add_column :users, :authentication_token, :string + add_column :users, :authentication_token_created_at, :datetime + add_index :users, :authentication_token + end +end diff --git a/spec/dummy/db/migrate/20171029063448_create_non_workflow_projects.rb b/spec/dummy/db/migrate/20171029063448_create_non_workflow_projects.rb new file mode 100644 index 0000000..e75054d --- /dev/null +++ b/spec/dummy/db/migrate/20171029063448_create_non_workflow_projects.rb @@ -0,0 +1,10 @@ +class CreateNonWorkflowProjects < ActiveRecord::Migration[5.1] + def change + create_table :non_workflow_projects do |t| + t.string :name + t.string :identifier + + t.timestamps + end + end +end diff --git a/spec/dummy/db/migrate/20171128140114_add_epoch_columns_to_non_workflow_projects.rb b/spec/dummy/db/migrate/20171128140114_add_epoch_columns_to_non_workflow_projects.rb new file mode 100644 index 0000000..1e64249 --- /dev/null +++ b/spec/dummy/db/migrate/20171128140114_add_epoch_columns_to_non_workflow_projects.rb @@ -0,0 +1,8 @@ +class AddEpochColumnsToNonWorkflowProjects < ActiveRecord::Migration[5.1] + def change + add_column :non_workflow_projects, :working_week, :integer + add_column :non_workflow_projects, :working_year, :integer + add_column :non_workflow_projects, :working_date, :date + add_column :non_workflow_projects, :epoch_week, :integer + end +end diff --git a/spec/dummy/db/schema.rb b/spec/dummy/db/schema.rb new file mode 100644 index 0000000..d21e640 --- /dev/null +++ b/spec/dummy/db/schema.rb @@ -0,0 +1,188 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# Note that this schema.rb definition is the authoritative source for your +# database schema. If you need to create the application database on another +# system, you should be using db:schema:load, not running all the migrations +# from scratch. The latter is a flawed and unsustainable approach (the more migrations +# you'll amass, the slower it'll run and the greater likelihood for issues). +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 20171128140114) do + + create_table "contractor_initializations", force: :cascade do |t| + t.integer "workflow_instance_id" + t.string "state" + t.string "sti_type" + t.text "notes" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["workflow_instance_id"], name: "index_contractor_initializations_on_workflow_instance_id" + end + + create_table "dunlop_file_downloads", force: :cascade do |t| + t.integer "user_id" + t.string "file_type" + t.integer "file_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["file_type", "file_id"], name: "index_dunlop_file_downloads_on_file_type_and_file_id" + t.index ["user_id"], name: "index_dunlop_file_downloads_on_user_id" + end + + create_table "dunlop_last_attribute_changes", force: :cascade do |t| + t.integer "target_id" + t.string "target_type" + t.datetime "changed_at" + t.string "attribute_name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "dunlop_log_entries", force: :cascade do |t| + t.integer "loggable_id" + t.string "loggable_type" + t.text "body" + t.datetime "created_at" + t.datetime "updated_at" + t.text "backtrace" + t.index ["loggable_id", "loggable_type"], name: "index_dunlop_log_entries_on_loggable_id_and_loggable_type" + end + + create_table "execution_batches", force: :cascade do |t| + t.string "state" + t.string "sti_type" + t.string "identifier" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "info_message" + end + + create_table "non_workflow_projects", force: :cascade do |t| + t.string "name" + t.string "identifier" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "working_week" + t.integer "working_year" + t.date "working_date" + t.integer "epoch_week" + end + + create_table "owner_initializations", force: :cascade do |t| + t.integer "workflow_instance_id" + t.string "state" + t.string "sti_type" + t.text "notes" + t.boolean "initialization_check_done", default: false, null: false + t.date "plan_date" + t.string "my_identifier" + t.integer "window_from" + t.integer "window_to" + t.integer "number_of_visits" + t.float "my_fraction" + t.text "my_explanation" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["workflow_instance_id"], name: "index_owner_initializations_on_workflow_instance_id" + end + + create_table "source_files", force: :cascade do |t| + t.string "sti_type" + t.string "state" + t.string "description" + t.integer "number_of_records" + t.integer "duration" + t.string "original_file" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.date "current_at_day" + t.integer "creator_id" + t.string "creator_type" + end + + create_table "source_record_my_sources", force: :cascade do |t| + t.date "my_date" + t.string "my_var" + t.integer "my_count" + t.float "my_num" + t.boolean "my_truth" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "target_files", force: :cascade do |t| + t.string "sti_type" + t.string "state" + t.integer "size" + t.integer "number_of_records" + t.datetime "start_time" + t.datetime "end_time" + t.string "original_file" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "users", force: :cascade do |t| + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false + t.string "reset_password_token" + t.datetime "reset_password_sent_at" + t.datetime "remember_created_at" + t.integer "sign_in_count", default: 0, null: false + t.datetime "current_sign_in_at" + t.datetime "last_sign_in_at" + t.string "current_sign_in_ip" + t.string "last_sign_in_ip" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "admin", default: false, null: false + t.text "role_names" + t.text "settings_storage" + t.text "role_names_admin" + t.string "authentication_token" + t.datetime "authentication_token_created_at" + t.index ["authentication_token"], name: "index_users_on_authentication_token" + t.index ["email"], name: "index_users_on_email", unique: true + t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true + end + + create_table "wba_submissions", force: :cascade do |t| + t.integer "workflow_instance_id" + t.string "state" + t.string "sti_type" + t.text "notes" + t.boolean "all_clear", default: false, null: false + t.string "new_service_group" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["workflow_instance_id"], name: "index_wba_submissions_on_workflow_instance_id" + end + + create_table "workflow_instance_batches", force: :cascade do |t| + t.string "sti_type" + t.string "state" + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.date "plan_date" + end + + create_table "workflow_instances", force: :cascade do |t| + t.string "sti_type" + t.string "state" + t.datetime "archived_at" + t.string "bucket" + t.integer "workflow_instance_batch_id" + t.date "plan_date" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "zipcode" + t.integer "housenumber" + t.string "housenumber_ext" + t.string "my_name" + end + +end diff --git a/spec/dummy/db/seeds.rb b/spec/dummy/db/seeds.rb new file mode 100644 index 0000000..b1df734 --- /dev/null +++ b/spec/dummy/db/seeds.rb @@ -0,0 +1,5 @@ +unless User.find_by(email: 'admin@example.com').present? + puts "Creating admin" + User.create!(email: "admin@example.com", password: "admin123", admin: true) +end + diff --git a/spec/dummy/lib/assets/.keep b/spec/dummy/lib/assets/.keep new file mode 100644 index 0000000..e69de29 diff --git a/spec/dummy/public/404.html b/spec/dummy/public/404.html new file mode 100644 index 0000000..b612547 --- /dev/null +++ b/spec/dummy/public/404.html @@ -0,0 +1,67 @@ + + + + The page you were looking for doesn't exist (404) + + + + + + +
+
+

The page you were looking for doesn't exist.

+

You may have mistyped the address or the page may have moved.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/spec/dummy/public/422.html b/spec/dummy/public/422.html new file mode 100644 index 0000000..a21f82b --- /dev/null +++ b/spec/dummy/public/422.html @@ -0,0 +1,67 @@ + + + + The change you wanted was rejected (422) + + + + + + +
+
+

The change you wanted was rejected.

+

Maybe you tried to change something you didn't have access to.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/spec/dummy/public/500.html b/spec/dummy/public/500.html new file mode 100644 index 0000000..061abc5 --- /dev/null +++ b/spec/dummy/public/500.html @@ -0,0 +1,66 @@ + + + + We're sorry, but something went wrong (500) + + + + + + +
+
+

We're sorry, but something went wrong.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/spec/dummy/public/favicon.ico b/spec/dummy/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/spec/dunlop_integrity_spec.rb b/spec/dunlop_integrity_spec.rb new file mode 100644 index 0000000..c4d22f3 --- /dev/null +++ b/spec/dunlop_integrity_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' + +describe Rails.application do + it_behaves_like "a dunlop application" +end + diff --git a/spec/factories/contractor_initialization.rb b/spec/factories/contractor_initialization.rb new file mode 100644 index 0000000..5d3a611 --- /dev/null +++ b/spec/factories/contractor_initialization.rb @@ -0,0 +1,4 @@ +FactoryBot.define do + factory :contractor_initialization + factory 'contractor_initialization/scenario1', aliases: [:contractor_initialization_scenario1], class: 'ContractorInitialization::Scenario1', parent: :contractor_initialization +end diff --git a/spec/factories/execution_batch.rb b/spec/factories/execution_batch.rb new file mode 100644 index 0000000..9f0332b --- /dev/null +++ b/spec/factories/execution_batch.rb @@ -0,0 +1,4 @@ +FactoryBot.define do + factory :execution_batch + factory 'execution_batch/my_batch', aliases: [:execution_batch_my_batch], class: 'ExecutionBatch::MyBatch', parent: :execution_batch +end diff --git a/spec/factories/non_workflow_project.rb b/spec/factories/non_workflow_project.rb new file mode 100644 index 0000000..f31d17e --- /dev/null +++ b/spec/factories/non_workflow_project.rb @@ -0,0 +1,3 @@ +FactoryBot.define do + factory :non_workflow_project +end diff --git a/spec/factories/owner_initialization.rb b/spec/factories/owner_initialization.rb new file mode 100644 index 0000000..c1022fa --- /dev/null +++ b/spec/factories/owner_initialization.rb @@ -0,0 +1,4 @@ +FactoryBot.define do + factory :owner_initialization + factory 'owner_initialization/scenario1', aliases: [:owner_initialization_scenario1], class: 'OwnerInitialization::Scenario1', parent: :owner_initialization +end diff --git a/spec/factories/source_files.rb b/spec/factories/source_files.rb new file mode 100644 index 0000000..ea0284d --- /dev/null +++ b/spec/factories/source_files.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :source_file + factory 'source_file/my_source', aliases: [:source_file_my_source], class: 'SourceFile::MySource', parent: :source_file do + original_file { File.open Dunlop::Engine.root.join('spec/fixtures/source_files/my_source_1.csv') } + end +end diff --git a/spec/factories/target_files.rb b/spec/factories/target_files.rb new file mode 100644 index 0000000..365535c --- /dev/null +++ b/spec/factories/target_files.rb @@ -0,0 +1,11 @@ +FactoryBot.define do + factory :target_file do + original_file { Tempfile.new("target_file_factory") } + trait :failed do + after(:build) { |record| record.log_entries.build(body: 'ERROR - some reason to fail') } + state 'failed' + end + end + + factory 'target_file/my_target', aliases: [:target_file_my_target], class: 'TargetFile::MyTarget', parent: :target_file +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb new file mode 100644 index 0000000..71114d9 --- /dev/null +++ b/spec/factories/users.rb @@ -0,0 +1,19 @@ +FactoryBot.define do + + sequence :email do |n| + "user#{n}@example.com" + end + + factory :user do + email + password 'password' + role_names %w[read-class-WorkflowInstance] + end + + factory :admin, class: User do + email 'admin@example.com' + password 'password' + admin true + end +end + diff --git a/spec/factories/wba_submission.rb b/spec/factories/wba_submission.rb new file mode 100644 index 0000000..d825088 --- /dev/null +++ b/spec/factories/wba_submission.rb @@ -0,0 +1,4 @@ +FactoryBot.define do + factory :wba_submission + factory 'wba_submission/scenario2', aliases: [:wba_submission_scenario2], class: 'WbaSubmission::Scenario2', parent: :wba_submission +end diff --git a/spec/factories/workflow_instance.rb b/spec/factories/workflow_instance.rb new file mode 100644 index 0000000..b31aa67 --- /dev/null +++ b/spec/factories/workflow_instance.rb @@ -0,0 +1,57 @@ +FactoryBot.define do + factory :workflow_instance do + after(:build){|instance| instance.setup_workflow_steps } + state "unplanned" # default for Vula + + trait :uncategorized do + state 'uncategorized' + end + + trait :unplanned do + state 'unplanned' + end + + trait :planned do + state 'planned' + end + + trait :active do + state 'active' + after :build do |instance| + instance.workflow_steps.first.state = 'processing' + end + end + + trait :overdue do + state 'overdue' + after :build do |instance| + instance.owner_initialization.plan_date = 1.year.ago + instance.contractor_initialization.state = 'overdue' + end + end + + trait :all_completed do + state 'all_completed' + after :build do |instance| + instance.workflow_steps.each{|workflow_step| workflow_step.state = 'completed' } + end + end + + trait :any_rejected do + state 'any_rejected' + after :build do |instance| + instance.workflow_steps.first.state = 'rejected' + end + end + + trait :with_values do + + end + end + + factory :workflow_instance_for_workflow_step_test, class: 'WorkflowInstance::Scenario1', parent: :workflow_instance_scenario1 + + factory 'workflow_instance/scenario1', aliases: [:workflow_instance_scenario1], class: 'WorkflowInstance::Scenario1', parent: :workflow_instance + factory 'workflow_instance/scenario2', aliases: [:workflow_instance_scenario2], class: 'WorkflowInstance::Scenario2', parent: :workflow_instance +end + diff --git a/spec/factories/workflow_instance_batch.rb b/spec/factories/workflow_instance_batch.rb new file mode 100644 index 0000000..ce3752f --- /dev/null +++ b/spec/factories/workflow_instance_batch.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :workflow_instance_batch + factory 'workflow_instance_batch/scenario1', aliases: [:workflow_instance_batch_scenario1], class: 'WorkflowInstanceBatch::Scenario1', parent: :workflow_instance_batch + factory 'workflow_instance_batch/scenario2', aliases: [:workflow_instance_batch_scenario2], class: 'WorkflowInstanceBatch::Scenario2', parent: :workflow_instance_batch +end + diff --git a/spec/features/badge_info_spec.rb b/spec/features/badge_info_spec.rb new file mode 100644 index 0000000..ed51602 --- /dev/null +++ b/spec/features/badge_info_spec.rb @@ -0,0 +1,95 @@ +require 'rails_helper' + +feature "Show badge info" do + describe "Not linked to service provider" do + background do + i_am_logged_in_as_admin + @workflow_instance_batch = create :workflow_instance_batch_scenario1, name: 'Batchy' + @dslam_1 = create :workflow_instance_scenario1, :active, my_name: 'DSLA/111' + @dslam_2 = create :workflow_instance_scenario1, :active, plan_date: '2019-03-09', my_name: 'DSLA/222', workflow_instance_batch: @workflow_instance_batch + @dslam_3 = create :workflow_instance_scenario1, my_name: 'DSLA/333', workflow_instance_batch: @workflow_instance_batch + @dslam_4 = create :workflow_instance_scenario1, my_name: 'DSLA/444' + @owner_initialization = @dslam_3.owner_initialization + @owner_initialization.replace_notes 'PreparationNotes' + @owner_initialization.is_overdue! + end + + describe 'Statistics' do + it "Shows the dslams for a workflow_instance state", js: true do + visit root_path + badge = find('.workflow_instance.scenario1.active.display-badge') + expect( badge.text ).to eq 'Actief (2)' + badge.click + within '#display-badge-info-popup' do + expect( page ).to have_content 'DSLA/111' + expect( page ).to have_content 'DSLA/222' + expect( page ).not_to have_content 'DSLA/333' + expect( page ).not_to have_content 'DSLA/444' + + click_on "Toon lijst" + end + page.current_url.should include "/workflow_instance/scenario1s?q%5Bstate_eq%5D=active" + expect( page ).to have_content 'DSLA/111' + expect( page ).to have_content 'DSLA/222' + expect( page ).not_to have_content 'DSLA/333' + expect( page ).not_to have_content 'DSLA/444' + end + + it "Show the dslams for a subprocess state", js: true do + visit root_path + badge = find('.owner_initialization.scenario1.overdue.display-badge') + expect( badge.text ).to eq 'Overdue (1)' + badge.click + within '#display-badge-info-popup' do + page.should_not have_content 'DSLA/111' + page.should_not have_content 'DSLA/222' + page.should have_content 'DSLA/333' + page.should_not have_content 'DSLA/444' + end + end + end + + describe "Actual record" do + + it "Shows basic information for the workflow_instance record", js: true, disabled: true do # disabled because plan_date out of scope + visit workflow_instance_scenario1s_path + badge = find(%|[data-record='{"id":#{@dslam_2.id}}'] .workflow_instance.scenario1.display-badge|) + badge.click + within '#display-badge-info-popup' do + expect( page ).to have_content '09-03-2019' + end + end + + it "Shows basic information for the subprocess record", js: true do + visit workflow_instance_scenario1s_path + badge = find(%|[data-record='{"id":#{@dslam_3.id}}'] .owner_initialization.scenario1.display-badge|) + badge.click + within '#display-badge-info-popup' do + expect( page ).to have_content 'PreparationNotes' + end + end + end + + describe "Batch scope" do + it "Shows batch limited informatio", js: true do + # Create bogus not batch records, if batch is not activated on + # query, two records are found in stead of 1 + dslam_4 = create :workflow_instance_scenario1, :active, my_name: 'DSLA/444' + dslam_4.owner_initialization.update state: 'processing' + + visit workflow_instance_batch_scenario1s_path + badge = find(%|.contractor_initialization.scenario1.pending.display-badge|) + expect( badge.text ).to eq 'Contr. init (2)' + badge.click + within '#display-badge-info-popup' do + expect( page ).to have_content 'DSLA/222' + expect( page ).not_to have_content 'DSLA/444' + end + #TODO: workflow step filtering + #click_on "Toon lijst" + #page.current_url.should include "/workflow_instance/scenario1s?q%5Bworkflow_instance_batch_id_eq%5D=#{@workflow_instance_batch.id}&q%5Bworkflow_step%5D%5Bowner_initialization%5D=pending" + end + end + end + +end diff --git a/spec/features/dashboard/changelog_spec.rb b/spec/features/dashboard/changelog_spec.rb new file mode 100644 index 0000000..6df1cfa --- /dev/null +++ b/spec/features/dashboard/changelog_spec.rb @@ -0,0 +1,11 @@ +require 'rails_helper' + +RSpec.feature 'Changelog' do + background { i_am_logged_in_as_admin } + scenario "underscore integrity" do + visit '/dunlop/changelog' + page.should have_content "These un_der_scores are not replaced by italic" + page.should have_content "Already escaped_under_scores are not double escaped" + end +end + diff --git a/spec/features/dashboard/home_spec.rb b/spec/features/dashboard/home_spec.rb new file mode 100644 index 0000000..0429cda --- /dev/null +++ b/spec/features/dashboard/home_spec.rb @@ -0,0 +1,16 @@ +require 'rails_helper' + +RSpec.feature 'Home page' do + background { i_am_logged_in_as_admin } + scenario "there are no javascript error messages", js: true do + visit '/' + case page.driver.class.name.underscore + when /webkit/ + expect( page.driver.error_messages ).to be_empty + when /poltergeist/ + # nothing, js_errors: true will raise ruby error + end + page.should have_content Rails.application.config.application_name + end +end + diff --git a/spec/features/execution_batches/show_spec.rb b/spec/features/execution_batches/show_spec.rb new file mode 100644 index 0000000..0598cb7 --- /dev/null +++ b/spec/features/execution_batches/show_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +feature "Show execution batches" do + background { i_am_logged_in_as_admin } + + scenario "From the index to the show" do + batch = create 'execution_batch/my_batch', info_message: 'I did stuff' + visit "/dunlop/execution_batches" + page.should have_content "I did stuff" + click_on "Uitvoerings batch" + page.status_code.should eq 200 + page.current_path.should eq "/dunlop/execution_batches/#{batch.id}" + page.should have_content "I did stuff" + end +end diff --git a/spec/features/helpers/application_helper_spec.rb b/spec/features/helpers/application_helper_spec.rb new file mode 100644 index 0000000..11a3306 --- /dev/null +++ b/spec/features/helpers/application_helper_spec.rb @@ -0,0 +1,13 @@ +require 'rails_helper' + +feature "Application helpers with feature spec" do + background { i_am_logged_in_as_admin } + + scenario '#render_as_dunlop_modal', js: true do + visit "/dashboard/render_as_dunlop_modal_test" + + within "#workflow-step-modal" do + page.should have_content "Special partial for testing, passed value is 78" + end + end +end diff --git a/spec/features/reports/index_page_spec.rb b/spec/features/reports/index_page_spec.rb new file mode 100644 index 0000000..d989c71 --- /dev/null +++ b/spec/features/reports/index_page_spec.rb @@ -0,0 +1,12 @@ +require 'rails_helper' + +feature "Workflow steps in batch finished report" do + background { i_am_logged_in_as_admin } + + scenario "rendering the page" do + visit "/dunlop/reports" + page.should have_content "Workflow instance fields" + page.should have_content "Report listing actions performed when all workflow steps in a batch are in a final state" + page.should have_content "Users permissions overview" + end +end diff --git a/spec/features/reports/system/workflow_steps_in_batch_finished_spec.rb b/spec/features/reports/system/workflow_steps_in_batch_finished_spec.rb new file mode 100644 index 0000000..e6c0751 --- /dev/null +++ b/spec/features/reports/system/workflow_steps_in_batch_finished_spec.rb @@ -0,0 +1,10 @@ +require 'rails_helper' + +feature "Workflow steps in batch finished report" do + background { i_am_logged_in_as_admin } + + scenario "rendering the page" do + visit "/dunlop/reports/system/workflow_steps_in_batch_finished" + page.should have_content "Report listing actions performed when all workflow steps in a batch are in a final state" + end +end diff --git a/spec/features/reports/system/workflow_steps_overdue_spec.rb b/spec/features/reports/system/workflow_steps_overdue_spec.rb new file mode 100644 index 0000000..14c116d --- /dev/null +++ b/spec/features/reports/system/workflow_steps_overdue_spec.rb @@ -0,0 +1,10 @@ +require 'rails_helper' + +feature "Workflow steps overdue report" do + background { i_am_logged_in_as_admin } + + scenario "rendering the page" do + visit "/dunlop/reports/system/workflow_steps_overdue" + page.should have_content "Contr. initialisatie is overdue als hij 2 dagen na Core initialisatie → Dattum nog niet afgerond is" + end +end diff --git a/spec/features/source_files/view_life_cicle_spec.rb b/spec/features/source_files/view_life_cicle_spec.rb new file mode 100644 index 0000000..3dc19d2 --- /dev/null +++ b/spec/features/source_files/view_life_cicle_spec.rb @@ -0,0 +1,36 @@ +require 'rails_helper' + +RSpec.feature 'Source files index page' do + background { i_am_logged_in_as_admin } + scenario "it renders with all kinds of source file without problems, also a good check wether all layout files have app prefix in navigation links" do + user = create :user, email: 'source-user8731@example.com' + batch = create 'execution_batch/my_batch' + source_file_1 = create 'source_file/my_source', creator: nil + source_file_2 = create 'source_file/my_source', creator: user, original_file: fixture_file("source_files/my_source_1.csv") + source_file_3 = create 'source_file/my_source', creator: batch + visit '/dunlop/source_files' + page.should have_content "Bronbestanden overzicht" + page.body.should include "source-user8731@example.com" + page.body.should_not include "source-user8731@example.com" # users have no show page + page.body.should include "Uitvoerings batch" # link to batch + + find(".table-link.show[href='/dunlop/source_files/#{source_file_2.id}']").click + + current_path.should eq "/dunlop/source_files/#{source_file_2.id}" + page.should have_content "Download" + + find(".title-back-link").click + + current_path.should eq "/dunlop/source_files" + + # download source file 2 + find(".table-link.download[href='/dunlop/source_files/#{source_file_2.id}/download']").click + + page.body.should eq <<-CSV.strip_heredoc + my_date,my_var,My count,my_num,my_truth + 2015-05-09,abc D,19,4.8,true + 2015-11-09,def G,19,7.8,false + CSV + end +end + diff --git a/spec/features/target_files/download_latest_spec.rb b/spec/features/target_files/download_latest_spec.rb new file mode 100644 index 0000000..1b0d2e4 --- /dev/null +++ b/spec/features/target_files/download_latest_spec.rb @@ -0,0 +1,30 @@ +require 'rails_helper' + +feature 'Target files index page' do + background { i_am_logged_in_as_admin } + + scenario "download_latest when completed with specific type" do + allow_any_instance_of(TargetFile::MyTarget).to receive(:gzip_file?).and_return false # no gzip for this test + target_file = create 'target_file/my_target' + target_file.execute_generation_process + target_file.should be_completed + + visit "/dunlop/target_files/download_latest/my_target" + + page.response_headers['Content-Disposition'].should match /attachment; filename="my_target-/ + page.body.should eq target_file.original_file.read + end + + scenario "download_latest when failed with specific type should not return" do + allow_any_instance_of(TargetFile::MyTarget).to receive(:gzip_file?).and_return false # no gzip for this test + target_file = create 'target_file/my_target', :failed + target_file.execute_generation_process + target_file.should be_failed + + visit "/dunlop/target_files/download_latest/my_target" + + page.body.should be_empty + page.status_code.should be 404 + end +end + diff --git a/spec/features/target_files/index_spec.rb b/spec/features/target_files/index_spec.rb new file mode 100644 index 0000000..5349cfe --- /dev/null +++ b/spec/features/target_files/index_spec.rb @@ -0,0 +1,12 @@ +require 'rails_helper' + +feature 'Target files index page' do + background { i_am_logged_in_as_admin } + + scenario "it renders with all kinds of target file without problems, also a good check wether all layout files have app prefix in navigation links" do + TargetFile.classes.each(&:create!) + visit '/dunlop/target_files' + page.should have_content "Target files overzicht" + end +end + diff --git a/spec/features/target_files/show_spec.rb b/spec/features/target_files/show_spec.rb new file mode 100644 index 0000000..493ecac --- /dev/null +++ b/spec/features/target_files/show_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' + +feature 'Target files show page' do + background { i_am_logged_in_as_admin } + + scenario "download button when completed" do + target_file = create 'target_file/my_target' + target_file.execute_generation_process + target_file.should be_completed + + visit "/dunlop/target_files/#{target_file.id}" + + page.should have_content "Download" + end + + scenario "no download button when not completed" do + target_file = create 'target_file/my_target' + target_file.should be_new + + visit "/dunlop/target_files/#{target_file.id}" + + page.should_not have_content "Download" + end +end + diff --git a/spec/features/users/manage_user_spec.rb b/spec/features/users/manage_user_spec.rb new file mode 100644 index 0000000..0e7a4df --- /dev/null +++ b/spec/features/users/manage_user_spec.rb @@ -0,0 +1,66 @@ +require 'rails_helper' + +RSpec.feature "Admin manages user" do + let(:user_options){ Hash.new } + let!(:admin) { create :admin, user_options.merge(email: 'admin@example.com') } + #let(:user) { create :user, user_options.merge(email: 'user@example.com') } + background { i_am_logged_in_as user } + + context "With manage user rights" do + let(:user) { create(:user, email: 'user@example.com', role_names_admin: ['manage-class-User']) } + + scenario 'list users' do + user and click_on "Gebruikers" + expect(page).to have_content('admin@example.com') + expect(page).to have_content('user@example.com') + end + + scenario "create new user" do + click_on "Gebruikers" + click_on "Gebruiker toevoegen" + fill_in 'Email', with: "new_user@example.com" + fill_in 'Password', with: "password123" + + #find('#user-role-migration-read').set true + #find('#user-role-owner-read').set true + #find('#user-role-service_provider-read').set true + #find('#user-role-contractor-manage').set true + #find('#user-role-report-timewax').set true + #check 'Role1' + #check 'Role2' + click_on "Gebruiker toevoegen" + + expect(current_path).to eq dunlop.users_path + + user = User.find_by(email: 'new_user@example.com') + expect( admin ).to_not eq user + + expect( user.valid_password?('password123')) .to be true + expect( user.role_names.map(&:presence).compact ).to match_array %w[read-class-WorkflowInstance] + #expect(user.roles).to eq [Role::Role1, Role::Role2, ''] + end + + scenario "edit user" do + other_user = create :user, email: 'other-user@example.com' + click_on "Gebruikers" + + within "#user-#{other_user.id}" do + click_on_selector ".table-link.edit" + end + # Clear all permissions (empty value ...) + #find('#user-role-migration-read').set false + #click_on "Update" + click_on "Gebruiker bijwerken" + + expect(current_path).to eq "/dunlop/users" + expect(other_user.reload.role_names).to eq ["", "read-class-WorkflowInstance"] + end + + scenario "show permissions table" do + visit "/dunlop/users/permissions_table" + page.should have_selector '.role-icon.read' + end + end +end + + diff --git a/spec/features/workflow_instance/archive_spec.rb b/spec/features/workflow_instance/archive_spec.rb new file mode 100644 index 0000000..4c9cecb --- /dev/null +++ b/spec/features/workflow_instance/archive_spec.rb @@ -0,0 +1,55 @@ +require "rails_helper" + +feature "Archive and revive workflow instances", js: true do + let(:user_params) { {} } + let(:user) { create :user, user_params } + background { i_am_logged_in_as user } + + context "with permissions" do + let(:user_params) { {role_names: ["read-class-WorkflowInstance", "archive-class-WorkflowInstance"] } } + + scenario "Archive workflow instances" do + create 'workflow_instance/scenario1', bucket: 'B1' + create 'workflow_instance/scenario1', bucket: 'B2' + visit "/workflow_instance/scenario1s" + select_all_workflow_instances + js_click ".collection-edit.workflow_instance-actions" + page.should_not have_selector ".workflow-instance-revive-button" + js_click ".workflow-instance-archive-button" + + #page.current_path.should eq "/workflow_instance/scenario1s" + page.should have_selector "body.archived-mode" + page.current_path.should eq "/" + + WorkflowInstance.archived.count.should eq 2 + WorkflowInstance.scope_for(user).count.should eq 0 + + visit "/workflow_instance/scenario1s" + page.should have_content "B1" + page.should have_content "B2" + select_all_workflow_instances + js_click ".collection-edit.workflow_instance-actions" + page.should_not have_selector ".workflow-instance-archive-button" + js_click ".workflow-instance-revive-button" + + page.current_path.should eq "/workflow_instance/scenario1s" + page.should_not have_selector "body.archived-mode" + + WorkflowInstance.archived.count.should eq 0 + WorkflowInstance.scope_for(user).count.should eq 2 + end + end + + context "without permissions" do + let(:user_params) { {role_names: ["read-class-WorkflowInstance", "update-class-WorkflowInstance"] } } + + scenario "business as usual, but should not have archive action" do + create 'workflow_instance/scenario1', bucket: 'B1' + create 'workflow_instance/scenario1', bucket: 'B2' + visit "/workflow_instance/scenario1s" + select_all_workflow_instances + js_click ".collection-edit.workflow_instance-actions" + page.should_not have_selector ".workflow-instance-archive-button" + end + end +end diff --git a/spec/features/workflow_instance/categorize_as_spec.rb b/spec/features/workflow_instance/categorize_as_spec.rb new file mode 100644 index 0000000..7d5a5a5 --- /dev/null +++ b/spec/features/workflow_instance/categorize_as_spec.rb @@ -0,0 +1,50 @@ +require 'rails_helper' + +feature "Categorize workflow instances as other scenario", js: true do + let(:user_params) { {} } + let(:user) { create :user, user_params } + background { i_am_logged_in_as user } + context "with permissions" do + let(:user_params) { {role_names: ["read-class-WorkflowInstance", "categorize-class-WorkflowInstance"] } } + + scenario "nothing selected" do + create 'workflow_instance/scenario1', bucket: 'B1' + create 'workflow_instance/scenario1', bucket: 'B2' + visit "/workflow_instance/scenario1s" + js_click "#categorize-as-button" + page.accept_alert "Nothing selected" # will fail if no modal is present + end + + scenario "from scenario1 to 2" do + create 'workflow_instance/scenario1', bucket: 'B1' + create 'workflow_instance/scenario1', bucket: 'B2' + visit "/workflow_instance/scenario1s" + select_all_workflow_instances + js_click "#categorize-as-button" + accept_confirm "Weet je dit zeker? Stappen die niet aanwezig zijn in SC2 zullen verdwijnen." do + sleep 0.3 + WorkflowInstance.pluck(:sti_type).uniq.should eq ["WorkflowInstance::Scenario2"] + page.current_path.should eq "/workflow_instance/scenario2s/selection" + page.should have_content "B1" + page.should have_content "B2" + end + end + end + + context "without permissions" do + let(:user_params) { {role_names: ["read-class-WorkflowInstance"] } } + scenario "there is no button" do + create 'workflow_instance/scenario1', bucket: 'B1' + create 'workflow_instance/scenario1', bucket: 'B2' + page.should_not have_selector "#categorize-as-button" + end + + scenario "fabricate hack request", js: false do + wi = create 'workflow_instance/scenario1', bucket: 'B1' + page.driver.post "/dunlop/workflow_instances/categorize_as?scenario=scenario2&ids=#{wi.id}" + page.status_code.should eq 403 + + WorkflowInstance.pluck(:sti_type).should eq ["WorkflowInstance::Scenario1"] + end + end +end diff --git a/spec/features/workflow_instance/reset_spec.rb b/spec/features/workflow_instance/reset_spec.rb new file mode 100644 index 0000000..077dea9 --- /dev/null +++ b/spec/features/workflow_instance/reset_spec.rb @@ -0,0 +1,41 @@ +require "rails_helper" + +feature "Reset workflow instances", js: true do + let(:user_params) { {} } + let(:user) { create :user, user_params } + background { i_am_logged_in_as user } + + context "with permissions" do + let(:user_params) { {role_names: ["read-class-WorkflowInstance", "reset-class-WorkflowInstance"] } } + + scenario "Reset workflow instances" do + wi = create 'workflow_instance/scenario1', :active, bucket: 'B1' + wi.workflow_steps.map(&:state).uniq.should match_array ['processing', 'pending'] + visit "/workflow_instance/scenario1s" + select_all_workflow_instances + js_click ".collection-edit.workflow_instance-actions" + js_click ".workflow-instance-reset-button" + + #page.current_path.should eq "/workflow_instance/scenario1s" + page.current_path.should eq "/workflow_instance/scenario1s" # redirect back + page.should_not have_content "B1" + + wi_reset = WorkflowInstance.find(wi.id) + wi_reset.sti_type.should eq "WorkflowInstance::Scenario1" # no scenario reset here + wi_reset.workflow_steps.map(&:state).uniq.should eq ['pending'] + end + end + + context "without permissions" do + let(:user_params) { {role_names: ["read-class-WorkflowInstance", "update-class-WorkflowInstance"] } } + + scenario "business as usual, but should not have reset action" do + create 'workflow_instance/scenario1', bucket: 'B1' + create 'workflow_instance/scenario1', bucket: 'B2' + visit "/workflow_instance/scenario1s" + select_all_workflow_instances + js_click ".collection-edit.workflow_instance-actions" + page.should_not have_selector ".workflow-instance-reset-button" + end + end +end diff --git a/spec/features/workflow_steps/contractor_initialization_spec.rb b/spec/features/workflow_steps/contractor_initialization_spec.rb new file mode 100644 index 0000000..1f19802 --- /dev/null +++ b/spec/features/workflow_steps/contractor_initialization_spec.rb @@ -0,0 +1,12 @@ +require 'rails_helper' + +RSpec.feature "ContractorInitialization for Scenario1" do + it_behaves_like "a workflow step", :contractor_initialization do + let(:workflow_instance_factory) { :workflow_instance_scenario1 } + let :attributes do + { + } + end + end +end + diff --git a/spec/features/workflow_steps/owner_initialization_spec.rb b/spec/features/workflow_steps/owner_initialization_spec.rb new file mode 100644 index 0000000..a6bd86d --- /dev/null +++ b/spec/features/workflow_steps/owner_initialization_spec.rb @@ -0,0 +1,20 @@ +require 'rails_helper' + +RSpec.feature "OwnerInitialization for Scenario1" do + it_behaves_like "a workflow step", :owner_initialization do + let(:workflow_instance_factory) { :workflow_instance_scenario1 } + let :attributes do + { + initialization_check_done: { type: :boolean }, + plan_date: { type: :date }, + my_identifier: { type: :string }, + window_from: { type: :window_from}, + window_to: { type: :window_to}, + number_of_visits: { type: :integer }, + my_fraction: { type: :float }, + my_explanation: { type: :string }, + } + end + end +end + diff --git a/spec/features/workflow_steps/wba_submission_spec.rb b/spec/features/workflow_steps/wba_submission_spec.rb new file mode 100644 index 0000000..749b1b6 --- /dev/null +++ b/spec/features/workflow_steps/wba_submission_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' + +RSpec.feature "WbaSubmission for Scenario2" do + it_behaves_like "a workflow step", :wba_submission do + let(:workflow_instance_factory) { :workflow_instance_scenario2 } + let :attributes do + { + all_clear: { type: :boolean }, + new_service_group: { type: :string}, + } + end + end +end + diff --git a/spec/fixtures/file_zip/sip24_kvd_1.gz b/spec/fixtures/file_zip/sip24_kvd_1.gz new file mode 100644 index 0000000..1b06735 Binary files /dev/null and b/spec/fixtures/file_zip/sip24_kvd_1.gz differ diff --git a/spec/fixtures/file_zip/sip24_kvd_1.txt b/spec/fixtures/file_zip/sip24_kvd_1.txt new file mode 100644 index 0000000..8a80c13 --- /dev/null +++ b/spec/fixtures/file_zip/sip24_kvd_1.txt @@ -0,0 +1,12 @@ +kvd;primair;lengte;lengte_primair;adressen;totaal +1000_;;;;;2 +1000_HVD;1000_HVD;;;0;0 +1000_AA;1000_AA;1000;;2;2 +1000_BB;1000_AA;;1000;1;1 +1000_CC;1000_AA;;1000;1;1 +2000_;;;;;1 +2000_HVD;2000_HVD;;;0;0 +2000_AA;2000_AA;1000;;1;1 +3000_;;;;;1 +3000_HVD;3000_HVD;;;1;1 +172500_A;172500_A;1000;;3;3 diff --git a/spec/fixtures/file_zip/sip24_kvd_1.zip b/spec/fixtures/file_zip/sip24_kvd_1.zip new file mode 100644 index 0000000..aef633a Binary files /dev/null and b/spec/fixtures/file_zip/sip24_kvd_1.zip differ diff --git a/spec/fixtures/source_files/my_source_1.csv b/spec/fixtures/source_files/my_source_1.csv new file mode 100644 index 0000000..940d3bb --- /dev/null +++ b/spec/fixtures/source_files/my_source_1.csv @@ -0,0 +1,3 @@ +my_date,my_var,My count,my_num,my_truth +2015-05-09,abc D,19,4.8,true +2015-11-09,def G,19,7.8,false diff --git a/spec/helpers/dunlop/active_class_helper_spec.rb b/spec/helpers/dunlop/active_class_helper_spec.rb new file mode 100644 index 0000000..0349048 --- /dev/null +++ b/spec/helpers/dunlop/active_class_helper_spec.rb @@ -0,0 +1,101 @@ +require 'rails_helper' + +RSpec.describe Dunlop::ActiveClassHelper, type: :helper do + subject { helper } + + describe '#active_class' do + describe 'normal routes' do + before do + allow( subject ).to receive(:controller_path).and_return 'admin/users' + allow( subject ).to receive(:action_name).and_return 'index' + end + + it 'returns active on match with full spec' do + expect( subject.active_class 'admin/users#index' ).to eq 'active' + end + + it 'returns active on match with controller only spec' do + expect( subject.active_class 'admin/users' ).to eq 'active' + end + + it 'returns active when only the first part of the controller is requested' do + expect( subject.active_class 'admin' ).to eq 'active' + end + + it 'returns active on match with action only spec' do + expect( subject.active_class '#index' ).to eq 'active' + end + + it 'returns nil on match with incorrect full spec' do + expect( subject.active_class 'admin/users#show' ).to be nil + end + + it 'returns active on match with incorrect controller only spec' do + expect( subject.active_class 'pages' ).to be nil + end + + it 'returns active on match with incorrect action only spec' do + expect( subject.active_class '#show' ).to be nil + end + + it 'returns nil for a different controller with the same action with full spec' do + expect( subject.active_class 'pages#index' ).to be nil + end + end + + describe "regular expression arguments" do + it "works with regular expressions" do + allow( subject ).to receive(:controller_path).and_return 'admin/users' + allow( subject ).to receive(:action_name).and_return 'index' + subject.active_class(/#index/).should eq 'active' + subject.active_class(/admin\/users#index/).should eq 'active' + subject.active_class(/^admin.*#index$/).should eq 'active' + subject.active_class(/#other|#index/).should eq 'active' + + subject.active_class(/#other|#yet_another/).should be nil + subject.active_class(/admin\/users#show/).should be nil + end + end + + describe "except option" do + before do + allow( subject ).to receive(:controller_path).and_return 'admin/users' + allow( subject ).to receive(:action_name).and_return 'index' + end + + it "works with single statement" do + subject.active_class('admin/users', except: '#index').should be nil + subject.active_class('admin/users', except: '#show').should eq 'active' + end + + it "works with regexp" do + subject.active_class('admin/users', except: /#index$/).should be nil + subject.active_class('admin/users', except: /#show$/).should eq 'active' + subject.active_class('admin/users', except: /#show|#index/).should be nil + end + + it "works with array spec" do + subject.active_class('admin/users', except: ['admin']).should be nil + subject.active_class('admin/users', except: ['#index']).should be nil + subject.active_class('admin/users', except: ['bogus', /users/]).should be nil # users matches + end + end + + describe 'HighVoltage' do + before do + allow( subject ).to receive(:controller_path).and_return 'high_voltage/pages' + allow( subject ).to receive(:action_name).and_return 'show' + end + + it "works with high_voltage when there is a match" do + expect( subject ).to receive(:params).and_return(id: 'changelog') + expect( subject.active_class 'pages#changelog' ).to eq 'active' + end + + it "works with high_voltage when there is no match" do + expect( subject ).to receive(:params).and_return(id: 'about') + expect( subject.active_class 'pages#changelog' ).to be nil + end + end + end +end diff --git a/spec/helpers/dunlop/application_helper_spec.rb b/spec/helpers/dunlop/application_helper_spec.rb new file mode 100644 index 0000000..93c7a08 --- /dev/null +++ b/spec/helpers/dunlop/application_helper_spec.rb @@ -0,0 +1,209 @@ +require 'rails_helper' + +RSpec.describe Dunlop::ApplicationHelper, type: :helper do + subject { helper } + + describe '#hours_for_select' do + it 'returns a proper array' do + subject.hours_for_select.first.should be_a DayTimeMinutes + subject.hours_for_select.map(&:to_s).should eq %w[ + 00:00 + 01:00 + 02:00 + 03:00 + 04:00 + 05:00 + 06:00 + 07:00 + 08:00 + 09:00 + 10:00 + 11:00 + 12:00 + 13:00 + 14:00 + 15:00 + 16:00 + 17:00 + 18:00 + 19:00 + 20:00 + 21:00 + 22:00 + 23:00 + ] + end + end + + + describe '#plan_date', disabled: true do + let(:window_prefix) { '   ' } + it 'is blank when none is set' do + expect( subject.plan_date ).to be_blank + end + + it "is shows when window_from is blank" do + expect( subject.plan_date window_to: 6 ).to eq "#{window_prefix}- 6:00" + end + + it "shows when window_to is blank" do + expect( subject.plan_date window_from: 4).to eq "#{window_prefix}4:00 -" + end + + it 'returns a proper display when window is set' do + expect( subject.plan_date window_from: 4, window_to: 11 ).to eq "#{window_prefix}4:00 - 11:00" + expect( subject.plan_date window_from: 4, window_to: 11 ).to be_html_safe + end + + it "Prepends the date if given" do + expect( subject.plan_date date: Date.current, window_from: 4, window_to: 11 ).to eq "#{Date.current.strftime('%d-%m-%Y')} #{window_prefix}4:00 - 11:00" + end + end + + describe '#plan_date_for_record', disabled: true do + it "Calls the plan_date helper with correct paramenters" do + expect( subject ).to receive(:plan_date).with(date: Date.current, window_from: 8, window_to: 10) + subject.plan_date_for_record build(:owner_initialization_scenario1, plan_date: Date.current, window_from: 8, window_to: 10) + end + end + + describe '#boolean_show' do + it 'returns a span including yes as a class if value is present' do + expect( subject.boolean_show "a" ).to eq '' + end + it 'returns a span including no as a class if value is not present' do + expect( subject.boolean_show "" ).to eq '' + end + end + + describe '#search_result_info' do + it "displays the found results" do + 5.times { create :workflow_instance } + expect( subject.search_result_info WorkflowInstance.all.page(2).per(2) ).to eq "3 - 4 / #{WorkflowInstance.count}" + end + end + + describe '#page_title' do + it 'works with a custom string' do + expect( subject.page_title "Custom title" ).to eq '

Custom title

' + end + end + + describe '#plan_date_for_record' do + it 'display all info' do + record = double plan_date: '2015-12-4'.to_date, window_from: 2, window_to: 5 + expect( subject.plan_date_for_record record ).to eq '04-12-2015    2:00 - 5:00' + end + it "displays without window_to" do + record = double plan_date: '2015-12-4'.to_date, window_from: 2, window_to: nil + expect( subject.plan_date_for_record record ).to eq '04-12-2015    2:00 -' + end + + it "does not crash on blank record" do + expect( subject.plan_date_for_record nil ).to eq '' + end + end + + describe '#show_address' do + it 'gives a proper address' do + record = build :workflow_instance_scenario1, zipcode: '2554', housenumber: 22, housenumber_ext: 'E' + expect( subject.show_address record ).to eq "2554 22E" + end + end + + describe "#human_duration" do + it "uses translations" do + subject.human_duration(1.day + 1.day).should eq "2 dagen" + subject.human_duration(3.days + 2.months).should eq "2 maanden en 3 dagen" + subject.human_duration(1.day + 1.month + 3.years).should eq "3 jaar, 1 maand en 1 dag" + subject.human_duration(-2.months).should eq "-2 maanden" + end + end + + describe "#tarray" do + it "works with normal arrays" do + subject.tarray(%w[ a b c]).should eq [['a', 'a'], ['b', 'b'], ['c', 'c']] + end + + it "works with models" do + expect(subject).to receive(:can?).with(:read, WorkflowInstance::Scenario1).and_return true + expect(subject).to receive(:can?).with(:read, OwnerInitialization::Scenario1).and_return false + + subject.tarray([WorkflowInstance::Scenario1, OwnerInitialization::Scenario1]).should eq [ + ["SC1", WorkflowInstance::Scenario1] + ] + end + end + + describe "#link_to_model" do + it "handles nil parameter" do + subject.link_to_model(nil).should_not be_present + end + + it "handles models without a defined url and presentation name" do + model = Dunlop::LogEntry.create + expect { subject.polymorphic_path [dunlop, model] }.to raise_error NoMethodError + expect { subject.polymorphic_path [main_app, model] }.to raise_error NoMethodError + expect(subject).to receive(:can?).with(:read, model).and_return true + subject.link_to_model(model).should_not be_present + end + + it "handles models without a defined url but with presentation name" do + model = create :user, email: "user-link-to-model234@example.com" + expect { subject.polymorphic_path [main_app, model] }.to raise_error NoMethodError + expect(subject).to receive(:can?).with(:read, model).and_return true + subject.link_to_model(model).should eq "user-link-to-model234@example.com" + end + + it "returns a proper link for main path" do + expect(subject).to receive(:can?).with(:read, WorkflowInstance::Scenario1).and_return true + model = create(:workflow_instance_scenario1, my_name: 'Hellooooo') + subject.link_to_model(model).should eq %|Hellooooo| + end + + it "returns a proper link for a dunlop handled model" do + model = create :execution_batch_my_batch + expect(subject).to receive(:can?).with(:read, model).and_return true + subject.link_to_model(model).should eq %|Uitvoerings batch| + end + + it "does not return a link for a model without permission" do + model = create :execution_batch_my_batch + expect(subject).to receive(:can?).with(:read, model).and_return false + subject.link_to_model(model).should_not be_present + end + end + + describe "#collapsible_content" do + it "Works with simple title argument" do + result = subject.collapsible_content "My Title" do + "I am collapsed" + end + result.should eq '' + end + + it "works with extra classes and id" do + result = subject.collapsible_content "My Title", class: 'abc', id: 'def' do + "I am collapsed" + end + result.should eq '' + end + + it "works without block with content argument" do + result = subject.collapsible_content "My Title", content: "I am collapsed" + result.should eq '' + end + + it "does not add the collapsed class when argument is given as false" do + result = subject.collapsible_content "My Title", content: "I am collapsed", collapsed: false + result.should eq '
My Title
I am collapsed
' + end + + it "works with class as array argument" do + result = subject.collapsible_content "My Title", class: ['abc', 'def'] do + "I am collapsed" + end + result.should eq '' + end + end +end diff --git a/spec/helpers/dunlop/authorization_helper_spec.rb b/spec/helpers/dunlop/authorization_helper_spec.rb new file mode 100644 index 0000000..d251671 --- /dev/null +++ b/spec/helpers/dunlop/authorization_helper_spec.rb @@ -0,0 +1,46 @@ +require 'rails_helper' + +RSpec.describe Dunlop::AuthorizationHelper, type: :helper do + subject { helper } + describe '#has_permission_of_type' do + it "returns wether a permission exists" do + user = create :user, role_names: ['read-adapter-epots', 'manage-model-epots/work-order'] + helper.has_permission_of_type('adapter', user).should be true + helper.has_permission_of_type('class', user).should be false + end + + it 'returns always true for admin users' do + user = create :admin + helper.has_permission_of_type('adapter', user).should be true + end + end + + describe '#any_permission_starts_with' do + it 'matches as a whole' do + user = create :user, role_names: ['read-adapter-epots', 'manage-model-epots/source-file'] + helper.any_permission_starts_with('epots/source-file', user).should be true + helper.any_permission_starts_with('panda', user).should be false + end + + it 'matches for larger paths but not part of it' do + user = create :user, role_names: ['manage-model-epots/source-file/work-order', 'read-model-epots/source-file/han-planning', 'read-model-different-start-epots/source-file'] + helper.any_permission_starts_with('epots/source-file', user).should be true + end + + it 'does not get confused when expression is part of the target' do + user = create :user, role_names: ['read-model-different-start-epots/source-file'] + helper.any_permission_starts_with('epots/source-file', user).should be false + end + + it 'returns always true for admin users' do + user = create :admin + helper.any_permission_starts_with('epots/source-file', user).should be true + end + + it 'supports multiple' do + user = create :user, role_names: ['manage-model-epots/source-file'] + helper.any_permission_starts_with(['blabla', 'epots/source-file'], user).should be true + helper.any_permission_starts_with(['blabla', 'epots/target-file'], user).should be false + end + end +end diff --git a/spec/helpers/dunlop/interpreted_text_helper_spec.rb b/spec/helpers/dunlop/interpreted_text_helper_spec.rb new file mode 100644 index 0000000..506ef71 --- /dev/null +++ b/spec/helpers/dunlop/interpreted_text_helper_spec.rb @@ -0,0 +1,11 @@ +require 'rails_helper' + +RSpec.describe Dunlop::InterpretedTextHelper, type: :helper do + subject { helper } + describe '#interpreted_text' do + it "interprets model names" do + result = subject.interpreted_text "The ${models.user} is part of the ${models.plural.user} community" + result.should eq "The Gebruiker is part of the Gebruikers community" + end + end +end diff --git a/spec/helpers/dunlop/last_attribute_change_helper_spec.rb b/spec/helpers/dunlop/last_attribute_change_helper_spec.rb new file mode 100644 index 0000000..8eeb153 --- /dev/null +++ b/spec/helpers/dunlop/last_attribute_change_helper_spec.rb @@ -0,0 +1,69 @@ +require 'rails_helper' + +RSpec.describe Dunlop::LastAttributeChangeHelper, type: :helper do + subject { helper } + describe '#last_changed_info' do + let(:workflow_instance_1) { create :workflow_instance_scenario1 } + let(:workflow_instance_2) { create :workflow_instance_scenario1 } + let(:attr) { :initialization_check_done } + let(:second_attr) { :my_identifier } + context 'collection with activerecord relation' do + let(:target){ ContractorInitialization::Scenario1::Collection.where(id: [workflow_instance_1.contractor_initialization.id, workflow_instance_2.contractor_initialization.id])} + + it "returns mixed for different changed values" do + workflow_instance_1.contractor_initialization.last_attribute_changes.create changed_at: 1.minute.ago, attribute_name: attr + workflow_instance_2.contractor_initialization.last_attribute_changes.create changed_at: 3.minutes.ago, attribute_name: attr + expect( subject.last_changed_info target, attr ).to eq '(mixed)' + end + + it "returns a timestamp when change timestamps are the same" do + time = "2015-12-03T03:42:00Z".to_time + workflow_instance_1.contractor_initialization.last_attribute_changes.create changed_at: time, attribute_name: attr + workflow_instance_2.contractor_initialization.last_attribute_changes.create changed_at: time, attribute_name: attr + expect( subject.last_changed_info target, attr ).to eq '3 dec 6:42' # test suite is Moscow + end + + it "returns blank when no attribute changes are given" do + expect( subject.last_changed_info target, attr ).to be_blank + end + + it "reduces the number of queries on large collections with added relation refinement" do + target.refine_relation{ with_last_attribute_changes }.map(&:state) # initiate subject first to remove the initialization queries. Only interested in the queries performed by helper method + expect{ + subject.last_changed_info target, attr + subject.last_changed_info target, second_attr + }.not_to exceed_query_limit 1 # two records, two attributes should be done in one query + end + end + + context "collection array" do + let(:target){ ContractorInitialization::Scenario1::Collection.new([workflow_instance_1.contractor_initialization, workflow_instance_2.contractor_initialization]) } + + it "returns mixed for different changed values" do + workflow_instance_1.contractor_initialization.last_attribute_changes.create changed_at: 1.minute.ago, attribute_name: attr + workflow_instance_2.contractor_initialization.last_attribute_changes.create changed_at: 3.minutes.ago, attribute_name: attr + expect( subject.last_changed_info target, attr ).to eq '(mixed)' + end + + it "returns a timestamp when change timestamps are the same" do + time = "2015-12-03T03:42:00Z".to_time + workflow_instance_1.contractor_initialization.last_attribute_changes.create changed_at: time, attribute_name: attr + workflow_instance_2.contractor_initialization.last_attribute_changes.create changed_at: time, attribute_name: attr + expect( subject.last_changed_info target, attr ).to eq '3 dec 6:42' # test suite is Moscow + end + + it "returns blank when no attribute changes are given" do + expect( subject.last_changed_info target, attr ).to be_blank + end + end + + context "activerecord model" do + let(:target){ workflow_instance_1.contractor_initialization } + it "returns a timestamp when there is one" do + time = "2015-12-03T03:42:00Z".to_time + workflow_instance_1.contractor_initialization.last_attribute_changes.create changed_at: time, attribute_name: attr + expect( subject.last_changed_info target, attr ).to eq '3 dec 6:42' # test suite is Moscow + end + end + end +end diff --git a/spec/helpers/dunlop/state_badge_helper_spec.rb b/spec/helpers/dunlop/state_badge_helper_spec.rb new file mode 100644 index 0000000..856ea68 --- /dev/null +++ b/spec/helpers/dunlop/state_badge_helper_spec.rb @@ -0,0 +1,83 @@ +require 'rails_helper' + +RSpec.describe Dunlop::StateBadgeHelper, type: :helper do + subject { helper } + + describe '#state_badge' do + it 'returns an empty html_safe string when no target is present' do + target = double(present?: false) + expect( subject.state_badge target).to be_blank + expect( subject.state_badge target ).to be_html_safe + end + + it 'returns an empty html_safe string when no state is present' do + target = create :owner_initialization_scenario1 + target.state = nil + expect( subject.state_badge target ).to be_blank + expect( subject.state_badge target ).to be_html_safe + end + + it 'returns proper badge htm' do + target = create :owner_initialization_scenario1 + result = subject.state_badge target + result.should eq <<-HTML.strip_heredoc.split("\n").join + + + Owner Init + Pending + + + HTML + result.should be_html_safe + end + + it 'can append text to the state' do + target = FactoryBot.create :owner_initialization_scenario1 + expect( subject.state_badge target, state_append_text: ' (7)' ).to include %|Pending (7)| + end + + it 'allows the state to be overridden by another valid state for the target and displays the translated version' do + target = FactoryBot.create :owner_initialization_scenario1 + expect( subject.state_badge target, state: 'completed' ).to include 'Completed' + end + + it 'works with decorated objects' do + target = create(:workflow_instance_scenario1, :active).decorate + expect( subject.state_badge target ).to eq <<-HTML.strip_heredoc.split("\n").join + + + WI + Actief + + + HTML + end + + end + + describe "#workflow_instance_state_badges_for" do + it "returns in the proper state order (uncategorize, unplanned, planned, overdue, active, any_rejected, all_completed)" do + create(:workflow_instance_scenario1).owner_initialization.reject! + create(:workflow_instance_scenario1, :unplanned) + create(:workflow_instance_scenario1).owner_initialization.process! + expect( subject ).to receive(:current_user).and_return User.new + + result = subject.workflow_instance_state_badges_for(WorkflowInstance::Scenario1) + document = Nokogiri::HTML(result) + document.css('.human-state').map(&:text).should eq ["Unplanned", "Actief", "Any rejected"] + end + end + + describe "#workflow_step_state_badges_for" do + it "returns in the proper state order (uncategorize, unplanned, planned, overdue, active, any_rejected, all_completed)" do + create(:workflow_instance_scenario1).owner_initialization.process! + create(:workflow_instance_scenario1, :unplanned) + create(:workflow_instance_scenario1).owner_initialization.reject! + expect( subject ).to receive(:current_user).and_return User.new + + result = subject.workflow_step_state_badges_for(OwnerInitialization::Scenario1) + document = Nokogiri::HTML(result) + document.css('.human-state').map(&:text).should eq ["Pending", "Actief", "Rejected"] + end + end +end diff --git a/spec/helpers/dunlop/table_helper_spec.rb b/spec/helpers/dunlop/table_helper_spec.rb new file mode 100644 index 0000000..4610a87 --- /dev/null +++ b/spec/helpers/dunlop/table_helper_spec.rb @@ -0,0 +1,56 @@ +require 'rails_helper' + +RSpec.describe Dunlop::ApplicationHelper, type: :helper do + subject { helper } + + describe "#table_show_link" do + it 'returns a valid result' do + record = create 'workflow_instance_batch/scenario1', name: 'B1' + allow(subject).to receive(:can?).and_return true + subject.table_show_link(record).should eq %| + + |.strip + end + + it "does not return when there are no permissions" do + record = create 'workflow_instance_batch/scenario1', name: 'B1' + allow(subject).to receive(:can?).with(:show, record).and_return false + subject.table_show_link(record).should be_blank + subject.table_show_link(record, nil, authorized: true).should be_present + end + end + + describe "#table_edit_link" do + it 'returns a valid result' do + record = create 'workflow_instance_batch/scenario1', name: 'B1' + allow(subject).to receive(:can?).and_return true + subject.table_edit_link(record).should eq %| + + |.strip + end + + it "does not return when there are no permissions" do + record = create 'workflow_instance_batch/scenario1', name: 'B1' + allow(subject).to receive(:can?).with(:update, record).and_return false + subject.table_edit_link(record).should be_blank + subject.table_edit_link(record, nil, authorized: true).should be_present + end + end + + describe '#table_destroy_link' do + it 'returns a valid result' do + record = create 'workflow_instance_batch/scenario1', name: 'B1' + allow(subject).to receive(:can?).and_return true + subject.table_destroy_link(record).should eq %| + + |.strip + end + + it "does not return when there are no permissions" do + record = create 'workflow_instance_batch/scenario1', name: 'B1' + allow(subject).to receive(:can?).with(:destroy, record).and_return false + subject.table_destroy_link(record).should be_blank + subject.table_destroy_link(record, nil, authorized: true).should be_present + end + end +end diff --git a/spec/lib/core_extensions/activerecord/find_each_ordered_spec.rb b/spec/lib/core_extensions/activerecord/find_each_ordered_spec.rb new file mode 100644 index 0000000..3201b3d --- /dev/null +++ b/spec/lib/core_extensions/activerecord/find_each_ordered_spec.rb @@ -0,0 +1,35 @@ +require 'rails_helper' + +RSpec.describe 'ActiveRecord::Relation#find_each_ordered' do + it "preserves order" do + oi1 = SourceFile.create!(current_at_day: '2017-12-25') + oi2 = SourceFile.create!(current_at_day: '2017-02-04') + oi3 = SourceFile.create!(current_at_day: '2017-09-25') + + scope = SourceFile.order(current_at_day: :asc) + + find_each_result = [] + scope.find_each{|r| find_each_result.push r.current_at_day.iso8601} + find_each_result.should eq ['2017-12-25', '2017-02-04', '2017-09-25'] + + find_each_ordered_result = [] + scope.find_each_ordered{|r| find_each_ordered_result.push r.current_at_day.iso8601} + find_each_ordered_result.should eq ['2017-02-04', '2017-09-25', '2017-12-25'] + end + + it "works on a raw model class" do + oi1 = SourceFile.create!(current_at_day: '2017-12-25') + oi2 = SourceFile.create!(current_at_day: '2017-02-04') + oi3 = SourceFile.create!(current_at_day: '2017-09-25') + + scope = SourceFile + + find_each_result = [] + scope.find_each{|r| find_each_result.push r.current_at_day.iso8601} + find_each_result.should match_array ['2017-12-25', '2017-02-04', '2017-09-25'] + + find_each_ordered_result = [] + scope.find_each_ordered{|r| find_each_ordered_result.push r.current_at_day.iso8601} + find_each_ordered_result.should match_array ['2017-02-04', '2017-09-25', '2017-12-25'] + end +end diff --git a/spec/lib/core_extensions/date/epoch_week_spec.rb b/spec/lib/core_extensions/date/epoch_week_spec.rb new file mode 100644 index 0000000..0bf34a8 --- /dev/null +++ b/spec/lib/core_extensions/date/epoch_week_spec.rb @@ -0,0 +1,18 @@ +require 'rails_helper' + +RSpec.describe Date do #epoch_week + expectations = { + '2017-01-01' => 2452, # year 2016, week 52 + '2020-12-24' => 2660, # year 2020, week 52 + '2021-01-01' => 2661, # year 2020, week 53 + '2021-01-03' => 2661, # year 2020, week 53 + '2021-01-04' => 2662, # year 2021, week 1 + '2021-01-07' => 2662, # year 2021, week 1 + '2021-01-12' => 2663, # year 2021, week 2 + } + expectations.each do |date, expected_epoch_week| + it "has the proper epoch_week for #{date}" do + date.to_date.epoch_week.should eq expected_epoch_week + end + end +end diff --git a/spec/lib/core_extensions/time/epoch_week_spec.rb b/spec/lib/core_extensions/time/epoch_week_spec.rb new file mode 100644 index 0000000..7a7d713 --- /dev/null +++ b/spec/lib/core_extensions/time/epoch_week_spec.rb @@ -0,0 +1,18 @@ +require 'rails_helper' + +RSpec.describe Time do #epoch_week + expectations = { + '2017-01-01' => 2452, # year 2016, week 52 + '2020-12-24' => 2660, # year 2020, week 52 + '2021-01-01' => 2661, # year 2020, week 53 + '2021-01-03' => 2661, # year 2020, week 53 + '2021-01-04' => 2662, # year 2021, week 1 + '2021-01-07' => 2662, # year 2021, week 1 + '2021-01-12' => 2663, # year 2021, week 2 + } + expectations.each do |time, expected_epoch_week| + it "has the proper epoch_week for #{time}" do + time.to_time.epoch_week.should eq expected_epoch_week + end + end +end diff --git a/spec/lib/dunlop/csv_builder_spec.rb b/spec/lib/dunlop/csv_builder_spec.rb new file mode 100644 index 0000000..fe90bcb --- /dev/null +++ b/spec/lib/dunlop/csv_builder_spec.rb @@ -0,0 +1,34 @@ +require "rails_helper" + +# Test against de implemented csv builder in +# spec/dummy/app/csv_builders/csv_builder.rb +# that implements the module +describe CsvBuilder do + + describe ".find_for_route" do + it "returns the actual builder class" do + described_class.find_for_route("workflow_instance/scenario1s#selection").should eq WorkflowInstance::SelectionCsvBuilder + described_class.find_for_route("workflow_instance/scenario2s#selection").should eq WorkflowInstance::Scenario2::SelectionCsvBuilder + end + + it "returns nil when no builder can be found" do + described_class.find_for_route("bogus/route#show").should eq nil + end + end + + describe ".find_for_route_lookup_order" do + it "returns the preferred lookup order highest priority first" do + described_class.find_for_route_lookup_order("workflow_instance/scenario1s#selection").should eq [ + 'WorkflowInstance::Scenario1::SelectionCsvBuilder', + 'WorkflowInstance::SelectionCsvBuilder' + ] + end + end + + describe "#data" do + it "uses #to_csv for base class builders" do + described_class.new(double(to_csv: 'custom;csv')).data.should eq "custom;csv" + end + + end +end diff --git a/spec/lib/generators/dunlop/install/base/base_generator_spec.rb b/spec/lib/generators/dunlop/install/base/base_generator_spec.rb new file mode 100644 index 0000000..d09938d --- /dev/null +++ b/spec/lib/generators/dunlop/install/base/base_generator_spec.rb @@ -0,0 +1,35 @@ +require 'rails_helper' +require 'generator_spec' + +require 'generators/dunlop/install/base/base_generator' +describe Dunlop::Install::BaseGenerator, type: :generator do + #destination File.expand_path("../../tmp", __FILE__) + destination Rails.root.join('tmp') + #arguments %w(something) + + before :all do + prepare_destination + FileUtils.cp_r(Rails.root.join('config'), Rails.root.join('tmp')) + destination = Rails.root.join('tmp') + Dir.mkdir(destination.join('spec')) + FileUtils.touch destination.join('Gemfile') + File.open(destination.join('spec/rails_helper.rb'), 'w+') do |file| + file.puts "require 'rspec/rails'" + file.puts "RSpec.configure do |config|" + file.puts "end" + end + run_generator + end + + specify do + expect( destination_root ).to have_structure { + directory "spec" do + file "rails_helper.rb" do + contains "Capybara.javascript_driver = :poltergeist" + contains "require 'rspec/dunlop'" + contains "Rspec::Dunlop::GeneralHelpers" + end + end + } + end +end diff --git a/spec/lib/generators/dunlop/install/source_files/source_files_generator_spec.rb b/spec/lib/generators/dunlop/install/source_files/source_files_generator_spec.rb new file mode 100644 index 0000000..23e3a1b --- /dev/null +++ b/spec/lib/generators/dunlop/install/source_files/source_files_generator_spec.rb @@ -0,0 +1,30 @@ +require 'rails_helper' +require 'generator_spec' + +require 'generators/dunlop/install/source_files/source_files_generator' +describe Dunlop::Install::SourceFilesGenerator, type: :generator, broken: true do + #destination File.expand_path("../../tmp", __FILE__) + destination Rails.root.join('tmp') + #arguments %w[ my_source_file ] + + before :all do + prepare_destination + destination = Rails.root.join('tmp') + FileUtils.cp_r(Rails.root.join('config'), destination) + FileUtils.touch destination.join('Gemfile') + run_generator + end + + specify do + expect( destination_root ).to have_structure { + directory "config" do + file "application.rb" do + contains "config.file_storage_path = Rails.root.join('files')" + contains "config.working_path = '/tmp'" + end + + file "locales/source_files.yml" + end + } + end +end diff --git a/spec/lib/generators/dunlop/install/workflow/workflow_generator_spec.rb b/spec/lib/generators/dunlop/install/workflow/workflow_generator_spec.rb new file mode 100644 index 0000000..25f486a --- /dev/null +++ b/spec/lib/generators/dunlop/install/workflow/workflow_generator_spec.rb @@ -0,0 +1,32 @@ +require 'rails_helper' +require 'generator_spec' + +require 'generators/dunlop/install/workflow/workflow_generator' +describe Dunlop::Install::WorkflowGenerator, type: :generator, broken: true do + #destination File.expand_path("../../tmp", __FILE__) + destination Rails.root.join('tmp') + #arguments %w(something) + + before :all do + prepare_destination + destination = Rails.root.join('tmp') + FileUtils.cp_r(Rails.root.join('config'), destination) + run_generator + end + + specify do + expect( destination_root ).to have_structure { + directory "app" do + directory "models" do + file "workflow_instance.rb" do + contains "include Dunlop::WorkflowInstanceModel" + end + + directory "workflow_instance" do + file "data_setup.rb" + end + end + end + } + end +end diff --git a/spec/mailers/owner_mailer_spec.rb b/spec/mailers/owner_mailer_spec.rb new file mode 100644 index 0000000..f337b57 --- /dev/null +++ b/spec/mailers/owner_mailer_spec.rb @@ -0,0 +1,20 @@ +require "rails_helper" + +RSpec.describe OwnerMailer, type: :mailer do + describe "owner_initialization_finished" do + let(:batch) { build 'workflow_instance_batch/scenario1' } + let(:workflow_step) { build 'owner_initialization/scenario1' } + let(:mail) { OwnerMailer.owner_initialization_finished batch, workflow_step} + + it "renders the headers" do + expect(mail.subject).to eq("Owner initialization finished") + expect(mail.to).to eq(["to@example.org"]) + expect(mail.from).to eq(["from@example.com"]) + end + + it "renders the body" do + expect(mail.body.encoded).to match("Hi") + end + end + +end diff --git a/spec/mailers/previews/owner_mailer_preview.rb b/spec/mailers/previews/owner_mailer_preview.rb new file mode 100644 index 0000000..c250334 --- /dev/null +++ b/spec/mailers/previews/owner_mailer_preview.rb @@ -0,0 +1,9 @@ +# Preview all emails at http://localhost:3000/rails/mailers/owner_mailer +class OwnerMailerPreview < ActionMailer::Preview + + # Preview this email at http://localhost:3000/rails/mailers/owner_mailer/owner_initialization_finished + def owner_initialization_finished + OwnerMailer.owner_initialization_finished + end + +end diff --git a/spec/models/dunlop/application_record_additions/epoch_weeks_spec.rb b/spec/models/dunlop/application_record_additions/epoch_weeks_spec.rb new file mode 100644 index 0000000..31aed81 --- /dev/null +++ b/spec/models/dunlop/application_record_additions/epoch_weeks_spec.rb @@ -0,0 +1,28 @@ +require 'rails_helper' + +RSpec.describe Dunlop::ApplicationRecordAdditions::EpochWeeks do + let(:model) { NonWorkflowProject } + + describe 'set_epoch_weeks!' do + it "creates the proper iso weeks" do + record1 = create :non_workflow_project, working_week: 53, working_year: 2015, working_date: '2017-11-28' + record2 = create :non_workflow_project + + model.set_epoch_weeks! date_column: :working_date + record1.reload.epoch_week.should eq 2500 + record2.reload.epoch_week.should be nil + end + end + + describe 'set_epoch_weeks_from_iso_week!' do + it "creates the proper iso weeks" do + record1 = create :non_workflow_project, working_week: 53, working_year: 2015, working_date: '2017-11-28' + record2 = create :non_workflow_project + + model.set_epoch_weeks_from_iso_week! week_column: :working_week, year_column: :working_year + record1.reload.epoch_week.should eq 2400 + record2.reload.epoch_week.should be nil + end + end +end + diff --git a/spec/models/dunlop/execution_batch_spec.rb b/spec/models/dunlop/execution_batch_spec.rb new file mode 100644 index 0000000..d27be63 --- /dev/null +++ b/spec/models/dunlop/execution_batch_spec.rb @@ -0,0 +1,83 @@ +require 'rails_helper' + +describe Dunlop::ExecutionBatch do + let(:time) { '1981-03-09T13:33:04Z'.to_time } + let(:model) { ExecutionBatch::MyBatch } + before { Timecop.freeze time } + after { Timecop.return } + subject { model.create! } + + context "state machine behaviour", non_transactional: true do + before { allow(subject).to receive(:process_steps).and_return(true) } + + it "transitions from new => processing => completed" do + subject.state.should == "new" + subject.execute_process + subject.state.should == "completed" + end + + it "logs all sorts of info" do + subject.execute_process + subject.log_entries.map(&:body).should == [ + "transition to processing", + "transition to completed", + ] + end + + it "transitions to failed" do + subject.update_attribute(:state, 'completed') + subject.execute_process + subject.state.should == "failed" + end + + it "logs error when failing" do + allow(subject).to receive(:process_steps).and_raise('boom') + subject.execute_process + subject.log_entries.count.should == 3 + subject.should have_log_entries 'transition to processing' + subject.should have_log_entries 'transition to failed' + subject.should have_log_entries /ERROR - boom/ + end + end + + context ".execute_if_required" do + + it "processes if required" do + create(:source_file_my_source, updated_at: 1.hour.ago) + expect( model ).to receive(:create).exactly(1).times.and_return(double(execute_process: true)) + model.execute_if_required + end + + end + + describe ".cleanup!" do + it "cleans up olther than one month" do + model.create! created_at: 1.week.ago + model.create! created_at: 2.months.ago + model.create! created_at: 3.weeks.ago + expect { model.cleanup! }.to change { model.count }.by(-1) + end + + it "works with a duration argument" do + model.create! created_at: 1.week.ago + model.create! created_at: 2.months.ago + model.create! created_at: 3.weeks.ago + expect { model.cleanup! 2.weeks }.to change { model.count }.by(-2) + end + + it "works with a time argument" do + model.create! created_at: 1.day.ago + model.create! created_at: 1.week.ago + model.create! created_at: 2.months.ago + model.create! created_at: 3.weeks.ago + expect { model.cleanup! 2.days.ago }.to change { model.count }.by(-3) + end + end + + describe "info_message" do + it "sets and persists the info_message" do + subject.execute_process + subject.reload.info_message.should eq "I migth have processed 47 records" + end + end +end diff --git a/spec/models/dunlop/source_file_model_spec.rb b/spec/models/dunlop/source_file_model_spec.rb new file mode 100644 index 0000000..87a5247 --- /dev/null +++ b/spec/models/dunlop/source_file_model_spec.rb @@ -0,0 +1,43 @@ +require 'rails_helper' + +describe SourceFile::MySource do + subject { create('source_file/my_source', original_file: fixture_file("source_files/my_source_1.csv") ) } + let(:model) { SourceRecord::MySource } + + describe "#first_line" do + it "returns the first line" do + subject.first_line(subject.original_file.path).should eq "my_date,my_var,My count,my_num,my_truth" + end + end + + describe ".last_current_day" do + it "returns the latest non nil non failed source file current_at_day stamp" do + create 'source_file/my_source' + create 'source_file/my_source', state: 'active', current_at_day: '2016-08-26' + create 'source_file/my_source', state: 'scheduled', current_at_day: '2016-08-28' + create 'source_file/my_source', state: 'inactive', current_at_day: '2016-08-24' + create 'source_file/my_source', state: 'failed', current_at_day: '2016-08-29' + create 'source_file/my_source' + described_class.last_current_day.iso8601.should eq '2016-08-28' + end + + it "works without source files" do + described_class.last_current_day.should be nil + end + end + + describe "#source" do + it "can be a user" do + user = create :user + source_file = create 'source_file/my_source', original_file: fixture_file("source_files/my_source_1.csv"), creator: user + user.source_files.should eq [source_file] + SourceFile.find(source_file.id).creator.should eq user + end + end + + describe '#default_file' do + it "returns the original_file" do + subject.default_file.path.should end_with 'my_source_1.csv' + end + end +end diff --git a/spec/models/dunlop/workflow_instance_model/categorization_and_reset_handling_spec.rb b/spec/models/dunlop/workflow_instance_model/categorization_and_reset_handling_spec.rb new file mode 100644 index 0000000..ff069bf --- /dev/null +++ b/spec/models/dunlop/workflow_instance_model/categorization_and_reset_handling_spec.rb @@ -0,0 +1,81 @@ +require 'rails_helper' + +describe WorkflowInstance do + + let(:time) { '2016-12-24T04:22:00Z'.to_time } + let(:user) { build :user } + before do + Timecop.freeze time + User.current = user + end + after { Timecop.return } + + describe '#reset!' do + it "sunny" do + batch = create :workflow_instance_batch_scenario1 + wi = build :workflow_instance_scenario1, :active, my_name: 'Hi My Name is', zipcode: '1234AB', bucket: 'Edited bucket' + wi.workflow_instance_batch = batch + wi.owner_initialization.assign_attributes( + window_from: 4, + notes: 'QED' + ) + wi.save + + wi.reset! + + reload_model wi + + wi.my_name.should eq 'Hi My Name is' # non editable attributes are not reset! + wi.zipcode.should eq '1234AB' + wi.bucket.should be_blank + wi.workflow_instance_batch.should be_blank + wi.should be_unplanned + + wi.owner_initialization.window_from.should be_blank + wi.owner_initialization.notes.should eq <<-NOTES.strip_heredoc.strip + 2016-12-24T07:22:00+03:00 #{user.email} + QED + + 2016-12-24T07:22:00+03:00 #{user.email} + [RESET] + [CHANGE] Window from changed to [EMPTY] + [STATE_CHANGE] processing => pending + NOTES + wi.owner_initialization.should be_pending + end + + end + + describe "#categorize_as" do + it "works from base WorkflowInstance with symbol" do + wi = create :workflow_instance, bucket: 'Hi there' + wi.class.should eq WorkflowInstance + wi.workflow_steps.should be_empty + + categorized = wi.categorize_as :scenario1 + categorized.reload + categorized.class.should eq WorkflowInstance::Scenario1 + categorized.workflow_steps.map(&:class).map(&:name).should eq %w[OwnerInitialization::Scenario1 ContractorInitialization::Scenario1] + end + + it "works from base WorkflowInstance with string" do + wi = create :workflow_instance, bucket: 'Hi there' + wi.class.should eq WorkflowInstance + wi.workflow_steps.should be_empty + + categorized = wi.categorize_as 'scenario1' + categorized.reload + categorized.class.should eq WorkflowInstance::Scenario1 + categorized.workflow_steps.map(&:class).map(&:name).should eq %w[OwnerInitialization::Scenario1 ContractorInitialization::Scenario1] + end + + it "works from one scenario to another" do + wi = create :workflow_instance_scenario1, bucket: 'Hi there', contractor_initialization: build(:contractor_initialization_scenario1, replace_notes: "My notes\n\n") + categorized = wi.categorize_as :scenario2 + categorized.reload + categorized.class.should eq WorkflowInstance::Scenario2 + categorized.workflow_steps.map(&:process_name).should eq %w[contractor_initialization wba_submission] + categorized.contractor_initialization.notes.should eq "My notes\n\n" # preserved + end + end +end diff --git a/spec/models/dunlop/workflow_instance_model/state_machine_handling_spec.rb b/spec/models/dunlop/workflow_instance_model/state_machine_handling_spec.rb new file mode 100644 index 0000000..7359469 --- /dev/null +++ b/spec/models/dunlop/workflow_instance_model/state_machine_handling_spec.rb @@ -0,0 +1,21 @@ +require 'rails_helper' + +describe OwnerInitialization::Scenario1 do + describe ".sorted_state_count" do + it "returns the counts sorted in the required way" do + create(:workflow_instance_scenario1).owner_initialization.reject! + create(:workflow_instance_scenario1, :unplanned) + create(:workflow_instance_scenario1).owner_initialization.process! + create(:workflow_instance_scenario1, :unplanned) + + create(:workflow_instance_scenario2, :unplanned) + + result = WorkflowInstance::Scenario1.sorted_state_count + result.to_a.should eq [ + ["unplanned", 2], + ["active", 1], + ["any_rejected", 1], + ] + end + end +end diff --git a/spec/models/dunlop/workflow_instance_model/workflow_step_handling_spec.rb b/spec/models/dunlop/workflow_instance_model/workflow_step_handling_spec.rb new file mode 100644 index 0000000..53ad875 --- /dev/null +++ b/spec/models/dunlop/workflow_instance_model/workflow_step_handling_spec.rb @@ -0,0 +1,12 @@ +require 'rails_helper' + +describe WorkflowInstance do + + describe '#workflow_steps' do + it "only returns present workflow steps" do + wi = create :workflow_instance_scenario1 + wi.owner_initialization.destroy + wi.reload.workflow_steps.should be_all + end + end +end diff --git a/spec/models/dunlop/workflow_step_model/state_machine_spec.rb b/spec/models/dunlop/workflow_step_model/state_machine_spec.rb new file mode 100644 index 0000000..1cd4a3d --- /dev/null +++ b/spec/models/dunlop/workflow_step_model/state_machine_spec.rb @@ -0,0 +1,21 @@ +require 'rails_helper' + +describe WorkflowInstance do + describe ".sorted_state_count" do + it "returns the counts sorted in the required way" do + create(:workflow_instance_scenario1).owner_initialization.reject! + create(:workflow_instance_scenario1, :unplanned) + create(:workflow_instance_scenario1).owner_initialization.is_overdue! + create(:workflow_instance_scenario1, :unplanned) + create(:workflow_instance_scenario1).owner_initialization.complete! + create(:workflow_instance_scenario1).owner_initialization.process! + + create(:workflow_instance_scenario2, :unplanned) + + result = OwnerInitialization::Scenario1.sorted_state_count + result.to_a.should eq [ + ["pending", 2], ["overdue", 1], ["processing", 1], ["rejected", 1], ["completed", 1] + ] + end + end +end diff --git a/spec/models/non_workflow_project_spec.rb b/spec/models/non_workflow_project_spec.rb new file mode 100644 index 0000000..ae2bcc9 --- /dev/null +++ b/spec/models/non_workflow_project_spec.rb @@ -0,0 +1,23 @@ +require 'rails_helper' + +RSpec.describe NonWorkflowProject, type: :model do + describe "#identifier" do + it "gets set on initialization as uuid" do + record = create :non_workflow_project + record.identifier.should be_present + record.identifier.size.should eq 36 + match = record.identifier.match(HasUuidIdentifier::UUID_MATCH) + match[1].should eq '1' + match[:version].should eq '1' + end + + it "can be found by its uuid (and also still its id)" do + record = create :non_workflow_project + uuid_queried_record = described_class.find(record.identifier) + uuid_queried_record.should eq record + + id_queried_record = described_class.find(record.id) + id_queried_record.should eq record + end + end +end diff --git a/spec/models/owner_initialization_spec.rb b/spec/models/owner_initialization_spec.rb new file mode 100644 index 0000000..5177b30 --- /dev/null +++ b/spec/models/owner_initialization_spec.rb @@ -0,0 +1,61 @@ +require 'rails_helper' + +describe OwnerInitialization do + describe "#window_from DayTimeMinutes" do + + it "works with assignment" do + described_class.connection.execute "INSERT INTO #{described_class.table_name} (state, window_from, window_to, created_at, updated_at) VALUES ('pending', 78, 11656, TIME(), TIME())" + record = described_class.first + record.window_from.should eq DayTimeMinutes.new('01:18') + record.window_from.should eq '01:18' + record.window_from.should eq 78 + + record.window_from = "16:48" + record.window_from.should eq DayTimeMinutes.new('16:48') + record.window_from.should eq '16:48' + record.window_from.should eq 1008 + + record.save + record.errors.should be_empty + + described_class.connection.exec_query("SELECT window_from FROM #{described_class.table_name}").rows.should eq [[1008]] + + record.reload + record.window_from.should eq '16:48' + + record = described_class.find(record.id) + record.window_from.should eq '16:48' + + record.window_from = 789 + record.window_from.to_s.should eq '13:09' + + record.save + record = described_class.find(record.id) + record.window_from.to_s.should eq '13:09' + + record.update window_from: nil + described_class.connection.exec_query("SELECT window_from FROM #{described_class.table_name}").rows.should eq [[nil]] + + # assign empty + record.update window_from: '' + record = described_class.find(record.id) + record.window_from.should_not be_present + record.window_from.number.should be nil + record.window_from.should eq '' + end + + it "works in queries" do + record = create :owner_initialization, window_from: '13:56' + column = described_class.arel_table[:window_from] + described_class.where(column.gt record.window_from).select(:id).to_sql.should eq %|SELECT "owner_initializations"."id" FROM "owner_initializations" WHERE ("owner_initializations"."window_from" > 836)| + described_class.where("window_from > ?", record.window_from).select(:id).to_sql.should eq %|SELECT "owner_initializations"."id" FROM "owner_initializations" WHERE (window_from > 836)| + end + + it "works in queries with null value" do + record = create :owner_initialization, window_from: nil + column = described_class.arel_table[:window_from] + described_class.where(column.gt record.window_from).select(:id).to_sql.should eq %|SELECT "owner_initializations"."id" FROM "owner_initializations" WHERE ("owner_initializations"."window_from" > NULL)| + described_class.where("window_from > ?", record.window_from).select(:id).to_sql.should eq %|SELECT "owner_initializations"."id" FROM "owner_initializations" WHERE (window_from > NULL)| + end + end +end diff --git a/spec/models/source_file/my_source_spec.rb b/spec/models/source_file/my_source_spec.rb new file mode 100644 index 0000000..c66b066 --- /dev/null +++ b/spec/models/source_file/my_source_spec.rb @@ -0,0 +1,44 @@ +require 'rails_helper' + +describe SourceFile::MySource do + subject { create('source_file/my_source', original_file: fixture_file("source_files/my_source_1.csv") ) } + let(:model) { SourceRecord::MySource } + + describe '#execute_loading_process after_activate_hook' do + it 'calls hooks' do + expect(WorkflowInstance).to receive(:create_or_update_from_my_source) + subject.execute_loading_process + end + end + + describe "#execute_loading_process" do + + it "loads" do + subject.execute_loading_process + subject.should_not have_log_entries /ERROR/ + subject.should have_log_entries 'Inladen gereed' + + model.count.should eq 2 + + record1 = model.first + record1.my_date.should eq "2015-05-09".to_date + record1.my_var.should eq "abc D" + record1.my_count.should eq 19 + record1.my_num.should eq 4.8 + record1.my_truth.should be true + + record2 = model.last + record2.my_date.should eq "2015-11-09".to_date + record2.my_var.should eq "def G" + record2.my_count.should eq 19 + record2.my_num.should eq 7.8 + record2.my_truth.should be false + end + + it "handles loading errors" do + expect( subject ).to receive(:after_activate_hook) { raise "Oops, did not see that one coming :(" } + subject.execute_loading_process + subject.should have_log_entries /ERROR/ + end + end +end diff --git a/spec/models/target_file/file_with_builder_spec.rb b/spec/models/target_file/file_with_builder_spec.rb new file mode 100644 index 0000000..c895820 --- /dev/null +++ b/spec/models/target_file/file_with_builder_spec.rb @@ -0,0 +1,21 @@ +require 'rails_helper' + +describe TargetFile::FileWithBuilder do + it "works without resource" do + file = described_class.generate! + file.should_not have_log_entries /ERROR/ + file.original_file.read.should eq "one,two\n" + end + + it "allows to add a resource to the generate command" do + user = create :user + file1 = described_class.generate! user + file1.should_not have_log_entries /ERROR/ + file1.original_file.read.should eq "one,two\nUser,#{user.id}\n" + + wi = create 'workflow_instance/scenario2' + file2 = described_class.generate! wi + file2.should_not have_log_entries /ERROR/ + file2.original_file.read.should eq "one,two\nWorkflowInstance::Scenario2,#{wi.id}\n" + end +end diff --git a/spec/models/target_file/file_without_builder_spec.rb b/spec/models/target_file/file_without_builder_spec.rb new file mode 100644 index 0000000..321d40e --- /dev/null +++ b/spec/models/target_file/file_without_builder_spec.rb @@ -0,0 +1,41 @@ +require 'rails_helper' + +describe TargetFile::FileWithoutBuilder do + describe ".builder_class" do + it "returns the basic CsvBuilder class when no specific builder can be found" do + described_class.builder_class.should eq CsvBuilder + end + end + + describe "#builder" do + subject { described_class.new.builder } + it "works with not implemented defaults" do + subject.should be_a CsvBuilder + subject.options.should eq Hash.new + end + + it "can implement custop options" do + allow_any_instance_of( described_class ).to receive(:builder_options).and_return(has_custom_options: 8) + subject.options.should eq(has_custom_options: 8) + end + + it "implements default data behaviour expecting a good iteration resource not being enumerable, but responding to id" do + allow_any_instance_of( described_class ).to receive(:resource).and_return(double(id: 8)) + subject.data.should eq "8\n" + end + + it "implements default data behaviour expecting a good iteration resource" do + allow_any_instance_of( described_class ).to receive(:resource).and_return([double(id: 8), double(id: 9)]) + subject.data.should eq "8\n9\n" + end + + it "implements default data behaviour expecting a good iteration resource not being enumerable, but responding to id" do + allow_any_instance_of( described_class ).to receive(:resource).and_return(double(id: 8, to_csv: "custom;implemented;csv")) + subject.data.should eq "custom;implemented;csv" + end + + it "uses the predefined custom builder without stubs" do + subject.data.should eq "my;custom;csv\n1;2;3\n" + end + end +end diff --git a/spec/models/target_file/my_target_spec.rb b/spec/models/target_file/my_target_spec.rb new file mode 100644 index 0000000..3fc99b5 --- /dev/null +++ b/spec/models/target_file/my_target_spec.rb @@ -0,0 +1,105 @@ +require 'rails_helper' + +describe TargetFile::MyTarget do + subject { create 'target_file/my_target' } + + describe "#execute_generation_process" do + + it "generates the file" do + subject.execute_generation_process + + subject.reload.should_not have_log_entries /ERROR/ + subject.should be_completed + + File.basename(subject.original_file.path).should == "my_target-#{Date.current.to_s(:number)}.csv.gz" + + actual_string = read_zipfile(subject.original_file.path) + + actual_string.should eq <<-EXP.strip_heredoc + My;Content;From;Dummy;App + EXP + + subject.size.should be_within(2).of(46) + end + end + + describe ".generate!" do + it "creates an instance and executes the generation process" do + expect_any_instance_of( described_class ).to receive(:execute_generation_process) + described_class.generate! + end + end + + describe ".builder_class" do + it "returns the proper builder class" do + described_class.builder_class.should eq TargetFile::MyTargetCsvBuilder + end + end + + describe ".create_from_source_file!" do + let(:source_file) { create('source_file/my_source', original_file: fixture_file("source_files/my_source_1.csv") ).execute_loading_process } + let(:time) { '2016-03-09T18:22:40Z'.to_time } + before { Timecop.freeze time } + after { Timecop.return } + + it "generates a target file with proper content and attributes" do + allow_any_instance_of(described_class).to receive(:gzip_file?).and_return false + target_file = described_class.create_from_source_file!(source_file) + target_file.should be_completed + target_file.start_time.should be_a Time + target_file.end_time.should be_a Time + target_file.original_file.path.should include "files/target_file/my_target/original_file" + target_file.original_file.path.should end_with "/my_target-20160309.csv" # take target file name, not source file name + target_file.size.should be_within(3).of(100) + target_file.number_of_records.should eq 2 + + actual_string = read_zipfile(target_file.original_file.path) + actual_string.should eq <<-CSV.strip_heredoc + my_date,my_var,My count,my_num,my_truth + 2015-05-09,abc D,19,4.8,true + 2015-11-09,def G,19,7.8,false + CSV + end + + it "works when source file is .gz and target file has gzip_file? => true" do + allow_any_instance_of(described_class).to receive(:gzip_file?).and_return true + target_path = source_file.original_file.path + ".gz" + Dunlop::FileZip.gzip(source_file.original_file.path, target_path) + gzip_source_file = create("source_file/my_source", original_file: File.open(target_path)) + gzip_source_file.execute_loading_process + + target_file = described_class.create_from_source_file!(gzip_source_file) + target_file.number_of_records.should eq 2 + target_file.size.should be_within(3).of(100) + end + end + + describe "#builder" do + subject { described_class.new.builder } + it "works with not implemented defaults" do + subject.should be_a TargetFile::MyTargetCsvBuilder + subject.options.should eq Hash.new + end + + it "can implement custop options" do + allow_any_instance_of( described_class ).to receive(:builder_options).and_return(has_custom_options: 8) + subject.options.should eq(has_custom_options: 8) + end + + it "implements default data behaviour expecting a good iteration resource not being enumerable, but responding to id" do + allow_any_instance_of( described_class ).to receive(:resource).and_return(double(id: 8)) + subject.data.should eq "8\n" + end + + it "implements default data behaviour expecting a good iteration resource" do + allow_any_instance_of( described_class ).to receive(:resource).and_return([double(id: 8), double(id: 9)]) + subject.data.should eq "8\n9\n" + end + end + + describe '#default_file' do + it "returns the original_file" do + subject.default_file.original_filename.should include 'target_file_factory' + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb new file mode 100644 index 0000000..47a31bb --- /dev/null +++ b/spec/models/user_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe User, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/workflow_step/notes_spec.rb b/spec/models/workflow_step/notes_spec.rb new file mode 100644 index 0000000..5a50c87 --- /dev/null +++ b/spec/models/workflow_step/notes_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' + +describe OwnerInitialization::Scenario1 do + subject { build 'owner_initialization/scenario1' } + its(:notes) { should be_blank } + + describe "with user" do + before { User.current = build :user, email: 'user-for-notes@example.com' } + + describe '#notes=' do + it "adds time and user info and proper notes" do + subject.replace_notes "\n abc .\n d" + Timecop.freeze("2016-05-20T12:34:38+03:00") { subject.notes = "alsdfj" } + subject.notes.should eq <<-NOTE.strip_heredoc.strip + abc . + d + + 2016-05-20T12:34:38+03:00 user-for-notes@example.com + alsdfj + NOTE + end + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 0000000..63dda7e --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,114 @@ +# This file is copied to spec/ when you run 'rails generate rspec:install' +ENV['RAILS_ENV'] ||= 'test' +require File.expand_path('../dummy/config/environment', __FILE__) + +# Prevent database truncation if the environment is production +abort("The Rails environment is running in production mode!") if Rails.env.production? +require 'spec_helper' +require 'rspec/rails' +require 'rspec/dunlop' +require 'capybara/poltergeist' +require "cancan/matchers" +# Add additional requires below this line. Rails is not loaded until this point! + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } +ENGINE_RAILS_ROOT=File.join(File.dirname(__FILE__), '../') +Dir[File.join(ENGINE_RAILS_ROOT, "spec/support/**/*.rb")].each {|f| require f } +Dir[File.join(ENGINE_RAILS_ROOT, "spec/factories/**/*.rb")].each {|f| require f } + +I18n.locale = :nl + +# Checks for pending migration and applies them before tests are run. +# If you are not using ActiveRecord, you can remove this line. +ActiveRecord::Migration.maintain_test_schema! + +Capybara.javascript_driver = :poltergeist +Capybara::Screenshot.webkit_options = { width: 1024, height: 768 } +Capybara.save_and_open_page_path = Rails.root.join('tmp/screenshots') + +RSpec::Support::ObjectFormatter.default_instance.max_formatted_output_length = 10_000 # do not trash string like: expected: "abc .... def" + +RSpec.configure do |config| + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_path = Dunlop::Engine.root.join("spec/fixtures") + config.expect_with(:rspec) { |c| c.syntax = [:should, :expect] } + + + config.filter_run_excluding broken: true + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + # + config.use_transactional_fixtures = false + + config.include FactoryBot::Syntax::Methods + config.include Warden::Test::Helpers + config.include Devise::Test::ControllerHelpers, type: :controller + config.include Rspec::Dunlop::GeneralHelpers + config.include Rspec::Dunlop::FeatureHelpers, type: :feature + config.before :suite do + # Generator tests need gemfile + %w[touch #{Rails.root.join('Gemfile')}] + Warden.test_mode! + end + + + # RSpec Rails can automatically mix in different behaviours to your tests + # based on their file location, for example enabling you to call `get` and + # `post` in specs under `spec/controllers`. + # + # You can disable this behaviour by removing the line below, and instead + # explicitly tag your specs with their type, e.g.: + # + # RSpec.describe UsersController, :type => :controller do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://relishapp.com/rspec/rspec-rails/docs + config.infer_spec_type_from_file_location! + + config.before :suite do + DatabaseCleaner.clean_with(:truncation) + end + + config.before :each do |example| + ActionMailer::Base.deliveries.clear + Dunlop::NestedLogger.reset + #Delayed::Worker.delay_jobs = false + if example.metadata.slice(:non_transactional, :js).values.any? + # DatabaseCleaner.strategy = :deletion + # DatabaseCleaner.strategy = :truncation, {pre_count: true, reset_ids: false} + DatabaseCleaner.strategy = :truncation + else + DatabaseCleaner.strategy = :transaction + end + DatabaseCleaner.start + end + + config.append_after(:each) do + DatabaseCleaner.clean + end + + config.after :suite do + FileUtils.rm_f Rails.root.join('Gemfile') + end + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") +end diff --git a/spec/services/day_time_minutes_spec.rb b/spec/services/day_time_minutes_spec.rb new file mode 100644 index 0000000..9800ab1 --- /dev/null +++ b/spec/services/day_time_minutes_spec.rb @@ -0,0 +1,89 @@ +require 'rails_helper' + +describe DayTimeMinutes do + describe "initialization" do + it "can be initialized using a string" do + described_class.new('17:48').number.should eq 17 * 60 + 48 + end + + it "is empty when initialized with empty string" do + described_class.new('').number.should be nil + end + + it "is empty when initialized with nil" do + described_class.new(nil).number.should be nil + end + end + + describe '#inspect' do + it "works with sub hour float values and rounds by minute" do + described_class.new(47.56).inspect.should eq "00:48" + described_class.new(47.46).inspect.should eq "00:47" + end + end + + describe "#to_s" do + it "is the same as inspect" do + described_class.new(7).to_s.should eq "00:07" + end + end + + describe "comparison" do + subject { described_class.new 78 } + describe "equality" do + it "works with numbers and object as base" do + (subject == 78 ).should be true + (subject == 79 ).should be false + (subject == 77 ).should be false + + (subject == '01:18' ).should be true + (subject == '00:54' ).should be false + (subject == '78:00' ).should be false + end + + it "works the other way around" do + (78 == subject).should be true + (79 == subject).should be false + (77 == subject).should be false + + # cannot coerce strings aparently + ('01:18' == subject.to_s).should be true + ('00:54' == subject.to_s).should be false + ('78:00' == subject.to_s).should be false + end + + it "works with objects" do + (subject == described_class.new(5) ).should be false + (subject == described_class.new(78) ).should be true + (described_class.new(nil) == described_class.new('') ).should be true + end + + it "works agains different class objects" do + DayTimeMinutes.new(2).should eq DayTimeSeconds.new(120) + DayTimeSeconds.new(120).should eq DayTimeMinutes.new(2) + end + end + + describe ">" do + it "works with numbers and object as base" do + (subject > 77 ).should be true + (subject > 78 ).should be false + (subject > 79 ).should be false + + (subject > '01:17' ).should be true + (subject > '01:18' ).should be false + (subject > '16:05' ).should be false + end + + it "works the other way around" do + (77 > subject).should be false + (78 > subject).should be false + (79 > subject).should be true + + ('01:17' > subject.to_s).should be false + ('01:18' > subject.to_s).should be false + ('16:05' > subject.to_s).should be true + end + end + end +end diff --git a/spec/services/day_time_seconds_spec.rb b/spec/services/day_time_seconds_spec.rb new file mode 100644 index 0000000..646b293 --- /dev/null +++ b/spec/services/day_time_seconds_spec.rb @@ -0,0 +1,71 @@ +require 'rails_helper' + +describe DayTimeSeconds do + describe "initialization" do + it "can be initialized using a string" do + described_class.new('17:48:23').number.should eq 17 * 3600 + 48 * 60 + 23 + end + end + + describe '#inspect' do + it "works with sub hour float values" do + described_class.new(474.56).inspect.should eq "00:07:55" + end + end + + describe "#to_s" do + it "is the same as inspect" do + described_class.new(7).to_s.should eq "00:00:07" + end + end + + describe "comparison" do + subject { described_class.new 11656 } + describe "equality" do + it "works with numbers and object as base" do + (subject == 11656).should be true + (subject == 7992).should be false + + (subject == '03:14:16' ).should be true + (subject == '00:54:32' ).should be false + end + + it "works the other way around" do + (11656 == subject).should be true + (79 == subject).should be false + + # cannot coerce strings aparently + ('03:14:16' == subject.to_s).should be true + ('00:54:33' == subject.to_s).should be false + end + + it "works with missing minutes and seconds" do + described_class.new('15').should eq '15:00:00' + described_class.new('15:24').should eq '15:24:00' + end + end + + describe ">" do + it "works with numbers and object as base" do + (subject > 77 ).should be true + (subject > 11656 ).should be false + (subject > 12379 ).should be false + + (subject > '02:17' ).should be true + (subject > '03:14:15' ).should be true + (subject > '03:14:17' ).should be false + (subject > '16:05' ).should be false + end + + it "works the other way around" do + (77352 > subject).should be true + (11656 > subject).should be false + (79 > subject).should be false + + ('01:17:24' > subject.to_s).should be false + ('03:14:16' > subject.to_s).should be false + ('03:18:25' > subject.to_s).should be true + end + end + end +end diff --git a/spec/services/dunlop/ability_spec.rb b/spec/services/dunlop/ability_spec.rb new file mode 100644 index 0000000..1e0cda1 --- /dev/null +++ b/spec/services/dunlop/ability_spec.rb @@ -0,0 +1,102 @@ +require 'rails_helper' + +describe Ability do + let(:user_options) { {} } + let(:user) { build :user, user_options } + subject { Ability.new user } + before do + Rails.application.initializers.find{|i| i.name == 'dunlop.ability'}.run + end + + describe '#user_roles, #user_class_roles, #setup_dunlop_class_based_abilities' do + it "handles class based authorizations correctly" do + user_options[:role_names] = [ + 'rubbish', + 'a-b-c-d', + 'read-class-ABC', + 'download-class-SourceFile', + 'download-class-SourceFile::MySource' + ] + + subject.user_roles.should eq [Dunlop::Ability::UserRole.new('download', 'class', 'SourceFile::MySource')] + subject.all_class_roles.should eq [Dunlop::Ability::UserRole.new('download', 'class', 'SourceFile::MySource')] + + allow( user ).to receive(:inspect).and_return "Test User" # devise 4.2.0 issue + + expect( user ).not_to receive(:role_names) # memoize result + subject.user_roles + + subject.should be_able_to :download, SourceFile::MySource + subject.should_not be_able_to :download, SourceFile + subject.should_not be_able_to :manage, SourceFile::MySource + end + + it "authorizes the engine when authorized as app" do + user_options[:role_names] = ['read-app-dunlop'] + subject.should be_able_to :read, Dunlop::Engine + subject.should_not be_able_to :manage, Dunlop::Engine + end + + it "authorizes the engine when authorized as adapter" do + user_options[:role_names] = ['read-adapter-dunlop'] + subject.should be_able_to :read, Dunlop::Engine + subject.should_not be_able_to :manage, Dunlop::Engine + end + + it "authorized camelized classes on model specification" do + user_options[:role_names] = ['read-model-source-file/my_source'] + subject.should be_able_to :read, SourceFile::MySource + subject.should_not be_able_to :manage, SourceFile::MySource + end + end + + describe "#setup_dunlop_class_based_abilities" do + it "sets :index ability to top level class" do + user_options[:role_names] = [ + 'download-class-SourceFile::MySource' + ] + + subject.should_not be_able_to :download, SourceFile + subject.should be_able_to :index, SourceFile + end + end + + describe "advanced permissions (see spec/dummy/app/models/ability.rb for implementation)" do + it "can edit other users with the same domain is advanced permission manage-domain-User is present as admin role" do + user_options[:role_names_admin] = [ + 'manage-domain-User' + ] + user.my_domain = "ServiceProviderA" + + other_user_same_domain = build :user, my_domain: "ServiceProviderA" + other_user_other_domain = build :user, my_domain: "ServiceProviderB" + + subject.should be_able_to :manage, other_user_same_domain + subject.should_not be_able_to :manage, other_user_other_domain + end + end + + describe ".alias_subject" do + let(:ability_class) do + Class.new do + include Dunlop::Ability + add_dunlop_allowed_authorization_classes! + alias_subject WorkflowInstanceBatch, to: WorkflowInstance + end + end + + # The dunlop has a default of alias_subject WorkflowInstanceBatch, to: WorkflowInstance + # lets test this one + it "copies permissions on target" do + ability = ability_class.new(build(:user, role_names: ['download-class-WorkflowInstance'])) + ability.should_not be_able_to :update, WorkflowInstance + ability.should be_able_to :download, WorkflowInstance + ability.should_not be_able_to :update, WorkflowInstance::Scenario1 + ability.should be_able_to :download, WorkflowInstance::Scenario1 + ability.should_not be_able_to :update, WorkflowInstanceBatch + ability.should be_able_to :download, WorkflowInstanceBatch + ability.should_not be_able_to :update, WorkflowInstanceBatch::Scenario1 + ability.should be_able_to :download, WorkflowInstanceBatch::Scenario1 + end + end +end diff --git a/spec/services/dunlop/configuration_spec.rb b/spec/services/dunlop/configuration_spec.rb new file mode 100644 index 0000000..2cbcf53 --- /dev/null +++ b/spec/services/dunlop/configuration_spec.rb @@ -0,0 +1,8 @@ +require 'rails_helper' + +describe Dunlop::Workflow do + # configured in spec/dummy/config/initializers/dunlop + it "has a max_number_of_records_in_display_badge configured of 72 (100 default)" do + Dunlop::Workflow.configuration.max_number_of_records_in_display_badge.should eq 72 + end +end diff --git a/spec/services/dunlop/execution_batch_pid_spec.rb b/spec/services/dunlop/execution_batch_pid_spec.rb new file mode 100644 index 0000000..b9190fc --- /dev/null +++ b/spec/services/dunlop/execution_batch_pid_spec.rb @@ -0,0 +1,95 @@ +require 'rails_helper' + +describe Dunlop::ExecutionBatchPid do + + context "#lock" do + + context 'no id provided' do + subject { Dunlop::ExecutionBatchPid.new } + + it "gets the lock and releases it when done" do + subject.locked?.should == false + subject.lock do + subject.locked?.should == true + end + subject.locked?.should == false + end + + it "raises when lock is already set" do + subject.locked?.should == false + subject.lock do + lambda{ subject.lock { 1 } }.should raise_error('lock already taken') + subject.locked?.should == true + end + subject.locked?.should == false + end + end + + context 'with id provided' do + let(:batch_pid_1) { Dunlop::ExecutionBatchPid.new(:one) } + let(:batch_pid_2) { Dunlop::ExecutionBatchPid.new(:two) } + + it "gets the lock and releases it when done" do + batch_pid_1.locked?.should == false + batch_pid_1.lock do + batch_pid_1.locked?.should == true + end + batch_pid_1.locked?.should == false + end + + it "raises when lock is already set" do + batch_pid_1.locked?.should == false + batch_pid_1.lock do + lambda{ batch_pid_1.lock { 1 } }.should raise_error('lock already taken') + batch_pid_1.locked?.should == true + end + batch_pid_1.locked?.should == false + lambda{ batch_pid_1.lock { 1 } }.should_not raise_error + end + + it "allows other id to lock in parallel" do + batch_pid_1.locked?.should == false + batch_pid_2.locked?.should == false + batch_pid_1.lock do + batch_pid_1.locked?.should == true + batch_pid_2.locked?.should == false + batch_pid_2.lock do + batch_pid_1.locked?.should == true + batch_pid_2.locked?.should == true + end + batch_pid_1.locked?.should == true + batch_pid_2.locked?.should == false + end + batch_pid_1.locked?.should == false + batch_pid_2.locked?.should == false + end + + it "raises other id if lock is already set in parallel" do + batch_pid_1.locked?.should == false + batch_pid_2.locked?.should == false + batch_pid_1.lock do + lambda{ batch_pid_1.lock { 1 } }.should raise_error('lock already taken') + lambda{ batch_pid_2.lock { 1 } }.should_not raise_error + batch_pid_1.locked?.should == true + batch_pid_2.locked?.should == false + batch_pid_2.lock do + lambda{ batch_pid_1.lock { 1 } }.should raise_error('lock already taken') + lambda{ batch_pid_2.lock { 1 } }.should raise_error('lock already taken') + batch_pid_1.locked?.should == true + batch_pid_2.locked?.should == true + end + lambda{ batch_pid_1.lock { 1 } }.should raise_error('lock already taken') + lambda{ batch_pid_2.lock { 1 } }.should_not raise_error + batch_pid_1.locked?.should == true + batch_pid_2.locked?.should == false + end + batch_pid_1.locked?.should == false + batch_pid_2.locked?.should == false + lambda{ batch_pid_1.lock { 1 } }.should_not raise_error + lambda{ batch_pid_2.lock { 1 } }.should_not raise_error + end + end + + end + +end diff --git a/spec/services/dunlop/file_grep_spec.rb b/spec/services/dunlop/file_grep_spec.rb new file mode 100644 index 0000000..e8799d0 --- /dev/null +++ b/spec/services/dunlop/file_grep_spec.rb @@ -0,0 +1,49 @@ +require 'rails_helper' + +RSpec.describe Dunlop::FileGrep do + + let(:filename) { fixture_path.join("file_zip/sip24_kvd_1.txt") } + let(:destination_filename) { "/tmp/sip24_kvd_1.txt" } + + before do + FileUtils.rm_f(destination_filename) + end + + context ".egrep" do + + it "greps the lines matching the regexp" do + expect(File.exists?(destination_filename)).to be_falsey + expect(File.exists?(filename)).to be true + subject.egrep(filename, destination_filename, 'AA|HVD') + expect(File.exists?(destination_filename)).to be_truthy + expect(File.read(destination_filename)).to eq <<-DOC.strip_heredoc + 1000_HVD;1000_HVD;;;0;0 + 1000_AA;1000_AA;1000;;2;2 + 1000_BB;1000_AA;;1000;1;1 + 1000_CC;1000_AA;;1000;1;1 + 2000_HVD;2000_HVD;;;0;0 + 2000_AA;2000_AA;1000;;1;1 + 3000_HVD;3000_HVD;;;1;1 + DOC + end + + end + + context ".egrep_v" do + + it "greps the lines not matching the regexp" do + expect(File.exists?(destination_filename)).to be_falsey + subject.egrep_v(filename, destination_filename, 'AA|HVD') + expect(File.exists?(destination_filename)).to be_truthy + expect(File.read(destination_filename)).to eq <<-DOC.strip_heredoc + kvd;primair;lengte;lengte_primair;adressen;totaal + 1000_;;;;;2 + 2000_;;;;;1 + 3000_;;;;;1 + 172500_A;172500_A;1000;;3;3 + DOC + end + + end + +end diff --git a/spec/services/dunlop/file_sed_spec.rb b/spec/services/dunlop/file_sed_spec.rb new file mode 100644 index 0000000..ae9e541 --- /dev/null +++ b/spec/services/dunlop/file_sed_spec.rb @@ -0,0 +1,34 @@ +require 'rails_helper' + +RSpec.describe Dunlop::FileSed do + + context ".egrep" do + + let(:file) { Tempfile.new('file_sed', Rails.application.config.working_path) } + let(:filename) { file.path } + + after do + file.unlink + end + + it "replaces single infile" do + File.open(filename,'w') { |file| file << %Q{hello world\nhere is some " for you} } + subject.in_place(filename, %Q{s/"//g}) + expect(File.read(filename).strip).to eq %Q{hello world\nhere is some for you} + end + + it "replaces multiple infile" do + File.open(filename,'w') { |file| file << %Q{hello world\nhere is some " for you} } + subject.in_place(filename, [%Q{s/"//g},%Q{s/u$/a/}]) + expect(File.read(filename).strip).to eq %Q{hello world\nhere is some for yoa} + end + + it "handles CFLR, CR, LF => LF with the right combination" do + File.open(filename,'w') { |file| file << %Q{hello world\r\nthis is CRLF\r\nmixed with CR only\rand some LF only\nwierd eh?} } + subject.fix_line_endings(filename) + expect(File.read(filename)).to eq %Q{hello world\nthis is CRLF\nmixed with CR only\nand some LF only\nwierd eh?} + end + + end + +end diff --git a/spec/services/dunlop/file_zip_spec.rb b/spec/services/dunlop/file_zip_spec.rb new file mode 100644 index 0000000..eee1a13 --- /dev/null +++ b/spec/services/dunlop/file_zip_spec.rb @@ -0,0 +1,84 @@ +require 'rails_helper' + +RSpec.describe Dunlop::FileZip do + + let(:zipped_filename) { fixture_path.join("file_zip/sip24_kvd_1.zip") } + let(:gzipped_filename) { fixture_path.join("file_zip/sip24_kvd_1.gz") } + let(:unzipped_filename) { fixture_path.join("file_zip/sip24_kvd_1.txt") } + let(:destination_filename) { "/tmp/sip24_kvd_1.txt" } + + before do + FileUtils.rm_f(destination_filename) + end + + after do + FileUtils.rm_f(destination_filename) + end + + context ".unzip" do + + context "mime-type: application/zip" do + + it "unzips the file to the provided filename" do + expect(File.exists?(destination_filename)).to be_falsey + subject.unzip(zipped_filename, destination_filename) + expect(File.exists?(destination_filename)).to be_truthy + expect(File.exists?(zipped_filename)).to be_truthy + expect(File.read(destination_filename)).to eq File.read(unzipped_filename) + end + + + + end + + context "mime-type: application/x-gzip" do + + it "unzips the file to the provided filename" do + expect(File.exists?(destination_filename)).to be_falsey + subject.unzip(gzipped_filename, destination_filename) + expect(File.exists?(destination_filename)).to be_truthy + expect(File.exists?(gzipped_filename)).to be_truthy + expect(File.read(destination_filename)).to eq File.read(unzipped_filename) + end + + end + + context "mime-type: text/plain" do + + it "copies the file to the provided filename" do + expect(File.exists?(destination_filename)).to be_falsey + subject.unzip(unzipped_filename, destination_filename) + expect(File.exists?(destination_filename)).to be_truthy + expect(File.exists?(unzipped_filename)).to be_truthy + expect(File.read(destination_filename)).to eq File.read(unzipped_filename) + end + + end + + end + + context ".gzip" do + let(:gzipped_filename) { "/tmp/sip24_kvd_1.txt.gz" } + + before do + FileUtils.rm_f(gzipped_filename) + end + + after do + FileUtils.rm_f(gzipped_filename) + end + + it "gzips the file to the provided filename" do + expect(File.exists?(gzipped_filename)).to be_falsey + subject.gzip(unzipped_filename, gzipped_filename) + expect(File.exists?(gzipped_filename)).to be_truthy + expect(File.exists?(unzipped_filename)).to be_truthy + + expect(File.size(unzipped_filename)).to eq 305 + expect(File.size(gzipped_filename)).to eq 153 + expect(subject.mime_type(gzipped_filename)).to eq 'application/x-gzip' + end + + end + +end diff --git a/spec/services/dunlop/log_entry_creator_spec.rb b/spec/services/dunlop/log_entry_creator_spec.rb new file mode 100644 index 0000000..2085107 --- /dev/null +++ b/spec/services/dunlop/log_entry_creator_spec.rb @@ -0,0 +1,16 @@ +require 'rails_helper' + +describe Dunlop::LogEntryCreator do + let(:model) { ExecutionBatch::MyBatch } + + it "creates log entries inside a long running process" do + record = model.execute! + record.log_entries.pluck(:body).should eq [ + "transition to processing", + "INFO - Actual info in long running process", + "transition to completed", + "- execute_process:\n - INFO - Finished 30 imaginary tasks\n" + ] + end + +end diff --git a/spec/services/dunlop/loggable_spec.rb b/spec/services/dunlop/loggable_spec.rb new file mode 100644 index 0000000..9549eda --- /dev/null +++ b/spec/services/dunlop/loggable_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +describe Dunlop::Loggable do + let(:model) { ExecutionBatch::MyBatch } + + it "handles failures" do + expect_any_instance_of(model).to receive(:process_steps).and_raise "Oh no!" + record = model.execute! + record.should have_log_entries "ERROR - Oh no!" + record.should_not have_log_entries "and_raise" + log_entry = record.log_entries.last + log_entry.backtrace.should include "and_raise" + end + +end diff --git a/spec/services/dunlop/nested_logger_spec.rb b/spec/services/dunlop/nested_logger_spec.rb new file mode 100644 index 0000000..077ef95 --- /dev/null +++ b/spec/services/dunlop/nested_logger_spec.rb @@ -0,0 +1,240 @@ +require 'rails_helper' + +describe Dunlop::NestedLogger do + + let(:time) { '2016-03-09T18:22:40Z'.to_time } + before do + Dunlop::NestedLogger.reset + Timecop.freeze time + end + after { Timecop.return } + + it 'logs complex nested situation' do + Dunlop::NestedLogger.scope('scope 1') do + Dunlop::NestedLogger.info('text 1') + Dunlop::NestedLogger.scope('scope 2a') do + Dunlop::NestedLogger.info('text 2') + end + Dunlop::NestedLogger.scope('scope 2b') do + Dunlop::NestedLogger.info('text 3a') + Dunlop::NestedLogger.warn('text 3b') + end + Dunlop::NestedLogger.info('text q') + Dunlop::NestedLogger.scope('scope 2b') do + Dunlop::NestedLogger.info('text 4') + end + Dunlop::NestedLogger.info('text 5') + end + + Dunlop::NestedLogger.flush.should eq [{ + 'scope 1' => [ + 'INFO - text 1', + { 'scope 2a' => ['INFO - text 2'] }, + { 'scope 2b' => ['INFO - text 3a', 'WARNING - text 3b'] }, + 'INFO - text q', + { 'scope 2b' => ['INFO - text 4'] }, + 'INFO - text 5', + ] + }] + end + + it 'returns return value of block inside scope (thus is transparant)' do + value = Dunlop::NestedLogger.scope('scope 1') do + Dunlop::NestedLogger.info('text 1') + Dunlop::NestedLogger.scope('scope 2a') do + 'test' + end + end + + value.should eq 'test' + end + + it 'is thread safe and merges intermediate loggings from other threads' do + Dunlop::NestedLogger.scope('scope 1') do + Dunlop::NestedLogger.info('text 1') + threads = [] + 5.times do |i| + threads << Thread.new do + if [0,1,4].include? i + Dunlop::NestedLogger.scope("thread scope #{i}") do + Dunlop::NestedLogger.info("thread text #{i}") + end + elsif i == 3 + Dunlop::NestedLogger.info("thread text #{i}a") + Dunlop::NestedLogger.info("thread text #{i}b") + end + end + end + threads.each { |thread| thread.join } + Dunlop::NestedLogger.merge_threads(threads) + Dunlop::NestedLogger.info('text 3') + end + + Dunlop::NestedLogger.flush.should eq [{ + "scope 1"=>[ + "INFO - text 1", + {"thread scope 0"=>["INFO - thread text 0"]}, + {"thread scope 1"=>["INFO - thread text 1"]}, + "INFO - thread text 3a", + "INFO - thread text 3b", + {"thread scope 4"=>["INFO - thread text 4"]}, + "INFO - text 3" + ] + }] + end + + it 'benchmarks' do + Timecop.freeze do + Dunlop::NestedLogger.benchmark do + Dunlop::NestedLogger.scope('scope 1') do + Dunlop::NestedLogger.info('text 1') + Dunlop::NestedLogger.scope('scope 2a') do + Dunlop::NestedLogger.info('text 2') + end + Dunlop::NestedLogger.scope('scope 2b', benchmark: false) do + Dunlop::NestedLogger.info('text 2') + end + Dunlop::NestedLogger.scope('empty sandbox', benchmark: false) do + end + end + end + end + + Dunlop::NestedLogger.flush_as_yaml.should eq <<-DOC +- scope 1: + - INFO - text 1 + - scope 2a: + - INFO - text 2 + - INFO - duration = 0.0s + - scope 2b: + - INFO - text 2 + - INFO - duration = 0.0s + DOC + end + + #it 'resets after flush' do + #Dunlop::NestedLogger.info('text 1') + #Dunlop::NestedLogger.flush + #Dunlop::NestedLogger.info('text 2') + #Dunlop::NestedLogger.flush.should eq ["INFO - text 2"] + #end + + it 'use sandbox scope to ignore existing scope' do + flush_1 = {} + flush_2 = {} + flush_3 = {} + + Dunlop::NestedLogger.scope('scope 1') do + Dunlop::NestedLogger.info('text 1a') + Dunlop::NestedLogger.scope('sandbox scope') do + Dunlop::NestedLogger.scope('scope 2') do + Dunlop::NestedLogger.info('text 2a') + Dunlop::NestedLogger.scope('scope 3') do + Dunlop::NestedLogger.info('text 3a') + end + end + flush_3 = Dunlop::NestedLogger.flush + end + flush_2 = Dunlop::NestedLogger.flush + end + flush_1 = Dunlop::NestedLogger.flush + + flush_1.should eq [] + flush_2.should eq ["INFO - text 1a"] + flush_3.should eq [{"scope 2"=>["INFO - text 2a", {"scope 3"=>["INFO - text 3a"]}]}] + end + + it 'removes flushed scope from existing scope' do + flush_1 = {} + flush_2 = {} + flush_3 = {} + + Dunlop::NestedLogger.info('text 0a') + Dunlop::NestedLogger.scope('scope 1') do + Dunlop::NestedLogger.info('text 1a') + Dunlop::NestedLogger.scope('scope 2') do + Dunlop::NestedLogger.info('text 2a') + end + flush_1 = Dunlop::NestedLogger.flush + Dunlop::NestedLogger.info('text 1b') + end + Dunlop::NestedLogger.info('text 0b') + flush_2 = Dunlop::NestedLogger.flush + Dunlop::NestedLogger.info('text 0c') + flush_3 = Dunlop::NestedLogger.flush + + flush_1.should eq ["INFO - text 1a", {"scope 2"=>["INFO - text 2a"]}] + flush_2.should eq ["INFO - text 0a", {"scope 1"=>["INFO - text 1b"]}, "INFO - text 0b"] + flush_3.should eq ["INFO - text 0c"] + end + + it 'ignores empty scopes (non top level)' do + Dunlop::NestedLogger.scope('scope 1') do + Dunlop::NestedLogger.info('text 1') + Dunlop::NestedLogger.scope('scope 2') {} + Timecop.freeze do + Dunlop::NestedLogger.benchmark do + Dunlop::NestedLogger.scope('scope 3') {} + end + end + end + + Dunlop::NestedLogger.flush.should eq [{"scope 1"=>["INFO - text 1", {"scope 3"=>["INFO - duration = 0.0s"]}]}] + end + + it 'ignores empty scopes (top level)' do + Dunlop::NestedLogger.scope('scope 1') {} + Dunlop::NestedLogger.flush.should eq [] + end + + it 'merges scope stack (useful for handing over after fork)' do + Dunlop::NestedLogger.scope('scope 1') do + Dunlop::NestedLogger.info('text 1') + end + stack_1 = Dunlop::NestedLogger.copy_scope_stack_for_merge + Dunlop::NestedLogger.flush.should eq [{"scope 1"=>["INFO - text 1"]}] + + Dunlop::NestedLogger.scope('scope 3') do + Dunlop::NestedLogger.info('text 3') + Dunlop::NestedLogger.merge_scope_stack(stack_1) + end + + Dunlop::NestedLogger.flush.should eq [{"scope 3"=>["INFO - text 3", {"scope 1"=>["INFO - text 1"]}]}] + end + + it 'allows unscoped entries' do + Dunlop::NestedLogger.info('text 1') + Dunlop::NestedLogger.scope('scope 1') do + Dunlop::NestedLogger.info('text 2') + end + Dunlop::NestedLogger.warn('text 3') + Dunlop::NestedLogger.flush.should eq [ + "INFO - text 1", + {"scope 1"=>["INFO - text 2"]}, + "WARNING - text 3" + ] + end + + it 'ignores empty flush' do + expect { + Dunlop::NestedLogger.flush + Dunlop::NestedLogger.flush + }.not_to raise_error + end + + context 'formatter' do + before do + Dunlop::NestedLogger.formatter = proc do |severity, datetime, progname, msg| + "#{severity} - custom - #{msg}" + end + end + after { Dunlop::NestedLogger.reset_formatter } + + it 'allows custom formatter' do + Dunlop::NestedLogger.info 'test' + Dunlop::NestedLogger.flush.should eq ['info - custom - test'] + end + end + +end + diff --git a/spec/services/dunlop/overdue_handling_spec.rb b/spec/services/dunlop/overdue_handling_spec.rb new file mode 100644 index 0000000..6850bdf --- /dev/null +++ b/spec/services/dunlop/overdue_handling_spec.rb @@ -0,0 +1,166 @@ +require 'rails_helper' + +describe Dunlop::OverdueHandling do + before do + subject.define do + when_workflow_step :contractor_initialization, is: 2.days, from: {owner_initialization: :plan_date} + end + end + + describe ".handle_overdue!" do + it "sets to overdue when needed" do + workflow_instance_1 = create :workflow_instance_scenario1 + workflow_instance_1.owner_initialization.update_columns(plan_date: 3.days.ago.to_date) + + subject.handle_overdue! + + workflow_instance_1.reload.should be_overdue + workflow_instance_1.contractor_initialization.should be_overdue + end + + it "sets to overdue when needed" do + workflow_instance_2 = create :workflow_instance_scenario1 + workflow_instance_2.owner_initialization.update_columns(plan_date: 1.day.ago.to_date) + + subject.handle_overdue! + + workflow_instance_2.reload.should_not be_overdue + workflow_instance_2.contractor_initialization.should_not be_overdue + end + + it "sets to overdue when needed" do + workflow_instance_3 = create :workflow_instance_scenario1 + workflow_instance_3.owner_initialization.update_columns(plan_date: 5.days.from_now.to_date) + + subject.handle_overdue! + + workflow_instance_3.reload.should_not be_overdue + workflow_instance_3.contractor_initialization.should_not be_overdue + end + + it "sets to overdue when needed" do + # exact match, also overdue + workflow_instance_4 = create :workflow_instance_scenario1 + workflow_instance_4.owner_initialization.update_columns(plan_date: 2.days.ago.to_date) + + subject.handle_overdue! + + # exact match, also overdue + workflow_instance_4.reload.should be_overdue + workflow_instance_4.contractor_initialization.should be_overdue + end + + it "sets to overdue when needed" do + # completed overdue times are ignored of course + workflow_instance_5 = create :workflow_instance_scenario1, :all_completed + workflow_instance_5.owner_initialization.update_columns(plan_date: 20.days.ago.to_date) + + subject.handle_overdue! + + workflow_instance_5.reload.should be_all_completed + workflow_instance_5.contractor_initialization.should be_completed + end + + it "sets to overdue when needed" do + workflow_instance_6 = create :workflow_instance_scenario1 + workflow_instance_6.owner_initialization.update_columns(plan_date: 20.days.ago.to_date) + workflow_instance_6.owner_initialization.reject! + + subject.handle_overdue! + + # any_rejected wins over overdue for overal workflow_instance state + workflow_instance_6.reload.should be_any_rejected + workflow_instance_6.contractor_initialization.should be_overdue + end + end + + describe "changing the date specified as target in the configuration" do + it "sets to overdue when date change creates overdue situation" do + workflow_instance = create :workflow_instance_scenario1, :overdue + workflow_instance.owner_initialization.update_columns(plan_date: 1.day.ago.to_date) + + # Trigger the callbacks + workflow_instance.owner_initialization.update plan_date: 3.days.ago.to_date + + reload_model workflow_instance + + workflow_instance.contractor_initialization.should be_overdue + workflow_instance.should be_overdue + end + + it "removes overdue when the date is no longer overdue" do + workflow_instance = create :workflow_instance_scenario1, :overdue + workflow_instance.owner_initialization.update_columns(plan_date: 3.days.ago.to_date) + workflow_instance.contractor_initialization.update_columns(state: 'overdue') + + # Trigger the callbacks + workflow_instance.owner_initialization.update plan_date: 1.day.ago.to_date + + reload_model workflow_instance + + workflow_instance.contractor_initialization.should be_processing + workflow_instance.should be_active + end + + it "removes overdue when the date is removed" do + workflow_instance = create :workflow_instance_scenario1, :overdue + workflow_instance.owner_initialization.update_columns(plan_date: 3.days.ago.to_date) + workflow_instance.contractor_initialization.update_columns(state: 'overdue') + + # Trigger the callbacks + workflow_instance.owner_initialization.update plan_date: nil + + reload_model workflow_instance + + workflow_instance.contractor_initialization.should be_processing + workflow_instance.should be_active + end + + it "does not set rejected records to overdue" do + workflow_instance = create :workflow_instance_scenario1, :overdue + workflow_instance.owner_initialization.update_columns(plan_date: 1.day.ago.to_date) + workflow_instance.contractor_initialization.reject! + + # Trigger the callbacks + workflow_instance.owner_initialization.update plan_date: 3.days.ago.to_date + + reload_model workflow_instance + + workflow_instance.contractor_initialization.should be_rejected + workflow_instance.should be_any_rejected + end + + it "does not set completed records to overdue" do + workflow_instance = create :workflow_instance_scenario1, :overdue + workflow_instance.owner_initialization.update_columns(plan_date: 1.day.ago.to_date) + workflow_instance.contractor_initialization.complete! + + # Trigger the callbacks + workflow_instance.owner_initialization.update plan_date: 3.days.ago.to_date + + reload_model workflow_instance + + workflow_instance.contractor_initialization.should be_completed + workflow_instance.should be_active + end + end + + describe "completing an overdue workflow step" do + it "activates the workflow instance from overdue state when the overdue workflow step is completed" do + workflow_instance = create :workflow_instance_scenario1, :overdue + workflow_instance.reload.should be_overdue + workflow_instance.owner_initialization.should be_pending + workflow_instance.contractor_initialization.should be_overdue + workflow_instance.contractor_initialization.complete! + + workflow_instance.reload.should be_active + end + + it "rejects the workflow instance from overdue state when the overdue workflow step is completed" do + workflow_instance = create :workflow_instance_scenario1, :overdue + workflow_instance.contractor_initialization.reject! + + workflow_instance.reload.should be_any_rejected + end + end +end diff --git a/spec/services/dunlop/state_badge_spec.rb b/spec/services/dunlop/state_badge_spec.rb new file mode 100644 index 0000000..e038b08 --- /dev/null +++ b/spec/services/dunlop/state_badge_spec.rb @@ -0,0 +1,23 @@ +require 'rails_helper' + +#describe Dunlop::StateBadge, type: :helper do +describe Dunlop::ApplicationHelper, type: :helper do + let(:arguments) { {context: helper, target: target, options: {}} } + let(:target) { build :workflow_instance_scenario1, :active, id: 987 } + subject { Dunlop::StateBadge.new(arguments[:context], arguments[:target], arguments[:options]) } + + it "shows the default html" do + subject.html.should eq <<-HTML.strip_heredoc.split("\n").map(&:strip).join + + WI + Actief + + + HTML + end + + it "adds linkt_to as target if given" do + arguments[:options][:link_to] = "/smart_url?q[state_eq]=my_state" + subject.html.should include 'data-target="/smart_url?q[state_eq]=my_state"' + end +end diff --git a/spec/services/dunlop/user_settings_spec.rb b/spec/services/dunlop/user_settings_spec.rb new file mode 100644 index 0000000..9e5e75e --- /dev/null +++ b/spec/services/dunlop/user_settings_spec.rb @@ -0,0 +1,65 @@ +require 'rails_helper' + +describe Dunlop::UserSettings do + let(:user) { build :user } + let(:settings_options) { {storage: {}} } + subject { described_class.new(user, settings_options[:storage].deep_stringify_keys) } + + describe "#get" do + it "gets values as multiple arguments with mixed values" do + settings_options[:storage] = { + hawk_eye: {analysis: {4 => {45 => [1,2,3]}}} + } + subject.get(:hawk_eye, 'analysis', 4, "45").should eq [1,2,3] + end + end + + describe "#set" do + it "sets the proper value into the storage" do + subject.set(%i[a b 9 d], {a: 1}) + user.settings_storage.should eq( + "a" => { + "b" => { + "9" => { + "d" => { "a" => 1 } + } + } + } + ) + user.settings_storage.should eq subject.storage + + end + + it "works with parameters assignment" do + subject.set :a, 'b', 4, 9 + user.settings_storage.should eq( + "a" => { + "b" => { + "4" => 9 + } + } + ) + user.settings_storage.should eq subject.storage + end + + it "recognizes simple key, value structures" do + subject.set a: 1, b: 9 + subject.get(:a).should eq 1 + subject.get(:b).should eq 9 + end + + it "does not persist the record" do + subject.set a: 4 + subject.record.should_not be_persisted + end + end + + describe "#set!" do + it "persists the record" do + subject.set! a: 4 + subject.get(:a).should eq 4 + subject.record.should be_persisted + subject.record.reload.settings.get(:a).should eq 4 + end + end +end diff --git a/spec/services/week_spec.rb b/spec/services/week_spec.rb new file mode 100644 index 0000000..0334ee0 --- /dev/null +++ b/spec/services/week_spec.rb @@ -0,0 +1,31 @@ +require 'rails_helper' + +describe Week do + describe "2016" do + let(:time) { '2016-03-09T18:22:40Z'.to_time } + before { Timecop.freeze time } + after { Timecop.return } + it "returns a date range from a week number" do + described_class.new(38).should eq '2016-09-19'.to_date..'2016-09-25'.to_date + described_class.new(38).year.should eq 2016 + described_class.new(38).number.should eq 38 + end + + it "can be given a custom year as option" do + described_class.new(38, year: 2015).should eq '2015-09-14'.to_date..'2015-09-20'.to_date + described_class.new(38, year: 2015).year.should eq 2015 + described_class.new(38, year: 2015).number.should eq 38 + end + end + + describe "2015" do + let(:time) { '2015-03-09T18:22:40Z'.to_time } + before { Timecop.freeze time } + after { Timecop.return } + it "returns a date range from a week number" do + described_class.new(38).should eq '2015-09-14'.to_date..'2015-09-20'.to_date + described_class.new(38).year.should eq 2015 + described_class.new(38).number.should eq 38 + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..61e2738 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,92 @@ +# This file was generated by the `rails generate rspec:install` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# The `.rspec` file also contains a few flags that are not defaults but that +# users commonly want. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # These two settings work together to allow you to limit a spec run + # to individual examples or groups you care about by tagging them with + # `:focus` metadata. When nothing is tagged with `:focus`, all examples + # get run. + config.filter_run :focus + config.run_all_when_everything_filtered = true + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + config.disable_monkey_patching! + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = 'doc' + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..e377e9e --- /dev/null +++ b/todo.md @@ -0,0 +1,6 @@ +TODO +===== + +* Add high voltage dependency +* Add redcarpet dependency +* Add changelog diff --git a/vendor/assets/javascripts/inflection.js b/vendor/assets/javascripts/inflection.js new file mode 100644 index 0000000..d53b5ed --- /dev/null +++ b/vendor/assets/javascripts/inflection.js @@ -0,0 +1,656 @@ +/* +Copyright (c) 2010 Ryan Schuft (ryan.schuft@gmail.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +/* + This code is based in part on the work done in Ruby to support + infection as part of Ruby on Rails in the ActiveSupport's Inflector + and Inflections classes. It was initally ported to Javascript by + Ryan Schuft (ryan.schuft@gmail.com) in 2007. + + The code is available at http://code.google.com/p/inflection-js/ + + The basic usage is: + 1. Include this script on your web page. + 2. Call functions on any String object in Javascript + + Currently implemented functions: + + String.pluralize(plural) == String + renders a singular English language noun into its plural form + normal results can be overridden by passing in an alternative + + String.singularize(singular) == String + renders a plural English language noun into its singular form + normal results can be overridden by passing in an alterative + + String.camelize(lowFirstLetter) == String + renders a lower case underscored word into camel case + the first letter of the result will be upper case unless you pass true + also translates "/" into "::" (underscore does the opposite) + + String.underscore() == String + renders a camel cased word into words seperated by underscores + also translates "::" back into "/" (camelize does the opposite) + + String.humanize(lowFirstLetter) == String + renders a lower case and underscored word into human readable form + defaults to making the first letter capitalized unless you pass true + + String.capitalize() == String + renders all characters to lower case and then makes the first upper + + String.dasherize() == String + renders all underbars and spaces as dashes + + String.titleize() == String + renders words into title casing (as for book titles) + + String.demodulize() == String + renders class names that are prepended by modules into just the class + + String.tableize() == String + renders camel cased singular words into their underscored plural form + + String.classify() == String + renders an underscored plural word into its camel cased singular form + + String.foreign_key(dropIdUbar) == String + renders a class name (camel cased singular noun) into a foreign key + defaults to seperating the class from the id with an underbar unless + you pass true + + String.ordinalize() == String + renders all numbers found in the string into their sequence like "22nd" +*/ + +/* + This sets up a container for some constants in its own namespace + We use the window (if available) to enable dynamic loading of this script + Window won't necessarily exist for non-browsers. +*/ +if (window && !window.InflectionJS) +{ + window.InflectionJS = null; +} + +/* + This sets up some constants for later use + This should use the window namespace variable if available +*/ +InflectionJS = +{ + /* + This is a list of nouns that use the same form for both singular and plural. + This list should remain entirely in lower case to correctly match Strings. + */ + uncountable_words: [ + 'equipment', 'information', 'rice', 'money', 'species', 'series', + 'fish', 'sheep', 'moose', 'deer', 'news' + ], + + /* + These rules translate from the singular form of a noun to its plural form. + */ + plural_rules: [ + [new RegExp('(m)an$', 'gi'), '$1en'], + [new RegExp('(pe)rson$', 'gi'), '$1ople'], + [new RegExp('(child)$', 'gi'), '$1ren'], + [new RegExp('^(ox)$', 'gi'), '$1en'], + [new RegExp('(ax|test)is$', 'gi'), '$1es'], + [new RegExp('(octop|vir)us$', 'gi'), '$1i'], + [new RegExp('(alias|status)$', 'gi'), '$1es'], + [new RegExp('(bu)s$', 'gi'), '$1ses'], + [new RegExp('(buffal|tomat|potat)o$', 'gi'), '$1oes'], + [new RegExp('([ti])um$', 'gi'), '$1a'], + [new RegExp('sis$', 'gi'), 'ses'], + [new RegExp('(?:([^f])fe|([lr])f)$', 'gi'), '$1$2ves'], + [new RegExp('(hive)$', 'gi'), '$1s'], + [new RegExp('([^aeiouy]|qu)y$', 'gi'), '$1ies'], + [new RegExp('(x|ch|ss|sh)$', 'gi'), '$1es'], + [new RegExp('(matr|vert|ind)ix|ex$', 'gi'), '$1ices'], + [new RegExp('([m|l])ouse$', 'gi'), '$1ice'], + [new RegExp('(quiz)$', 'gi'), '$1zes'], + [new RegExp('s$', 'gi'), 's'], + [new RegExp('$', 'gi'), 's'] + ], + + /* + These rules translate from the plural form of a noun to its singular form. + */ + singular_rules: [ + [new RegExp('(m)en$', 'gi'), '$1an'], + [new RegExp('(pe)ople$', 'gi'), '$1rson'], + [new RegExp('(child)ren$', 'gi'), '$1'], + [new RegExp('([ti])a$', 'gi'), '$1um'], + [new RegExp('((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$','gi'), '$1$2sis'], + [new RegExp('(hive)s$', 'gi'), '$1'], + [new RegExp('(tive)s$', 'gi'), '$1'], + [new RegExp('(curve)s$', 'gi'), '$1'], + [new RegExp('([lr])ves$', 'gi'), '$1f'], + [new RegExp('([^fo])ves$', 'gi'), '$1fe'], + [new RegExp('([^aeiouy]|qu)ies$', 'gi'), '$1y'], + [new RegExp('(s)eries$', 'gi'), '$1eries'], + [new RegExp('(m)ovies$', 'gi'), '$1ovie'], + [new RegExp('(x|ch|ss|sh)es$', 'gi'), '$1'], + [new RegExp('([m|l])ice$', 'gi'), '$1ouse'], + [new RegExp('(bus)es$', 'gi'), '$1'], + [new RegExp('(o)es$', 'gi'), '$1'], + [new RegExp('(shoe)s$', 'gi'), '$1'], + [new RegExp('(cris|ax|test)es$', 'gi'), '$1is'], + [new RegExp('(octop|vir)i$', 'gi'), '$1us'], + [new RegExp('(alias|status)es$', 'gi'), '$1'], + [new RegExp('^(ox)en', 'gi'), '$1'], + [new RegExp('(vert|ind)ices$', 'gi'), '$1ex'], + [new RegExp('(matr)ices$', 'gi'), '$1ix'], + [new RegExp('(quiz)zes$', 'gi'), '$1'], + [new RegExp('s$', 'gi'), ''] + ], + + /* + This is a list of words that should not be capitalized for title case + */ + non_titlecased_words: [ + 'and', 'or', 'nor', 'a', 'an', 'the', 'so', 'but', 'to', 'of', 'at', + 'by', 'from', 'into', 'on', 'onto', 'off', 'out', 'in', 'over', + 'with', 'for' + ], + + /* + These are regular expressions used for converting between String formats + */ + id_suffix: new RegExp('(_ids|_id)$', 'g'), + underbar: new RegExp('_', 'g'), + space_or_underbar: new RegExp('[\ _]', 'g'), + uppercase: new RegExp('([A-Z])', 'g'), + underbar_prefix: new RegExp('^_'), + + /* + This is a helper method that applies rules based replacement to a String + Signature: + InflectionJS.apply_rules(str, rules, skip, override) == String + Arguments: + str - String - String to modify and return based on the passed rules + rules - Array: [RegExp, String] - Regexp to match paired with String to use for replacement + skip - Array: [String] - Strings to skip if they match + override - String (optional) - String to return as though this method succeeded (used to conform to APIs) + Returns: + String - passed String modified by passed rules + Examples: + InflectionJS.apply_rules("cows", InflectionJs.singular_rules) === 'cow' + */ + apply_rules: function(str, rules, skip, override) + { + if (override) + { + str = override; + } + else + { + var ignore = (skip.indexOf(str.toLowerCase()) > -1); + if (!ignore) + { + for (var x = 0; x < rules.length; x++) + { + if (str.match(rules[x][0])) + { + str = str.replace(rules[x][0], rules[x][1]); + break; + } + } + } + } + return str; + } +}; + +/* + This lets us detect if an Array contains a given element + Signature: + Array.indexOf(item, fromIndex, compareFunc) == Integer + Arguments: + item - Object - object to locate in the Array + fromIndex - Integer (optional) - starts checking from this position in the Array + compareFunc - Function (optional) - function used to compare Array item vs passed item + Returns: + Integer - index position in the Array of the passed item + Examples: + ['hi','there'].indexOf("guys") === -1 + ['hi','there'].indexOf("hi") === 0 +*/ +if (!Array.prototype.indexOf) +{ + Array.prototype.indexOf = function(item, fromIndex, compareFunc) + { + if (!fromIndex) + { + fromIndex = -1; + } + var index = -1; + for (var i = fromIndex; i < this.length; i++) + { + if (this[i] === item || compareFunc && compareFunc(this[i], item)) + { + index = i; + break; + } + } + return index; + }; +} + +/* + You can override this list for all Strings or just one depending on if you + set the new values on prototype or on a given String instance. +*/ +if (!String.prototype._uncountable_words) +{ + String.prototype._uncountable_words = InflectionJS.uncountable_words; +} + +/* + You can override this list for all Strings or just one depending on if you + set the new values on prototype or on a given String instance. +*/ +if (!String.prototype._plural_rules) +{ + String.prototype._plural_rules = InflectionJS.plural_rules; +} + +/* + You can override this list for all Strings or just one depending on if you + set the new values on prototype or on a given String instance. +*/ +if (!String.prototype._singular_rules) +{ + String.prototype._singular_rules = InflectionJS.singular_rules; +} + +/* + You can override this list for all Strings or just one depending on if you + set the new values on prototype or on a given String instance. +*/ +if (!String.prototype._non_titlecased_words) +{ + String.prototype._non_titlecased_words = InflectionJS.non_titlecased_words; +} + +/* + This function adds plurilization support to every String object + Signature: + String.pluralize(plural) == String + Arguments: + plural - String (optional) - overrides normal output with said String + Returns: + String - singular English language nouns are returned in plural form + Examples: + "person".pluralize() == "people" + "octopus".pluralize() == "octopi" + "Hat".pluralize() == "Hats" + "person".pluralize("guys") == "guys" +*/ +if (!String.prototype.pluralize) +{ + String.prototype.pluralize = function(plural) + { + return InflectionJS.apply_rules( + this, + this._plural_rules, + this._uncountable_words, + plural + ); + }; +} + +/* + This function adds singularization support to every String object + Signature: + String.singularize(singular) == String + Arguments: + singular - String (optional) - overrides normal output with said String + Returns: + String - plural English language nouns are returned in singular form + Examples: + "people".singularize() == "person" + "octopi".singularize() == "octopus" + "Hats".singularize() == "Hat" + "guys".singularize("person") == "person" +*/ +if (!String.prototype.singularize) +{ + String.prototype.singularize = function(singular) + { + return InflectionJS.apply_rules( + this, + this._singular_rules, + this._uncountable_words, + singular + ); + }; +} + +/* + This function adds camelization support to every String object + Signature: + String.camelize(lowFirstLetter) == String + Arguments: + lowFirstLetter - boolean (optional) - default is to capitalize the first + letter of the results... passing true will lowercase it + Returns: + String - lower case underscored words will be returned in camel case + additionally '/' is translated to '::' + Examples: + "message_properties".camelize() == "MessageProperties" + "message_properties".camelize(true) == "messageProperties" +*/ +if (!String.prototype.camelize) +{ + String.prototype.camelize = function(lowFirstLetter) + { + var str = this.toLowerCase(); + var str_path = str.split('/'); + for (var i = 0; i < str_path.length; i++) + { + var str_arr = str_path[i].split('_'); + var initX = ((lowFirstLetter && i + 1 === str_path.length) ? (1) : (0)); + for (var x = initX; x < str_arr.length; x++) + { + str_arr[x] = str_arr[x].charAt(0).toUpperCase() + str_arr[x].substring(1); + } + str_path[i] = str_arr.join(''); + } + str = str_path.join('::'); + return str; + }; +} + +/* + This function adds underscore support to every String object + Signature: + String.underscore() == String + Arguments: + N/A + Returns: + String - camel cased words are returned as lower cased and underscored + additionally '::' is translated to '/' + Examples: + "MessageProperties".camelize() == "message_properties" + "messageProperties".underscore() == "message_properties" +*/ +if (!String.prototype.underscore) +{ + String.prototype.underscore = function() + { + var str = this; + var str_path = str.split('::'); + for (var i = 0; i < str_path.length; i++) + { + str_path[i] = str_path[i].replace(InflectionJS.uppercase, '_$1'); + str_path[i] = str_path[i].replace(InflectionJS.underbar_prefix, ''); + } + str = str_path.join('/').toLowerCase(); + return str; + }; +} + +/* + This function adds humanize support to every String object + Signature: + String.humanize(lowFirstLetter) == String + Arguments: + lowFirstLetter - boolean (optional) - default is to capitalize the first + letter of the results... passing true will lowercase it + Returns: + String - lower case underscored words will be returned in humanized form + Examples: + "message_properties".humanize() == "Message properties" + "message_properties".humanize(true) == "message properties" +*/ +if (!String.prototype.humanize) +{ + String.prototype.humanize = function(lowFirstLetter) + { + var str = this.toLowerCase(); + str = str.replace(InflectionJS.id_suffix, ''); + str = str.replace(InflectionJS.underbar, ' '); + if (!lowFirstLetter) + { + str = str.capitalize(); + } + return str; + }; +} + +/* + This function adds capitalization support to every String object + Signature: + String.capitalize() == String + Arguments: + N/A + Returns: + String - all characters will be lower case and the first will be upper + Examples: + "message_properties".capitalize() == "Message_properties" + "message properties".capitalize() == "Message properties" +*/ +if (!String.prototype.capitalize) +{ + String.prototype.capitalize = function() + { + var str = this.toLowerCase(); + str = str.substring(0, 1).toUpperCase() + str.substring(1); + return str; + }; +} + +/* + This function adds dasherization support to every String object + Signature: + String.dasherize() == String + Arguments: + N/A + Returns: + String - replaces all spaces or underbars with dashes + Examples: + "message_properties".capitalize() == "message-properties" + "Message Properties".capitalize() == "Message-Properties" +*/ +if (!String.prototype.dasherize) +{ + String.prototype.dasherize = function() + { + var str = this; + str = str.replace(InflectionJS.space_or_underbar, '-'); + return str; + }; +} + +/* + This function adds titleize support to every String object + Signature: + String.titleize() == String + Arguments: + N/A + Returns: + String - capitalizes words as you would for a book title + Examples: + "message_properties".titleize() == "Message Properties" + "message properties to keep".titleize() == "Message Properties to Keep" +*/ +if (!String.prototype.titleize) +{ + String.prototype.titleize = function() + { + var str = this.toLowerCase(); + str = str.replace(InflectionJS.underbar, ' '); + var str_arr = str.split(' '); + for (var x = 0; x < str_arr.length; x++) + { + var d = str_arr[x].split('-'); + for (var i = 0; i < d.length; i++) + { + if (this._non_titlecased_words.indexOf(d[i].toLowerCase()) < 0) + { + d[i] = d[i].capitalize(); + } + } + str_arr[x] = d.join('-'); + } + str = str_arr.join(' '); + str = str.substring(0, 1).toUpperCase() + str.substring(1); + return str; + }; +} + +/* + This function adds demodulize support to every String object + Signature: + String.demodulize() == String + Arguments: + N/A + Returns: + String - removes module names leaving only class names (Ruby style) + Examples: + "Message::Bus::Properties".demodulize() == "Properties" +*/ +if (!String.prototype.demodulize) +{ + String.prototype.demodulize = function() + { + var str = this; + var str_arr = str.split('::'); + str = str_arr[str_arr.length - 1]; + return str; + }; +} + +/* + This function adds tableize support to every String object + Signature: + String.tableize() == String + Arguments: + N/A + Returns: + String - renders camel cased words into their underscored plural form + Examples: + "MessageBusProperty".tableize() == "message_bus_properties" +*/ +if (!String.prototype.tableize) +{ + String.prototype.tableize = function() + { + var str = this; + str = str.underscore().pluralize(); + return str; + }; +} + +/* + This function adds classification support to every String object + Signature: + String.classify() == String + Arguments: + N/A + Returns: + String - underscored plural nouns become the camel cased singular form + Examples: + "message_bus_properties".classify() == "MessageBusProperty" +*/ +if (!String.prototype.classify) +{ + String.prototype.classify = function() + { + var str = this; + str = str.camelize().singularize(); + return str; + }; +} + +/* + This function adds foreign key support to every String object + Signature: + String.foreign_key(dropIdUbar) == String + Arguments: + dropIdUbar - boolean (optional) - default is to seperate id with an + underbar at the end of the class name, you can pass true to skip it + Returns: + String - camel cased singular class names become underscored with id + Examples: + "MessageBusProperty".foreign_key() == "message_bus_property_id" + "MessageBusProperty".foreign_key(true) == "message_bus_propertyid" +*/ +if (!String.prototype.foreign_key) +{ + String.prototype.foreign_key = function(dropIdUbar) + { + var str = this; + str = str.demodulize().underscore() + ((dropIdUbar) ? ('') : ('_')) + 'id'; + return str; + }; +} + +/* + This function adds ordinalize support to every String object + Signature: + String.ordinalize() == String + Arguments: + N/A + Returns: + String - renders all found numbers their sequence like "22nd" + Examples: + "the 1 pitch".ordinalize() == "the 1st pitch" +*/ +if (!String.prototype.ordinalize) +{ + String.prototype.ordinalize = function() + { + var str = this; + var str_arr = str.split(' '); + for (var x = 0; x < str_arr.length; x++) + { + var i = parseInt(str_arr[x]); + if (i === NaN) + { + var ltd = str_arr[x].substring(str_arr[x].length - 2); + var ld = str_arr[x].substring(str_arr[x].length - 1); + var suf = "th"; + if (ltd != "11" && ltd != "12" && ltd != "13") + { + if (ld === "1") + { + suf = "st"; + } + else if (ld === "2") + { + suf = "nd"; + } + else if (ld === "3") + { + suf = "rd"; + } + } + str_arr[x] += suf; + } + } + str = str_arr.join(' '); + return str; + }; +}