mirror of
https://github.com/anteo/redmine_custom_workflows.git
synced 2026-01-26 00:04:20 +00:00
Initial commit (#8593)
This commit is contained in:
commit
c105acf3b8
3
README.rdoc
Normal file
3
README.rdoc
Normal file
@ -0,0 +1,3 @@
|
||||
= custom_workflow
|
||||
|
||||
Description goes here
|
||||
71
app/controllers/custom_workflows_controller.rb
Normal file
71
app/controllers/custom_workflows_controller.rb
Normal file
@ -0,0 +1,71 @@
|
||||
class CustomWorkflowsController < ApplicationController
|
||||
unloadable
|
||||
|
||||
layout 'admin'
|
||||
before_filter :require_admin
|
||||
before_filter :find_workflow, :only => [:show, :edit, :update, :destroy]
|
||||
|
||||
def index
|
||||
@workflows = CustomWorkflow.all
|
||||
respond_to do |format|
|
||||
format.html
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
format.html { redirect_to edit_custom_workflow_path }
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def new
|
||||
@workflow = CustomWorkflow.new
|
||||
respond_to do |format|
|
||||
format.html
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
@workflow = CustomWorkflow.new(params[:custom_workflow])
|
||||
respond_to do |format|
|
||||
if @workflow.save
|
||||
flash[:notice] = l(:notice_successful_create)
|
||||
format.html { redirect_to(custom_workflows_path) }
|
||||
else
|
||||
format.html { render :action => "new" }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
respond_to do |format|
|
||||
if @workflow.update_attributes(params[:custom_workflow])
|
||||
flash[:notice] = l(:notice_successful_update)
|
||||
format.html { redirect_to(custom_workflows_path) }
|
||||
else
|
||||
format.html { render :action => :edit }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@workflow.destroy
|
||||
|
||||
respond_to do |format|
|
||||
flash[:notice] = l(:notice_successful_delete)
|
||||
format.html { redirect_to(custom_workflows_path) }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_workflow
|
||||
@workflow = CustomWorkflow.find(params[:id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
end
|
||||
36
app/models/custom_workflow.rb
Normal file
36
app/models/custom_workflow.rb
Normal file
@ -0,0 +1,36 @@
|
||||
class WorkflowError < StandardError
|
||||
attr_accessor :error
|
||||
def initialize(message)
|
||||
@error = message.dup
|
||||
super message
|
||||
end
|
||||
end
|
||||
|
||||
class CustomWorkflow < ActiveRecord::Base
|
||||
has_and_belongs_to_many :projects
|
||||
acts_as_list
|
||||
|
||||
default_scope :order => 'position ASC'
|
||||
validates_presence_of :script
|
||||
validates_presence_of :name
|
||||
validates_uniqueness_of :name, :case_sensitive => false
|
||||
validate :validate_syntax
|
||||
|
||||
def eval_script(context)
|
||||
context.each { |k, v| instance_variable_set ("@#{k}").to_sym, v }
|
||||
eval(script)
|
||||
end
|
||||
|
||||
def validate_syntax
|
||||
begin
|
||||
eval_script(:issue => Issue.new)
|
||||
rescue WorkflowError => e
|
||||
rescue Exception => e
|
||||
errors.add :script, :invalid_script, :error => e
|
||||
end
|
||||
end
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
end
|
||||
24
app/views/custom_workflows/_form.html.erb
Normal file
24
app/views/custom_workflows/_form.html.erb
Normal file
@ -0,0 +1,24 @@
|
||||
|
||||
<div class="box tabular">
|
||||
<p><%= f.text_field :name, :required => true, :size => 50 %></p>
|
||||
<p><%= f.text_area :description, :cols => 80, :rows => 5 %></p>
|
||||
<p><%= f.text_area :script, :cols => 80, :rows => 20, :wrap => 'off', :required => true %>
|
||||
<em class="info"><%= l(:text_custom_workflow_script_note) %></em>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
jQuery('#custom_workflow_script').taboverride(2, true);
|
||||
</script>
|
||||
|
||||
<%= wikitoolbar_for :custom_workflow_description %>
|
||||
|
||||
<% content_for :header_tags do %>
|
||||
<% if (Redmine::VERSION::ARRAY <=> [2,1,0]) < 0 %>
|
||||
<%= javascript_include_tag "jquery-1.8.1.min.js", :plugin => 'redmine_custom_workflows' %>
|
||||
<script type="text/javascript">
|
||||
jQuery.noConflict();
|
||||
</script>
|
||||
<% end %>
|
||||
<%= javascript_include_tag "tab_override", :plugin => 'redmine_custom_workflows' %>
|
||||
<% end %>
|
||||
12
app/views/custom_workflows/edit.html.erb
Normal file
12
app/views/custom_workflows/edit.html.erb
Normal file
@ -0,0 +1,12 @@
|
||||
<h2><%= link_to l(:label_custom_workflow_plural), custom_workflows_path %> » <%= @workflow %></h2>
|
||||
|
||||
<%= error_messages_for @workflow %>
|
||||
|
||||
<% form = labelled_form_for @workflow do |f| %>
|
||||
<%= render :partial => 'form', :locals => {:f => f} %>
|
||||
<%= submit_tag l(:button_save) %>
|
||||
<% end %>
|
||||
|
||||
<%= form if Redmine::VERSION::MAJOR >= 2 %>
|
||||
|
||||
<% html_title(l(:label_custom_workflow_plural), @workflow, l(:label_administration)) -%>
|
||||
35
app/views/custom_workflows/index.html.erb
Normal file
35
app/views/custom_workflows/index.html.erb
Normal file
@ -0,0 +1,35 @@
|
||||
<% html_title(l(:label_custom_workflow_plural)) -%>
|
||||
<div class="contextual">
|
||||
<%= link_to l(:label_custom_workflow_new), new_custom_workflow_path, :class => 'icon icon-add' %>
|
||||
</div>
|
||||
|
||||
<h2><%=l(:label_custom_workflow_plural)%></h2>
|
||||
|
||||
<div class="autoscroll">
|
||||
<% if @workflows.any? %>
|
||||
<table class="custom-workflows list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><%=l(:field_name)%></th>
|
||||
<th><%=l(:field_description)%></th>
|
||||
<th style="width:15%;"><%= l(:button_sort) %></th>
|
||||
<th style="width:10%;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @workflows.each do |workflow| %>
|
||||
<tr class="<%= cycle("odd", "even") %>">
|
||||
<td class="name"><%= link_to(workflow.name, edit_custom_workflow_path(workflow)) %></td>
|
||||
<td><%= textilizable(workflow.description) %></td>
|
||||
<td align="center"><%= reorder_links("custom_workflow", {:action => 'update', :id => workflow}, :put) %></td>
|
||||
<td class="buttons">
|
||||
<%= link_to(l(:button_delete), workflow, :class => 'icon icon-del', :data => {:confirm => l(:text_are_you_sure)}, :confirm => l(:text_are_you_sure), :method => :delete) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% else %>
|
||||
<p class="nodata"><%= l(:label_no_data) %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
12
app/views/custom_workflows/new.html.erb
Normal file
12
app/views/custom_workflows/new.html.erb
Normal file
@ -0,0 +1,12 @@
|
||||
<h2><%= link_to l(:label_custom_workflow_plural), custom_workflows_path %> » <%= l(:label_custom_workflow_new) %></h2>
|
||||
|
||||
<%= error_messages_for @workflow %>
|
||||
|
||||
<% form = labelled_form_for @workflow do |f| %>
|
||||
<%= render :partial => 'form', :locals => {:f => f} %>
|
||||
<%= submit_tag l(:button_create) %>
|
||||
<% end %>
|
||||
|
||||
<%= form if Redmine::VERSION::MAJOR >= 2 %>
|
||||
|
||||
<% html_title(l(:label_custom_workflow_plural), l(:label_custom_workflow_new), l(:label_administration)) -%>
|
||||
20
app/views/projects/settings/_custom_workflow.html.erb
Normal file
20
app/views/projects/settings/_custom_workflow.html.erb
Normal file
@ -0,0 +1,20 @@
|
||||
<% form = form_for @project do |f| %>
|
||||
<%= hidden_field_tag :tab, 'custom_workflow' %>
|
||||
<%= hidden_field_tag 'project[custom_workflow_ids][]', '' %>
|
||||
<fieldset>
|
||||
<legend><%= l(:text_select_project_custom_workflows) %></legend>
|
||||
<% if CustomWorkflow.exists? %>
|
||||
<dl>
|
||||
<% CustomWorkflow.all.each do |w| %>
|
||||
<dt><label><%= check_box_tag 'project[custom_workflow_ids][]', w.id, @project.custom_workflow_ids.include?(w.id) %> <%= w.name %></label></dt>
|
||||
<dd><em><%= textilizable(w.description) %></em></dd>
|
||||
<% end %>
|
||||
</dl>
|
||||
<% else %>
|
||||
<p class="nodata"><%= l(:label_no_data) %></p>
|
||||
<% end %>
|
||||
</fieldset>
|
||||
<%= submit_tag l(:button_save) %>
|
||||
<% end %>
|
||||
|
||||
<%= form if Redmine::VERSION::MAJOR >= 2 %>
|
||||
2
assets/javascripts/jquery-1.8.1.min.js
vendored
Normal file
2
assets/javascripts/jquery-1.8.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
324
assets/javascripts/tab_override.js
Normal file
324
assets/javascripts/tab_override.js
Normal file
@ -0,0 +1,324 @@
|
||||
(function($) {
|
||||
$.fn.taboverride = function(tabSize, autoIndent) {
|
||||
this.each(function() {
|
||||
$(this).data('taboverride', new TabOverride(this, tabSize, autoIndent));
|
||||
});
|
||||
};
|
||||
|
||||
function TabOverride(element, tabSize, autoIndent) {
|
||||
var ta = document.createElement('textarea');
|
||||
ta.value = '\n';
|
||||
|
||||
this.newline = ta.value;
|
||||
this.newlineLen = this.newline.length;
|
||||
this.autoIndent = autoIndent;
|
||||
this.inWhitespace = false;
|
||||
this.element = element;
|
||||
this.setTabSize(tabSize);
|
||||
|
||||
$(element).on('keypress', $.proxy(this.overrideKeyPress, this));
|
||||
$(element).on('keydown', $.proxy(this.overrideKeyDown, this));
|
||||
}
|
||||
|
||||
TabOverride.prototype = {
|
||||
/**
|
||||
* Returns the current tab size. 0 represents the tab character.
|
||||
*
|
||||
* @return {Number} the size (length) of the tab string or 0 for the tab character
|
||||
*
|
||||
* @name getTabSize
|
||||
* @function
|
||||
*/
|
||||
getTabSize:function () {
|
||||
return this.aTab === '\t' ? 0 : this.aTab.length;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the tab size for all elements that have Tab Override enabled.
|
||||
* 0 represents the tab character.
|
||||
*
|
||||
* @param {Number} size the tab size (default = 0)
|
||||
*
|
||||
* @name setTabSize
|
||||
* @function
|
||||
*/
|
||||
setTabSize:function (size) {
|
||||
var i;
|
||||
if (!size) { // size is 0 or not specified (or falsy)
|
||||
this.aTab = '\t';
|
||||
} else if (typeof size === 'number' && size > 0) {
|
||||
this.aTab = '';
|
||||
for (i = 0; i < size; i += 1) {
|
||||
this.aTab += ' ';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Prevents the default action for the keyPress event when tab or enter are
|
||||
* pressed. Opera (and Firefox) also fire a keypress event when the tab or
|
||||
* enter key is pressed. Opera requires that the default action be prevented
|
||||
* on this event or the textarea will lose focus.
|
||||
*
|
||||
* @param {Event} e the event object
|
||||
* @private
|
||||
*/
|
||||
overrideKeyPress:function (e) {
|
||||
var key = e.keyCode;
|
||||
if ((key === 9 || (key === 13 && this.autoIndent && !this.inWhitespace)) && !e.ctrlKey && !e.altKey) {
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Inserts / removes tabs and newlines on the keyDown event for the tab or enter key.
|
||||
*
|
||||
* @param {Event} e the event object
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
overrideKeyDown:function (e) {
|
||||
var key = e.keyCode, // the key code for the key that was pressed
|
||||
tab, // the string representing a tab
|
||||
tabLen, // the length of a tab
|
||||
text, // initial text in the textarea
|
||||
range, // the IE TextRange object
|
||||
tempRange, // used to calculate selection start and end positions in IE
|
||||
preNewlines, // the number of newline character sequences before the selection start (for IE)
|
||||
selNewlines, // the number of newline character sequences within the selection (for IE)
|
||||
initScrollTop, // initial scrollTop value used to fix scrolling in Firefox
|
||||
selStart, // the selection start position
|
||||
selEnd, // the selection end position
|
||||
sel, // the selected text
|
||||
startLine, // for multi-line selections, the first character position of the first line
|
||||
endLine, // for multi-line selections, the last character position of the last line
|
||||
numTabs, // the number of tabs inserted / removed in the selection
|
||||
startTab, // if a tab was removed from the start of the first line
|
||||
preTab, // if a tab was removed before the start of the selection
|
||||
whitespace, // the whitespace at the beginning of the first selected line
|
||||
whitespaceLen; // the length of the whitespace at the beginning of the first selected line
|
||||
|
||||
// don't do any unnecessary work
|
||||
if ((key !== 9 && (key !== 13 || !this.autoIndent)) || e.ctrlKey || e.altKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
// initialize variables used for tab and enter keys
|
||||
this.inWhitespace = false; // this will be set to true if enter is pressed in the leading whitespace
|
||||
text = this.element.value;
|
||||
|
||||
// this is really just for Firefox, but will be used by all browsers that support
|
||||
// selectionStart and selectionEnd - whenever the textarea value property is reset,
|
||||
// Firefox scrolls back to the top - this is used to set it back to the original value
|
||||
// scrollTop is nonstandard, but supported by all modern browsers
|
||||
initScrollTop = this.element.scrollTop;
|
||||
|
||||
// get the text selection
|
||||
// prefer the nonstandard document.selection way since it allows for
|
||||
// automatic scrolling to the cursor via the range.select() method
|
||||
if (document.selection) { // IE
|
||||
range = document.selection.createRange();
|
||||
sel = range.text;
|
||||
tempRange = range.duplicate();
|
||||
tempRange.moveToElementText(this.element);
|
||||
tempRange.setEndPoint('EndToEnd', range);
|
||||
selEnd = tempRange.text.length;
|
||||
selStart = selEnd - sel.length;
|
||||
|
||||
// whenever the value of the textarea is changed, the range needs to be reset
|
||||
// IE <9 (and Opera) use both \r and \n for newlines - this adds an extra character
|
||||
// that needs to be accounted for when doing position calculations with ranges
|
||||
// these values are used to offset the selection start and end positions
|
||||
if (this.newlineLen > 1) {
|
||||
preNewlines = text.slice(0, selStart).split(this.newline).length - 1;
|
||||
selNewlines = sel.split(this.newline).length - 1;
|
||||
} else {
|
||||
preNewlines = selNewlines = 0;
|
||||
}
|
||||
} else if (typeof this.element.selectionStart !== 'undefined') {
|
||||
selStart = this.element.selectionStart;
|
||||
selEnd = this.element.selectionEnd;
|
||||
sel = text.slice(selStart, selEnd);
|
||||
} else {
|
||||
return; // cannot access textarea selection - do nothing
|
||||
}
|
||||
|
||||
// tab key - insert / remove tab
|
||||
if (key === 9) {
|
||||
// initialize tab variables
|
||||
tab = this.aTab;
|
||||
tabLen = tab.length;
|
||||
numTabs = 0;
|
||||
startTab = 0;
|
||||
preTab = 0;
|
||||
|
||||
// multi-line selection
|
||||
if (selStart !== selEnd && sel.indexOf('\n') !== -1) {
|
||||
if (text.charAt(selEnd - 1) === '\n') {
|
||||
selEnd = selEnd - this.newlineLen;
|
||||
sel = text.slice(selStart, selEnd);
|
||||
}
|
||||
// for multiple lines, only insert / remove tabs from the beginning of each line
|
||||
|
||||
// find the start of the first selected line
|
||||
if (selStart === 0 || text.charAt(selStart - 1) === '\n') {
|
||||
// the selection starts at the beginning of a line
|
||||
startLine = selStart;
|
||||
} else {
|
||||
// the selection starts after the beginning of a line
|
||||
// set startLine to the beginning of the first partially selected line
|
||||
// subtract 1 from selStart in case the cursor is at the newline character,
|
||||
// for instance, if the very end of the previous line was selected
|
||||
// add 1 to get the next character after the newline
|
||||
// if there is none before the selection, lastIndexOf returns -1
|
||||
// when 1 is added to that it becomes 0 and the first character is used
|
||||
startLine = text.lastIndexOf('\n', selStart - 1) + 1;
|
||||
}
|
||||
|
||||
// find the end of the last selected line
|
||||
if (selEnd === text.length || text.charAt(selEnd) === '\n') {
|
||||
// the selection ends at the end of a line
|
||||
endLine = selEnd;
|
||||
} else {
|
||||
// the selection ends before the end of a line
|
||||
// set endLine to the end of the last partially selected line
|
||||
endLine = text.indexOf('\n', selEnd);
|
||||
if (endLine === -1) {
|
||||
endLine = text.length;
|
||||
}
|
||||
}
|
||||
// if the shift key was pressed, remove tabs instead of inserting them
|
||||
if (e.shiftKey) {
|
||||
if (text.slice(startLine).indexOf(tab) === 0) {
|
||||
// is this tab part of the selection?
|
||||
if (startLine === selStart) {
|
||||
// it is, remove it
|
||||
sel = sel.slice(tabLen);
|
||||
} else {
|
||||
// the tab comes before the selection
|
||||
preTab = tabLen;
|
||||
}
|
||||
startTab = tabLen;
|
||||
}
|
||||
|
||||
this.element.value = text.slice(0, startLine) + text.slice(startLine + preTab, selStart) +
|
||||
sel.replace(new RegExp('\n' + tab, 'g'), function () {
|
||||
numTabs += 1;
|
||||
return '\n';
|
||||
}) + text.slice(selEnd);
|
||||
|
||||
// set start and end points
|
||||
if (range) { // IE
|
||||
// setting end first makes calculations easier
|
||||
range.collapse();
|
||||
range.moveEnd('character', selEnd - startTab - (numTabs * tabLen) - selNewlines - preNewlines);
|
||||
range.moveStart('character', selStart - preTab - preNewlines);
|
||||
range.select();
|
||||
} else {
|
||||
// set start first for Opera
|
||||
this.element.selectionStart = selStart - preTab; // preTab is 0 or tabLen
|
||||
// move the selection end over by the total number of tabs removed
|
||||
this.element.selectionEnd = selEnd - startTab - (numTabs * tabLen);
|
||||
}
|
||||
} else { // no shift key
|
||||
numTabs = 1; // for the first tab
|
||||
// insert tabs at the beginning of each line of the selection
|
||||
this.element.value = text.slice(0, startLine) + tab + text.slice(startLine, selStart) +
|
||||
sel.replace(/\n/g, function () {
|
||||
numTabs += 1;
|
||||
return '\n' + tab;
|
||||
}) + text.slice(selEnd);
|
||||
|
||||
// set start and end points
|
||||
if (range) { // IE
|
||||
range.collapse();
|
||||
range.moveEnd('character', selEnd + (numTabs * tabLen) - selNewlines - preNewlines);
|
||||
range.moveStart('character', selStart + tabLen - preNewlines);
|
||||
range.select();
|
||||
} else {
|
||||
// the selection start is always moved by 1 character
|
||||
this.element.selectionStart = selStart + (selStart == startLine ? 0 : tabLen);
|
||||
// move the selection end over by the total number of tabs inserted
|
||||
this.element.selectionEnd = selEnd + (numTabs * tabLen);
|
||||
this.element.scrollTop = initScrollTop;
|
||||
}
|
||||
}
|
||||
} else { // single line selection
|
||||
// if the shift key was pressed, remove a tab instead of inserting one
|
||||
if (e.shiftKey) {
|
||||
// if the character before the selection is a tab, remove it
|
||||
if (text.slice(selStart - tabLen).indexOf(tab) === 0) {
|
||||
this.element.value = text.slice(0, selStart - tabLen) + text.slice(selStart);
|
||||
|
||||
// set start and end points
|
||||
if (range) { // IE
|
||||
// collapses range and moves it by -1 tab
|
||||
range.move('character', selStart - tabLen - preNewlines);
|
||||
range.select();
|
||||
} else {
|
||||
this.element.selectionEnd = this.element.selectionStart = selStart - tabLen;
|
||||
this.element.scrollTop = initScrollTop;
|
||||
}
|
||||
}
|
||||
} else { // no shift key - insert a tab
|
||||
if (range) { // IE
|
||||
range.text = tab;
|
||||
range.select();
|
||||
} else {
|
||||
this.element.value = text.slice(0, selStart) + tab + text.slice(selEnd);
|
||||
this.element.selectionEnd = this.element.selectionStart = selStart + tabLen;
|
||||
this.element.scrollTop = initScrollTop;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (this.autoIndent) { // Enter key
|
||||
// insert a newline and copy the whitespace from the beginning of the line
|
||||
// find the start of the first selected line
|
||||
if (selStart === 0 || text.charAt(selStart - 1) === '\n') {
|
||||
// the selection starts at the beginning of a line
|
||||
// do nothing special
|
||||
this.inWhitespace = true;
|
||||
return;
|
||||
} else {
|
||||
// see explanation under "multi-line selection" above
|
||||
startLine = text.lastIndexOf('\n', selStart - 1) + 1;
|
||||
}
|
||||
|
||||
// find the end of the first selected line
|
||||
endLine = text.indexOf('\n', selStart);
|
||||
|
||||
// if no newline is found, set endLine to the end of the text
|
||||
if (endLine === -1) {
|
||||
endLine = text.length;
|
||||
}
|
||||
|
||||
// get the whitespace at the beginning of the first selected line (spaces and tabs only)
|
||||
whitespace = text.slice(startLine, endLine).match(/^[ \t]*/)[0];
|
||||
whitespaceLen = whitespace.length;
|
||||
|
||||
// the cursor (selStart) is in the whitespace at beginning of the line
|
||||
// do nothing special
|
||||
if (selStart < startLine + whitespaceLen) {
|
||||
this.inWhitespace = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (range) { // IE
|
||||
// insert the newline and whitespace
|
||||
range.text = '\n' + whitespace;
|
||||
range.select();
|
||||
} else {
|
||||
// insert the newline and whitespace
|
||||
this.element.value = text.slice(0, selStart) + '\n' + whitespace + text.slice(selEnd);
|
||||
// Opera uses \r\n for a newline, instead of \n,
|
||||
// so use newlineLen instead of a hard-coded value
|
||||
this.element.selectionEnd = this.element.selectionStart = selStart + this.newlineLen + whitespaceLen;
|
||||
this.element.scrollTop = initScrollTop;
|
||||
}
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery);
|
||||
11
assets/stylesheets/style.css
Normal file
11
assets/stylesheets/style.css
Normal file
@ -0,0 +1,11 @@
|
||||
#admin-menu a.custom-workflows {
|
||||
background-image: url(../../../images/ticket_go.png);
|
||||
}
|
||||
|
||||
table.list.custom-workflows td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#custom_workflow_description, #custom_workflow_script {
|
||||
width: 99%;
|
||||
}
|
||||
23
config/locales/en.yml
Normal file
23
config/locales/en.yml
Normal file
@ -0,0 +1,23 @@
|
||||
en:
|
||||
project_module_custom_workflows_module: "Custom workflows"
|
||||
permission_manage_project_workflow: "Manage project custom workflows"
|
||||
label_custom_workflow: "Custom workflows"
|
||||
label_custom_workflow_plural: "Custom workflows"
|
||||
label_custom_workflow_new: "Create a custom workflow"
|
||||
|
||||
field_script: "Workflow script"
|
||||
field_is_enabled: "Enabled"
|
||||
|
||||
field_custom_workflow:
|
||||
script: "Workflow script"
|
||||
|
||||
|
||||
activerecord:
|
||||
errors:
|
||||
messages:
|
||||
invalid_script: "contains error: %{error}"
|
||||
custom_workflow_error: "Custom workflow error (please contact administrator)"
|
||||
new_status_invalid: "transition from '%{old_status}' to '%{new_status}' is prohibited"
|
||||
|
||||
text_select_project_custom_workflows: Select project custom workflows
|
||||
text_custom_workflow_script_note: Use the variable @issue for an access to the issue. To finish the process with an error, use raise WorkflowError, "Message to user"
|
||||
23
config/locales/ru.yml
Normal file
23
config/locales/ru.yml
Normal file
@ -0,0 +1,23 @@
|
||||
ru:
|
||||
project_module_custom_workflows_module: "Пользовательские рабочие процессы"
|
||||
permission_manage_project_workflow: "Управление пользовательскими рабочими процессами в проекте"
|
||||
label_custom_workflow: "Пользовательский рабочий процесс"
|
||||
label_custom_workflow_plural: "Пользовательские рабочие процессы"
|
||||
label_custom_workflow_new: "Новый процесс"
|
||||
|
||||
field_script: "Скрипт управления"
|
||||
field_is_enabled: "Разрешено"
|
||||
|
||||
field_custom_workflow:
|
||||
script: "Скрипт управления"
|
||||
|
||||
|
||||
activerecord:
|
||||
errors:
|
||||
messages:
|
||||
invalid_script: "содержит ошибку: %{error}"
|
||||
custom_workflow_error: "Ошибка в рабочем процессе (обратитесь к администратору)"
|
||||
new_status_invalid: "- переход от '%{old_status}' к '%{new_status}' невозможен"
|
||||
|
||||
text_select_project_custom_workflows: Выберите процессы для данного проекта
|
||||
text_custom_workflow_script_note: Используйте переменную @issue для доступа к задаче. Для завершения процесса с ошибкой используйте исключение WorkflowError.
|
||||
9
config/routes.rb
Normal file
9
config/routes.rb
Normal file
@ -0,0 +1,9 @@
|
||||
if Redmine::VERSION::MAJOR >= 2
|
||||
RedmineApp::Application.routes.draw do
|
||||
resources :custom_workflows
|
||||
end
|
||||
else
|
||||
ActionController::Routing::Routes.draw do |map|
|
||||
map.resources :custom_workflows
|
||||
end
|
||||
end
|
||||
14
db/migrate/20110915084858_create_custom_workflows.rb
Normal file
14
db/migrate/20110915084858_create_custom_workflows.rb
Normal file
@ -0,0 +1,14 @@
|
||||
class CreateCustomWorkflows < ActiveRecord::Migration
|
||||
def self.up
|
||||
create_table :custom_workflows, :force => true do |t|
|
||||
t.references :project
|
||||
t.text :script
|
||||
t.boolean :is_enabled
|
||||
end
|
||||
add_index :custom_workflows, [:project_id], :unique => true
|
||||
end
|
||||
|
||||
def self.down
|
||||
drop_table :custom_workflows
|
||||
end
|
||||
end
|
||||
13
db/migrate/20120601054047_alter_custom_workflows.rb
Normal file
13
db/migrate/20120601054047_alter_custom_workflows.rb
Normal file
@ -0,0 +1,13 @@
|
||||
class AlterCustomWorkflows < ActiveRecord::Migration
|
||||
def self.up
|
||||
remove_column :custom_workflows, :project_id
|
||||
remove_column :custom_workflows, :is_enabled
|
||||
add_column :custom_workflows, :name, :string, :null => false
|
||||
add_column :custom_workflows, :description, :string, :null => false
|
||||
add_column :custom_workflows, :position, :integer, :null => false, :default => 1
|
||||
end
|
||||
|
||||
def self.down
|
||||
raise ActiveRecord::IrreversibleMigration
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,14 @@
|
||||
class CreateCustomWorkflowsProjects < ActiveRecord::Migration
|
||||
def self.up
|
||||
create_table :custom_workflows_projects, :force => true, :id => false do |t|
|
||||
t.references :project
|
||||
t.references :custom_workflow
|
||||
end
|
||||
add_index :custom_workflows_projects, [:project_id]
|
||||
add_index :custom_workflows_projects, [:custom_workflow_id]
|
||||
end
|
||||
|
||||
def self.down
|
||||
drop_table :custom_workflows_projects
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,9 @@
|
||||
class ChangeCustomWorkflowsDescriptionType < ActiveRecord::Migration
|
||||
def self.up
|
||||
change_column :custom_workflows, :description, :text, :null => false
|
||||
end
|
||||
|
||||
def self.down
|
||||
change_column :custom_workflows, :description, :string, :null => false
|
||||
end
|
||||
end
|
||||
33
db/migrate/20120831064944_create_example_workflow.rb
Normal file
33
db/migrate/20120831064944_create_example_workflow.rb
Normal file
@ -0,0 +1,33 @@
|
||||
class CreateExampleWorkflow < ActiveRecord::Migration
|
||||
def self.up
|
||||
CustomWorkflow.create(:name => "Duration/Done Ratio/Status correlation", :description => <<EOD, :script => <<EOS)
|
||||
Set up a correlation between the start date, due date, done ratio and status of issues.
|
||||
|
||||
* If done ratio is changed to 100% and status is "In Process", status changes to "Resolved"
|
||||
* If status is "New", "Resolved" or "Feedback" and done ratio is changed to value less than 100%, status changes to "In process"
|
||||
* If status is changed to "In process" and start date is not set, then it sets to current date
|
||||
* If status is changed to "Resolved" and end date is not set, then it set to due date
|
||||
|
||||
To use this script properly, turn off "Use current date as start date for new issues" option in the settings as this script already do it own way.
|
||||
EOD
|
||||
if @issue.done_ratio_changed?
|
||||
if @issue.done_ratio==100 && @issue.status_id=2
|
||||
@issue.status_id=3
|
||||
elsif [1,3,4].include?(@issue.status_id) && @issue.done_ratio<100
|
||||
@issue.status_id=2
|
||||
end
|
||||
end
|
||||
|
||||
if @issue.status_id_changed?
|
||||
if @issue.status_id==2
|
||||
@issue.start_date ||= Time.now
|
||||
end
|
||||
if @issue.status_id==3
|
||||
@issue.done_ratio = 100
|
||||
@issue.start_date ||= @issue.created_on
|
||||
@issue.due_date ||= Time.now
|
||||
end
|
||||
end
|
||||
EOS
|
||||
end
|
||||
end
|
||||
35
init.rb
Normal file
35
init.rb
Normal file
@ -0,0 +1,35 @@
|
||||
require 'redmine_custom_workflows/hooks'
|
||||
|
||||
to_prepare = Proc.new do
|
||||
unless Project.included_modules.include?(RedmineCustomWorkflows::ProjectPatch)
|
||||
Project.send(:include, RedmineCustomWorkflows::ProjectPatch)
|
||||
end
|
||||
unless ProjectsHelper.included_modules.include?(RedmineCustomWorkflows::ProjectsHelperPatch)
|
||||
ProjectsHelper.send(:include, RedmineCustomWorkflows::ProjectsHelperPatch)
|
||||
end
|
||||
unless Issue.included_modules.include?(RedmineCustomWorkflows::IssuePatch)
|
||||
Issue.send(:include, RedmineCustomWorkflows::IssuePatch)
|
||||
end
|
||||
end
|
||||
|
||||
if Redmine::VERSION::MAJOR >= 2
|
||||
Rails.configuration.to_prepare(&to_prepare)
|
||||
else
|
||||
require 'dispatcher'
|
||||
Dispatcher.to_prepare(:redmine_custom_workflows, &to_prepare)
|
||||
end
|
||||
|
||||
Redmine::Plugin.register :redmine_custom_workflows do
|
||||
name 'Redmine Custom Workflow plugin'
|
||||
author 'Anton Argirov'
|
||||
description 'Allows to create custom workflows for issues, defined in the plain Ruby language'
|
||||
version '0.0.1'
|
||||
url 'http://redmine.academ.org'
|
||||
|
||||
menu :admin_menu, :custom_workflows, {:controller => 'custom_workflows', :action => 'index'}, :caption => :label_custom_workflow_plural
|
||||
|
||||
project_module :custom_workflows_module do
|
||||
permission :manage_project_workflow, {}, :require => :member
|
||||
end
|
||||
|
||||
end
|
||||
7
lib/redmine_custom_workflows/hooks.rb
Normal file
7
lib/redmine_custom_workflows/hooks.rb
Normal file
@ -0,0 +1,7 @@
|
||||
module RedmineCustomWorkflows
|
||||
class Hooks < Redmine::Hook::ViewListener
|
||||
def view_layouts_base_html_head(context)
|
||||
stylesheet_link_tag :style, :plugin => 'redmine_custom_workflows'
|
||||
end
|
||||
end
|
||||
end
|
||||
44
lib/redmine_custom_workflows/issue_patch.rb
Normal file
44
lib/redmine_custom_workflows/issue_patch.rb
Normal file
@ -0,0 +1,44 @@
|
||||
module RedmineCustomWorkflows
|
||||
module IssuePatch
|
||||
unloadable
|
||||
|
||||
def self.included(base)
|
||||
base.send(:include, InstanceMethods)
|
||||
base.class_eval do
|
||||
before_save :custom_workflow_eval
|
||||
validate :validate_status
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
def validate_status
|
||||
if status_id_was != status_id && !new_statuses_allowed_to(User.current, new_record?).collect(&:id).include?(status_id)
|
||||
status_was = IssueStatus.find_by_id(status_id_was)
|
||||
status_new = IssueStatus.find_by_id(status_id)
|
||||
|
||||
errors.add :status, :new_status_invalid,
|
||||
:old_status => status_was && status_was.name,
|
||||
:new_status => status_new && status_new.name
|
||||
end
|
||||
end
|
||||
|
||||
def custom_workflow_eval
|
||||
return true unless project && project.module_enabled?(:custom_workflows_module)
|
||||
saved_attributes = attributes.dup
|
||||
project.custom_workflows.each do |workflow|
|
||||
begin
|
||||
workflow.eval_script(:issue => self)
|
||||
rescue WorkflowError => e
|
||||
errors.add :base, e.error
|
||||
return false
|
||||
rescue Exception => e
|
||||
Rails.logger.warn e
|
||||
errors.add :base, :custom_workflow_error
|
||||
return false
|
||||
end
|
||||
end
|
||||
saved_attributes == attributes || valid?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
14
lib/redmine_custom_workflows/project_patch.rb
Normal file
14
lib/redmine_custom_workflows/project_patch.rb
Normal file
@ -0,0 +1,14 @@
|
||||
module RedmineCustomWorkflows
|
||||
module ProjectPatch
|
||||
unloadable
|
||||
|
||||
def self.included(base)
|
||||
base.class_eval do
|
||||
has_and_belongs_to_many :custom_workflows
|
||||
#accepts_nested_attributes_for :custom_workflow, :update_only => true
|
||||
safe_attributes :custom_workflow_ids, :if =>
|
||||
lambda { |project, user| project.new_record? || user.allowed_to?(:manage_project_workflow, project) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
21
lib/redmine_custom_workflows/projects_helper_patch.rb
Normal file
21
lib/redmine_custom_workflows/projects_helper_patch.rb
Normal file
@ -0,0 +1,21 @@
|
||||
module RedmineCustomWorkflows
|
||||
module ProjectsHelperPatch
|
||||
unloadable
|
||||
|
||||
def self.included(base)
|
||||
base.send(:include, InstanceMethods)
|
||||
base.class_eval do
|
||||
alias_method_chain :project_settings_tabs, :custom_workflows
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
def project_settings_tabs_with_custom_workflows
|
||||
tabs = project_settings_tabs_without_custom_workflows
|
||||
tabs << {:name => 'custom_workflows', :action => :manage_project_workflow, :partial => 'projects/settings/custom_workflow',
|
||||
:label => :label_custom_workflow_plural} if User.current.allowed_to?(:manage_project_workflow, @project)
|
||||
tabs
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
x
Reference in New Issue
Block a user