├── .gitignore ├── config ├── locales │ └── en.yml └── routes.rb ├── Gemfile ├── lib └── workflow_enhancements │ ├── hooks.rb │ ├── patches │ ├── tracker_patch.rb │ └── action_view_rendering.rb │ └── graph.rb ├── app ├── views │ ├── trackers │ │ ├── edit.html.erb │ │ └── _form.html.erb │ ├── workflow_enhancements │ │ ├── show.html.erb │ │ ├── _issue_popup.html.erb │ │ └── _workflow_graph.html.erb │ └── workflows │ │ └── edit.html.erb ├── controllers │ └── workflow_enhancements_controller.rb └── models │ └── tracker_status.rb ├── test ├── fixtures │ ├── trackers.yml │ ├── issue_categories.yml │ ├── issue_statuses.yml │ ├── member_roles.yml │ ├── projects_trackers.yml │ ├── members.yml │ ├── projects.yml │ ├── enabled_modules.yml │ ├── enumerations.yml │ ├── workflows.yml │ ├── users.yml │ ├── roles.yml │ └── issues.yml ├── unit │ ├── tracker_status_test.rb │ ├── tracker_test.rb │ └── graph_test.rb ├── functional │ ├── workflows_controller_test.rb │ ├── workflow_enhancements_controller_test.rb │ ├── issues_controller_test.rb │ └── trackers_controller_test.rb └── test_helper.rb ├── db └── migrate │ └── 001_create_tracker_statuses.rb ├── COPYRIGHT ├── init.rb ├── CHANGELOG.md ├── assets ├── stylesheets │ └── workflow_enhancements.css └── javascripts │ ├── saveSvgAsPng.js │ └── dagre-d3.min.js ├── README.md └── COPYING /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage/ 2 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # English strings go here for Rails i18n 2 | en: 3 | permission_workflow_graph_view: "View workflow graph" 4 | # my_label: "My label" 5 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | match '/projects/:project_id/workflow_enhancements/:tracker_id/:issue_id', 2 | :to => 'workflow_enhancements#show', :via => :get, 3 | :as => 'workflow_show_graph' 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | group :test do 2 | if !dependencies.any? { |d| d.name == "simplecov" } 3 | gem "simplecov", :require => false 4 | end 5 | if !dependencies.any? { |d| d.name == "shoulda" } 6 | gem "shoulda" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/workflow_enhancements/hooks.rb: -------------------------------------------------------------------------------- 1 | module WorkflowEnhancements 2 | class Hooks < Redmine::Hook::ViewListener 3 | render_on :view_issues_form_details_bottom, 4 | :partial => 'workflow_enhancements/issue_popup' 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/views/trackers/edit.html.erb: -------------------------------------------------------------------------------- 1 | <%= render :super %> 2 | 3 |
4 | <%= render :partial => 'workflow_enhancements/workflow_graph', :locals => { 5 | :trackers => @tracker, :roles => nil } %> 6 |
7 | -------------------------------------------------------------------------------- /app/views/workflow_enhancements/show.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= render :partial => 'workflow_enhancements/workflow_graph', :locals => { 3 | :trackers => @tracker, :roles => @roles, :issue => @issue, :minimal => true} %> 4 |
5 | -------------------------------------------------------------------------------- /test/fixtures/trackers.yml: -------------------------------------------------------------------------------- 1 | # This is copied from Redmine 2.5 and modified 2 | --- 3 | trackers_001: 4 | name: Bug 5 | id: 1 6 | is_in_chlog: true 7 | default_status_id: 1 8 | position: 1 9 | trackers_002: 10 | name: Feature request 11 | id: 2 12 | is_in_chlog: true 13 | default_status_id: 1 14 | position: 2 15 | -------------------------------------------------------------------------------- /db/migrate/001_create_tracker_statuses.rb: -------------------------------------------------------------------------------- 1 | class CreateTrackerStatuses < ActiveRecord::Migration 2 | def change 3 | create_table :tracker_statuses do |t| 4 | t.integer :tracker_id, :null => false 5 | t.integer :issue_status_id, :null => false 6 | end 7 | 8 | add_index :tracker_statuses, :tracker_id 9 | add_index :tracker_statuses, :issue_status_id 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/controllers/workflow_enhancements_controller.rb: -------------------------------------------------------------------------------- 1 | class WorkflowEnhancementsController < ApplicationController 2 | before_filter :find_project_by_project_id #, :authorize 3 | 4 | def show 5 | @roles = User.current.roles_for_project(@project) 6 | @tracker = Tracker.find(params[:tracker_id]) 7 | issue_id = params[:issue_id] 8 | if issue_id 9 | @issue = Issue.find_by_id(issue_id) 10 | end 11 | render :layout => false 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/models/tracker_status.rb: -------------------------------------------------------------------------------- 1 | class TrackerStatus < ActiveRecord::Base 2 | unloadable 3 | 4 | attr_accessible :tracker_id, :issue_status_id 5 | 6 | belongs_to :tracker 7 | belongs_to :predef_issue_status, :class_name => 'IssueStatus', :foreign_key => 'issue_status_id' 8 | 9 | validates :tracker, :presence => true 10 | validates :issue_status_id, :presence => true 11 | validates :issue_status_id, :uniqueness => { :scope => :tracker_id } 12 | end 13 | -------------------------------------------------------------------------------- /test/fixtures/issue_categories.yml: -------------------------------------------------------------------------------- 1 | --- 2 | issue_categories_001: 3 | name: Printing 4 | project_id: 1 5 | assigned_to_id: 2 6 | id: 1 7 | issue_categories_002: 8 | name: Recipes 9 | project_id: 1 10 | assigned_to_id: 11 | id: 2 12 | issue_categories_003: 13 | name: Stock management 14 | project_id: 2 15 | assigned_to_id: 16 | id: 3 17 | issue_categories_004: 18 | name: Printing 19 | project_id: 2 20 | assigned_to_id: 21 | id: 4 22 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014-2015 Daniel Ritz 2 | 3 | This software is licensed under the terms of the GNU General Public License 4 | (GPL) v2. See COPYING for details. 5 | 6 | =============================================================================== 7 | 8 | This Software contains third party components as follows: 9 | 10 | d3.js 11 | ----- 12 | 13 | BSD license. 14 | http://d3js.org/ 15 | 16 | 17 | 18 | dagre-d3 19 | -------- 20 | 21 | MIT License. 22 | https://github.com/cpettitt/dagre-d3 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/fixtures/issue_statuses.yml: -------------------------------------------------------------------------------- 1 | --- 2 | issue_statuses_001: 3 | id: 1 4 | name: New 5 | is_closed: false 6 | position: 1 7 | issue_statuses_002: 8 | id: 2 9 | name: Assigned 10 | is_closed: false 11 | position: 2 12 | issue_statuses_003: 13 | id: 3 14 | name: Resolved 15 | is_closed: false 16 | position: 3 17 | issue_statuses_004: 18 | name: Feedback 19 | id: 4 20 | is_closed: false 21 | position: 4 22 | issue_statuses_005: 23 | id: 5 24 | name: Closed 25 | is_closed: true 26 | position: 5 27 | issue_statuses_006: 28 | id: 6 29 | name: Rejected 30 | is_closed: true 31 | position: 6 32 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'workflow_enhancements/hooks' 2 | require_dependency 'workflow_enhancements/patches/action_view_rendering' 3 | require_dependency 'workflow_enhancements/patches/tracker_patch' 4 | 5 | Redmine::Plugin.register :redmine_workflow_enhancements do 6 | name 'Redmine Workflow Enhancements' 7 | author 'Daniel Ritz' 8 | description 'Enhancements for Workflow' 9 | version '0.5.0' 10 | url 'https://github.com/dr-itz/redmine_workflow_enhancements' 11 | author_url 'https://github.com/dr-itz/' 12 | 13 | requires_redmine '2.2.0' 14 | 15 | project_module :issue_tracking do 16 | permission :workflow_graph_view, :workflow_enhancements => :show 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/unit/tracker_status_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../test_helper', __FILE__) 2 | 3 | class TrackerStatusTest < ActiveSupport::TestCase 4 | context "relations" do 5 | should belong_to(:tracker) 6 | should belong_to(:predef_issue_status) 7 | end 8 | 9 | context "validations" do 10 | should validate_presence_of(:tracker) 11 | should validate_presence_of(:issue_status_id) 12 | 13 | subject { TrackerStatus.create(:tracker_id => 1, :issue_status_id => 1) } 14 | should validate_uniqueness_of(:issue_status_id).scoped_to(:tracker_id) 15 | end 16 | 17 | def test_new_tracker 18 | t = Tracker.new 19 | assert_equal [], t.issue_statuses 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/views/trackers/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= render :super %> 2 | 3 | <% content_for :header_tags do -%> 4 | <%= stylesheet_link_tag "workflow_enhancements.css", :plugin => :redmine_workflow_enhancements %> 5 | <% end -%> 6 | 7 |
8 |
9 | <%= l(:label_issue_status_plural) %> 10 | 11 | <% IssueStatus.order(:name).each do |status| %> 12 | 17 | <% end %> 18 |
19 |
20 | -------------------------------------------------------------------------------- /app/views/workflows/edit.html.erb: -------------------------------------------------------------------------------- 1 | <%= render :super %> 2 | 3 | <% if @tracker && @role || @trackers && @roles %> 4 | <% content_for :header_tags do -%> 5 | <%= stylesheet_link_tag "workflow_enhancements.css", :plugin => :redmine_workflow_enhancements %> 6 | <% end -%> 7 | 8 |
9 | <% if @tracker && @role %> 10 | <%= render :partial => 'workflow_enhancements/workflow_graph', :locals => { 11 | :trackers => @tracker, :roles => @role} %> 12 | <% else %> 13 | <%= render :partial => 'workflow_enhancements/workflow_graph', :locals => { 14 | :trackers => @trackers, :roles => @roles} %> 15 | <% end %> 16 |
17 | <% end %> 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | v0.5.0 2 | * Compatible with Redmine 3.2 3 | * Add permission to control whether a role can see the workflow graph (Victor Campos) 4 | * Fix interaction with gem "haml" 5 | * Show all possible initial states a new issue can have, default is highlighted 6 | 7 | v0.4.0 8 | * Fixes for Redmine 3.x 9 | * Highlight current and allowed statuses in issue popup 10 | * Workflow popup in issue: prevent opening the popup multiple times 11 | * Fix invalid SQL 12 | * Automatically scale down graph to better fit on page 13 | 14 | v0.3.0 15 | * Alternative approach to render super template 16 | 17 | v0.2.0 18 | * Fixes for Redmine 3 pre-release 19 | * Show workflow graph in issue using pop up 20 | * Various fixes 21 | 22 | v0.1.1 23 | * Redmine compatibility fix 24 | 25 | v0.1.0 26 | * First public release 27 | -------------------------------------------------------------------------------- /test/functional/workflows_controller_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../test_helper', __FILE__) 2 | 3 | class WorkflowsControllerTest < ActionController::TestCase 4 | fixtures :roles, :trackers, :workflows, :users, :issue_statuses 5 | 6 | def setup 7 | User.current = nil 8 | @request.session[:user_id] = 1 # admin 9 | end 10 | 11 | def test_get_edit_with_role_and_tracker 12 | get :edit, :role_id => 1, :tracker_id => 1 13 | assert_response :success 14 | assert_template 'edit' 15 | 16 | assert_not_nil assigns(:statuses) 17 | assert_not_nil assigns(:roles) 18 | assert_not_nil assigns(:trackers) 19 | 20 | # test if standard Redmine stuff is there (i.e. render_super() works) 21 | assert_select '#role_id', true 22 | 23 | # test if workflow visualization is there 24 | assert_select 'svg#workflow-vis', true 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/fixtures/member_roles.yml: -------------------------------------------------------------------------------- 1 | --- 2 | member_roles_001: 3 | id: 1 4 | role_id: 1 5 | member_id: 1 6 | member_roles_002: 7 | id: 2 8 | role_id: 2 9 | member_id: 2 10 | member_roles_003: 11 | id: 3 12 | role_id: 2 13 | member_id: 3 14 | member_roles_004: 15 | id: 4 16 | role_id: 2 17 | member_id: 4 18 | member_roles_005: 19 | id: 5 20 | role_id: 1 21 | member_id: 5 22 | member_roles_006: 23 | id: 6 24 | role_id: 1 25 | member_id: 6 26 | member_roles_007: 27 | id: 7 28 | role_id: 2 29 | member_id: 6 30 | member_roles_008: 31 | id: 8 32 | role_id: 1 33 | member_id: 7 34 | inherited_from: 6 35 | member_roles_009: 36 | id: 9 37 | role_id: 2 38 | member_id: 7 39 | inherited_from: 7 40 | member_roles_010: 41 | id: 10 42 | role_id: 2 43 | member_id: 9 44 | inherited_from: 45 | member_roles_011: 46 | id: 11 47 | role_id: 2 48 | member_id: 10 49 | inherited_from: 10 50 | -------------------------------------------------------------------------------- /lib/workflow_enhancements/patches/tracker_patch.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'tracker' 2 | 3 | class Tracker 4 | has_many :tracker_statuses 5 | has_many :predef_issue_statuses, :through => :tracker_statuses 6 | 7 | def issue_statuses_with_workflow_enhancements 8 | if @issue_statuses 9 | return @issue_statuses 10 | elsif new_record? 11 | return [] 12 | end 13 | 14 | ids = WorkflowTransition.connection.select_rows( 15 | "SELECT DISTINCT old_status_id, new_status_id 16 | FROM #{WorkflowTransition.table_name} 17 | WHERE tracker_id = #{id} AND type = 'WorkflowTransition'").flatten 18 | ids.concat TrackerStatus.connection.select_rows( 19 | "SELECT issue_status_id 20 | FROM #{TrackerStatus.table_name} 21 | WHERE tracker_id = #{id}") 22 | 23 | ids = ids.flatten.uniq 24 | @issue_statuses = IssueStatus.where(:id => ids).all.sort 25 | end 26 | 27 | alias_method_chain :issue_statuses, :workflow_enhancements 28 | end 29 | 30 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | 3 | if Dir.pwd.match(/plugins\/redmine_workflow_enhancements/) 4 | covdir = 'coverage' 5 | else 6 | covdir = 'plugins/redmine_workflow_enhancements/coverage' 7 | end 8 | 9 | SimpleCov.coverage_dir(covdir) 10 | SimpleCov.start 'rails' do 11 | add_filter do |source_file| 12 | # only show files belonging to the plugin, except init.rb which is not fully testable 13 | source_file.filename.match(/redmine_workflow_enhancements/) == nil || 14 | source_file.filename.match(/redmine_workflow_enhancements\/init.rb/) != nil 15 | end 16 | end 17 | 18 | # Load the Redmine helper 19 | require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper') 20 | 21 | # Ensure that we are using the plugin's fixtures 22 | # This is necessary as the Redmine fixtures are too complex and don't cover all needs 23 | ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__) 24 | ActionDispatch::IntegrationTest.fixture_path = File.expand_path("../fixtures", __FILE__) 25 | -------------------------------------------------------------------------------- /test/fixtures/projects_trackers.yml: -------------------------------------------------------------------------------- 1 | --- 2 | projects_trackers_001: 3 | project_id: 4 4 | tracker_id: 3 5 | projects_trackers_002: 6 | project_id: 1 7 | tracker_id: 1 8 | projects_trackers_003: 9 | project_id: 5 10 | tracker_id: 1 11 | projects_trackers_004: 12 | project_id: 1 13 | tracker_id: 2 14 | projects_trackers_005: 15 | project_id: 5 16 | tracker_id: 2 17 | projects_trackers_006: 18 | project_id: 5 19 | tracker_id: 3 20 | projects_trackers_007: 21 | project_id: 2 22 | tracker_id: 1 23 | projects_trackers_008: 24 | project_id: 2 25 | tracker_id: 2 26 | projects_trackers_009: 27 | project_id: 2 28 | tracker_id: 3 29 | projects_trackers_010: 30 | project_id: 3 31 | tracker_id: 2 32 | projects_trackers_011: 33 | project_id: 3 34 | tracker_id: 3 35 | projects_trackers_012: 36 | project_id: 4 37 | tracker_id: 1 38 | projects_trackers_013: 39 | project_id: 4 40 | tracker_id: 2 41 | projects_trackers_014: 42 | project_id: 1 43 | tracker_id: 3 44 | projects_trackers_015: 45 | project_id: 6 46 | tracker_id: 1 47 | -------------------------------------------------------------------------------- /test/functional/workflow_enhancements_controller_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../test_helper', __FILE__) 2 | 3 | class WorkflowEnhancementsControllerTest < ActionController::TestCase 4 | fixtures :projects, 5 | :users, 6 | :roles, 7 | :members, 8 | :member_roles, 9 | :issues, 10 | :issue_statuses, 11 | :issue_categories, 12 | :trackers, 13 | :projects_trackers, 14 | :enabled_modules, 15 | :enumerations, 16 | :workflows 17 | 18 | def setup 19 | User.current = nil 20 | @request.session[:user_id] = 2 # manager 21 | end 22 | 23 | def test_show_as_manager_newissue 24 | get :show, :project_id => 1, :tracker_id => 1, :issue_id => 1 25 | assert_response :success 26 | assert_template 'show' 27 | 28 | assert_not_nil assigns(:project) 29 | assert_not_nil assigns(:roles) 30 | assert_not_nil assigns(:tracker) 31 | 32 | # test if workflow visualization is there 33 | assert_select 'svg#workflow-vis', true 34 | assert_select 'graphContextual', false 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /assets/stylesheets/workflow_enhancements.css: -------------------------------------------------------------------------------- 1 | #predef-statuses label { 2 | white-space: nowrap; 3 | } 4 | 5 | .workflowGraphContainer { 6 | clear: both; 7 | padding-top: 15px; 8 | } 9 | 10 | .workflowGraph { 11 | background: white; 12 | border: 1px solid #E4E4E4; 13 | } 14 | 15 | text { 16 | font-weight: 300; 17 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; 18 | font-size: 14px; 19 | } 20 | 21 | .node rect { 22 | stroke-width: 2px; 23 | stroke: #333; 24 | fill: #fff; 25 | } 26 | 27 | .state-new rect, 28 | .state-new-possible rect { 29 | fill: #F2DEDE; 30 | } 31 | .state-new text { 32 | font-weight: bold; 33 | } 34 | .state-closed rect { 35 | fill: #DFF0D8; 36 | } 37 | 38 | .state-current rect { 39 | stroke-width: 4px; 40 | stroke: gray; 41 | } 42 | .state-current text { 43 | font-style: italic; 44 | } 45 | .state-possible rect { 46 | stroke-width: 4px; 47 | stroke: #628DB6; 48 | } 49 | 50 | .edgeLabel rect { 51 | fill: #fff; 52 | } 53 | 54 | .edgePath path { 55 | stroke: #333; 56 | stroke-width: 1.5px; 57 | fill: none; 58 | } 59 | 60 | .transOther path { 61 | stroke: #ccc; 62 | stroke-width: 1px; 63 | } 64 | .transOwn-assignee path { 65 | stroke: #a33; 66 | } 67 | .transOwn-author path { 68 | /*stroke: #3a3;*/ 69 | stroke-dasharray: 5,5; 70 | } 71 | -------------------------------------------------------------------------------- /app/views/workflow_enhancements/_issue_popup.html.erb: -------------------------------------------------------------------------------- 1 | <% if User.current.allowed_to?(:workflow_graph_view, @project) %> 2 | <% content_for :header_tags do -%> 3 | <%= stylesheet_link_tag "workflow_enhancements.css", :plugin => :redmine_workflow_enhancements %> 4 | <% end -%> 5 | 6 | 34 |
35 | <% end %> 36 | -------------------------------------------------------------------------------- /test/fixtures/members.yml: -------------------------------------------------------------------------------- 1 | --- 2 | members_001: 3 | created_on: 2006-07-19 19:35:33 +02:00 4 | project_id: 1 5 | id: 1 6 | user_id: 2 7 | mail_notification: true 8 | members_002: 9 | created_on: 2006-07-19 19:35:36 +02:00 10 | project_id: 1 11 | id: 2 12 | user_id: 3 13 | mail_notification: true 14 | members_003: 15 | created_on: 2006-07-19 19:35:36 +02:00 16 | project_id: 2 17 | id: 3 18 | user_id: 2 19 | mail_notification: true 20 | members_004: 21 | id: 4 22 | created_on: 2006-07-19 19:35:36 +02:00 23 | project_id: 1 24 | # Locked user 25 | user_id: 5 26 | mail_notification: true 27 | members_005: 28 | id: 5 29 | created_on: 2006-07-19 19:35:33 +02:00 30 | project_id: 5 31 | user_id: 2 32 | mail_notification: true 33 | members_006: 34 | id: 6 35 | created_on: 2006-07-19 19:35:33 +02:00 36 | project_id: 5 37 | user_id: 10 38 | mail_notification: false 39 | members_007: 40 | id: 7 41 | created_on: 2006-07-19 19:35:33 +02:00 42 | project_id: 5 43 | user_id: 8 44 | mail_notification: false 45 | members_008: 46 | created_on: 2006-07-19 19:35:33 +02:00 47 | project_id: 5 48 | id: 8 49 | user_id: 1 50 | mail_notification: true 51 | members_009: 52 | id: 9 53 | created_on: 2006-07-19 19:35:33 +02:00 54 | project_id: 2 55 | user_id: 11 56 | mail_notification: false 57 | members_010: 58 | id: 10 59 | created_on: 2006-07-19 19:35:33 +02:00 60 | project_id: 2 61 | user_id: 8 62 | mail_notification: false 63 | -------------------------------------------------------------------------------- /test/unit/tracker_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../test_helper', __FILE__) 2 | 3 | class TrackerTest < ActiveSupport::TestCase 4 | context "relations" do 5 | should have_many(:tracker_statuses) 6 | should have_many(:predef_issue_statuses).through(:tracker_statuses) 7 | end 8 | 9 | context "issue_statuses" do 10 | def test_default_behaviour 11 | tracker = Tracker.find(1) 12 | WorkflowTransition.delete_all 13 | WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 2, :new_status_id => 3) 14 | WorkflowTransition.create!(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 5) 15 | 16 | assert_kind_of Array, tracker.issue_statuses 17 | assert_kind_of IssueStatus, tracker.issue_statuses.first 18 | assert_equal [2, 3, 5], Tracker.find(1).issue_statuses.collect(&:id) 19 | end 20 | 21 | def test_should_be_empty_for_new_record 22 | assert_equal [], Tracker.new.issue_statuses 23 | end 24 | 25 | def test_predef_statuses 26 | WorkflowTransition.delete_all 27 | tracker = Tracker.find(1) 28 | tracker.predef_issue_status_ids = [1, 6] 29 | assert_kind_of Array, tracker.issue_statuses 30 | assert_kind_of IssueStatus, tracker.issue_statuses.first 31 | assert_equal [1, 6], Tracker.find(1).issue_statuses.collect(&:id) 32 | end 33 | 34 | def test_predef_statuses_and_workflow 35 | tracker = Tracker.find(1) 36 | tracker.predef_issue_status_ids = [1, 6] 37 | assert_kind_of Array, tracker.issue_statuses 38 | assert_kind_of IssueStatus, tracker.issue_statuses.first 39 | assert_equal [1, 2, 3, 4, 5, 6], Tracker.find(1).issue_statuses.collect(&:id) 40 | end 41 | end 42 | end 43 | 44 | -------------------------------------------------------------------------------- /test/functional/issues_controller_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../test_helper', __FILE__) 2 | 3 | class IssuesControllerTest < ActionController::TestCase 4 | fixtures :projects, 5 | :users, 6 | :roles, 7 | :members, 8 | :member_roles, 9 | :issues, 10 | :issue_statuses, 11 | :issue_categories, 12 | :trackers, 13 | :projects_trackers, 14 | :enabled_modules, 15 | :enumerations, 16 | :workflows 17 | 18 | def setup 19 | User.current = nil 20 | @request.session[:user_id] = 2 # manager 21 | end 22 | 23 | def test_show_by_manager_with_permission 24 | Role.find(1).add_permission! :workflow_graph_view 25 | get :show, :id => 1 26 | assert_response :success 27 | assert_template 'show' 28 | 29 | assert_equal Issue.find(1), assigns(:issue) 30 | 31 | # test if standard Redmine stuff is there (i.e. render_super() works) 32 | assert_select 'a', :text => /Quote/ 33 | assert_select 'form#issue-form', true 34 | assert_select '#issue_tracker_id', true 35 | 36 | # test if workflow visualization is there 37 | assert_select '#workflow-display', true 38 | end 39 | 40 | def test_show_by_manager_without_permission 41 | get :show, :id => 1 42 | assert_response :success 43 | assert_template 'show' 44 | 45 | assert_equal Issue.find(1), assigns(:issue) 46 | 47 | # test if standard Redmine stuff is there (i.e. render_super() works) 48 | assert_select 'a', :text => /Quote/ 49 | assert_select 'form#issue-form', true 50 | assert_select '#issue_tracker_id', true 51 | 52 | # test if workflow visualization is there 53 | assert_select '#workflow-display', false 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/fixtures/projects.yml: -------------------------------------------------------------------------------- 1 | --- 2 | projects_001: 3 | created_on: 2006-07-19 19:13:59 +02:00 4 | name: eCookbook 5 | updated_on: 2006-07-19 22:53:01 +02:00 6 | id: 1 7 | description: Recipes management application 8 | homepage: http://ecookbook.somenet.foo/ 9 | is_public: true 10 | identifier: ecookbook 11 | parent_id: 12 | lft: 1 13 | rgt: 10 14 | projects_002: 15 | created_on: 2006-07-19 19:14:19 +02:00 16 | name: OnlineStore 17 | updated_on: 2006-07-19 19:14:19 +02:00 18 | id: 2 19 | description: E-commerce web site 20 | homepage: "" 21 | is_public: false 22 | identifier: onlinestore 23 | parent_id: 24 | lft: 11 25 | rgt: 12 26 | projects_003: 27 | created_on: 2006-07-19 19:15:21 +02:00 28 | name: eCookbook Subproject 1 29 | updated_on: 2006-07-19 19:18:12 +02:00 30 | id: 3 31 | description: eCookBook Subproject 1 32 | homepage: "" 33 | is_public: true 34 | identifier: subproject1 35 | parent_id: 1 36 | lft: 6 37 | rgt: 7 38 | projects_004: 39 | created_on: 2006-07-19 19:15:51 +02:00 40 | name: eCookbook Subproject 2 41 | updated_on: 2006-07-19 19:17:07 +02:00 42 | id: 4 43 | description: eCookbook Subproject 2 44 | homepage: "" 45 | is_public: true 46 | identifier: subproject2 47 | parent_id: 1 48 | lft: 8 49 | rgt: 9 50 | projects_005: 51 | created_on: 2006-07-19 19:15:51 +02:00 52 | name: Private child of eCookbook 53 | updated_on: 2006-07-19 19:17:07 +02:00 54 | id: 5 55 | description: This is a private subproject of a public project 56 | homepage: "" 57 | is_public: false 58 | identifier: private-child 59 | parent_id: 1 60 | lft: 2 61 | rgt: 5 62 | projects_006: 63 | created_on: 2006-07-19 19:15:51 +02:00 64 | name: Child of private child 65 | updated_on: 2006-07-19 19:17:07 +02:00 66 | id: 6 67 | description: This is a public subproject of a private project 68 | homepage: "" 69 | is_public: true 70 | identifier: project6 71 | parent_id: 5 72 | lft: 3 73 | rgt: 4 74 | -------------------------------------------------------------------------------- /test/functional/trackers_controller_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../test_helper', __FILE__) 2 | 3 | class TrackersControllerTest < ActionController::TestCase 4 | fixtures :roles, :trackers, :workflows, :users, :issue_statuses 5 | 6 | def setup 7 | User.current = nil 8 | @request.session[:user_id] = 1 # admin 9 | end 10 | 11 | def test_get_edit 12 | Tracker.find(1).predef_issue_status_ids = [1, 3] 13 | 14 | get :edit, :id => 1 15 | assert_response :success 16 | assert_template 'edit' 17 | 18 | assert_not_nil assigns(:tracker) 19 | 20 | # test if standard Redmine stuff is there (i.e. render_super() works) 21 | assert_select '#tracker_name', true 22 | 23 | # test if workflow visualization is there 24 | assert_select 'svg#workflow-vis', true 25 | 26 | # test if the issue status selectino is there 27 | assert_select '#predef-statuses', true 28 | 29 | assert_select 'input[name=?][value="1"][checked=checked]', 'tracker[predef_issue_status_ids][]' 30 | assert_select 'input[name=?][value="3"][checked=checked]', 'tracker[predef_issue_status_ids][]' 31 | end 32 | 33 | def test_create_with_predef_statuses 34 | assert_difference 'Tracker.count' do 35 | post :create, :tracker => { 36 | :name => 'New tracker', 37 | :default_status_id => 1, 38 | :predef_issue_status_ids => ['1', '2', '3', '5'] } 39 | end 40 | assert_redirected_to :action => 'index' 41 | 42 | tracker = Tracker.order('id DESC').first 43 | assert_equal 'New tracker', tracker.name 44 | assert_equal 0, tracker.workflow_rules.count 45 | assert_equal 4, tracker.predef_issue_statuses.count 46 | end 47 | 48 | def test_updat_with_predef_statusese 49 | put :update, :id => 1, :tracker => { 50 | :name => 'Renamed', 51 | :predef_issue_status_ids => ['3', '5'] } 52 | assert_redirected_to :action => 'index' 53 | assert_equal [3, 5], Tracker.find(1).predef_issue_status_ids.sort 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/fixtures/enabled_modules.yml: -------------------------------------------------------------------------------- 1 | --- 2 | enabled_modules_001: 3 | name: issue_tracking 4 | project_id: 1 5 | id: 1 6 | enabled_modules_002: 7 | name: time_tracking 8 | project_id: 1 9 | id: 2 10 | enabled_modules_003: 11 | name: news 12 | project_id: 1 13 | id: 3 14 | enabled_modules_004: 15 | name: documents 16 | project_id: 1 17 | id: 4 18 | enabled_modules_005: 19 | name: files 20 | project_id: 1 21 | id: 5 22 | enabled_modules_006: 23 | name: wiki 24 | project_id: 1 25 | id: 6 26 | enabled_modules_007: 27 | name: repository 28 | project_id: 1 29 | id: 7 30 | enabled_modules_008: 31 | name: boards 32 | project_id: 1 33 | id: 8 34 | enabled_modules_009: 35 | name: repository 36 | project_id: 3 37 | id: 9 38 | enabled_modules_010: 39 | name: wiki 40 | project_id: 3 41 | id: 10 42 | enabled_modules_011: 43 | name: issue_tracking 44 | project_id: 2 45 | id: 11 46 | enabled_modules_012: 47 | name: time_tracking 48 | project_id: 3 49 | id: 12 50 | enabled_modules_013: 51 | name: issue_tracking 52 | project_id: 3 53 | id: 13 54 | enabled_modules_014: 55 | name: issue_tracking 56 | project_id: 5 57 | id: 14 58 | enabled_modules_015: 59 | name: wiki 60 | project_id: 2 61 | id: 15 62 | enabled_modules_016: 63 | name: boards 64 | project_id: 2 65 | id: 16 66 | enabled_modules_017: 67 | name: calendar 68 | project_id: 1 69 | id: 17 70 | enabled_modules_018: 71 | name: gantt 72 | project_id: 1 73 | id: 18 74 | enabled_modules_019: 75 | name: calendar 76 | project_id: 2 77 | id: 19 78 | enabled_modules_020: 79 | name: gantt 80 | project_id: 2 81 | id: 20 82 | enabled_modules_021: 83 | name: calendar 84 | project_id: 3 85 | id: 21 86 | enabled_modules_022: 87 | name: gantt 88 | project_id: 3 89 | id: 22 90 | enabled_modules_023: 91 | name: calendar 92 | project_id: 5 93 | id: 23 94 | enabled_modules_024: 95 | name: gantt 96 | project_id: 5 97 | id: 24 98 | enabled_modules_025: 99 | name: news 100 | project_id: 2 101 | id: 25 102 | enabled_modules_026: 103 | name: repository 104 | project_id: 2 105 | id: 26 106 | -------------------------------------------------------------------------------- /test/fixtures/enumerations.yml: -------------------------------------------------------------------------------- 1 | --- 2 | enumerations_001: 3 | name: Uncategorized 4 | id: 1 5 | type: DocumentCategory 6 | active: true 7 | position: 1 8 | enumerations_002: 9 | name: User documentation 10 | id: 2 11 | type: DocumentCategory 12 | active: true 13 | position: 2 14 | enumerations_003: 15 | name: Technical documentation 16 | id: 3 17 | type: DocumentCategory 18 | active: true 19 | position: 3 20 | enumerations_004: 21 | name: Low 22 | id: 4 23 | type: IssuePriority 24 | active: true 25 | position: 1 26 | position_name: lowest 27 | enumerations_005: 28 | name: Normal 29 | id: 5 30 | type: IssuePriority 31 | is_default: true 32 | active: true 33 | position: 2 34 | position_name: default 35 | enumerations_006: 36 | name: High 37 | id: 6 38 | type: IssuePriority 39 | active: true 40 | position: 3 41 | position_name: high3 42 | enumerations_007: 43 | name: Urgent 44 | id: 7 45 | type: IssuePriority 46 | active: true 47 | position: 4 48 | position_name: high2 49 | enumerations_008: 50 | name: Immediate 51 | id: 8 52 | type: IssuePriority 53 | active: true 54 | position: 5 55 | position_name: highest 56 | enumerations_009: 57 | name: Design 58 | id: 9 59 | type: TimeEntryActivity 60 | position: 1 61 | active: true 62 | enumerations_010: 63 | name: Development 64 | id: 10 65 | type: TimeEntryActivity 66 | position: 2 67 | is_default: true 68 | active: true 69 | enumerations_011: 70 | name: QA 71 | id: 11 72 | type: TimeEntryActivity 73 | position: 3 74 | active: true 75 | enumerations_012: 76 | name: Default Enumeration 77 | id: 12 78 | type: Enumeration 79 | is_default: true 80 | active: true 81 | enumerations_013: 82 | name: Another Enumeration 83 | id: 13 84 | type: Enumeration 85 | active: true 86 | enumerations_014: 87 | name: Inactive Activity 88 | id: 14 89 | type: TimeEntryActivity 90 | position: 4 91 | active: false 92 | enumerations_015: 93 | name: Inactive Priority 94 | id: 15 95 | type: IssuePriority 96 | position: 6 97 | active: false 98 | enumerations_016: 99 | name: Inactive Document Category 100 | id: 16 101 | type: DocumentCategory 102 | active: false 103 | position: 4 104 | -------------------------------------------------------------------------------- /lib/workflow_enhancements/patches/action_view_rendering.rb: -------------------------------------------------------------------------------- 1 | # 2 | # This is mostly copied from active_scaffold, 3 | # https://github.com/activescaffold/active_scaffold/ 4 | # 5 | module ActionView 6 | class LookupContext 7 | module ViewPaths 8 | def find_all_templates(name, partial = false, locals = {}) 9 | prefixes.collect do |prefix| 10 | view_paths.collect do |resolver| 11 | temp_args = *args_for_lookup(name, [prefix], partial, locals, {}) 12 | temp_args[1] = temp_args[1][0] 13 | resolver.find_all(*temp_args) 14 | end 15 | end.flatten! 16 | end 17 | end 18 | end 19 | end 20 | 21 | module ActionView 22 | class Base 23 | # 24 | # Adds rendering option. 25 | # 26 | # ==render :super 27 | # 28 | # This renders the "super" template, i.e. the one hidden by the plugin 29 | # 30 | def render_with_workflow_enhancements(*args, &block) 31 | if args.first == :super 32 | last_view = view_stack.last || {:view => instance_variable_get(:@virtual_path).split('/').last} 33 | options = args[1] || {} 34 | options[:locals] ||= {} 35 | options[:locals].reverse_merge!(last_view[:locals] || {}) 36 | if last_view[:templates].nil? 37 | last_view[:templates] = lookup_context.find_all_templates(last_view[:view], last_view[:partial], options[:locals].keys) 38 | last_view[:templates].shift 39 | end 40 | options[:template] = last_view[:templates].shift 41 | view_stack << last_view 42 | result = render_without_workflow_enhancements options 43 | view_stack.pop 44 | result 45 | else 46 | options = args.first 47 | if options.is_a?(Hash) 48 | current_view = {:view => options[:partial], :partial => true} if options[:partial] 49 | current_view = {:view => options[:template], :partial => false} if current_view.nil? && options[:template] 50 | current_view[:locals] = options[:locals] if !current_view.nil? && options[:locals] 51 | view_stack << current_view if current_view.present? 52 | end 53 | result = render_without_workflow_enhancements(*args, &block) 54 | view_stack.pop if current_view.present? 55 | result 56 | end 57 | end 58 | 59 | alias_method_chain :render, :workflow_enhancements 60 | 61 | def view_stack 62 | @_view_stack ||= [] 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/fixtures/workflows.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # manager: new -> assigned 3 | WorkflowTransitions_001: 4 | id: 1 5 | role_id: 1 6 | tracker_id: 1 7 | old_status_id: 1 8 | new_status_id: 2 9 | type: WorkflowTransition 10 | # manager: assigned -> new 11 | WorkflowTransitions_002: 12 | id: 2 13 | role_id: 1 14 | tracker_id: 1 15 | old_status_id: 2 16 | new_status_id: 1 17 | type: WorkflowTransition 18 | # manager: assigned -> resolved 19 | WorkflowTransitions_003: 20 | id: 3 21 | role_id: 1 22 | tracker_id: 1 23 | old_status_id: 2 24 | new_status_id: 3 25 | type: WorkflowTransition 26 | # manager: resolved -> assigned 27 | WorkflowTransitions_004: 28 | id: 4 29 | role_id: 1 30 | tracker_id: 1 31 | old_status_id: 3 32 | new_status_id: 2 33 | type: WorkflowTransition 34 | # manager: resolved -> closed 35 | WorkflowTransitions_005: 36 | id: 5 37 | role_id: 1 38 | tracker_id: 1 39 | old_status_id: 3 40 | new_status_id: 5 41 | type: WorkflowTransition 42 | # manager: resolved -> feedback 43 | WorkflowTransitions_006: 44 | id: 6 45 | role_id: 1 46 | tracker_id: 1 47 | old_status_id: 3 48 | new_status_id: 4 49 | type: WorkflowTransition 50 | # manager: feedback -> assigned 51 | WorkflowTransitions_007: 52 | id: 7 53 | role_id: 1 54 | tracker_id: 1 55 | old_status_id: 4 56 | new_status_id: 2 57 | type: WorkflowTransition 58 | # manager: feedback -> closed 59 | WorkflowTransitions_008: 60 | id: 8 61 | role_id: 1 62 | tracker_id: 1 63 | old_status_id: 4 64 | new_status_id: 5 65 | type: WorkflowTransition 66 | # manager: assigned -> rejected 67 | WorkflowTransitions_009: 68 | id: 9 69 | role_id: 1 70 | tracker_id: 1 71 | old_status_id: 2 72 | new_status_id: 6 73 | type: WorkflowTransition 74 | # reporter: feedback -> assigned if author 75 | WorkflowTransitions_010: 76 | id: 10 77 | role_id: 3 78 | tracker_id: 1 79 | old_status_id: 4 80 | new_status_id: 2 81 | author: true 82 | type: WorkflowTransition 83 | # reporter: feedback -> closed if author 84 | WorkflowTransitions_011: 85 | id: 11 86 | role_id: 3 87 | tracker_id: 1 88 | old_status_id: 4 89 | new_status_id: 5 90 | author: 1 91 | type: WorkflowTransition 92 | # reporter: assigned -> resolved if assignee 93 | WorkflowTransitions_012: 94 | id: 12 95 | role_id: 3 96 | tracker_id: 1 97 | old_status_id: 2 98 | new_status_id: 3 99 | assignee: 1 100 | type: WorkflowTransition 101 | -------------------------------------------------------------------------------- /lib/workflow_enhancements/graph.rb: -------------------------------------------------------------------------------- 1 | module WorkflowEnhancements::Graph 2 | 3 | def self.load_data(roles, trackers, issue=nil) 4 | tracker = nil 5 | if trackers.is_a?(Array) 6 | tracker = trackers.length == 1 ? trackers.first : nil 7 | else 8 | tracker = trackers 9 | end 10 | unless tracker 11 | return { :nodes => [], :edges => [] } 12 | end 13 | 14 | current_status = nil 15 | possible_statuses = {} 16 | if issue 17 | current_status = issue.status_id 18 | issue.new_statuses_allowed_to().each {|x| possible_statuses[x.id] = true } 19 | end 20 | 21 | role_map = {} 22 | Array(roles).each {|x| role_map[x.id] = x } if roles 23 | 24 | new_issue_status_map = {} 25 | edges_map = {} 26 | WorkflowTransition.where(:tracker_id => tracker).each do |t| 27 | if t.old_status_id != 0 28 | key = t.old_status_id.to_s + '-' + t.new_status_id.to_s 29 | own = role_map.include?(t.role_id) 30 | author = own && t.author 31 | assignee = own && t.assignee 32 | always = own && !author && !assignee 33 | 34 | if edges_map.include?(key) 35 | edges_map[key][:own] ||= own 36 | edges_map[key][:author] ||= author 37 | edges_map[key][:assignee] ||= assignee 38 | edges_map[key][:always] ||= always 39 | else 40 | edges_map[key] = { :u => t.old_status_id, :v => t.new_status_id, 41 | :own => own, :author => author, :assignee => assignee, :always => always } 42 | end 43 | else 44 | new_issue_status_map[t.new_status_id] = 1 45 | end 46 | end 47 | edges_array = [] 48 | edges_map.each_value do |e| 49 | cls = role_map.empty? ? '' : 'transOther' 50 | if e[:own] 51 | cls = 'transOwn' 52 | unless e[:always] 53 | cls += ' transOwn-author' if e[:author] 54 | cls += ' transOwn-assignee' if e[:assignee] 55 | end 56 | end 57 | edges_array << { :u => e[:u], :v => e[:v], :value => { :edgeclass => cls } } 58 | end 59 | 60 | statuses_array = tracker.issue_statuses.map do |s| 61 | cls = '' 62 | if is_default_status(tracker, s) 63 | cls = 'state-new' 64 | elsif new_issue_status_map.include?(s.id) 65 | cls = 'state-new-possible' 66 | elsif s.is_closed 67 | cls = 'state-closed' 68 | end 69 | if s.id == current_status 70 | cls += ' state-current' 71 | elsif possible_statuses.include?(s.id) 72 | cls += ' state-possible' 73 | end 74 | { :id => s.id, :value => { :label => s.name, :nodeclass => cls } } 75 | end 76 | 77 | { :nodes => statuses_array, :edges => edges_array } 78 | end 79 | 80 | private 81 | 82 | def self.is_default_status(tracker, status) 83 | if Redmine::VERSION::MAJOR == 3 84 | tracker.default_status == status 85 | else 86 | status.is_default 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /app/views/workflow_enhancements/_workflow_graph.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | trackers = Array(trackers) 3 | roles = Array(roles) 4 | minimal = false if local_assigns[:minimal].nil? 5 | issue ||= nil 6 | %> 7 | <% if trackers.length == 1 %> 8 | <%= javascript_include_tag "d3.v3.min.js", :plugin => :redmine_workflow_enhancements %> 9 | <%= javascript_include_tag "dagre-d3.min.js", :plugin => :redmine_workflow_enhancements %> 10 | <%= javascript_include_tag "saveSvgAsPng.js", :plugin => :redmine_workflow_enhancements %> 11 | 12 |
"> 13 | <% unless minimal %> 14 | 17 | <% end %> 18 |

"> 19 | <%= l(:label_workflow) %>: <%= trackers.first.name %> 20 | <% unless roles.empty? %> 21 | - <%= roles.length == 1 ? l(:label_role) : l(:label_role_plural) %>: 22 | <%= roles.map(&:name).join(', ') %> 23 | <% end %> 24 |

25 |
26 | 27 | 28 | 29 |
30 |
31 | 32 | 106 | <% end %> 107 | 108 | -------------------------------------------------------------------------------- /assets/javascripts/saveSvgAsPng.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var out$ = typeof exports != 'undefined' && exports || this; 3 | 4 | var doctype = ''; 5 | 6 | function inlineImages(callback) { 7 | var images = document.querySelectorAll('svg image'); 8 | var left = images.length; 9 | if (left == 0) { 10 | callback(); 11 | } 12 | for (var i = 0; i < images.length; i++) { 13 | (function(image) { 14 | if (image.getAttribute('xlink:href')) { 15 | var href = image.getAttribute('xlink:href').value; 16 | if (/^http/.test(href) && !(new RegExp('^' + window.location.host).test(href))) { 17 | throw new Error("Cannot render embedded images linking to external hosts."); 18 | } 19 | } 20 | var canvas = document.createElement('canvas'); 21 | var ctx = canvas.getContext('2d'); 22 | var img = new Image(); 23 | img.src = image.getAttribute('xlink:href'); 24 | img.onload = function() { 25 | canvas.width = img.width; 26 | canvas.height = img.height; 27 | ctx.drawImage(img, 0, 0); 28 | image.setAttribute('xlink:href', canvas.toDataURL('image/png')); 29 | left--; 30 | if (left == 0) { 31 | callback(); 32 | } 33 | } 34 | })(images[i]); 35 | } 36 | } 37 | 38 | function styles(dom) { 39 | var used = ""; 40 | var sheets = document.styleSheets; 41 | for (var i = 0; i < sheets.length; i++) { 42 | var rules = sheets[i].cssRules; 43 | for (var j = 0; j < rules.length; j++) { 44 | var rule = rules[j]; 45 | if (typeof(rule.style) != "undefined") { 46 | var elems = dom.querySelectorAll(rule.selectorText); 47 | if (elems.length > 0) { 48 | used += rule.selectorText + " { " + rule.style.cssText + " }\n"; 49 | } 50 | } 51 | } 52 | } 53 | 54 | var s = document.createElement('style'); 55 | s.setAttribute('type', 'text/css'); 56 | s.innerHTML = ""; 57 | 58 | var defs = document.createElement('defs'); 59 | defs.appendChild(s); 60 | return defs; 61 | } 62 | 63 | out$.svgAsDataUri = function(el, scaleFactor, cb) { 64 | scaleFactor = scaleFactor || 1; 65 | 66 | inlineImages(function() { 67 | var outer = document.createElement("div"); 68 | var clone = el.cloneNode(true); 69 | var width = parseInt(clone.getAttribute("width")); 70 | var height = parseInt(clone.getAttribute("height")); 71 | 72 | var xmlns = "http://www.w3.org/2000/xmlns/"; 73 | 74 | clone.setAttribute("version", "1.1"); 75 | clone.setAttributeNS(xmlns, "xmlns", "http://www.w3.org/2000/svg"); 76 | clone.setAttributeNS(xmlns, "xmlns:xlink", "http://www.w3.org/1999/xlink"); 77 | clone.setAttribute("width", width * scaleFactor); 78 | clone.setAttribute("height", height * scaleFactor); 79 | clone.setAttribute("viewBox", "0 0 " + width + " " + height); 80 | outer.appendChild(clone); 81 | 82 | clone.insertBefore(styles(clone), clone.firstChild); 83 | 84 | var svg = doctype + outer.innerHTML; 85 | var uri = 'data:image/svg+xml;base64,' + window.btoa(unescape(encodeURIComponent(svg))); 86 | if (cb) { 87 | cb(uri); 88 | } 89 | }); 90 | } 91 | 92 | out$.saveSvgAsPng = function(el, name, scaleFactor) { 93 | out$.svgAsDataUri(el, scaleFactor, function(uri) { 94 | var image = new Image(); 95 | image.src = uri; 96 | image.onload = function() { 97 | var canvas = document.createElement('canvas'); 98 | canvas.width = image.width; 99 | canvas.height = image.height; 100 | var context = canvas.getContext('2d'); 101 | context.drawImage(image, 0, 0); 102 | 103 | var a = document.createElement('a'); 104 | a.download = name; 105 | a.href = canvas.toDataURL('image/png'); 106 | document.body.appendChild(a); 107 | a.click(); 108 | } 109 | }); 110 | } 111 | })(); 112 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | # This is copied from Redmine 2.5 2 | --- 3 | users_004: 4 | created_on: 2006-07-19 19:34:07 +02:00 5 | status: 1 6 | last_login_on: 7 | language: en 8 | # password = foo 9 | salt: 3126f764c3c5ac61cbfc103f25f934cf 10 | hashed_password: 9e4dd7eeb172c12a0691a6d9d3a269f7e9fe671b 11 | updated_on: 2006-07-19 19:34:07 +02:00 12 | admin: false 13 | lastname: Hill 14 | firstname: Robert 15 | id: 4 16 | auth_source_id: 17 | mail_notification: all 18 | login: rhill 19 | type: User 20 | users_001: 21 | created_on: 2006-07-19 19:12:21 +02:00 22 | status: 1 23 | last_login_on: 2006-07-19 22:57:52 +02:00 24 | language: en 25 | # password = admin 26 | salt: 82090c953c4a0000a7db253b0691a6b4 27 | hashed_password: b5b6ff9543bf1387374cdfa27a54c96d236a7150 28 | updated_on: 2006-07-19 22:57:52 +02:00 29 | admin: true 30 | lastname: Admin 31 | firstname: Redmine 32 | id: 1 33 | auth_source_id: 34 | mail_notification: all 35 | login: admin 36 | type: User 37 | users_002: 38 | created_on: 2006-07-19 19:32:09 +02:00 39 | status: 1 40 | last_login_on: 2006-07-19 22:42:15 +02:00 41 | language: en 42 | # password = jsmith 43 | salt: 67eb4732624d5a7753dcea7ce0bb7d7d 44 | hashed_password: bfbe06043353a677d0215b26a5800d128d5413bc 45 | updated_on: 2006-07-19 22:42:15 +02:00 46 | admin: false 47 | lastname: Smith 48 | firstname: John 49 | id: 2 50 | auth_source_id: 51 | mail_notification: all 52 | login: jsmith 53 | type: User 54 | users_003: 55 | created_on: 2006-07-19 19:33:19 +02:00 56 | status: 1 57 | last_login_on: 58 | language: en 59 | # password = foo 60 | salt: 7599f9963ec07b5a3b55b354407120c0 61 | hashed_password: 8f659c8d7c072f189374edacfa90d6abbc26d8ed 62 | updated_on: 2006-07-19 19:33:19 +02:00 63 | admin: false 64 | lastname: Lopper 65 | firstname: Dave 66 | id: 3 67 | auth_source_id: 68 | mail_notification: all 69 | login: dlopper 70 | type: User 71 | users_005: 72 | id: 5 73 | created_on: 2006-07-19 19:33:19 +02:00 74 | # Locked 75 | status: 3 76 | last_login_on: 77 | language: en 78 | hashed_password: 1 79 | updated_on: 2006-07-19 19:33:19 +02:00 80 | admin: false 81 | lastname: Lopper2 82 | firstname: Dave2 83 | auth_source_id: 84 | mail_notification: all 85 | login: dlopper2 86 | type: User 87 | users_006: 88 | id: 6 89 | created_on: 2006-07-19 19:33:19 +02:00 90 | status: 0 91 | last_login_on: 92 | language: '' 93 | hashed_password: 1 94 | updated_on: 2006-07-19 19:33:19 +02:00 95 | admin: false 96 | lastname: Anonymous 97 | firstname: '' 98 | auth_source_id: 99 | mail_notification: only_my_events 100 | login: '' 101 | type: AnonymousUser 102 | users_007: 103 | # A user who does not belong to any project 104 | id: 7 105 | created_on: 2006-07-19 19:33:19 +02:00 106 | status: 1 107 | last_login_on: 108 | language: 'en' 109 | # password = foo 110 | salt: 7599f9963ec07b5a3b55b354407120c0 111 | hashed_password: 8f659c8d7c072f189374edacfa90d6abbc26d8ed 112 | updated_on: 2006-07-19 19:33:19 +02:00 113 | admin: false 114 | lastname: One 115 | firstname: Some 116 | auth_source_id: 117 | mail_notification: only_my_events 118 | login: someone 119 | type: User 120 | users_008: 121 | id: 8 122 | created_on: 2006-07-19 19:33:19 +02:00 123 | status: 1 124 | last_login_on: 125 | language: 'it' 126 | # password = foo 127 | salt: 7599f9963ec07b5a3b55b354407120c0 128 | hashed_password: 8f659c8d7c072f189374edacfa90d6abbc26d8ed 129 | updated_on: 2006-07-19 19:33:19 +02:00 130 | admin: false 131 | lastname: Misc 132 | firstname: User 133 | auth_source_id: 134 | mail_notification: only_my_events 135 | login: miscuser8 136 | type: User 137 | users_009: 138 | id: 9 139 | created_on: 2006-07-19 19:33:19 +02:00 140 | status: 1 141 | last_login_on: 142 | language: 'it' 143 | hashed_password: 1 144 | updated_on: 2006-07-19 19:33:19 +02:00 145 | admin: false 146 | lastname: Misc 147 | firstname: User 148 | auth_source_id: 149 | mail_notification: only_my_events 150 | login: miscuser9 151 | type: User 152 | groups_010: 153 | id: 10 154 | lastname: A Team 155 | type: Group 156 | groups_011: 157 | id: 11 158 | lastname: B Team 159 | type: Group 160 | 161 | 162 | -------------------------------------------------------------------------------- /test/fixtures/roles.yml: -------------------------------------------------------------------------------- 1 | # This is copied from Redmine 2.5 and modified 2 | --- 3 | roles_001: 4 | name: Manager 5 | id: 1 6 | builtin: 0 7 | issues_visibility: all 8 | permissions: | 9 | --- 10 | - :add_project 11 | - :edit_project 12 | - :close_project 13 | - :select_project_modules 14 | - :manage_members 15 | - :manage_versions 16 | - :manage_categories 17 | - :view_issues 18 | - :add_issues 19 | - :edit_issues 20 | - :manage_issue_relations 21 | - :manage_subtasks 22 | - :add_issue_notes 23 | - :move_issues 24 | - :delete_issues 25 | - :view_issue_watchers 26 | - :add_issue_watchers 27 | - :set_issues_private 28 | - :set_notes_private 29 | - :view_private_notes 30 | - :delete_issue_watchers 31 | - :manage_public_queries 32 | - :save_queries 33 | - :view_gantt 34 | - :view_calendar 35 | - :log_time 36 | - :view_time_entries 37 | - :edit_time_entries 38 | - :delete_time_entries 39 | - :manage_news 40 | - :comment_news 41 | - :view_documents 42 | - :add_documents 43 | - :edit_documents 44 | - :delete_documents 45 | - :view_wiki_pages 46 | - :export_wiki_pages 47 | - :view_wiki_edits 48 | - :edit_wiki_pages 49 | - :delete_wiki_pages_attachments 50 | - :protect_wiki_pages 51 | - :delete_wiki_pages 52 | - :rename_wiki_pages 53 | - :add_messages 54 | - :edit_messages 55 | - :delete_messages 56 | - :manage_boards 57 | - :view_files 58 | - :manage_files 59 | - :browse_repository 60 | - :manage_repository 61 | - :view_changesets 62 | - :manage_related_issues 63 | - :manage_project_activities 64 | 65 | position: 1 66 | roles_002: 67 | name: Developer 68 | id: 2 69 | builtin: 0 70 | issues_visibility: default 71 | permissions: | 72 | --- 73 | - :edit_project 74 | - :manage_members 75 | - :manage_versions 76 | - :manage_categories 77 | - :view_issues 78 | - :add_issues 79 | - :edit_issues 80 | - :manage_issue_relations 81 | - :manage_subtasks 82 | - :add_issue_notes 83 | - :move_issues 84 | - :delete_issues 85 | - :view_issue_watchers 86 | - :save_queries 87 | - :view_gantt 88 | - :view_calendar 89 | - :log_time 90 | - :view_time_entries 91 | - :edit_own_time_entries 92 | - :manage_news 93 | - :comment_news 94 | - :view_documents 95 | - :add_documents 96 | - :edit_documents 97 | - :delete_documents 98 | - :view_wiki_pages 99 | - :view_wiki_edits 100 | - :edit_wiki_pages 101 | - :protect_wiki_pages 102 | - :delete_wiki_pages 103 | - :add_messages 104 | - :edit_own_messages 105 | - :delete_own_messages 106 | - :manage_boards 107 | - :view_files 108 | - :manage_files 109 | - :browse_repository 110 | - :view_changesets 111 | 112 | position: 2 113 | roles_003: 114 | name: Reporter 115 | id: 3 116 | builtin: 0 117 | issues_visibility: default 118 | permissions: | 119 | --- 120 | - :edit_project 121 | - :manage_members 122 | - :manage_versions 123 | - :manage_categories 124 | - :view_issues 125 | - :add_issues 126 | - :edit_issues 127 | - :manage_issue_relations 128 | - :add_issue_notes 129 | - :move_issues 130 | - :view_issue_watchers 131 | - :save_queries 132 | - :view_gantt 133 | - :view_calendar 134 | - :log_time 135 | - :view_time_entries 136 | - :manage_news 137 | - :comment_news 138 | - :view_documents 139 | - :add_documents 140 | - :edit_documents 141 | - :delete_documents 142 | - :view_wiki_pages 143 | - :view_wiki_edits 144 | - :edit_wiki_pages 145 | - :delete_wiki_pages 146 | - :add_messages 147 | - :manage_boards 148 | - :view_files 149 | - :manage_files 150 | - :browse_repository 151 | - :view_changesets 152 | 153 | position: 3 154 | roles_004: 155 | name: Non member 156 | id: 4 157 | builtin: 1 158 | issues_visibility: default 159 | permissions: | 160 | --- 161 | - :view_issues 162 | - :add_issues 163 | - :edit_issues 164 | - :manage_issue_relations 165 | - :add_issue_notes 166 | - :save_queries 167 | - :view_gantt 168 | - :view_calendar 169 | - :log_time 170 | - :view_time_entries 171 | - :comment_news 172 | - :view_documents 173 | - :view_wiki_pages 174 | - :view_wiki_edits 175 | - :edit_wiki_pages 176 | - :add_messages 177 | - :view_files 178 | - :manage_files 179 | - :browse_repository 180 | - :view_changesets 181 | 182 | position: 4 183 | roles_005: 184 | name: Anonymous 185 | id: 5 186 | builtin: 2 187 | issues_visibility: default 188 | permissions: | 189 | --- 190 | - :view_issues 191 | - :add_issue_notes 192 | - :view_gantt 193 | - :view_calendar 194 | - :view_time_entries 195 | - :view_documents 196 | - :view_wiki_pages 197 | - :view_wiki_edits 198 | - :view_files 199 | - :browse_repository 200 | - :view_changesets 201 | 202 | position: 5 203 | 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redmine Workflow Enhancements 2 | 3 | Add various enhancements to workflow editing. Currently this consists of these: 4 | 5 | * Visualization with dagre-d3: 6 | 7 | * Workflow edit 8 | * Tracker edit 9 | * Issue edit (behind the little question mark next to "Issue Status") 10 | 11 | * Ability to pre-define association between tracker and issue statuses for 12 | better overview. 13 | 14 | 15 | Requirements: 16 | 17 | * Redmine 2.2 or higher 18 | * Ruby 1.9.3 or higher 19 | 20 | ## Installation 21 | 22 | Installing the plugin requires these steps. From within the Redmine root 23 | directory: 24 | 25 | 1. **Get the plugin** 26 | 27 | ``` 28 | cd plugins 29 | git clone git://github.com/dr-itz/redmine_workflow_enhancements.git 30 | ``` 31 | 2. **Run bundler** 32 | 33 | Run excatly the same as during Redmine installation. This might be: 34 | 35 | ``` 36 | bundle install --without development test 37 | ``` 38 | 39 | 3. **Run plugin migrations** 40 | 41 | ``` 42 | bundle exec rake redmine:plugins NAME=redmine_workflow_enhancements RAILS_ENV=production 43 | ``` 44 | 45 | 4. **Restart Redmine** 46 | 47 | The second step is to restart Redmine. How this is done depends on how Redmine is 48 | setup. After the restart, configuration of the plugin can begin. 49 | 50 | ## Uninstalling 51 | 52 | Uninstalling the plugin is easy as well: 53 | 54 | 1. **Reverse plugin migrations** 55 | 56 | ``` 57 | bundle exec rake redmine:plugins NAME=redmine_workflow_enhancements RAILS_ENV=production VERSION=0 58 | ``` 59 | 60 | 2. **Removing the plugin directory** 61 | 62 | ``` 63 | rm -r plugins/redmine_workflow_enhancements 64 | ``` 65 | 66 | 3. **Restart Redmine** 67 | 68 | The second step is to restart Redmine. Once restarted, the plugin will be gone. 69 | 70 | ## Usage 71 | 72 | * Go to Administration -> Workflow, select a single workflow and click 'Edit' 73 | * Go to Administration -> Tracker, select a tracker 74 | * For each role that should be able to display the workflow graph in the issue form: 75 | 76 | * Go to Administration -> Roles and permissions, select the desired role 77 | * Check "View workflow graph" in the category "Issue Tracking" 78 | 79 | * In the issue edit form, click on the "?" next to the issue status 80 | 81 | ### What is displayed 82 | 83 | The generated graph always includes the full workflow of a tracker. To 84 | differentiate things: 85 | 86 | **Transitions:** 87 | 88 | * Black 89 | 90 | The transition is possible with the current roles 91 | 92 | * Grey 93 | 94 | The transition is not possible by the current role 95 | 96 | * Dashed line 97 | 98 | The transition is only possible when the user is the author of the issue 99 | 100 | * Red line 101 | 102 | The transition is only possible when the user is the assignee of the issue 103 | 104 | 105 | **Statuses:** 106 | 107 | * Green background 108 | 109 | The issue is closed in this state 110 | 111 | * Red background 112 | 113 | This is a status a new issues is can have. The default status for a new 114 | issues is displayed with bold text. 115 | 116 | Notes: 117 | 118 | * The default issue status for a tracker is configurable since Redmine 119 | 3.0. Older versions of Redmine always use "New". 120 | 121 | * The possible statuses for a new issue is configurable since Redmine 3.2. 122 | Older versions of Redmine allow the default status and all directly 123 | reachable statuses. The highlighting only shows the new Redmine 3.2 124 | configuration. 125 | 126 | * Grey border 127 | 128 | In issue edit form, this is the current selected status 129 | 130 | * Blue border 131 | 132 | In issue edit form, these are the next possible statuses in the workflow 133 | 134 | 135 | ### How the association between Tracker and Statuses works 136 | 137 | The idea is to control what's showed in "Administration" -> "Workflow" -> 138 | "Status transitions" -> "Edit". With a lot of different statuses and creating a 139 | new tracker, things get messy. The default is to only show statuses that are 140 | included in the workflow ("Only display statuses that are used by this tracker" 141 | checked). With a new tracker there are none. Unchecking said checkbox displays 142 | all statuses which makes it hard to find the relevant ones. It also involves a 143 | lot of scrolling on small screens. 144 | 145 | The plugin solves this with the pre-defined association in the tracker. It 146 | changes the behavior when "Only display statuses that are used by this tracker" 147 | is checked. It will display all statuses involved in the actual workflow (normal 148 | behavior) but also includes all the pre-defined ones. The behavior with that 149 | checkbox unchecked is unchanged, it will display all the statuses. 150 | 151 | 152 | ## Development and test 153 | 154 | To run the tests, an additional Gem is required for code coverage: simplecov. If 155 | bundler was initially run with `--without developmen test`, run again without 156 | these arguments to install *with* the development and test gems. 157 | 158 | To run the tests: 159 | 160 | ```` 161 | bundle exec rake redmine:plugins:test NAME=redmine_workflow_enhancements 162 | ```` 163 | 164 | 165 | ## License and thanks 166 | 167 | Since this is a Redmine plugin, Redmine is licensed under the GPLv2 and the 168 | GPLv2 is very clear about derived work and such, this plugin is licensed under 169 | the same license. 170 | 171 | This plugin borrows an extension to ActiveView that provides a `render :super` 172 | from ActiveScaffold, https://github.com/activescaffold/active_scaffold 173 | -------------------------------------------------------------------------------- /test/unit/graph_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../test_helper', __FILE__) 2 | 3 | class GraphTest < ActionView::TestCase 4 | fixtures :projects, 5 | :users, 6 | :roles, 7 | :members, 8 | :member_roles, 9 | :issues, 10 | :issue_statuses, 11 | :trackers, 12 | :projects_trackers, 13 | :enabled_modules, 14 | :workflows 15 | 16 | include ActionView::TestCase::Behavior 17 | 18 | def setup 19 | @tracker_bug = Tracker.find(1) 20 | @role_manager = Role.find(1) 21 | @role_reporter = Role.find(3) 22 | @role_nonmember = Role.find(4) 23 | @issue = Issue.find(1) 24 | 25 | @bug_states = [ 26 | { :id => 1, :value => { :label => "New", :nodeclass => "state-new" }}, 27 | { :id => 2, :value => { :label => "Assigned", :nodeclass => "" }}, 28 | { :id => 3, :value => { :label => "Resolved", :nodeclass => "" }}, 29 | { :id => 4, :value => { :label => "Feedback", :nodeclass => "" }}, 30 | { :id => 5, :value => { :label => "Closed", :nodeclass => "state-closed" }}, 31 | { :id => 6, :value => { :label => "Rejected", :nodeclass => "state-closed" }}] 32 | 33 | @bug_states_issue = [ 34 | { :id => 1, :value => { :label => "New", :nodeclass => "state-new state-current" }}, 35 | { :id => 2, :value => { :label => "Assigned", :nodeclass => " state-possible" }}, 36 | { :id => 3, :value => { :label => "Resolved", :nodeclass => "" }}, 37 | { :id => 4, :value => { :label => "Feedback", :nodeclass => "" }}, 38 | { :id => 5, :value => { :label => "Closed", :nodeclass => "state-closed" }}, 39 | { :id => 6, :value => { :label => "Rejected", :nodeclass => "state-closed" }}] 40 | 41 | @bug_transition_all = [ 42 | { :u => 1, :v => 2, :value => { :edgeclass => "" }}, 43 | { :u => 2, :v => 1, :value => { :edgeclass => "" }}, 44 | { :u => 2, :v => 3, :value => { :edgeclass => "" }}, 45 | { :u => 3, :v => 2, :value => { :edgeclass => "" }}, 46 | { :u => 3, :v => 5, :value => { :edgeclass => "" }}, 47 | { :u => 3, :v => 4, :value => { :edgeclass => "" }}, 48 | { :u => 4, :v => 2, :value => { :edgeclass => "" }}, 49 | { :u => 4, :v => 5, :value => { :edgeclass => "" }}, 50 | { :u => 2, :v => 6, :value => { :edgeclass => "" }}] 51 | 52 | @bug_transition_manager = Marshal.load(Marshal.dump(@bug_transition_all)) 53 | @bug_transition_manager.each {|x| x[:value][:edgeclass] = 'transOwn' } 54 | 55 | @bug_transition_reporter = [ 56 | { :u => 1, :v => 2, :value => { :edgeclass => "transOther" }}, 57 | { :u => 2, :v => 1, :value => { :edgeclass => "transOther" }}, 58 | { :u => 2, :v => 3, :value => { :edgeclass => "transOwn transOwn-assignee" }}, 59 | { :u => 3, :v => 2, :value => { :edgeclass => "transOther" }}, 60 | { :u => 3, :v => 5, :value => { :edgeclass => "transOther" }}, 61 | { :u => 3, :v => 4, :value => { :edgeclass => "transOther" }}, 62 | { :u => 4, :v => 2, :value => { :edgeclass => "transOwn transOwn-author" }}, 63 | { :u => 4, :v => 5, :value => { :edgeclass => "transOwn transOwn-author" }}, 64 | { :u => 2, :v => 6, :value => { :edgeclass => "transOther" }}] 65 | 66 | @bug_transition_nonmember = Marshal.load(Marshal.dump(@bug_transition_all)) 67 | @bug_transition_nonmember.each {|x| x[:value][:edgeclass] = 'transOther' } 68 | end 69 | 70 | def test_empty_data 71 | result = WorkflowEnhancements::Graph.load_data(nil, nil) 72 | assert_equal({ :nodes => [], :edges => [] }, result) 73 | end 74 | 75 | def test_empty_data_array 76 | result = WorkflowEnhancements::Graph.load_data([], []) 77 | assert_equal({ :nodes => [], :edges => [] }, result) 78 | end 79 | 80 | def test_multiple_trackers 81 | #trackers = Tracker.all 82 | #result = WorkflowEnhancements::Graph.load_data(nil, trackers) 83 | #assert_equal({ :nodes => [], :edges => [] }, result) 84 | end 85 | 86 | def test_with_tracker_bug_all 87 | result = WorkflowEnhancements::Graph.load_data(nil, @tracker_bug) 88 | assert result.has_key? :nodes 89 | assert result.has_key? :edges 90 | assert_equal @bug_states, result[:nodes] 91 | assert_equal @bug_transition_all, result[:edges] 92 | end 93 | 94 | def test_with_tracker_bug_all_empty_array 95 | result = WorkflowEnhancements::Graph.load_data([], @tracker_bug) 96 | assert_equal @bug_states, result[:nodes] 97 | assert_equal @bug_transition_all, result[:edges] 98 | end 99 | 100 | def test_with_tracker_bug_manager 101 | result = WorkflowEnhancements::Graph.load_data(@role_manager, @tracker_bug) 102 | assert_equal @bug_states, result[:nodes] 103 | assert_equal @bug_transition_manager, result[:edges] 104 | end 105 | 106 | def test_with_tracker_bug_manager_issue 107 | User.current = User.find(2) # manager 108 | 109 | result = WorkflowEnhancements::Graph.load_data(@role_manager, @tracker_bug, @issue) 110 | Rails.logger.warn result[:nodes].inspect 111 | assert_equal @bug_states_issue, result[:nodes] 112 | assert_equal @bug_transition_manager, result[:edges] 113 | end 114 | 115 | def test_with_tracker_bug_reporter 116 | result = WorkflowEnhancements::Graph.load_data(@role_reporter, @tracker_bug) 117 | assert_equal @bug_states, result[:nodes] 118 | assert_equal @bug_transition_reporter, result[:edges] 119 | end 120 | 121 | def test_with_tracker_bug_nonmember 122 | result = WorkflowEnhancements::Graph.load_data(@role_nonmember, @tracker_bug) 123 | assert_equal @bug_states, result[:nodes] 124 | assert_equal @bug_transition_nonmember, result[:edges] 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /test/fixtures/issues.yml: -------------------------------------------------------------------------------- 1 | --- 2 | issues_001: 3 | created_on: <%= 3.days.ago.to_s(:db) %> 4 | project_id: 1 5 | updated_on: <%= 1.day.ago.to_s(:db) %> 6 | priority_id: 4 7 | subject: Can't print recipes 8 | id: 1 9 | fixed_version_id: 10 | category_id: 1 11 | description: Unable to print recipes 12 | tracker_id: 1 13 | assigned_to_id: 14 | author_id: 2 15 | status_id: 1 16 | start_date: <%= 1.day.ago.to_date.to_s(:db) %> 17 | due_date: <%= 10.day.from_now.to_date.to_s(:db) %> 18 | root_id: 1 19 | lft: 1 20 | rgt: 2 21 | lock_version: 3 22 | issues_002: 23 | created_on: 2006-07-19 21:04:21 +02:00 24 | project_id: 1 25 | updated_on: 2006-07-19 21:09:50 +02:00 26 | priority_id: 5 27 | subject: Add ingredients categories 28 | id: 2 29 | fixed_version_id: 2 30 | category_id: 31 | description: Ingredients of the recipe should be classified by categories 32 | tracker_id: 2 33 | assigned_to_id: 3 34 | author_id: 2 35 | status_id: 2 36 | start_date: <%= 2.day.ago.to_date.to_s(:db) %> 37 | due_date: 38 | root_id: 2 39 | lft: 1 40 | rgt: 2 41 | lock_version: 3 42 | done_ratio: 30 43 | issues_003: 44 | created_on: 2006-07-19 21:07:27 +02:00 45 | project_id: 1 46 | updated_on: 2006-07-19 21:07:27 +02:00 47 | priority_id: 4 48 | subject: Error 281 when updating a recipe 49 | id: 3 50 | fixed_version_id: 51 | category_id: 52 | description: Error 281 is encountered when saving a recipe 53 | tracker_id: 1 54 | assigned_to_id: 3 55 | author_id: 2 56 | status_id: 1 57 | start_date: <%= 15.day.ago.to_date.to_s(:db) %> 58 | due_date: <%= 5.day.ago.to_date.to_s(:db) %> 59 | root_id: 3 60 | lft: 1 61 | rgt: 2 62 | issues_004: 63 | created_on: <%= 5.days.ago.to_s(:db) %> 64 | project_id: 2 65 | updated_on: <%= 2.days.ago.to_s(:db) %> 66 | priority_id: 4 67 | subject: Issue on project 2 68 | id: 4 69 | fixed_version_id: 70 | category_id: 71 | description: Issue on project 2 72 | tracker_id: 1 73 | assigned_to_id: 2 74 | author_id: 2 75 | status_id: 1 76 | root_id: 4 77 | lft: 1 78 | rgt: 2 79 | issues_005: 80 | created_on: <%= 5.days.ago.to_s(:db) %> 81 | project_id: 3 82 | updated_on: <%= 2.days.ago.to_s(:db) %> 83 | priority_id: 4 84 | subject: Subproject issue 85 | id: 5 86 | fixed_version_id: 87 | category_id: 88 | description: This is an issue on a cookbook subproject 89 | tracker_id: 1 90 | assigned_to_id: 91 | author_id: 2 92 | status_id: 1 93 | root_id: 5 94 | lft: 1 95 | rgt: 2 96 | issues_006: 97 | created_on: <%= 1.minute.ago.to_s(:db) %> 98 | project_id: 5 99 | updated_on: <%= 1.minute.ago.to_s(:db) %> 100 | priority_id: 4 101 | subject: Issue of a private subproject 102 | id: 6 103 | fixed_version_id: 104 | category_id: 105 | description: This is an issue of a private subproject of cookbook 106 | tracker_id: 1 107 | assigned_to_id: 108 | author_id: 2 109 | status_id: 1 110 | start_date: <%= Date.today.to_s(:db) %> 111 | due_date: <%= 1.days.from_now.to_date.to_s(:db) %> 112 | root_id: 6 113 | lft: 1 114 | rgt: 2 115 | issues_007: 116 | created_on: <%= 10.days.ago.to_s(:db) %> 117 | project_id: 1 118 | updated_on: <%= 10.days.ago.to_s(:db) %> 119 | priority_id: 5 120 | subject: Issue due today 121 | id: 7 122 | fixed_version_id: 123 | category_id: 124 | description: This is an issue that is due today 125 | tracker_id: 1 126 | assigned_to_id: 127 | author_id: 2 128 | status_id: 1 129 | start_date: <%= 10.days.ago.to_s(:db) %> 130 | due_date: <%= Date.today.to_s(:db) %> 131 | lock_version: 0 132 | root_id: 7 133 | lft: 1 134 | rgt: 2 135 | issues_008: 136 | created_on: <%= 10.days.ago.to_s(:db) %> 137 | project_id: 1 138 | updated_on: <%= 10.days.ago.to_s(:db) %> 139 | priority_id: 5 140 | subject: Closed issue 141 | id: 8 142 | fixed_version_id: 143 | category_id: 144 | description: This is a closed issue. 145 | tracker_id: 1 146 | assigned_to_id: 147 | author_id: 2 148 | status_id: 5 149 | start_date: 150 | due_date: 151 | lock_version: 0 152 | root_id: 8 153 | lft: 1 154 | rgt: 2 155 | closed_on: <%= 3.days.ago.to_s(:db) %> 156 | issues_009: 157 | created_on: <%= 1.minute.ago.to_s(:db) %> 158 | project_id: 5 159 | updated_on: <%= 1.minute.ago.to_s(:db) %> 160 | priority_id: 5 161 | subject: Blocked Issue 162 | id: 9 163 | fixed_version_id: 164 | category_id: 165 | description: This is an issue that is blocked by issue #10 166 | tracker_id: 1 167 | assigned_to_id: 168 | author_id: 2 169 | status_id: 1 170 | start_date: <%= Date.today.to_s(:db) %> 171 | due_date: <%= 1.days.from_now.to_date.to_s(:db) %> 172 | root_id: 9 173 | lft: 1 174 | rgt: 2 175 | issues_010: 176 | created_on: <%= 1.minute.ago.to_s(:db) %> 177 | project_id: 5 178 | updated_on: <%= 1.minute.ago.to_s(:db) %> 179 | priority_id: 5 180 | subject: Issue Doing the Blocking 181 | id: 10 182 | fixed_version_id: 183 | category_id: 184 | description: This is an issue that blocks issue #9 185 | tracker_id: 1 186 | assigned_to_id: 187 | author_id: 2 188 | status_id: 1 189 | start_date: <%= Date.today.to_s(:db) %> 190 | due_date: <%= 1.days.from_now.to_date.to_s(:db) %> 191 | root_id: 10 192 | lft: 1 193 | rgt: 2 194 | issues_011: 195 | created_on: <%= 3.days.ago.to_s(:db) %> 196 | project_id: 1 197 | updated_on: <%= 1.day.ago.to_s(:db) %> 198 | priority_id: 5 199 | subject: Closed issue on a closed version 200 | id: 11 201 | fixed_version_id: 1 202 | category_id: 1 203 | description: 204 | tracker_id: 1 205 | assigned_to_id: 206 | author_id: 2 207 | status_id: 5 208 | start_date: <%= 1.day.ago.to_date.to_s(:db) %> 209 | due_date: 210 | root_id: 11 211 | lft: 1 212 | rgt: 2 213 | closed_on: <%= 1.day.ago.to_s(:db) %> 214 | issues_012: 215 | created_on: <%= 3.days.ago.to_s(:db) %> 216 | project_id: 1 217 | updated_on: <%= 1.day.ago.to_s(:db) %> 218 | priority_id: 5 219 | subject: Closed issue on a locked version 220 | id: 12 221 | fixed_version_id: 2 222 | category_id: 1 223 | description: 224 | tracker_id: 1 225 | assigned_to_id: 226 | author_id: 3 227 | status_id: 5 228 | start_date: <%= 1.day.ago.to_date.to_s(:db) %> 229 | due_date: 230 | root_id: 12 231 | lft: 1 232 | rgt: 2 233 | closed_on: <%= 1.day.ago.to_s(:db) %> 234 | issues_013: 235 | created_on: <%= 5.days.ago.to_s(:db) %> 236 | project_id: 3 237 | updated_on: <%= 2.days.ago.to_s(:db) %> 238 | priority_id: 4 239 | subject: Subproject issue two 240 | id: 13 241 | fixed_version_id: 242 | category_id: 243 | description: This is a second issue on a cookbook subproject 244 | tracker_id: 1 245 | assigned_to_id: 246 | author_id: 2 247 | status_id: 1 248 | root_id: 13 249 | lft: 1 250 | rgt: 2 251 | issues_014: 252 | id: 14 253 | created_on: <%= 15.days.ago.to_s(:db) %> 254 | project_id: 3 255 | updated_on: <%= 15.days.ago.to_s(:db) %> 256 | priority_id: 5 257 | subject: Private issue on public project 258 | fixed_version_id: 259 | category_id: 260 | description: This is a private issue 261 | tracker_id: 1 262 | assigned_to_id: 263 | author_id: 2 264 | status_id: 1 265 | is_private: true 266 | root_id: 14 267 | lft: 1 268 | rgt: 2 269 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | 2 | GNU GENERAL PUBLIC LICENSE 3 | Version 2, June 1991 4 | 5 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 6 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 7 | Everyone is permitted to copy and distribute verbatim copies 8 | of this license document, but changing it is not allowed. 9 | 10 | Preamble 11 | 12 | The licenses for most software are designed to take away your 13 | freedom to share and change it. By contrast, the GNU General Public 14 | License is intended to guarantee your freedom to share and change free 15 | software--to make sure the software is free for all its users. This 16 | General Public License applies to most of the Free Software 17 | Foundation's software and to any other program whose authors commit to 18 | using it. (Some other Free Software Foundation software is covered by 19 | the GNU Lesser General Public License instead.) You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | this service if you wish), that you receive source code or can get it 26 | if you want it, that you can change the software or use pieces of it 27 | in new free programs; and that you know you can do these things. 28 | 29 | To protect your rights, we need to make restrictions that forbid 30 | anyone to deny you these rights or to ask you to surrender the rights. 31 | These restrictions translate to certain responsibilities for you if you 32 | distribute copies of the software, or if you modify it. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must give the recipients all the rights that 36 | you have. You must make sure that they, too, receive or can get the 37 | source code. And you must show them these terms so they know their 38 | rights. 39 | 40 | We protect your rights with two steps: (1) copyright the software, and 41 | (2) offer you this license which gives you legal permission to copy, 42 | distribute and/or modify the software. 43 | 44 | Also, for each author's protection and ours, we want to make certain 45 | that everyone understands that there is no warranty for this free 46 | software. If the software is modified by someone else and passed on, we 47 | want its recipients to know that what they have is not the original, so 48 | that any problems introduced by others will not reflect on the original 49 | authors' reputations. 50 | 51 | Finally, any free program is threatened constantly by software 52 | patents. We wish to avoid the danger that redistributors of a free 53 | program will individually obtain patent licenses, in effect making the 54 | program proprietary. To prevent this, we have made it clear that any 55 | patent must be licensed for everyone's free use or not licensed at all. 56 | 57 | The precise terms and conditions for copying, distribution and 58 | modification follow. 59 | 60 | GNU GENERAL PUBLIC LICENSE 61 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 62 | 63 | 0. This License applies to any program or other work which contains 64 | a notice placed by the copyright holder saying it may be distributed 65 | under the terms of this General Public License. The "Program", below, 66 | refers to any such program or work, and a "work based on the Program" 67 | means either the Program or any derivative work under copyright law: 68 | that is to say, a work containing the Program or a portion of it, 69 | either verbatim or with modifications and/or translated into another 70 | language. (Hereinafter, translation is included without limitation in 71 | the term "modification".) Each licensee is addressed as "you". 72 | 73 | Activities other than copying, distribution and modification are not 74 | covered by this License; they are outside its scope. The act of 75 | running the Program is not restricted, and the output from the Program 76 | is covered only if its contents constitute a work based on the 77 | Program (independent of having been made by running the Program). 78 | Whether that is true depends on what the Program does. 79 | 80 | 1. You may copy and distribute verbatim copies of the Program's 81 | source code as you receive it, in any medium, provided that you 82 | conspicuously and appropriately publish on each copy an appropriate 83 | copyright notice and disclaimer of warranty; keep intact all the 84 | notices that refer to this License and to the absence of any warranty; 85 | and give any other recipients of the Program a copy of this License 86 | along with the Program. 87 | 88 | You may charge a fee for the physical act of transferring a copy, and 89 | you may at your option offer warranty protection in exchange for a fee. 90 | 91 | 2. You may modify your copy or copies of the Program or any portion 92 | of it, thus forming a work based on the Program, and copy and 93 | distribute such modifications or work under the terms of Section 1 94 | above, provided that you also meet all of these conditions: 95 | 96 | a) You must cause the modified files to carry prominent notices 97 | stating that you changed the files and the date of any change. 98 | 99 | b) You must cause any work that you distribute or publish, that in 100 | whole or in part contains or is derived from the Program or any 101 | part thereof, to be licensed as a whole at no charge to all third 102 | parties under the terms of this License. 103 | 104 | c) If the modified program normally reads commands interactively 105 | when run, you must cause it, when started running for such 106 | interactive use in the most ordinary way, to print or display an 107 | announcement including an appropriate copyright notice and a 108 | notice that there is no warranty (or else, saying that you provide 109 | a warranty) and that users may redistribute the program under 110 | these conditions, and telling the user how to view a copy of this 111 | License. (Exception: if the Program itself is interactive but 112 | does not normally print such an announcement, your work based on 113 | the Program is not required to print an announcement.) 114 | 115 | These requirements apply to the modified work as a whole. If 116 | identifiable sections of that work are not derived from the Program, 117 | and can be reasonably considered independent and separate works in 118 | themselves, then this License, and its terms, do not apply to those 119 | sections when you distribute them as separate works. But when you 120 | distribute the same sections as part of a whole which is a work based 121 | on the Program, the distribution of the whole must be on the terms of 122 | this License, whose permissions for other licensees extend to the 123 | entire whole, and thus to each and every part regardless of who wrote it. 124 | 125 | Thus, it is not the intent of this section to claim rights or contest 126 | your rights to work written entirely by you; rather, the intent is to 127 | exercise the right to control the distribution of derivative or 128 | collective works based on the Program. 129 | 130 | In addition, mere aggregation of another work not based on the Program 131 | with the Program (or with a work based on the Program) on a volume of 132 | a storage or distribution medium does not bring the other work under 133 | the scope of this License. 134 | 135 | 3. You may copy and distribute the Program (or a work based on it, 136 | under Section 2) in object code or executable form under the terms of 137 | Sections 1 and 2 above provided that you also do one of the following: 138 | 139 | a) Accompany it with the complete corresponding machine-readable 140 | source code, which must be distributed under the terms of Sections 141 | 1 and 2 above on a medium customarily used for software interchange; or, 142 | 143 | b) Accompany it with a written offer, valid for at least three 144 | years, to give any third party, for a charge no more than your 145 | cost of physically performing source distribution, a complete 146 | machine-readable copy of the corresponding source code, to be 147 | distributed under the terms of Sections 1 and 2 above on a medium 148 | customarily used for software interchange; or, 149 | 150 | c) Accompany it with the information you received as to the offer 151 | to distribute corresponding source code. (This alternative is 152 | allowed only for noncommercial distribution and only if you 153 | received the program in object code or executable form with such 154 | an offer, in accord with Subsection b above.) 155 | 156 | The source code for a work means the preferred form of the work for 157 | making modifications to it. For an executable work, complete source 158 | code means all the source code for all modules it contains, plus any 159 | associated interface definition files, plus the scripts used to 160 | control compilation and installation of the executable. However, as a 161 | special exception, the source code distributed need not include 162 | anything that is normally distributed (in either source or binary 163 | form) with the major components (compiler, kernel, and so on) of the 164 | operating system on which the executable runs, unless that component 165 | itself accompanies the executable. 166 | 167 | If distribution of executable or object code is made by offering 168 | access to copy from a designated place, then offering equivalent 169 | access to copy the source code from the same place counts as 170 | distribution of the source code, even though third parties are not 171 | compelled to copy the source along with the object code. 172 | 173 | 4. You may not copy, modify, sublicense, or distribute the Program 174 | except as expressly provided under this License. Any attempt 175 | otherwise to copy, modify, sublicense or distribute the Program is 176 | void, and will automatically terminate your rights under this License. 177 | However, parties who have received copies, or rights, from you under 178 | this License will not have their licenses terminated so long as such 179 | parties remain in full compliance. 180 | 181 | 5. You are not required to accept this License, since you have not 182 | signed it. However, nothing else grants you permission to modify or 183 | distribute the Program or its derivative works. These actions are 184 | prohibited by law if you do not accept this License. Therefore, by 185 | modifying or distributing the Program (or any work based on the 186 | Program), you indicate your acceptance of this License to do so, and 187 | all its terms and conditions for copying, distributing or modifying 188 | the Program or works based on it. 189 | 190 | 6. Each time you redistribute the Program (or any work based on the 191 | Program), the recipient automatically receives a license from the 192 | original licensor to copy, distribute or modify the Program subject to 193 | these terms and conditions. You may not impose any further 194 | restrictions on the recipients' exercise of the rights granted herein. 195 | You are not responsible for enforcing compliance by third parties to 196 | this License. 197 | 198 | 7. If, as a consequence of a court judgment or allegation of patent 199 | infringement or for any other reason (not limited to patent issues), 200 | conditions are imposed on you (whether by court order, agreement or 201 | otherwise) that contradict the conditions of this License, they do not 202 | excuse you from the conditions of this License. If you cannot 203 | distribute so as to satisfy simultaneously your obligations under this 204 | License and any other pertinent obligations, then as a consequence you 205 | may not distribute the Program at all. For example, if a patent 206 | license would not permit royalty-free redistribution of the Program by 207 | all those who receive copies directly or indirectly through you, then 208 | the only way you could satisfy both it and this License would be to 209 | refrain entirely from distribution of the Program. 210 | 211 | If any portion of this section is held invalid or unenforceable under 212 | any particular circumstance, the balance of the section is intended to 213 | apply and the section as a whole is intended to apply in other 214 | circumstances. 215 | 216 | It is not the purpose of this section to induce you to infringe any 217 | patents or other property right claims or to contest validity of any 218 | such claims; this section has the sole purpose of protecting the 219 | integrity of the free software distribution system, which is 220 | implemented by public license practices. Many people have made 221 | generous contributions to the wide range of software distributed 222 | through that system in reliance on consistent application of that 223 | system; it is up to the author/donor to decide if he or she is willing 224 | to distribute software through any other system and a licensee cannot 225 | impose that choice. 226 | 227 | This section is intended to make thoroughly clear what is believed to 228 | be a consequence of the rest of this License. 229 | 230 | 8. If the distribution and/or use of the Program is restricted in 231 | certain countries either by patents or by copyrighted interfaces, the 232 | original copyright holder who places the Program under this License 233 | may add an explicit geographical distribution limitation excluding 234 | those countries, so that distribution is permitted only in or among 235 | countries not thus excluded. In such case, this License incorporates 236 | the limitation as if written in the body of this License. 237 | 238 | 9. The Free Software Foundation may publish revised and/or new versions 239 | of the General Public License from time to time. Such new versions will 240 | be similar in spirit to the present version, but may differ in detail to 241 | address new problems or concerns. 242 | 243 | Each version is given a distinguishing version number. If the Program 244 | specifies a version number of this License which applies to it and "any 245 | later version", you have the option of following the terms and conditions 246 | either of that version or of any later version published by the Free 247 | Software Foundation. If the Program does not specify a version number of 248 | this License, you may choose any version ever published by the Free Software 249 | Foundation. 250 | 251 | 10. If you wish to incorporate parts of the Program into other free 252 | programs whose distribution conditions are different, write to the author 253 | to ask for permission. For software which is copyrighted by the Free 254 | Software Foundation, write to the Free Software Foundation; we sometimes 255 | make exceptions for this. Our decision will be guided by the two goals 256 | of preserving the free status of all derivatives of our free software and 257 | of promoting the sharing and reuse of software generally. 258 | 259 | NO WARRANTY 260 | 261 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 262 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 263 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 264 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 265 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 266 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 267 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 268 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 269 | REPAIR OR CORRECTION. 270 | 271 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 272 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 273 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 274 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 275 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 276 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 277 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 278 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 279 | POSSIBILITY OF SUCH DAMAGES. 280 | 281 | END OF TERMS AND CONDITIONS 282 | 283 | How to Apply These Terms to Your New Programs 284 | 285 | If you develop a new program, and you want it to be of the greatest 286 | possible use to the public, the best way to achieve this is to make it 287 | free software which everyone can redistribute and change under these terms. 288 | 289 | To do so, attach the following notices to the program. It is safest 290 | to attach them to the start of each source file to most effectively 291 | convey the exclusion of warranty; and each file should have at least 292 | the "copyright" line and a pointer to where the full notice is found. 293 | 294 | 295 | Copyright (C) 296 | 297 | This program is free software; you can redistribute it and/or modify 298 | it under the terms of the GNU General Public License as published by 299 | the Free Software Foundation; either version 2 of the License, or 300 | (at your option) any later version. 301 | 302 | This program is distributed in the hope that it will be useful, 303 | but WITHOUT ANY WARRANTY; without even the implied warranty of 304 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 305 | GNU General Public License for more details. 306 | 307 | You should have received a copy of the GNU General Public License along 308 | with this program; if not, write to the Free Software Foundation, Inc., 309 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 310 | 311 | Also add information on how to contact you by electronic and paper mail. 312 | 313 | If the program is interactive, make it output a short notice like this 314 | when it starts in an interactive mode: 315 | 316 | Gnomovision version 69, Copyright (C) year name of author 317 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 318 | This is free software, and you are welcome to redistribute it 319 | under certain conditions; type `show c' for details. 320 | 321 | The hypothetical commands `show w' and `show c' should show the appropriate 322 | parts of the General Public License. Of course, the commands you use may 323 | be called something other than `show w' and `show c'; they could even be 324 | mouse-clicks or menu items--whatever suits your program. 325 | 326 | You should also get your employer (if you work as a programmer) or your 327 | school, if any, to sign a "copyright disclaimer" for the program, if 328 | necessary. Here is a sample; alter the names: 329 | 330 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 331 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 332 | 333 | , 1 April 1989 334 | Ty Coon, President of Vice 335 | 336 | This General Public License does not permit incorporating your program into 337 | proprietary programs. If your program is a subroutine library, you may 338 | consider it more useful to permit linking proprietary applications with the 339 | library. If this is what you want to do, use the GNU Lesser General 340 | Public License instead of this License. 341 | -------------------------------------------------------------------------------- /assets/javascripts/dagre-d3.min.js: -------------------------------------------------------------------------------- 1 | (function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var j=typeof require=="function"&&require;if(!h&&j)return j(g,!0);if(f)return f(g,!0);throw new Error("Cannot find module '"+g+"'")}var k=c[g]={exports:{}};b[g][0].call(k.exports,function(a){var c=b[g][1][a];return e(c?c:a)},k,k.exports,a,b,c,d)}return c[g].exports}var f=typeof require=="function"&&require;for(var g=0;gMath.abs(e)*h?(f<0&&(h=-h),i=f===0?0:h*e/f,j=h):(e<0&&(g=-g),i=g,j=e===0?0:g*f/e),{x:c+i,y:d+j}}function B(a,b){return"children"in a&&a.children(b).length}function C(a,b){return a.bind?a.bind(b):function(){return a.apply(b,arguments)}}var d=a("dagre").layout,e;try{e=a("d3")}catch(f){e=window.d3}b.exports=g,g.prototype.layout=function(a){return arguments.length?(this._layout=a,this):this._layout},g.prototype.drawNodes=function(a){return arguments.length?(this._drawNodes=C(a,this),this):this._drawNodes},g.prototype.drawEdgeLabels=function(a){return arguments.length?(this._drawEdgeLabels=C(a,this),this):this._drawEdgeLabels},g.prototype.drawEdgePaths=function(a){return arguments.length?(this._drawEdgePaths=C(a,this),this):this._drawEdgePaths},g.prototype.positionNodes=function(a){return arguments.length?(this._positionNodes=C(a,this),this):this._positionNodes},g.prototype.positionEdgeLabels=function(a){return arguments.length?(this._positionEdgeLabels=C(a,this),this):this._positionEdgeLabels},g.prototype.positionEdgePaths=function(a){return arguments.length?(this._positionEdgePaths=C(a,this),this):this._positionEdgePaths},g.prototype.transition=function(a){return arguments.length?(this._transition=C(a,this),this):this._transition},g.prototype.zoomSetup=function(a){return arguments.length?(this._zoomSetup=C(a,this),this):this._zoomSetup},g.prototype.zoom=function(a){return arguments.length?(a?this._zoom=C(a,this):delete this._zoom,this):this._zoom},g.prototype.postLayout=function(a){return arguments.length?(this._postLayout=C(a,this),this):this._postLayout},g.prototype.postRender=function(a){return arguments.length?(this._postRender=C(a,this),this):this._postRender},g.prototype.edgeInterpolate=function(a){return arguments.length?(this._edgeInterpolate=a,this):this._edgeInterpolate},g.prototype.edgeTension=function(a){return arguments.length?(this._edgeTension=a,this):this._edgeTension},g.prototype.run=function(a,b){a=h(a),b=this._zoomSetup(a,b),b.selectAll("g.edgePaths, g.edgeLabels, g.nodes").data(["edgePaths","edgeLabels","nodes"]).enter().append("g").attr("class",function(a){return a});var c=this._drawNodes(a,b.select("g.nodes")),d=this._drawEdgeLabels(a,b.select("g.edgeLabels"));c.each(function(b){i(this,a.node(b))}),d.each(function(b){i(this,a.edge(b))});var e=j(a,this._layout);this._postLayout(e,b);var f=this._drawEdgePaths(a,b.select("g.edgePaths"));return this._positionNodes(e,c),this._positionEdgeLabels(e,d),this._positionEdgePaths(e,f),this._postRender(e,b),e};var m=function(a,b){var c=b.selectAll("g.edgePath").classed("enter",!1).data(a.edges(),function(a){return a});return c.enter().append("g").attr("class","edgePath enter").append("path").style("opacity",0).attr("marker-end","url(#arrowhead)"),this._transition(c.exit()).style("opacity",0).remove(),c}},{d3:10,dagre:11}],4:[function(a,b,c){b.exports="0.2.3"},{}],5:[function(a,b,c){c.Set=a("./lib/Set"),c.PriorityQueue=a("./lib/PriorityQueue"),c.version=a("./lib/version")},{"./lib/PriorityQueue":6,"./lib/Set":7,"./lib/version":9}],6:[function(a,b,c){function d(){this._arr=[],this._keyIndices={}}b.exports=d,d.prototype.size=function(){return this._arr.length},d.prototype.keys=function(){return this._arr.map(function(a){return a.key})},d.prototype.has=function(a){return a in this._keyIndices},d.prototype.priority=function(a){var b=this._keyIndices[a];if(b!==undefined)return this._arr[b].priority},d.prototype.min=function(){if(this.size()===0)throw new Error("Queue underflow");return this._arr[0].key},d.prototype.add=function(a,b){var c=this._keyIndices;if(a in c)return!1;var d=this._arr,e=d.length;return c[a]=e,d.push({key:a,priority:b}),this._decrease(e),!0},d.prototype.removeMin=function(){this._swap(0,this._arr.length-1);var a=this._arr.pop();return delete this._keyIndices[a.key],this._heapify(0),a.key},d.prototype.decrease=function(a,b){var c=this._keyIndices[a];if(b>this._arr[c].priority)throw new Error("New priority is greater than current priority. Key: "+a+" Old: "+this._arr[c].priority+" New: "+b);this._arr[c].priority=b,this._decrease(c)},d.prototype._heapify=function(a){var b=this._arr,c=2*a,d=c+1,e=a;c>1;if(b[d].priority>>0,g=!1;1d;++d)a.hasOwnProperty(d)&&(g?e=b(e,a[d],d,a):(e=a[d],g=!0));if(!g)throw new TypeError("Reduce of empty array with no initial value");return e}:c.reduce=function(a,b,c){return a.reduce(b,c)}},{}],9:[function(a,b,c){b.exports="1.1.3"},{}],10:[function(a,b,c){a("./d3"),b.exports=d3,function(){delete this.d3}()},{}],11:[function(a,b,c){c.Digraph=a("graphlib").Digraph,c.Graph=a("graphlib").Graph,c.layout=a("./lib/layout"),c.version=a("./lib/version")},{"./lib/layout":12,"./lib/version":27,graphlib:28}],12:[function(a,b,c){var d=a("./util"),e=a("./rank"),f=a("./order"),g=a("graphlib").CGraph,h=a("graphlib").CDigraph;b.exports=function(){function j(a){var c=new h;a.eachNode(function(a,b){b===undefined&&(b={}),c.addNode(a,{width:b.width,height:b.height}),b.hasOwnProperty("rank")&&(c.node(a).prefRank=b.rank)}),a.parent&&a.nodes().forEach(function(b){c.parent(b,a.parent(b))}),a.eachEdge(function(a,b,d,e){e===undefined&&(e={});var f={e:a,minLen:e.minLen||1,width:e.width||0,height:e.height||0,points:[]};c.addEdge(null,b,d,f)});var d=a.graph()||{};return c.graph({rankDir:d.rankDir||b.rankDir,orderRestarts:d.orderRestarts}),c}function k(a){var g=i.rankSep(),h;try{return h=d.time("initLayoutGraph",j)(a),h.order()===0?h:(h.eachEdge(function(a,b,c,d){d.minLen*=2}),i.rankSep(g/2),d.time("rank.run",e.run)(h,b.rankSimplex),d.time("normalize",l)(h),d.time("order",f)(h,b.orderMaxSweeps),d.time("position",c.run)(h),d.time("undoNormalize",m)(h),d.time("fixupEdgePoints",n)(h),d.time("rank.restoreEdges",e.restoreEdges)(h),d.time("createFinalGraph",o)(h,a.isDirected()))}finally{i.rankSep(g)}}function l(a){var b=0;a.eachEdge(function(c,d,e,f){var g=a.node(d).rank,h=a.node(e).rank;if(g+10),d.log(2,"Order phase start cross count: "+a.graph().orderInitCC);var q,r,s;for(q=0,r=0;r<4&&q0;++q,++r,++i)n(a,h,q),s=e(a),s=0;--i)h(b[i],c,m(a,b[i].nodes()))}var d=a("./util"),e=a("./order/crossCount"),f=a("./order/initLayerGraphs"),g=a("./order/initOrder"),h=a("./order/sortLayer");b.exports=k;var j=24;k.DEFAULT_MAX_SWEEPS=j},{"./order/crossCount":14,"./order/initLayerGraphs":15,"./order/initOrder":16,"./order/sortLayer":17,"./util":26}],14:[function(a,b,c){function e(a){var b=0,c=d.ordering(a);for(var e=1;e0)b%2&&(i+=g[b+1]),b=b-1>>1,++g[b]}),i}var d=a("../util");b.exports=e},{"../util":26}],15:[function(a,b,c){function f(a){function c(d){if(d===null){a.children(d).forEach(function(a){c(a)});return}var f=a.node(d);f.minRank="rank"in f?f.rank:Number.MAX_VALUE,f.maxRank="rank"in f?f.rank:Number.MIN_VALUE;var h=new e;return a.children(d).forEach(function(b){var d=c(b);h=e.union([h,d]),f.minRank=Math.min(f.minRank,a.node(b).minRank),f.maxRank=Math.max(f.maxRank,a.node(b).maxRank)}),"rank"in f&&h.add(f.rank),h.keys().forEach(function(a){a in b||(b[a]=[]),b[a].push(d)}),h}var b=[];c(null);var f=[];return b.forEach(function(b,c){f[c]=a.filterNodes(d(b))}),f}var d=a("graphlib").filter.nodesFromList,e=a("cp-data").Set;b.exports=f},{"cp-data":5,graphlib:28}],16:[function(a,b,c){function f(a,b){var c=[];a.eachNode(function(b,d){var e=c[d.rank];if(a.children&&a.children(b).length>0)return;e||(e=c[d.rank]=[]),e.push(b)}),c.forEach(function(c){b&&e.shuffle(c),c.forEach(function(b,c){a.node(b).order=c})});var f=d(a);a.graph().orderInitCC=f,a.graph().orderCC=Number.MAX_VALUE}var d=a("./crossCount"),e=a("../util");b.exports=f},{"../util":26,"./crossCount":14}],17:[function(a,b,c){function e(a,b,c){var e=[],f={};a.eachNode(function(a,b){e[b.order]=a;var g=c[a];g.length&&(f[a]=d.sum(g)/g.length)});var g=a.nodes().filter(function(a){return f[a]!==undefined});g.sort(function(b,c){return f[b]-f[c]||a.node(b).order-a.node(c).order});for(var h=0,i=0,j=g.length;i=3&&t(d+f,b,c,i[j]),f==="r"&&l(i[j]),f==="r"&&m(c)}),d==="d"&&c.reverse()}),k(b,c,i),b.eachNode(function(a){var c=[];for(var d in i){var e=i[d][a];r(d,b,a,e),c.push(e)}c.sort(function(a,b){return a-b}),q(b,a,(c[1]+c[2])/2)});var j=0,p=b.graph().rankDir==="BT"||b.graph().rankDir==="RL";c.forEach(function(c){var e=d.max(c.map(function(a){return o(b,a)}));j+=e/2,c.forEach(function(a){s(b,a,p?-j:j)}),j+=e/2+a.rankSep});var u=d.min(b.nodes().map(function(a){return q(b,a)-n(b,a)/2})),v=d.min(b.nodes().map(function(a){return s(b,a)-o(b,a)/2}));b.eachNode(function(a){q(b,a,q(b,a)-u),s(b,a,s(b,a)-v)})}function e(a,b){return aj)c[e(g[i],a)]=!0}var c={},d={},f,g,h,i,j;if(b.length<=2)return c;b[1].forEach(function(a,b){d[a]=b});for(var l=1;l0&&(j.sort(function(a,b){return f[a]-f[b]}),k=(j.length-1)/2,j.slice(Math.floor(k),Math.ceil(k)+1).forEach(function(a){h[b]===b&&!c[e(a,b)]&&i0){var h=e[j[d]];m(h),g[b]===b&&(g[b]=g[h]);var i=p(a,j[d])+p(a,d);g[b]!==g[h]?l(g[h],g[b],k[b]-k[h]-i):k[b]=Math.max(k[b],k[h]+i)}d=f[d]}while(d!==b)}}var g={},h={},i={},j={},k={};return b.forEach(function(a){a.forEach(function(b,c){g[b]=b,h[b]={},c>0&&(j[b]=a[c-1])})}),d.values(e).forEach(function(a){m(a)}),b.forEach(function(a){a.forEach(function(a){k[a]=k[e[a]];if(a===e[a]&&a===g[a]){var b=0;a in h&&Object.keys(h[a]).length>0&&(b=d.min(Object.keys(h[a]).map(function(b){return h[a][b]+(b in i?i[b]:0)}))),i[a]=b}})}),b.forEach(function(a){a.forEach(function(a){k[a]+=i[g[e[a]]]||0})}),k}function i(a,b,c){return d.min(b.map(function(a){var b=a[0];return c[b]}))}function j(a,b,c){return d.max(b.map(function(a){var b=a[a.length-1];return c[b]}))}function k(a,b,c){function h(a){c[l][a]+=g[l]}var d={},e={},f,g={},k=Number.POSITIVE_INFINITY;for(var l in c){var m=c[l];d[l]=i(a,b,m),e[l]=j(a,b,m);var n=e[l]-d[l];na.node(d).rank&&(a.delEdge(b),e.reversed=!0,a.addEdge(b,d,c,e))})}function r(a,b){var c=g(a);b&&(d.log(1,"Using network simplex for ranking"),i(a,c)),s(a)}function s(a){var b=d.min(a.nodes().map(function(b){return a.node(b).rank}));a.eachNode(function(a,c){c.rank-=b})}var d=a("./util"),e=a("./rank/acyclic"),f=a("./rank/initRank"),g=a("./rank/feasibleTree"),h=a("./rank/constraints"),i=a("./rank/simplex"),j=a("graphlib").alg.components,k=a("graphlib").filter;c.run=l,c.restoreEdges=m},{"./rank/acyclic":20,"./rank/constraints":21,"./rank/feasibleTree":22,"./rank/initRank":23,"./rank/simplex":25,"./util":26,graphlib:28}],20:[function(a,b,c){function e(a){function f(d){if(d in c)return;c[d]=b[d]=!0,a.outEdges(d).forEach(function(c){var h=a.target(c),i;d===h?console.error('Warning: found self loop "'+c+'" for node "'+d+'"'):h in b?(i=a.edge(c),a.delEdge(c),i.reversed=!0,++e,a.addEdge(c,h,d,i)):f(h)}),delete b[d]}var b={},c={},e=0;return a.eachNode(function(a){f(a)}),d.log(2,"Acyclic Phase: reversed "+e+" edge(s)"),e}function f(a){a.eachEdge(function(b,c,d,e){e.reversed&&(delete e.reversed,a.delEdge(b),a.addEdge(b,d,c,e))})}var d=a("../util");b.exports=e,b.exports.undo=f},{"../util":26}],21:[function(a,b,c){function d(a){return a!=="min"&&a!=="max"&&a.indexOf("same_")!==0?(console.error("Unsupported rank type: "+a),!1):!0}function e(a,b,c,d){a.inEdges(b).forEach(function(b){var e=a.edge(b),f;e.originalEdge?f=e:f={originalEdge:{e:b,u:a.source(b),v:a.target(b),value:e},minLen:a.edge(b).minLen},e.selfLoop&&(d=!1),d?(a.addEdge(null,c,a.source(b),f),f.reversed=!0):a.addEdge(null,a.source(b),c,f)})}function f(a,b,c,d){a.outEdges(b).forEach(function(b){var e=a.edge(b),f;e.originalEdge?f=e:f={originalEdge:{e:b,u:a.source(b),v:a.target(b),value:e},minLen:a.edge(b).minLen},e.selfLoop&&(d=!1),d?(a.addEdge(null,a.target(b),c,f),f.reversed=!0):a.addEdge(null,c,a.target(b),f)})}function g(a,b,c){c!==undefined&&a.children(b).forEach(function(b){b!==c&&!a.outEdges(c,b).length&&!a.node(b).dummy&&a.addEdge(null,c,b,{minLen:0})})}function h(a,b,c){c!==undefined&&a.children(b).forEach(function(b){b!==c&&!a.outEdges(b,c).length&&!a.node(b).dummy&&a.addEdge(null,b,c,{minLen:0})})}c.apply=function(a){function b(c){var i={};a.children(c).forEach(function(g){if(a.children(g).length){b(g);return}var h=a.node(g),j=h.prefRank;if(j!==undefined){if(!d(j))return;j in i?i.prefRank.push(g):i.prefRank=[g];var k=i[j];k===undefined&&(k=i[j]=a.addNode(null,{originalNodes:[]}),a.parent(k,c)),e(a,g,k,j==="min"),f(a,g,k,j==="max"),a.node(k).originalNodes.push({u:g,value:h,parent:c}),a.delNode(g)}}),g(a,c,i.min),h(a,c,i.max)}b(null)},c.relax=function(a){var b=[];a.eachEdge(function(a,c,d,e){var f=e.originalEdge;f&&b.push(f)}),a.eachNode(function(b,c){var d=c.originalNodes;d&&(d.forEach(function(b){b.value.rank=c.rank,a.addNode(b.u,b.value),a.parent(b.u,b.parent)}),a.delNode(b))}),b.forEach(function(b){a.addEdge(b.e,b.u,b.v,b.value)})}},{}],22:[function(a,b,c){function g(a){function g(d){var e=!0;return a.predecessors(d).forEach(function(f){b.has(f)&&!h(a,f,d)&&(b.has(d)&&(c.addNode(d,{}),b.remove(d),c.graph({root:d})),c.addNode(f,{}),c.addEdge(null,f,d,{reversed:!0}),b.remove(f),g(f),e=!1)}),a.successors(d).forEach(function(f){b.has(f)&&!h(a,d,f)&&(b.has(d)&&(c.addNode(d,{}),b.remove(d),c.graph({root:d})),c.addNode(f,{}),c.addEdge(null,d,f,{}),b.remove(f),g(f),e=!1)}),e}function i(){var d=Number.MAX_VALUE;b.keys().forEach(function(c){a.predecessors(c).forEach(function(e){if(!b.has(e)){var f=h(a,e,c);Math.abs(f)0)i=b.source(j[0]),j=b.inEdges(i);b.graph().root=i,b.addEdge(null,e,f,{cutValue:0}),g(a,b),n(a,b)}function n(a,b){function c(d){var e=b.successors(d);e.forEach(function(b){var e=o(a,d,b);a.node(b).rank=a.node(d).rank+e,c(b)})}c(b.graph().root)}function o(a,b,c){var e=a.outEdges(b,c);if(e.length>0)return d.max(e.map(function(b){return a.edge(b).minLen}));var f=a.inEdges(b,c);if(f.length>0)return-d.max(f.map(function(b){return a.edge(b).minLen}))}var d=a("../util"),e=a("./rankUtil");b.exports=f},{"../util":26,"./rankUtil":24}],26:[function(a,b,c){function d(a,b){return function(){var c=(new Date).getTime();try{return b.apply(null,arguments)}finally{e(1,a+" time: "+((new Date).getTime()-c)+"ms")}}}function e(a){e.level>=a&&console.log.apply(console,Array.prototype.slice.call(arguments,1))}c.min=function(a){return Math.min.apply(Math,a)},c.max=function(a){return Math.max.apply(Math,a)},c.all=function(a,b){for(var c=0;c0;--i){var b=Math.floor(Math.random()*(i+1)),c=a[b];a[b]=a[i],a[i]=c}},c.propertyAccessor=function(a,b,c,d){return function(e){return arguments.length?(b[c]=e,d&&d(e),a):b[c]}},c.ordering=function(a){var b=[];return a.eachNode(function(a,c){var d=b[c.rank]||(b[c.rank]=[]);d[c.order]=a}),b},c.filterNonSubgraphs=function(a){return function(b){return a.children(b).length===0}},d.enabled=!1,c.time=d,e.level=0,c.log=e},{}],27:[function(a,b,c){b.exports="0.4.5"},{}],28:[function(a,b,c){c.Graph=a("./lib/Graph"),c.Digraph=a("./lib/Digraph" 2 | ),c.CGraph=a("./lib/CGraph"),c.CDigraph=a("./lib/CDigraph"),a("./lib/graph-converters"),c.alg={isAcyclic:a("./lib/alg/isAcyclic"),components:a("./lib/alg/components"),dijkstra:a("./lib/alg/dijkstra"),dijkstraAll:a("./lib/alg/dijkstraAll"),findCycles:a("./lib/alg/findCycles"),floydWarshall:a("./lib/alg/floydWarshall"),postorder:a("./lib/alg/postorder"),preorder:a("./lib/alg/preorder"),prim:a("./lib/alg/prim"),tarjan:a("./lib/alg/tarjan"),topsort:a("./lib/alg/topsort")},c.converter={json:a("./lib/converter/json.js")};var d=a("./lib/filter");c.filter={all:d.all,nodesFromList:d.nodesFromList},c.version=a("./lib/version")},{"./lib/CDigraph":30,"./lib/CGraph":31,"./lib/Digraph":32,"./lib/Graph":33,"./lib/alg/components":34,"./lib/alg/dijkstra":35,"./lib/alg/dijkstraAll":36,"./lib/alg/findCycles":37,"./lib/alg/floydWarshall":38,"./lib/alg/isAcyclic":39,"./lib/alg/postorder":40,"./lib/alg/preorder":41,"./lib/alg/prim":42,"./lib/alg/tarjan":43,"./lib/alg/topsort":44,"./lib/converter/json.js":46,"./lib/filter":47,"./lib/graph-converters":48,"./lib/version":50}],29:[function(a,b,c){function e(){this._value=undefined,this._nodes={},this._edges={},this._nextId=0}function f(a,b,c){(a[b]||(a[b]=new d)).add(c)}function g(a,b,c){var d=a[b];d.remove(c),d.size()===0&&delete a[b]}var d=a("cp-data").Set;b.exports=e,e.prototype.order=function(){return Object.keys(this._nodes).length},e.prototype.size=function(){return Object.keys(this._edges).length},e.prototype.graph=function(a){if(arguments.length===0)return this._value;this._value=a},e.prototype.hasNode=function(a){return a in this._nodes},e.prototype.node=function(a,b){var c=this._strictGetNode(a);if(arguments.length===1)return c.value;c.value=b},e.prototype.nodes=function(){var a=[];return this.eachNode(function(b){a.push(b)}),a},e.prototype.eachNode=function(a){for(var b in this._nodes){var c=this._nodes[b];a(c.id,c.value)}},e.prototype.hasEdge=function(a){return a in this._edges},e.prototype.edge=function(a,b){var c=this._strictGetEdge(a);if(arguments.length===1)return c.value;c.value=b},e.prototype.edges=function(){var a=[];return this.eachEdge(function(b){a.push(b)}),a},e.prototype.eachEdge=function(a){for(var b in this._edges){var c=this._edges[b];a(c.id,c.u,c.v,c.value)}},e.prototype.incidentNodes=function(a){var b=this._strictGetEdge(a);return[b.u,b.v]},e.prototype.addNode=function(a,b){if(a===undefined||a===null){do a="_"+ ++this._nextId;while(this.hasNode(a))}else if(this.hasNode(a))throw new Error("Graph already has node '"+a+"'");return this._nodes[a]={id:a,value:b},a},e.prototype.delNode=function(a){this._strictGetNode(a),this.incidentEdges(a).forEach(function(a){this.delEdge(a)},this),delete this._nodes[a]},e.prototype._addEdge=function(a,b,c,d,e,g){this._strictGetNode(b),this._strictGetNode(c);if(a===undefined||a===null){do a="_"+ ++this._nextId;while(this.hasEdge(a))}else if(this.hasEdge(a))throw new Error("Graph already has edge '"+a+"'");return this._edges[a]={id:a,u:b,v:c,value:d},f(e[c],b,a),f(g[b],c,a),a},e.prototype._delEdge=function(a,b,c){var d=this._strictGetEdge(a);g(b[d.v],d.u,a),g(c[d.u],d.v,a),delete this._edges[a]},e.prototype.copy=function(){var a=new this.constructor;return a.graph(this.graph()),this.eachNode(function(b,c){a.addNode(b,c)}),this.eachEdge(function(b,c,d,e){a.addEdge(b,c,d,e)}),a._nextId=this._nextId,a},e.prototype.filterNodes=function(a){var b=new this.constructor;return b.graph(this.graph()),this.eachNode(function(c,d){a(c)&&b.addNode(c,d)}),this.eachEdge(function(a,c,d,e){b.hasNode(c)&&b.hasNode(d)&&b.addEdge(a,c,d,e)}),b},e.prototype._strictGetNode=function(a){var b=this._nodes[a];if(b===undefined)throw new Error("Node '"+a+"' is not in graph");return b},e.prototype._strictGetEdge=function(a){var b=this._edges[a];if(b===undefined)throw new Error("Edge '"+a+"' is not in graph");return b}},{"cp-data":5}],30:[function(a,b,c){var d=a("./Digraph"),e=a("./compoundify"),f=e(d);b.exports=f,f.fromDigraph=function(a){var b=new f,c=a.graph();return c!==undefined&&b.graph(c),a.eachNode(function(a,c){c===undefined?b.addNode(a):b.addNode(a,c)}),a.eachEdge(function(a,c,d,e){e===undefined?b.addEdge(null,c,d):b.addEdge(null,c,d,e)}),b},f.prototype.toString=function(){return"CDigraph "+JSON.stringify(this,null,2)}},{"./Digraph":32,"./compoundify":45}],31:[function(a,b,c){var d=a("./Graph"),e=a("./compoundify"),f=e(d);b.exports=f,f.fromGraph=function(a){var b=new f,c=a.graph();return c!==undefined&&b.graph(c),a.eachNode(function(a,c){c===undefined?b.addNode(a):b.addNode(a,c)}),a.eachEdge(function(a,c,d,e){e===undefined?b.addEdge(null,c,d):b.addEdge(null,c,d,e)}),b},f.prototype.toString=function(){return"CGraph "+JSON.stringify(this,null,2)}},{"./Graph":33,"./compoundify":45}],32:[function(a,b,c){function g(){e.call(this),this._inEdges={},this._outEdges={}}var d=a("./util"),e=a("./BaseGraph"),f=a("cp-data").Set;b.exports=g,g.prototype=new e,g.prototype.constructor=g,g.prototype.isDirected=function(){return!0},g.prototype.successors=function(a){return this._strictGetNode(a),Object.keys(this._outEdges[a]).map(function(a){return this._nodes[a].id},this)},g.prototype.predecessors=function(a){return this._strictGetNode(a),Object.keys(this._inEdges[a]).map(function(a){return this._nodes[a].id},this)},g.prototype.neighbors=function(a){return f.union([this.successors(a),this.predecessors(a)]).keys()},g.prototype.sources=function(){var a=this;return this._filterNodes(function(b){return a.inEdges(b).length===0})},g.prototype.sinks=function(){var a=this;return this._filterNodes(function(b){return a.outEdges(b).length===0})},g.prototype.source=function(a){return this._strictGetEdge(a).u},g.prototype.target=function(a){return this._strictGetEdge(a).v},g.prototype.inEdges=function(a,b){this._strictGetNode(a);var c=f.union(d.values(this._inEdges[a])).keys();return arguments.length>1&&(this._strictGetNode(b),c=c.filter(function(a){return this.source(a)===b},this)),c},g.prototype.outEdges=function(a,b){this._strictGetNode(a);var c=f.union(d.values(this._outEdges[a])).keys();return arguments.length>1&&(this._strictGetNode(b),c=c.filter(function(a){return this.target(a)===b},this)),c},g.prototype.incidentEdges=function(a,b){return arguments.length>1?f.union([this.outEdges(a,b),this.outEdges(b,a)]).keys():f.union([this.inEdges(a),this.outEdges(a)]).keys()},g.prototype.toString=function(){return"Digraph "+JSON.stringify(this,null,2)},g.prototype.addNode=function(a,b){return a=e.prototype.addNode.call(this,a,b),this._inEdges[a]={},this._outEdges[a]={},a},g.prototype.delNode=function(a){e.prototype.delNode.call(this,a),delete this._inEdges[a],delete this._outEdges[a]},g.prototype.addEdge=function(a,b,c,d){return e.prototype._addEdge.call(this,a,b,c,d,this._inEdges,this._outEdges)},g.prototype.delEdge=function(a){e.prototype._delEdge.call(this,a,this._inEdges,this._outEdges)},g.prototype._filterNodes=function(a){var b=[];return this.eachNode(function(c){a(c)&&b.push(c)}),b}},{"./BaseGraph":29,"./util":49,"cp-data":5}],33:[function(a,b,c){function g(){e.call(this),this._incidentEdges={}}var d=a("./util"),e=a("./BaseGraph"),f=a("cp-data").Set;b.exports=g,g.prototype=new e,g.prototype.constructor=g,g.prototype.isDirected=function(){return!1},g.prototype.neighbors=function(a){return this._strictGetNode(a),Object.keys(this._incidentEdges[a]).map(function(a){return this._nodes[a].id},this)},g.prototype.incidentEdges=function(a,b){return this._strictGetNode(a),arguments.length>1?(this._strictGetNode(b),b in this._incidentEdges[a]?this._incidentEdges[a][b].keys():[]):f.union(d.values(this._incidentEdges[a])).keys()},g.prototype.toString=function(){return"Graph "+JSON.stringify(this,null,2)},g.prototype.addNode=function(a,b){return a=e.prototype.addNode.call(this,a,b),this._incidentEdges[a]={},a},g.prototype.delNode=function(a){e.prototype.delNode.call(this,a),delete this._incidentEdges[a]},g.prototype.addEdge=function(a,b,c,d){return e.prototype._addEdge.call(this,a,b,c,d,this._incidentEdges,this._incidentEdges)},g.prototype.delEdge=function(a){e.prototype._delEdge.call(this,a,this._incidentEdges,this._incidentEdges)}},{"./BaseGraph":29,"./util":49,"cp-data":5}],34:[function(a,b,c){function e(a){function e(b,d){c.has(b)||(c.add(b),d.push(b),a.neighbors(b).forEach(function(a){e(a,d)}))}var b=[],c=new d;return a.nodes().forEach(function(a){var c=[];e(a,c),c.length>0&&b.push(c)}),b}var d=a("cp-data").Set;b.exports=e},{"cp-data":5}],35:[function(a,b,c){function e(a,b,c,e){function h(b){var d=a.incidentNodes(b),e=d[0]!==i?d[0]:d[1],h=f[e],k=c(b),l=j.distance+k;if(k<0)throw new Error("dijkstra does not allow negative edge weights. Bad edge: "+b+" Weight: "+k);l0){i=g.removeMin(),j=f[i];if(j.distance===Number.POSITIVE_INFINITY)break;e(i).forEach(h)}return f}var d=a("cp-data").PriorityQueue;b.exports=e},{"cp-data":5}],36:[function(a,b,c){function e(a,b,c){var e={};return a.eachNode(function(f){e[f]=d(a,f,b,c)}),e}var d=a("./dijkstra");b.exports=e},{"./dijkstra":35}],37:[function(a,b,c){function e(a){return d(a).filter(function(a){return a.length>1})}var d=a("./tarjan");b.exports=e},{"./tarjan":43}],38:[function(a,b,c){function d(a,b,c){var d={},e=a.nodes();return b=b||function(){return 1},c=c||(a.isDirected()?function(b){return a.outEdges(b)}:function(b){return a.incidentEdges(b)}),e.forEach(function(f){d[f]={},d[f][f]={distance:0},e.forEach(function(a){f!==a&&(d[f][a]={distance:Number.POSITIVE_INFINITY})}),c(f).forEach(function(c){var e=a.incidentNodes(c),h=e[0]!==f?e[0]:e[1],i=b(c);i0){h=g.removeMin();if(h in f)c.addEdge(null,h,f[h]);else{if(j)throw new Error("Input graph is not connected: "+a);j=!0}a.incidentEdges(h).forEach(i)}return c}var d=a("../Graph"),e=a("cp-data").PriorityQueue;b.exports=f},{"../Graph":33,"cp-data":5}],43:[function(a,b,c){function d(a){function f(h){var i=d[h]={onStack:!0,lowlink:b,index:b++};c.push(h),a.successors(h).forEach(function(a){a in d?d[a].onStack&&(i.lowlink=Math.min(i.lowlink,d[a].index)):(f(a),i.lowlink=Math.min(i.lowlink,d[a].lowlink))});if(i.lowlink===i.index){var j=[],k;do k=c.pop(),d[k].onStack=!1,j.push(k);while(h!==k);e.push(j)}}if(!a.isDirected())throw new Error("tarjan can only be applied to a directed graph. Bad input: "+a);var b=0,c=[],d={},e=[];return a.nodes().forEach(function(a){a in d||f(a)}),e}b.exports=d},{}],44:[function(a,b,c){function d(a){function f(g){if(g in c)throw new e;g in b||(c[g]=!0,b[g]=!0,a.predecessors(g).forEach(function(a){f(a)}),delete c[g],d.push(g))}if(!a.isDirected())throw new Error("topsort can only be applied to a directed graph. Bad input: "+a);var b={},c={},d=[],g=a.sinks();if(a.order()!==0&&g.length===0)throw new e;return a.sinks().forEach(function(a){f(a)}),d}function e(){}b.exports=d,d.CycleException=e,e.prototype.toString=function(){return"Graph has at least one cycle"}},{}],45:[function(a,b,c){function e(a){function b(){a.call(this),this._parents={},this._children={},this._children[null]=new d}return b.prototype=new a,b.prototype.constructor=b,b.prototype.parent=function(a,b){this._strictGetNode(a);if(arguments.length<2)return this._parents[a];if(a===b)throw new Error("Cannot make "+a+" a parent of itself");b!==null&&this._strictGetNode(b),this._children[this._parents[a]].remove(a),this._parents[a]=b,this._children[b].add(a)},b.prototype.children=function(a){return a!==null&&this._strictGetNode(a),this._children[a].keys()},b.prototype.addNode=function(b,c){return b=a.prototype.addNode.call(this,b,c),this._parents[b]=null,this._children[b]=new d,this._children[null].add(b),b},b.prototype.delNode=function(b){var c=this.parent(b);return this._children[b].keys().forEach(function(a){this.parent(a,c)},this),this._children[c].remove(b),delete this._parents[b],delete this._children[b],a.prototype.delNode.call(this,b)},b.prototype.copy=function(){var b=a.prototype.copy.call(this);return this.nodes().forEach(function(a){b.parent(a,this.parent(a))},this),b},b.prototype.filterNodes=function(b){function f(a){var b=c.parent(a);return b===null||d.hasNode(b)?(e[a]=b,b):b in e?e[b]:f(b)}var c=this,d=a.prototype.filterNodes.call(this,b),e={};return d.eachNode(function(a){d.parent(a,f(a))}),d},b}var d=a("cp-data").Set;b.exports=e},{"cp-data":5}],46:[function(a,b,c){function h(a){return Object.prototype.toString.call(a).slice(8,-1)}var d=a("../Graph"),e=a("../Digraph"),f=a("../CGraph"),g=a("../CDigraph");c.decode=function(a,b,c){c=c||e;if(h(a)!=="Array")throw new Error("nodes is not an Array");if(h(b)!=="Array")throw new Error("edges is not an Array");if(typeof c=="string")switch(c){case"graph":c=d;break;case"digraph":c=e;break;case"cgraph":c=f;break;case"cdigraph":c=g;break;default:throw new Error("Unrecognized graph type: "+c)}var i=new c;return a.forEach(function(a){i.addNode(a.id,a.value)}),i.parent&&a.forEach(function(a){a.children&&a.children.forEach(function(b){i.parent(b,a.id)})}),b.forEach(function(a){i.addEdge(a.id,a.u,a.v,a.value)}),i},c.encode=function(a){var b=[],c=[];a.eachNode(function(c,d){var e={id:c,value:d};if(a.children){var f=a.children(c);f.length&&(e.children=f)}b.push(e)}),a.eachEdge(function(a,b,d,e){c.push({id:a,u:b,v:d,value:e})});var h;if(a instanceof g)h="cdigraph";else if(a instanceof f)h="cgraph";else if(a instanceof e)h="digraph";else if(a instanceof d)h="graph";else throw new Error("Couldn't determine type of graph: "+a);return{nodes:b,edges:c,type:h}}},{"../CDigraph":30,"../CGraph":31,"../Digraph":32,"../Graph":33}],47:[function(a,b,c){var d=a("cp-data").Set;c.all=function(){return function(){return!0}},c.nodesFromList=function(a){var b=new d(a);return function(a){return b.has(a)}}},{"cp-data":5}],48:[function(a,b,c){var d=a("./Graph"),e=a("./Digraph");d.prototype.toDigraph=d.prototype.asDirected=function(){var a=new e;return this.eachNode(function(b,c){a.addNode(b,c)}),this.eachEdge(function(b,c,d,e){a.addEdge(null,c,d,e),a.addEdge(null,d,c,e)}),a},e.prototype.toGraph=e.prototype.asUndirected=function(){var a=new d;return this.eachNode(function(b,c){a.addNode(b,c)}),this.eachEdge(function(b,c,d,e){a.addEdge(b,c,d,e)}),a}},{"./Digraph":32,"./Graph":33}],49:[function(a,b,c){c.values=function(a){var b=Object.keys(a),c=b.length,d=new Array(c),e;for(e=0;e