Repository: hooopo/petri_flow Branch: master Commit: a36ba91c24dc Files: 294 Total size: 242.8 KB Directory structure: gitextract_22cx_jl_/ ├── .editorconfig ├── .gitattributes ├── .github/ │ └── workflows/ │ ├── ci.yml │ └── gempush.yml ├── .gitignore ├── .rubocop.yml ├── FormSpec.md ├── Gemfile ├── Guard.md ├── LICENSE ├── MIT-LICENSE ├── README.md ├── Rakefile ├── acts_as_party.md ├── app/ │ ├── assets/ │ │ ├── config/ │ │ │ └── wf_manifest.js │ │ ├── images/ │ │ │ └── wf/ │ │ │ └── .keep │ │ ├── javascripts/ │ │ │ └── wf/ │ │ │ └── application.js │ │ └── stylesheets/ │ │ └── wf/ │ │ ├── application.scss │ │ ├── arcs.css │ │ ├── cases.css │ │ ├── comments.css │ │ ├── fields.css │ │ ├── forms.css │ │ ├── guards.css │ │ ├── places.css │ │ ├── static_assignments.css │ │ ├── transitions.css │ │ ├── uikit/ │ │ │ ├── _colors.scss │ │ │ ├── _variables.scss │ │ │ ├── alert.scss │ │ │ ├── button.scss │ │ │ ├── card.scss │ │ │ ├── index.scss │ │ │ ├── navbar.scss │ │ │ └── table.scss │ │ ├── workflows.css │ │ ├── workitem_assignments.css │ │ └── workitems.css │ ├── controllers/ │ │ └── wf/ │ │ ├── application_controller.rb │ │ ├── arcs_controller.rb │ │ ├── cases_controller.rb │ │ ├── comments_controller.rb │ │ ├── fields_controller.rb │ │ ├── forms_controller.rb │ │ ├── guards_controller.rb │ │ ├── places_controller.rb │ │ ├── static_assignments_controller.rb │ │ ├── transitions_controller.rb │ │ ├── workflows_controller.rb │ │ ├── workitem_assignments_controller.rb │ │ └── workitems_controller.rb │ ├── helpers/ │ │ └── wf/ │ │ ├── application_helper.rb │ │ ├── arcs_helper.rb │ │ ├── cases_helper.rb │ │ ├── comments_helper.rb │ │ ├── fields_helper.rb │ │ ├── forms_helper.rb │ │ ├── guards_helper.rb │ │ ├── places_helper.rb │ │ ├── static_assignments_helper.rb │ │ ├── transitions_helper.rb │ │ ├── workflows_helper.rb │ │ ├── workitem_assignments_helper.rb │ │ └── workitems_helper.rb │ ├── jobs/ │ │ └── wf/ │ │ ├── application_job.rb │ │ └── fire_timed_workitem_job.rb │ ├── mailers/ │ │ └── wf/ │ │ └── application_mailer.rb │ ├── models/ │ │ └── wf/ │ │ ├── acts_as_party.rb │ │ ├── application_record.rb │ │ ├── arc.rb │ │ ├── callbacks/ │ │ │ ├── assignment_default.rb │ │ │ ├── deadline_default.rb │ │ │ ├── enable_default.rb │ │ │ ├── fire_default.rb │ │ │ ├── hold_timeout_default.rb │ │ │ ├── notification_default.rb │ │ │ ├── time_default.rb │ │ │ └── unassignment_default.rb │ │ ├── case.rb │ │ ├── case_assignment.rb │ │ ├── case_command/ │ │ │ ├── add_comment.rb │ │ │ ├── add_manual_assignment.rb │ │ │ ├── add_token.rb │ │ │ ├── add_workitem_assignment.rb │ │ │ ├── begin_workitem_action.rb │ │ │ ├── cancel.rb │ │ │ ├── cancel_workitem.rb │ │ │ ├── clear_manual_assignments.rb │ │ │ ├── clear_workitem_assignments.rb │ │ │ ├── consume_token.rb │ │ │ ├── create_entry.rb │ │ │ ├── enable_transitions.rb │ │ │ ├── end_workitem_action.rb │ │ │ ├── finish_workitem.rb │ │ │ ├── finished_p.rb │ │ │ ├── fire_message_transition.rb │ │ │ ├── fire_transition_internal.rb │ │ │ ├── lock_token.rb │ │ │ ├── new.rb │ │ │ ├── release_token.rb │ │ │ ├── remove_manual_assignment.rb │ │ │ ├── remove_workitem_assignment.rb │ │ │ ├── resume.rb │ │ │ ├── set_workitem_assignments.rb │ │ │ ├── start_case.rb │ │ │ ├── start_workitem.rb │ │ │ ├── suspend.rb │ │ │ ├── sweep_automatic_transitions.rb │ │ │ ├── sweep_timed_transitions.rb │ │ │ └── workitem_action.rb │ │ ├── comment.rb │ │ ├── demo_target.rb │ │ ├── entry.rb │ │ ├── field.rb │ │ ├── field_value.rb │ │ ├── form.rb │ │ ├── group.rb │ │ ├── guard.rb │ │ ├── lola.rb │ │ ├── multiple_instances/ │ │ │ └── all_finish.rb │ │ ├── party.rb │ │ ├── place.rb │ │ ├── token.rb │ │ ├── transition.rb │ │ ├── transition_static_assignment.rb │ │ ├── user.rb │ │ ├── workflow.rb │ │ ├── workitem.rb │ │ └── workitem_assignment.rb │ └── views/ │ ├── layouts/ │ │ └── wf/ │ │ ├── _alert.html.erb │ │ ├── _footer.html.erb │ │ ├── _nav.html.erb │ │ ├── _notice.html.erb │ │ └── application.html.erb │ └── wf/ │ ├── arcs/ │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ ├── new.html.erb │ │ └── show.html.erb │ ├── cases/ │ │ ├── _form.html.erb │ │ ├── index.html.erb │ │ ├── new.html.erb │ │ └── show.html.erb │ ├── comments/ │ │ └── new.html.erb │ ├── fields/ │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ └── new.html.erb │ ├── forms/ │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ ├── index.html.erb │ │ ├── new.html.erb │ │ └── show.html.erb │ ├── guards/ │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ └── new.html.erb │ ├── places/ │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ └── new.html.erb │ ├── static_assignments/ │ │ ├── _form.html.erb │ │ └── new.html.erb │ ├── transitions/ │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ ├── new.html.erb │ │ └── show.html.erb │ ├── workflows/ │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ ├── index.html.erb │ │ ├── new.html.erb │ │ └── show.html.erb │ ├── workitem_assignments/ │ │ └── new.html.erb │ └── workitems/ │ ├── index.html.erb │ ├── pre_finish.html.erb │ └── show.html.erb ├── bin/ │ └── rails ├── config/ │ └── routes.rb ├── db/ │ └── migrate/ │ ├── 20200130201043_init.rb │ ├── 20200130201641_init_some_data.rb │ ├── 20200131200455_create_wf_entries.rb │ ├── 20200201001543_add_target_field_name_for_guard.rb │ ├── 20200212120019_remove_targetable_from_workitem.rb │ ├── 20200213085258_add_formable.rb │ ├── 20200213125753_add_form_id_for_entry.rb │ ├── 20200213130900_remove_workflow_id_from_form_related.rb │ ├── 20200220070839_remove_unused_column.rb │ ├── 20200220072512_add_sub_workflow.rb │ ├── 20200222150432_add_multi_instance.rb │ └── 20200226195134_add_dynamic_assign_by.rb ├── lib/ │ ├── tasks/ │ │ └── wf_tasks.rake │ ├── wf/ │ │ ├── engine.rb │ │ └── version.rb │ └── wf.rb ├── lola.md ├── screenshots/ │ └── .keep ├── test/ │ ├── controllers/ │ │ └── wf/ │ │ ├── arcs_controller_test.rb │ │ ├── cases_controller_test.rb │ │ ├── comments_controller_test.rb │ │ ├── fields_controller_test.rb │ │ ├── forms_controller_test.rb │ │ ├── guards_controller_test.rb │ │ ├── places_controller_test.rb │ │ ├── static_assignments_controller_test.rb │ │ ├── transitions_controller_test.rb │ │ ├── workflows_controller_test.rb │ │ ├── workitem_assignments_controller_test.rb │ │ └── workitems_controller_test.rb │ ├── dummy/ │ │ ├── .ruby-version │ │ ├── Rakefile │ │ ├── app/ │ │ │ ├── assets/ │ │ │ │ ├── config/ │ │ │ │ │ └── manifest.js │ │ │ │ ├── images/ │ │ │ │ │ └── .keep │ │ │ │ └── stylesheets/ │ │ │ │ └── application.css │ │ │ ├── channels/ │ │ │ │ └── application_cable/ │ │ │ │ ├── channel.rb │ │ │ │ └── connection.rb │ │ │ ├── controllers/ │ │ │ │ ├── application_controller.rb │ │ │ │ └── concerns/ │ │ │ │ └── .keep │ │ │ ├── helpers/ │ │ │ │ └── application_helper.rb │ │ │ ├── javascript/ │ │ │ │ └── packs/ │ │ │ │ └── application.js │ │ │ ├── jobs/ │ │ │ │ └── application_job.rb │ │ │ ├── mailers/ │ │ │ │ └── application_mailer.rb │ │ │ ├── models/ │ │ │ │ ├── application_record.rb │ │ │ │ ├── concerns/ │ │ │ │ │ └── .keep │ │ │ │ ├── entry.rb │ │ │ │ ├── field.rb │ │ │ │ ├── field_value.rb │ │ │ │ └── form.rb │ │ │ └── views/ │ │ │ └── layouts/ │ │ │ ├── application.html.erb │ │ │ ├── mailer.html.erb │ │ │ └── mailer.text.erb │ │ ├── bin/ │ │ │ ├── rails │ │ │ ├── rake │ │ │ └── setup │ │ ├── config/ │ │ │ ├── application.rb │ │ │ ├── boot.rb │ │ │ ├── cable.yml │ │ │ ├── database.yml │ │ │ ├── environment.rb │ │ │ ├── environments/ │ │ │ │ ├── development.rb │ │ │ │ ├── production.rb │ │ │ │ └── test.rb │ │ │ ├── initializers/ │ │ │ │ ├── application_controller_renderer.rb │ │ │ │ ├── assets.rb │ │ │ │ ├── backtrace_silencers.rb │ │ │ │ ├── content_security_policy.rb │ │ │ │ ├── cookies_serializer.rb │ │ │ │ ├── filter_parameter_logging.rb │ │ │ │ ├── inflections.rb │ │ │ │ ├── mime_types.rb │ │ │ │ ├── my_assignment_callback.rb │ │ │ │ ├── wf_config.rb │ │ │ │ └── wrap_parameters.rb │ │ │ ├── locales/ │ │ │ │ └── en.yml │ │ │ ├── mysql_database.yml │ │ │ ├── puma.rb │ │ │ ├── routes.rb │ │ │ ├── spring.rb │ │ │ └── storage.yml │ │ ├── config.ru │ │ ├── db/ │ │ │ ├── migrate/ │ │ │ │ ├── 20200213081814_new_form.rb │ │ │ │ ├── 20200213133942_add_form_id_in_entry1.rb │ │ │ │ └── 20200214005535_add_entry_id_for_field_values1.rb │ │ │ ├── schema.rb │ │ │ └── seeds.rb │ │ ├── lib/ │ │ │ └── assets/ │ │ │ └── .keep │ │ ├── log/ │ │ │ └── .keep │ │ ├── public/ │ │ │ ├── 404.html │ │ │ ├── 422.html │ │ │ └── 500.html │ │ └── storage/ │ │ └── .keep │ ├── fixtures/ │ │ └── wf/ │ │ ├── case_assignments.yml │ │ ├── comments.yml │ │ ├── demo_targets.yml │ │ ├── entries.yml │ │ ├── field_values.yml │ │ ├── fields.yml │ │ ├── forms.yml │ │ ├── guards.yml │ │ ├── parties.yml │ │ ├── transition_static_assignments.yml │ │ ├── users.yml │ │ └── workitem_assignments.yml │ ├── integration/ │ │ └── navigation_test.rb │ ├── models/ │ │ └── wf/ │ │ ├── case_assignment_test.rb │ │ ├── comment_test.rb │ │ ├── demo_target_test.rb │ │ ├── entry_test.rb │ │ ├── field_test.rb │ │ ├── field_value_test.rb │ │ ├── form_test.rb │ │ ├── guard_test.rb │ │ ├── party_test.rb │ │ ├── transition_static_assignment_test.rb │ │ ├── user_test.rb │ │ ├── wf_test.rb │ │ └── workitem_assignment_test.rb │ └── test_helper.rb └── wf.gemspec ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # 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 ================================================ FILE: .gitattributes ================================================ *.rb diff=ruby *.gemspec diff=ruby ================================================ FILE: .github/workflows/ci.yml ================================================ name: Testing on: [push, pull_request] jobs: build: runs-on: ubuntu-latest services: db: image: postgres:11 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: postgres ports: ['5432:5432'] options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v2 - name: Set up Ruby 2.6 uses: actions/setup-ruby@v1 with: ruby-version: "2.6" - name: Build and test with Rake env: DATABASE_URL: "postgresql://postgres:postgres@127.0.0.1:5432/postgres" POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: postgres RAILS_ENV: test run: | sudo apt-get -yqq install libpq-dev sudo apt-get install libmysqlclient-dev sudo apt-get install graphviz gem install bundler bundle install --jobs 4 --retry 3 bundle exec rake app:wf bundle exec rails app:db:create && bundle exec rails app:db:migrate && bundle exec rails test ================================================ FILE: .github/workflows/gempush.yml ================================================ name: Ruby Gem on: push: tags: - v* jobs: build: name: Build + Publish runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Ruby 2.6 uses: actions/setup-ruby@v1 with: ruby-version: "2.6.x" - name: Publish to RubyGems run: | mkdir -p $HOME/.gem touch $HOME/.gem/credentials chmod 0600 $HOME/.gem/credentials printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials gem build *.gemspec gem push *.gem env: GEM_HOST_API_KEY: ${{secrets.RUBYGEMS_API_KEY}} ================================================ FILE: .gitignore ================================================ # 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. /.idea .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 # Ignore compiled mruby in dummy app /test/dummy/mruby/bin pkg/ .byebug_history node_modules/ test/dummy/public/packs test/dummy/node_modules/ yarn-error.log *.gem .env ================================================ FILE: .rubocop.yml ================================================ require: - rubocop-performance - rubocop-rails AllCops: TargetRubyVersion: 2.5 Exclude: - bin/**/* - test/dummy/bin/**/* - test/dummy/db/schema.rb Rails: Enabled: true Layout/LineLength: Max: 150 Metrics/MethodLength: Max: 100 Metrics/BlockLength: Max: 50 Metrics/ClassLength: Enabled: false Style/GuardClause: Enabled: false Style/Documentation: Enabled: false Style/ClassAndModuleChildren: Enabled: false Naming/AccessorMethodName: Enabled: false Naming/MemoizedInstanceVariableName: Enabled: false # Prefer assert_not over assert ! Rails/AssertNot: Include: - 'test/**/*' # Prefer assert_not_x over refute_x Rails/RefuteMethods: Include: - 'test/**/*' # 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 EnforcedStyle: context_dependent # Align `when` with `case`. Layout/CaseIndentation: Enabled: true # Align comments with method definitions. Layout/CommentIndentation: Enabled: true Layout/ElseAlignment: 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 AutoCorrect: true Layout/EmptyLineAfterMagicComment: Enabled: true Layout/EmptyLinesAroundBlockBody: 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 Layout/FirstArgumentIndentation: 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: indented_internal_methods # Two spaces, no tabs (for indentation). Layout/IndentationWidth: Enabled: true Layout/LeadingCommentSpace: Enabled: true Layout/SpaceAfterColon: Enabled: true Layout/SpaceAfterComma: Enabled: true Layout/SpaceAfterSemicolon: Enabled: true Layout/SpaceAroundEqualsInParameterDefault: Enabled: true Layout/SpaceAroundKeyword: Enabled: true Layout/SpaceAroundOperators: Enabled: true Layout/SpaceBeforeComma: Enabled: true Layout/SpaceBeforeFirstArg: Enabled: true Style/DefWithParentheses: Enabled: true # Defining a method with parameters needs parentheses. Style/MethodDefParentheses: Enabled: true Style/FrozenStringLiteralComment: Enabled: true EnforcedStyle: always Style/RedundantFreeze: Enabled: true # Use `foo {}` not `foo{}`. Layout/SpaceBeforeBlockBraces: Enabled: true # Use `foo { bar }` not `foo {bar}`. Layout/SpaceInsideBlockBraces: Enabled: true EnforcedStyleForEmptyBraces: space # Use `{ a: 1 }` not `{a:1}`. Layout/SpaceInsideHashLiteralBraces: Enabled: true 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/TrailingEmptyLines: Enabled: true # No trailing whitespace. Layout/TrailingWhitespace: Enabled: true # Use quotes for string literals when they are enough. Style/RedundantPercentQ: Enabled: true Lint/AmbiguousOperator: Enabled: true Lint/AmbiguousRegexpLiteral: Enabled: true Lint/ErbNewArguments: Enabled: true # Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg. Lint/RequireParentheses: Enabled: true Lint/ShadowingOuterLocalVariable: Enabled: true Lint/RedundantStringCoercion: Enabled: true Lint/UriEscapeUnescape: Enabled: true Lint/UselessAssignment: Enabled: true Lint/DeprecatedClassMethods: Enabled: true Style/ParenthesesAroundCondition: Enabled: true Style/RedundantBegin: Enabled: true Style/RedundantReturn: Enabled: true AllowMultipleReturnValues: true Style/Semicolon: Enabled: true AllowAsExpressionSeparator: true # Prefer Foo.method over Foo::method Style/ColonMethodCall: Enabled: true Style/TrivialAccessors: Enabled: true Performance/FlatMap: Enabled: true Performance/RedundantMerge: Enabled: true Performance/StartWith: Enabled: true Performance/EndWith: Enabled: true Performance/RegexpMatch: Enabled: true Performance/ReverseEach: Enabled: true Performance/UnfreezeString: Enabled: true ================================================ FILE: FormSpec.md ================================================ ## Why The core of Petri Flow is the workflow engine. However, workflow, dynamic forms, and organization systems are inseparable. Therefore, petri flow provides simple built-in dynamic form functions, but in practice dynamic forms require more complex features. Such as selecting data from other data sources, data validation, application-oriented data fields, UI customization, etc. Petri Flow abstracts the interface needed to integrate with dynamic forms, in order to integrate more complex dynamic forms systems, such as [form core](https://github.com/rails-engine/form_core). ## Core Entity * Form * Field * Entry * FieldValue ## Relations * form has_many fields * form has_many entries * field belongs_to form * field_value belongs_to form * field_value belongs_to field * field_value belongs_to entry * entry belongs_to form * entry belongs_to user * entry belongs_to workitem * entry has_many field_values ## Casting * Field#cast(value) * FieldValue#value_after_cast ## Setting ```ruby # config/initializers/wf_config.rb Wf.user_class = "::Wf::User" WF.form_class = "::Form" Wf.entry_class = "::Entry" Wf.field_class = "::Field" ``` ================================================ FILE: Gemfile ================================================ # frozen_string_literal: true source "https://rubygems.org" git_source(:github) { |repo| "https://github.com/#{repo}.git" } # Declare your gem's dependencies in wf.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 "annotate" gem "bootstrap", "~> 4.4.1" gem "bootstrap4-kaminari-views" gem "jquery-rails" gem "kaminari" gem "pg" gem "pry-rails" gem "simple_command" gem "loaf" gem "mysql2" gem "rubocop" gem "rubocop-performance" gem "rubocop-rails" gem "ruby-graphviz", require: "graphviz" ================================================ FILE: Guard.md ================================================ ## Guard Expression There are two per-defined variables for your guard expression, `workitem`, `target`. Schema for `workitem`: ```json { "id":1, "case_id":1, "workflow_id":10, "transition_id":24, "state":"enabled", "enabled_at":"2020-02-24T12:37:28.459Z", "started_at":null, "canceled_at":null, "finished_at":null, "overridden_at":null, "deadline":null, "created_at":"2020-02-24T12:37:28.601Z", "updated_at":"2020-02-24T12:37:28.601Z", "trigger_time":null, "holding_user_id":null, "children_count":3, "children_finished_count":1, "forked":false, "parent_id":null, "holding_user":{ }, "form":{ }, "children":[ { "id":3, "case_id":1, "workflow_id":10, "transition_id":24, "state":"enabled", "enabled_at":"2020-02-24T12:37:28.459Z", "started_at":null, "canceled_at":null, "finished_at":null, "overridden_at":null, "deadline":null, "created_at":"2020-02-24T12:37:28.757Z", "updated_at":"2020-02-24T12:37:28.757Z", "trigger_time":null, "holding_user_id":"7", "children_count":0, "children_finished_count":0, "forked":true, "parent_id":1, "holding_user":{ "id":7, "name":"User6", "created_at":"2020-02-24T12:36:46.994Z", "updated_at":"2020-02-24T12:36:46.994Z", "group_id":2 }, "form":{ }, "children":[ ] }, { "id":4, "case_id":1, "workflow_id":10, "transition_id":24, "state":"enabled", "enabled_at":"2020-02-24T12:37:28.459Z", "started_at":null, "canceled_at":null, "finished_at":null, "overridden_at":null, "deadline":null, "created_at":"2020-02-24T12:37:28.788Z", "updated_at":"2020-02-24T12:37:28.788Z", "trigger_time":null, "holding_user_id":"5", "children_count":0, "children_finished_count":0, "forked":true, "parent_id":1, "holding_user":{ "id":5, "name":"User4", "created_at":"2020-02-24T12:36:46.984Z", "updated_at":"2020-02-24T12:36:46.984Z", "group_id":1 }, "form":{ }, "children":[ ] }, { "id":2, "case_id":1, "workflow_id":10, "transition_id":24, "state":"finished", "enabled_at":"2020-02-24T12:37:28.459Z", "started_at":null, "canceled_at":null, "finished_at":"2020-02-24T12:37:46.221Z", "overridden_at":null, "deadline":null, "created_at":"2020-02-24T12:37:28.700Z", "updated_at":"2020-02-24T12:37:46.232Z", "trigger_time":null, "holding_user_id":"1", "children_count":0, "children_finished_count":0, "forked":true, "parent_id":1, "holding_user":{ "id":1, "name":"User0", "created_at":"2020-02-24T12:36:46.963Z", "updated_at":"2020-02-24T12:36:46.963Z", "group_id":4 }, "form":{ "score":90 }, "children":[ ] } ] } ``` ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Hooopo 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. ================================================ FILE: MIT-LICENSE ================================================ Copyright 2020 Hooopo Wang 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. ================================================ FILE: README.md ================================================ # Petri Flow ![Ruby Gem](https://github.com/hooopo/petri_flow/workflows/Ruby%20Gem/badge.svg?event=push) ![Testing](https://github.com/hooopo/petri_flow/workflows/Testing/badge.svg?event=push) Workflow engine for Rails. ## Features * Full petri net features support (seq, parallel, iterative, timed, automitic etc.) * Both approval workflow and business workflow. * Simple web admin for workflow definition and case management. * Build-in simple dynamic form. * Replaceable dynamic form. * Support sub workflow. * Graph screen for workflow definition. * Graph screen for case and token migration. * Powerful guard expression. * MySQL and Postgres Support. * Powerful assignment management. * Flexible integration of organizational structure system(role, group, position or department etc.) ## Docs * [Petri-Nets and Workflows](https://hooopo.gitbook.io/petri-flow/) * [Workflow Conceptual Guide](https://hooopo.gitbook.io/petri-flow/workflow-conceptual-guide) * [Workflow Concepts Reference](https://hooopo.gitbook.io/petri-flow/workflow-concepts-reference) * [Petri Flow ERD](https://hooopo.gitbook.io/petri-flow/erd) * [Developer Doc](https://hooopo.gitbook.io/petri-flow/developer-document) ## Screenshots ### iterative routing ![](https://blobscdn.gitbook.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-M-GhlU_QaD6nbLAbaJI%2F-M-X0nIxUUBwJsNhY4FN%2F-M-XAAQJbxDdaxoaYVda%2Fimage.png?alt=media&token=e74d1ae7-fa16-47ab-83b5-ad73a382fa07) ### parallel_routing ![](https://blobscdn.gitbook.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-M-GhlU_QaD6nbLAbaJI%2F-M-X0nIxUUBwJsNhY4FN%2F-M-XAKm9VN1MJxPZT9Xe%2Fimage.png?alt=media&token=c8beba84-72ec-470f-9987-81cf40762e15) ### guard ![](https://blobscdn.gitbook.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-M-GhlU_QaD6nbLAbaJI%2F-M-X0nIxUUBwJsNhY4FN%2F-M-XAT8Ui_xjqy9Niccp%2Fimage.png?alt=media&token=de4298fb-14b9-40bc-ab75-92ef0b98a533) ### case state graph ![](https://blobscdn.gitbook.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-M-GhlU_QaD6nbLAbaJI%2F-M-X0nIxUUBwJsNhY4FN%2F-M-XAeeR42ZRVIVKuUae%2Fimage.png?alt=media&token=90c96af9-d01f-4d6e-ae2b-445ea343a5ac) ### ## Installation Add this line to your application's Gemfile: ```ruby gem 'petri_flow', require: 'wf' ``` And then execute: ```bash $ bundle ``` Install graphviz ``` brew install graphviz ``` Migration: ``` bundle exec rake wf:install:migrations bundle exec rails db:create bundle exec rails db:migrate bundle exec rails db:seed ``` ## Usage Add wf_config: ```ruby # config/initializers/wf_config.rb Wf.user_class = "::User" Wf.org_classes = { group: "::Group" } ``` Set parties: For normal org model, for example group or role etc. ```ruby module Wf class Group < ApplicationRecord has_many :users include Wf::ActsAsParty acts_as_party(user: false, party_name: :name) end end ``` For user model: ```ruby module Wf class User < ApplicationRecord belongs_to :group, optional: true include Wf::ActsAsParty acts_as_party(user: true, party_name: :name) end end ``` then ``` bundle exec rails ``` visit: ``` http://localhost:3000/wf ``` ## Testing * RAILS_ENV=test rake app:db:migrate && RAILS_ENV=test rake app:db:test:prepare && bundle exec rake test ## Contributing Contribution directions go here. ## License The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). ================================================ FILE: Rakefile ================================================ # 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 = "Wf" 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 ================================================ FILE: acts_as_party.md ================================================ ## Usage for normal org model, for example group or role etc. ```ruby module Wf class Group < ApplicationRecord has_many :users include Wf::ActsAsParty acts_as_party(user: false, party_name: :name) end end ``` for user model: ```ruby module Wf class User < ApplicationRecord belongs_to :group, optional: true include Wf::ActsAsParty acts_as_party(user: true, party_name: :name) end end ``` ================================================ FILE: app/assets/config/wf_manifest.js ================================================ //= link_directory ../stylesheets/wf .css //= link_directory ../javascripts/wf .js ================================================ FILE: app/assets/images/wf/.keep ================================================ ================================================ FILE: app/assets/javascripts/wf/application.js ================================================ //= require jquery3 //= require popper //= require bootstrap //= require rails-ujs //= require select2-full ================================================ FILE: app/assets/stylesheets/wf/application.scss ================================================ /* *= require select2 *= require select2-bootstrap4 */ @import "uikit/index"; main{ min-height: 80vh; h2 { margin-bottom: 1rem; } } .footer{ text-align: center; .container{ border-top: 1px solid rgb(245, 239, 228); padding-top: 1rem; } a{ color: $gray-10; } } ================================================ FILE: app/assets/stylesheets/wf/arcs.css ================================================ /* Place all the styles related to the matching controller here. They will automatically be included in application.css. */ ================================================ FILE: app/assets/stylesheets/wf/cases.css ================================================ /* Place all the styles related to the matching controller here. They will automatically be included in application.css. */ ================================================ FILE: app/assets/stylesheets/wf/comments.css ================================================ /* Place all the styles related to the matching controller here. They will automatically be included in application.css. */ ================================================ FILE: app/assets/stylesheets/wf/fields.css ================================================ /* Place all the styles related to the matching controller here. They will automatically be included in application.css. */ ================================================ FILE: app/assets/stylesheets/wf/forms.css ================================================ /* Place all the styles related to the matching controller here. They will automatically be included in application.css. */ ================================================ FILE: app/assets/stylesheets/wf/guards.css ================================================ /* Place all the styles related to the matching controller here. They will automatically be included in application.css. */ ================================================ FILE: app/assets/stylesheets/wf/places.css ================================================ /* Place all the styles related to the matching controller here. They will automatically be included in application.css. */ ================================================ FILE: app/assets/stylesheets/wf/static_assignments.css ================================================ /* Place all the styles related to the matching controller here. They will automatically be included in application.css. */ ================================================ FILE: app/assets/stylesheets/wf/transitions.css ================================================ /* Place all the styles related to the matching controller here. They will automatically be included in application.css. */ ================================================ FILE: app/assets/stylesheets/wf/uikit/_colors.scss ================================================ // fork from: https://github.com/oortcast/42page/blob/master/app/javascript/42design/ // // inspired by https://github.com/ant-design/ant-design // // The [colorPalette] means that the color is // generated by https://ant.design/docs/spec/colors#Palette-Generation-Tool // Brand Color $pagegreen-base: #00d192; $pagegreen-1: #f6fdfb; // tint background color $pagegreen-2: #cff5e8; // background color $pagegreen-3: #77f7c4; // [colorPalette] $pagegreen-4: #4bebb0; // [colorPalette] $pagegreen-5: #20CF97; // hover $pagegreen-6: $pagegreen-base; // normal $pagegreen-7: #00ab74; // click $pagegreen-8: #008566; // [colorPalette] $pagegreen-9: #005e4b; // [colorPalette] $pagegreen-10: #00382f; // [colorPalette] // Neutral Color $gray-1: #fff; $gray-2: #f7f7f7; // $gray-3: #f1f1f2; // table header $gray-4: #d2d3d4; // disable background $gray-5: #92989c; // border $gray-6: #868b8f; // disable text $gray-7: #7a7e81; // secondary text $gray-8: #53585c; $gray-9: #23292f; // primary text $gray-10: #000; // title // Color Palette $red-base: #fa4a4a; $red-1: #fff2f0; // [colorPalette] $red-2: #fff2f0; $red-3: #ffcdc7; // [colorPalette] $red-4: #ffa59e; // [colorPalette] $red-5: #ff7a75; // [colorPalette] $red-6: $red-base; $red-7: #d4353a; // [colorPalette] $red-8: #ad232c; // [colorPalette] $red-9: #871420; // [colorPalette] $red-10: #610e19; // [colorPalette] $yellow-base: #F9C84A; $yellow-1: #fffdf0; // [colorPalette] $yellow-2: #FFF3E0; $yellow-3: #fff5c7; // [colorPalette] $yellow-4: #ffea9e; // [colorPalette] $yellow-5: #ffdd75; // mark $yellow-6: $yellow-base; $yellow-7: #d4a135; // [colorPalette] $yellow-8: #ad7d23; // [colorPalette] $yellow-9: #875b14; // [colorPalette] $yellow-10: #613e0e; // [colorPalette] $blue-base: #3d90eb; $blue-1: #f0faff; // [colorPalette] $blue-2: #e6f5ff; $blue-3: #bde3ff; // [colorPalette] $blue-4: #94cfff; // [colorPalette] $blue-5: #68b2f7; // [colorPalette] $blue-6: $blue-base; $blue-7: #296fc4; // [colorPalette] $blue-8: #19519e; // [colorPalette] $blue-9: #0d3678; // [colorPalette] $blue-10: #082252; // [colorPalette] $cyan-base: #01C1B2; $cyan-1: #e6fff9; // [colorPalette] $cyan-2: #E3FFF8; $cyan-3: #72e8d2; // [colorPalette] $cyan-4: #48dbc5; // [colorPalette] $cyan-5: #23cfbb; // [colorPalette] $cyan-6: $cyan-base; $cyan-7: #009c94; // [colorPalette] $cyan-8: #007573; // [colorPalette] $cyan-9: #004e4f; // [colorPalette] $cyan-10: #002729; // [colorPalette] ================================================ FILE: app/assets/stylesheets/wf/uikit/_variables.scss ================================================ @import "colors"; @import "bootstrap/functions"; $blue: $blue-base; $red: $red-base; $yellow: $yellow-base; $green: $pagegreen-base; $cyan: $cyan-base; $primary: $green; $gray-100: $gray-1; $gray-200: $gray-2; $gray-300: $gray-3; // table header $gray-400: $gray-4; // disable background $gray-500: $gray-5; // border $gray-600: $gray-6; // disable text $gray-700: $gray-7; // secondary text $gray-800: $gray-8; $gray-900: $gray-9; // primary text $secondary: $gray-7; $body-bg: #fffefd; $link-color: $primary; $link-hover-color: $pagegreen-5; $font-family-sans-serif: system, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", "Segoe UI Symbol"; $font-family-monospace: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; $line-height-base: 1.6; @import "bootstrap/variables"; ================================================ FILE: app/assets/stylesheets/wf/uikit/alert.scss ================================================ @import "variables"; .alert{ margin-bottom: 2rem; p{ margin-bottom: 0; } &.alert-success{ background: $pagegreen-1; border-color: $pagegreen-4; color: $gray-9; } &.alert-warning{ background: $red-1; border-color: $red-5; color: $gray-9; } } ================================================ FILE: app/assets/stylesheets/wf/uikit/button.scss ================================================ @import "_variables"; .btn-light{ color: $gray-8; background: rgba(94, 94, 94, 0.2); border: rgb(94,94,94); } ================================================ FILE: app/assets/stylesheets/wf/uikit/card.scss ================================================ .card{ margin-bottom: 4rem; border: 1px solid rgba(230, 230, 230, 0.41); border-radius: 0.625rem; box-shadow: 0 2px 6px 0 rgba(0,0,0,0.05); } ================================================ FILE: app/assets/stylesheets/wf/uikit/index.scss ================================================ @import "_variables"; @import "bootstrap"; //overwrite @import "navbar"; @import "table"; @import "card"; @import "alert"; @import "button"; ================================================ FILE: app/assets/stylesheets/wf/uikit/navbar.scss ================================================ @import "variables"; .navbar-petri{ box-shadow: 0 12px 24px 0 rgba(0,0,0,0.05); margin-bottom: 2rem; background: $white; .navbar-brand a{ color: $gray-9; &:hover{ text-decoration: none; color: $gray-10; } } .navbar-end a{ color: #7b7f82; margin: 0 20px; font-size: 18px; border-bottom: transparent solid 2px; &:hover{ color: $gray-9; text-decoration: none; } &.is-active{ color: $gray-9; border-bottom: $primary solid 2px; } } } ================================================ FILE: app/assets/stylesheets/wf/uikit/table.scss ================================================ @import "variables"; .table-view{ thead th{ color: $gray-7; font-weight: 500; border-top: 0; border-bottom-width: 1px; border-color: $gray-3; } tbody tr:hover { background-color: $gray-2; } tbody td a:not(.btn) { color: $gray-10; } } ================================================ FILE: app/assets/stylesheets/wf/workflows.css ================================================ /* Place all the styles related to the matching controller here. They will automatically be included in application.css. */ ================================================ FILE: app/assets/stylesheets/wf/workitem_assignments.css ================================================ /* Place all the styles related to the matching controller here. They will automatically be included in application.css. */ ================================================ FILE: app/assets/stylesheets/wf/workitems.css ================================================ /* Place all the styles related to the matching controller here. They will automatically be included in application.css. */ ================================================ FILE: app/controllers/wf/application_controller.rb ================================================ # frozen_string_literal: true module Wf class ApplicationController < ::ApplicationController protect_from_forgery with: :exception helper_method :wf_current_user breadcrumb "Home", :root_path def wf_current_user current_user end end end ================================================ FILE: app/controllers/wf/arcs_controller.rb ================================================ # frozen_string_literal: true require_dependency "wf/application_controller" module Wf class ArcsController < ApplicationController breadcrumb "Workflows", :workflows_path def new @workflow = Wf::Workflow.find(params[:workflow_id]) @arc = @workflow.arcs.new breadcrumb @workflow.name, workflow_path(@workflow) end def create @workflow = Wf::Workflow.find(params[:workflow_id]) @arc = @workflow.arcs.new(arc_params) if @arc.save redirect_to workflow_path(@workflow), notice: "arc was successfully created." else render :new end end def destroy @workflow = Wf::Workflow.find(params[:workflow_id]) @arc = @workflow.arcs.find(params[:id]) @arc.destroy render js: "window.location.reload()" end def show @workflow = Wf::Workflow.find(params[:workflow_id]) @arc = @workflow.arcs.find(params[:id]) breadcrumb @workflow.name, workflow_path(@workflow) end def edit @workflow = Wf::Workflow.find(params[:workflow_id]) @arc = @workflow.arcs.find(params[:id]) breadcrumb @workflow.name, workflow_path(@workflow) breadcrumb @arc.name, workflow_arc_path(@workflow, @arc) end def update @workflow = Wf::Workflow.find(params[:workflow_id]) @arc = @workflow.arcs.find(params[:id]) if @arc.update(arc_params) redirect_to workflow_path(@workflow), notice: "arc was successfully created." else render :edit end end private def arc_params params.fetch(:arc, {}).permit(:direction, :transition_id, :place_id) end end end ================================================ FILE: app/controllers/wf/cases_controller.rb ================================================ # frozen_string_literal: true require_dependency "wf/application_controller" module Wf class CasesController < ApplicationController breadcrumb "Workflows", :workflows_path def new @workflow = Wf::Workflow.find(params[:workflow_id]) @wf_case = @workflow.cases.new breadcrumb @workflow.name, workflow_path(@workflow) end def create @workflow = Wf::Workflow.find(params[:workflow_id]) @wf_case = Wf::CaseCommand::New.call(@workflow, GlobalID::Locator.locate(case_params[:targetable])).result Wf::CaseCommand::StartCase.call(@wf_case) redirect_to workflow_cases_path(@workflow), notice: "case created." end def index @workflow = Wf::Workflow.find(params[:workflow_id]) @cases = @workflow.cases.order("id DESC") @cases = @cases.where(state: params[:state].intern) if params[:state].present? @cases = @cases.page(params[:page]) breadcrumb @workflow.name, workflow_path(@workflow) end def show @workflow = Wf::Workflow.find(params[:workflow_id]) @wf_case = @workflow.cases.find(params[:id]) breadcrumb @workflow.name, workflow_path(@workflow) end def destroy @workflow = Wf::Workflow.find(params[:workflow_id]) @case = @workflow.cases.find(params[:id]) @case.destroy render js: "window.location.reload()" end private def case_params params.fetch(:case, {}).permit(:targetable, :target_id, :target_type) end end end ================================================ FILE: app/controllers/wf/comments_controller.rb ================================================ # frozen_string_literal: true require_dependency "wf/application_controller" module Wf class CommentsController < ApplicationController breadcrumb "Workflows", :workflows_path def new @workitem = Wf::Workitem.find(params[:workitem_id]) @comment = @workitem.comments.new breadcrumb @workitem.workflow.name, workflow_path(@workitem.workflow) breadcrumb @workitem.case.name, workflow_case_path(@workitem.workflow, @workitem.case) breadcrumb @workitem.name, workitem_path(@workitem) end def create @workitem = Wf::Workitem.find(params[:workitem_id]) Wf::CaseCommand::AddComment.call(@workitem, params[:comment][:body], wf_current_user) redirect_to workitem_path(@workitem), notice: "Comment Added." end def destroy @workitem = Wf::Workitem.find(params[:workitem_id]) @comment = @workitem.comments.find(params[:id]) @comment.destroy render js: "window.location.reload()" end end end ================================================ FILE: app/controllers/wf/fields_controller.rb ================================================ # frozen_string_literal: true require_dependency "wf/application_controller" module Wf class FieldsController < ApplicationController breadcrumb "Forms", :forms_path def new @form = Wf::Form.find(params[:form_id]) @field = @form.fields.new breadcrumb @form.name, form_path(@form) end def create @form = Wf::Form.find(params[:form_id]) @field = @form.fields.new(field_params) if @field.save redirect_to form_path(@form), notice: "field was successfully created." else render :new end end def destroy @form = Wf::Form.find(params[:form_id]) @field = @form.fields.find(params[:id]) @field.destroy render js: "window.location.reload()" end def edit @form = Wf::Form.find(params[:form_id]) @field = @form.fields.find(params[:id]) breadcrumb @form.name, form_path(@form) end def update @form = Wf::Form.find(params[:form_id]) @field = @form.fields.find(params[:id]) if @field.update(field_params) redirect_to form_path(@form), notice: "field was successfully created." else render :edit end end private def field_params params.fetch(:field, {}).permit(:name, :form_id, :field_type, :position, :default_value) end end end ================================================ FILE: app/controllers/wf/forms_controller.rb ================================================ # frozen_string_literal: true require_dependency "wf/application_controller" module Wf class FormsController < ApplicationController breadcrumb "Forms", :forms_path def index @forms = Wf::Form.order("id DESC").page(params[:page]) end def new @form = Wf::Form.new end def edit @form = Wf::Form.find(params[:id]) end def show @form = Wf::Form.find(params[:id]) end def destroy @form = Wf::Form.find(params[:id]) @form.destroy respond_to do |format| format.html { redirect_to forms_path, notice: "form was successfully deleted." } format.js { render js: "window.location.reload();" } end end def update @form = Wf::Form.find(params[:id]) if @form.update(form_params) redirect_to form_path(@form), notice: "form was successfully updated." else render :edit end end def create @form = Wf::Form.new(form_params) if @form.save redirect_to forms_path, notice: "form was successfully created." else render :new end end private def form_params params.fetch(:form, {}).permit(:name, :description) end end end ================================================ FILE: app/controllers/wf/guards_controller.rb ================================================ # frozen_string_literal: true require_dependency "wf/application_controller" module Wf class GuardsController < ApplicationController breadcrumb "Workflows", :workflows_path def new @arc = Wf::Arc.find(params[:arc_id]) @guard = @arc.guards.new breadcrumb @arc.workflow.name, workflow_path(@arc.workflow) breadcrumb @arc.name, workflow_arc_path(@arc.workflow, @arc) end def create @arc = Wf::Arc.find(params[:arc_id]) gp = guard_params.merge(fieldable: GlobalID::Locator.locate(guard_params[:fieldable])) @guard = @arc.guards.new(gp.merge(workflow: @arc.workflow)) redirect_to workflow_arc_path(@arc.workflow, @arc), notice: "only out direction arc can set guard!" unless @arc.out? if @guard.save redirect_to workflow_arc_path(@arc.workflow, @arc), notice: "guard was successfully created." else render :new end end def destroy @arc = Wf::Arc.find(params[:arc_id]) @guard = @arc.guards.find(params[:id]) @guard.destroy render js: "window.location.reload()" end def edit @arc = Wf::Arc.find(params[:arc_id]) @guard = @arc.guards.find(params[:id]) breadcrumb @arc.workflow.name, workflow_path(@arc.workflow) breadcrumb @arc.name, workflow_arc_path(@arc.workflow, @arc) end def update @arc = Wf::Arc.find(params[:arc_id]) gp = guard_params.merge(fieldable: GlobalID::Locator.locate(guard_params[:fieldable])) @guard = @arc.guards.find(params[:id]) if @guard.update(gp) redirect_to workflow_arc_path(@arc.workflow, @arc), notice: "guard was successfully created." else render :edit end end private def guard_params params.fetch(:guard, {}).permit(:fieldable, :fieldable_type, :fieldable_id, :op, :value, :exp) end end end ================================================ FILE: app/controllers/wf/places_controller.rb ================================================ # frozen_string_literal: true require_dependency "wf/application_controller" module Wf class PlacesController < ApplicationController breadcrumb "Workflows", :workflows_path def new @workflow = Wf::Workflow.find(params[:workflow_id]) @place = @workflow.places.new breadcrumb @workflow.name, workflow_path(@workflow) end def create @workflow = Wf::Workflow.find(params[:workflow_id]) @place = @workflow.places.new(place_params) if @place.save redirect_to workflow_path(@workflow), notice: "place was successfully created." else render :new end end def destroy @workflow = Wf::Workflow.find(params[:workflow_id]) @place = @workflow.places.find(params[:id]) @place.destroy render js: "window.location.reload()" end def edit @workflow = Wf::Workflow.find(params[:workflow_id]) @place = @workflow.places.find(params[:id]) breadcrumb @workflow.name, workflow_path(@workflow) end def update @workflow = Wf::Workflow.find(params[:workflow_id]) @place = @workflow.places.find(params[:id]) if @place.update(place_params) redirect_to workflow_path(@workflow), notice: "place was successfully created." else render :edit end end private def place_params params.fetch(:place, {}).permit(:name, :description, :place_type, :sort_order) end end end ================================================ FILE: app/controllers/wf/static_assignments_controller.rb ================================================ # frozen_string_literal: true require_dependency "wf/application_controller" module Wf class StaticAssignmentsController < ApplicationController def new @transition = Wf::Transition.find(params[:transition_id]) @static_assignment = @transition.transition_static_assignments.new end def create @transition = Wf::Transition.find(params[:transition_id]) @party = Wf::Party.find(permit_params[:party_id]) @static_assignment = @transition.transition_static_assignments.new(party: @party) if @static_assignment.save redirect_to workflow_transition_path(@transition.workflow, @transition), notice: "static assignment was successfully created." else render :new end end def destroy @transition = Wf::Transition.find(params[:transition_id]) @static_assignment = @transition.transition_static_assignments.find(params[:id]) @static_assignment.destroy render js: "window.location.reload()" end def permit_params params.fetch(:transition_static_assignment, {}).permit(:party_id) end end end ================================================ FILE: app/controllers/wf/transitions_controller.rb ================================================ # frozen_string_literal: true require_dependency "wf/application_controller" module Wf class TransitionsController < ApplicationController breadcrumb "Workflows", :workflows_path def new @workflow = Wf::Workflow.find(params[:workflow_id]) @transition = @workflow.transitions.new breadcrumb @workflow.name, workflow_path(@workflow) end def show @workflow = Wf::Workflow.find(params[:workflow_id]) @transition = @workflow.transitions.find(params[:id]) end def create @workflow = Wf::Workflow.find(params[:workflow_id]) tp = transition_params.merge(form: GlobalID::Locator.locate(transition_params[:form])) @transition = @workflow.transitions.new(tp) if @transition.save redirect_to workflow_path(@workflow), notice: "transition was successfully created." else render :new end end def edit @workflow = Wf::Workflow.find(params[:workflow_id]) @transition = @workflow.transitions.find(params[:id]) breadcrumb @workflow.name, workflow_path(@workflow) end def destroy @workflow = Wf::Workflow.find(params[:workflow_id]) @transition = @workflow.transitions.find(params[:id]) @transition.destroy render js: "window.location.reload()" end def update @workflow = Wf::Workflow.find(params[:workflow_id]) @transition = @workflow.transitions.find(params[:id]) tp = transition_params.merge(form: GlobalID::Locator.locate(transition_params[:form])) if @transition.update(tp) redirect_to workflow_path(@workflow), notice: "transition was successfully updated." else render :edit end end private def transition_params params.fetch(:transition, {}).permit( :name, :description, :trigger_limit, :trigger_type, :sort_order, :form, :enable_callback, :fire_callback, :time_callback, :hold_timeout_callback, :assignment_callback, :unassignment_callback, :notification_callback, :deadline_callback, :sub_workflow_id, :multiple_instance, :finish_condition, :dynamic_assign_by_id ) end end end ================================================ FILE: app/controllers/wf/workflows_controller.rb ================================================ # frozen_string_literal: true require_dependency "wf/application_controller" module Wf class WorkflowsController < ApplicationController breadcrumb "Workflows", :workflows_path def index @workflows = Wf::Workflow.order("id DESC").page(params[:page]) end def new @workflow = Wf::Workflow.new end def edit @workflow = Wf::Workflow.find(params[:id]) breadcrumb @workflow.name, workflow_path(@workflow) end def show @workflow = Wf::Workflow.find(params[:id]) end def destroy @workflow = Wf::Workflow.find(params[:id]) @workflow.destroy respond_to do |format| format.html { redirect_to workflows_path, notice: "workflow was successfully deleted." } format.js { render js: "window.location.reload();" } end end def update @workflow = Wf::Workflow.find(params[:id]) if @workflow.update(workflow_params) redirect_to workflow_path(@workflow), notice: "workflow was successfully updated." else render :edit end end def create @workflow = Wf::Workflow.new(workflow_params) if @workflow.save redirect_to workflows_path, notice: "workflow was successfully created." else render :new end end private def workflow_params params.fetch(:workflow, {}).permit(:name, :description) end end end ================================================ FILE: app/controllers/wf/workitem_assignments_controller.rb ================================================ # frozen_string_literal: true require_dependency "wf/application_controller" module Wf class WorkitemAssignmentsController < ApplicationController breadcrumb "Workflows", :workflows_path def new @workitem = Wf::Workitem.find(params[:workitem_id]) @workitem_assignment = @workitem.workitem_assignments.new(party_id: params[:party_id]) breadcrumb @workitem.workflow.name, workflow_path(@workitem.workflow) breadcrumb @workitem.case.name, workflow_case_path(@workitem.workflow, @workitem.case) breadcrumb @workitem.name, workitem_path(@workitem) end def create @workitem = Wf::Workitem.find(params[:workitem_id]) party = Wf::Party.find(params[:workitem_assignment][:party_id]) Wf::CaseCommand::AddWorkitemAssignment.call(@workitem, party) redirect_to workitem_path(@workitem), notice: "assigned party to workitem." end def destroy @workitem = Wf::Workitem.find(params[:workitem_id]) party = Wf::Party.find(params[:party_id]) Wf::CaseCommand::RemoveWorkitemAssignment.call(@workitem, party) render js: "window.location.reload()" end end end ================================================ FILE: app/controllers/wf/workitems_controller.rb ================================================ # frozen_string_literal: true require_dependency "wf/application_controller" module Wf class WorkitemsController < ApplicationController before_action :find_workitem, except: [:index] before_action :check_start, only: [:start] before_action :check_finish, only: %i[pre_finish finish] breadcrumb "Workflows", :workflows_path def index @workitems = Wf::Workitem.todo(wf_current_user) @workitems = @workitems.where(state: params[:state].intern) if params[:state] @workitems = @workitems.where(state: params[:state].intern) if params[:state].present? @workitems = @workitems.distinct.order("id desc").page(params[:page]) end def show breadcrumb @workitem.workflow.name, workflow_path(@workitem.workflow) breadcrumb @workitem.case.name, workflow_case_path(@workitem.workflow, @workitem.case) end def start Wf::CaseCommand::StartWorkitem.call(@workitem, wf_current_user) breadcrumb @workitem.workflow.name, workflow_path(@workitem.workflow) breadcrumb @workitem.case.name, workflow_case_path(@workitem.workflow, @workitem.case) breadcrumb @workitem.name, workitem_path(@workitem) render :pre_finish end def pre_finish breadcrumb @workitem.workflow.name, workflow_path(@workitem.workflow) breadcrumb @workitem.case.name, workflow_case_path(@workitem.workflow, @workitem.case) breadcrumb @workitem.name, workitem_path(@workitem) end def finish if dynamic_assignments = params.dig(:workitem, :dynamic_assignments) dynamic_assignments.permit!.each do |t_id, party_id| Wf::CaseCommand::AddManualAssignment.call(@workitem.case, @workitem.workflow.transitions.find(t_id), Wf::Party.find(party_id)) end end if @workitem.transition.form && params[:workitem][:entry] form = @workitem.transition.form cmd = Wf::CaseCommand::CreateEntry.call(form, @workitem, wf_current_user, params[:workitem][:entry].permit!) if cmd.success? Wf::CaseCommand::FinishWorkitem.call(@workitem) finish_and_redirect else redirect_to pre_finish_workitem_path(@workitem), notice: "Your input no OK." end else Wf::CaseCommand::FinishWorkitem.call(@workitem) finish_and_redirect end end def finish_and_redirect if @workitem.case.finished? if started_by = @workitem.case.started_by_workitem redirect_to workflow_case_path(started_by.workflow, started_by.case), notice: "workitem is done, and goto parent case." else redirect_to workflow_case_path(@workitem.workflow, @workitem.case), notice: "workitem is done, and the case is finished." end else redirect_to workitem_path(@workitem.case.workitems.enabled.first), notice: "workitem is done, and goto next fireable workitem." end end def find_workitem @workitem = Wf::Workitem.find(params[:id]) end def check_start unless @workitem.started_by?(wf_current_user) redirect_to workitem_path(@workitem), notice: "You can not start this workitem, Please assign to youself first." end end def check_finish unless @workitem.finished_by?(wf_current_user) redirect_to workitem_path(@workitem), notice: "You can not the holding use of this workitem, Please assign to youself && start it first." end end end end ================================================ FILE: app/helpers/wf/application_helper.rb ================================================ # frozen_string_literal: true module Wf module ApplicationHelper end end ================================================ FILE: app/helpers/wf/arcs_helper.rb ================================================ # frozen_string_literal: true module Wf module ArcsHelper end end ================================================ FILE: app/helpers/wf/cases_helper.rb ================================================ # frozen_string_literal: true module Wf module CasesHelper end end ================================================ FILE: app/helpers/wf/comments_helper.rb ================================================ # frozen_string_literal: true module Wf module CommentsHelper end end ================================================ FILE: app/helpers/wf/fields_helper.rb ================================================ # frozen_string_literal: true module Wf module FieldsHelper end end ================================================ FILE: app/helpers/wf/forms_helper.rb ================================================ # frozen_string_literal: true module Wf module FormsHelper end end ================================================ FILE: app/helpers/wf/guards_helper.rb ================================================ # frozen_string_literal: true module Wf module GuardsHelper end end ================================================ FILE: app/helpers/wf/places_helper.rb ================================================ # frozen_string_literal: true module Wf module PlacesHelper end end ================================================ FILE: app/helpers/wf/static_assignments_helper.rb ================================================ # frozen_string_literal: true module Wf module StaticAssignmentsHelper end end ================================================ FILE: app/helpers/wf/transitions_helper.rb ================================================ # frozen_string_literal: true module Wf module TransitionsHelper end end ================================================ FILE: app/helpers/wf/workflows_helper.rb ================================================ # frozen_string_literal: true module Wf module WorkflowsHelper end end ================================================ FILE: app/helpers/wf/workitem_assignments_helper.rb ================================================ # frozen_string_literal: true module Wf module WorkitemAssignmentsHelper end end ================================================ FILE: app/helpers/wf/workitems_helper.rb ================================================ # frozen_string_literal: true module Wf module WorkitemsHelper end end ================================================ FILE: app/jobs/wf/application_job.rb ================================================ # frozen_string_literal: true module Wf class ApplicationJob < ActiveJob::Base end end ================================================ FILE: app/jobs/wf/fire_timed_workitem_job.rb ================================================ # frozen_string_literal: true module Wf class FireTimedWorkitemJob < ApplicationJob queue_as :default def perform(workitem_id) item = Wf::Workitem.find(workitem_id) if item.trigger_time && item.enabled? && item.case.active? CaseCommand::FireTransitionInternal.call(item) CaseCommand::SweepAutomaticTransitions.call(item.case) end end end end ================================================ FILE: app/mailers/wf/application_mailer.rb ================================================ # frozen_string_literal: true module Wf class ApplicationMailer < ActionMailer::Base default from: "from@example.com" layout "mailer" end end ================================================ FILE: app/models/wf/acts_as_party.rb ================================================ # frozen_string_literal: true require "active_support/concern" module Wf module ActsAsParty extend ActiveSupport::Concern included do has_one :party, as: :partable end module ClassMethods def acts_as_party(options = { user: false, party_name: :name }) cattr_accessor :yaffle_text_field has_many :users, foreign_key: :id if options[:user] after_create do create_party(party_name: options[:party_name]) end end end end end ================================================ FILE: app/models/wf/application_record.rb ================================================ # frozen_string_literal: true module Wf class ApplicationRecord < ActiveRecord::Base self.abstract_class = true end end ================================================ FILE: app/models/wf/arc.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_arcs # # id :integer not null, primary key # workflow_id :integer # transition_id :integer # place_id :integer # direction :integer default("0") # created_at :datetime not null # updated_at :datetime not null # guards_count :integer default("0") # module Wf class Arc < ApplicationRecord belongs_to :workflow, touch: true belongs_to :transition belongs_to :place has_many :guards, dependent: :destroy scope :with_guards, -> { where("guards_count > 0") } scope :without_guards, -> { where(guards_count: 0) } # direction is relative to the transition enum direction: { in: 0, out: 1 } def name if in? [place&.name, transition&.name].join(" -> ") else [transition&.name, place&.name].join(" -> ") end end end end ================================================ FILE: app/models/wf/callbacks/assignment_default.rb ================================================ # frozen_string_literal: true module Wf::Callbacks class AssignmentDefault < ApplicationJob queue_as :default def perform(_workitem_id) # return Party array. [] end end end ================================================ FILE: app/models/wf/callbacks/deadline_default.rb ================================================ # frozen_string_literal: true module Wf::Callbacks class DeadlineDefault < ApplicationJob queue_as :default def perform(*guests) $stdout.puts(guests.inspect) end end end ================================================ FILE: app/models/wf/callbacks/enable_default.rb ================================================ # frozen_string_literal: true module Wf::Callbacks class EnableDefault < ApplicationJob queue_as :default def perform(*guests) $stdout.puts(guests.inspect) end end end ================================================ FILE: app/models/wf/callbacks/fire_default.rb ================================================ # frozen_string_literal: true module Wf::Callbacks class FireDefault < ApplicationJob queue_as :default def perform(*guests) $stdout.puts(guests.inspect) end end end ================================================ FILE: app/models/wf/callbacks/hold_timeout_default.rb ================================================ # frozen_string_literal: true module Wf::Callbacks class HoldTimeoutDefault < ApplicationJob queue_as :default def perform(*guests) $stdout.puts(guests.inspect) end end end ================================================ FILE: app/models/wf/callbacks/notification_default.rb ================================================ # frozen_string_literal: true module Wf::Callbacks class NotificationDefault < ApplicationJob queue_as :default def perform(*guests) $stdout.puts(guests.inspect) end end end ================================================ FILE: app/models/wf/callbacks/time_default.rb ================================================ # frozen_string_literal: true module Wf::Callbacks class TimeDefault < ApplicationJob queue_as :default def perform(*guests) $stdout.puts(guests.inspect) end end end ================================================ FILE: app/models/wf/callbacks/unassignment_default.rb ================================================ # frozen_string_literal: true module Wf::Callbacks class UnassignmentDefault < ApplicationJob queue_as :default def perform(*guests) $stdout.puts(guests.inspect) end end end ================================================ FILE: app/models/wf/case.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_cases # # id :integer not null, primary key # workflow_id :integer # targetable_type :string # targetable_id :string # state :integer default("0") # created_at :datetime not null # updated_at :datetime not null # started_by_workitem_id :integer # module Wf class Case < ApplicationRecord belongs_to :workflow belongs_to :targetable, optional: true, polymorphic: true belongs_to :started_by_workitem, optional: true, class_name: "Wf::Workitem" has_many :workitems has_many :tokens has_many :case_assignments has_many :parties, through: :case_assignments, source: "party" enum state: { created: 0, active: 1, suspended: 2, canceled: 3, finished: 4 } def can_fire?(transition) ins = transition.arcs.in.to_a return false if ins.blank? ins.all? { |arc| arc.place.tokens.where(case: self).where(state: :free).exists? } end def name "Case->#{id}" end end end ================================================ FILE: app/models/wf/case_assignment.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_case_assignments # # id :integer not null, primary key # case_id :integer # transition_id :integer # party_id :integer # created_at :datetime not null # updated_at :datetime not null # # frozen_string_literal: true module Wf class CaseAssignment < ApplicationRecord belongs_to :case belongs_to :transition belongs_to :party end end ================================================ FILE: app/models/wf/case_command/add_comment.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class AddComment prepend SimpleCommand attr_reader :workitem, :comment, :user def initialize(workitem, comment, user) @workitem = workitem @comment = comment @user = user end def call workitem.comments.create!(user: user, body: comment) end end end ================================================ FILE: app/models/wf/case_command/add_manual_assignment.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class AddManualAssignment prepend SimpleCommand attr_reader :wf_case, :transition, :party def initialize(wf_case, transition, party) @wf_case = wf_case @transition = transition @party = party end def call wf_case.case_assignments.find_or_create_by!(transition: transition, party: party) end end end ================================================ FILE: app/models/wf/case_command/add_token.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class AddToken prepend SimpleCommand attr_reader :wf_case, :place def initialize(wf_case, place) @wf_case = wf_case @place = place end def call wf_case.tokens.create!( workflow: wf_case.workflow, place: place, state: :free ) end end end ================================================ FILE: app/models/wf/case_command/add_workitem_assignment.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class AddWorkitemAssignment prepend SimpleCommand attr_reader :workitem, :party, :permanent def initialize(workitem, party, permanent = true) @workitem = workitem @party = party @permanent = permanent end def call return if party.nil? Wf::ApplicationRecord.transaction do AddManualAssignment.call(workitem.case, workitem.transition, party) if permanent notified_users = workitem.parties.map do |p| p.partable.users.to_a end.flatten assign = workitem.workitem_assignments.where(party: party).first break if assign workitem.workitem_assignments.create!(party: party) new_users = party.partable.users.to_a to_notify = new_users - notified_users transition = workitem.transition to_notify.each do |user| # TODO: multiple instance + sub workflow if transition.multiple_instance? && !workitem.forked? next if workitem.children.where(holding_user: user).exists? child = workitem.children.create!( workflow_id: workitem.workflow_id, transition_id: workitem.transition_id, state: :enabled, trigger_time: workitem.trigger_time, forked: true, holding_user: user, case_id: workitem.case_id ) workitem.transition.notification_callback.constantize.new(child, user.id).perform_now else workitem.transition.notification_callback.constantize.new(workitem, user.id).perform_now end end end end end end ================================================ FILE: app/models/wf/case_command/begin_workitem_action.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class BeginWorkitemAction prepend SimpleCommand attr_reader :workitem, :action, :user def initialize(workitem, user, action = :start) @workitem = workitem @action = action @user = user end def call if action == :start raise("Workitem is in state #{workitem.state}, but it must be in state enabled to be started.") unless workitem.enabled? raise("You are not assigned to this workitem.") unless workitem.owned_by?(user) elsif action == :finish || action == :cancel if workitem.started? raise("You are not the user currently working on this workitem.") if workitem.holding_user != user elsif workitem.enabled? raise("You can only cancel a workitem in state started, but this workitem is in state #{workitem.state}.") if action == :cancel raise("You are not assigned to this workitem.") unless workitem.owned_by?(user) workitem.update!(holding_user: user) else raise("Workitem is in state #{workitem.state}, but it must be in state enabled or started to be finished.") end elsif action == :comment # TODO end end end end ================================================ FILE: app/models/wf/case_command/cancel.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class Cancel prepend SimpleCommand attr_reader :wf_case def initialize(wf_case) @wf_case = wf_case end def call raise("Only active or suspended cases can be canceled") unless wf_case.suspended? || wf_case.active? wf_case.canceled! end end end ================================================ FILE: app/models/wf/case_command/cancel_workitem.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class CancelWorkitem prepend SimpleCommand attr_reader :workitem def initialize(workitem) @workitem = workitem end def call raise("The workitem is not in state #{workitem.state}") unless workitem.started? Wf::ApplicationRecord.transaction do workitem.update!(state: :canceled, canceled_at: Time.zone.now) ReleaseToken.call(workitem) SweepAutomaticTransitions.call(workitem.case) end end end end ================================================ FILE: app/models/wf/case_command/clear_manual_assignments.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class ClearManualAssignments prepend SimpleCommand attr_reader :wf_case, :transition def initialize(wf_case, transition) @wf_case = wf_case @transition = transition end def call wf_case.case_assignments.where(transition: transition).find_each(&:destroy) end end end ================================================ FILE: app/models/wf/case_command/clear_workitem_assignments.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class ClearWorkitemAssignments prepend SimpleCommand attr_reader :workitem, :permanent def initialize(workitem, permanent = true) @workitem = workitem @permanent = permanent end def call Wf::ApplicationRecord.transaction do ClearManualAssignments.call(workitem.case, workitem.transition) if permanent workitem.workitem_assignments.delete_all workitem.transition.unassignment_callback.constantize.new(workitem.id).perform end end end end ================================================ FILE: app/models/wf/case_command/consume_token.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class ConsumeToken prepend SimpleCommand attr_reader :wf_case, :place, :locked_item def initialize(wf_case, place, locked_item = nil) @wf_case = wf_case @place = place @locked_item = locked_item end def call Wf::ApplicationRecord.transaction do if locked_item wf_case.tokens.where(place: place, state: :locked, locked_workitem_id: locked_item.id).update(consumed_at: Time.zone.now, state: :consumed) else wf_case.tokens.where(id: wf_case.tokens.where(place: place, state: :free).first&.id).update(consumed_at: Time.zone.now, state: :consumed) end end end end end ================================================ FILE: app/models/wf/case_command/create_entry.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class CreateEntry prepend SimpleCommand attr_reader :form, :workitem, :user, :params def initialize(form, workitem, user, params) @form = form @workitem = workitem @params = params @user = user end def call create_entry rescue StandardError binding.pry puts $ERROR_INFO # TODO: more detail errors.add(:base, :failure) end def create_entry Wf::ApplicationRecord.transaction do entry = form.entries.find_or_create_by!(user: user, workitem: workitem) params.each do |field_id, field_value| if field = entry.field_values.where(form: form, field_id: field_id).first field.update!(value: field_value) else entry.field_values.create!(form: form, field_id: field_id, value: field_value) end end entry.update_payload! entry end end end end ================================================ FILE: app/models/wf/case_command/enable_transitions.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class EnableTransitions prepend SimpleCommand attr_reader :wf_case def initialize(wf_case) @wf_case = wf_case end def call Wf::ApplicationRecord.transaction do wf_case.workitems.enabled.each do |workitem| workitem.update!(state: :overridden, overridden_at: Time.zone.now) unless wf_case.can_fire?(workitem.transition) end wf_case.workflow.transitions.each do |transition| next unless wf_case.can_fire?(transition) && !transition.workitems.where(case: wf_case, state: %i[enabled started]).exists? trigger_time = Time.zone.now + transition.trigger_limit.minutes if transition.trigger_limit && transition.time? workitem = wf_case.workitems.create!( workflow: wf_case.workflow, transition: transition, state: :enabled, trigger_time: trigger_time ) Wf::FireTimedWorkitemJob.set(wait: transition.trigger_limit.minutes).perform_later(workitem.id) if trigger_time SetWorkitemAssignments.call(workitem) workitem.transition.unassignment_callback.constantize.new(workitem.id).perform_now if workitem.workitem_assignments.count == 0 if sub_workflow = transition.sub_workflow sub_case = Wf::CaseCommand::New.call(sub_workflow, nil, workitem).result Wf::CaseCommand::StartCase.call(sub_case) end end end end end end ================================================ FILE: app/models/wf/case_command/end_workitem_action.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class EndWorkitemAction prepend SimpleCommand attr_reader :workitem, :action, :user def initialize(workitem, user, action = :start) @workitem = workitem @action = action @user = user end def call if action == :start StartWorkitem.call(workitem, user) elsif action == :finish FinishWorkitem.call(workitem, user) elsif action == :cancel CancelWorkitem.call(workitem, user) elsif action == :comment raise("Unknown action #{action}") end end end end ================================================ FILE: app/models/wf/case_command/finish_workitem.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class FinishWorkitem prepend SimpleCommand attr_reader :workitem def initialize(workitem) @workitem = workitem end def call Wf::ApplicationRecord.transaction do if workitem.forked? workitem.update!(finished_at: Time.zone.now, state: :finished) Wf::Workitem.increment_counter(:children_finished_count, workitem.parent_id) if parent = workitem.parent if (parent.children_finished_count >= parent.children_count) || workitem.transition.finish_condition.constantize.new.perform(workitem) parent.children.where(state: %i[started enabled]).find_each do |wi| wi.update!(overridden_at: Time.zone.now, state: :overridden) end FireTransitionInternal.call(parent) SweepAutomaticTransitions.call(parent.case) end end else FireTransitionInternal.call(workitem) SweepAutomaticTransitions.call(workitem.case) end end end end end ================================================ FILE: app/models/wf/case_command/finished_p.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class FinishedP prepend SimpleCommand attr_reader :wf_case def initialize(wf_case) @wf_case = wf_case end def call return true if wf_case.finished? end_place = wf_case.workflow.places.end.first end_place_token_num = Wf::ApplicationRecord.uncached { wf_case.tokens.where(place: end_place).count } if end_place_token_num == 0 false else free_and_locked_token_num = wf_case.tokens.where(place: end_place).where(state: %i[free locked]).count raise("The workflow net is misconstructed: Some parallel executions have not finished.") if free_and_locked_token_num > 1 ConsumeToken.call(wf_case, end_place) unless wf_case.finished? wf_case.finished! if started_by_workitem = wf_case.started_by_workitem Wf::CaseCommand::FinishWorkitem.call(started_by_workitem) end end true end end end end ================================================ FILE: app/models/wf/case_command/fire_message_transition.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class FireMessageTransition prepend SimpleCommand attr_reader :workitem def initialize(workitem) @workitem = workitem end def call raise("Transition #{workitem.transition.name} is not message triggered") unless workitem.transition.message? Wf::ApplicationRecord.transaction do FireTransitionInternal.call(workitem) SweepAutomaticTransitions.call(workitem.case) end end end end ================================================ FILE: app/models/wf/case_command/fire_transition_internal.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class FireTransitionInternal prepend SimpleCommand attr_reader :workitem def initialize(workitem) @workitem = workitem end def call if workitem.enabled? locked_item = nil elsif workitem.started? locked_item = workitem else raise("can not fire the transition if it is not in state enabled or started.") end Wf::ApplicationRecord.transaction do workitem.update!(finished_at: Time.zone.now, state: :finished) # TODO: only in? workitem.transition.arcs.each do |arc| ConsumeToken.call(workitem.case, arc.place, locked_item) end # last arc without guard -> pass has_passed = false workitem.transition.arcs.out.order("guards_count DESC").each do |arc| if workitem.transition.explicit_or_split? if workitem.pass_guard?(arc, has_passed) has_passed = true AddToken.call(workitem.case, arc.place) end else AddToken.call(workitem.case, arc.place) end end workitem.transition.fire_callback.constantize.new(workitem.id).perform_now end end end end ================================================ FILE: app/models/wf/case_command/lock_token.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class LockToken prepend SimpleCommand attr_reader :wf_case, :place, :workitem def initialize(wf_case, place, workitem) @wf_case = wf_case @place = place @workitem = workitem end def call wf_case.tokens.free.where(place: place).limit(1).update_all( state: :locked, locked_at: Time.zone.now, locked_workitem_id: workitem.id ) end end end ================================================ FILE: app/models/wf/case_command/new.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class New prepend SimpleCommand attr_reader :workflow, :target, :started_by def initialize(workflow, target = nil, started_by = nil) @workflow = workflow @target = target @started_by = started_by end def call wf_case = workflow.cases.create!(targetable: target, started_by_workitem: started_by, state: :created) wf_case end end end ================================================ FILE: app/models/wf/case_command/release_token.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class ReleaseToken prepend SimpleCommand attr_reader :workitem def initialize(workitem) @workitem = workitem end def call Wf::ApplicationRecord.transaction do Wf::Token.where(locked_workitem_id: workitem.id).locked.each do |token| AddToken.call(token.case, token.place) token.update!(state: :canceled, canceled_at: Time.zone.now) end end end end end ================================================ FILE: app/models/wf/case_command/remove_manual_assignment.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class RemoveManualAssignment prepend SimpleCommand attr_reader :wf_case, :transition, :party def initialize(wf_case, transition, party) @wf_case = wf_case @transition = transition @party = party end def call wf_case.case_assignments.where(transition: transition, party: party).find_each(&:destroy) end end end ================================================ FILE: app/models/wf/case_command/remove_workitem_assignment.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class RemoveWorkitemAssignment prepend SimpleCommand attr_reader :workitem, :party, :permanent def initialize(workitem, party, permanent = true) @workitem = workitem @party = party @permanent = permanent end def call return if party.nil? Wf::ApplicationRecord.transaction do RemoveManualAssignment.call(workitem.case, workitem.transition, party) if permanent workitem.workitem_assignments.where(party: party).first&.destroy workitem.transition.unassignment_callback.constantize.new(workitem.id).perform_now if workitem.workitem_assignments.count == 0 end end end end ================================================ FILE: app/models/wf/case_command/resume.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class Resume prepend SimpleCommand attr_reader :wf_case def initialize(wf_case) @wf_case = wf_case end def call raise("Only suspended or canceled cases can be resumed") unless wf_case.suspended? || wf_case.canceled? wf_case.active! end end end ================================================ FILE: app/models/wf/case_command/set_workitem_assignments.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class SetWorkitemAssignments prepend SimpleCommand attr_reader :workitem def initialize(workitem) @workitem = workitem end def call Wf::ApplicationRecord.transaction do has_case_ass = false workitem.case.case_assignments.where(transition: workitem.transition).find_each do |case_ass| AddWorkitemAssignment.call(workitem, case_ass.party, false) has_case_ass = true end unless has_case_ass callback_parties = workitem.transition.assignment_callback.constantize.new.perform(workitem.id) if callback_parties.present? callback_parties.each do |party| AddWorkitemAssignment.call(workitem, party, false) end else workitem.transition.transition_static_assignments.each do |static_assignment| AddWorkitemAssignment.call(workitem, static_assignment.party, false) end end end end end end end ================================================ FILE: app/models/wf/case_command/start_case.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class StartCase prepend SimpleCommand attr_reader :wf_case def initialize(wf_case) @wf_case = wf_case end def call Wf::ApplicationRecord.transaction do wf_case.active! AddToken.call(wf_case, wf_case.workflow.places.start.first) SweepAutomaticTransitions.call(wf_case) end end end end ================================================ FILE: app/models/wf/case_command/start_workitem.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class StartWorkitem prepend SimpleCommand attr_reader :workitem, :user def initialize(workitem, user) @workitem = workitem @user = user end def call raise("The workitem can not run by user.") unless workitem.real? raise("The workitem is not in state #{workitem.state}") unless workitem.enabled? # TODO: holding timeout Wf::ApplicationRecord.transaction do workitem.update!(state: :started, holding_user: user) workitem.transition.arcs.in.each do |arc| LockToken.call(workitem.case, arc.place, workitem) end end workitem end end end ================================================ FILE: app/models/wf/case_command/suspend.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class Suspend prepend SimpleCommand attr_reader :wf_case def initialize(wf_case) @wf_case = wf_case end def call raise("Only active or suspended cases can be canceled") unless wf_case.active? wf_case.suspended! end end end ================================================ FILE: app/models/wf/case_command/sweep_automatic_transitions.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class SweepAutomaticTransitions prepend SimpleCommand attr_reader :wf_case def initialize(wf_case) @wf_case = wf_case end def call Wf::ApplicationRecord.transaction do EnableTransitions.call(wf_case) done = false until done done = true finished = FinishedP.call(wf_case).result next if finished Wf::ApplicationRecord.uncached do wf_case.workitems.joins(:transition).where(state: :enabled).where(Wf::Transition.table_name => { trigger_type: Wf::Transition.trigger_types[:automatic] }).find_each do |item| FireTransitionInternal.call(item) done = false end end EnableTransitions.call(wf_case) end end end end end ================================================ FILE: app/models/wf/case_command/sweep_timed_transitions.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class SweepTimedTransitions prepend SimpleCommand def call Wf::ApplicationRecord.transaction do Wf::Workitem.enabled.where("trigger_time <= ?", Time.zone.now).find_each do |item| FireTransitionInternal.call(item) SweepAutomaticTransitions.call(item.case) end end end end end ================================================ FILE: app/models/wf/case_command/workitem_action.rb ================================================ # frozen_string_literal: true module Wf::CaseCommand class WorkitemAction prepend SimpleCommand attr_reader :workitem, :action, :user def initialize(workitem, user, action = :start) @workitem = workitem @action = action @user = user end def call Wf::ApplicationRecord.transaction do BeginWorkitemAction.call(workitem, user, action) EndWorkitemAction.call(workitem, user, action) end end end end ================================================ FILE: app/models/wf/comment.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_comments # # id :integer not null, primary key # workitem_id :integer # user_id :string # body :text # created_at :datetime not null # updated_at :datetime not null # # frozen_string_literal: true module Wf class Comment < ApplicationRecord belongs_to :workitem belongs_to :user, class_name: Wf.user_class.to_s end end ================================================ FILE: app/models/wf/demo_target.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_demo_targets # # id :integer not null, primary key # name :string # description :string # created_at :datetime not null # updated_at :datetime not null # module Wf class DemoTarget < ApplicationRecord has_many :cases, as: :targetable end end ================================================ FILE: app/models/wf/entry.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_entries # # id :integer not null, primary key # user_id :string # workitem_id :integer # payload :json default("{}") # created_at :datetime not null # updated_at :datetime not null # form_id :integer # module Wf class Entry < ApplicationRecord belongs_to :form belongs_to :user, class_name: Wf.user_class.to_s belongs_to :workitem has_many :field_values after_initialize do self.payload = {} if payload.blank? end def json field_values.includes(:field).map { |x| [x.field_id.to_i, { field_id: x.id.to_i, field_name: x.field.name, value: x.value_after_cast }] }.to_h end def for_mini_racer field_values.includes(:field).map { |x| [x.field.name, x.value_after_cast] }.to_h end def update_payload! update(payload: json) end end end ================================================ FILE: app/models/wf/field.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_fields # # id :integer not null, primary key # name :string # form_id :integer # position :integer default("0") # field_type :integer default("0") # field_type_name :string # default_value :string # created_at :datetime not null # updated_at :datetime not null # module Wf class Field < ApplicationRecord belongs_to :form, touch: true enum field_type: { string: 0, integer: 1, boolean: 2, date: 3, datetime: 4, decimal: 5, float: 6, json: 7, text: 8, "string[]": 20, "integer[]": 21, "date[]": 23, "datetime[]": 24, "decimal[]": 25, "float[]": 26, "json[]": 27, "text[]": 28 } # TODO: array type def field_type_for_view case field_type when "string" "text_field" when "integer" "number_field" when "date" "date_field" when "datetime" "datetime_field" when "boolean" "check_box" when "text" "text_area" else "text_field" end end def array? field_type.to_s.match(/^(\w+)(\[\])?$/)[2] == "[]" end def type_for_cast type = field_type.to_s.match(/^(\w+)(\[\])?$/)[1] if array? ActiveRecord::Type.lookup(type.to_sym, adapter: :postgresql, array: true) else ActiveRecord::Type.lookup(type.to_sym, adapter: :postgresql) end end delegate :cast, to: :type_for_cast end end ================================================ FILE: app/models/wf/field_value.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_field_values # # id :integer not null, primary key # form_id :integer # field_id :integer # value :text # created_at :datetime not null # updated_at :datetime not null # entry_id :integer # module Wf class FieldValue < ApplicationRecord belongs_to :form belongs_to :field belongs_to :entry def value_after_cast ov = self[:value] if field.array? && !ov.is_a?(Array) v = begin JSON.parse(ov) rescue StandardError [] end field.type_for_cast.cast(v) else field.type_for_cast.cast(ov) end end def value=(v) self[:value] = if field.array? Array(v.as_json) else v end end def value value_after_cast end end end ================================================ FILE: app/models/wf/form.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_forms # # id :integer not null, primary key # name :string # description :text # created_at :datetime not null # updated_at :datetime not null # module Wf class Form < ApplicationRecord has_many :fields, dependent: :destroy has_many :entries end end ================================================ FILE: app/models/wf/group.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_groups # # id :integer not null, primary key # name :string # created_at :datetime not null # updated_at :datetime not null # module Wf class Group < ApplicationRecord has_many :users include Wf::ActsAsParty acts_as_party(user: false, party_name: :name) end end ================================================ FILE: app/models/wf/guard.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_guards # # id :integer not null, primary key # arc_id :integer # workflow_id :integer # fieldable_type :string # fieldable_id :string # op :string # value :string # exp :string # created_at :datetime not null # updated_at :datetime not null # module Wf class Guard < ApplicationRecord belongs_to :workflow belongs_to :arc, touch: true, counter_cache: true belongs_to :fieldable, polymorphic: true, optional: true before_validation do self.workflow = arc.workflow end validate :validate_exp_and_fieldable OP = %w[ = > < >= <= is_empty ].freeze def value_after_cast field = fieldable fieldable&.cast(value) end def pass?(entry, workitem) if exp check_exp(entry, workitem) else check_fieldable(entry) end end def check_exp(_entry, workitem) # 1000ms, 200mb context = MiniRacer::Context.new(timeout: 1000, max_memory: 200_000_000) context.eval("let target = #{target_hash.to_json};") context.eval("let workitem = #{workitem.to_json};") exp_value = context.eval(exp) yes_or_no?(exp_value, value) end def check_fieldable(entry) fv = entry.field_values.where(field_id: fieldable_id).first return unless fv yes_or_no?(fv.value_after_cast, value_after_cast) end def yes_or_no?(input_value, setting_value) if op == "=" input_value == setting_value elsif op == ">" input_value > setting_value elsif op == "<" input_value < setting_value elsif op == ">=" input_value >= setting_value elsif op == "<=" input_value <= setting_value elsif op == "is_empty" input_value.blank? else false end end def inspect if exp %(eval(exp) #{op} #{value}) else %(#{fieldable&.form&.name}.#{fieldable&.name} #{op} #{value}) end end def validate_exp_and_fieldable if fieldable && exp.present? errors.add(:exp, "Exp and Fieldable can not be set at the same time.") return end errors.add(:exp, "Must set one of Exp and Fieldable.") unless fieldable || exp.present? end end end ================================================ FILE: app/models/wf/lola.rb ================================================ # frozen_string_literal: true module Wf class Lola attr_reader :end_p, :start_p, :workflow def initialize(workflow) @workflow = workflow @start_p = workflow.places.start.first @end_p = workflow.places.end.first generate_lola_file! end def to_text places = workflow.places transitions = workflow.transitions places_text = places.map(&:lola_id).join(",") marking_text = start_p.lola_id # TODO: with guard transitions_text = transitions.map do |t| consume = t.arcs.in.map { |arc| "#{arc.place.lola_id}:1" }.join(",") produce = t.arcs.out.map { |arc| "#{arc.place.lola_id}:1" }.join(",") [ "TRANSITION #{t.lola_id}", "CONSUME #{consume};", "PRODUCE #{produce};" ].join("\n") end.join("\n\n") <<~LOLA PLACE #{places_text}; MARKING #{marking_text}; #{transitions_text} LOLA end def json_path(bucket) Rails.root.join("tmp", "#{workflow.id}-#{bucket}.json") end def lola_path Rails.root.join("tmp", "#{workflow.id}-#{workflow.updated_at.to_i}.lola") end def generate_lola_file! File.open(lola_path, "w") { |f| f.write(Wf::Lola.new(workflow).to_text) } unless File.exist?(lola_path) end def soundness? reachability_of_final_marking? && quasiliveness? && !deadlock? end def reachability_of_final_marking? formula = workflow.places.reject { |p| p == end_p }.map { |p| "#{p.lola_id} = 0" }.join(" AND ") formula += " AND #{end_p.lola_id} >= 1" formula = "AGEF(#{formula})" result = run_cmd(formula, "reachability_of_final_marking") result.dig("analysis", "result") end def quasiliveness? workflow.transitions.all? { |t| !dead_transition?(t) } end def deadlock? formula = "EF (DEADLOCK AND (#{end_p.lola_id} = 0))" result = run_cmd(formula, "deadlock") result.dig("analysis", "result") end private def dead_transition?(transition) formula = "AG NOT FIREABLE (#{transition.lola_id})" result = run_cmd(formula, "dead_transition_#{transition.id}") result.dig("analysis", "result") end def run_cmd(formula, bucket) cmd = %(lola #{lola_path} --markinglimit=1000 --timelimit=1 --formula="#{formula}" --json=#{json_path(bucket)}) $stdout.puts cmd system(cmd) JSON.parse(File.read(json_path(bucket))) end end end ================================================ FILE: app/models/wf/multiple_instances/all_finish.rb ================================================ # frozen_string_literal: true module Wf module MultipleInstances class AllFinish def perform(_workitem) false end end end end ================================================ FILE: app/models/wf/party.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_parties # # id :integer not null, primary key # partable_type :string # partable_id :string # party_name :string # created_at :datetime not null # updated_at :datetime not null # module Wf class Party < ApplicationRecord # TODO: use acts_as_partable for sync group or role or user to party belongs_to :partable, polymorphic: true has_many :transition_static_assignments has_many :workitem_assignments end end ================================================ FILE: app/models/wf/place.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_places # # id :integer not null, primary key # workflow_id :integer # name :string # description :text # sort_order :integer default("0") # place_type :integer default("0") # created_at :datetime not null # updated_at :datetime not null # module Wf class Place < ApplicationRecord belongs_to :workflow, touch: true has_many :arcs has_many :tokens enum place_type: { start: 0, normal: 1, end: 2 } def graph_id "#{name}/#{id}" end def lola_id "P#{id}" end end end ================================================ FILE: app/models/wf/token.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_tokens # # id :integer not null, primary key # workflow_id :integer # case_id :integer # targetable_type :string # targetable_id :string # place_id :integer # state :integer default("0") # locked_workitem_id :integer # produced_at :datetime # locked_at :datetime # canceled_at :datetime # consumed_at :datetime # created_at :datetime not null # updated_at :datetime not null # module Wf class Token < ApplicationRecord belongs_to :workflow belongs_to :case belongs_to :place belongs_to :locked_workitem, class_name: "Wf::Workitem", optional: true enum state: { free: 0, locked: 1, canceled: 2, consumed: 3 } end end ================================================ FILE: app/models/wf/transition.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_transitions # # id :integer not null, primary key # name :string # description :text # workflow_id :integer # sort_order :integer default("0") # trigger_limit :integer # trigger_type :integer default("0") # created_at :datetime not null # updated_at :datetime not null # form_id :integer # enable_callback :string default("Wf::Callbacks::EnableDefault") # fire_callback :string default("Wf::Callbacks::FireDefault") # notification_callback :string default("Wf::Callbacks::NotificationDefault") # time_callback :string default("Wf::Callbacks::TimeDefault") # deadline_callback :string default("Wf::Callbacks::DeadlineDefault") # hold_timeout_callback :string default("Wf::Callbacks::HoldTimeoutDefault") # assignment_callback :string default("Wf::Callbacks::AssignmentDefault") # unassignment_callback :string default("Wf::Callbacks::UnassignmentDefault") # form_type :string default("Wf::Form") # sub_workflow_id :integer # multiple_instance :boolean default("false") # finish_condition :string default("Wf::MultipleInstances::AllFinish") # dynamic_assign_by_id :integer # module Wf class Transition < ApplicationRecord belongs_to :workflow, touch: true has_many :arcs has_many :transition_static_assignments has_many :static_parties, through: :transition_static_assignments, source: "party" has_many :workitems belongs_to :form, optional: true, polymorphic: true belongs_to :sub_workflow, optional: true, class_name: "Wf::Workflow" belongs_to :dynamic_assign_by, optional: true, class_name: "Wf::Transition" has_many :dynamic_assignments, class_name: "Wf::Transition", foreign_key: "dynamic_assign_by_id" enum trigger_type: { user: 0, automatic: 1, message: 2, time: 3 } validate :validate_trigger_type_and_sub def is_sub_workflow? !!sub_workflow_id end def explicit_or_split? arcs.out.sum(:guards_count) >= 1 end validates :name, presence: true def validate_trigger_type_and_sub errors.add(:trigger_type, "sub workflow must have trigger type: automatic, message and time.") if user? && is_sub_workflow? end def graph_id "#{name}/#{id}" end def lola_id "T#{id}" end end end ================================================ FILE: app/models/wf/transition_static_assignment.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_transition_static_assignments # # id :integer not null, primary key # party_id :integer # transition_id :integer # workflow_id :integer # created_at :datetime not null # updated_at :datetime not null # module Wf class TransitionStaticAssignment < ApplicationRecord belongs_to :workflow belongs_to :transition belongs_to :party validates :party_id, uniqueness: { scope: %i[workflow_id transition_id] } before_validation do self.workflow_id = transition&.workflow_id end end end ================================================ FILE: app/models/wf/user.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_users # # id :integer not null, primary key # name :string # created_at :datetime not null # updated_at :datetime not null # group_id :integer # module Wf class User < ApplicationRecord belongs_to :group, optional: true include Wf::ActsAsParty acts_as_party(user: true, party_name: :name) end end ================================================ FILE: app/models/wf/workflow.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_workflows # # id :integer not null, primary key # name :string # description :text # is_valid :boolean default("false") # creator_id :string # error_msg :text # created_at :datetime not null # updated_at :datetime not null # module Wf class Workflow < ApplicationRecord has_many :places, dependent: :destroy has_many :transitions, dependent: :destroy has_many :arcs, dependent: :destroy has_many :transition_static_assignments has_many :cases has_many :workitems has_many :tokens validates :name, presence: true scope :valid, -> { where(is_valid: true) } after_save do do_validate! end after_touch do do_validate! end # TODO: can from start place to end place # todo: remove color hex to const. def do_validate! msgs = [] start_place = places.start.first end_place = places.end.first msgs << "must have start place" if start_place.blank? msgs << "must have only one start place" if places.start.count > 1 msgs << "must have end place" if end_place.blank? msgs << "must have only one end place" if places.end.count > 1 msgs << "must not have discrete transition" if transitions.any? { |t| !t.arcs.in.exists? } if start_place && end_place rgl = to_rgl places.each do |p| msgs << "start place can not reach #{p.name}" unless rgl.path?(start_place.to_gid.to_s, p.to_gid.to_s) msgs << "#{p.name} can not reach end_place" unless rgl.path?(p.to_gid.to_s, end_place.to_gid.to_s) end transitions.each do |t| msgs << "start place can not reach #{t.name}" unless rgl.path?(start_place.to_gid.to_s, t.to_gid.to_s) msgs << "#{t.name} can not reach end_place" unless rgl.path?(t.to_gid.to_s, end_place.to_gid.to_s) end end if Wf.use_lola msgs << "has deadlock" if to_lola.deadlock? msgs << "has dead transition" unless to_lola.quasiliveness? msgs << "can not reach to the end place" unless to_lola.reachability_of_final_marking? end if msgs.present? update_columns(is_valid: false, error_msg: msgs.join("\n")) else update_columns(is_valid: true, error_msg: "") end end def to_graph(wf_case = nil, base = nil) fontfamily = "system, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Noto Color Emoji, Segoe UI Symbol" fontfamily_monospace = "SFMono-Regular, Consolas, Liberation Mono, Menlo, Courier, monospace" graph = base || GraphViz.new(name, type: :digraph, rankdir: "LR", splines: true, ratio: :auto) free_token_places = if wf_case wf_case.tokens.free.map(&:place_id) else [] end pg_mapping = {} places.order("place_type ASC").each do |p| if p.start? fillcolor = "#ffe7ba" textcolor = "#fa8c16" shape = :doublecircle elsif p.end? fillcolor = "#dff2ef" textcolor = "#29c0b1" shape = :doublecircle else fillcolor = "#fbdbe1" textcolor = "#ff3366" shape = :circle end token_count = free_token_places.count(p.id) if token_count >= 1 label = "•" xlabel = nil fontsize = 25 else label = p.name xlabel = nil end pg = graph.add_nodes(p.graph_id, label: label, xlabel: xlabel, shape: shape, fixedsize: true, style: :filled, fontname: fontfamily, fontcolor: textcolor, fontsize: fontsize, color: fillcolor, fillcolor: fillcolor, href: Wf::Engine.routes.url_helpers.edit_workflow_place_path(self, p)) pg_mapping[p] = pg end tg_mapping = {} transitions.each do |t| peripheries = if t.multiple_instance? 3 else 1 end tg = graph.add_nodes(t.graph_id, label: t.name, shape: :box, style: :filled, fillcolor: "#d6ddfa", color: "#d6ddfa", fontcolor: "#2c50ed", fontname: fontfamily, peripheries: peripheries, href: Wf::Engine.routes.url_helpers.edit_workflow_transition_path(self, t)) tg_mapping[t] = tg # NOTICE: if sub_workflow is transition's workflow, then graph will loop infinite, this is valid for workflow definition. next unless t.is_sub_workflow? && t.sub_workflow != t.workflow sub_graph = graph.add_graph("cluster#{t.sub_workflow_id}", rankdir: "LR", splines: true, ratio: :auto) sub_graph[:label] = t.sub_workflow.name sub_graph[:style] = :dashed sub_graph[:color] = :lightgrey # TODO: detect related case for sub workflow. t.sub_workflow.to_graph(nil, sub_graph) graph.add_edges(tg, t.sub_workflow.places.start.first.graph_id, style: :dashed, dir: :both) end arcs.order("direction desc").each do |arc| label = if arc.guards_count > 0 arc.guards.map(&:inspect).join(" & ") else "" end if arc.in? graph.add_edges( pg_mapping[arc.place], tg_mapping[arc.transition], label: label, weight: 1, labelfloat: false, labelfontcolor: :red, arrowhead: :vee, fontsize: 10, color: "#53585c", fontcolor: "#53585c", fontname: fontfamily_monospace, href: Wf::Engine.routes.url_helpers.edit_workflow_arc_path(self, arc) ) else graph.add_edges( tg_mapping[arc.transition], pg_mapping[arc.place], label: label, weight: 1, labelfloat: false, labelfontcolor: :red, arrowhead: :vee, fontsize: 10, color: "#53585c", fontcolor: "#53585c", fontname: fontfamily_monospace, href: Wf::Engine.routes.url_helpers.edit_workflow_arc_path(self, arc) ) end end graph end def render_graph(wf_case = nil) graph = to_graph(wf_case) path = Rails.root.join("tmp", "#{id}.svg") graph.output(svg: path) File.read(path) end def to_lola @lola ||= Wf::Lola.new(self) end def to_rgl graph = RGL::DirectedAdjacencyGraph.new places.order("place_type ASC").each do |p| graph.add_vertex(p.to_gid.to_s) end transitions.each do |t| graph.add_vertex(t.to_gid.to_s) end arcs.order("direction desc").each do |arc| if arc.in? graph.add_edge(arc.place.to_gid.to_s, arc.transition.to_gid.to_s) else graph.add_edge(arc.transition.to_gid.to_s, arc.place.to_gid.to_s) end end graph end end end ================================================ FILE: app/models/wf/workitem.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_workitems # # id :integer not null, primary key # case_id :integer # workflow_id :integer # transition_id :integer # state :integer default("0") # enabled_at :datetime # started_at :datetime # canceled_at :datetime # finished_at :datetime # overridden_at :datetime # deadline :datetime # created_at :datetime not null # updated_at :datetime not null # trigger_time :datetime # holding_user_id :string # children_count :integer default("0") # children_finished_count :integer default("0") # forked :boolean default("false") # parent_id :integer # module Wf class Workitem < ApplicationRecord belongs_to :workflow belongs_to :transition belongs_to :case belongs_to :parent, class_name: "Wf::Workitem", optional: true, counter_cache: :children_count belongs_to :holding_user, foreign_key: :holding_user_id, class_name: Wf.user_class.to_s, optional: true has_many :workitem_assignments has_many :parties, through: :workitem_assignments, source: "party" has_many :comments has_many :entries, class_name: Wf.entry_class.to_s has_one :started_case, foreign_key: :started_by_workitem_id, class_name: "Wf::Case" has_many :children, foreign_key: :parent_id, class_name: "Wf::Workitem" enum state: { enabled: 0, started: 1, canceled: 2, finished: 3, overridden: 4 } def self.todo(wf_current_user) current_party_ids = [ wf_current_user, Wf.org_classes.map { |org, _org_class| wf_current_user&.public_send(org) } ].flatten.map { |x| x&.party&.id }.compact Wf::Workitem.where(forked: false).joins(:workitem_assignments).where(Wf::WorkitemAssignment.table_name => { party_id: current_party_ids }) end def self.doing(wf_current_user) where(holding_user: wf_current_user).where(state: %i[started enabled]) end def self.done(wf_current_user) where(holding_user: wf_current_user).where(state: [:finished]) end def for_mini_racer attr = attributes attr = attr.merge(holding_user: holding_user&.attributes || {}) attr = attr.merge(form: entries.to_a.first&.for_mini_racer || {}) children_attrs = if forked? [] else children.includes(:holding_user, entries: :field_values).map(&:for_mini_racer) end attr = attr.merge(children: children_attrs) attr end def parent? !forked end def name "Workitem -> #{id}" end def pass_guard?(arc, has_passed = false) if arc.guards_count == 0 !has_passed else entry = entries.where(user: holding_user).first arc.guards.all? { |guard| guard.pass?(entry, self) } end end def real? return false if transition.multiple_instance? && parent_id.nil? return false if transition.sub_workflow_id.present? true end def started_by?(user) real? && enabled? && owned_by?(user) end def finished_by?(user) real? && started? && owned_by?(user) && holding_user == user end def owned_by?(user) Wf::Party.joins(workitem_assignments: { workitem: %i[transition case] }) .where(Wf::Transition.table_name => { trigger_type: Wf::Transition.trigger_types[:user] }) .where(Wf::Case.table_name => { state: Wf::Case.states[:active] }) .where(Wf::Workitem.table_name => { state: Wf::Workitem.states.values_at(:started, :enabled) }) .where(Wf::Workitem.table_name => { id: id }).map do |party| party.partable.users.to_a end.flatten.include?(user) end end end ================================================ FILE: app/models/wf/workitem_assignment.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_workitem_assignments # # id :integer not null, primary key # party_id :integer # workitem_id :integer # created_at :datetime not null # updated_at :datetime not null # module Wf class WorkitemAssignment < ApplicationRecord belongs_to :party belongs_to :workitem end end ================================================ FILE: app/views/layouts/wf/_alert.html.erb ================================================ <% return if alert.blank? %>

<%= alert %>

================================================ FILE: app/views/layouts/wf/_footer.html.erb ================================================ ================================================ FILE: app/views/layouts/wf/_nav.html.erb ================================================ ================================================ FILE: app/views/layouts/wf/_notice.html.erb ================================================ <% return if notice.blank? %>

<%= notice %>

================================================ FILE: app/views/layouts/wf/application.html.erb ================================================ Petri Flow <%= csrf_meta_tags %> <%= stylesheet_link_tag "wf/application", media: "all", "data-turbolinks-track": "reload" %> <%= javascript_include_tag "wf/application", "data-turbolinks-track": "reload" %> <%= render "layouts/wf/nav" %>
<%= render "layouts/wf/notice" %> <%= render "layouts/wf/alert" %> <%= content_for?(:content) ? yield(:content) : yield %>
<%= render "layouts/wf/footer" %> ================================================ FILE: app/views/wf/arcs/_form.html.erb ================================================ <%= form_with(model: arc, url: [@workflow, @arc], local: true) do |f| %> <% if arc.errors.any? %>

<%= pluralize(arc.errors.count, "error") %> prohibited this arc from being saved:

<% end %>
<%= f.label :place, class: "label" %> <%= f.select :place_id, @workflow.places.map{|x| [x.name, x.id]}, {}, class: "form-control custom-select", placeholder: "Place" %>
<%= f.label :transition, class: "label" %> <%= f.select :transition_id, @workflow.transitions.map{|x| [x.name, x.id]}, {}, class: "custom-select", placeholder: "Transition" %>
Direction is base on transition, in: P->T, out: T->P.
<%= f.label :direction, class: "label" %> <%= f.select :direction, Wf::Arc.directions.keys, {}, class: "form-control custom-select", placeholder: "Direction" %>
<%= f.submit class: "btn btn-primary", data: {disable_with: 'Waiting...'} %>
<% end %> ================================================ FILE: app/views/wf/arcs/edit.html.erb ================================================
<%= render "form", workflow: @workflow, arc: @arc %>
================================================ FILE: app/views/wf/arcs/new.html.erb ================================================
<%= render "form", workflow: @workflow, arc: @arc %>
================================================ FILE: app/views/wf/arcs/show.html.erb ================================================
<%= link_to 'Delete Arc', workflow_path(@workflow, @arc), data: {confirm: 'confirm?'}, method: :delete, class: 'btn btn-danger' %> <%= link_to 'Edit Arc', edit_workflow_arc_path(@workflow, @arc), class: 'btn btn-primary' %> <% if @arc.out? %> <%= link_to 'Create Guards', new_arc_guard_path(@arc), class: 'btn btn-primary' %> <% end %>

Arc Detail

ID <%= @arc.id %>
Name <%= @arc.name %>
Transition <%= @arc.transition.name %>
Place <%= @arc.place.name %>
Guards <%= @arc.guards.map {|x| x.inspect }.join(" & ") %>
Direction <%= @arc.direction %>

Guards

<% @arc.guards.each do |guard| %> <% end %>
ID Field Op Value Exp Actions
<%= guard.id %> <%= guard.fieldable&.form&.name %>/<%= guard.fieldable&.name %> <%= guard.op %> <%= guard.value %>
              <%= guard.exp %>
            
<%= link_to 'Edit Guard', edit_arc_guard_path(@arc, guard), class: 'btn btn-sm btn-info' %> <%= link_to 'Delete Guard', [@arc, guard], remote: true, method: :delete, data: {confirm: 'confirm?'}, class: 'btn btn-sm btn-info' %>
================================================ FILE: app/views/wf/cases/_form.html.erb ================================================ <%= form_with(model: wf_case, url: [@workflow, @wf_case], local: true) do |f| %> <% if wf_case.errors.any? %>

<%= pluralize(wf_case.errors.count, "error") %> prohibited this wf_case from being saved:

<% end %>
<%= f.label :targetable, class: "label" %> <%= f.select :targetable, options_for_select(Wf::DemoTarget.all.map {|x| [x.name, x.to_global_id]} || []), {include_blank: true}, class: "form-control custom-select", placeholder: "targetable" %>
<%= f.submit class: "btn btn-primary", data: {disable_with: 'Waiting...'} %>
<% end %> ================================================ FILE: app/views/wf/cases/index.html.erb ================================================

Cases

<%= link_to 'New Case', new_workflow_case_path(@workflow), class: 'btn btn-primary' %>
<% @cases.each do |wf_case| %> <% end %>
ID State Created At Targetable Type Targetable ID Action
<%= wf_case.id %> <%= wf_case.state %> <%= wf_case.created_at %> <%= wf_case.targetable_type %> <%= wf_case.targetable_id %> <%= link_to "Run", workflow_case_path(@workflow, wf_case), class: 'btn btn-success btn-sm' %>
<%= paginate @cases, theme: 'twitter-bootstrap-4' %>
================================================ FILE: app/views/wf/cases/new.html.erb ================================================
<%= render "form", workflow: @workflow, wf_case: @wf_case %>
================================================ FILE: app/views/wf/cases/show.html.erb ================================================

Case Detail

ID <%= @wf_case.id %>
Workflow <%= link_to @wf_case.workflow.name, workflow_path(@wf_case.workflow) %>
State <%= @wf_case.state %>
Targetable Type <%= @wf_case.targetable_type %>
Targetable ID <%= @wf_case.targetable_id %>
Created At <%= @wf_case.created_at %>

Case Graph

<%=raw @wf_case.workflow.render_graph(@wf_case) %>

Tokens

<% @wf_case.tokens.each do |token| %> <% end %>
ID Place State Locked Workitem Produced At Locked At Canceled At Consumed At Created At
<%= token.id %> <%= link_to token.place.name, workflow_place_path(token.workflow, token.place) %> <%= token.state %> <%= token.locked_workitem_id %> <%= token.produced_at %> <%= token.locked_at %> <%= token.canceled_at %> <%= token.consumed_at %> <%= token.created_at %>

Workitems

<% @wf_case.workitems.each do |workitem| %> <% end %>
ID Transition State Holding User Started At Enabled At Canceled At Finished At Overridden At Deadline Detail
<%= link_to workitem.id, workitem_path(workitem) %> <%= link_to workitem.transition.name, workflow_transition_path(workitem.workflow, workitem.transition) %> <%= workitem.state %> <%= workitem.holding_user_id %> <%= workitem.started_at %> <%= workitem.enabled_at %> <%= workitem.canceled_at %> <%= workitem.finished_at %> <%= workitem.overridden_at %> <%= workitem.deadline %> <%= link_to "Run", workitem_path(workitem), class: 'btn btn-sm btn-success' %>
================================================ FILE: app/views/wf/comments/new.html.erb ================================================
<%= form_with(model: @comment, url: workitem_comments_path(@workitem), local: true) do |f| %> <% if @comment.errors.any? %>

<%= pluralize(@comment.errors.count, "error") %> prohibited this comment from being saved:

    <% @comment.errors.full_messages.each do |message| %>
  • <%= message %>
  • <% end %>
<% end %>
<%= f.label :body, class: "label" %> <%= f.text_area :body, class: "form-control", rows: 3, placeholder: "comment" %>
<%= f.submit class: "btn btn-primary", value: "Comment", data: {disable_with: 'Waiting...'} %>
<% end %>
================================================ FILE: app/views/wf/fields/_form.html.erb ================================================ <%= form_with(model: field, url: [@form, field], local: true) do |f| %> <% if field.errors.any? %>

<%= pluralize(field.errors.count, "error") %> prohibited this field from being saved:

<% end %>
<%= f.label :name, class: "label" %> <%= f.text_field :name, class: "form-control", fieldholder: "Name" %>
<%= f.label :position, class: "label" %> <%= f.text_field :position, class: "form-control", fieldholder: "Position" %>
<%= f.label :default_value, class: "label" %> <%= f.text_field :default_value, class: "form-control", fieldholder: "Default Value" %>
<%= f.label :field_type, class: "label" %> <%= f.select :field_type, Wf::Field.field_types.keys, {}, class: "form-control custom-select", fieldholder: "Field Type" %>
<%= f.submit class: "btn btn-primary", data: {disable_with: 'Waiting...'} %>
<% end %> ================================================ FILE: app/views/wf/fields/edit.html.erb ================================================
<%= render "form", form: @form, field: @field %>
================================================ FILE: app/views/wf/fields/new.html.erb ================================================
<%= render "form", form: @form, field: @field %>
================================================ FILE: app/views/wf/forms/_form.html.erb ================================================ <%= form_with(model: form, local: true) do |f| %> <% if form.errors.any? %>

<%= pluralize(form.errors.count, "error") %> prohibited this form from being saved:

<% end %>
<%= f.label :name, class: "label" %> <%= f.text_field :name, class: "form-control", placeholder: "Name" %>
<%= f.label :description, class: "label" %> <%= f.text_area :description, class: "form-control", placeholder: "Description" %>
<%= f.submit class: "btn btn-primary", data: {disable_with: 'Waiting...'} %>
<% end %> ================================================ FILE: app/views/wf/forms/edit.html.erb ================================================
<%= render "form", form: @form %>
================================================ FILE: app/views/wf/forms/index.html.erb ================================================

Forms

<%= link_to '+ New Form', new_form_path, class: 'btn btn-primary' %>
<% @forms.each do |form| %> <% end %>
ID Name Description
<%= form.id %> <%= link_to form.name, form_path(form) %> <%= form.description %> <%= link_to 'Delete Form', form_path(form), method: :delete, data: {confirm: 'confirm?'}, class: 'btn-link btn btn-sm text-danger' %> <%= link_to 'Create Field', new_form_field_path(form), class: 'btn btn-link btn-sm' %>
<%= paginate @forms, theme: 'twitter-bootstrap-4' %>
================================================ FILE: app/views/wf/forms/new.html.erb ================================================
<%= render "form", form: @form %>
================================================ FILE: app/views/wf/forms/show.html.erb ================================================
<%= link_to 'Delete Form', form_path(@form), data: {confirm: 'confirm?'}, method: :delete, class: 'btn btn-danger mr-2' %> <%= link_to 'Edit Form', edit_form_path(@form), class: 'btn btn-light mr-2' %> <%= link_to 'Create Fields', new_form_field_path(@form), class: 'btn btn-light' %>

Form Detail

ID <%= @form.id %>
Name <%= @form.name %>
Description <%= @form.description %>

Fields

<% @form.fields.each do |field| %> <% end %>
ID Name Position Field Type Default Value
<%= field.id %> <%= field.name %> <%= field.position %> <%= field.field_type %> <%= field.default_value %> <%= link_to 'Edit Field', edit_form_field_path(@form, field), class: 'btn btn-sm btn-link mr-2' %> <%= link_to 'Delete Field', [@form, field], remote: true, method: :delete, data: {confirm: 'confirm?'}, class: 'btn btn-sm btn-link text-danger' %>
================================================ FILE: app/views/wf/guards/_form.html.erb ================================================ <%= form_with(model: guard, url: [@arc, @guard], local: true) do |f| %> <% if guard.errors.any? %>

<%= pluralize(guard.errors.count, "error") %> prohibited this guard from being saved:

<% end %>
<%= f.label :fieldable, class: "label" %> <%= f.select :fieldable, options_for_select(@arc.transition.form&.fields&.map {|x| [x.name, x.to_global_id]} || [], selected: f.object&.fieldable&.to_global_id), {include_blank: "select a field"}, class: "form-control custom-select", placeholder: "fieldable" %>
<%= f.label :exp, class: "label" %> JavaScript with build-in variable workitem, target. <%= f.text_area :exp, class: "form-control", placeholder: "Exp", rows: 8 %>
<%= f.label :op, class: "label" %> <%= f.select :op, Wf::Guard::OP, {}, class: "form-control custom-select", placeholder: "Op" %>
<%= f.label :value, class: "label" %> <%= f.text_field :value, class: "form-control", placeholder: "Value" %>
<%= f.submit class: "btn btn-primary", data: {disable_with: 'Waiting...'} %>
<% end %> ================================================ FILE: app/views/wf/guards/edit.html.erb ================================================
<%= render "form", arc: @arc, guard: @guard %>
================================================ FILE: app/views/wf/guards/new.html.erb ================================================
<%= render "form", arc: @arc, guard: @guard %>
================================================ FILE: app/views/wf/places/_form.html.erb ================================================ <%= form_with(model: place, url: [@workflow, @place], local: true) do |f| %> <% if place.errors.any? %>

<%= pluralize(place.errors.count, "error") %> prohibited this place from being saved:

<% end %>
<%= f.label :name, class: "label" %> <%= f.text_field :name, class: "form-control", placeholder: "Name" %>
<%= f.label :description, class: "label" %> <%= f.text_area :description, class: "form-control", placeholder: "Description" %>
<%= f.label :sort_order, class: "label" %> <%= f.text_field :sort_order, class: "form-control", placeholder: "Sort Order" %>
<%= f.label :place_type, class: "label" %> <%= f.select :place_type, Wf::Place.place_types.keys, {}, class: "form-control custom-select", placeholder: "Place Type" %>
<%= f.submit class: "btn btn-primary", data: {disable_with: 'Waiting...'} %>
<% end %> ================================================ FILE: app/views/wf/places/edit.html.erb ================================================
<%= render "form", workflow: @workflow, place: @place %>
================================================ FILE: app/views/wf/places/new.html.erb ================================================
<%= render "form", workflow: @workflow, place: @place %>
================================================ FILE: app/views/wf/static_assignments/_form.html.erb ================================================ <%= form_with(model: static_assignment, url: transition_static_assignments_path(static_assignment.transition), local: true) do |f| %> <% if static_assignment.errors.any? %>

<%= pluralize(static_assignment.errors.count, "error") %> prohibited this static_assignment from being saved:

<% end %>
<%= f.label :party, class: "label" %> <%= f.select :party_id, Wf::Party.all.map{|x| [x.party_name, x.id]}, {}, class: "form-control custom-select", placeholder: "fieldable" %>
<%= f.submit class: "btn btn-primary", value: 'Create Static Assignment', data: {disable_with: 'Waiting...'} %>
<% end %> ================================================ FILE: app/views/wf/static_assignments/new.html.erb ================================================
<%= render "form", static_assignment: @static_assignment %>
================================================ FILE: app/views/wf/transitions/_form.html.erb ================================================ <%= form_with(model: transition, url: [@workflow, @transition], local: true) do |f| %> <% if transition.errors.any? %>

<%= pluralize(transition.errors.count, "error") %> prohibited this transition from being saved:

<% end %>
Basic Information
<%= f.label :name, class: "label" %> <%= f.text_field :name, class: "form-control", placeholder: "Name" %>
<%= f.label :description, class: "label" %> <%= f.text_area :description, class: "form-control", placeholder: "Description" %>
<%= f.label :trigger_limit, class: "label" %> <%= f.text_field :trigger_limit, class: "form-control", placeholder: "Trigger Limit" %>
<%= f.label :trigger_type, class: "label" %> <%= f.select :trigger_type, Wf::Transition.trigger_types.keys, {}, class: "form-control custom-select", placeholder: "Trigger Type" %>
Sub Workflow
<%= f.label :sub_workflow, class: "label" %> <%= f.select :sub_workflow_id, options_for_select(Wf::Workflow.valid.all.map{|x| [x.name, x.id]} || []), {include_blank: 'Start a sub workflow?'}, class: "form-control custom-select", placeholder: "Sub Workflow" %>
Dynimac Form
<%= f.label :form, class: "label" %> <%= f.select :form, options_for_select(Wf.form_class.constantize.all.map{|x| [x.name, x.to_global_id]} || [], selected: f.object&.form&.to_global_id), {include_blank: 'user can input from custom form?'}, class: "form-control custom-select", placeholder: "Form" %>
Dynamic Assign By
<%= f.label :dynamic_assign_by, class: "label" %> <%= f.select :dynamic_assign_by_id, options_for_select(@workflow.transitions.where("id != ?", @transition.id).all.map{|x| [x.name, x.id]} || []), {include_blank: 'What other transition you want to do the assignment for this transition?'}, class: "form-control custom-select" %>
Callbacks
<%= f.label :enable_callback, class: "label" %> <%= f.select :enable_callback, Wf.enable_callbacks, {}, class: "form-control custom-select", placeholder: "Callback" %>
<%= f.label :fire_callback, class: "label" %> <%= f.select :fire_callback, Wf.fire_callbacks, {}, class: "form-control custom-select", placeholder: "Callback" %>
<%= f.label :deadline_callback, class: "label" %> <%= f.select :deadline_callback, Wf.deadline_callbacks, {}, class: "form-control custom-select", placeholder: "Callback" %>
<%= f.label :time_callback, class: "label" %> <%= f.select :time_callback, Wf.time_callbacks, {}, class: "form-control custom-select", placeholder: "Callback" %>
<%= f.label :hold_timeout_callback, class: "label" %> <%= f.select :hold_timeout_callback, Wf.hold_timeout_callbacks, {}, class: "form-control custom-select", placeholder: "Callback" %>
<%= f.label :assignment_callback, class: "label" %> <%= f.select :assignment_callback, Wf.assignment_callbacks, {}, class: "form-control custom-select", placeholder: "Callback" %>
<%= f.label :unassignment_callback, class: "label" %> <%= f.select :unassignment_callback, Wf.unassignment_callbacks, {}, class: "form-control custom-select", placeholder: "Callback" %>
<%= f.label :notification_callback, class: "label" %> <%= f.select :notification_callback, Wf.notification_callbacks, {}, class: "form-control custom-select", placeholder: "Callback" %>
Multiple Instances
<%= f.label :multiple_instance, class: "label" %> <%= f.check_box :multiple_instance %>
<%= f.label :finish_condition, class: "label" %> <%= f.select :finish_condition, Wf.finish_conditions, {}, class: "form-control custom-select", placeholder: "Finish_condition" %>
<%= f.submit class: "btn btn-primary", data: {disable_with: 'Waiting...'} %>
<% end %> ================================================ FILE: app/views/wf/transitions/edit.html.erb ================================================
<%= render "form", workflow: @workflow, transition: @transition %>
================================================ FILE: app/views/wf/transitions/new.html.erb ================================================
<%= render "form", workflow: @workflow, transition: @transition %>
================================================ FILE: app/views/wf/transitions/show.html.erb ================================================
<%= link_to 'Edit Transition', edit_workflow_transition_path(@workflow, @transition), class: 'btn btn-primary' %> <%= link_to 'Create Static Assignments', new_transition_static_assignment_path(@transition), class: 'btn btn-primary' %>

Transition Detail

ID <%= @transition.id %>
Name <%= @transition.name %>
Trigger Limit <%= @transition.trigger_limit %>
Trigger Type <%= @transition.trigger_type %>
Form <% if @transition.form %> <%= link_to @transition.form.name, form_path(@transition.form) %> <% else %> No Form <% end %>
Sub Workflow <% if @transition.sub_workflow %> <%= link_to @transition.sub_workflow.name, workflow_path(@transition.sub_workflow_id) %> <% else %> No <% end %>
Dynamic Assign By <% if @transition.dynamic_assign_by %> <%= link_to @transition.dynamic_assign_by.name, workflow_transition_path(@workflow, @transition.dynamic_assign_by_id) %> <% else %> No <% end %>
Multiple Instance? <%= @transition.multiple_instance %>
Finish Condition <%= @transition.finish_condition %>

Static Assignments

<% @transition.transition_static_assignments.each do |assign| %> <% end %>
ID Party Name Party Type Actions
<%= assign.id %> <%= assign.party.party_name %> <%= assign.party.partable_type %> <%= link_to 'Delete', transition_static_assignment_path(@transition, assign), remote: true, method: :delete, data: {confirm: 'confirm?'}, class: 'btn btn-sm btn-info' %>
================================================ FILE: app/views/wf/workflows/_form.html.erb ================================================ <%= form_with(model: workflow, local: true) do |f| %> <% if workflow.errors.any? %>

<%= pluralize(workflow.errors.count, "error") %> prohibited this workflow from being saved:

<% end %>
<%= f.label :name, class: "label" %> <%= f.text_field :name, class: "form-control", placeholder: "Name" %>
<%= f.label :description, class: "label" %> <%= f.text_area :description, class: "form-control", placeholder: "Description" %>
<%= f.submit class: "btn btn-primary", data: {disable_with: 'Waiting...'} %>
<% end %> ================================================ FILE: app/views/wf/workflows/edit.html.erb ================================================
<%= render "form", workflow: @workflow %>
================================================ FILE: app/views/wf/workflows/index.html.erb ================================================

Workflows

<%= link_to 'New Workflow', new_workflow_path, class: 'btn btn-primary' %>
<% @workflows.each do |workflow| %> <% end %>
ID Name Description Is Valid? Error Msg
<%= workflow.id %> <%= link_to workflow.name, workflow_path(workflow) %> <%= workflow.description %> <%= workflow.is_valid? %>
              <%= workflow.error_msg %>
            
<%= link_to 'Delete Workflow', workflow_path(workflow), remote: true, method: :delete, data: {confirm: 'confirm?'}, class: 'btn btn-link btn-sm text-danger' %> <%= link_to 'Create Transition', new_workflow_transition_path(workflow), class: 'btn btn-link btn-sm' %> <%= link_to 'Create Place', new_workflow_place_path(workflow), class: 'btn btn-link btn-sm' %> <%= link_to 'Create Arc', new_workflow_arc_path(workflow), class: 'btn btn-link btn-sm' %>
<%= paginate @workflows, theme: 'twitter-bootstrap-4' %>
================================================ FILE: app/views/wf/workflows/new.html.erb ================================================
<%= render "form", workflow: @workflow %>
================================================ FILE: app/views/wf/workflows/show.html.erb ================================================
<% if @workflow.is_valid? %> <%= link_to 'New Case', new_workflow_case_path(@workflow), class: 'btn btn-primary mr-2' %> <% end %> <%= link_to 'Delete Workflow', workflow_path(@workflow), data: {confirm: 'confirm?'}, method: :delete, class: 'btn btn-danger mr-2' %> <%= link_to 'Edit Workflow', edit_workflow_path(@workflow), class: 'btn btn-light mr-2' %> <%= link_to 'Create Transitions', new_workflow_transition_path(@workflow), class: 'btn btn-light mr-2' %> <%= link_to 'Create Places', new_workflow_place_path(@workflow), class: 'btn btn-light mr-2' %> <%= link_to 'Create Arcs', new_workflow_arc_path(@workflow), class: 'btn btn-light ' %>

Workflow Detail

ID <%= @workflow.id %>
Name <%= @workflow.name %>
Description <%= @workflow.description %>
Is Valid? <%= @workflow.is_valid? %>
Error Msg
            <%= @workflow.error_msg %>
          

Graph Editor

<%=raw @workflow.render_graph %>

Cases

All Created Active Suspended Canceled Finished
<%= link_to @workflow.cases.count, workflow_cases_path(@workflow) %> <%= link_to @workflow.cases.created.count, workflow_cases_path(@workflow, state: :created) %> <%= link_to @workflow.cases.active.count, workflow_cases_path(@workflow, state: :active) %> <%= link_to @workflow.cases.suspended.count, workflow_cases_path(@workflow, state: :suspended) %> <%= link_to @workflow.cases.canceled.count, workflow_cases_path(@workflow, state: :canceled) %> <%= link_to @workflow.cases.finished.count, workflow_cases_path(@workflow, state: :finished) %>

Places

<% @workflow.places.each do |place| %> <% end %>
ID Name Description Place Type Sort Order
<%= place.id %> <%= place.name %> <%= place.description %> <%= place.place_type %> <%= place.sort_order %> <%= link_to 'Edit Place', edit_workflow_place_path(@workflow, place), class: 'btn btn-sm btn-link mr-2' %> <%= link_to 'Delete Place', [@workflow, place], remote: true, method: :delete, data: {confirm: 'confirm?'}, class: 'btn btn-sm btn-link text-danger' %>

Transitions

<% @workflow.transitions.each do |transition| %> <% end %>
ID Name Description Trigger Limit Trigger Type Sort Order Custom Form Sub Workflow
<%= transition.id %> <%= link_to transition.name, workflow_transition_path(@workflow, transition) %> <%= transition.description %> <%= transition.trigger_limit %> <%= transition.trigger_type %> <%= transition.sort_order %> <% if transition.form %> <%= link_to transition.form.name, form_path(transition.form) %> <% else %> No Form <% end %> <% if transition.sub_workflow %> <%= link_to transition.sub_workflow.name, workflow_path(transition.sub_workflow_id) %> <% else %> No <% end %> <%= link_to 'Edit Transition', edit_workflow_transition_path(@workflow, transition), class: 'btn btn-sm btn-link mr-2' %> <%= link_to 'Delete Transition', [@workflow, transition], remote: true, method: :delete, data: {confirm: 'confirm?'}, class: 'btn btn-sm btn-link text-danger' %>

Arcs

<% @workflow.arcs.includes(:transition, :place).each do |arc| %> <% end %>
ID Name Direction Place Transition Arc Type Guards
<%= link_to arc.id, workflow_arc_path(@workflow, arc) %> <%= link_to arc.name, workflow_arc_path(@workflow, arc) %> <%= arc.direction %> <%= arc.place&.name %> <%= arc.transition&.name %> <%= arc.guards.map(&:inspect).join(" & ") %> <% if arc.out? %> <%= link_to 'Add Gruards', new_arc_guard_path(arc), class: 'btn btn-sm btn-outline-primary mr-2' %> <% end %> <%= link_to 'Edit Arc', edit_workflow_arc_path(@workflow, arc), class: 'btn btn-sm btn-link mr-2' %> <%= link_to 'Delete Arc', [@workflow, arc], remote: true, method: :delete, data: {confirm: 'confirm?'}, class: 'btn btn-sm btn-link text-danger' %>
================================================ FILE: app/views/wf/workitem_assignments/new.html.erb ================================================
<%= form_with(model: @workitem_assignment, url: workitem_workitem_assignments_path(@workitem), local: true) do |f| %> <% if @workitem_assignment.errors.any? %>

<%= pluralize(@workitem_assignment.errors.count, "error") %> prohibited this workitem_assignment from being saved:

    <% @workitem_assignment.errors.full_messages.each do |message| %>
  • <%= message %>
  • <% end %>
<% end %>
<%= f.label :party_id, class: "label" %> <%= f.select :party_id, Wf::Party.all.map{|x| [x.party_name, x.id]}, {}, class: "form-control custom-select", placeholder: "party_id" %>
<%= f.submit class: "btn btn-primary", value: "Assign", data: {disable_with: 'Waiting...'} %>
<% end %>
================================================ FILE: app/views/wf/workitems/index.html.erb ================================================

Stats

All Enabled Started Canceled Finished Overridden
<%= link_to @workitems.unscope(:offset).unscope(:limit).unscope(:order).unscope(:select).count, workitems_path %> <%= link_to @workitems.unscope(:offset).unscope(:limit).unscope(:order).unscope(:select).enabled.count, workitems_path(state: :enabled) %> <%= link_to @workitems.unscope(:offset).unscope(:limit).unscope(:order).unscope(:select).started.count, workitems_path(state: :started) %> <%= link_to @workitems.unscope(:offset).unscope(:limit).unscope(:order).unscope(:select).canceled.count, workitems_path(state: :canceled) %> <%= link_to @workitems.unscope(:offset).unscope(:limit).unscope(:order).unscope(:select).finished.count, workitems_path(state: :finished) %> <%= link_to @workitems.unscope(:offset).unscope(:limit).unscope(:order).unscope(:select).overridden.count, workitems_path(state: :overridden) %>

Workitems

<% @workitems.each do |workitem| %> <% end %>
ID Transition State Holding User Started At Enabled At Canceled At Finished At Overridden At Deadline Detail
<%= link_to workitem.id, workitem_path(workitem) %> <%= link_to workitem.transition.name, workflow_transition_path(workitem.workflow, workitem.transition) %> <%= workitem.state %> <%= workitem.holding_user_id %> <%= workitem.started_at %> <%= workitem.enabled_at %> <%= workitem.canceled_at %> <%= workitem.finished_at %> <%= workitem.overridden_at %> <%= workitem.deadline %> <%= link_to "Run", workitem_path(workitem), class: 'btn btn-sm btn-success' %>
<%= paginate @workitems, theme: 'twitter-bootstrap-4' %>
================================================ FILE: app/views/wf/workitems/pre_finish.html.erb ================================================ <%= form_with(model: @workitem, url: finish_workitem_path(@workitem), method: :put, local: true) do |f| %> <% if @workitem.errors.any? %>

<%= pluralize(@workitem.errors.count, "error") %> prohibited this workitem from being saved:

<% end %> <% @workitem.transition.dynamic_assignments.each do |tt| %> <%= f.fields_for :dynamic_assignments do |ff| %>
<%= ff.label "Assign #{tt.name}", class: "label" %> <%= ff.select tt.id, Wf::Party.all.map{|x| [x.party_name, x.id]}, {}, class: "form-control custom-select" %>
<% end %> <% end %> <% if @workitem.transition.form %> <%= f.fields_for :entry do |ff| %> <% @workitem.transition.form.fields.each do |field| %>
<%= ff.label field.name, class: "label" %> <% if field.array? %> <%= ff.select field.id, {}, {}, multiple: true, class: "form-control select2" %> <% else %> <%= ff.send(field.field_type_for_view, field.id, class: "form-control") %> <% end %>
<% end %> <% end %> <% end %>
<%= f.submit class: "btn btn-primary", value: "Done", data: {disable_with: 'Waiting...'} %>
<% end %> ================================================ FILE: app/views/wf/workitems/show.html.erb ================================================
<%= link_to 'Back to Case', workflow_case_path(@workitem.workflow, @workitem.case), class: 'btn btn-primary' %>

Transition

ID Name Description Trigger Limit Trigger Type Sort Order Custom Form
<%= @workitem.transition.id %> <%= @workitem.transition.name %> <%= @workitem.transition.description %> <%= @workitem.transition.trigger_limit %> <%= @workitem.transition.trigger_type %> <%= @workitem.transition.sort_order %> <% if @workitem.transition.form %> <%= link_to @workitem.transition.form.name, form_path(@workitem.transition.form) %> <% else %> No Form <% end %>

Detail

ID Transition State Holding User Started At Enabled At Canceled At Finished At Overridden At Deadline
<%= @workitem.id %> <%= link_to @workitem.transition.name, workflow_transition_path(@workitem.workflow, @workitem.transition) %> <%= @workitem.state %> <%= @workitem.holding_user_id %> <%= @workitem.started_at %> <%= @workitem.enabled_at %> <%= @workitem.canceled_at %> <%= @workitem.finished_at %> <%= @workitem.overridden_at %> <%= @workitem.deadline %>
<% if @workitem.transition.form %>

Entries

<% @workitem.entries.each do |entry| %> <% end %>
ID User Payload
<%= entry.id %> <%= entry.user_id %>
                <%= JSON.pretty_generate(entry.payload) %>
              
<% end %>

Assignments

<% @workitem.workitem_assignments.includes(:party).each do |assignment|%> <% end %>
ID Party ID Name Created At Action
<%= assignment.id %> <%= assignment.party_id %> <%= assignment.party.party_name %> <%= assignment.created_at %> <%= link_to "Remove", workitem_workitem_assignment_path(@workitem, party_id: assignment.party_id), remote: true, method: :delete, data: {confirm: 'confirm?'}, class: 'btn btn-sm btn-info' %>
<% if @workitem.transition.user? && (@workitem.started? || @workitem.enabled?) %> <% if @workitem.finished_by?(wf_current_user) %> <%= link_to "Pre Finish Workitem", pre_finish_workitem_path(@workitem), class: 'btn btn-sm btn-dark' %> <% elsif @workitem.started_by?(wf_current_user)%> <%= link_to "Start Workitem", start_workitem_path(@workitem), method: :put, class: 'btn btn-sm btn-dark' %> <% else %> You can not start workitem, Please assign to youself. <% end %> <%= link_to "Re Assign", new_workitem_workitem_assignment_path(@workitem), class: 'btn btn-success btn-sm' %> <%= link_to "Assign Yourself", new_workitem_workitem_assignment_path(@workitem, party_id: wf_current_user.party&.id), class: 'btn btn-success btn-sm' %> <%= link_to "Add Comment", new_workitem_comment_path(@workitem), class: 'btn btn-success btn-sm' %> <% end %>

Comments

<% @workitem.comments.order("id DESC").each do |comment| %> <% end %>
ID Body User Created At Action
<%= comment.id %> <%= comment.body %> <%= comment.user_id %> <%= comment.created_at %> <%= link_to "Delete", workitem_comment_path(@workitem, comment), remote: true, method: :delete, data: {confirm: 'confirm?', disable_with: 'Waiting...'}, class: 'btn btn-sm btn-info' %>

All Workitems

<% @workitem.case.workitems.includes(:transition).each do |workitem| %> <% end %>
ID Transition State Trigger Type Holding User Started At Enabled At Canceled At Finished At Overridden At Deadline Action
<%= link_to workitem.id, workitem_path(workitem) %> <%= workitem.transition.name %> <%= workitem.state %> <%= workitem.transition.trigger_type %> <%= workitem.holding_user_id %> <%= workitem.started_at %> <%= workitem.enabled_at %> <%= workitem.canceled_at %> <%= workitem.finished_at %> <%= workitem.overridden_at %> <%= workitem.deadline %> <%= link_to "Run", workitem_path(workitem), class: 'btn btn-sm btn-success' %>
================================================ FILE: bin/rails ================================================ #!/usr/bin/env ruby # 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/wf/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' ================================================ FILE: config/routes.rb ================================================ # frozen_string_literal: true Wf::Engine.routes.draw do resources :workflows do resources :transitions resources :places resources :arcs resources :cases end resources :arcs do resources :guards end resources :forms do resources :fields end resources :transitions do resources :static_assignments end resources :workitems do resources :workitem_assignments, only: %i[new create destroy] resources :comments, only: %i[new create destroy] member do put :start get :pre_finish put :finish end end root to: "workitems#index" end ================================================ FILE: db/migrate/20200130201043_init.rb ================================================ # frozen_string_literal: true class Init < ActiveRecord::Migration[6.0] def change create_table "wf_arcs", force: :cascade do |t| t.bigint "workflow_id" t.bigint "transition_id" t.bigint "place_id" t.integer "direction", default: 0, comment: "0-in, 1-out" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.integer "guards_count", default: 0 end create_table "wf_case_assignments", comment: "Manual per-case assignments of transition to parties", force: :cascade do |t| t.bigint "case_id" t.bigint "transition_id" t.bigint "party_id" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index %w[case_id transition_id party_id], name: "wf_ctp_u", unique: true end create_table "wf_cases", force: :cascade do |t| t.bigint "workflow_id" t.string "targetable_type", comment: "point to target type of Application." t.string "targetable_id", comment: "point to target ID of Application." t.integer "state", default: 0, comment: "0-created, 1-active, 2-suspended, 3-canceled, 4-finished" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end create_table "wf_comments", force: :cascade do |t| t.bigint "workitem_id" t.string "user_id" t.text "body" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["user_id"], name: "index_wf_comments_on_user_id" t.index ["workitem_id"], name: "index_wf_comments_on_workitem_id" end create_table "wf_demo_targets", comment: "For demo, useless.", force: :cascade do |t| t.string "name" t.string "description" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end create_table "wf_field_values", force: :cascade do |t| t.bigint "workflow_id" t.bigint "transition_id" t.bigint "form_id" t.bigint "field_id" t.text "value" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["field_id"], name: "index_wf_field_values_on_field_id" t.index ["form_id"], name: "index_wf_field_values_on_form_id" t.index ["transition_id"], name: "index_wf_field_values_on_transition_id" t.index ["workflow_id"], name: "index_wf_field_values_on_workflow_id" end create_table "wf_fields", force: :cascade do |t| t.string "name" t.bigint "form_id" t.integer "position", default: 0 t.integer "field_type", default: 0 t.string "field_type_name" t.string "default_value" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["form_id"], name: "index_wf_fields_on_form_id" end create_table "wf_forms", force: :cascade do |t| t.string "name" t.text "description" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end create_table "wf_groups", comment: "For demo", force: :cascade do |t| t.string "name" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end create_table "wf_guards", force: :cascade do |t| t.bigint "arc_id" t.bigint "workflow_id" t.string "fieldable_type" t.string "fieldable_id" t.string "op" t.string "value" t.string "exp" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["arc_id"], name: "index_wf_guards_on_arc_id" t.index ["workflow_id"], name: "index_wf_guards_on_workflow_id" end create_table "wf_parties", comment: "for groups or roles or users or positions etc.", force: :cascade do |t| t.string "partable_type" t.string "partable_id" t.string "party_name" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index %w[partable_type partable_id], name: "index_wf_parties_on_partable_type_and_partable_id", unique: true end create_table "wf_places", force: :cascade do |t| t.bigint "workflow_id" t.string "name" t.text "description" t.integer "sort_order", default: 0 t.integer "place_type", default: 0, comment: "类型:0-start,1-normal,2-end" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end create_table "wf_tokens", force: :cascade do |t| t.bigint "workflow_id" t.bigint "case_id" t.string "targetable_type" t.string "targetable_id" t.bigint "place_id" t.integer "state", default: 0, comment: "0-free, 1-locked, 2-canceled, 3-consumed" t.bigint "locked_workitem_id" if /mysql/i.match?(ActiveRecord::Base.connection.adapter_name) t.datetime "produced_at", default: -> { "current_timestamp" } elsif /postgre/i.match?(ActiveRecord::Base.connection.adapter_name) t.datetime "produced_at", default: -> { "timezone('utc'::text, now())" } end t.datetime "locked_at" t.datetime "canceled_at" t.datetime "consumed_at" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end create_table "wf_transition_static_assignments", comment: "pre assignment for transition", force: :cascade do |t| t.bigint "party_id" t.bigint "transition_id" t.bigint "workflow_id" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index %w[transition_id party_id], name: "wf_tp_u", unique: true end create_table "wf_transitions", force: :cascade do |t| t.string "name" t.text "description" t.bigint "workflow_id" t.integer "sort_order", default: 0 t.integer "trigger_limit", comment: "use with timed trigger, after x minitues, trigger exec" t.integer "trigger_type", default: 0, comment: "0-user,1-automatic, 2-message,3-time" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "form_id" t.string "enable_callback", default: "Wf::Callbacks::EnableDefault" t.string "fire_callback", default: "Wf::Callbacks::FireDefault" t.string "notification_callback", default: "Wf::Callbacks::NotificationDefault" t.string "time_callback", default: "Wf::Callbacks::TimeDefault" t.string "deadline_callback", default: "Wf::Callbacks::DeadlineDefault" t.string "hold_timeout_callback", default: "Wf::Callbacks::HoldTimeoutDefault" t.string "assignment_callback", default: "Wf::Callbacks::AssignmentDefault" t.string "unassignment_callback", default: "Wf::Callbacks::UnassignmentDefault" end create_table "wf_users", comment: "For demo", force: :cascade do |t| t.string "name" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "group_id" end create_table "wf_workflows", force: :cascade do |t| t.string "name" t.text "description" t.boolean "is_valid", default: false t.string "creator_id" t.text "error_msg" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end create_table "wf_workitem_assignments", force: :cascade do |t| t.bigint "party_id" t.bigint "workitem_id" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index %w[workitem_id party_id], name: "wf_wp_u", unique: true end create_table "wf_workitems", force: :cascade do |t| t.bigint "case_id" t.bigint "workflow_id" t.bigint "transition_id" t.string "targetable_type", comment: "point to type of Application target: Task or Issue or PullRequest or Project etc." t.string "targetable_id", comment: "point to id of Application target: task_id or issue_id or pull_request_id or project_id etc." t.integer "state", default: 0, comment: "0-enabled, 1-started, 2-canceled, 3-finished,4-overridden" if /mysql/i.match?(ActiveRecord::Base.connection.adapter_name) t.datetime "enabled_at", default: -> { "current_timestamp" } elsif /postgre/i.match?(ActiveRecord::Base.connection.adapter_name) t.datetime "enabled_at", default: -> { "timezone('utc'::text, now())" } end t.datetime "started_at" t.datetime "canceled_at" t.datetime "finished_at" t.datetime "overridden_at" t.datetime "deadline", comment: "" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.datetime "trigger_time", comment: "set when transition_trigger=TIME & trigger_limit present" t.string "holding_user_id", comment: "id of App user" if /mysql/i.match?(ActiveRecord::Base.connection.adapter_name) t.json "payload", comment: "store user input payload for workitem." elsif /postgre/i.match?(ActiveRecord::Base.connection.adapter_name) t.json "payload", default: {}, comment: "store user input payload for workitem." end t.index %w[state trigger_time], name: "index_wf_workitems_on_state_and_trigger_time" end end end ================================================ FILE: db/migrate/20200130201641_init_some_data.rb ================================================ # frozen_string_literal: true class InitSomeData < ActiveRecord::Migration[6.0] def change 5.times do |i| Wf::Group.create(name: "Group#{i}") end 10.times do |i| Wf::User.create(name: "User#{i}", group: Wf::Group.all.sample) end 10.times { |i| Wf::DemoTarget.create(name: "target #{i}") } end end ================================================ FILE: db/migrate/20200131200455_create_wf_entries.rb ================================================ # frozen_string_literal: true class CreateWfEntries < ActiveRecord::Migration[6.0] def change create_table :wf_entries, comment: "user input data for workitem with form." do |t| t.string :user_id, index: true t.bigint :workitem_id, index: true if /mysql/i.match?(ActiveRecord::Base.connection.adapter_name) t.json "payload" elsif /postgre/i.match?(ActiveRecord::Base.connection.adapter_name) t.json "payload", default: {} end t.timestamps end remove_column :wf_workitems, :payload add_column :wf_field_values, :entry_id, :bigint, index: true add_index :wf_entries, %i[workitem_id user_id], unique: true end end ================================================ FILE: db/migrate/20200201001543_add_target_field_name_for_guard.rb ================================================ # frozen_string_literal: true class AddTargetFieldNameForGuard < ActiveRecord::Migration[6.0] def change add_column :wf_guards, :target_attr_name, :string, comment: "point to workflow targetable's attribute" end end ================================================ FILE: db/migrate/20200212120019_remove_targetable_from_workitem.rb ================================================ # frozen_string_literal: true class RemoveTargetableFromWorkitem < ActiveRecord::Migration[6.0] def change remove_column :wf_workitems, :targetable_id remove_column :wf_workitems, :targetable_type end end ================================================ FILE: db/migrate/20200213085258_add_formable.rb ================================================ # frozen_string_literal: true class AddFormable < ActiveRecord::Migration[6.0] def change add_column :wf_transitions, :form_type, :string, default: "Wf::Form" add_index :wf_transitions, %i[form_type form_id] end end ================================================ FILE: db/migrate/20200213125753_add_form_id_for_entry.rb ================================================ # frozen_string_literal: true class AddFormIdForEntry < ActiveRecord::Migration[6.0] def change add_column :wf_entries, :form_id, :bigint, index: true end end ================================================ FILE: db/migrate/20200213130900_remove_workflow_id_from_form_related.rb ================================================ # frozen_string_literal: true class RemoveWorkflowIdFromFormRelated < ActiveRecord::Migration[6.0] def change remove_column :wf_field_values, :transition_id remove_column :wf_field_values, :workflow_id end end ================================================ FILE: db/migrate/20200220070839_remove_unused_column.rb ================================================ # frozen_string_literal: true class RemoveUnusedColumn < ActiveRecord::Migration[6.0] def change remove_column :wf_guards, :target_attr_name end end ================================================ FILE: db/migrate/20200220072512_add_sub_workflow.rb ================================================ # frozen_string_literal: true class AddSubWorkflow < ActiveRecord::Migration[6.0] def change add_column :wf_transitions, :sub_workflow_id, :bigint, index: true add_column :wf_cases, :started_by_workitem_id, :bigint, index: true, comment: "As a sub workflow instance, it is started by one workitem." end end ================================================ FILE: db/migrate/20200222150432_add_multi_instance.rb ================================================ # frozen_string_literal: true class AddMultiInstance < ActiveRecord::Migration[6.0] def change add_column :wf_transitions, :multiple_instance, :boolean, default: false, comment: "multiple instance mode or not" add_column :wf_transitions, :finish_condition, :string, comment: "set finish condition for parent workitem.", default: "Wf::MultipleInstances::AllFinish" add_column :wf_workitems, :children_count, :integer, default: 0 add_column :wf_workitems, :children_finished_count, :integer, default: 0 add_column :wf_workitems, :forked, :boolean, default: false add_column :wf_workitems, :parent_id, :bigint, comment: "parent workitem id" end end ================================================ FILE: db/migrate/20200226195134_add_dynamic_assign_by.rb ================================================ # frozen_string_literal: true class AddDynamicAssignBy < ActiveRecord::Migration[6.0] def change add_column :wf_transitions, :dynamic_assign_by_id, :bigint, comment: "dynamic assign by other transition", index: true end end ================================================ FILE: lib/tasks/wf_tasks.rake ================================================ # frozen_string_literal: true desc "Wf tasks" task wf: :environment do url = "http://service-technology.org/files/lola/lola-2.0.tar.gz" path = Rails.root.join("tmp", "lola.tar.gz").to_s puts "Downloading, wait!" puts `wget http://service-technology.org/files/lola/lola-2.0.tar.gz -v -t0 -O #{path}` unless File.exist?(path) puts `cd #{Rails.root.join("tmp")} && tar -zxvf lola.tar.gz` puts `cd #{Rails.root.join("tmp/lola-2.0")} && ./configure` puts `cd #{Rails.root.join("tmp/lola-2.0")} && make` puts `cd #{Rails.root.join("tmp/lola-2.0")} && sudo make install` puts `lola --help` end ================================================ FILE: lib/wf/engine.rb ================================================ # frozen_string_literal: true module Wf class Engine < ::Rails::Engine isolate_namespace Wf config.autoload_paths += %W[ #{config.root}/app/models/wf/concerns ] config.to_prepare do require_dependency(Rails.root + "config/initializers/wf_config.rb") rescue LoadError puts("config/initializers/wf_config.rb not found.") end end end require "bootstrap" require "bootstrap4-kaminari-views" require "jquery-rails" require "kaminari" require "simple_command" require "loaf" require "graphviz" require "rgl/adjacency" require "rgl/dijkstra" require "rgl/topsort" require "rgl/traversal" require "rgl/path" require "active_record/connection_adapters/postgresql_adapter.rb" require "select2-rails" require "mini_racer" ================================================ FILE: lib/wf/version.rb ================================================ # frozen_string_literal: true module Wf VERSION = "0.2.5" end ================================================ FILE: lib/wf.rb ================================================ # frozen_string_literal: true require "wf/engine" module Wf class << self attr_accessor :enable_callbacks attr_accessor :fire_callbacks attr_accessor :assignment_callbacks attr_accessor :unassignment_callbacks attr_accessor :notification_callbacks attr_accessor :time_callbacks attr_accessor :deadline_callbacks attr_accessor :hold_timeout_callbacks attr_accessor :form_class attr_accessor :entry_class attr_accessor :field_class attr_accessor :user_class attr_accessor :org_classes attr_accessor :finish_conditions attr_accessor :use_lola end self.enable_callbacks = ["Wf::Callbacks::EnableDefault"] self.fire_callbacks = ["Wf::Callbacks::FireDefault"] self.assignment_callbacks = ["Wf::Callbacks::AssignmentDefault"] self.unassignment_callbacks = ["Wf::Callbacks::UnassignmentDefault"] self.notification_callbacks = ["Wf::Callbacks::NotificationDefault"] self.deadline_callbacks = ["Wf::Callbacks::DeadlineDefault"] self.time_callbacks = ["Wf::Callbacks::TimeDefault"] self.hold_timeout_callbacks = ["Wf::Callbacks::HoldTimeoutDefault"] self.form_class = "::Wf::Form" self.entry_class = "::Wf::Entry" self.field_class = "::Wf::Field" self.user_class = "::Wf::User" self.org_classes = { group: "::Wf::Group" } self.finish_conditions = ["Wf::MultipleInstances::AllFinish"] self.use_lola = false end ================================================ FILE: lola.md ================================================ ## LoLA LoLA is a Petri nets model-checking tool. To install LoLA, download lola-2.0.tar.gz from http://home.gna.org/service-tech/lola/index.html and extract it. You will need a working C++ compiler such as GCC or Clang. On Linux and OS X, LoLA can be compiled with ./configure and make. On Windows, you might need a Unix-like environment via Cygwin. LoLA can answer reachability queries using the flag '-f' followed by 'REACHABLE' and a formula, e.g.: lola petrinet.lola -f "REACHABLE DEADLOCK" # Tests whether a dead marking can be reached lola petrinet.lola -f "REACHABLE (p = 1 AND q > 2)" # Tests whether a marking M such that # M(p) = 1 and M(q) > 2 can be reached lola petrinet.lola -f "REACHABLE (p + q = 1 OR r > 2)" # Tests whether a marking M such that # M(p) + M(q) = 1 or M(r) > 2 can be reached lola petrinet.lola -f "REACHABLE FIREABLE(t)" # Tests whether it is possible to reach a marking # at which t is fireable Positive reachability queries can be witnessed by a state and a path using flags -s and -p respectively. The tool documentation can be found at http://download.gna.org/service-tech/lola/lola.pdf. ## Check Workflow Net ``` ## generate lola format file. # PLACE P1,P2,P3; # MARKING P1; # TRANSITION T1 # CONSUME P1:1; # PRODUCE P3:1; # TRANSITION T2 # CONSUME P3:1; # PRODUCE P2:1; ## checking # reachability of final marking # lola /Users/hooopo/w/wf/test/dummy/tmp/1-1582990693.lola --formula="AGEF(P1 = 0 AND P3 = 0 AND P2 = 1)" --json=/Users/hooopo/w/wf/test/dummy/tmp/1-reachability_of_final_marking.json # quasiliveness # lola /Users/hooopo/w/wf/test/dummy/tmp/1-1582990693.lola --formula="AG NOT FIREABLE (T1)" --json=/Users/hooopo/w/wf/test/dummy/tmp/1-dead_transition_1.json # lola /Users/hooopo/w/wf/test/dummy/tmp/1-1582990693.lola --formula="AG NOT FIREABLE (T2)" --json=/Users/hooopo/w/wf/test/dummy/tmp/1-dead_transition_2.json # deadlock # lola /Users/hooopo/w/wf/test/dummy/tmp/1-1582990693.lola --formula="EF (DEADLOCK AND (P2 = 0))" --json=/Users/hooopo/w/wf/test/dummy/tmp/1-deadlock.json ``` ================================================ FILE: screenshots/.keep ================================================ ================================================ FILE: test/controllers/wf/arcs_controller_test.rb ================================================ # frozen_string_literal: true require "test_helper" module Wf class ArcsControllerTest < ActionDispatch::IntegrationTest include Engine.routes.url_helpers # test "the truth" do # assert true # end end end ================================================ FILE: test/controllers/wf/cases_controller_test.rb ================================================ # frozen_string_literal: true require "test_helper" module Wf class CasesControllerTest < ActionDispatch::IntegrationTest include Engine.routes.url_helpers # test "the truth" do # assert true # end end end ================================================ FILE: test/controllers/wf/comments_controller_test.rb ================================================ # frozen_string_literal: true require "test_helper" module Wf class CommentsControllerTest < ActionDispatch::IntegrationTest include Engine.routes.url_helpers # test "the truth" do # assert true # end end end ================================================ FILE: test/controllers/wf/fields_controller_test.rb ================================================ # frozen_string_literal: true require "test_helper" module Wf class FieldsControllerTest < ActionDispatch::IntegrationTest include Engine.routes.url_helpers # test "the truth" do # assert true # end end end ================================================ FILE: test/controllers/wf/forms_controller_test.rb ================================================ # frozen_string_literal: true require "test_helper" module Wf class FormsControllerTest < ActionDispatch::IntegrationTest include Engine.routes.url_helpers # test "the truth" do # assert true # end end end ================================================ FILE: test/controllers/wf/guards_controller_test.rb ================================================ # frozen_string_literal: true require "test_helper" module Wf class GuardsControllerTest < ActionDispatch::IntegrationTest include Engine.routes.url_helpers # test "the truth" do # assert true # end end end ================================================ FILE: test/controllers/wf/places_controller_test.rb ================================================ # frozen_string_literal: true require "test_helper" module Wf class PlacesControllerTest < ActionDispatch::IntegrationTest include Engine.routes.url_helpers # test "the truth" do # assert true # end end end ================================================ FILE: test/controllers/wf/static_assignments_controller_test.rb ================================================ # frozen_string_literal: true require "test_helper" module Wf class StaticAssignmentsControllerTest < ActionDispatch::IntegrationTest include Engine.routes.url_helpers # test "the truth" do # assert true # end end end ================================================ FILE: test/controllers/wf/transitions_controller_test.rb ================================================ # frozen_string_literal: true require "test_helper" module Wf class TransitionsControllerTest < ActionDispatch::IntegrationTest include Engine.routes.url_helpers # test "the truth" do # assert true # end end end ================================================ FILE: test/controllers/wf/workflows_controller_test.rb ================================================ # frozen_string_literal: true require "test_helper" module Wf class WorkflowsControllerTest < ActionDispatch::IntegrationTest include Engine.routes.url_helpers # test "the truth" do # assert true # end end end ================================================ FILE: test/controllers/wf/workitem_assignments_controller_test.rb ================================================ # frozen_string_literal: true require "test_helper" module Wf class WorkitemAssignmentsControllerTest < ActionDispatch::IntegrationTest include Engine.routes.url_helpers # test "the truth" do # assert true # end end end ================================================ FILE: test/controllers/wf/workitems_controller_test.rb ================================================ # frozen_string_literal: true require "test_helper" module Wf class WorkitemsControllerTest < ActionDispatch::IntegrationTest include Engine.routes.url_helpers # test "the truth" do # assert true # end end end ================================================ FILE: test/dummy/.ruby-version ================================================ ruby-3.1.1 ================================================ FILE: test/dummy/Rakefile ================================================ # 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 ================================================ FILE: test/dummy/app/assets/config/manifest.js ================================================ //= link_tree ../images //= link_directory ../stylesheets .css //= link wf_manifest.js ================================================ FILE: test/dummy/app/assets/images/.keep ================================================ ================================================ FILE: test/dummy/app/assets/stylesheets/application.css ================================================ /* * This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. * * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the bottom of the * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS * files in this directory. Styles in this file should be added after the last require_* statement. * It is generally better to create a new file per style scope. * *= require_tree . *= require_self */ ================================================ FILE: test/dummy/app/channels/application_cable/channel.rb ================================================ # frozen_string_literal: true module ApplicationCable class Channel < ActionCable::Channel::Base end end ================================================ FILE: test/dummy/app/channels/application_cable/connection.rb ================================================ # frozen_string_literal: true module ApplicationCable class Connection < ActionCable::Connection::Base end end ================================================ FILE: test/dummy/app/controllers/application_controller.rb ================================================ # frozen_string_literal: true class ApplicationController < ActionController::Base def current_user Wf::User.first end helper_method :current_user end ================================================ FILE: test/dummy/app/controllers/concerns/.keep ================================================ ================================================ FILE: test/dummy/app/helpers/application_helper.rb ================================================ # frozen_string_literal: true module ApplicationHelper end ================================================ FILE: test/dummy/app/javascript/packs/application.js ================================================ // This is a manifest file that'll be compiled into application.js, which will include all the files // listed below. // // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. // // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // compiled file. JavaScript code in this file should be added after the last require_* statement. // // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details // about supported directives. // //= require rails-ujs //= require activestorage //= require_tree . ================================================ FILE: test/dummy/app/jobs/application_job.rb ================================================ # frozen_string_literal: true class ApplicationJob < ActiveJob::Base # Automatically retry jobs that encountered a deadlock # retry_on ActiveRecord::Deadlocked # Most jobs are safe to ignore if the underlying records are no longer available # discard_on ActiveJob::DeserializationError end ================================================ FILE: test/dummy/app/mailers/application_mailer.rb ================================================ # frozen_string_literal: true class ApplicationMailer < ActionMailer::Base default from: "from@example.com" layout "mailer" end ================================================ FILE: test/dummy/app/models/application_record.rb ================================================ # frozen_string_literal: true class ApplicationRecord < ActiveRecord::Base self.abstract_class = true end ================================================ FILE: test/dummy/app/models/concerns/.keep ================================================ ================================================ FILE: test/dummy/app/models/entry.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_entries # # id :integer not null, primary key # user_id :string # workitem_id :integer # payload :json default("{}") # created_at :datetime not null # updated_at :datetime not null # class Entry < ApplicationRecord belongs_to :form belongs_to :user, class_name: Wf.user_class.to_s belongs_to :workitem, class_name: "Wf::Workitem" has_many :field_values after_initialize do self.payload = {} if payload.blank? end def json field_values.includes(:field).map { |x| [x.field_id.to_i, { field_id: x.id.to_i, field_name: x.field.name, value: x.value_after_cast }] }.to_h end def update_payload! update(payload: json) end end ================================================ FILE: test/dummy/app/models/field.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_fields # # id :integer not null, primary key # name :string # form_id :integer # position :integer default("0") # field_type :integer default("0") # field_type_name :string # default_value :string # created_at :datetime not null # updated_at :datetime not null # class Field < ApplicationRecord belongs_to :form, touch: true enum field_type: { string: 0, integer: 1, boolean: 2, date: 3, datetime: 4, decimal: 5, float: 6, json: 7, text: 8, "string[]": 20, "integer[]": 21, "date[]": 23, "datetime[]": 24, "decimal[]": 25, "float[]": 26, "json[]": 27, "text[]": 28 } # TODO: array type def field_type_for_view case field_type when "string" "text_field" when "integer" "number_field" when "date" "date_field" when "datetime" "datetime_field" when "boolean" "check_box" when "text" "text_area" else "text_field" end end def array? field_type.to_s.match(/^(\w+)(\[\])?$/)[2] == "[]" end delegate :cast, to: :type_for_cast def type_for_cast type = field_type.to_s.match(/^(\w+)(\[\])?$/)[1] if array? ActiveRecord::Type.lookup(type.to_sym, adapter: :postgresql, array: true) else ActiveRecord::Type.lookup(type.to_sym, adapter: :postgresql) end end end ================================================ FILE: test/dummy/app/models/field_value.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_field_values # # id :integer not null, primary key # workflow_id :integer # transition_id :integer # form_id :integer # field_id :integer # value :text # created_at :datetime not null # updated_at :datetime not null # entry_id :integer # class FieldValue < ApplicationRecord belongs_to :form belongs_to :field belongs_to :entry def value_after_cast ov = self[:value] if field.array? && !ov.is_a?(Array) v = begin JSON.parse(ov) rescue StandardError [] end field.type_for_cast.cast(v) else field.type_for_cast.cast(ov) end end def value=(v) self[:value] = if field.array? Array(v.as_json) else v end end def value value_after_cast end end ================================================ FILE: test/dummy/app/models/form.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_forms # # id :integer not null, primary key # name :string # description :text # created_at :datetime not null # updated_at :datetime not null # class Form < ApplicationRecord has_many :fields, dependent: :destroy has_many :entries end ================================================ FILE: test/dummy/app/views/layouts/application.html.erb ================================================ Dummy <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= stylesheet_link_tag 'application', media: 'all' %> <%= yield %> ================================================ FILE: test/dummy/app/views/layouts/mailer.html.erb ================================================ <%= yield %> ================================================ FILE: test/dummy/app/views/layouts/mailer.text.erb ================================================ <%= yield %> ================================================ FILE: test/dummy/bin/rails ================================================ #!/usr/bin/env ruby APP_PATH = File.expand_path('../config/application', __dir__) require_relative '../config/boot' require 'rails/commands' ================================================ FILE: test/dummy/bin/rake ================================================ #!/usr/bin/env ruby require_relative '../config/boot' require 'rake' Rake.application.run ================================================ FILE: test/dummy/bin/setup ================================================ #!/usr/bin/env ruby require 'fileutils' # path to your application root. APP_ROOT = File.expand_path('..', __dir__) def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") end FileUtils.chdir APP_ROOT do # This script is a way to setup or update your development environment automatically. # This script is idempotent, so that you can run it at anytime and get an expectable outcome. # Add necessary setup steps to this file. puts '== Installing dependencies ==' system! 'gem install bundler --conservative' system('bundle check') || system!('bundle install') # puts "\n== Copying sample files ==" # unless File.exist?('config/database.yml') # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' # end puts "\n== Preparing database ==" system! 'bin/rails db:prepare' puts "\n== Removing old logs and tempfiles ==" system! 'bin/rails log:clear tmp:clear' puts "\n== Restarting application server ==" system! 'bin/rails restart' end ================================================ FILE: test/dummy/config/application.rb ================================================ # frozen_string_literal: true require_relative "boot" require "rails/all" Bundler.require(*Rails.groups) require "wf" module Dummy class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 6.0 # Settings in config/environments/* take precedence over those specified here. # Application configuration can go into files in config/initializers # -- all .rb files in that directory are automatically loaded after loading # the framework and any gems in your application. end end ================================================ FILE: test/dummy/config/boot.rb ================================================ # frozen_string_literal: true # Set up gems listed in the Gemfile. ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__) require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) $LOAD_PATH.unshift File.expand_path("../../../lib", __dir__) ================================================ FILE: test/dummy/config/cable.yml ================================================ development: adapter: async test: adapter: test production: adapter: redis url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> channel_prefix: dummy_production ================================================ FILE: test/dummy/config/database.yml ================================================ # PostgreSQL. Versions 9.3 and up are supported. # # Install the pg driver: # gem install pg # On macOS with Homebrew: # gem install pg -- --with-pg-config=/usr/local/bin/pg_config # On macOS with MacPorts: # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config # On Windows: # gem install pg # Choose the win32 build. # Install PostgreSQL and put its /bin directory on your path. # # Configure Using Gemfile # gem 'pg' # default: &default adapter: postgresql encoding: unicode # For details on connection pooling, see Rails configuration guide # https://guides.rubyonrails.org/configuring.html#database-pooling pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> development: <<: *default database: wf_dummy_development # The specified database role being used to connect to postgres. # To create additional roles in postgres see `$ createuser --help`. # When left blank, postgres will use the default role. This is # the same name as the operating system user that initialized the database. #username: dummy # The password associated with the postgres role (username). #password: # Connect on a TCP socket. Omitted by default since the client uses a # domain socket that doesn't need configuration. Windows does not have # domain sockets, so uncomment these lines. #host: localhost # The TCP port the server listens on. Defaults to 5432. # If your server runs on a different port number, change accordingly. #port: 5432 # Schema search path. The server defaults to $user,public #schema_search_path: myapp,sharedapp,public # Minimum log levels, in increasing order: # debug5, debug4, debug3, debug2, debug1, # log, notice, warning, error, fatal, and panic # Defaults to warning. #min_messages: notice # 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: dummy_test # As with config/credentials.yml, you never want to store sensitive information, # like your database password, in your source code. If your source code is # ever seen by anyone, they now have access to your database. # # Instead, provide the password as a unix environment variable when you boot # the app. Read https://guides.rubyonrails.org/configuring.html#configuring-a-database # for a full rundown on how to provide these environment variables in a # production deployment. # # On Heroku and other platform providers, you may have a full connection URL # available as an environment variable. For example: # # DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" # # You can use this database configuration with: # # production: # url: <%= ENV['DATABASE_URL'] %> # production: <<: *default database: dummy_production username: dummy password: <%= ENV['DUMMY_DATABASE_PASSWORD'] %> ================================================ FILE: test/dummy/config/environment.rb ================================================ # frozen_string_literal: true # Load the Rails application. require_relative "application" # Initialize the Rails application. Rails.application.initialize! ================================================ FILE: test/dummy/config/environments/development.rb ================================================ # frozen_string_literal: true 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. config.consider_all_requests_local = true # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. if Rails.root.join("tmp", "caching-dev.txt").exist? config.action_controller.perform_caching = true config.action_controller.enable_fragment_cache_logging = true config.cache_store = :memory_store config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{2.days.to_i}" } else config.action_controller.perform_caching = false config.cache_store = :null_store end # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false config.action_mailer.perform_caching = 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 # Highlight code that triggered database queries in logs. config.active_record.verbose_query_logs = true # 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 # Suppress logger output for asset requests. config.assets.quiet = true # Raises error for missing translations. # config.action_view.raise_on_missing_translations = true # Use an evented file watcher to asynchronously detect changes in source code, # routes, locales, etc. This feature depends on the listen gem. # config.file_watcher = ActiveSupport::EventedFileUpdateChecker end ================================================ FILE: test/dummy/config/environments/production.rb ================================================ # frozen_string_literal: true 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 # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). # config.require_master_key = true # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? # Compress CSS using a preprocessor. # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.action_controller.asset_host = 'http://assets.example.com' # 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 # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local # Mount Action Cable outside main process or domain. # config.action_cable.mount_path = nil # config.action_cable.url = 'wss://example.com/cable' # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] # 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 = [:request_id] # Use a different cache store in production. # config.cache_store = :mem_cache_store # Use a real queuing backend for Active Job (and separate queues per environment). # config.active_job.queue_adapter = :resque # config.active_job.queue_name_prefix = "dummy_production" config.action_mailer.perform_caching = false # 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 # Use a different logger for distributed setups. # require 'syslog/logger' # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') if ENV["RAILS_LOG_TO_STDOUT"].present? logger = ActiveSupport::Logger.new(STDOUT) logger.formatter = config.log_formatter config.logger = ActiveSupport::TaggedLogging.new(logger) end # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false # Inserts middleware to perform automatic connection switching. # The `database_selector` hash is used to pass options to the DatabaseSelector # middleware. The `delay` is used to determine how long to wait after a write # to send a subsequent read to the primary. # # The `database_resolver` class is used by the middleware to determine which # database is appropriate to use based on the time delay. # # The `database_resolver_context` class is used by the middleware to set # timestamps for the last write to the primary. The resolver uses the context # class timestamps to determine how long to wait before reading from the # replica. # # By default Rails will store a last write timestamp in the session. The # DatabaseSelector middleware is designed as such you can define your own # strategy for connection switching and pass that into the middleware through # these configuration options. # config.active_record.database_selector = { delay: 2.seconds } # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session end ================================================ FILE: test/dummy/config/environments/test.rb ================================================ # frozen_string_literal: true # 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! Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. config.cache_classes = false # 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 public file server for tests with Cache-Control for performance. config.public_file_server.enabled = true config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{1.hour.to_i}" } # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false config.cache_store = :null_store # 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 # Store uploaded files on the local file system in a temporary directory. config.active_storage.service = :test config.action_mailer.perform_caching = false # 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 # 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 ================================================ FILE: test/dummy/config/initializers/application_controller_renderer.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # ActiveSupport::Reloader.to_prepare do # ApplicationController.renderer.defaults.merge!( # http_host: 'example.org', # https: false # ) # end ================================================ FILE: test/dummy/config/initializers/assets.rb ================================================ # frozen_string_literal: true # 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 the app/assets # folder are already added. # Rails.application.config.assets.precompile += %w( admin.js admin.css ) ================================================ FILE: test/dummy/config/initializers/backtrace_silencers.rb ================================================ # frozen_string_literal: true # 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! ================================================ FILE: test/dummy/config/initializers/content_security_policy.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # Define an application-wide content security policy # For further information see the following documentation # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy # Rails.application.config.content_security_policy do |policy| # policy.default_src :self, :https # policy.font_src :self, :https, :data # policy.img_src :self, :https, :data # policy.object_src :none # policy.script_src :self, :https # policy.style_src :self, :https # # Specify URI for violation reports # # policy.report_uri "/csp-violation-report-endpoint" # end # If you are using UJS then enable automatic nonce generation # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } # Set the nonce only to specific directives # Rails.application.config.content_security_policy_nonce_directives = %w(script-src) # Report CSP violations to a specified URI # For further information see the following documentation: # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only # Rails.application.config.content_security_policy_report_only = true ================================================ FILE: test/dummy/config/initializers/cookies_serializer.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # Specify a serializer for the signed and encrypted cookie jars. # Valid options are :json, :marshal, and :hybrid. Rails.application.config.action_dispatch.cookies_serializer = :json ================================================ FILE: test/dummy/config/initializers/filter_parameter_logging.rb ================================================ # frozen_string_literal: true # 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] ================================================ FILE: test/dummy/config/initializers/inflections.rb ================================================ # frozen_string_literal: true # 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 ================================================ FILE: test/dummy/config/initializers/mime_types.rb ================================================ # frozen_string_literal: true # 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 ================================================ FILE: test/dummy/config/initializers/my_assignment_callback.rb ================================================ # frozen_string_literal: true class MyAssignmentCallback def perform(_workitem_id) Wf::Party.all.sample(2) end end Wf.assignment_callbacks = ["Wf::Callbacks::AssignmentDefault", "MyAssignmentCallback"] ================================================ FILE: test/dummy/config/initializers/wf_config.rb ================================================ # frozen_string_literal: true # TODO: use setter Wf.user_class = "::Wf::User" Wf.org_classes = { group: "::Wf::Group" } Wf.form_class = "Wf::Form" Wf.entry_class = "Wf::Entry" Wf.field_class = "Wf::Field" ================================================ FILE: test/dummy/config/initializers/wrap_parameters.rb ================================================ # frozen_string_literal: true # 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] end # To enable root element in JSON for ActiveRecord objects. # ActiveSupport.on_load(:active_record) do # self.include_root_in_json = true # end ================================================ FILE: test/dummy/config/locales/en.yml ================================================ # 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. # # The following keys must be escaped otherwise they will not be retrieved by # the default I18n backend: # # true, false, on, off, yes, no # # Instead, surround them with single quotes. # # en: # 'true': 'foo' # # To learn more, please read the Rails Internationalization guide # available at https://guides.rubyonrails.org/i18n.html. en: hello: "Hello world" ================================================ FILE: test/dummy/config/mysql_database.yml ================================================ development: prepared_statements: false encoding: utf8mb4 socket: /tmp/mysql.sock adapter: mysql2 database: petri_flow_dev username: root min_messages: warning pool: 5 timeout: 5000 checkout_timeout: <%= ENV['CHECKOUT_TIMEOUT'] || 5 %> test: prepared_statements: false encoding: utf8mb4 socket: /tmp/mysql.sock adapter: mysql2 database: petri_flow_test username: root min_messages: warning pool: 5 timeout: 5000 ================================================ FILE: test/dummy/config/puma.rb ================================================ # frozen_string_literal: true # Puma can serve each request in a thread from an internal thread pool. # The `threads` method setting takes two numbers: a minimum and maximum. # Any libraries that use thread pools should be configured to match # the maximum value specified for Puma. Default is set to 5 threads for minimum # and maximum; this matches the default thread size of Active Record. # max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } threads min_threads_count, max_threads_count # Specifies the `port` that Puma will listen on to receive requests; default is 3000. # port ENV.fetch("PORT") { 3000 } # Specifies the `environment` that Puma will run in. # environment ENV.fetch("RAILS_ENV") { "development" } # Specifies the `pidfile` that Puma will use. pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } # Specifies the number of `workers` to boot in clustered mode. # Workers are forked web server processes. If using threads and workers together # the concurrency of the application would be max `threads` * `workers`. # Workers do not work on JRuby or Windows (both of which do not support # processes). # # workers ENV.fetch("WEB_CONCURRENCY") { 2 } # Use the `preload_app!` method when specifying a `workers` number. # This directive tells Puma to first boot the application and load code # before forking the application. This takes advantage of Copy On Write # process behavior so workers use less memory. # # preload_app! # Allow puma to be restarted by `rails restart` command. plugin :tmp_restart ================================================ FILE: test/dummy/config/routes.rb ================================================ # frozen_string_literal: true Rails.application.routes.draw do mount Wf::Engine => "/wf" end ================================================ FILE: test/dummy/config/spring.rb ================================================ # frozen_string_literal: true Spring.watch( ".ruby-version", ".rbenv-vars", "tmp/restart.txt", "tmp/caching-dev.txt" ) ================================================ FILE: test/dummy/config/storage.yml ================================================ test: service: Disk root: <%= Rails.root.join("tmp/storage") %> local: service: Disk root: <%= Rails.root.join("storage") %> # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) # amazon: # service: S3 # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> # region: us-east-1 # bucket: your_own_bucket # Remember not to checkin your GCS keyfile to a repository # google: # service: GCS # project: your_project # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> # bucket: your_own_bucket # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) # microsoft: # service: AzureStorage # storage_account_name: your_account_name # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> # container: your_container_name # mirror: # service: Mirror # primary: local # mirrors: [ amazon, google, microsoft ] ================================================ FILE: test/dummy/config.ru ================================================ # frozen_string_literal: true # This file is used by Rack-based servers to start the application. require_relative "config/environment" run Rails.application ================================================ FILE: test/dummy/db/migrate/20200213081814_new_form.rb ================================================ # frozen_string_literal: true class NewForm < ActiveRecord::Migration[6.0] def change create_table "field_values", force: :cascade do |t| t.bigint "form_id" t.bigint "field_id" t.text "value" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end create_table "fields", force: :cascade do |t| t.string "name" t.bigint "form_id" t.integer "position", default: 0 t.integer "field_type", default: 0 t.string "field_type_name" t.string "default_value" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["form_id"], name: "index_fields_on_form_id" end create_table "forms", force: :cascade do |t| t.string "name" t.text "description" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end create_table :entries do |t| t.string :user_id, index: true t.bigint :workitem_id, index: true t.json "payload" t.timestamps end add_index :entries, %i[workitem_id user_id], unique: true end end ================================================ FILE: test/dummy/db/migrate/20200213133942_add_form_id_in_entry1.rb ================================================ # frozen_string_literal: true class AddFormIdInEntry1 < ActiveRecord::Migration[6.0] def change add_column :entries, :form_id, :bigint, index: true end end ================================================ FILE: test/dummy/db/migrate/20200214005535_add_entry_id_for_field_values1.rb ================================================ # frozen_string_literal: true class AddEntryIdForFieldValues1 < ActiveRecord::Migration[6.0] def change add_column :field_values, :entry_id, :bigint, index: true end end ================================================ FILE: test/dummy/db/schema.rb ================================================ # 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. # # This file is the source Rails uses to define your schema when running `rails # db:schema:load`. When creating a new database, `rails db:schema:load` tends to # be faster and is potentially less error prone than running all of your # migrations from scratch. Old migrations may fail to apply correctly if those # migrations use external dependencies or application code. # # It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema.define(version: 2020_02_26_195134) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" create_table "entries", force: :cascade do |t| t.string "user_id" t.bigint "workitem_id" t.json "payload" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "form_id" t.index ["user_id"], name: "index_entries_on_user_id" t.index ["workitem_id", "user_id"], name: "index_entries_on_workitem_id_and_user_id", unique: true t.index ["workitem_id"], name: "index_entries_on_workitem_id" end create_table "field_values", force: :cascade do |t| t.bigint "form_id" t.bigint "field_id" t.text "value" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "entry_id" end create_table "fields", force: :cascade do |t| t.string "name" t.bigint "form_id" t.integer "position", default: 0 t.integer "field_type", default: 0 t.string "field_type_name" t.string "default_value" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["form_id"], name: "index_fields_on_form_id" end create_table "forms", force: :cascade do |t| t.string "name" t.text "description" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end create_table "wf_arcs", force: :cascade do |t| t.bigint "workflow_id" t.bigint "transition_id" t.bigint "place_id" t.integer "direction", default: 0, comment: "0-in, 1-out" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.integer "guards_count", default: 0 end create_table "wf_case_assignments", comment: "Manual per-case assignments of transition to parties", force: :cascade do |t| t.bigint "case_id" t.bigint "transition_id" t.bigint "party_id" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["case_id", "transition_id", "party_id"], name: "wf_ctp_u", unique: true end create_table "wf_cases", force: :cascade do |t| t.bigint "workflow_id" t.string "targetable_type", comment: "point to target type of Application." t.string "targetable_id", comment: "point to target ID of Application." t.integer "state", default: 0, comment: "0-created, 1-active, 2-suspended, 3-canceled, 4-finished" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "started_by_workitem_id", comment: "As a sub workflow instance, it is started by one workitem." end create_table "wf_comments", force: :cascade do |t| t.bigint "workitem_id" t.string "user_id" t.text "body" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["user_id"], name: "index_wf_comments_on_user_id" t.index ["workitem_id"], name: "index_wf_comments_on_workitem_id" end create_table "wf_demo_targets", comment: "For demo, useless.", force: :cascade do |t| t.string "name" t.string "description" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end create_table "wf_entries", comment: "user input data for workitem with form.", force: :cascade do |t| t.string "user_id" t.bigint "workitem_id" t.json "payload", default: {} t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "form_id" t.index ["user_id"], name: "index_wf_entries_on_user_id" t.index ["workitem_id", "user_id"], name: "index_wf_entries_on_workitem_id_and_user_id", unique: true t.index ["workitem_id"], name: "index_wf_entries_on_workitem_id" end create_table "wf_field_values", force: :cascade do |t| t.bigint "form_id" t.bigint "field_id" t.text "value" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "entry_id" t.index ["field_id"], name: "index_wf_field_values_on_field_id" t.index ["form_id"], name: "index_wf_field_values_on_form_id" end create_table "wf_fields", force: :cascade do |t| t.string "name" t.bigint "form_id" t.integer "position", default: 0 t.integer "field_type", default: 0 t.string "field_type_name" t.string "default_value" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["form_id"], name: "index_wf_fields_on_form_id" end create_table "wf_forms", force: :cascade do |t| t.string "name" t.text "description" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end create_table "wf_groups", comment: "For demo", force: :cascade do |t| t.string "name" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end create_table "wf_guards", force: :cascade do |t| t.bigint "arc_id" t.bigint "workflow_id" t.string "fieldable_type" t.string "fieldable_id" t.string "op" t.string "value" t.string "exp" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["arc_id"], name: "index_wf_guards_on_arc_id" t.index ["workflow_id"], name: "index_wf_guards_on_workflow_id" end create_table "wf_parties", comment: "for groups or roles or users or positions etc.", force: :cascade do |t| t.string "partable_type" t.string "partable_id" t.string "party_name" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["partable_type", "partable_id"], name: "index_wf_parties_on_partable_type_and_partable_id", unique: true end create_table "wf_places", force: :cascade do |t| t.bigint "workflow_id" t.string "name" t.text "description" t.integer "sort_order", default: 0 t.integer "place_type", default: 0, comment: "类型:0-start,1-normal,2-end" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end create_table "wf_tokens", force: :cascade do |t| t.bigint "workflow_id" t.bigint "case_id" t.string "targetable_type" t.string "targetable_id" t.bigint "place_id" t.integer "state", default: 0, comment: "0-free, 1-locked, 2-canceled, 3-consumed" t.bigint "locked_workitem_id" t.datetime "produced_at", default: -> { "timezone('utc'::text, now())" } t.datetime "locked_at" t.datetime "canceled_at" t.datetime "consumed_at" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end create_table "wf_transition_static_assignments", comment: "pre assignment for transition", force: :cascade do |t| t.bigint "party_id" t.bigint "transition_id" t.bigint "workflow_id" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["transition_id", "party_id"], name: "wf_tp_u", unique: true end create_table "wf_transitions", force: :cascade do |t| t.string "name" t.text "description" t.bigint "workflow_id" t.integer "sort_order", default: 0 t.integer "trigger_limit", comment: "use with timed trigger, after x minitues, trigger exec" t.integer "trigger_type", default: 0, comment: "0-user,1-automatic, 2-message,3-time" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "form_id" t.string "enable_callback", default: "Wf::Callbacks::EnableDefault" t.string "fire_callback", default: "Wf::Callbacks::FireDefault" t.string "notification_callback", default: "Wf::Callbacks::NotificationDefault" t.string "time_callback", default: "Wf::Callbacks::TimeDefault" t.string "deadline_callback", default: "Wf::Callbacks::DeadlineDefault" t.string "hold_timeout_callback", default: "Wf::Callbacks::HoldTimeoutDefault" t.string "assignment_callback", default: "Wf::Callbacks::AssignmentDefault" t.string "unassignment_callback", default: "Wf::Callbacks::UnassignmentDefault" t.string "form_type", default: "Wf::Form" t.bigint "sub_workflow_id" t.boolean "multiple_instance", default: false, comment: "multiple instance mode or not" t.string "finish_condition", default: "Wf::MultipleInstances::AllFinish", comment: "set finish condition for parent workitem." t.bigint "dynamic_assign_by_id", comment: "dynamic assign by other transition" t.index ["form_type", "form_id"], name: "index_wf_transitions_on_form_type_and_form_id" end create_table "wf_users", comment: "For demo", force: :cascade do |t| t.string "name" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "group_id" end create_table "wf_workflows", force: :cascade do |t| t.string "name" t.text "description" t.boolean "is_valid", default: false t.string "creator_id" t.text "error_msg" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end create_table "wf_workitem_assignments", force: :cascade do |t| t.bigint "party_id" t.bigint "workitem_id" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["workitem_id", "party_id"], name: "wf_wp_u", unique: true end create_table "wf_workitems", force: :cascade do |t| t.bigint "case_id" t.bigint "workflow_id" t.bigint "transition_id" t.integer "state", default: 0, comment: "0-enabled, 1-started, 2-canceled, 3-finished,4-overridden" t.datetime "enabled_at", default: -> { "timezone('utc'::text, now())" } t.datetime "started_at" t.datetime "canceled_at" t.datetime "finished_at" t.datetime "overridden_at" t.datetime "deadline" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.datetime "trigger_time", comment: "set when transition_trigger=TIME & trigger_limit present" t.string "holding_user_id", comment: "id of App user" t.integer "children_count", default: 0 t.integer "children_finished_count", default: 0 t.boolean "forked", default: false t.bigint "parent_id", comment: "parent workitem id" t.index ["state", "trigger_time"], name: "index_wf_workitems_on_state_and_trigger_time" end end ================================================ FILE: test/dummy/db/seeds.rb ================================================ # frozen_string_literal: true Wf::Workflow.destroy_all Wf::Case.destroy_all Wf::Transition.destroy_all Wf::Place.destroy_all Wf::Arc.destroy_all Wf::Form.destroy_all form = Wf.form_class.constantize.create(name: "From One") name_field = form.fields.create!(name: :name, field_type: :string) age_field = form.fields.create!(name: :age, field_type: :integer) form2 = Wf.form_class.constantize.create(name: "From Two") score_field = form2.fields.create!(name: :score, field_type: :integer) proc do seq = Wf::Workflow.create(name: "Seq Workflow") s = seq.places.create!(place_type: :start, name: "start") e = seq.places.create!(place_type: :end, name: "end") p = seq.places.create!(place_type: :normal, name: "p") t1 = seq.transitions.create!(name: "t1") t2 = seq.transitions.create!(name: "t2") arc1 = seq.arcs.create!(direction: :in, transition: t1, place: s) arc2 = seq.arcs.create!(direction: :out, transition: t1, place: p) arc3 = seq.arcs.create!(direction: :in, transition: t2, place: p) arc4 = seq.arcs.create!(direction: :out, transition: t2, place: e) end.call proc do seq = Wf::Workflow.create(name: "Workflow with automatic transition") s = seq.places.create!(place_type: :start, name: "start") e = seq.places.create!(place_type: :end, name: "end") p = seq.places.create!(place_type: :normal, name: "p") t1 = seq.transitions.create!(name: "t1") t2 = seq.transitions.create!(name: "t2", trigger_type: :automatic) arc1 = seq.arcs.create!(direction: :in, transition: t1, place: s) arc2 = seq.arcs.create!(direction: :out, transition: t1, place: p) arc3 = seq.arcs.create!(direction: :in, transition: t2, place: p) arc4 = seq.arcs.create!(direction: :out, transition: t2, place: e) end.call proc do seq = Wf::Workflow.create(name: "Workflow with timed transition") s = seq.places.create!(place_type: :start, name: "start") e = seq.places.create!(place_type: :end, name: "end") p = seq.places.create!(place_type: :normal, name: "p") t1 = seq.transitions.create!(name: "t1") t2 = seq.transitions.create!(name: "t2", trigger_type: :time, trigger_limit: 1) arc1 = seq.arcs.create!(direction: :in, transition: t1, place: s) arc2 = seq.arcs.create!(direction: :out, transition: t1, place: p) arc3 = seq.arcs.create!(direction: :in, transition: t2, place: p) arc4 = seq.arcs.create!(direction: :out, transition: t2, place: e) end.call proc do seq = Wf::Workflow.create(name: "Workflow with timed split") s = seq.places.create!(place_type: :start, name: "start") e = seq.places.create!(place_type: :end, name: "end") p = seq.places.create!(place_type: :normal, name: "p") t1 = seq.transitions.create!(name: "t1") t2 = seq.transitions.create!(name: "t2") t3 = seq.transitions.create!(name: "t3", trigger_type: :time, trigger_limit: 1) arc1 = seq.arcs.create!(direction: :in, transition: t1, place: s) arc2 = seq.arcs.create!(direction: :in, transition: t3, place: s) arc3 = seq.arcs.create!(direction: :out, transition: t1, place: p) arc4 = seq.arcs.create!(direction: :in, transition: t2, place: p) arc5 = seq.arcs.create!(direction: :out, transition: t2, place: e) arc6 = seq.arcs.create!(direction: :out, transition: t3, place: e) end.call proc do seq = Wf::Workflow.create(name: "Workflow with guard") s = seq.places.create!(place_type: :start, name: "start") e = seq.places.create!(place_type: :end, name: "end") p1 = seq.places.create!(place_type: :normal, name: "p1") p2 = seq.places.create!(place_type: :normal, name: "p2") t1 = seq.transitions.create!(name: "t1", form: form) t2 = seq.transitions.create!(name: "t2") t3 = seq.transitions.create!(name: "t3") arc2 = seq.arcs.create!(direction: :in, transition: t1, place: s) arc3 = seq.arcs.create!(direction: :out, transition: t1, place: p1) arc3 = seq.arcs.create!(direction: :out, transition: t1, place: p2) arc4 = seq.arcs.create!(direction: :in, transition: t2, place: p1) arc5 = seq.arcs.create!(direction: :in, transition: t3, place: p2) arc6 = seq.arcs.create!(direction: :out, transition: t2, place: e) arc7 = seq.arcs.create!(direction: :out, transition: t3, place: e) arc3.guards.create!(fieldable: age_field, op: ">".to_sym, value: 18) end.call proc do seq = Wf::Workflow.create(name: "Workflow with parallel routing") s = seq.places.create!(place_type: :start, name: "start") e = seq.places.create!(place_type: :end, name: "end") p1 = seq.places.create!(place_type: :normal, name: "p1") p2 = seq.places.create!(place_type: :normal, name: "p2") p3 = seq.places.create!(place_type: :normal, name: "p3") p4 = seq.places.create!(place_type: :normal, name: "p4") t1 = seq.transitions.create!(name: "t1", form: form) t2 = seq.transitions.create!(name: "t2") t3 = seq.transitions.create!(name: "t3") t4 = seq.transitions.create!(name: "t4") arc1 = seq.arcs.create!(direction: :in, transition: t1, place: s) arc2 = seq.arcs.create!(direction: :out, transition: t1, place: p1) arc3 = seq.arcs.create!(direction: :out, transition: t1, place: p2) arc4 = seq.arcs.create!(direction: :in, transition: t2, place: p1) arc5 = seq.arcs.create!(direction: :in, transition: t3, place: p2) arc6 = seq.arcs.create!(direction: :out, transition: t2, place: p3) arc7 = seq.arcs.create!(direction: :out, transition: t3, place: p4) arc8 = seq.arcs.create!(direction: :in, transition: t4, place: p3) arc9 = seq.arcs.create!(direction: :in, transition: t4, place: p4) arc10 = seq.arcs.create!(direction: :out, transition: t4, place: e) end.call proc do seq = Wf::Workflow.create(name: "Workflow with iterative routing") s = seq.places.create!(place_type: :start, name: "start") e = seq.places.create!(place_type: :end, name: "end") p = seq.places.create!(place_type: :normal, name: "p") t1 = seq.transitions.create!(name: "t1", form: form) t2 = seq.transitions.create!(name: "t2") arc1 = seq.arcs.create!(direction: :in, transition: t1, place: s) arc2 = seq.arcs.create!(direction: :out, transition: t1, place: p) arc3 = seq.arcs.create!(direction: :in, transition: t2, place: p) arc4 = seq.arcs.create!(direction: :out, transition: t2, place: e) arc5 = seq.arcs.create!(direction: :out, transition: t1, place: s) arc5.guards.create!(fieldable: age_field, op: ">".to_sym, value: 18) end.call proc do seq = Wf::Workflow.create(name: "Workflow with expression guard") s = seq.places.create!(place_type: :start, name: "start") e = seq.places.create!(place_type: :end, name: "end") p1 = seq.places.create!(place_type: :normal, name: "p1") p2 = seq.places.create!(place_type: :normal, name: "p2") t1 = seq.transitions.create!(name: "t1", form: form) t2 = seq.transitions.create!(name: "t2") t3 = seq.transitions.create!(name: "t3") arc2 = seq.arcs.create!(direction: :in, transition: t1, place: s) arc3 = seq.arcs.create!(direction: :out, transition: t1, place: p1) arc3 = seq.arcs.create!(direction: :out, transition: t1, place: p2) arc4 = seq.arcs.create!(direction: :in, transition: t2, place: p1) arc5 = seq.arcs.create!(direction: :in, transition: t3, place: p2) arc6 = seq.arcs.create!(direction: :out, transition: t2, place: e) arc7 = seq.arcs.create!(direction: :out, transition: t3, place: e) exp = <<~JS let age_great_than_18 = function(){ if (workitem.form.age > 18) { return "Yes" } else { return "No" } }; age_great_than_18(); JS arc3.guards.create!(exp: exp, op: "=".to_sym, value: "Yes") end.call proc do sub_workflow = Wf::Workflow.where(name: "Seq Workflow").first seq = Wf::Workflow.create(name: "Workflow with sub workflow") s = seq.places.create!(place_type: :start, name: "start") e = seq.places.create!(place_type: :end, name: "end") p = seq.places.create!(place_type: :normal, name: "p") t1 = seq.transitions.create!(name: "t1", sub_workflow: sub_workflow, trigger_type: :message) t2 = seq.transitions.create!(name: "t2") arc1 = seq.arcs.create!(direction: :in, transition: t1, place: s) arc2 = seq.arcs.create!(direction: :out, transition: t1, place: p) arc3 = seq.arcs.create!(direction: :in, transition: t2, place: p) arc4 = seq.arcs.create!(direction: :out, transition: t2, place: e) end.call proc do seq = Wf::Workflow.create(name: "Workflow with multiple instances") s = seq.places.create!(place_type: :start, name: "start") e = seq.places.create!(place_type: :end, name: "end") p = seq.places.create!(place_type: :normal, name: "p") t1 = seq.transitions.create!(name: "t1", trigger_type: :user, multiple_instance: true, form: form2) t2 = seq.transitions.create!(name: "t2") arc1 = seq.arcs.create!(direction: :in, transition: t1, place: s) arc2 = seq.arcs.create!(direction: :out, transition: t1, place: p) arc3 = seq.arcs.create!(direction: :in, transition: t2, place: p) arc4 = seq.arcs.create!(direction: :out, transition: t2, place: e) Wf::User.all.sample(3).each do |user| t1.transition_static_assignments.create!(party: user.party) end end.call proc do seq = Wf::Workflow.create(name: "Workflow with AssigmentCallback") s = seq.places.create!(place_type: :start, name: "start") e = seq.places.create!(place_type: :end, name: "end") p = seq.places.create!(place_type: :normal, name: "p") t1 = seq.transitions.create!(name: "t1", assignment_callback: "MyAssignmentCallback") t2 = seq.transitions.create!(name: "t2") arc1 = seq.arcs.create!(direction: :in, transition: t1, place: s) arc2 = seq.arcs.create!(direction: :out, transition: t1, place: p) arc3 = seq.arcs.create!(direction: :in, transition: t2, place: p) arc4 = seq.arcs.create!(direction: :out, transition: t2, place: e) end.call ================================================ FILE: test/dummy/lib/assets/.keep ================================================ ================================================ FILE: test/dummy/log/.keep ================================================ ================================================ FILE: test/dummy/public/404.html ================================================ 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.

================================================ FILE: test/dummy/public/422.html ================================================ 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.

================================================ FILE: test/dummy/public/500.html ================================================ 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.

================================================ FILE: test/dummy/storage/.keep ================================================ ================================================ FILE: test/fixtures/wf/case_assignments.yml ================================================ # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html # This model initially had no columns defined. If you add columns to the # model remove the '{}' from the fixture names and add the columns immediately # below each fixture, per the syntax in the comments below # one: {} # column: value # two: {} # column: value ================================================ FILE: test/fixtures/wf/comments.yml ================================================ # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html # This model initially had no columns defined. If you add columns to the # model remove the '{}' from the fixture names and add the columns immediately # below each fixture, per the syntax in the comments below # one: {} # column: value # two: {} # column: value ================================================ FILE: test/fixtures/wf/demo_targets.yml ================================================ # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html # This model initially had no columns defined. If you add columns to the # model remove the '{}' from the fixture names and add the columns immediately # below each fixture, per the syntax in the comments below # one: {} # column: value # two: {} # column: value ================================================ FILE: test/fixtures/wf/entries.yml ================================================ # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html # This model initially had no columns defined. If you add columns to the # model remove the '{}' from the fixture names and add the columns immediately # below each fixture, per the syntax in the comments below # one: {} # column: value # two: {} # column: value ================================================ FILE: test/fixtures/wf/field_values.yml ================================================ # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html # This model initially had no columns defined. If you add columns to the # model remove the '{}' from the fixture names and add the columns immediately # below each fixture, per the syntax in the comments below # one: {} # column: value # two: {} # column: value ================================================ FILE: test/fixtures/wf/fields.yml ================================================ # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html # This model initially had no columns defined. If you add columns to the # model remove the '{}' from the fixture names and add the columns immediately # below each fixture, per the syntax in the comments below # one: {} # column: value # two: {} # column: value ================================================ FILE: test/fixtures/wf/forms.yml ================================================ # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html # This model initially had no columns defined. If you add columns to the # model remove the '{}' from the fixture names and add the columns immediately # below each fixture, per the syntax in the comments below # one: {} # column: value # two: {} # column: value ================================================ FILE: test/fixtures/wf/guards.yml ================================================ # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html # This model initially had no columns defined. If you add columns to the # model remove the '{}' from the fixture names and add the columns immediately # below each fixture, per the syntax in the comments below # one: {} # column: value # two: {} # column: value ================================================ FILE: test/fixtures/wf/parties.yml ================================================ # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html # This model initially had no columns defined. If you add columns to the # model remove the '{}' from the fixture names and add the columns immediately # below each fixture, per the syntax in the comments below # one: {} # column: value # two: {} # column: value ================================================ FILE: test/fixtures/wf/transition_static_assignments.yml ================================================ # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html # This model initially had no columns defined. If you add columns to the # model remove the '{}' from the fixture names and add the columns immediately # below each fixture, per the syntax in the comments below # one: {} # column: value # two: {} # column: value ================================================ FILE: test/fixtures/wf/users.yml ================================================ # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html # This model initially had no columns defined. If you add columns to the # model remove the '{}' from the fixture names and add the columns immediately # below each fixture, per the syntax in the comments below # one: {} # column: value # two: {} # column: value ================================================ FILE: test/fixtures/wf/workitem_assignments.yml ================================================ # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html # This model initially had no columns defined. If you add columns to the # model remove the '{}' from the fixture names and add the columns immediately # below each fixture, per the syntax in the comments below # one: {} # column: value # two: {} # column: value ================================================ FILE: test/integration/navigation_test.rb ================================================ # frozen_string_literal: true require "test_helper" class NavigationTest < ActionDispatch::IntegrationTest # test "the truth" do # assert true # end end ================================================ FILE: test/models/wf/case_assignment_test.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_case_assignments # # id :integer not null, primary key # case_id :integer # transition_id :integer # party_id :integer # created_at :datetime not null # updated_at :datetime not null # # frozen_string_literal: true require "test_helper" module Wf class CaseAssignmentTest < ActiveSupport::TestCase # test "the truth" do # assert true # end end end ================================================ FILE: test/models/wf/comment_test.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_comments # # id :integer not null, primary key # workitem_id :integer # user_id :string # body :text # created_at :datetime not null # updated_at :datetime not null # # frozen_string_literal: true require "test_helper" module Wf class CommentTest < ActiveSupport::TestCase # test "the truth" do # assert true # end end end ================================================ FILE: test/models/wf/demo_target_test.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_demo_targets # # id :integer not null, primary key # name :string # description :string # created_at :datetime not null # updated_at :datetime not null # require "test_helper" module Wf class DemoTargetTest < ActiveSupport::TestCase # test "the truth" do # assert true # end end end ================================================ FILE: test/models/wf/entry_test.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_entries # # id :integer not null, primary key # user_id :string # workitem_id :integer # payload :json default("{}") # created_at :datetime not null # updated_at :datetime not null # form_id :integer # require "test_helper" module Wf class EntryTest < ActiveSupport::TestCase # test "the truth" do # assert true # end end end ================================================ FILE: test/models/wf/field_test.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_fields # # id :integer not null, primary key # name :string # form_id :integer # position :integer default("0") # field_type :integer default("0") # field_type_name :string # default_value :string # created_at :datetime not null # updated_at :datetime not null # require "test_helper" module Wf class FieldTest < ActiveSupport::TestCase # test "the truth" do # assert true # end end end ================================================ FILE: test/models/wf/field_value_test.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_field_values # # id :integer not null, primary key # form_id :integer # field_id :integer # value :text # created_at :datetime not null # updated_at :datetime not null # entry_id :integer # require "test_helper" module Wf class FieldValueTest < ActiveSupport::TestCase # test "the truth" do # assert true # end end end ================================================ FILE: test/models/wf/form_test.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_forms # # id :integer not null, primary key # name :string # description :text # created_at :datetime not null # updated_at :datetime not null # require "test_helper" module Wf class FormTest < ActiveSupport::TestCase test "the truth" do assert true end end end ================================================ FILE: test/models/wf/guard_test.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_guards # # id :integer not null, primary key # arc_id :integer # workflow_id :integer # fieldable_type :string # fieldable_id :string # op :string # value :string # exp :string # created_at :datetime not null # updated_at :datetime not null # require "test_helper" module Wf class GuardTest < ActiveSupport::TestCase # test "the truth" do # assert true # end end end ================================================ FILE: test/models/wf/party_test.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_parties # # id :integer not null, primary key # partable_type :string # partable_id :string # party_name :string # created_at :datetime not null # updated_at :datetime not null # require "test_helper" module Wf class PartyTest < ActiveSupport::TestCase # test "the truth" do # assert true # end end end ================================================ FILE: test/models/wf/transition_static_assignment_test.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_transition_static_assignments # # id :integer not null, primary key # party_id :integer # transition_id :integer # workflow_id :integer # created_at :datetime not null # updated_at :datetime not null # require "test_helper" module Wf class TransitionStaticAssignmentTest < ActiveSupport::TestCase # test "the truth" do # assert true # end end end ================================================ FILE: test/models/wf/user_test.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_users # # id :integer not null, primary key # name :string # created_at :datetime not null # updated_at :datetime not null # group_id :integer # require "test_helper" module Wf class UserTest < ActiveSupport::TestCase # test "the truth" do # assert true # end end end ================================================ FILE: test/models/wf/wf_test.rb ================================================ # frozen_string_literal: true require "test_helper" module Wf class WfTest < ActiveSupport::TestCase # test "the truth" do # assert true # end end end ================================================ FILE: test/models/wf/workitem_assignment_test.rb ================================================ # frozen_string_literal: true # == Schema Information # # Table name: wf_workitem_assignments # # id :integer not null, primary key # party_id :integer # workitem_id :integer # created_at :datetime not null # updated_at :datetime not null # require "test_helper" module Wf class WorkitemAssignmentTest < ActiveSupport::TestCase # test "the truth" do # assert true # end end end ================================================ FILE: test/test_helper.rb ================================================ # frozen_string_literal: true # Configure Rails Environment ENV["RAILS_ENV"] = "test" require_relative "../test/dummy/config/environment" ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)] ActiveRecord::Migrator.migrations_paths << File.expand_path("../db/migrate", __dir__) require "rails/test_help" # Filter out the backtrace from minitest while preserving the one from other libraries. Minitest.backtrace_filter = Minitest::BacktraceFilter.new # Load fixtures from the engine if ActiveSupport::TestCase.respond_to?(:fixture_path=) ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__) ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "/files" ActiveSupport::TestCase.fixtures :all end ================================================ FILE: wf.gemspec ================================================ # frozen_string_literal: true $LOAD_PATH.push File.expand_path("lib", __dir__) # Maintain your gem's version: require "wf/version" # Describe your gem and declare its dependencies: Gem::Specification.new do |spec| spec.name = "petri_flow" spec.version = Wf::VERSION spec.authors = ["Hooopo Wang"] spec.email = ["hoooopo@gmail.com"] spec.homepage = "https://github.com/hooopo/petri_flow" spec.summary = "Petri Net Workflow Engine for Ruby." spec.description = "Petri Net Workflow Engine for Ruby." spec.license = "MIT" spec.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] spec.add_dependency "bootstrap", "~> 4.4.1" spec.add_dependency "bootstrap4-kaminari-views" spec.add_dependency "jquery-rails" spec.add_dependency "kaminari" spec.add_dependency "loaf" spec.add_dependency "mini_racer" spec.add_dependency "pg" spec.add_dependency "rails" spec.add_dependency "rgl" spec.add_dependency "ruby-graphviz" spec.add_dependency "select2-rails-2020" spec.add_dependency "simple_command" end