Commit 3923fd96 by jasl

first commit

parents
# This file is for unifying the coding style for different editors and IDEs
# editorconfig.org
root = true
[*]
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 2
end_of_line = lf
[*.md]
trim_trailing_whitespace = true
*.rb diff=ruby
*.gemspec diff=ruby
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
#
# If you find yourself ignoring temporary files generated by your text editor
# or operating system, you probably want to add a global ignore instead:
# git config --global core.excludesfile '~/.gitignore_global'
# Ignore bundler config.
.bundle/
# Ignore the default SQLite database.
test/dummy/db/*.sqlite3
test/dummy/db/*.sqlite3-journal
# Ignore all logfiles and tempfiles.
log/*.log
!test/dummy/log/.keep
!test/dummy/tmp/.keep
test/dummy/log/*.log
test/dummy/tmp/
# Ignore uploaded files in development
test/dummy/storage/*
!test/dummy/storage/.keep
pkg/
.byebug_history
node_modules/
test/dummy/public/packs
test/dummy/node_modules/
yarn-error.log
*.gem
inherit_gem:
rubocop-rails_config:
- config/rails.yml
AllCops:
TargetRubyVersion: 2.4
Exclude:
- 'test/dummy/db/schema.rb'
# frozen_string_literal: true
Style/FrozenStringLiteralComment:
Enabled: true
EnforcedStyle: when_needed
# Prefer &&/|| over and/or.
Style/AndOr:
Enabled: true
# Do not use braces for hash literals when they are the last argument of a
# method call.
Style/BracesAroundHashParameters:
Enabled: true
# Align `when` with `case`.
Layout/CaseIndentation:
Enabled: true
# Align comments with method definitions.
Layout/CommentIndentation:
Enabled: true
# No extra empty lines.
Layout/EmptyLines:
Enabled: true
# In a regular class definition, no empty lines around the body.
Layout/EmptyLinesAroundClassBody:
Enabled: true
# In a regular method definition, no empty lines around the body.
Layout/EmptyLinesAroundMethodBody:
Enabled: true
# In a regular module definition, no empty lines around the body.
Layout/EmptyLinesAroundModuleBody:
Enabled: true
# Use Ruby >= 1.9 syntax for hashes. Prefer {a: :b} over { :a => :b }.
Style/HashSyntax:
Enabled: true
# Method definitions after `private` or `protected` isolated calls need one
# extra level of indentation.
Layout/IndentationConsistency:
Enabled: true
EnforcedStyle: normal
# Two spaces, no tabs (for indentation).
Layout/IndentationWidth:
Enabled: true
Layout/SpaceAfterColon:
Enabled: true
Layout/SpaceAfterComma:
Enabled: true
Layout/SpaceAroundEqualsInParameterDefault:
Enabled: true
Layout/SpaceAroundKeyword:
Enabled: true
Layout/SpaceAroundOperators:
Enabled: true
Layout/SpaceBeforeFirstArg:
Enabled: true
# Defining a method with parameters needs parentheses.
Style/MethodDefParentheses:
Enabled: true
# Use `foo {}` not `foo{}`.
Layout/SpaceBeforeBlockBraces:
Enabled: true
# Use `foo { bar }` not `foo {bar}`.
Layout/SpaceInsideBlockBraces:
Enabled: true
# Use `{a: 1}` not `{ a:1 }`.
Layout/SpaceInsideHashLiteralBraces:
Enabled: false
Layout/SpaceInsideParens:
Enabled: true
# Check quotes usage according to lint rule below.
Style/StringLiterals:
Enabled: true
EnforcedStyle: double_quotes
# Detect hard tabs, no hard tabs.
Layout/Tab:
Enabled: true
# Blank lines should not have any spaces.
Layout/TrailingBlankLines:
Enabled: true
# No trailing whitespace.
Layout/TrailingWhitespace:
Enabled: true
# Use quotes for string literals when they are enough.
Style/UnneededPercentQ:
Enabled: true
# Align `end` with the matching keyword or starting expression except for
# assignments, where it should be aligned with the LHS.
Layout/EndAlignment:
Enabled: true
EnforcedStyleAlignWith: variable
# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
Lint/RequireParentheses:
Enabled: true
ruby-2.5.1
# frozen_string_literal: true
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
# Declare your gem's dependencies in workflow_core.gemspec.
# Bundler will treat runtime dependencies like base dependencies, and
# development dependencies will be added by default to the :development group.
gemspec
# Declare any dependencies that are still in development here instead of in
# your gemspec. These might include edge Rails or gems from your path or
# Git. Remember to move these dependencies to your gemspec before releasing
# your gem to rubygems.org.
# To use a debugger
# gem 'byebug', group: [:development, :test]
gem "sqlite3"
# Use Puma as the app server
gem "puma"
# For better console experience
gem "pry-rails"
# Access an IRB console on exception pages or by using <%= console %> anywhere in the code.
gem "web-console"
gem "listen", ">= 3.0.5", "< 3.2"
# Call "byebug" anywhere in the code to stop execution and get a debugger console
gem "pry-byebug"
gem "better_errors"
gem "binding_of_caller"
# To support ES6
gem "sprockets", "~> 4.0.0.beta4"
# Support ES6
gem "babel-transpiler"
# Use SCSS for stylesheets
gem "sass-rails"
# Use Uglifier as compressor for JavaScript assets
gem "uglifier", ">= 1.3.0"
gem "jquery-rails"
gem "turbolinks"
gem "selectize-rails"
gem "bulma-rails"
gem "rubocop"
gem "rubocop-rails_config"
gem "form_core"
gem "duck_record"
gem "closure_tree"
gem "cocoon"
gem "script_core", github: "rails-engine/script_core", submodules: true
gem "graphviz"
GIT
remote: https://github.com/rails-engine/script_core.git
revision: f086b63c03061732c8a760b97d54a9d2e7cbec68
submodules: true
specs:
script_core (0.0.4)
msgpack (~> 1.0)
rake-compiler (~> 1.0)
PATH
remote: .
specs:
workflow_core (0.0.1)
rails (~> 5.2)
GEM
remote: https://rubygems.org/
specs:
actioncable (5.2.1)
actionpack (= 5.2.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailer (5.2.1)
actionpack (= 5.2.1)
actionview (= 5.2.1)
activejob (= 5.2.1)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (5.2.1)
actionview (= 5.2.1)
activesupport (= 5.2.1)
rack (~> 2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.2.1)
activesupport (= 5.2.1)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
activejob (5.2.1)
activesupport (= 5.2.1)
globalid (>= 0.3.6)
activemodel (5.2.1)
activesupport (= 5.2.1)
activerecord (5.2.1)
activemodel (= 5.2.1)
activesupport (= 5.2.1)
arel (>= 9.0)
activestorage (5.2.1)
actionpack (= 5.2.1)
activerecord (= 5.2.1)
marcel (~> 0.3.1)
activesupport (5.2.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
arel (9.0.0)
ast (2.4.0)
babel-source (5.8.35)
babel-transpiler (0.7.0)
babel-source (>= 4.0, < 6)
execjs (~> 2.0)
better_errors (2.5.0)
coderay (>= 1.0.0)
erubi (>= 1.0.0)
rack (>= 0.9.0)
bindex (0.5.0)
binding_of_caller (0.8.0)
debug_inspector (>= 0.0.1)
builder (3.2.3)
bulma-rails (0.7.1)
sass (~> 3.2)
byebug (10.0.2)
closure_tree (7.0.0)
activerecord (>= 4.2.10)
with_advisory_lock (>= 4.0.0)
cocoon (1.2.11)
coderay (1.1.2)
concurrent-ruby (1.0.5)
crass (1.0.4)
debug_inspector (0.0.3)
duck_record (0.0.26)
activemodel (~> 5.0)
activesupport (~> 5.0)
erubi (1.7.1)
execjs (2.7.0)
ffi (1.9.25)
form_core (0.0.14)
duck_record (~> 0)
rails (~> 5.0)
globalid (0.4.1)
activesupport (>= 4.2.0)
graphviz (1.1.0)
process-pipeline
i18n (1.1.0)
concurrent-ruby (~> 1.0)
jaro_winkler (1.5.1)
jquery-rails (4.3.3)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
ruby_dep (~> 1.2)
loofah (2.2.2)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.0)
mini_mime (>= 0.1.1)
marcel (0.3.3)
mimemagic (~> 0.3.2)
method_source (0.9.0)
mimemagic (0.3.2)
mini_mime (1.0.1)
mini_portile2 (2.3.0)
minitest (5.11.3)
msgpack (1.2.4)
nio4r (2.3.1)
nokogiri (1.8.4)
mini_portile2 (~> 2.3.0)
parallel (1.12.1)
parser (2.5.1.2)
ast (~> 2.4.0)
powerpack (0.1.2)
process-group (1.1.0)
process-pipeline (1.0.1)
process-group
pry (0.11.3)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
pry-byebug (3.6.0)
byebug (~> 10.0)
pry (~> 0.10)
pry-rails (0.3.6)
pry (>= 0.10.4)
puma (3.12.0)
rack (2.0.5)
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (5.2.1)
actioncable (= 5.2.1)
actionmailer (= 5.2.1)
actionpack (= 5.2.1)
actionview (= 5.2.1)
activejob (= 5.2.1)
activemodel (= 5.2.1)
activerecord (= 5.2.1)
activestorage (= 5.2.1)
activesupport (= 5.2.1)
bundler (>= 1.3.0)
railties (= 5.2.1)
sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.0.4)
loofah (~> 2.2, >= 2.2.2)
railties (5.2.1)
actionpack (= 5.2.1)
activesupport (= 5.2.1)
method_source
rake (>= 0.8.7)
thor (>= 0.19.0, < 2.0)
rainbow (3.0.0)
rake (12.3.1)
rake-compiler (1.0.5)
rake
rb-fsevent (0.10.3)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
rubocop (0.59.2)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
parser (>= 2.5, != 2.5.1.1)
powerpack (~> 0.1)
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
rubocop-rails_config (0.2.3)
railties (>= 3.0)
rubocop (~> 0.56)
ruby-progressbar (1.10.0)
ruby_dep (1.5.0)
sass (3.6.0)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sass-rails (5.0.7)
railties (>= 4.0.0, < 6)
sass (~> 3.1)
sprockets (>= 2.8, < 4.0)
sprockets-rails (>= 2.0, < 4.0)
tilt (>= 1.1, < 3)
selectize-rails (0.12.5)
sprockets (4.0.0.beta8)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.1)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
sqlite3 (1.3.13)
thor (0.20.0)
thread_safe (0.3.6)
tilt (2.0.8)
turbolinks (5.2.0)
turbolinks-source (~> 5.2)
turbolinks-source (5.2.0)
tzinfo (1.2.5)
thread_safe (~> 0.1)
uglifier (4.1.19)
execjs (>= 0.3.0, < 3)
unicode-display_width (1.4.0)
web-console (3.7.0)
actionview (>= 5.0)
activemodel (>= 5.0)
bindex (>= 0.4.0)
railties (>= 5.0)
websocket-driver (0.7.0)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.3)
with_advisory_lock (4.0.0)
activerecord (>= 4.2)
PLATFORMS
ruby
DEPENDENCIES
babel-transpiler
better_errors
binding_of_caller
bulma-rails
closure_tree
cocoon
duck_record
form_core
graphviz
jquery-rails
listen (>= 3.0.5, < 3.2)
pry-byebug
pry-rails
puma
rubocop
rubocop-rails_config
sass-rails
script_core!
selectize-rails
sprockets (~> 4.0.0.beta4)
sqlite3
turbolinks
uglifier (>= 1.3.0)
web-console
workflow_core!
BUNDLED WITH
1.16.4
Copyright 2018 Jun Jiang
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.
Workflow Core
====
> WorkflowCore is under development, the codebase is unoptimized and has many bad practices, I may do breaking changes even force pushing to master branch.
>
> In short, it's not ready yet, but I realize that a workflow engine is complicated to design and it needs a long term to done well, so I decide to open source at early stage.
>
> Any way, feedbacks and suggestions are highly welcome!
A Rails engine which providing essential infrastructure of workflow.
It's based on [Workflow Net](http://mlwiki.org/index.php/Workflow_Nets) technique.
The gem provides:
- Models to describe workflow nets
- Models to describe workflow instances
- Interfaces to define transitions
## Why “core”
Because it's not aim to "out-of-box", some gem like Devise giving developer an out-of-box experience, that's awesome, but on the other hand, it also introducing a very complex abstraction that may hard to understanding how it works, especially when you attempting to customize it.
I believe that the gem is tightly coupled with features that face to end users directly, so having a good customizability and easy to understanding are of the most concern, so I just wanna give you a domain framework that you can build your own that just fitting your need, and you shall have fully control and without any unnecessary abstraction.
BTW, the dummy app is a full-featured app with production level codebase that you can freely to reference it.
## Todo
- Make sure transit must be an atomic operation, and help developers to avoiding Rails' nested transaction pitfalls.
- Consider consequences of changing the Net, well handle Net changes. e.g: what about running instances?
- Stabilizing interfaces.
- Evaluate that can supporting async, scheduled and event-based transition properly.
- Efficiency (especially database queries).
- Easy to use.
- Transforming to graph representation for visualization and other usages (e.g proving [Soundness](http://mlwiki.org/index.php/Workflow_Soundness)).
- Polish codebase.
- Continually improving dummy app.
## Requirements
- MRI 2.3+
- Rails 5.0+
## Usage
See demo for now.
## Installation
Add this line to your Gemfile:
```ruby
gem 'workflow_core'
```
Or you may want to include the gem directly from GitHub:
```ruby
gem 'workflow_core', github: 'rails-engine/workflow_core'
```
And then execute:
```sh
$ bundle
```
Copy migrations
```sh
$ bin/rails workflow_core:install:migrations
```
Then do migrate
```sh
$ bin/rails db:migrate
```
## Demo
**Demo is also under development.**
The dummy app integrates with [Form Core](https://github.com/rails-engine/form_core) and [Script Core](https://github.com/rails-engine/script_core) shows an Approving Manage System.
![](_assets/dummy_overview.png)
### Features
#### Importing workflow definitions from a BPMN2 xml,
![](_assets/importing_bpmn.png)
Because there isn't have a easy-to-use web-based flowchart designer, I implement a stupid BPMN2 importer, it have many restrictions:
- Only supports `Sequence`, `Start event`, `End event`, `Parallel gateway` and `Exclusive gateway`
- Using gateway to fork flows must have corresponding join (or merge) gateway
- Only read `name` property, other such as `condition expression` must configure on the dummy app
You can check `_samples` folder, I've already provided some samples, or you can try a BPMN2 designer (e.g [Camunda modeler](https://github.com/camunda/camunda-modeler)).
#### Defining form
![](_assets/defining_form.png)
You can defining a dynamic form for a workflow.
In transition's options, you can configure field's accessibility
#### Exclusive choice configuration supports Ruby expression
![](_assets/editing_transition.png)
Exclusive choice is a special transition that needs to configure conditions that determine how to transit to a branch.
The condition is a Ruby expression, and running in a mRuby sandbox (powered by ScriptCore but it's also undone yet), and you can access form data through `@input[:payload]`, for example, there is a field named `approved`, we can check the field checked by `@input[:payload]["approved"]`
#### Run a workflow
See `Instance` tab, that should make sense.
### Usage
**You need install Graphviz first**
Clone the repository.
```sh
$ git clone https://github.com/rails-engine/workflow_core.git
```
Change directory
```sh
$ cd workflow_core
```
Run bundler
```sh
$ bundle install
```
Preparing database
```sh
$ bin/rails db:migrate
```
Start the Rails server
```sh
$ bin/rails s
```
Open your browser, and visit `http://localhost:3000`
## Contributing
Bug report or pull request are welcome.
### Make a pull request
1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create new Pull Request
Please write unit test with your code if necessary.
## License
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
# frozen_string_literal: true
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 = "WorkflowCore"
rdoc.options << "--line-numbers"
rdoc.rdoc_files.include("README.md")
rdoc.rdoc_files.include("lib/**/*.rb")
end
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
load "rails/tasks/engine.rake"
load "rails/tasks/statistics.rake"
require "bundler/gem_tasks"
require "rake/testtask"
Rake::TestTask.new(:test) do |t|
t.libs << "test"
t.pattern = "test/**/*_test.rb"
t.verbose = false
end
task default: :test
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" id="Definitions_1ao30nf" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="2.0.1">
<bpmn:process id="Process_1" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_0z87m50</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:task id="Task_1jpb9fm" name="Step 1">
<bpmn:incoming>SequenceFlow_0z87m50</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1ph0afh</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_0z87m50" sourceRef="StartEvent_1" targetRef="Task_1jpb9fm" />
<bpmn:task id="Task_0rnfiox" name="Step 2">
<bpmn:incoming>SequenceFlow_1ph0afh</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1h0vowt</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_1ph0afh" sourceRef="Task_1jpb9fm" targetRef="Task_0rnfiox" />
<bpmn:endEvent id="EndEvent_0d80vbz">
<bpmn:incoming>SequenceFlow_1h0vowt</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_1h0vowt" sourceRef="Task_0rnfiox" targetRef="EndEvent_0d80vbz" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="173" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_1jpb9fm_di" bpmnElement="Task_1jpb9fm">
<dc:Bounds x="259" y="80" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0z87m50_di" bpmnElement="SequenceFlow_0z87m50">
<di:waypoint x="209" y="120" />
<di:waypoint x="259" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Task_0rnfiox_di" bpmnElement="Task_0rnfiox">
<dc:Bounds x="409" y="80" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_1ph0afh_di" bpmnElement="SequenceFlow_1ph0afh">
<di:waypoint x="359" y="120" />
<di:waypoint x="409" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="EndEvent_0d80vbz_di" bpmnElement="EndEvent_0d80vbz">
<dc:Bounds x="559" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_1h0vowt_di" bpmnElement="SequenceFlow_1h0vowt">
<di:waypoint x="509" y="120" />
<di:waypoint x="559" y="120" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" id="Definitions_12bthnl" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="2.0.3">
<bpmn:process id="Process_1" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_0ihulyl</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:task id="Task_0f5ie7w" name="Step 1">
<bpmn:incoming>SequenceFlow_0ihulyl</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1wljg2h</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_0ihulyl" sourceRef="StartEvent_1" targetRef="Task_0f5ie7w" />
<bpmn:sequenceFlow id="SequenceFlow_1wljg2h" sourceRef="Task_0f5ie7w" targetRef="ExclusiveGateway_01sqwfm" />
<bpmn:parallelGateway id="ExclusiveGateway_01sqwfm" name="Parallel Split">
<bpmn:incoming>SequenceFlow_1wljg2h</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_190bkby</bpmn:outgoing>
<bpmn:outgoing>SequenceFlow_03lhyba</bpmn:outgoing>
</bpmn:parallelGateway>
<bpmn:task id="Task_0ac7qx7" name="Step 2a">
<bpmn:incoming>SequenceFlow_190bkby</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_047f1j5</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_190bkby" sourceRef="ExclusiveGateway_01sqwfm" targetRef="Task_0ac7qx7" />
<bpmn:task id="Task_0ym8z2m" name="Step 2b">
<bpmn:incoming>SequenceFlow_03lhyba</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0rl07dr</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_03lhyba" sourceRef="ExclusiveGateway_01sqwfm" targetRef="Task_0ym8z2m" />
<bpmn:sequenceFlow id="SequenceFlow_047f1j5" sourceRef="Task_0ac7qx7" targetRef="ExclusiveGateway_1ni4fs4" />
<bpmn:parallelGateway id="ExclusiveGateway_1ni4fs4" name="Synchronization">
<bpmn:incoming>SequenceFlow_047f1j5</bpmn:incoming>
<bpmn:incoming>SequenceFlow_0rl07dr</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1a0tblh</bpmn:outgoing>
</bpmn:parallelGateway>
<bpmn:sequenceFlow id="SequenceFlow_0rl07dr" sourceRef="Task_0ym8z2m" targetRef="ExclusiveGateway_1ni4fs4" />
<bpmn:task id="Task_0h6xqca" name="Step 3">
<bpmn:incoming>SequenceFlow_1a0tblh</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_18vdzfl</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_1a0tblh" sourceRef="ExclusiveGateway_1ni4fs4" targetRef="Task_0h6xqca" />
<bpmn:endEvent id="EndEvent_0lta3qi">
<bpmn:incoming>SequenceFlow_18vdzfl</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_18vdzfl" sourceRef="Task_0h6xqca" targetRef="EndEvent_0lta3qi" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="173" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_0f5ie7w_di" bpmnElement="Task_0f5ie7w">
<dc:Bounds x="259" y="80" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0ihulyl_di" bpmnElement="SequenceFlow_0ihulyl">
<di:waypoint x="209" y="120" />
<di:waypoint x="259" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1wljg2h_di" bpmnElement="SequenceFlow_1wljg2h">
<di:waypoint x="359" y="120" />
<di:waypoint x="409" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="ParallelGateway_0ukzmc6_di" bpmnElement="ExclusiveGateway_01sqwfm">
<dc:Bounds x="409" y="95" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="484" y="110" width="62" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_0ac7qx7_di" bpmnElement="Task_0ac7qx7">
<dc:Bounds x="509" y="-19" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_190bkby_di" bpmnElement="SequenceFlow_190bkby">
<di:waypoint x="434" y="95" />
<di:waypoint x="434" y="21" />
<di:waypoint x="509" y="21" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Task_0ym8z2m_di" bpmnElement="Task_0ym8z2m">
<dc:Bounds x="509" y="190" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_03lhyba_di" bpmnElement="SequenceFlow_03lhyba">
<di:waypoint x="434" y="145" />
<di:waypoint x="434" y="230" />
<di:waypoint x="509" y="230" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_047f1j5_di" bpmnElement="SequenceFlow_047f1j5">
<di:waypoint x="609" y="21" />
<di:waypoint x="685" y="21" />
<di:waypoint x="685" y="95" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="ParallelGateway_14q60vy_di" bpmnElement="ExclusiveGateway_1ni4fs4">
<dc:Bounds x="660" y="95" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="566" y="110" width="79" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0rl07dr_di" bpmnElement="SequenceFlow_0rl07dr">
<di:waypoint x="609" y="230" />
<di:waypoint x="685" y="230" />
<di:waypoint x="685" y="145" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Task_0h6xqca_di" bpmnElement="Task_0h6xqca">
<dc:Bounds x="761" y="80" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_1a0tblh_di" bpmnElement="SequenceFlow_1a0tblh">
<di:waypoint x="710" y="120" />
<di:waypoint x="761" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="EndEvent_0lta3qi_di" bpmnElement="EndEvent_0lta3qi">
<dc:Bounds x="912" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_18vdzfl_di" bpmnElement="SequenceFlow_18vdzfl">
<di:waypoint x="861" y="120" />
<di:waypoint x="912" y="120" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" id="Definitions_0w9rj1f" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="2.0.3">
<bpmn:process id="Process_1" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_0qzo3lb</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:task id="Task_07nyace" name="Step 1">
<bpmn:incoming>SequenceFlow_0qzo3lb</bpmn:incoming>
<bpmn:incoming>SequenceFlow_1fr8c9x</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0v54qlb</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_0qzo3lb" sourceRef="StartEvent_1" targetRef="Task_07nyace" />
<bpmn:exclusiveGateway id="ExclusiveGateway_1jpd3ln">
<bpmn:incoming>SequenceFlow_0v54qlb</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1s8t845</bpmn:outgoing>
<bpmn:outgoing>SequenceFlow_1fr8c9x</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="SequenceFlow_0v54qlb" sourceRef="Task_07nyace" targetRef="ExclusiveGateway_1jpd3ln" />
<bpmn:task id="Task_0ym21fx" name="Step 2">
<bpmn:incoming>SequenceFlow_1s8t845</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0nzmb8s</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_1s8t845" sourceRef="ExclusiveGateway_1jpd3ln" targetRef="Task_0ym21fx" />
<bpmn:endEvent id="EndEvent_1jlbp8k">
<bpmn:incoming>SequenceFlow_0nzmb8s</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_0nzmb8s" sourceRef="Task_0ym21fx" targetRef="EndEvent_1jlbp8k" />
<bpmn:sequenceFlow id="SequenceFlow_1fr8c9x" sourceRef="ExclusiveGateway_1jpd3ln" targetRef="Task_07nyace" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="173" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_07nyace_di" bpmnElement="Task_07nyace">
<dc:Bounds x="259" y="80" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0qzo3lb_di" bpmnElement="SequenceFlow_0qzo3lb">
<di:waypoint x="209" y="120" />
<di:waypoint x="259" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="ExclusiveGateway_1jpd3ln_di" bpmnElement="ExclusiveGateway_1jpd3ln" isMarkerVisible="true">
<dc:Bounds x="409" y="95" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0v54qlb_di" bpmnElement="SequenceFlow_0v54qlb">
<di:waypoint x="359" y="120" />
<di:waypoint x="409" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Task_0ym21fx_di" bpmnElement="Task_0ym21fx">
<dc:Bounds x="509" y="80" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_1s8t845_di" bpmnElement="SequenceFlow_1s8t845">
<di:waypoint x="459" y="120" />
<di:waypoint x="509" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="EndEvent_1jlbp8k_di" bpmnElement="EndEvent_1jlbp8k">
<dc:Bounds x="659" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0nzmb8s_di" bpmnElement="SequenceFlow_0nzmb8s">
<di:waypoint x="609" y="120" />
<di:waypoint x="659" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1fr8c9x_di" bpmnElement="SequenceFlow_1fr8c9x">
<di:waypoint x="434" y="120" />
<di:waypoint x="434" y="30" />
<di:waypoint x="309" y="30" />
<di:waypoint x="309" y="80" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
# frozen_string_literal: true
module WorkflowCore
class ApplicationJob < ActiveJob::Base
end
end
# frozen_string_literal: true
module WorkflowCore
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
end
# frozen_string_literal: true
module WorkflowCore
class Place < ApplicationRecord
self.table_name = "workflow_places"
belongs_to :workflow
belongs_to :input_transition, optional: true, foreign_key: "input_transition_id",
class_name: "WorkflowCore::Transition"
belongs_to :output_transition, optional: true, foreign_key: "output_transition_id",
class_name: "WorkflowCore::Transition"
has_many :tokens
end
end
# frozen_string_literal: true
module WorkflowCore
class Token < ApplicationRecord
self.table_name = "workflow_tokens"
belongs_to :instance,
class_name: "WorkflowCore::WorkflowInstance"
belongs_to :workflow
belongs_to :place
belongs_to :previous, optional: true,
class_name: "WorkflowCore::Token"
enum status: {
processing: 0,
completed: 1,
failed: 2,
unexpected: 3,
terminated: 4
}
end
end
# frozen_string_literal: true
module WorkflowCore
class Transition < ApplicationRecord
self.table_name = "workflow_transitions"
belongs_to :workflow
has_many :input_places, dependent: :nullify,
foreign_key: "output_transition_id", class_name: "WorkflowCore::Place"
has_many :output_places, dependent: :destroy,
foreign_key: "input_transition_id", class_name: "WorkflowCore::Place"
def fire(_token)
raise NotImplementedError
end
end
end
# frozen_string_literal: true
module WorkflowCore
class Workflow < ApplicationRecord
self.table_name = "workflows"
has_one :start_place, class_name: "WorkflowCore::Place", dependent: :destroy
has_many :transitions, class_name: "WorkflowCore::Transition", dependent: :destroy
has_many :places, class_name: "WorkflowCore::Place", dependent: :destroy
has_many :instances, class_name: "WorkflowCore::WorkflowInstance", dependent: :destroy
has_many :tokens, class_name: "WorkflowCore::Token", dependent: :destroy
end
end
# frozen_string_literal: true
module WorkflowCore
class WorkflowInstance < ApplicationRecord
self.table_name = "workflow_instances"
belongs_to :workflow
has_many :tokens, foreign_key: "instance_id", dependent: :destroy
enum status: {
processing: 0,
completed: 1,
failed: 2,
unexpected: 3,
terminated: 4
}
serialize :payload
end
end
#!/usr/bin/env ruby
# frozen_string_literal: true
# This command will automatically be run when you run "rails" with Rails gems
# installed from the root of your application.
ENGINE_ROOT = File.expand_path("..", __dir__)
ENGINE_PATH = File.expand_path("../lib/workflow_core/engine", __dir__)
APP_PATH = File.expand_path("../test/dummy/config/application", __dir__)
# Set up gems listed in the Gemfile.
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
require "rails/all"
require "rails/engine/commands"
# frozen_string_literal: true
class CreateWorkflows < ActiveRecord::Migration[5.2]
def change
create_table :workflows do |t|
t.string :type, null: false
t.timestamps
end
end
end
# frozen_string_literal: true
class CreateWorkflowPlaces < ActiveRecord::Migration[5.2]
def change
create_table :workflow_places do |t|
t.references :input_transition, foreign_key: {to_table: "workflow_transitions"}
t.references :output_transition, foreign_key: {to_table: "workflow_transitions"}
t.string :type, null: false
t.references :workflow, foreign_key: true
t.timestamps
end
end
end
# frozen_string_literal: true
class CreateWorkflowTransitions < ActiveRecord::Migration[5.2]
def change
create_table :workflow_transitions do |t|
t.string :type, null: false
t.references :workflow, foreign_key: true
t.timestamps
end
end
end
# frozen_string_literal: true
class CreateWorkflowInstances < ActiveRecord::Migration[5.2]
def change
create_table :workflow_instances do |t|
t.text :payload
t.integer :status, null: false, default: 0
t.references :workflow, foreign_key: true
t.timestamps
end
end
end
# frozen_string_literal: true
class CreateWorkflowTokens < ActiveRecord::Migration[5.2]
def change
create_table :workflow_tokens do |t|
t.integer :status, null: false, default: 0
t.references :place, foreign_key: {to_table: "workflow_places"}
t.references :previous, foreign_key: {to_table: "workflow_tokens"}
t.references :instance, foreign_key: {to_table: "workflow_instances"}
t.references :workflow, foreign_key: true
t.timestamps
end
end
end
# frozen_string_literal: true
# desc "Explaining what the task does"
# task :workflow_core do
# # Task goes here
# end
# frozen_string_literal: true
require "workflow_core/engine"
module WorkflowCore
# Your code goes here...
end
# frozen_string_literal: true
module WorkflowCore
class Engine < ::Rails::Engine
isolate_namespace WorkflowCore
end
end
# frozen_string_literal: true
module WorkflowCore
VERSION = "0.0.1"
end
ruby-2.5.1
\ No newline at end of file
# frozen_string_literal: true
# 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_relative "config/application"
Rails.application.load_tasks
//= link_tree ../images
//= link_directory ../javascripts .js
//= link_directory ../stylesheets .css
//= require rails-ujs
//= require turbolinks
//= require jquery3
//= require selectize
//= require cocoon
// Cheat form https://github.com/jgthms/bulma/blob/master/docs/_javascript/main.js
document.addEventListener('DOMContentLoaded', () => {
// Dropdowns
const $dropdowns = getAll('.dropdown:not(.is-hoverable)');
if ($dropdowns.length > 0) {
$dropdowns.forEach($el => {
$el.addEventListener('click', event => {
event.stopPropagation();
$el.classList.toggle('is-active');
});
});
document.addEventListener('click', event => {
closeDropdowns();
});
}
function closeDropdowns() {
$dropdowns.forEach($el => {
$el.classList.remove('is-active');
});
}
// Functions
function getAll(selector) {
return Array.prototype.slice.call(document.querySelectorAll(selector), 0);
}
// Utils
function removeFromArray(array, value) {
if (array.includes(value)) {
const value_index = array.indexOf(value);
array.splice(value_index, 1);
}
return array;
}
Array.prototype.diff = function (a) {
return this.filter(function (i) {
return a.indexOf(i) < 0;
});
};
});
@import "selectize";
@import "bulma";
body {
display: flex;
min-height: 100vh;
flex-direction: column;
& nav.nav {
a.brand {
padding: 0.5rem 0.75em 0.5em 0;
}
}
& .main {
flex: 1;
}
& footer.footer {
padding: 3rem 1.5rem 3rem;
}
}
.flash {
overflow: hidden;
top: 0;
left: 0;
right: 0;
line-height: 2.5;
background: $info;
color: $text-invert;
text-align: center;
}
#alert {
background: $danger;
}
.nested_form_field {
.collection {
.nested_form {
border: 1px solid $green;
padding: 0.5em;
margin-left: 0.75em;
margin-bottom: 0.75rem;
}
}
}
.content {
.nested-content {
margin-left: 0.75em;
margin-bottom: 1em;
}
}
.message-body.content {
> :first-child {
margin-top: 0;
}
}
.field {
flex: 1;
}
# frozen_string_literal: true
class ApplicationController < ActionController::Base
helper_method :current_user
private
def current_user
@_current_user ||=
if session[:current_user_id].present?
User.where(id: session[:current_user_id]).first
else
nil
end
end
def require_signed_in
unless current_user
redirect_to users_url
end
end
end
# frozen_string_literal: true
class GroupsController < ApplicationController
before_action :set_group, only: [:show, :edit, :update, :destroy]
# GET /groups
def index
# https://ruby-china.org/topics/32802
@groups = Group.includes(:parent)
end
# GET /groups/new
def new
@group = Group.new
end
# GET /groups/1/edit
def edit
end
# POST /groups
def create
@group = Group.new(group_params)
if @group.save
redirect_to groups_url, notice: "Group was successfully created."
else
render :new
end
end
# PATCH/PUT /groups/1
def update
if @group.update(group_params)
redirect_to groups_url, notice: "Group was successfully updated."
else
render :edit
end
end
# DELETE /groups/1
def destroy
@group.destroy
redirect_to groups_url, notice: "Group was successfully destroyed."
end
private
# Use callbacks to share common setup or constraints between actions.
def set_group
@group = Group.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def group_params
params.require(:group).permit(:name, :parent_id)
end
end
# frozen_string_literal: true
class SessionsController < ApplicationController
def create
session[:current_user_id] = params[:user_id]
redirect_back fallback_location: root_url
end
def destroy
session[:current_user_id] = nil
redirect_to root_url
end
end
# frozen_string_literal: true
class UsersController < ApplicationController
before_action :set_user, only: [:show, :edit, :update, :destroy]
# GET /users
def index
@users = User.all.includes(:group)
end
# GET /users/new
def new
@user = User.new
end
# GET /users/1/edit
def edit
end
# POST /users
def create
@user = User.new(user_params)
if @user.save
redirect_to users_url, notice: "User was successfully created."
else
render :new
end
end
# PATCH/PUT /users/1
def update
if @user.update(user_params)
redirect_to users_url, notice: "User was successfully updated."
else
render :edit
end
end
# DELETE /users/1
def destroy
@user.destroy
redirect_to users_url, notice: "User was successfully destroyed."
end
private
# Use callbacks to share common setup or constraints between actions.
def set_user
@user = User.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def user_params
params.require(:user).permit(:name, :group_id)
end
end
# frozen_string_literal: true
class Workflows::ApplicationController < ApplicationController
layout "workflows"
before_action :set_workflow
protected
# Use callbacks to share common setup or constraints between actions.
def set_workflow
@workflow = Workflow.find(params[:workflow_id])
end
end
# frozen_string_literal: true
module Workflows
class Fields::ApplicationController < ApplicationController
before_action :set_form
before_action :set_field
protected
# Use callbacks to share common setup or constraints between actions.
def set_form
@form = @workflow.form
end
def set_field
@field = @form.fields.find(params[:field_id])
end
end
end
# frozen_string_literal: true
module Workflows
class Fields::OptionsController < Fields::ApplicationController
before_action :set_options
def edit
end
def update
@options.assign_attributes(options_params)
if @options.valid? && @field.save(validate: false)
redirect_to workflow_fields_url(@workflow), notice: "Field was successfully updated."
else
render :edit
end
end
private
def set_options
@options = @field.options
end
def options_params
params.fetch(:options, {}).permit!
end
end
end
# frozen_string_literal: true
module Workflows
class Fields::ValidationsController < Fields::ApplicationController
before_action :set_validations
def edit
end
def update
@validations.assign_attributes(validations_params)
if @validations.valid? && @field.save(validate: false)
redirect_to workflow_fields_url(@workflow), notice: "Field was successfully updated."
else
render :edit
end
end
private
def set_validations
@validations = @field.validations
end
def validations_params
params.fetch(:validations, {}).permit!
end
end
end
# frozen_string_literal: true
class Workflows::FieldsController < Workflows::ApplicationController
before_action :set_form
before_action :set_field, only: %i[show edit update destroy]
# GET /workflows/1/fields
def index
@fields = @form.fields.all
end
# GET /workflows/fields/new
def new
@field = @form.fields.build
end
# GET /workflows/1/fields/1/edit
def edit
end
# POST /workflows/1/fields
def create
@field = @form.fields.build(field_params)
if @field.save
redirect_to workflow_fields_url(@workflow), notice: "Field was successfully created."
else
render :new
end
end
# PATCH/PUT /workflows/1/fields/1
def update
if @field.update(field_params)
redirect_to workflow_fields_url(@workflow), notice: "Field was successfully updated."
else
render :edit
end
end
# DELETE /workflows/1/fields/1
def destroy
@field.destroy
redirect_to workflow_fields_url(@workflow), notice: "Field was successfully destroyed."
end
private
# Use callbacks to share common setup or constraints between actions.
def set_form
@form = @workflow.form
end
def set_field
@field = @form.fields.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def field_params
params.fetch(:field, {}).permit(:name, :label, :hint, :type)
end
end
# frozen_string_literal: true
module Workflows
class Instances::ApplicationController < ApplicationController
layout "workflow_instances"
before_action :set_instance
protected
# Use callbacks to share common setup or constraints between actions.
def set_instance
@instance = @workflow.instances.find(params[:instance_id])
end
end
end
# frozen_string_literal: true
module Workflows
class Instances::TokensController < Instances::ApplicationController
before_action :set_token, only: %i[show fire]
before_action :set_form_model, only: %i[show fire]
# GET /workflows/1/tokens
def index
@tokens = @instance.tokens.includes(place: :output_transition)
end
# POST /workflows/1/tokens/1/fire
def show
@form_record = @virtual_model.load(@instance.payload)
end
# POST /workflows/1/tokens/1/fire
def fire
@form_record = @virtual_model.load(@instance.payload)
@form_record.assign_attributes(form_record_params)
if @form_record.valid?
@instance.update! payload: (@instance.payload || {}).merge(@form_record.serializable_hash)
@token.place.output_transition.fire(@token)
redirect_to workflow_instance_tokens_url(@workflow, @instance), notice: "Token was successfully fired."
else
render :show
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_token
@token = @instance.tokens.find(params[:id] || params[:token_id])
end
def set_form_model
@form = @workflow.form
overrides = @token.place.output_transition.options.field_overrides.map { |o| {o.name => {accessibility: o.accessibility}} }.reduce(&:merge)
@virtual_model = @form.to_virtual_model overrides: overrides
end
def form_record_params
params.fetch(:form_record, {}).permit!
end
end
end
# frozen_string_literal: true
class Workflows::InstancesController < Workflows::ApplicationController
before_action :set_instance, only: %i[show]
# GET /workflows/1/instances
def index
@instances = @workflow.instances.all
end
# GET /workflows/1/instances
def show
@form = @workflow.form
@virtual_model = @form.to_virtual_model
@form_record = @virtual_model.load(@instance.payload)
render layout: "workflow_instances"
end
# POST /workflows/1/instances
def create
@workflow.instances.create! type: "WorkflowInstance"
redirect_to workflow_instances_url(@workflow), notice: "instance was successfully created."
end
private
# Use callbacks to share common setup or constraints between actions.
def set_instance
@instance = @workflow.instances.find(params[:id])
end
end
# frozen_string_literal: true
class Workflows::LoadsController < Workflows::ApplicationController
# GET /workflows/1/load
def show
end
# PATCH/PUT /workflows/1/load
def update
bpmn_xml = permitted_params[:bpmn_xml].present? ? permitted_params[:bpmn_xml] : permitted_params[:bpmn_file]&.read
@workflow.load_from_bpmn!(bpmn_xml)
redirect_to workflow_url(@workflow), notice: "Workflow definition was successfully imported."
end
private
# Only allow a trusted parameter "white list" through.
def permitted_params
params.permit(:bpmn_xml, :bpmn_file)
end
end
# frozen_string_literal: true
module Workflows
class Transitions::ApplicationController < ApplicationController
before_action :set_transition
protected
# Use callbacks to share common setup or constraints between actions.
def set_transition
@transition = @workflow.transitions.find(params[:transition_id])
end
end
end
# frozen_string_literal: true
module Workflows
class Transitions::OptionsController < Transitions::ApplicationController
before_action :set_options
def edit
end
def update
@options.assign_attributes(options_params)
if @options.valid? && @transition.save(validate: false)
redirect_to workflow_transitions_url(@workflow), notice: "Transition was successfully updated."
else
render :edit
end
end
private
def set_options
@options = @transition.options
end
def options_params
params.fetch(:options, {}).permit!
end
end
end
# frozen_string_literal: true
class Workflows::TransitionsController < Workflows::ApplicationController
before_action :set_transition, only: %i[edit update destroy]
# GET /workflows/1/transitions
def index
@transitions = @workflow.transitions.all
end
# GET /workflows/transitions/new
def new
@transition = @workflow.transitions.build
end
# GET /workflows/1/transitions/1/edit
def edit
end
# POST /workflows/1/transitions
def create
@transition = @workflow.transitions.build(transition_params)
if @transition.save
redirect_to workflow_transitions_url(@workflow), notice: "transition was successfully created."
else
render :new
end
end
# PATCH/PUT /workflows/1/transitions/1
def update
if @transition.update(transition_params)
redirect_to workflow_transitions_url(@workflow), notice: "transition was successfully updated."
else
render :edit
end
end
# DELETE /workflows/1/transitions/1
def destroy
@transition.destroy
redirect_to workflow_transitions_url(@workflow), notice: "transition was successfully destroyed."
end
private
# Use callbacks to share common setup or constraints between actions.
def set_transition
@transition = @workflow.transitions.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def transition_params
params.fetch(:transition, {}).permit(:name, :type)
end
end
# frozen_string_literal: true
class WorkflowsController < ApplicationController
layout "application"
before_action :set_workflow, only: %i[show edit update destroy]
# GET /workflows
def index
@workflows = Workflow.all
end
# GET /workflows/new
def new
@workflow = Workflow.new
end
# GET /workflows/1
def show
render layout: "workflows"
end
# GET /workflows/1/edit
def edit
end
# POST /workflows
def create
@workflow = Workflow.new(workflow_params)
if @workflow.save
redirect_to workflow_url(@workflow), notice: "workflow was successfully created."
else
render :new
end
end
# PATCH/PUT /workflows/1
def update
if @workflow.update(workflow_params)
redirect_to workflow_url(@workflow), notice: "workflow was successfully updated."
else
render :edit
end
end
# DELETE /workflows/1
def destroy
@workflow.destroy
redirect_to workflows_url, notice: "workflow was successfully destroyed."
end
private
# Use callbacks to share common setup or constraints between actions.
def set_workflow
@workflow = Workflow.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def workflow_params
params.fetch(:workflow, {}).permit(:name, :description)
end
end
# frozen_string_literal: true
module ApplicationHelper
def options_for_enum_select(klass, attribute, selected = nil)
container = klass.public_send(attribute.to_s.pluralize).map do |k, v|
v ||= k
[klass.human_enum_value(attribute, k), v]
end
options_for_select(container, selected)
end
def present(model, options = {})
klass = options.delete(:presenter_class) || "#{model.class}Presenter".constantize
presenter = klass.new(model, self, options)
yield(presenter) if block_given?
presenter
end
end
# frozen_string_literal: true
module FieldsHelper
def options_for_field_types(selected: nil)
options_for_select(Field.descendants.map { |klass| [klass.model_name.human, klass.to_s] }, selected)
end
def field_label(form, field_name:)
field_name = field_name.to_s.split(".").first.to_sym
form.fields.select do |field|
field.name == field_name
end.first&.label
end
end
# frozen_string_literal: true
module TransitionsHelper
def options_for_transition_types(selected: nil)
options_for_select(Transition.descendants.map { |klass| [klass.model_name.human, klass.to_s] }, selected)
end
end
# frozen_string_literal: true
class ApplicationJob < ActiveJob::Base
end
# frozen_string_literal: true
module Bpmn
class DefinitionContainer
attr_reader :collection
def initialize(tokens)
@collection = tokens.map { |t| {t.id => t} }.reduce(&:merge!)
@start_id = tokens.first { |t| t.node_type == :start_event }.id
end
def start_event
@collection[@start_id]
end
def [](id)
@collection[id]
end
def slice(*ids)
@collection.slice(*ids).values
end
def self.parse(bpmn_xml)
tokens = Bpmn::Tokenizer.new.tokenize(bpmn_xml)
return nil unless tokens
new tokens
end
end
end
# frozen_string_literal: true
module Bpmn
class Tokenizer
def tokenize(bpmn_xml)
return nil unless bpmn_xml.present?
doc = Nokogiri::XML(bpmn_xml) rescue nil
return nil unless doc && doc.errors.count.zero?
process = doc.at_xpath("//bpmn:process") rescue nil
return nil unless process
return unless process.present?
process.elements.map { |el| factory(el) }.compact
end
private
def factory(element)
case element.name
when "startEvent"
Bpmn::Tokens::StartEvent.new(element)
when "endEvent"
Bpmn::Tokens::EndEvent.new(element)
when "task"
Bpmn::Tokens::Task.new(element)
when "userTask"
Bpmn::Tokens::UserTask.new(element)
when "scriptTask"
Bpmn::Tokens::ScriptTask.new(element)
when "parallelGateway"
Bpmn::Tokens::ParallelGateway.new(element)
when "exclusiveGateway"
Bpmn::Tokens::ExclusiveGateway.new(element)
when "inclusiveGateway"
Bpmn::Tokens::InclusiveGateway.new(element)
when "sequenceFlow"
Bpmn::Tokens::SequenceFlow.new(element)
else
raise NotImplementedError.new("#{element.name} is unsupported yet.")
end
end
end
end
# frozen_string_literal: true
module Bpmn
class Tokens::CommonToken < Bpmn::Tokens::Token
attr_reader :incoming_ids, :outgoing_ids
def initialize(element)
super
@incoming_ids = element.xpath("bpmn:incoming").map(&:content)
@outgoing_ids = element.xpath("bpmn:outgoing").map(&:content)
end
def to_hash
super.merge(
incoming_ids: incoming_ids,
outgoing_ids: outgoing_ids
)
end
end
end
# frozen_string_literal: true
module Bpmn
class Tokens::EndEvent < Bpmn::Tokens::CommonToken
def event?
true
end
end
end
# frozen_string_literal: true
module Bpmn
class Tokens::ExclusiveGateway < Bpmn::Tokens::CommonToken
attr_reader :default_flow_id
def initialize(element)
super
@default_flow_id = element["default"]
end
def gateway?
true
end
def to_hash
super.merge(
default_flow_id: default_flow_id
)
end
end
end
# frozen_string_literal: true
module Bpmn
class Tokens::Extensions::ConditionExpression
attr_reader :language, :condition, :type
def initialize(element)
@language = element["language"]
@condition = element.content
@type = element["xsi:type"]
end
def self.factory(xelement)
ce = xelement.at_xpath("bpmn:conditionExpression")
return nil unless ce
new(ce)
end
def to_hash
{
language: language,
condition: condition,
type: type,
}
end
alias_method :to_h, :to_hash
end
end
# frozen_string_literal: true
module Bpmn
class Tokens::InclusiveGateway < Bpmn::Tokens::CommonToken
attr_reader :default_flow_id
def initialize(element)
super
@default_flow_id = element["default"]
end
def gateway?
true
end
def to_hash
super.merge(
default_flow_id: default_flow_id
)
end
end
end
# frozen_string_literal: true
module Bpmn
class Tokens::ParallelGateway < Bpmn::Tokens::CommonToken
def gateway?
true
end
end
end
# frozen_string_literal: true
module Bpmn
class Tokens::ScriptTask < Bpmn::Tokens::CommonToken
attr_reader :script_format, :script
def initialize(element)
super
@script_format = element["scriptFormat"]
@script = element.at_xpath("bpmn:script").try(:content)
end
def task?
true
end
def to_hash
super.merge(
extensions: {
script_format: script_format,
script: script
}
)
end
end
end
# frozen_string_literal: true
module Bpmn
class Tokens::SequenceFlow < Bpmn::Tokens::Token
attr_reader :source_id, :target_id
attr_reader :condition_expression
def initialize(element)
super
@source_id = element["sourceRef"]
@target_id = element["targetRef"]
@condition_expression = Bpmn::Tokens::Extensions::ConditionExpression.factory(element)
end
def flow?
true
end
def to_hash
super.merge(
source_id: source_id,
target_id: target_id,
extensions: {
condition_expression: condition_expression.to_hash
}
)
end
end
end
# frozen_string_literal: true
module Bpmn
class Tokens::StartEvent < Bpmn::Tokens::CommonToken
def event?
true
end
end
end
# frozen_string_literal: true
module Bpmn
class Tokens::Task < Bpmn::Tokens::CommonToken
def task?
true
end
end
end
# frozen_string_literal: true
module Bpmn
class Tokens::Token
attr_reader :id, :name, :node_type
attr_reader :documentation
def initialize(element)
# @element = element
@id = element["id"]
@name = element["name"] || ""
@node_type = element.name.underscore.to_sym
@documentation = element.at_xpath("bpmn:documentation")&.content || ""
end
def gateway?
false
end
def event?
false
end
def task?
false
end
def flow?
false
end
def to_hash
{
node_id: id,
node_type: node_type,
name: name
}
end
alias_method :to_h, :to_hash
end
end
# frozen_string_literal: true
module Bpmn
class Tokens::UserTask < Bpmn::Tokens::CommonToken
def task?
true
end
end
end
# frozen_string_literal: true
class ApplicationMailer < ActionMailer::Base
default from: "from@example.com"
layout "mailer"
end
# frozen_string_literal: true
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
include ActsAsDefaultValue
include EnumAttributeLocalizable
end
# frozen_string_literal: true
# https://github.com/FooBarWidget/default_value_for
#
# Copyright (c) 2008-2012 Phusion
#
# 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.
module ActsAsDefaultValue
extend ActiveSupport::Concern
class NormalValueContainer
def initialize(value)
@value = value
end
def evaluate(_instance)
if @value.duplicable?
@value.dup
else
@value
end
end
end
class BlockValueContainer
def initialize(block)
@block = block
end
def evaluate(instance)
if @block.arity == 0
@block.call
else
@block.call(instance)
end
end
end
included do
after_initialize :set_default_values
end
def initialize(attributes = nil)
@initialization_attributes = attributes.is_a?(Hash) ? attributes.stringify_keys : {}
super
end
def set_default_values
self.class._all_default_attribute_values.each do |attribute, container|
next unless new_record? || self.class._all_default_attribute_values_not_allowing_nil.include?(attribute)
connection_default_value_defined = new_record? && respond_to?("#{attribute}_changed?") && !send("#{attribute}_changed?")
column = self.class.columns.detect { |c| c.name == attribute }
attribute_blank =
if column && column.type == :boolean
send(attribute).nil?
else
send(attribute).blank?
end
next unless connection_default_value_defined || attribute_blank
# allow explicitly setting nil through allow nil option
next if @initialization_attributes.is_a?(Hash) &&
(
@initialization_attributes.has_key?(attribute) ||
(
@initialization_attributes.has_key?("#{attribute}_attributes") &&
nested_attributes_options.stringify_keys[attribute]
)
) &&
!self.class._all_default_attribute_values_not_allowing_nil.include?(attribute)
send("#{attribute}=", container.evaluate(self))
clear_attribute_changes [attribute] if has_attribute?(attribute)
end
end
def attributes_for_create(attribute_names)
attribute_names += self.class._all_default_attribute_values.keys.map(&:to_s).find_all do |name|
self.class.columns_hash.key?(name)
end
super
end
module ClassMethods
def _default_attribute_values # :nodoc:
@default_attribute_values ||= {}
end
def _default_attribute_values_not_allowing_nil # :nodoc:
@default_attribute_values_not_allowing_nil ||= Set.new
end
def _all_default_attribute_values # :nodoc:
if superclass.respond_to?(:_default_attribute_values)
superclass._all_default_attribute_values.merge(_default_attribute_values)
else
_default_attribute_values
end
end
def _all_default_attribute_values_not_allowing_nil # :nodoc:
if superclass.respond_to?(:_default_attribute_values_not_allowing_nil)
superclass._all_default_attribute_values_not_allowing_nil + _default_attribute_values_not_allowing_nil
else
_default_attribute_values_not_allowing_nil
end
end
# Declares a default value for the given attribute.
#
# Sets the default value to the given options parameter
#
# The <tt>options</tt> can be used to specify the following things:
# * <tt>allow_nil (default: true)</tt> - Sets explicitly passed nil values if option is set to true.
def default_value_for(attribute, value, **options)
allow_nil = options.fetch(:allow_nil, true)
container =
if value.is_a? Proc
BlockValueContainer.new(value)
else
NormalValueContainer.new(value)
end
_default_attribute_values[attribute.to_s] = container
_default_attribute_values_not_allowing_nil << attribute.to_s unless allow_nil
attribute
end
end
end
# frozen_string_literal: true
module EnumAttributeLocalizable
extend ActiveSupport::Concern
module ClassMethods
def human_enum_value(attribute, value, options = {})
parts = attribute.to_s.split(".")
attribute = parts.pop.pluralize
attributes_scope = "#{i18n_scope}.attributes"
if parts.any?
namespace = parts.join("/")
defaults = lookup_ancestors.map do |klass|
:"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}.#{value}"
end
defaults << :"#{attributes_scope}.#{namespace}.#{attribute}.#{value}"
else
defaults = lookup_ancestors.map do |klass|
:"#{attributes_scope}.#{klass.model_name.i18n_key}.#{attribute}.#{value}"
end
end
defaults << :"attributes.#{attribute}.#{value}"
defaults << options.delete(:default) if options[:default]
defaults << value.to_s.humanize
options[:default] = defaults
I18n.translate(defaults.shift, options)
end
end
end
# frozen_string_literal: true
module Concerns::Fields
module Validations::Acceptance
extend ActiveSupport::Concern
included do
attribute :acceptance, :boolean, default: false
end
def interpret_to(model, field_name, _accessibility, _options = {})
super
return unless acceptance
model.validates field_name, acceptance: true
end
end
end
# frozen_string_literal: true
module Concerns::Fields
module Validations::Confirmation
extend ActiveSupport::Concern
included do
attribute :confirmation, :boolean, default: false
end
def interpret_to(model, field_name, _accessibility, _options = {})
super
return unless confirmation
model.validates field_name, confirmation: true
end
end
end
# frozen_string_literal: true
module Concerns::Fields
module Validations::Exclusion
extend ActiveSupport::Concern
included do
embeds_one :exclusion, class_name: "Concerns::Fields::Validations::Exclusion::ExclusionOptions"
accepts_nested_attributes_for :exclusion
after_initialize do
build_exclusion unless exclusion
end
end
def interpret_to(model, field_name, accessibility, options = {})
super
exclusion&.interpret_to model, field_name, accessibility, options
end
class ExclusionOptions < FieldOptions
attribute :message, :string, default: ""
attribute :in, :string, default: [], array: true
def interpret_to(model, field_name, _accessibility, _options = {})
return if self.in.empty?
options = {in: self.in}
options[:message] = message if message.present?
model.validates field_name, exclusion: options, allow_blank: true
end
end
end
end
# frozen_string_literal: true
module Concerns::Fields
module Validations::Format
extend ActiveSupport::Concern
included do
embeds_one :format, class_name: "Concerns::Fields::Validations::Format::FormatOptions"
accepts_nested_attributes_for :format
after_initialize do
build_format unless format
end
end
def interpret_to(model, field_name, accessibility, options = {})
super
format&.interpret_to model, field_name, accessibility, options
end
class FormatOptions < FieldOptions
attribute :with, :string, default: ""
attribute :message, :string, default: ""
validate do
begin
Regexp.new(with) if with.present?
rescue RegexpError
errors.add :with, :invalid
end
end
def interpret_to(model, field_name, _accessibility, _options = {})
return if with.blank?
with = Regexp.new(self.with)
options = {with: with}
options[:message] = message if message.present?
model.validates field_name, format: options, allow_blank: true
end
end
end
end
# frozen_string_literal: true
module Concerns::Fields
module Validations::Inclusion
extend ActiveSupport::Concern
included do
embeds_one :inclusion, class_name: "Concerns::Fields::Validations::Inclusion::InclusionOptions"
accepts_nested_attributes_for :inclusion
after_initialize do
build_inclusion unless inclusion
end
end
def interpret_to(model, field_name, accessibility, options = {})
super
inclusion&.interpret_to model, field_name, accessibility, options
end
class InclusionOptions < FieldOptions
attribute :message, :string, default: ""
attribute :in, :string, default: [], array: true
def interpret_to(model, field_name, _accessibility, _options = {})
return if self.in.empty?
options = {in: self.in}
options[:message] = message if message.present?
model.validates field_name, inclusion: options, allow_blank: true
end
end
end
end
# frozen_string_literal: true
module Concerns::Fields
module Validations::Length
extend ActiveSupport::Concern
included do
embeds_one :length, class_name: "Concerns::Fields::Validations::Length::LengthOptions"
accepts_nested_attributes_for :length
after_initialize do
build_length unless length
end
end
def interpret_to(model, field_name, accessibility, options = {})
super
length&.interpret_to model, field_name, accessibility, options
end
class LengthOptions < FieldOptions
attribute :minimum, :integer, default: 0
attribute :maximum, :integer, default: 0
attribute :is, :integer, default: 0
validates :minimum, :maximum, :is,
numericality: {
greater_than_or_equal_to: 0
}
validates :maximum,
numericality: {
greater_than: :minimum
},
if: proc { |record| record.maximum <= record.minimum && record.maximum.positive? }
validates :is,
numericality: {
equal_to: 0
},
if: proc { |record| !record.maximum.zero? || !record.minimum.zero? }
def interpret_to(model, field_name, _accessibility, _options = {})
return if self.minimum.zero? && self.maximum.zero? && self.is.zero?
if is.positive?
model.validates field_name, length: {is: is}, allow_blank: true
return
end
options = {}
options[:minimum] = minimum if minimum.positive?
options[:maximum] = maximum if maximum.positive?
return if options.empty?
model.validates field_name, length: options, allow_blank: true
end
end
end
end
# frozen_string_literal: true
module Concerns::Fields
module Validations::Numericality
extend ActiveSupport::Concern
included do
embeds_one :numericality, class_name: "Concerns::Fields::Validations::Numericality::NumericalityOptions"
accepts_nested_attributes_for :numericality
after_initialize do
build_numericality unless numericality
end
end
def interpret_to(model, field_name, accessibility, options = {})
super
numericality&.interpret_to model, field_name, accessibility, options
end
class NumericalityOptions < FieldOptions
attribute :lower_bound_check, :string, default: "disabled"
attribute :upper_bound_check, :string, default: "disabled"
attribute :lower_bound_value, :float, default: 0.0
attribute :upper_bound_value, :float, default: 0.0
enum lower_bound_check: {
disabled: "disabled",
greater_than: "greater_than",
greater_than_or_equal_to: "greater_than_or_equal_to"
}, _prefix: :lower_bound_check
enum upper_bound_check: {
disabled: "disabled",
less_than: "less_than",
less_than_or_equal_to: "less_than_or_equal_to"
}, _prefix: :upper_bound_check
validates :upper_bound_value,
numericality: {
greater_than: :lower_bound_value
},
if: proc { upper_bound_check != "disabled" && lower_bound_check != "disabled" }
def interpret_to(model, field_name, _accessibility, _options = {})
options = {}
options[lower_bound_check] = lower_bound_value unless lower_bound_check_disabled?
options[upper_bound_check] = upper_bound_value unless upper_bound_check_disabled?
return if options.empty?
options.symbolize_keys!
model.validates field_name, numericality: options, allow_blank: true
end
end
end
end
# frozen_string_literal: true
module Concerns::Fields
module Validations::Presence
extend ActiveSupport::Concern
included do
attribute :presence, :boolean, default: false
end
def interpret_to(model, field_name, _accessibility, _options = {})
super
return unless presence
model.validates field_name, presence: true
end
end
end
# frozen_string_literal: true
# https://github.com/FooBarWidget/default_value_for
#
# Copyright (c) 2008-2012 Phusion
#
# 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.
module FormCore
module ActsAsDefaultValue
extend ActiveSupport::Concern
class NormalValueContainer
def initialize(value)
@value = value
end
def evaluate(_instance)
if @value.duplicable?
@value.dup
else
@value
end
end
end
class BlockValueContainer
def initialize(block)
@block = block
end
def evaluate(instance)
if @block.arity == 0
@block.call
else
@block.call(instance)
end
end
end
included do
after_initialize :set_default_values
end
def initialize(attributes = nil)
@initialization_attributes = attributes.is_a?(Hash) ? attributes.stringify_keys : {}
super
end
def set_default_values
self.class._all_default_attribute_values.each do |attribute, container|
next unless new_record? || self.class._all_default_attribute_values_not_allowing_nil.include?(attribute)
attribute_blank =
if self.class.attribute_types[attribute]&.type == :boolean
send(attribute).nil?
else
send(attribute).blank?
end
next unless attribute_blank
# allow explicitly setting nil through allow nil option
next if @initialization_attributes.is_a?(Hash) &&
(
@initialization_attributes.has_key?(attribute) ||
(
@initialization_attributes.has_key?("#{attribute}_attributes") &&
nested_attributes_options.stringify_keys[attribute]
)
) &&
!self.class._all_default_attribute_values_not_allowing_nil.include?(attribute)
send("#{attribute}=", container.evaluate(self))
clear_attribute_changes [attribute] if has_attribute?(attribute)
end
end
module ClassMethods
def _default_attribute_values # :nodoc:
@default_attribute_values ||= {}
end
def _default_attribute_values_not_allowing_nil # :nodoc:
@default_attribute_values_not_allowing_nil ||= Set.new
end
def _all_default_attribute_values # :nodoc:
if superclass.respond_to?(:_default_attribute_values)
superclass._all_default_attribute_values.merge(_default_attribute_values)
else
_default_attribute_values
end
end
def _all_default_attribute_values_not_allowing_nil # :nodoc:
if superclass.respond_to?(:_default_attribute_values_not_allowing_nil)
superclass._all_default_attribute_values_not_allowing_nil + _default_attribute_values_not_allowing_nil
else
_default_attribute_values_not_allowing_nil
end
end
# Declares a default value for the given attribute.
#
# Sets the default value to the given options parameter
#
# The <tt>options</tt> can be used to specify the following things:
# * <tt>allow_nil (default: true)</tt> - Sets explicitly passed nil values if option is set to true.
def default_value_for(attribute, value, **options)
allow_nil = options.fetch(:allow_nil, true)
container =
if value.is_a? Proc
BlockValueContainer.new(value)
else
NormalValueContainer.new(value)
end
_default_attribute_values[attribute.to_s] = container
_default_attribute_values_not_allowing_nil << attribute.to_s unless allow_nil
attribute
end
end
end
end
# frozen_string_literal: true
class Field < ApplicationRecord
include FormCore::Concerns::Models::Field
self.table_name = "fields"
belongs_to :form, class_name: "Form", foreign_key: "form_id", touch: true
validates :label,
presence: true
validates :type,
inclusion: {
in: ->(_) { Field.descendants.map(&:to_s) }
},
allow_blank: false
default_value_for :name,
-> (_) { "field_#{SecureRandom.hex(3)}" },
allow_nil: false
def self.type_key
model_name.name.split("::").last.underscore
end
def type_key
self.class.type_key
end
def options_configurable?
options.is_a?(FieldOptions) && options.attributes.any?
end
def validations_configurable?
validations.is_a?(FieldOptions) && validations.attributes.any?
end
def attach_choices?
false
end
protected
def interpret_validations_to(model, accessibility, overrides = {})
return unless accessibility == :read_and_write
validations_overrides = overrides.fetch(:validations) { {} }
validations =
if validations_overrides.any?
self.validations.dup.update(validations_overrides)
else
self.validations
end
validations.interpret_to(model, name, accessibility)
end
def interpret_extra_to(model, accessibility, overrides = {})
options_overrides = overrides.fetch(:options) { {} }
options =
if options_overrides.any?
self.options.dup.update(options_overrides)
else
self.options
end
options.interpret_to(model, name, accessibility)
end
end
require_dependency "fields"
# frozen_string_literal: true
class FieldOptions < DuckRecord::Base
include FormCore::ActsAsDefaultValue
include EnumAttributeLocalizable
class_attribute :keeping_old_serialization
attr_accessor :raw_attributes
def interpret_to(_model, _field_name, _accessibility, _options = {})
end
def serializable_hash(options = {})
options = (options || {}).reverse_merge include: self.class._embeds_reflections.keys
super options
end
private
def _assign_attribute(k, v)
return unless respond_to?("#{k}=")
public_send("#{k}=", v)
end
class << self
def _embeds_reflections
_reflections.select { |_, v| v.is_a? DuckRecord::Reflection::EmbedsAssociationReflection }
end
def model_version
1
end
def root_key_for_serialization
"#{self}.#{model_version}"
end
def dump(obj)
return YAML.dump({}) unless obj
serializable_hash =
if obj.respond_to?(:serializable_hash)
obj.serializable_hash
elsif obj.respond_to?(:to_hash)
obj.to_hash
else
raise ArgumentError, "`obj` required can be cast to `Hash` -- #{obj.class}"
end.stringify_keys
data = {root_key_for_serialization => serializable_hash}
if keeping_old_serialization
data.reverse_merge! obj.raw_attributes
end
YAML.dump(data)
end
def load(yaml_or_hash)
case yaml_or_hash
when Hash
load_from_hash(yaml_or_hash)
when String
load_from_yaml(yaml_or_hash)
else
new
end
end
WHITELIST_CLASSES = [BigDecimal, Date, Time, Symbol]
def load_from_yaml(yaml)
return new if yaml.blank?
unless yaml.is_a?(String) && /^---/.match?(yaml)
return new
end
decoded = YAML.safe_load(yaml, WHITELIST_CLASSES)
unless decoded.is_a? Hash
return new
end
record = new decoded[root_key_for_serialization]
record.raw_attributes = decoded.freeze
record
end
def load_from_hash(hash)
return new if hash.blank?
record = new hash[root_key_for_serialization]
record.raw_attributes = hash.freeze
record
end
end
end
# frozen_string_literal: true
module Fields
%w[
text boolean decimal integer
].each do |type|
require_dependency "fields/#{type}_field"
end
end
# frozen_string_literal: true
module Fields
class BooleanField < Field
serialize :validations, Validations::BooleanField
serialize :options, Options::BooleanField
def stored_type
:boolean
end
end
end
# frozen_string_literal: true
module Fields
class DecimalField < Field
serialize :validations, Validations::DecimalField
serialize :options, Options::DecimalField
def stored_type
:decimal
end
end
end
# frozen_string_literal: true
module Fields
class IntegerField < Field
serialize :validations, Validations::IntegerField
serialize :options, Options::IntegerField
def stored_type
:integer
end
protected
def interpret_extra_to(model, accessibility, _overrides = {})
return if accessibility != :read_and_write
model.validates name, numericality: {only_integer: true}, allow_blank: true
end
end
end
# frozen_string_literal: true
module Fields::Options
class BooleanField < FieldOptions
end
end
# frozen_string_literal: true
module Fields::Options
class DecimalField < FieldOptions
attribute :step, :decimal, default: 0.01
validates :step,
numericality: {
greater_than_or_equal_to: 0.0
}
end
end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment