Commit 2f17a28d by Félicie

plugin added

parent e0fed82c
......@@ -20,8 +20,6 @@
/lib/redmine/scm/adapters/mercurial/redminehelper.pyo
/log/*.log*
/log/mongrel_debug
/plugins/*
!/plugins/README
/public/dispatch.*
/public/plugin_assets/*
/public/themes/*
......
= Redmine Scrum plugin
== Main features
Take a look to https://redmine.ociotec.com/projects/redmine-plugin-scrum or
https://redmine.ociotec.com/projects/redmine-plugin-scrum/wiki for further
details.
== Access to plugin source code
If you want to access to the plugin source code repository you need to
support this plugin via https://www.patreon.com/ociotec (check tier with GIT
access).
If not you can always download a tagged version of it at:
https://redmine.ociotec.com/projects/redmine-plugin-scrum.
== Installation instructions
As any other Redmine plugin (you need GitLab access, take a look to
https://www.patreon.com/ociotec in order to gain this access):
cd <redmine-path>/plugins
git clone ssh://git@git.ociotec.com:10022/redmine/scrum.git
cd ..
bundle exec rake redmine:plugins:migrate
Finally restart your Redmine installation.
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
class ProductBacklogController < ApplicationController
menu_item :product_backlog
model_object Sprint
before_action :find_model_object,
:only => [:show, :edit, :update, :destroy, :edit_effort, :update_effort, :burndown,
:release_plan, :stats, :sort, :check_dependencies]
before_action :find_project_from_association,
:only => [:show, :edit, :update, :destroy, :edit_effort, :update_effort, :burndown,
:release_plan, :stats, :sort, :check_dependencies]
before_action :find_project_by_project_id,
:only => [:index, :new, :create]
before_action :find_subprojects,
:only => [:show, :burndown, :release_plan]
before_action :filter_by_project,
:only => [:show, :burndown, :release_plan]
before_action :check_issue_positions, :only => [:show]
before_action :calculate_stats, :only => [:show, :burndown, :release_plan]
before_action :authorize
helper :scrum
def index
unless @project.product_backlogs.empty?
redirect_to product_backlog_path(@project.product_backlogs.first)
else
render_error l(:error_no_sprints)
end
rescue
render_404
end
def show
unless @product_backlog.is_product_backlog?
render_404
end
end
def sort
# First, detect dependent issues.
error_messages = []
the_pbis = @product_backlog.pbis
the_pbis.each do |pbi|
pbi.init_journal(User.current)
pbi.position = params['pbi'].index(pbi.id.to_s) + 1
message = pbi.check_bad_dependencies(false)
error_messages << message unless message.nil?
end
if error_messages.empty?
# No dependency issue, we can sort.
the_pbis.each do |pbi|
pbi.save!
end
end
respond_to do |format|
format.json {render :json => error_messages.to_json}
end
end
def check_dependencies
@pbis_dependencies = @product_backlog.get_dependencies
respond_to do |format|
format.js
end
end
def new_pbi
@pbi = Issue.new
@pbi.project = @project
@pbi.author = User.current
@pbi.tracker = @project.trackers.find(params[:tracker_id])
@pbi.sprint = @product_backlog
respond_to do |format|
format.html
format.js
end
end
def create_pbi
begin
@continue = !(params[:create_and_continue].nil?)
@pbi = Issue.new(params[:issue])
@pbi.project = @project
@pbi.author = User.current
@pbi.sprint = @product_backlog
@pbi.save!
@pbi.story_points = params[:issue][:story_points]
rescue Exception => @exception
end
respond_to do |format|
format.js
end
end
MAX_SERIES = 10
def burndown
if @pbi_filter and @pbi_filter[:filter_by_project] == 'without-total'
@pbi_filter.delete(:filter_by_project)
without_total = true
end
@only_one = @project.children.visible.empty?
if @pbi_filter and @pbi_filter[:filter_by_project] == 'only-total'
@pbi_filter.delete(:filter_by_project)
@only_one = true
end
@x_axis_labels = []
all_projects_serie = burndown_for_project(@product_backlog, @project, l(:label_all), @pbi_filter, nil, @x_axis_labels)
@sprints_count = all_projects_serie[:sprints_count]
@velocity = all_projects_serie[:velocity]
@velocity_type = all_projects_serie[:velocity_type]
@series = []
@series << all_projects_serie unless without_total
if Scrum::Setting.product_burndown_extra_sprints == 0
extra_sprints = nil
else
extra_sprints = all_projects_serie[:data].length - @project.sprints.length
extra_sprints += Scrum::Setting.product_burndown_extra_sprints
end
unless @only_one
if @pbi_filter.empty? and @subprojects.count > 2
sub_series = recursive_burndown(@product_backlog, @project, extra_sprints)
@series += sub_series
end
@series.sort! { |serie_1, serie_2|
closed = ((serie_1[:project].respond_to?('closed?') and serie_1[:project].closed?) ? 1 : 0) -
((serie_2[:project].respond_to?('closed?') and serie_2[:project].closed?) ? 1 : 0)
if 0 != closed
closed
else
serie_2[:pending_story_points] <=> serie_1[:pending_story_points]
end
}
end
if @series.count > MAX_SERIES
@warning = l(:label_limited_to_n_series, :n => MAX_SERIES)
@series = @series.first(MAX_SERIES)
end
end
def release_plan
@sprints = []
velocity_all_pbis, velocity_scheduled_pbis, @sprints_count = @project.story_points_per_sprint(@pbi_filter)
@velocity_type = params[:velocity_type] || 'only_scheduled'
case @velocity_type
when 'all'
@velocity = velocity_all_pbis
when 'only_scheduled'
@velocity = velocity_scheduled_pbis
else
@velocity = params[:custom_velocity].to_f unless params[:custom_velocity].blank?
end
@velocity = 1.0 if @velocity.blank? or @velocity < 1.0
@total_story_points = 0.0
@pbis_with_estimation = 0
@pbis_without_estimation = 0
versions = {}
accumulated_story_points = @velocity
current_sprint = {:pbis => [], :story_points => 0.0, :versions => []}
@product_backlog.pbis(@pbi_filter).each do |pbi|
if pbi.story_points
@pbis_with_estimation += 1
story_points = pbi.story_points.to_f
@total_story_points += story_points
while accumulated_story_points < story_points
@sprints << current_sprint
accumulated_story_points += @velocity
current_sprint = {:pbis => [], :story_points => 0.0, :versions => []}
end
accumulated_story_points -= story_points
current_sprint[:pbis] << pbi
current_sprint[:story_points] += story_points
if pbi.fixed_version
versions[pbi.fixed_version.id] = {:version => pbi.fixed_version,
:sprint => @sprints.count}
end
else
@pbis_without_estimation += 1
end
end
if current_sprint and (current_sprint[:pbis].count > 0)
@sprints << current_sprint
end
versions.values.each do |info|
@sprints[info[:sprint]][:versions] << info[:version]
end
end
private
def check_issue_positions
check_issue_position(Issue.where(:sprint_id => @product_backlog, :position => nil))
end
def check_issue_position(issue)
if issue.is_a?(Issue)
if issue.position.nil?
issue.reset_positions_in_list
issue.save!
issue.reload
end
elsif issue.respond_to?(:each)
issue.each do |i|
check_issue_position(i)
end
else
raise "Invalid type: #{issue.inspect}"
end
end
def find_subprojects
if @project and @product_backlog
@subprojects = [[l(:label_all), calculate_path(@product_backlog)]]
@subprojects << [l(:label_only_total), calculate_path(@product_backlog, 'only-total')] if action_name == 'burndown'
@subprojects << [l(:label_all_but_total), calculate_path(@product_backlog, 'without-total')] if action_name == 'burndown'
@subprojects += find_recursive_subprojects(@project, @product_backlog)
end
end
def find_recursive_subprojects(project, product_backlog, tabs = '')
options = [[tabs + project.name, calculate_path(product_backlog, project)]]
project.children.visible.to_a.each do |child|
options += find_recursive_subprojects(child, product_backlog, tabs + '» ')
end
return options
end
def filter_by_project
@pbi_filter = {}
unless params[:filter_by_project].blank?
@pbi_filter = {:filter_by_project => params[:filter_by_project]}
end
end
def calculate_path(product_backlog, project = nil)
options = {}
if 'burndown' == action_name
path_method = :burndown_product_backlog_path
elsif 'release_plan' == action_name
path_method = :release_plan_product_backlog_path
else
path_method = :product_backlog_path
end
if ['burndown', 'release_plan'].include?(action_name)
options[:velocity_type] = params[:velocity_type] unless params[:velocity_type].blank?
options[:custom_velocity] = params[:custom_velocity] unless params[:custom_velocity].blank?
end
if project.nil?
project_id = nil
elsif project == 'without-total'
options[:filter_by_project] = 'without-total'
project_id = 'without-total'
elsif project == 'only-total'
options[:filter_by_project] = 'only-total'
project_id = 'only-total'
else
options[:filter_by_project] = project.id
project_id = project.id.to_s
end
result = send(path_method, product_backlog, options)
if (project.nil? and params[:filter_by_project].blank?) or
(project_id == params[:filter_by_project])
@selected_subproject = result
end
return result
end
def burndown_for_project(product_backlog, project, label, pbi_filter = {}, extra_sprints = nil, x_axis_labels = nil)
serie = {:data => [],
:label => label,
:project => pbi_filter.include?(:filter_by_project) ?
Project.find(pbi_filter[:filter_by_project]) :
project}
project.sprints.each do |sprint|
x_axis_labels << sprint.name unless x_axis_labels.nil?
serie[:data] << {:story_points => sprint.story_points(pbi_filter).round(2),
:pending_story_points => 0}
end
velocity_all_pbis, velocity_scheduled_pbis, serie[:sprints_count] = project.story_points_per_sprint(pbi_filter)
serie[:velocity_type] = params[:velocity_type] || 'only_scheduled'
case serie[:velocity_type]
when 'all'
serie[:velocity] = velocity_all_pbis
when 'only_scheduled'
serie[:velocity] = velocity_scheduled_pbis
else
serie[:velocity] = params[:custom_velocity].to_f unless params[:custom_velocity].blank?
end
serie[:velocity] = 1.0 if serie[:velocity].blank? or serie[:velocity] < 1.0
pending_story_points = product_backlog.story_points(pbi_filter)
serie[:pending_story_points] = pending_story_points
new_sprints = 1
while pending_story_points > 0 and (!extra_sprints or new_sprints <= extra_sprints)
x_axis_labels << "#{l(:field_sprint)} +#{new_sprints}" unless x_axis_labels.nil?
serie[:data] << {:story_points => ((serie[:velocity] <= pending_story_points) ?
serie[:velocity] : pending_story_points).round(2),
:pending_story_points => 0}
pending_story_points -= serie[:velocity]
new_sprints += 1
end
for i in 0..(serie[:data].length - 1)
others = serie[:data][(i + 1)..(serie[:data].length - 1)]
serie[:data][i][:pending_story_points] = serie[:data][i][:story_points] +
(others.blank? ? 0.0 : others.collect{|other| other[:story_points]}.sum.round(2))
serie[:data][i][:story_points_tooltip] = l(:label_pending_story_points,
:pending_story_points => serie[:data][i][:pending_story_points],
:sprint => serie[:data][i][:axis_label],
:story_points => serie[:data][i][:story_points])
end
return serie
end
def recursive_burndown(product_backlog, project, extra_sprints)
series = [burndown_for_project(@product_backlog, @project, project.name,
{:filter_by_project => project.id}, extra_sprints)]
project.children.visible.to_a.each do |child|
series += recursive_burndown(product_backlog, child, extra_sprints)
end
return series
end
def serie_sps(serie, index)
(serie[:data].count <= index) ? 0.0 : serie[:data][index][:story_points]
end
def calculate_stats
if Scrum::Setting.show_project_totals_on_backlog
total_pbis_count = @project.pbis_count(@pbi_filter)
closed_pbis_count = @project.closed_pbis_count(@pbi_filter)
total_sps_count = @project.total_sps(@pbi_filter)
closed_sps_count = @project.closed_sps(@pbi_filter)
closed_total_percentage = (total_sps_count == 0.0) ? 0.0 : ((closed_sps_count * 100.0) / total_sps_count)
@stats = {:total_pbis_count => total_pbis_count,
:closed_pbis_count => closed_pbis_count,
:total_sps_count => total_sps_count,
:closed_sps_count => closed_sps_count,
:closed_total_percentage => closed_total_percentage}
end
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
class ScrumController < ApplicationController
menu_item :product_backlog, :except => [:stats]
menu_item :overview, :only => [:stats]
before_action :find_issue,
:only => [:change_story_points, :change_remaining_story_points,
:change_pending_effort,
:change_assigned_to, :new_time_entry,
:create_time_entry, :edit_task, :update_task,
:change_pending_efforts]
before_action :find_sprint,
:only => [:new_pbi, :create_pbi,
:move_not_closed_pbis_to_last_sprint]
before_action :find_pbi,
:only => [:new_task, :create_task, :edit_pbi, :update_pbi,
:move_pbi, :move_to_last_sprint,
:move_to_product_backlog]
before_action :find_project_by_project_id,
:only => [:stats]
before_action :authorize,
:except => [:new_pbi, :create_pbi, :new_task, :create_task,
:move_to_last_sprint,
:move_not_closed_pbis_to_last_sprint,
:move_to_product_backlog,
:new_time_entry, :create_time_entry]
before_action :authorize_add_issues,
:only => [:new_pbi, :create_pbi, :new_task, :create_task]
before_action :authorize_edit_issues,
:only => [:move_to_last_sprint,
:move_not_closed_pbis_to_last_sprint,
:move_to_product_backlog]
before_action :authorize_log_time,
:only => [:new_time_entry, :create_time_entry]
helper :custom_fields
helper :projects
helper :scrum
helper :timelog
def change_story_points
begin
@issue.story_points = params[:value]
status = 200
rescue
status = 503
end
render :body => nil, :status => status
end
def change_remaining_story_points
@issue.remaining_story_points = params[:value]
render :body => nil, :status => status
end
def change_pending_effort
@issue.pending_effort = params[:value]
render :body => nil, :status => 200
end
def change_pending_efforts
params['pending_efforts'].each_pair do |id, value|
pending_effort = PendingEffort.find(id)
raise "Invalid pending effort ID #{id}" if pending_effort.nil?
raise "Pending effort ID #{id} is not owned by this issue" if pending_effort.issue_id != @issue.id
if value.blank?
pending_effort.delete
else
pending_effort.effort = value.to_f
pending_effort.save!
end
end
redirect_to issue_path(@issue)
end
def change_assigned_to
@issue.init_journal(User.current)
@issue.assigned_to = params[:value].blank? ? nil : User.find(params[:value].to_i)
@issue.save!
render_task(@project, @issue, params)
end
def new_time_entry
@pbi_status_id = params[:pbi_status_id]
@other_pbi_status_ids = params[:other_pbi_status_ids]
@task_id = params[:task_id]
respond_to do |format|
format.js
end
end
def create_time_entry
begin
time_entry = TimeEntry.new(params.require(:time_entry).permit(:hours, :spent_on, :comments, :activity_id, :user_id))
time_entry.project_id = @project.id
time_entry.issue_id = @issue.id
time_entry.user_id = params[:time_entry][:user_id]
call_hook(:controller_timelog_edit_before_save, {:params => params, :time_entry => time_entry})
time_entry.save!
rescue Exception => @exception
logger.error("Exception: #{@exception.inspect}")
end
respond_to do |format|
format.js
end
end
def new_pbi
@pbi = Issue.new
@pbi.project = @project
@pbi.tracker = @project.trackers.find(params[:tracker_id])
@pbi.status = @pbi.default_status
@pbi.author = User.current
@pbi.sprint = @sprint
@top = true unless params[:top].nil? or (params[:top] == 'false')
respond_to do |format|
format.html
format.js
end
end
def create_pbi
begin
@continue = !(params[:create_and_continue].nil?)
@top = !(params[:top].nil?)
@pbi = Issue.new
if params[:issue][:project_id]
@pbi.project_id = params[:issue][:project_id]
else
@pbi.project = @project
end
@pbi.author = User.current
@pbi.tracker_id = params[:issue][:tracker_id]
@pbi.set_on_top if @top
@pbi.sprint = @sprint
update_attributes(@pbi, params)
@pbi.save!
rescue Exception => @exception
logger.error("Exception: #{@exception.inspect}")
end
respond_to do |format|
format.js
end
end
def edit_pbi
respond_to do |format|
format.js
end
end
def update_pbi
begin
@pbi.init_journal(User.current, params[:issue][:notes])
update_attributes(@pbi, params)
@pbi.save!
rescue Exception => @exception
logger.error("Exception: #{@exception.inspect}")
end
respond_to do |format|
format.js
end
end
def move_pbi
begin
@position = params[:position]
case params[:position]
when 'top', 'bottom'
@pbi.move_pbi_to(@position)
when 'before'
@other_pbi = params[:before_other_pbi]
@pbi.move_pbi_to(@position, @other_pbi)
when 'after'
@other_pbi = params[:after_other_pbi]
@pbi.move_pbi_to(@position, @other_pbi)
else
raise "Invalid position: #{@position.inspect}"
end
rescue Exception => @exception
logger.error("Exception: #{@exception.inspect}")
end
end
def move_to_last_sprint
begin
raise "The project hasn't defined any Sprint yet" unless @project.last_sprint
@previous_sprint = @pbi.sprint
move_issue_to_sprint(@pbi, @project.last_sprint)
rescue Exception => @exception
logger.error("Exception: #{@exception.inspect}")
end
respond_to do |format|
format.js
end
end
def move_not_closed_pbis_to_last_sprint
begin
last_sprint = @project.last_sprint
raise "The project hasn't defined any Sprint yet" unless last_sprint
not_closed_pbis = @sprint.not_closed_pbis
if not_closed_pbis.empty?
flash[:notice] = l(:label_nothing_to_move)
else
not_closed_pbis_links = []
not_closed_pbis.each do |pbi|
link = view_context.link_to_issue(pbi,
:project => pbi.project != @project,
:tracker => true)
not_closed_pbis_links << link
move_issue_to_sprint(pbi, last_sprint)
end
flash[:notice] = l(:label_pbis_moved,
:pbis => not_closed_pbis_links.join(', '))
end
rescue Exception => exception
logger.error("Exception: #{exception.inspect}")
flash[:error] = exception
end
redirect_to sprint_path(@sprint)
end
def move_to_product_backlog
begin
product_backlog = @project.product_backlogs.find(params[:id])
move_issue_to_sprint(@pbi, product_backlog)
rescue Exception => @exception
logger.error("Exception: #{@exception.inspect}")
end
respond_to do |format|
format.js
end
end
def new_task
@task = Issue.new
@task.project = @pbi.project
@task.tracker = Tracker.find(params[:tracker_id])
@task.status = @task.default_status
@task.parent = @pbi
@task.author = User.current
@task.sprint = @sprint
if Scrum::Setting.inherit_pbi_attributes
@task.inherit_from_issue(@pbi)
end
respond_to do |format|
format.html
format.js
end
rescue Exception => e
logger.error("Exception: #{e.inspect}")
render_404
end
def create_task
begin
@continue = !(params[:create_and_continue].nil?)
@task = Issue.new
if params[:issue][:project_id]
@task.project_id = params[:issue][:project_id]
else
@task.project = @pbi.project
end
@task.parent_issue_id = @pbi.id
@task.author = User.current
@task.sprint = @sprint
@task.tracker_id = params[:issue][:tracker_id]
update_attributes(@task, params)
@task.save!
@task.pending_effort = params[:issue][:pending_effort]
rescue Exception => @exception
end
respond_to do |format|
format.js
end
end
def edit_task
respond_to do |format|
format.js
end
end
def update_task
begin
@issue.init_journal(User.current, params[:issue][:notes])
@old_status = @issue.status
update_attributes(@issue, params)
@issue.save!
@issue.pending_effort = params[:issue][:pending_effort]
rescue Exception => @exception
logger.error("Exception: #{@exception.inspect}")
end
respond_to do |format|
format.js do
render "scrum/update_issue"
end
end
end
def stats
if User.current.allowed_to?(:view_time_entries, @project)
cond = @project.project_condition(Setting.display_subprojects_issues?)
@total_hours = TimeEntry.visible.where(cond).sum(:hours).to_f
end
@closed_story_points_per_sprint = @project.closed_story_points_per_sprint
@closed_story_points_per_sprint_chart = {:id => 'closed_story_points_per_sprint', :height => 400}
@hours_per_story_point = @project.hours_per_story_point
@hours_per_story_point_chart = {:id => 'hours_per_story_point', :height => 400}
@sps_by_pbi_category, @sps_by_pbi_category_total = @project.sps_by_category
@sps_by_pbi_type, @sps_by_pbi_type_total = @project.sps_by_pbi_type
@effort_by_activity, @effort_by_activity_total = @project.effort_by_activity
end
private
def render_task(project, task, params)
render :partial => "post_its/sprint_board/task",
:status => 200,
:locals => {:project => project,
:task => task,
:pbi_status_id => params[:pbi_status_id],
:other_pbi_status_ids => params[:other_pbi_status_ids].split(","),
:task_id => params[:task_id],
:read_only => false}
end
def find_sprint
@sprint = Sprint.find(params[:sprint_id])
@project = @sprint.project
rescue
logger.error("Sprint #{params[:sprint_id]} not found")
render_404
end
def find_pbi
@pbi = Issue.find(params[:pbi_id])
@sprint = @pbi.sprint
@project = @sprint.project
rescue
logger.error("PBI #{params[:pbi_id]} not found")
render_404
end
def authorize_action_on_current_project(action)
if User.current.allowed_to?(action, @project)
return true
else
render_403
return false
end
end
def authorize_add_issues
authorize_action_on_current_project(:add_issues)
end
def authorize_log_time
authorize_action_on_current_project(:log_time)
end
def authorize_edit_issues
authorize_action_on_current_project(:edit_issues)
end
def update_attributes(issue, params)
issue.status_id = params[:issue][:status_id] unless params[:issue][:status_id].nil?
raise 'New status is not allowed' unless issue.new_statuses_allowed_to.include?(issue.status)
issue.project_id = params[:issue][:project_id] unless params[:issue][:project_id].nil?
issue.assigned_to_id = params[:issue][:assigned_to_id] unless params[:issue][:assigned_to_id].nil?
issue.subject = params[:issue][:subject] unless params[:issue][:subject].nil?
issue.priority_id = params[:issue][:priority_id] unless params[:issue][:priority_id].nil?
issue.estimated_hours = params[:issue][:estimated_hours].gsub(',', '.') if issue.safe_attribute?(:estimated_hours) and (!(params[:issue][:estimated_hours].nil?))
issue.done_ratio = params[:issue][:done_ratio] unless params[:issue][:done_ratio].nil?
issue.description = params[:issue][:description] unless params[:issue][:description].nil?
issue.category_id = params[:issue][:category_id] if issue.safe_attribute?(:category_id) and (!(params[:issue][:category_id].nil?))
issue.fixed_version_id = params[:issue][:fixed_version_id] if issue.safe_attribute?(:fixed_version_id) and (!(params[:issue][:fixed_version_id].nil?))
issue.start_date = params[:issue][:start_date] if issue.safe_attribute?(:start_date) and (!(params[:issue][:start_date].nil?))
issue.due_date = params[:issue][:due_date] if issue.safe_attribute?(:due_date) and (!(params[:issue][:due_date].nil?))
issue.custom_field_values = params[:issue][:custom_field_values] unless params[:issue][:custom_field_values].nil?
end
def move_issue_to_sprint(issue, sprint)
issue.init_journal(User.current)
issue.sprint = sprint
issue.save!
issue.children.each do |child|
unless child.closed?
move_issue_to_sprint(child, sprint)
end
end
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
class SprintsController < ApplicationController
menu_item :sprint
model_object Sprint
before_action :find_model_object,
:only => [:show, :edit, :update, :destroy, :edit_effort, :update_effort, :burndown,
:stats, :sort]
before_action :find_project_from_association,
:only => [:show, :edit, :update, :destroy, :edit_effort, :update_effort, :burndown,
:stats, :sort]
before_action :find_project_by_project_id,
:only => [:index, :new, :create, :change_issue_status, :burndown_index,
:stats_index]
before_action :find_pbis, :only => [:sort]
before_action :find_subprojects,
:only => [:burndown]
before_action :filter_by_project,
:only => [:burndown]
before_action :calculate_stats, :only => [:show, :burndown, :stats]
before_action :authorize
helper :custom_fields
helper :scrum
helper :timelog
include Redmine::Utils::DateCalculation
def index
if (current_sprint = @project.current_sprint)
redirect_to sprint_path(current_sprint)
else
render_error l(:error_no_sprints)
end
rescue
render_404
end
def show
redirect_to project_product_backlog_index_path(@project) if @sprint.is_product_backlog?
end
def new
@sprint = Sprint.new(:project => @project, :is_product_backlog => params[:create_product_backlog])
if @sprint.is_product_backlog
@sprint.name = l(:label_product_backlog)
@sprint.sprint_start_date = @sprint.sprint_end_date = Date.today
elsif @project.sprints.empty?
@sprint.name = Scrum::Setting.default_sprint_name
@sprint.sprint_start_date = Date.today
@sprint.sprint_end_date = add_working_days(@sprint.sprint_start_date, Scrum::Setting.default_sprint_days - 1)
@sprint.shared = Scrum::Setting.default_sprint_shared
else
last_sprint = @project.sprints.last
result = last_sprint.name.match(/^(.*)(\d+)(.*)$/)
@sprint.name = result.nil? ? Scrum::Setting.default_sprint_name : (result[1] + (result[2].to_i + 1).to_s + result[3])
@sprint.description = last_sprint.description
@sprint.sprint_start_date = next_working_date(last_sprint.sprint_end_date + 1)
last_sprint_duration = last_sprint.sprint_end_date - last_sprint.sprint_start_date
@sprint.sprint_end_date = next_working_date(@sprint.sprint_start_date + last_sprint_duration)
@sprint.shared = last_sprint.shared
end
end
def create
is_product_backlog = !(params[:create_product_backlog].nil?)
@sprint = Sprint.new(:user => User.current, :project => @project, :is_product_backlog => is_product_backlog)
@sprint.safe_attributes = params[:sprint]
if request.post? and @sprint.save
if is_product_backlog
@project.product_backlogs << @sprint
raise 'Fail to update project with product backlog' unless @project.save!
end
flash[:notice] = l(:notice_successful_create)
redirect_back_or_default settings_project_path(@project, :tab => is_product_backlog ? 'product_backlogs' : 'sprints')
else
render :action => :new
end
rescue ActiveRecord::RecordNotFound
render_404
end
def edit
@product_backlog = @sprint if @sprint.is_product_backlog
end
def update
@sprint.safe_attributes = params[:sprint]
if @sprint.save
flash[:notice] = l(:notice_successful_update)
redirect_back_or_default settings_project_path(@project, :tab => 'sprints')
else
render :action => :edit
end
end
def destroy
if @sprint.issues.any?
flash[:error] = l(:notice_sprint_has_issues)
else
@sprint.destroy
end
rescue
flash[:error] = l(:notice_unable_delete_sprint)
ensure
redirect_to settings_project_path(@project, :tab => 'sprints')
end
def change_issue_status
result = params[:task].match(/^(task|pbi)_(\d+)$/)
issue_id = result[2].to_i
@issue = Issue.find(issue_id)
@old_status = @issue.status
# Do not change issue status if not necessary
new_status = IssueStatus.find(params[:status].to_i)
# Manage case where new status is allowed
if new_status && @issue.new_statuses_allowed_to.include?(new_status)
@issue.init_journal(User.current)
@issue.status = new_status
@issue.save!
else
# Exception replaced by an instance variable
# Create error message if new status not allowed
@error_message = l(:error_new_status_no_allowed,
:status_from => @old_status,
:status_to => new_status)
end
respond_to do |format|
format.js { render 'scrum/update_issue' }
end
end
def edit_effort
end
def update_effort
params[:user].each_pair do |user_id, days|
user_id = user_id.to_i
days.each_pair do |day, effort|
day = day.to_i
date = @sprint.sprint_start_date + day.to_i
sprint_effort = SprintEffort.where(:sprint_id => @sprint.id,
:user_id => user_id,
:date => date).first
if sprint_effort.nil?
unless effort.blank?
sprint_effort = SprintEffort.new(:sprint_id => @sprint.id,
:user_id => user_id,
:date => @sprint.sprint_start_date + day,
:effort => effort)
end
elsif effort.blank?
sprint_effort.destroy
sprint_effort = nil
else
sprint_effort.effort = effort
end
sprint_effort.save! unless sprint_effort.nil?
end
end
flash[:notice] = l(:notice_successful_update)
redirect_back_or_default settings_project_path(@project, :tab => 'sprints')
end
def burndown_index
if @project.last_sprint
redirect_to burndown_sprint_path(@project.last_sprint, :type => params[:type])
else
render_error l(:error_no_sprints)
end
rescue Exception => exception
render_404
end
MAX_SERIES = 10
def burndown
if @sprint.is_product_backlog
redirect_to(burndown_product_backlog_path(@sprint))
else
if @pbi_filter and @pbi_filter[:filter_by_project] == 'without-total'
@pbi_filter.delete(:filter_by_project)
without_total = true
else
without_total = false
end
@only_one = @project.children.visible.empty?
@x_axis_labels = []
serie_label = @only_one ? l(:field_pending_effort) : "#{l(:field_pending_effort)} (#{l(:label_all)})"
all_projects_serie = burndown_for_project(@sprint, @project, serie_label, @pbi_filter, @x_axis_labels)
@series = []
@series << all_projects_serie unless without_total
unless @only_one
if @pbi_filter.empty? and @subprojects.count > 2
sub_series = recursive_burndown(@sprint, @project)
@series += sub_series
end
@series.sort! { |serie_1, serie_2|
closed = ((serie_1[:project].respond_to?('closed?') and serie_1[:project].closed?) ? 1 : 0) -
((serie_2[:project].respond_to?('closed?') and serie_2[:project].closed?) ? 1 : 0)
if 0 != closed
closed
else
serie_2[:max_value] <=> serie_1[:max_value]
end
}
end
if params[:type] == 'effort'
@series = [estimated_effort_serie(@sprint)] + @series
end
if @series.count > MAX_SERIES
@warning = l(:label_limited_to_n_series, :n => MAX_SERIES)
@series = @series.first(MAX_SERIES)
end
end
end
def stats_index
if @project.last_sprint
redirect_to stats_sprint_path(@project.last_sprint)
else
render_error l(:error_no_sprints)
end
rescue
render_404
end
def stats
@days = []
@members_efforts = {}
@estimated_efforts_totals = {:days => {}, :total => 0.0}
@done_efforts_totals = {:days => {}, :total => 0.0}
((@sprint.sprint_start_date)..(@sprint.sprint_end_date)).each do |date|
if @sprint.efforts.where(['date = ?', date]).count > 0
@days << {:date => date, :label => "#{I18n.l(date, :format => :scrum_day)} #{date.day}"}
if User.current.allowed_to?(:view_sprint_stats_by_member, @project)
estimated_effort_conditions = ['date = ?', date]
done_effort_conditions = ['spent_on = ?', date]
else
estimated_effort_conditions = ['date = ? AND user_id = ?', date, User.current.id]
done_effort_conditions = ['spent_on = ? AND user_id = ?', date, User.current.id]
end
@sprint.efforts.where(estimated_effort_conditions).each do |sprint_effort|
if sprint_effort.effort
init_members_efforts(@members_efforts, sprint_effort.user)
member_estimated_efforts_days = init_member_efforts_days(@members_efforts,
@sprint,
sprint_effort.user,
date,
true)
member_estimated_efforts_days[date] += sprint_effort.effort
@members_efforts[sprint_effort.user.id][:estimated_efforts][:total] += sprint_effort.effort
@estimated_efforts_totals[:days][date] = 0.0 unless @estimated_efforts_totals[:days].include?(date)
@estimated_efforts_totals[:days][date] += sprint_effort.effort
@estimated_efforts_totals[:total] += sprint_effort.effort
end
end
project_efforts_for_stats(@project, @sprint, date, done_effort_conditions, @members_efforts, @done_efforts_totals)
end
end
@members_efforts = @members_efforts.values.sort{|a, b| a[:member] <=> b[:member]}
@sps_by_pbi_category, @sps_by_pbi_category_total = @sprint.sps_by_pbi_category
@sps_by_pbi_type, @sps_by_pbi_type_total = @sprint.sps_by_pbi_type
@sps_by_pbi_creation_date, @sps_by_pbi_creation_date_total = @sprint.sps_by_pbi_creation_date
@effort_by_activity, @effort_by_activity_total = @sprint.time_entries_by_activity
if User.current.allowed_to?(:view_sprint_stats_by_member, @project)
@efforts_by_member_and_activity = @sprint.efforts_by_member_and_activity
@efforts_by_member_and_activity_chart = {:id => 'stats_efforts_by_member_and_activity', :height => 400}
end
end
def sort
new_pbis_order = []
params.keys.each do |param|
id = param.scan(/pbi\_(\d+)/)
new_pbis_order << id[0][0].to_i if id and id[0] and id[0][0]
end
@pbis.each do |pbi|
if (index = new_pbis_order.index(pbi.id))
pbi.position = index + 1
pbi.save!
end
end
render :body => nil
end
private
def init_members_efforts(members_efforts, member)
unless members_efforts.include?(member.id)
members_efforts[member.id] = {
:member => member,
:estimated_efforts => {
:days => {},
:total => 0.0
},
:done_efforts => {
:days => {},
:total => 0.0
}
}
end
end
def init_member_efforts_days(members_efforts, sprint, member, date, estimated)
member_efforts_days = members_efforts[member.id][estimated ? :estimated_efforts : :done_efforts][:days]
unless member_efforts_days.include?(date)
member_efforts_days[date] = 0.0
end
return member_efforts_days
end
def project_efforts_for_stats(project, sprint, date, done_effort_conditions, members_efforts, done_efforts_totals)
project.time_entries.where(done_effort_conditions).each do |time_entry|
if time_entry.hours
init_members_efforts(members_efforts, time_entry.user)
member_done_efforts_days = init_member_efforts_days(members_efforts,
sprint,
time_entry.user,
date,
false)
member_done_efforts_days[date] += time_entry.hours
members_efforts[time_entry.user.id][:done_efforts][:total] += time_entry.hours
done_efforts_totals[:days][date] = 0.0 unless done_efforts_totals[:days].include?(date)
done_efforts_totals[:days][date] += time_entry.hours
done_efforts_totals[:total] += time_entry.hours
end
end
if sprint.shared
project.children.visible.each do |sub_project|
project_efforts_for_stats(sub_project, sprint, date, done_effort_conditions, members_efforts, done_efforts_totals)
end
end
end
def find_pbis
@pbis = @sprint.pbis
rescue
render_404
end
def calculate_stats
if Scrum::Setting.show_project_totals_on_sprint
total_pbis_count = @sprint.pbis().count
closed_pbis_count = @sprint.closed_pbis().count
total_sps_count = @sprint.story_points()
closed_sps_count = @sprint.closed_story_points()
closed_total_percentage = (total_sps_count == 0.0) ? 0.0 : ((closed_sps_count * 100.0) / total_sps_count)
@stats = {:total_pbis_count => total_pbis_count,
:closed_pbis_count => closed_pbis_count,
:total_sps_count => total_sps_count,
:closed_sps_count => closed_sps_count,
:closed_total_percentage => closed_total_percentage}
end
end
def find_subprojects
if @project and @sprint
@subprojects = [[l(:label_all), calculate_path(@sprint)]]
@subprojects << [l(:label_all_but_total), calculate_path(@sprint, 'without-total')] if action_name == 'burndown'
@subprojects += find_recursive_subprojects(@project, @sprint)
end
end
def find_recursive_subprojects(project, sprint, tabs = '')
options = [[tabs + project.name, calculate_path(sprint, project)]]
project.children.visible.to_a.each do |child|
options += find_recursive_subprojects(child, sprint, tabs + '» ')
end
return options
end
def filter_by_project
@pbi_filter = {}
unless params[:filter_by_project].blank?
@pbi_filter = {:filter_by_project => params[:filter_by_project]}
end
end
def calculate_path(sprint, project = nil)
options = {}
path_method = :burndown_sprint_path
if ['burndown'].include?(action_name)
options[:type] = params[:type] unless params[:type].blank?
end
if project.nil?
project_id = nil
elsif project == 'without-total'
options[:filter_by_project] = 'without-total'
project_id = 'without-total'
else
options[:filter_by_project] = project.id
project_id = project.id.to_s
end
result = send(path_method, sprint, options)
if (project.nil? and params[:filter_by_project].blank?) or
(project_id == params[:filter_by_project])
@selected_subproject = result
end
return result
end
def burndown_for_project(sprint, project, label, pbi_filter = {}, x_axis_labels = nil)
serie = {:data => [],
:label => label,
:project => pbi_filter.include?(:filter_by_project) ?
Project.find(pbi_filter[:filter_by_project]) :
project,
:max_value => 0.0}
if params[:type] == 'sps'
last_sps = sprint.completed_sps_at_day(sprint.sprint_start_date - 1, pbi_filter)
last_day = nil
last_label = l(:label_begin) if Scrum::Setting.sprint_burndown_day_zero?
sprint.completed_sps_by_day(pbi_filter).each do |date, sps|
date_label = "#{I18n.l(date, :format => :scrum_day)} #{date.day}"
last_label = date_label unless Scrum::Setting.sprint_burndown_day_zero?
x_axis_labels << last_label unless x_axis_labels.nil?
serie[:max_value] = last_sps if last_sps and last_sps > serie[:max_value]
serie[:data] << {:day => date,
:pending_sps => last_sps,
:pending_sps_tooltip => l(:label_pending_sps_tooltip,
:date => last_label,
:sps => last_sps)}
last_sps = sps
last_day = date.day
last_label = date_label if Scrum::Setting.sprint_burndown_day_zero?
end
if serie[:data].any?
unless x_axis_labels.nil?
if Scrum::Setting.sprint_burndown_day_zero?
x_axis_labels << last_label
else
x_axis_labels[x_axis_labels.length - 1] = l(:label_end)
end
end
serie[:max_value] = last_sps if last_sps and last_sps > serie[:max_value]
serie[:data].last[:pending_sps_tooltip] = l(:label_pending_sps_tooltip,
:date => last_label,
:sps => last_sps)
end
@type = :sps
else
sprint_tasks = sprint.tasks(pbi_filter)
last_pending_effort = pending_effort_at_day(sprint_tasks, sprint.sprint_start_date - 1)
last_day = nil
last_label = l(:label_begin) if Scrum::Setting.sprint_burndown_day_zero?
((sprint.sprint_start_date)..(sprint.sprint_end_date)).each do |date|
sprint_efforts = sprint.efforts.where(['date >= ?', date])
if sprint_efforts.any?
if date <= Date.today
pending_effort = pending_effort_at_day(sprint_tasks, date)
end
date_label = "#{I18n.l(date, :format => :scrum_day)} #{date.day}"
last_label = date_label unless Scrum::Setting.sprint_burndown_day_zero?
x_axis_labels << last_label unless x_axis_labels.nil?
serie[:max_value] = last_pending_effort if last_pending_effort and last_pending_effort > serie[:max_value]
serie[:data] << {:day => date,
:effort => last_pending_effort,
:tooltip => l(:label_pending_effort_tooltip,
:date => last_label,
:hours => last_pending_effort)}
last_pending_effort = pending_effort
last_day = date.day
last_label = date_label if Scrum::Setting.sprint_burndown_day_zero?
end
end
last_label = l(:label_end) unless Scrum::Setting.sprint_burndown_day_zero?
x_axis_labels << last_label unless x_axis_labels.nil?
serie[:max_value] = last_pending_effort if last_pending_effort and last_pending_effort > serie[:max_value]
serie[:data] << {:day => last_day,
:effort => last_pending_effort,
:tooltip => l(:label_pending_effort_tooltip,
:date => last_label,
:hours => last_pending_effort)}
@type = :effort
end
return serie
end
def estimated_effort_serie(sprint)
serie = {:data => [],
:label => l(:label_estimated_effort)}
last_day = nil
last_label = l(:label_begin) if Scrum::Setting.sprint_burndown_day_zero
((sprint.sprint_start_date)..(sprint.sprint_end_date)).each do |date|
sprint_efforts = sprint.efforts.where(['date >= ?', date])
if sprint_efforts.any?
estimated_effort = sprint_efforts.collect{|effort| effort.effort}.compact.sum
date_label = "#{I18n.l(date, :format => :scrum_day)} #{date.day}"
last_label = date_label unless Scrum::Setting.sprint_burndown_day_zero
serie[:data] << {:day => date,
:effort => estimated_effort,
:tooltip => l(:label_estimated_effort_tooltip,
:date => last_label,
:hours => estimated_effort)}
last_day = date.day
last_label = date_label if Scrum::Setting.sprint_burndown_day_zero
end
end
last_label = l(:label_end) unless Scrum::Setting.sprint_burndown_day_zero
serie[:data] << {:day => last_day,
:effort => 0,
:tooltip => l(:label_estimated_effort_tooltip,
:date => last_label,
:hours => 0)}
return serie
end
def recursive_burndown(sprint, project)
serie_name = "#{l(:field_pending_effort)} (#{project.name})"
series = [burndown_for_project(@sprint, @project, serie_name,
{:filter_by_project => project.id})]
project.children.visible.to_a.each do |child|
series += recursive_burndown(sprint, child)
end
return series
end
def pending_effort_at_day(tasks, date)
efforts = []
tasks.each do |task|
if task.use_in_burndown?
task_efforts = task.pending_efforts.where(['date <= ?', date])
efforts << (task_efforts.any? ? task_efforts.last.effort : task.estimated_hours)
end
end
return efforts.compact.sum
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
module ScrumHelper
include ProjectsHelper
def render_pbi_left_header(pbi)
parts = []
if Scrum::Setting.render_position_on_pbi
parts << "#{l(:field_position)}: #{pbi.position}"
end
if Scrum::Setting.render_category_on_pbi and pbi.category
parts << "#{l(:field_category)}: #{h(pbi.category.name)}"
end
if Scrum::Setting.render_version_on_pbi and pbi.fixed_version
parts << "#{l(:field_fixed_version)}: #{link_to_version(pbi.fixed_version)}"
end
render :inline => parts.join(", ")
end
def render_pbi_right_header(pbi)
parts = []
if Scrum::Setting.render_author_on_pbi
parts << authoring(pbi.created_on, pbi.author)
end
if Scrum::Setting.render_assigned_to_on_pbi and pbi.assigned_to
parts << "#{l(:field_assigned_to)}: #{link_to_user(pbi.assigned_to)}"
end
if Scrum::Setting.render_updated_on_pbi and pbi.created_on != pbi.updated_on
parts << "#{l(:label_updated_time, time_tag(pbi.updated_on))}"
end
render :inline => parts.join(", ")
end
def render_issue_icons(issue)
icons = []
if (User.current.allowed_to?(:view_time_entries, issue.project) and
((issue.is_pbi? and Scrum::Setting.render_pbis_speed) or
(issue.is_task? and Scrum::Setting.render_tasks_speed)) and
(speed = issue.speed))
if speed <= Scrum::Setting.lowest_speed
icons << render_issue_icon(LOWEST_SPEED_ICON, speed)
elsif speed <= Scrum::Setting.low_speed
icons << render_issue_icon(LOW_SPEED_ICON, speed)
elsif speed >= Scrum::Setting.high_speed
icons << render_issue_icon(HIGH_SPEED_ICON, speed)
end
end
render :inline => icons.join('\n')
end
def project_selector_tree(project, indent = '')
options = [["#{indent}#{project.name}", project.id]]
project.children.visible.each do |child_project|
options += project_selector_tree(child_project, indent + '» ')
end
return options
end
DEVIATION_ICONS = [LOWEST_SPEED_ICON = "icon-major-deviation",
LOW_SPEED_ICON = "icon-minor-deviation",
HIGH_SPEED_ICON = "icon-below-deviation"]
private
def render_issue_icon(icon, speed)
link_to("", "#", :class => "icon float-icon #{icon}",
:title => l(:label_issue_speed, :speed => speed))
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
class PendingEffort < ActiveRecord::Base
belongs_to :issue
include Redmine::SafeAttributes
safe_attributes :issue_id, :date, :effort
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
class Sprint < ActiveRecord::Base
belongs_to :user
belongs_to :project
has_many :issues, :dependent => :destroy
has_many :efforts, :class_name => "SprintEffort", :dependent => :destroy
scope :sorted, -> { order(fields_for_order_statement) }
scope :open, -> { where(:status => 'open') }
include Redmine::SafeAttributes
safe_attributes :name, :description, :sprint_start_date, :sprint_end_date, :status, :shared
SPRINT_STATUSES = %w(open closed)
validates_presence_of :name
validates_uniqueness_of :name, :scope => [:project_id]
validates_length_of :name, :maximum => 60
validates_presence_of :sprint_start_date, :unless => :is_product_backlog?
validates_presence_of :sprint_end_date, :unless => :is_product_backlog?
validates_inclusion_of :status, :in => SPRINT_STATUSES
def to_s
name
end
def is_product_backlog?
self.is_product_backlog
end
def pbis(options = {})
conditions = {:tracker_id => Scrum::Setting.pbi_tracker_ids,
:status_id => Scrum::Setting.pbi_status_ids}
order = "position ASC"
if options[:position_bellow]
first_issue = issues.where(conditions).order(order).first
first_position = first_issue ? first_issue.position : (options[:position_bellow] - 1)
last_position = options[:position_bellow] - 1
elsif options[:position_above]
last_issue = issues.where(conditions).order(order).last
first_position = options[:position_above] + 1
last_position = last_issue ? last_issue.position : (options[:position_above] + 1)
end
if options[:position_bellow] or options[:position_above]
if last_position < first_position
temp = last_position
last_position = first_position
first_position = temp
end
conditions[:position] = first_position..last_position
end
conditions[:project_id] = options[:filter_by_project] if options[:filter_by_project]
issues.where(conditions).order(order).select{|issue| issue.visible?}
end
def closed_pbis(options = {})
pbis(options).select {|pbi| pbi.scrum_closed?}
end
def not_closed_pbis(options = {})
pbis(options).select {|pbi| !pbi.scrum_closed?}
end
def story_points(options = {})
pbis(options).collect{|pbi| pbi.story_points.to_f}.sum
end
def closed_story_points(options = {})
pbis(options).collect{|pbi| pbi.closed_story_points}.sum
end
def scheduled_story_points(options = {})
pbis(options).select{|pbi| pbi.scheduled?}.collect{|pbi| pbi.story_points.to_f}.sum
end
def tasks(options = {})
modified_options = options.clone
conditions = {:tracker_id => Scrum::Setting.task_tracker_ids}
if modified_options[:filter_by_project]
conditions[:project_id] = modified_options[:filter_by_project]
modified_options.delete(:filter_by_project)
end
conditions.merge!(modified_options)
issues.where(conditions).select{|issue| issue.visible?}
end
def orphan_tasks
tasks(:parent_id => nil)
end
def estimated_hours(filter = {})
sum = 0.0
tasks(filter).each do |task|
if task.use_in_burndown?
pending_effort = task.pending_efforts.where(['date < ?', self.sprint_start_date]).order('date ASC').last
pending_effort = pending_effort.effort unless pending_effort.nil?
if (!(pending_effort.nil?))
sum += pending_effort
elsif (!((estimated_hours = task.estimated_hours).nil?))
sum += estimated_hours
end
end
end
return sum
end
def time_entries
tasks.collect{|task| task.time_entries}.flatten
end
def time_entries_by_activity
results = {}
total = 0.0
if User.current.allowed_to?(:view_sprint_stats, project)
time_entries.each do |time_entry|
if time_entry.activity and time_entry.hours > 0.0 and
time_entry.spent_on and sprint_start_date and sprint_end_date and
time_entry.spent_on >= sprint_start_date and time_entry.spent_on <= sprint_end_date
if !results.key?(time_entry.activity_id)
results[time_entry.activity_id] = {:activity => time_entry.activity, :total => 0.0}
end
results[time_entry.activity_id][:total] += time_entry.hours
total += time_entry.hours
end
end
results.values.each do |result|
result[:percentage] = ((result[:total] * 100.0) / total).round
end
end
return results.values, total
end
def time_entries_by_member
results = {}
total = 0.0
if User.current.allowed_to?(:view_sprint_stats_by_member, project)
time_entries.each do |time_entry|
if time_entry.activity and time_entry.hours > 0.0 and
time_entry.spent_on >= sprint_start_date and time_entry.spent_on <= sprint_end_date
if !results.key?(time_entry.user_id)
results[time_entry.user_id] = {:member => time_entry.user, :total => 0.0}
end
results[time_entry.user_id][:total] += time_entry.hours
total += time_entry.hours
end
end
results.values.each do |result|
result[:percentage] = ((result[:total] * 100.0) / total).round
end
end
results = results.values.sort{|a, b| a[:member] <=> b[:member]}
return results, total
end
def efforts_by_member
results = {}
total = 0.0
if User.current.allowed_to?(:view_sprint_stats_by_member, project)
efforts.each do |effort|
if effort.user and effort.effort > 0.0
if !results.key?(effort.user_id)
results[effort.user_id] = {:member => effort.user, :total => 0.0}
end
results[effort.user_id][:total] += effort.effort
total += effort.effort
end
end
results.values.each do |result|
result[:percentage] = ((result[:total] * 100.0) / total).round
end
end
results = results.values.sort{|a, b| a[:member] <=> b[:member]}
return results, total
end
def efforts_by_member_and_activity
results = {}
if User.current.allowed_to?(:view_sprint_stats_by_member, project)
members = Set.new
time_entries.each do |time_entry|
if time_entry.activity and time_entry.hours > 0.0 and
time_entry.spent_on >= sprint_start_date and time_entry.spent_on <= sprint_end_date
activity = time_entry.activity.name
member = time_entry.user.name
if !results.key?(activity)
results[activity] = {}
end
if !results[activity].key?(member)
results[activity][member] = 0.0
end
results[activity][member] += time_entry.hours
members << member
end
end
results.values.each do |data|
members.each do |member|
data[member] = 0.0 unless data.key?(member)
end
end
end
return results
end
def sps_by_pbi_category
return sps_by_pbi_field(:category_id, nil, :category, :name, nil, nil)
end
def sps_by_pbi_type
return sps_by_pbi_field(:tracker_id, nil, :tracker, :name, nil, nil)
end
def sps_by_pbi_creation_date
return sps_by_pbi_field(:created_on, :to_date, :created_on, :to_date, self.sprint_start_date,
l(:label_date_previous_to, :date => self.sprint_start_date))
end
def self.fields_for_order_statement(table = nil)
table ||= table_name
["(CASE WHEN #{table}.sprint_end_date IS NULL THEN 1 ELSE 0 END)",
"#{table}.sprint_end_date",
"#{table}.name",
"#{table}.id"]
end
def total_time
pbis.collect{|pbi| pbi.total_time}.compact.sum
end
def hours_per_story_point
sps = story_points
sps > 0 ? (total_time / sps).round(2) : 0.0
end
def closed?
status == 'closed'
end
def open?
status == 'open'
end
def get_dependencies
dependencies = []
pbis.each do |pbi|
pbi_dependencies = pbi.get_dependencies
dependencies << {:pbi => pbi, :dependencies => pbi_dependencies} if pbi_dependencies.count > 0
end
return dependencies
end
def completed_sps_by_day(filter = {})
days = {}
non_working_days = Setting.non_working_week_days.collect{|day| (day == '7') ? 0 : day.to_i}
end_date = self.sprint_end_date + 1
(self.sprint_start_date..end_date).each do |day|
if (day == end_date) or (!(non_working_days.include?(day.wday)))
days[day] = self.completed_sps_at_day(day, filter)
end
end
return days
end
def completed_sps_at_day(day, filter = {})
sps = self.pbis(filter).collect { |pbi| pbi.story_points_for_burdown(day) }.compact.sum
sps = 0.0 unless sps
return sps
end
private
def sps_by_pbi_field(field_id, subfield_id, field, subfield, field_min, label_min)
results = {}
total = 0.0
if User.current.allowed_to?(:view_sprint_stats, project)
pbis.each do |pbi|
pbi_story_points = pbi.story_points
if pbi_story_points
pbi_story_points = pbi_story_points.to_f
if pbi_story_points > 0.0
field_id_value = pbi.public_send(field_id)
field_id_value = field_id_value.public_send(subfield_id) unless field_id_value.nil? or subfield_id.nil? or !(field_id_value.respond_to?(subfield_id))
field_id_value = field_min unless field_min.nil? or (field_id_value > field_min)
if !results.key?(field_id_value)
field_value = pbi.public_send(field) unless !(pbi.respond_to?(field))
field_value = field_value.public_send(subfield) unless field_value.nil? or subfield.nil? or !(field_value.respond_to?(subfield))
field_value = label_min unless field_min.nil? or label_min.nil? or (field_value >= field_min)
results[field_id_value] = {field => field_value, :total => 0.0}
end
results[field_id_value][:total] += pbi_story_points
total += pbi_story_points
end
end
end
results.values.each do |result|
result[:percentage] = ((result[:total] * 100.0) / total).round
end
end
return results.values, total
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
class SprintEffort < ActiveRecord::Base
belongs_to :user
belongs_to :sprint
end
<%= javascript_tag do %>
var <%= graph[:id] %>_data = [
<%- rows.each do |name, data| -%>
{
key: '<%= name ? name : "---" %>',
values: [<%= raw(data.collect{ |name, value| "{x: '#{name}', y: #{value}}" }.join(', ')) %>]
},
<%- end -%>
];
if (<%= graph[:id] %>_data.length > 0)
{
nv.addGraph(function() {
var chart = nv.models.multiBarChart()
.color(d3.scale.category10().range())
.reduceXTicks(false)
.showControls(false);
chart.tooltip.contentGenerator(function(data) {
return '<h3>' + data.value + ' (' + data.series.length + ')</h3><p>' + data.series[0]['key'] + ': ' + data.series[0]['value'] + '</p>';
});
chart.yAxis.tickFormat(d3.format('.2f'));
chart.yAxis.ticks(10);
if (<%= graph[:id] %>_data[0].values.length > 1)
{
chart.margin({bottom: 100});
chart.xAxis.rotateLabels(-30);
}
d3.select("#<%= graph[:id] %> svg")
.datum(<%= graph[:id] %>_data)
.call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
}
<% end %>
<div class="scrum-content">
<div class="scrum-contextual">
<%= link_to('',
'#',
:title => "#{l(:label_fullscreen)}",
:class => 'icon icon-fullscreen',
:onclick => "$('.scrum-content').toggleClass('scrum-content-fullscreen');") %>
<%= link_to('',
'#',
:title => "#{l(:label_exit_fullscreen)}",
:class => 'icon icon-normal-screen',
:onclick => "$('.scrum-content').toggleClass('scrum-content-fullscreen');") %>
<%= javascript_tag do %>
$(document).keyup(function(event) {
if (event.keyCode == 27) {
$('.scrum-content').removeClass('scrum-content-fullscreen');
}
});
<% end %>
</div>
<%= yield %>
</div>
<div class="contextual scrum-menu">
<%- if User.current.allowed_to?(:view_product_backlog, @project) -%>
<%= link_to(l(:label_product_backlog), product_backlog_path(@product_backlog),
:class => 'icon icon-product-backlog') %>
<%- end -%>
<%- if User.current.allowed_to?(:view_product_backlog_burndown, @project) -%>
<%= link_to(l(:label_product_backlog_burndown_chart), burndown_product_backlog_path(@product_backlog),
:class => 'icon icon-burndown') %>
<%- end -%>
<%- if User.current.allowed_to?(:view_release_plan, @project) -%>
<%= link_to(l(:label_release_plan), release_plan_product_backlog_path(@product_backlog),
:class => 'icon icon-release-plan') %>
<%- end -%>
<%- if User.current.allowed_to?(:manage_sprints, @project) and @product_backlog -%>
<%= link_to('', new_project_sprint_path(@project, :back_url => url_for(params.permit!), :create_product_backlog => true),
:class => 'icon icon-add', :title => l(:button_add)) %>
<%= link_to('', edit_sprint_path(@product_backlog, :back_url => url_for(params.permit!)),
:class => 'icon icon-edit', :title => l(:button_edit)) %>
<%= link_to('', sprint_path(@product_backlog),
{:method => :delete,
:data => {:confirm => l(:text_are_you_sure)},
:class => 'icon icon-del',
:title => l(:button_delete)}) %>
<%- end -%>
<%= render_scrum_help %>
</div>
<input id="<%= element_id %>" type="text" class="editable-time" size="2" title="<%= title %>"
value="<%= number_with_precision(value, precision: 2, strip_insignificant_zeros: true) %>"
<%= 'readonly' unless editable %>/>
<%- if defined?(unit) -%>
<span <% if defined?(unit_title) %> title="<%= unit_title %>"<% end %>>
<%= unit %>
</span>
<%- end -%>
<%- if editable -%>
<%= javascript_tag do %>
$(document).ready(function() {
$("#<%= element_id %>").change(function() {
if ($.isFunction($.fn.setupAjaxIndicator)) {
setupAjaxIndicator();
}
$.ajax({
url: "<%= change_element_path %>",
type: "POST",
data: {value: $(this).val()},
error: function() {
alert("<%= l(:error_changing_value) %>");
},
complete: function() {
if ($.isFunction($.fn.hideOnLoad)) {
hideOnLoad();
}
}
});
});
});
<% end %>
<%- end -%>
<%= javascript_tag do %>
$(document).ready(function() {
<%- link = link_to(l(:label_scrum), Redmine::Plugin::registered_plugins[:scrum].url)
author = Redmine::Plugin::registered_plugins[:scrum].author
copy_right = link_to("©", "http://creativecommons.org/licenses/by-nd/4.0/")
legend = "Powered by #{link} plugin #{copy_right} 2013-2015 #{author}" -%>
$("#footer").append('<div class="bgl"><div class="bgr"><%= raw legend %></div></div>');
});
<% end %>
\ No newline at end of file
<%- if User.current.allowed_to?(:view_pending_effort, project) or
User.current.allowed_to?(:edit_pending_effort, project) -%>
<%= render :partial => "common/scrum_editable_value",
:locals => {:project => project,
:title => l(:field_pending_effort),
:element_id => "pending_effort_#{task.id}",
:value => task.pending_effort,
:unit => "h",
:unit_title => l(:field_pending_effort),
:editable => (User.current.allowed_to?(:edit_sprint_board, project) and
User.current.allowed_to?(:edit_pending_effort, project) and
task.editable?),
:change_element_path => change_pending_effort_path(task)} %>
<%- end -%>
<%= render :partial => "common/scrum_editable_value",
:locals => {:title => l(:label_remaining_story_point_plural),
:element_id => "remaining_story_points_#{pbi.id}",
:value => pbi.remaining_story_points,
:unit => l(:label_remaining_story_point_unit),
:unit_title => l(:label_remaining_story_point_plural),
:editable => (User.current.allowed_to?(:edit_issues, pbi.project) and
pbi.editable? and !read_only),
:change_element_path => change_remaining_story_points_path(pbi)} %>
<div class="contextual scrum-menu">
<%- if User.current.allowed_to?(:view_sprint_board, @project) -%>
<%= link_to(l(:label_sprint_board),
@sprint ? sprint_path(@sprint) : project_sprints_path(@project),
:class => 'icon icon-sprint-board') %>
<%- end -%>
<%- if User.current.allowed_to?(:view_sprint_burndown, @project) -%>
<%= link_to(l(:label_sprint_burndown_chart_hours),
@sprint ? burndown_sprint_path(@sprint, :type => :effort) :
burndown_index_project_sprints_path(@project, :type => :effort),
:class => 'icon icon-burndown') %>
<%= link_to(l(:label_sprint_burndown_chart_sps),
@sprint ? burndown_sprint_path(@sprint, :type => :sps) :
burndown_index_project_sprints_path(@project, :type => :sps),
:class => 'icon icon-burndown') %>
<%- end -%>
<%- if User.current.allowed_to?(:view_sprint_stats, @project) -%>
<%= link_to(l(:label_sprint_stats),
@sprint ? stats_sprint_path(@sprint) : stats_index_project_sprints_path(@project),
:class => 'icon icon-stats') %>
<%- end -%>
<%- if User.current.allowed_to?(:manage_sprints, @project) and @sprint -%>
<%= link_to('', new_project_sprint_path(@project, :back_url => url_for(params.permit!)),
:class => 'icon icon-add', :title => l(:button_add)) %>
<%= link_to('', edit_sprint_path(@sprint, :back_url => url_for(params.permit!)),
:class => 'icon icon-edit', :title => l(:button_edit)) %>
<%= link_to('', sprint_path(@sprint),
{:method => :delete,
:data => {:confirm => l(:text_are_you_sure)},
:class => 'icon icon-del',
:title => l(:button_delete)}) %>
<%= link_to('', edit_effort_sprint_path(@sprint, :back_url => url_for(params.permit!)),
:class => 'icon icon-time-add', :title => l(:label_edit_effort)) %>
<%- end -%>
<%= render_scrum_help %>
</div>
<%= render :partial => "common/scrum_editable_value",
:locals => {:title => l(:label_story_point_plural),
:element_id => "story_points_#{pbi.id}",
:value => pbi.story_points,
:unit => l(:label_story_point_unit),
:unit_title => l(:label_story_point_plural),
:editable => (User.current.allowed_to?(:edit_issues, pbi.project) and
pbi.editable? and !read_only),
:change_element_path => change_story_points_path(pbi)} %>
<table class="scrum-stats-chart-table">
<tr>
<td align="right">
<svg id="<%= graph[:id] %>"
style="width: <%= graph[:width] %>px; height: <%= graph[:height] %>px;">
</svg>
</td>
<td align="left">
<table class="scrum-stats-table">
<thead>
<tr>
<th align="left"><%= element[:label] %></th>
<th align="center"><%= unit[:plural_label] %></th>
<th align="center">%</th>
</tr>
</thead>
<tbody>
<%- rows.each do |row| -%>
<tr>
<td align="left"><%= row[element[:name]].nil? ? "---" : row[element[:name]] %></td>
<td align="center"><%= "#{row[:total].round(2)} #{unit[:label]}" %></td>
<td align="center"><%= "#{row[:percentage].round}%" %></td>
</tr>
<%- end -%>
<tr>
<td align="left" class="total"><%= l(:label_total) %></td>
<td align="center" class="total"><%= "#{total.round(2)} #{unit[:label]}" %></td>
<td align="center" class="total">100%</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
<%= render :partial => "sprints/pie_chart",
:formats => [:js],
:locals => {:graph => graph,
:element => element,
:rows => rows} %>
<h3>Burndown en equipos multi-proyecto</h3>
<p>Si tienes varios proyectos por debajo del proyecto del equipo Scrum, y
asignas EPPs y tareas a esos proyectos hijos (estos han de tener tambi&eacute;n
el m&oacute;dulo de Scrum activo), entonces el burndown puede descomponerse en
varias series, una serie por subproyecto.</p>
<h3>Burndown on multi-project teams</h3>
<p>If you have several projects under the Scrum team project, and you assign
PBIs &amp; tasks to those child projects (they should also have Scrum module
enabled), then the burndown could be decomposed in several series, one serie
per subproject.</p>
<%= link_to(content_tag('b', l(:label_help)), '#', :class => 'icon icon-help',
:id => "helpLink_#{unique_id}") %>
<div id="helpDialog_<%= unique_id %>">
<%= render :partial => "help/#{template}", :locals => {:links => links} %>
<%= render :partial => "help/support" %>
</div>
<%= render(:partial => 'help/help', :formats => [:js],
:locals => {:unique_id => unique_id}) %>
<%= javascript_tag do %>
$("#helpDialog_<%= unique_id %>").dialog({
autoOpen: false,
modal: true,
width: 850,
height: 600,
title: "<%= l(:label_help) %>"
});
$('#helpLink_<%= unique_id %>').click(function() {
$('#helpDialog_<%= unique_id %>').dialog('open');
return(false);
});
<% end %>
<hr />
<h2>Apoya este plugin de Scrum para Redmine</h2>
<p>Si quieres que este plugin siga siendo de <b>c&oacute;digo abierto y
gratis</b>, necesito tu apoyo:</p>
<table style="width: 100%">
<tr>
<td align="center">
<a href="http://www.patreon.com/ociotec" target="_blank">
<%= image_tag('patreon-logo.png', :plugin => 'scrum') %>
</a>
</td>
<td align="center">
o
</td>
<td align="center">
<a href="https://www.paypal.me/ociotec/5" target="_blank">
<%= image_tag('paypal-logo.png', :plugin => 'scrum') %>
</a>
</td>
</tr>
</table>
<p>Saludos,<br/>
<a href="https://redmine.ociotec.com/projects/redmine-plugin-scrum"
target="_blank">Emilio Gonz&aacute;lez Monta&ntilde;a</a></p>
<hr />
<h2>Support this Redmine Scrum plugin</h2>
<p>In order to be an <b>open source &amp; free plugin</b>, I need your
support:</p>
<table style="width: 100%">
<tr>
<td align="center">
<a href="http://www.patreon.com/ociotec" target="_blank">
<%= image_tag('patreon-logo.png', :plugin => 'scrum') %>
</a>
</td>
<td align="center">
or
</td>
<td align="center">
<a href="https://www.paypal.me/ociotec/5" target="_blank">
<%= image_tag('paypal-logo.png', :plugin => 'scrum') %>
</a>
</td>
</tr>
</table>
<p>Kindly regards,<br/>
<a href="https://redmine.ociotec.com/projects/redmine-plugin-scrum"
target="_blank">Emilio Gonz&aacute;lez Monta&ntilde;a</a></p>
<h2>Ayuda del tablero de la pila de producto</h2>
<h3>EPPs</h3>
<p>Una pila de producto incluye EPPs (Elementos de la Pila de Producto).
T&iacute;picamente historias de usuario, errores y deuda t&eacute;cnica...
En <%= links[:plugin_settings] %> puedes configurar que tipos de petici&oacute;
son consideradors EPPs.</p>
<p>Los EPPs aparecen en una cola ordenada, donde la cima de la pila contiene
los EPPs que van a ser implementados primero por el equipo.</p>
<p>Muchas caracter&iacute;sticas s&oacute;lo aparecen si los permisos lo
permiten, as&iacute; que echa un vistazo a <%= links[:permissions] %> para
habilitarlos/deshabilitarlos para cada perfil.</p>
<h3>Trabajando con EPPs</h3>
<p>Los EPPs pueden crearse en este tablero usando los
<i class="icon icon-add">enlaces</i> al principio del tablero (si quieres crear
EPPs en la cima de la pila) or al final del tablero (si quieres crear EPPs al
final de la pila).</p>
<p>Una vez que tienes EPPs en la pila de producto, puedes arrastralos en
vertical para cambiar su orden (as&iacute; el equipo empezar&aacute; con estos
primero). Puesdes moverlos al inicio/final de la pila usando
<i class="icon icon-move-top">el icono de mover arriba</i> y
<i class="icon icon-move-bottom">el icono de mover abajo</i>.</p>
<p>En el post-it del EPP ver&aacute;s sus atributos (para habilitar
m&aacute;s/menos atributos echa un vistazo a <%= links[:plugin_settings] %>).
Puedes editar los haciendo click en <i class="icon icon-edit">el icono del
lapicero</i>.</p>
<p>Los EPPs se estiman en puntos de historia (PHs), puedes editar su valor
directamente haciendo click en el valor de PHs y escribiendo un nuevo valor
(luego pulsa ENTER o TAB). Si los puntos de historia pendientes est&aacute;
habilidato (en <%= links[:plugin_settings] %>) entonces ver&aacute;s que
este campo tambi&eacute;n est&aacute;, tambi&eacute;n ser&aacute; editable.</p>
<h3>La pila de producto en equipos multi-proyecto</h3>
<p>Si tienes varios proyectos bajo el proyecto del equipo Scrum, y asignas
EPPs y tareas a esos proyectos hijos (deben tener tambi&eacute;n el
m&oacute;dulo Scrum activo), entonces la pila de producto puede ser filtrada
por subproyecto. Si filtras la pila de producto, la ordenación de EPPs no
estará permitida.</p>
<h2>Product backlog board help</h2>
<h3>PBIs</h3>
<p>A product backlog includes PBIs (Product Backlog Items). Typically they
represent user stories, bugs, technical debts... Under
<%= links[:plugin_settings] %> you can configure which issue trackers are
considered PBIs.</p>
<p>PBIs are listed in a sorted queue, where the top of the queue contains the
PBIs that are going to be implemented by the team first.</p>
<p>A lot of the described features are enabled only with permissions, so take a
look to <%= links[:permissions] %> to enable/disable permissions for each
role.</p>
<h3>Working with PBIs</h3>
<p>PBIs can be created in this board using the
<i class="icon icon-add">links</i> at the begin of the board (if you want to
create PBIs at the top of the queue) or at the end of the board (if you want
to create PBIs at the bottom of the queue).</p>
<p>Once you have PBIs on the product backlog, you can dragged in vertical
to change their order (so the team will start with the ones before).
You can also move them to the top/bottom of the queue using
<i class="icon icon-move-top">move to top icon</i> and
<i class="icon icon-move-bottom">move to bottom icon</i>.</p>
<p>In the PBI post-it you will see its attributes (to enable more/less
attributes take a look to <%= links[:plugin_settings] %>). You can edit them
clicking on the <i class="icon icon-edit">pencil icon</i>.</p>
<p>PBIs are estimated in story points (SPs), you can edit directly this value
clicking on the SPs value and typing a new value (then press ENTER or TAB).
If remaining story points is enabled (under <%= links[:plugin_settings] %>)
then you will see that field also in the post-it, it's also editable.</p>
<h3>Product backlog on multi-project teams</h3>
<p>If you have several projects under the Scrum team project, and you assign
PBIs &amp; tasks to those child projects (they should also have Scrum module
enabled), then the product backlog could be filtered by subproject. If you
filter the product backlog, sorting PBIs will not be permitted.</p>
<h2>Ayuda del burndown de la pila de producto</h2>
<p>El burndown de la pila de producto muestra el progreso del equipo en puntos
de historia a lo largo de los Sprints y la preducci&oacute;n del progreso del
equipo de los futuros Sprints.</p>
<h3>Entendiendo el burndown</h3>
<p>La l&iacute;nea (o l&iacute;neas en equipos multi-proyecto) representa los
PHs pendientes de los EPPs del Sprint d&iacute;a a d&iacute;a (no tiene que
ver de ning&uacute;n modo con el campo de PHs del EPP, sin embargo normalmente
el primer valor de los PHs pendientes del EPP es igual a los PHs).</p>
<%= render :partial => 'help/product_backlog/velocity_modes',
:locals => {:links => links} %>
<%= render :partial => 'help/burndown_multi_project',
:locals => {:links => links} %>
<h2>Product backlog burndown help</h2>
<p>Product backlog burndown shows the progress of the team in story points
along the Sprints and the prediction based on team velocity of future
Sprints.</p>
<h3>Understanding the burndown</h3>
<p>The line (or lines in multi-project teams) represent the pending
SPs of Sprint PBIs day by day (this is not related in any way with the
PBI SPs field, nevertheless first PBI pending SPs value uses to be equal to the
SPs field).</p>
<%= render :partial => 'help/product_backlog/velocity_modes',
:locals => {:links => links} %>
<%= render :partial => 'help/burndown_multi_project',
:locals => {:links => links} %>
<h2>Ayuda de la pila de producto</h2>
<p>C&oacute;mo usar el formaulario de la pila de producto:</p>
<ul>
<li><b>Nombre:</b> normalmente algo como <i>Pila de producto</i>.</li>
<li><b>Descripci&oacute;n:</b> puedes poner aqu&iacute; cualquier
informaci&oacute;n relevante del equipo.</li>
<li><b>Compartido:</b> si compartes la pila de producto podr&aacute;s
asignar EPPs de los proyectos hijos a la pila de producto, esto permite
el uso de multiples proyectos.</li>
</ul>
<h2>Product backlog help</h2>
<p>How to use the product backlog form:</p>
<ul>
<li><b>Name:</b> usually something like <i>Product backlog</i>.</li>
<li><b>Description:</b> you can put here any relevant information for the
team.</li>
<li><b>Shared:</b> if you share a product backlog you will be able to assign
PBIs of child projects to this product backlog, this will enable
multi-project usage.</li>
</ul>
<h2>Ayuda del plan de liberaciones</h2>
<p>En el plan de liberaciones ver&aacute;s una especie de pila de producto
pero centrada en dar informaci&oacute;n de cuando est&aacute; previsto que
cada versi&oacute;n sea liberada.</p>
<p>En esta vista no se pueden editar u ordenar EPPs.</p>
<%= render :partial => 'help/product_backlog/velocity_modes',
:locals => {:links => links} %>
<h3>Plan de liberaciones en equipos multi-proyecto</h3>
<p>Si tienes varios proyectos bajo el proyecto del equipo Scrum, y asignas
EPPs a esos proyectos hijos (deben tener tambi&eacute;n el
m&oacute;dulo Scrum activo), entonces el plan de liberaciones puede ser filtrado
por subproyecto.</p>
<h2>Release plan help</h2>
<p>In the release plan you will see a kind of product backlog but focused on
giving information to plan when each version is going to be released.</p>
<p>In this view you cannot edit or sort PBIs.</p>
<%= render :partial => 'help/product_backlog/velocity_modes',
:locals => {:links => links} %>
<h3>Release plan on multi-project teams</h3>
<p>If you have several projects under the Scrum team project, and you assign
PBIs &amp; tasks to those child projects (they should also have Scrum module
enabled), then the release plan could be filtered by subproject.</p>
<h3>Trabajando con predicciones</h3>
<p>Este plugin tiene tres modos de predicci&oacute;n:</p>
<ul>
<li><b>Modo normal</b> (<i>Usa todos los EPPs de los &uacute;ltimos N Sprints
para calcular la velocidad</i>): la velocidad del equipo es calculada usando
la media de los PHs de los EPPs cerrados en los &uacute;ltimos N Sprints.</li>
<li><b>Modo planificaci&oacute;n</b> (<i>s&oacute;lo los planificados</i>):
igual que el modo normal pero s&oacute;lo los EPPs planificados
contar&aacute;n en la velocidad del equipo. Los planificados son aqueyos
EPPs que se crearon antes de que el Sprint empezara, los otros se consideran
interrupciones del Sprint, as&iacute; que no incrementar&aacute;n la
velocidad del equipo.</li>
<li><b>Modo personalizado</b>: sirve para jugar con diferentes valores de
velocidad.</li>
</ul>
<h3>Working with predictions</h3>
<p>This plugin has three predictions modes:</p>
<ul>
<li><b>Regular mode</b> (<i>Use all PBIs from last N Sprints to calculate the
velocity</i>): team velocity is calculated using the media of the closed PBIs
SPs in the last N Sprints.</li>
<li><b>Scheduled mode</b> (<i>Only scheduled ones</i>): same as regular mode
but only the scheduled PBIs will count in the team velocity. Scheduled ones
are PBIs created before the Sprint starts, others are considered as Sprint
interruptions, so they will not increase the team velocity.</li>
<li><b>Custom mode</b>: just play with custom velocity values.</li>
</ul>
<h2>Ayuda de la configuraci&oacute;n de proyecto de pilas de producto</h2>
<p>En esta vista encontrar&aacute;s la lista de pilas de producto, normalmente
crear&aacute;s una sola pila de producto, pero se permite crear m&aacute;s de
una (por ejemplo para EPPs no claros, o para deuda t&eacute;cnica si prefieres
separarlos de otras caracter&iacute;sticas...).</p>
<p>No puedes borrar una pila de producto si tiene peticiones asignadas.
A veces es dificil darse cuenta porque el tablero de la pila de producto
s&oacute;lo muestra las peticiones EPP en unos estados. As&iacute; que si el
plugin puede indicar que la pila no puede ser borrada, ve a la pesta&ntilde;a
de peticiones y crea un filtro por cualquier tipo de petici&oacute;n, en
cualquier estado asignado a esta pila de producto (mostrado como campo
<i>Sprint</i>).</p>
<h2>Product backlogs project settings help</h2>
<p>In this view you will find the list of product backlogs, usually you'll
create only one product backlog, but it's OK if you create more than one
(i.e. for not clear PBIs, or for technical debt if you prefer to separate them
from regular features...).</p>
<p>You cannot delete a product backlog if it has any issue assigned to it.
This sometimes is difficult to detect because in the product backlog board only
PBI issues in a configured state are rendered. So if the plugin tells you that
the product backlog cannot be deleted, go to issues tab &amp; create a filter
by any tracker, in any state assigned to this product backlog (rendered as
<i>Sprint</i> field).</p>
<h2>Ayuda de la configuraci&oacute;n de proyecto de Sprints</h2>
<p>En esta vista encontrar&aacute;s la lista de Sprints, los Sprints se
ordenan por las fechas de inicio y fin para que sea m&aacute;s f&aacute;cil
su seguimiento</p>
<p>No puedes borrar un Sprint si tiene peticiones asignadas.
A veces es dificil darse cuenta porque el tablero del Sprint
s&oacute;lo muestra las peticiones EPP en unos estados. As&iacute; que si el
plugin puede indicar que el Sprint no puede ser borrado, ve a la pesta&ntilde;a
de peticiones y crea un filtro por cualquier tipo de petici&oacute;n, en
cualquier estado asignado a este Sprint.</p>
<h2>Sprints project settings help</h2>
<p>In this view you will find the list of Sprints, Sprints are sorted by
begin &amp; end dates in order to make easier the follow up.</p>
<p>You cannot delete an Sprint if it has any issue assigned to it.
This sometimes is difficult to detect because in the Sprint board only
PBI issues in a configured state are rendered. So if the plugin tells you that
the Sprint cannot be deleted, go to issues tab &amp; create a filter
by any tracker, in any state assigned to this Sprint.</p>
<h2>Ayuda de la configuraci&oacute;n del plugin Scrum</h2>
<p>Este plugin es altamente configurable, normalmente los valores por defecto
est&aacute;n bien, pero algunos deben ser configurados antes de empezar a usar
este plugin.</p>
<h3>Pasos obligatorios/m&iacute;nimos</h3>
<p>Normalmente configurar este plugin implica ejecutar varios pasos en la
secci&oacute;n de administraci&oacute;n antes de empezar a configura el plugin
en s&iacute; mismo:</p>
<ol>
<li>Habilitar los permisos del plugin en <%= links[:permissions] %>.</li>
<li>Crear tipos de petici&oacute;n para los EPPs. Normalmente un tipo para
historias de usuario, otro para errores y otro para deuda t&eacute;cnica.
Cuanto menos mejor, as&iacute; que habilita solo los campos: categor&iacute;a,
versi&oacute;n prevista y descripci&oacute;n.</li>
<li>Crea un tipo de petici&oacute;n para tareas, normalmente con un tipo es
suficiente, pero est&aacute; bien si quieres crear varios. De nuevo menos es
m&aacute;s, as&iacute; que habilita s&oacute;lo estos campos: asignado,
petici&oacute;n padre, tiempo estimado y descripci&oacute;n.</li>
<li>Crea un campo de petici&oacute;n personalizado para los puntos de
historia: debe de ser un valor num&eacute;rico, entero o de punto flotante,
pero es mejor si usas una lista de valores similar a una secuencia de
Fibonacci como: 0, 0.5, 1, 2, 3, 5, 8, 13, 20, 40, 100.</li>
</ol>
<h3>Configurando los ajustes del plugin</h3>
<p>Casi todos los valores son auto explicativos o incluyen descripciones para
entenderlos.</p>
<p>Varios valores son selecciones de m&uacute;ltiples valores, simplemente
presiona CTRL (o CMD en MacOS) para seleccionar varios valores.</p>
<p>Si no entiendes el prop&oacute;sito de algo y quieres probarlo, simplemente
cambia un &uacute;nico valor y comprueba qu&eacute; ha cambiado en la vista
relacionada.</p>
<p><a href="https://redmine.ociotec.com/projects/redmine-plugin-scrum/boards/11"
target="_blank">El foro de soporte del plugin</a> es tambi&eacute;n un buen
lugar para preguntar si lo necesitas.</p>
<h3>Pasos opcionales</h3>
<ol>
<li>Crear un campo personalizado para peticiones para indicar si el EPP o
tarea est&aacute; bloquedo.</li>
<li>Crear un campo personalizado booleano para peticiones para indicar si
el EPP se permite estar solo en el tamblero del Sprint, es decir sin tareas
hijas.</li>
</ol>
<h2>Scrum plugin settings help</h2>
<p>This plugin is highly configurable, usually default values are OK, but
some of them must be configured before start using the plugin.</p>
<h3>Mandatory/minimum steps</h3>
<p>Usually configuring this plugin implies doing several steps in the admin
section before start configuring the plugin itself:</p>
<ol>
<li>Enable Scrum plugin permissions under <%= links[:permissions] %>.</li>
<li>Create trackers for PBIs. Usually one tracker for user stories, other
for bugs and another for technical debts. The less the better, so just enable
fields as: category, target version &amp; description.</li>
<li>Create a tracker for tasks, usually only one tracker is enough, but it's
OK if you create several. Again the less the better, so just enable fields:
assignee, parent issue, estimated time &amp; description.</li>
<li>Create issue custom field for story points: it must be a numeric value,
integer or float, but it's better if you use a list of values type with
a kind of Fibonacci values as: 0, 0.5, 1, 2, 3, 5, 8, 13, 20, 40, 100.</li>
</ol>
<h3>Configuring plugin settings</h3>
<p>Almost all values are auto explanatory or include descriptions to understand
them.</p>
<p>Several values are multiple value selections, just press CTRL (or CMD on
MacOS) to select several values.</p>
<p>If you don't understand the purpose of something and you want to try it out,
just change that single value and check what changed in the related view.</p>
<p><a href="https://redmine.ociotec.com/projects/redmine-plugin-scrum/boards/11"
target="_blank">Plugin support forum</a> it's also a good place to ask if you
need it.</p>
<h3>Optional steps</h3>
<ol>
<li>Create an issue custom boolean value to set that a PBI/task is
blocked.</li>
<li>Create an issue custom boolean value to set that a PBI is
allowed to be in the Sprint board alone, that means without child tasks.</li>
</ol>
<h2>Ayuda de las estad&iacute;sticas de Scrum</h2>
<p>Las estad&iacute;sticas de Scrum muestran informaci&oacute;n de diversos
aspectos del proyecto a nivel global, no s&oacute;lo relativas a un s&oacute;lo
Sprint.</p>
<p>Aqu&iacute; encontrar&aacute;s ayuda para responder a preguntas como:</p>
<ul>
<li>La velocidad del equipo a lo largo de los Sprints.</li>
<li>Cu&aacute;ntas horas son un punto de historia a lo largo de los
Sprints.</li>
<li>Cu&aacute;ntos PHs gastamos en cada categor&iacute;a del proyecto o por
tipo de EPP.</li>
<li>Cu&aacute;nto tiempo gastamos en cada actividad del proyecto.</li>
</ul>
<h2>Scrum stats help</h2>
<p>Scrum stats shows information about different aspects of the global project,
not only regarding a single Sprint.</p>
<p>Here you will find help to answer questions like:</p>
<ul>
<li>Team velocity along the Sprints.</li>
<li>How many hours mean an story point along the Sprints.</li>
<li>How much SPs do we spend in each project issue category or PBI type.</li>
<li>How much time do we spent by project activity.</li>
</ul>
<h2>Ayuda del tablero del Sprint</h2>
<h3>EPPs y tareas</h3>
<p>Un tablero del Sprint incluye EPPs y tareas. EPPs (o Elementos de la Pila
de Producto) son normalmente historias de usuario, errores o deuda
t&eacute;cnica... y estos se descomponen en tareas hijas. En
<%= links[:plugin_settings] %> puedes configurar qu&eacute; tipos de peticiones
son considerados EPPs y cuales son considerados tareas.</p>
<p>Los EPPs se encuentran en la parte izquierda del tablero, las tareas en las
columnas a la derecha. Hay una columna por cada posible estado de las tereas
(estos estados son configurables en <%= links[:plugin_settings] %>).</p>
<p>Muchas caracter&iacute;sticas aqu&iacute; descritas est&aacute;n s&oacute;lo
habilitadas con los permisos apropiados, as&iacute; que echa un vistazo a
<%= links[:permissions] %> para habilitarlos/deshabilitarlos para cada
perfil.</p>
<h3>Trabajando con EPPs</h3>
<p>Los EPPs pueden crearse en este tablero con los
<i class="icon icon-add">enlaces</i> al final del tablero o movi&eacute;ndolos
desde la vista de la pila de producto.</p>
<p>Una vez que tienes EPPs en el tablero del Sprint, puedes arrastrarlos en
vertical para cambiar su orden (as&iacute; el equipo empezar&aacute; antes por
los que est&eacute;n al principio). En los post-its de los EPPs ver&aacute;s
sus atributos (para habilitar m&aacute;s/menos atributos echa un vistazo a
<%= links[:plugin_settings] %>). Puedes editarlos haciendo click en el
<i class="icon icon-edit">icono del lapicero</i>.</p>
<p>Los EPPs se estima en puntos de historia (PHs), puedes editar esta valor
directamente haciendo click en el valor de PHs y escribiendo un nuevo valor
(luego pulsa ENTER o TAB). Si el ajuste de usar puntos de historia restante
est&aacute; habilitado (en <%= links[:plugin_settings] %>) entonces ver&aacute;s
que el campo aparece tambi&eacute;n en el post-it, tambi&eacute;n ser&aacute;
editable; este valor se usa para calcular un burndown por PHs preciso,
as&iacute; que no olvideis actualizarlo d&iaacute;a a d&iacute;a (antes de
salir de la oficina).</p>
<h3>Trabajando con tareas</h3>
<p>Las tareas son asuntos t&eacute;cnicos a realizar para completar un EPP.
Pueden ser creadas directamente haciendo click en el
<i class="icon icon-add">icono</i> en los post-its de EPP (o creando una
petici&oacute;n hija en la ficha del EPP).</p>
<p>Para cambiar el estado de la tarea, simplemente arr&aacute;strala en
direcci&oacute;n horizontal (o ed&iacute;tala y cambia su estado). Ten en
cuenta tu flujo de trabajo del tipo de petici&oacute;n, porque puede que algunos
movimientos esten prohibidos para algunos perdiles (pregunta a tus
administradores). Hay varias acciones autom&aacute;ticas implicadas cuando
mueves post-its (echa un vistazo a <%= links[:plugin_settings] %> para
habilitarlas/deshabilitarlas):</p>
<ul>
<li>Cuando mueves la primera tarea del estado nuevo (primer estado de los
tipos de peticiones en los ajustes de administraci&oacute;n) a cualquier otro
estado, el plugin cambia el estado del EPP padre a en progreso (segundo
estado).</li>
<li>Cuando cambias el estado de una tarea a cualquiera menos nuevo, y esta
no tiene asignado, el plugin la asigna al usuario actual; si la tarea es
cambiada al estado nuevo, el campo asignado es vaciado.</li>
<li>Cuando la &uacute;ltima tarea es completada, el esfuerzo pendiente se
establece a cero y el EPP padre es cambiado a un estado configurable (por
ejemplo resuelto).</li>
</ul>
<p>Las tareas son estimadas en horas, usando el campo esfuerzo estimaod, este
tambi&eacute;n puede editarse en el post-it. Para calcular el burndown del
Sprint por horas es necesario que tambi&eacute;n se rellene el campo de esfuerzo
pendiente (d&iacute;a a d&iacute;a). Puedes imputar tiempo en las tareas
haciendo click en el <i class="icon icon-time-add">icono de imputar
tiempo</i>.</p>
<p>Tan pronto como asignes una tarea o imputes tiempo en ella, un mini-post-it
naranja (el color es configurable en <%= links[:plugin_settings] %>) es
a&ntilde;adido a la tarea, y un mini-post-it verde en el caso de que imputes
tiempo de revisi&oacute;n (la actividad para el tiempo de revisi&oacute;n es
configurada en <%= links[:plugin_settings] %>). Esto permite hacer seguimiento
de que se est&aacute; haciendo m&aacute;s f&aacute;cilmente.</p>
<h2>Sprint board help</h2>
<h3>PBIs &amp; tasks</h3>
<p>An Sprint board includes PBIs &amp; tasks. PBIs (or Product Backlog Items)
are typically user stories, bugs, technical debts... and they are composed of
child tasks. Under <%= links[:plugin_settings] %> you can configure which issue
trackers are considered PBIs and which are considered tasks.</p>
<p>PBIs are located on the left part of the board, tasks on the right columns.
There is a column per possible task status (these statuses are configurable
under <%= links[:plugin_settings] %>).</p>
<p>A lot of the described features are enabled only with permissions, so take a
look to <%= links[:permissions] %> to enable/disable permissions for each
role.</p>
<h3>Working with PBIs</h3>
<p>PBIs can be created in this board with the <i class="icon icon-add">links</i>
at the end of the board or moved from the product backlog view.</p>
<p>Once you have PBIs on the Sprint board, you can dragged in vertical to
change their order (so the team will start with the ones at the beginning).
In the PBI post-it you will see its attributes (to enable more/less attributes
take a look to <%= links[:plugin_settings] %>). You can edit them clicking on
the <i class="icon icon-edit">pencil icon</i>.</p>
<p>PBIs are estimated in story points (SPs), you can edit directly this value
clicking on the SPs value and typing a new value (then press ENTER or TAB).
If use remaining story points is enabled (under <%= links[:plugin_settings] %>)
then you will see that field also in the post-it, it's also editable; this
is used to calculate a precise Sprint burndown by SPs, so don't forget to
update it day by day (before leaving the office).</p>
<h3>Working with tasks</h3>
<p>Tasks are technical things to be done in order to complete a PBI. They can
be created directly clicking on the <i class="icon icon-add">icon</i> in the
PBI post-its (or creating new child issues from the PBI issue form).</p>
<p>To change task status, just drag &amp; drop in horizontal direction (or edit
&amp; change its status). Take care of your tracker work flow, because maybe
some movements are forbidden for some roles (ask your administrators).
There are a few automatic actions involved when you move task post-its
(take a look to <%= links[:plugin_settings] %> to enable/configure them):</p>
<ul>
<li>When you move the first task from new (first tracker state defined
in the admin settings) to any other state, the plugin change parent PBI
status to in progress (the second state).</li>
<li>When you change a task status to anything but new, and it has not
assignee, the plugin set it to current user; if the task is changed to new
status, the assignee is removed.</li>
<li>When the last task is closed, the remaining effort is set to zero and the
parent PBI is changed to a configurable status (i.e. resolved).</li>
</ul>
<p>Tasks are estimated in hours, using the estimated effort field, this is also
editable in the post-it. In order to calculate an Sprint burndown by hours is
also necessary to fill the remaining effort field (day by day). You can log
time to the tasks clicking on
<i class="icon icon-time-add">log entry icon</i>.</p>
<p>As soon as you assign a task or log time into it, an orange mini-post-it
(color is configurable under <%= links[:plugin_settings] %>) is attached to the
task, and a green one in case you log review time (review time has also to be
configured under <%= links[:plugin_settings] %>). This allow to follow up how
is doing what easily.</p>
<h2>Ayuda para el burndown por esfuerzo del Sprint</h2>
<p>El burndown por esfuerzo del Sprint muestra el progreso del equipo comparando
el esfuerzo estimado con el esfuerzo pendiente real. Todos los esfuerzos son
medidos en horas.</p>
<h3>Rellenado el esfuerzo estimado</h3>
<p>Cuando el Sprint comienza el equipo configura el esfuerzo estimado en
<%= links[:sprint_effort] %>, para rellenar esta tabla (una fila por miembro
del equipo, una columna por d&iacute;a del Sprint) debes seguir las siguientes
reglas:</p>
<ul>
<li>S&oacute;lo los miembros activos (aquellos que van a trabar en tareas)
deben rellenarse.</li>
<li>No pongas nada cuando un miembro del equipo no est&eacute; disponible
(por ejemplo en fin de semana o festivos). Si todo el mundo no tiene esfuerzo
un d&iacute;a, entonces ese d&iacute;a no ser&aacute; mostrado.</li>
<li>Es v&aacute;lido poner 0.0 en una celda si quieres reflejar que el
miembro del equipo trabaja en el Sprint, pero quiz&aacute;s s&oacute;lo para
asistir a reuniones.</li>
<li>Se acepta poner decimales, pero no inviertas mucho tiempo pensando acerca
de cuanto tiempo pner aqu&iacute;, esto es Scrum, pon un n&uacute;mero y
val&oacute;ralo en la siguiente retrospectiva del Sprint ;)</li>
</ul>
<h3>Imputando esfuerzo d&iacute;a a d&iacute;a</h3>
<p>Cada miembro del equipo debe imputar las horas d&iacute;a a d&iacute;a.
Esto se debe a que el plugin registra en que d&iacute;a se hace cada esfuerzo,
no olvides imputar las horas cada d&iacute;a si quieres un gr&aacute;fico de
burndown preciso.</p>
<h3>Entendiendo el burndown por esfuerzo</h3>
<p>La l&iacute;nea de esfuerzo estimado representa la tabla que el equipo
rellen&oacute; antes. En cambio, la l&iacute;nea de esfuerzo pendiente (o
l&iacute;neas en equipos multi-proyecto) representa el esfuerzo pendiente de
las tareas del Sprint d&iacute;a a d&iacute;a (no tiene nada que ver con el
campo de esfuerzo estimado de las tareas).</p>
<%= render :partial => 'help/burndown_multi_project',
:locals => {:links => links} %>
<h2>Sprint burndown by effort help</h2>
<p>Sprint burndown by effort shows the progress of the team comparing the
estimated effort with the actual pending effort. All these efforts are measured
in hours.</p>
<h3>Filling the estimated effort</h3>
<p>When the Sprint starts the team must configure the estimated effort under
<%= links[:sprint_effort] %>, to fill this table (one row per team member,
one column per Sprint day) you must follow these rules:</p>
<ul>
<li>Only active members (those who are going to work on tasks) must be
filled.</li>
<li>Don't fill anything when a team member is not available (i.e. weekend
days or holidays). If everybody has no effort one day, that day will not be
rendered.</li>
<li>It's valid to put 0.0 in a cell if you want to reflect that the team
member is working on the Sprint, but maybe only to attend to meetings.</li>
<li>It's OK to put decimals, but don't invest too much time thinking about
how much time to put here, this is Scrum, put a number and evaluate it in
the next Sprint retrospective ;)</li>
</ul>
<h3>Logging effort day by day</h3>
<p>Every team member must log the time day by day. Due the fact the plugin
records every effort on the day is logged, don't forget to log the time every
day if you want an accurate burndown graph.</p>
<h3>Understanding the burndown by effort</h3>
<p>Estimated effort line it's just the table that the team filled before.
On the other hand, pending effort line (or lines in multi-project teams)
represents the pending effort of Sprint tasks day by day (this is not related
in any way with the estimated effort tasks field).</p>
<%= render :partial => 'help/burndown_multi_project',
:locals => {:links => links} %>
<h2>Ayuda del burndown por PHs del Sprint</h2>
<p>El burndown por PHs del Sprint muestra el progreso del equipo en puntos de
historia. El valor puede ser imputado por los usuarios de manera precisa
(v&iacute;a el campo de puntos de historia pendientes, que debe ser habilitado
en <%= links[:plugin_settings] %>) o de manera menos precisa cuando cada EPP
es completado.</p>
<h3>Imputando PHs pendientes d&iacute;a a d&iacute;a</h3>
<p>Cada miembro del equipo debe imputar los PHs pendientes d&iacute;a a
d&iaute;a en los EPPs. Debido al hecho de que el plugin guarda en que
d&iacute;a se registra cada valor de PHs pendientes, no olvides imputar dicho
valor si quieres que el gr&aacute;fico del burndown sea preciso.</p>
<h3>Entendiendo el burndown por PHs</h3>
<p>La l&iacute;nea de PHs pendientes (o l&iacute;neas para equipos
multi-proyecto) representa los PHs pendientes de los EPPs del Sprint d&iacute;a
a d&iacute;a (esto no est&aacute; relacionado de ninguna manera con el campo
de PHs del EPP, sin embargo normalemente los PHs pendientes del EPP suelen ser
iguales al campo de PHs cuando se empieza a trabajar en el EPP).</p>
<%= render :partial => 'help/burndown_multi_project',
:locals => {:links => links} %>
<h2>Sprint burndown by SPs help</h2>
<p>Sprint burndown by SPs shows the progress of the team in story points. The
valor can accurately logged by users (via pending story points field, it must
be enabled under <%= links[:plugin_settings] %>) or with less precision when
each PBI is closed.</p>
<h3>Logging pending SPs day by day</h3>
<p>Every team member must log the pending SPs day by day in the PBIs. Due the
fact the plugin records every pending SPs on the day is logged, don't forget
to log that value every day if you want an accurate burndown graph.</p>
<h3>Understanding the burndown by SPs</h3>
<p>Pending SPs line (or lines in multi-project teams) represents the pending
SPs of Sprint PBIs day by day (this is not related in any way with the
PBI SPs field, nevertheless usually PBI pending SPs use to be equal to the
SPs field when we start working on the PBI).</p>
<%= render :partial => 'help/burndown_multi_project',
:locals => {:links => links} %>
<h2>Ayuda de editar esfuerzo estimado del Sprint</h2>
<p>Cada miembro del equipo (trabajando y haciendo tareas) debe estimar el tiempo
que va a poder dedicar al proyecto cada d&iacute;a del Sprint.</p>
<p>Deja en blanco las celdas si no se prev&eacute; trabajo ese d&iacute; (por
ejmplo en fin de semana) para evitar que el burndown por esfuerzo muestre dicho
d&iacute;a.</p>
<p>Los decimales o incluso 0.0 son valores permitidos.</p>
<h2>Sprint edit estimated effort help</h2>
<p>For each team member (working &amp; doing tasks) should estimate the time
that is going to allocate for the project each day of the Sprint.</p>
<p>Left blank the cell if no work is going to be done in a day (i.e. weekend
days) to avoid burndown by effort to render that day.</p>
<p>Decimals or even 0.0 are allowed values.</p>
<h2>Ayuda del Sprint</h2>
<p>C&oacute;mo usar el formulario del Sprint:</p>
<ul>
<li><b>Nombre:</b> normalmente algo como <i>Sprint 14</i>.</li>
<li><b>Descripci&oacute;n:</b> puedes poner aqu&iacute; el objetivo del Sprint
o cualquier otra informaci&oacute;n relevante para el equipo.</li>
<li><b>Fechas de inicio y fin:</b> estas fechas se usan para calcular cosas
como los burndowns o la tabla de esfuerzo estimado.</li>
<li><b>Estado:</b> un Sprint puede estar abierto o cerrado, los EPPs
s&oacute;lo pueden ser asignados a Sprints abiertos.</li>
<li><b>Compartido:</b> si compartes un Sprint se podr&aacute; asignar EPPs de
proyectos hijos a este Sprint, esto habilita el uso de multi-proyecto.</li>
</ul>
<h2>Sprint help</h2>
<p>How to use the Sprint form:</p>
<ul>
<li><b>Name:</b> usually something like <i>Sprint 14</i>.</li>
<li><b>Description:</b> you can put here the Sprint objective or any other
relevant information for the team.</li>
<li><b>Start &amp; end dates:</b> these dates are used to calculate things
like burndowns, estimated effort table.</li>
<li><b>State:</b> an Sprint can be open or closed, PBIs can be only assigned
to open Sprints.</li>
<li><b>Shared:</b> if you share an Sprint you will be able to assign PBIs of
child projects to this Sprint, this will enable multi-project usage.</li>
</ul>
<h2>Ayuda de las estad&iacute;sticas del Sprint</h2>
<p>Las estad&iacute;sticas del Sprint muestran informaci&oacute;n sobre
diversos aspectos del Sprint, algunos requiren el suo de estimaciones de
esfuerzo de tiempo, imputar esfuerzos, categor&iacute;s de proyecto...</p>
<p>Usa esta informaci&oacute;n para conseguir feedback de de tu Sprint y usarla
en tus retrospectivas de Scrum para contestar a preguntas como:</p>
<ul>
<li>¿Cu&aacute;nto esfuerzo/PHs gastamos en arreglar errores? puede que se
necesite priorizar la deuda t&eacute;cnica.</li>
<li>¿Invertimos suficiente tiempo en revisiones? o ¿quiz&aacute;s
demasiado?</li>
<li>¿Todas los miembros del equipo han conseguido dedicar el tiempo estimado?
¿hay algui&eacute;n que no est&eacute; haciendo revisiones?</li>
<li>¿Ponemos el esfuerzo en EPPs planificados (creados antes de la fecha de
inicio del Sprint)? o ¿quiz&aacute;s tenemos muchas interrupciones (EPPs
creados a mitad del Sprint)?</li>
<li>...</li>
</ul>
<h2>Sprint stats help</h2>
<p>Sprint stats shows information about different aspects of the Sprint, some
of them will require to use time effort estimations, effort logging, project
categories...</p>
<p>Use this information to get feedback of your Sprint &amp; use it in your
Scrum retrospectives to answer things like:</p>
<ul>
<li>How much effort/SPs did I spend on bugs? maybe we need to prioritize
technical debt.</li>
<li>Do I spend enough time on reviews? maybe too much?</li>
<li>Did everyone in the team achieved to do all the estimated time? is anyone
not doing reviews?</li>
<li>Do we put the effort in planned PBIs (created at the beginning of the
Sprint)? or maybe do we have a lot of interruptions (PBIs created in the
middle of the Sprint)?</li>
<li>...</li>
</ul>
<td width="250px">
<div class="tabular">
<p>
<label><%= l(:label_pbis_count) %>:</label>
<%= stats[:closed_pbis_count] %>/<%= stats[:total_pbis_count] %><br />
<label><%= l(:label_sps_count) %>:</label>
<%= stats[:closed_sps_count] %>/<%= stats[:total_sps_count] %><br />
<label><%= l(:label_percentage_closed_sps) %>:</label>
<%= number_to_percentage(stats[:closed_total_percentage],
:precision => 1, :separator => '.') %>
</p>
</div>
</td>
<tr valign="top">
<td width="300px">
<div class="tabular">
<%- filtered_params = params.to_h.symbolize_keys.except(:controller, :action, :id) -%>
<p>
<label><%= l(:label_product_backlog) %>:</label>
<%- options = product_backlog.project.product_backlogs.collect{ |other_product_backlog|
[other_product_backlog.name, path.call(other_product_backlog, filtered_params)] }
options = options_for_select(options, path.call(product_backlog, filtered_params)) -%>
<%= select_tag('selected_sprint_id', options,
:onchange => "if (this.value != '') { window.location = this.value; }") %>
</p>
<%- if product_backlog.shared and @subprojects and !@project.children.visible.empty? -%>
<p>
<label><%= l(:label_filter_by_project) %>:</label>
<%- options = options_for_select(@subprojects, @selected_subproject) -%>
<%= select_tag('select_project_id', options,
:onchange => "if (this.value != '') { window.location = this.value; }") %>
</p>
<%- end -%>
</div>
</td>
<td>
<%- unless product_backlog.description.blank? -%>
<%= textilizable(product_backlog.description) %>
<%- end -%>
</td>
<%- if Scrum::Setting.show_project_totals_on_backlog -%>
<%= render :partial => 'post_its/stats', :locals => {:stats => @stats} %>
<%- end -%>
</tr>
<%- pbi_id = "pbi_#{pbi.id}" -%>
<li id="<%= pbi_id %>"
class="<%= pbi.post_it_css_class(:small_rotate => true, :small_scale => true,
:draggable => User.current.allowed_to?(:sort_product_backlog, pbi.project) &&
params[:filter_by_project].blank?) %>">
<table>
<tr>
<td class="header-1">
<span class="pb-pbi-mini-post-its-container">
<%- if pbi.scrum_blocked? -%>
<span class="<%= Issue.blocked_post_it_css_class %>">
<%= l :label_blocked %>
</span>
&nbsp;&nbsp;
<%- end -%>
</span>
<%= render_pbi_left_header(pbi) %>
</td>
<td class="header-2">
<%= render_pbi_right_header(pbi) %>
</td>
<%- if pbi.has_story_points? -%>
<td class="story-points" rowspan="2">
<%= render :partial => 'common/scrum_story_points',
:locals => {:pbi => pbi, :read_only => read_only} %>
</td>
<%- end -%>
<%- if Scrum::Setting.use_remaining_story_points? -%>
<td class="story-points" rowspan="2">
<%= render :partial => 'common/scrum_remaining_story_points',
:locals => {:pbi => pbi, :read_only => read_only} %>
</td>
<%- end -%>
<%- if User.current.allowed_to?(:edit_product_backlog, project) and
User.current.allowed_to?(:edit_issues, pbi.project) and
pbi.editable? and !read_only -%>
<td rowspan="2">
<%= link_to '', edit_pbi_path(pbi),
:remote => true, :method => 'GET', :class => 'icon icon-edit' %>
</td>
<%- end -%>
<%- if User.current.allowed_to?(:sort_product_backlog, project) and
User.current.allowed_to?(:edit_issues, pbi.project) and
pbi.editable? and !read_only -%>
<td rowspan="2">
<%= link_to '', move_pbi_path(pbi, :top),
:remote => true, :method => 'GET', :class => 'icon icon-move-top' %>
<br />
<%= link_to '', move_pbi_path(pbi, :bottom),
:remote => true, :method => 'GET', :class => 'icon icon-move-bottom' %>
</td>
<%- end -%>
</tr>
<tr>
<td class="content" colspan="2">
<%= link_to_issue(pbi, :tracker => false, :project => project != pbi.project) %>
</td>
</tr>
</table>
</li>
<%- if User.current.allowed_to?(:edit_product_backlog, pbi.project) and pbi.editable? and !read_only -%>
<%= javascript_tag do %>
draggableOnTouchScreen("<%= pbi_id %>");
<% end %>
<%- end -%>
<%- if User.current.allowed_to?(:edit_product_backlog, project) &&
params[:filter_by_project].blank? -%>
<%= javascript_tag do %>
$(document).ready(function() {
$('#pbis').sortable({
update: function() {
if ($.isFunction($.fn.setupAjaxIndicator)) {
setupAjaxIndicator();
}
$.ajax({
url: '<%= sort_product_backlog_path(product_backlog) %>',
type: 'POST',
data: $('#pbis').sortable('serialize'),
dataType: 'script',
error: function() {
alert('<%= l(:error_changing_pbi_order) %>');
location.reload(true);
},
complete: function() {
if ($.isFunction($.fn.hideOnLoad)) {
hideOnLoad();
}
},
success: function(data) {
var error_messages = JSON.parse(data);
if (error_messages && error_messages.length > 0) {
alert('<%= l(:error_changing_pbi_order) %>\n' +
error_messages.join('\n'));
location.reload(true);
} else if ($.isFunction($.fn.hideOnLoad)) {
hideOnLoad();
}
}
});
}
});
});
<% end %>
<%- end -%>
<%- Scrum::Setting.tracker_fields(issue.tracker.id, Scrum::Setting::TrackerFields::SPRINT_BOARD_CUSTOM_FIELDS).each do |custom_field_id| -%>
<%- custom_field = CustomField.find(custom_field_id)
field_value = issue.custom_field_values.select{|c| c.custom_field == custom_field}.first -%>
<%- if custom_field.visible_by?(issue.project, User.current) and
field_value.present? and field_value.value.present? and
(!field_value.value.is_a?(Array) or (field_value.value.any? and !field_value.value.first.blank?)) -%>
<%= content_tag("div", :title => custom_field.description) do %>
<span class="post-it-legend"><%= custom_field.name %>:</span>
<%= show_value(field_value) %>
<% end %>
<%- end -%>
<%- end -%>
<%- if Scrum::Setting.tracker_field?(issue.tracker.id, :status_id, Scrum::Setting::TrackerFields::SPRINT_BOARD_FIELDS) and issue.status -%>
<%= content_tag("div") do %>
<span class="post-it-legend"><%= l(:field_status) %>:</span>
<%= h(issue.status.name) %>
<% end %>
<%- end -%>
<%- if Scrum::Setting.tracker_field?(issue.tracker.id, :category_id, Scrum::Setting::TrackerFields::SPRINT_BOARD_FIELDS) and issue.category -%>
<%= content_tag("div") do %>
<span class="post-it-legend"><%= l(:field_category) %>:</span>
<%= h(issue.category.name) %>
<% end %>
<%- end -%>
<%- if Scrum::Setting.tracker_field?(issue.tracker.id, :fixed_version_id, Scrum::Setting::TrackerFields::SPRINT_BOARD_FIELDS) and issue.fixed_version -%>
<%= content_tag("div") do %>
<span class="post-it-legend"><%= l(:field_fixed_version) %>:</span>
<%= link_to_version(issue.fixed_version) %>
<% end %>
<%- end -%>
<%- there_is_last_sprint = (!(project.last_sprint.nil?))
render_move_to_last_sprint = (there_is_last_sprint and
((sprint.is_product_backlog?) or (sprint.id != project.last_sprint.id)))
menu_needed = render_move_to_last_sprint -%>
<%- if menu_needed -%>
<div class="contextual scrum-menu">
<%- if User.current.allowed_to?(:edit_issues, project) and
User.current.allowed_to?(:edit_sprint_board, project) and
sprint.open? -%>
<%- if render_move_to_last_sprint -%>
<%= link_to(l(:label_move_not_closed_pbis_to_last_sprint), move_not_closed_pbis_to_last_sprint_path(sprint),
:method => :post, :class => 'icon icon-sprint-board') %>
<%- end -%>
<%- end -%>
</div>
<%- end -%>
<table class="box attributes" width="100%">
<tr valign="top">
<td width="300px">
<div class="tabular">
<p>
<label><%= l(:label_sprint) %>:</label>
<%- options = project.sprints.collect{ |other_sprint|
[other_sprint.name, path.call(other_sprint, :type => @type)]}
options = options_for_select(options, path.call(sprint, :type => @type)) -%>
<%= select_tag "selected_sprint_id", options,
:onchange => "if (this.value != '') { window.location = this.value; }" %>
</p>
<p>
<label><%= l(:field_start_date) %>:</label>
<%= format_date sprint.sprint_start_date %>
</p>
<p>
<label><%= l(:field_end_date) %>:</label>
<%= format_date sprint.sprint_end_date %>
</p>
<%- if params[:action] == 'show' -%>
<p>
<label><%= l(:label_filter_by_assignee) %>:</label>
<select id="tasks-assignees"></select>
</p>
<%- end -%>
<%- if sprint.shared and @subprojects and !@only_one -%>
<p>
<label><%= l(:label_filter_by_project) %>:</label>
<%- options = options_for_select(@subprojects, @selected_subproject) -%>
<%= select_tag('select_project_id', options,
:onchange => "if (this.value != '') { window.location = this.value; }") %>
</p>
<%- end -%>
</div>
</td>
<td>
<%- if !(sprint.description.blank?) -%>
<%= textilizable(sprint.description) %>
<%- end -%>
</td>
<%- if Scrum::Setting.show_project_totals_on_sprint -%>
<%= render :partial => 'post_its/stats', :locals => {:stats => @stats} %>
<%- end -%>
</tr>
</table>
<%- pbi_id = "pbi_#{pbi.id}" -%>
<%- is_simple = pbi.is_simple_pbi? -%>
<table id="<%= pbi_id %>"
class="<%= pbi.post_it_css_class(:rotate => true,
:scale => true,
:draggable => true) %>">
<tr>
<td class="content" colspan="3">
<%- if User.current.allowed_to?(:edit_sprint_board, project) and
User.current.allowed_to?(:edit_issues, project) and
pbi.editable? and !read_only -%>
<%= link_to "", edit_pbi_path(pbi), :remote => true, :method => "GET",
:class => "icon icon-edit float-icon", :title => l(:label_edit_pbi) %>
<%- unless is_simple -%>
<%- Tracker.task_trackers.each do |tracker| -%>
<%= link_to "", new_task_path(pbi, tracker), :remote => true, :method => "GET",
:class => "icon icon-add float-icon",
:title => l(:label_add_task, :tracker => tracker.name) %>
<%- end -%>
<%- end -%>
<%- end -%>
<%= render_issue_icons(pbi) %>
<div>
<%= link_to_issue(pbi, :tracker => false, :truncate => 150,
:project => project != pbi.project) %>
</div>
<%= render :partial => "post_its/sprint_board/fields", :formats => [:html],
:locals => {:project => project, :issue => pbi} %>
<%= render :partial => "post_its/sprint_board/custom_fields", :formats => [:html],
:locals => {:project => project, :issue => pbi} %>
<div class="doers-reviewers-post-its-container">
<%- if pbi.scrum_blocked? -%>
<div class="<%= Issue.blocked_post_it_css_class %>">
<%= l :label_blocked %>
</div>
<%- end -%>
</div>
</td>
</tr>
<tr>
<td class="estimation">
<%- if pbi.has_story_points? -%>
<%= render :partial => "common/scrum_story_points", :formats => [:html],
:locals => {:pbi => pbi, :read_only => read_only} %>
<%- end -%>
<%- if Scrum::Setting.use_remaining_story_points? -%>
<%= render :partial => "common/scrum_remaining_story_points", :formats => [:html],
:locals => {:pbi => pbi, :read_only => read_only} %>
<%- end -%>
</td>
<td class="spent">
<%- if User.current.allowed_to?(:view_time_entries, project) -%>
<%= render_hours(pbi.total_time, :title => l(:label_total_effort),
:ignore_zero => pbi.children.empty?) %>
<%- end -%>
</td>
<td class="pending">
<%- if User.current.allowed_to?(:view_pending_effort, project) -%>
<%= render_hours(pbi.pending_effort_children, :title => l(:field_pending_effort),
:ignore_zero => pbi.children.empty?) %>
<%- end -%>
</td>
</tr>
</table>
<%= render :partial => 'post_its/sprint_board/pbi', :formats => [:js],
:locals => {:project => project,
:pbi => pbi,
:pbi_id => pbi_id,
:other_pbi_status_ids => defined?(other_pbi_status_ids) ? other_pbi_status_ids : []} %>
<%- if User.current.allowed_to?(:sort_sprint_board, project) and pbi.editable? -%>
<%= javascript_tag do %>
draggableOnTouchScreen("pbi_<%= pbi.id %>_handle");
<% end %>
<%- end -%>
<%- if User.current.allowed_to?(:edit_sprint_board, project) and pbi.editable? and pbi.is_simple_pbi? -%>
<%= javascript_tag do %>
<%- other_pbi_status_ids.each do |pbi_status_id| -%>
$(document).ready(function() {
$("#<%= pbi_id %>").draggable({
revert: "invalid",
axis: "x",
containment: "<%= pbi_status_id %>",
cursor: "ew-resize",
stack: "table"
});
draggableOnTouchScreen("<%= pbi_id %>");
});
<%- end -%>
<% end %>
<%- end -%>
\ No newline at end of file
<%- pbi_row_id = "pbi_#{pbi.id}_row" -%>
<%- is_simple = pbi.is_simple_pbi? -%>
<tr class="sprint-board"
id="<%= pbi_row_id %>">
<td class="<%= pbi.sortable? ? 'sprint-board-pbi-handle post-it-vertical-move-cursor' : '' %>"
id="pbi_<%= pbi.id %>_handle">
</td>
<td class="sprint-board">
<%- unless is_simple -%>
<%= render :partial => 'post_its/sprint_board/pbi', :formats => [:html],
:locals => {:project => project,
:pbi => pbi,
:read_only => false} %>
<%- end -%>
</td>
<%- tasks = pbi.tasks_by_status_id -%>
<%- task_statuses.each do |status| -%>
<%- pbi_status_id = "pbi_#{pbi.id}_status_#{status.id}" -%>
<%- other_pbi_status_ids = task_statuses.select{|other| other != status}.collect{
|other| "pbi_#{pbi.id}_status_#{other.id}"
} -%>
<td id="<%= pbi_status_id %>" class="sprint-board">
<%- if is_simple -%>
<%- if pbi.status == status -%>
<%= render :partial => 'post_its/sprint_board/pbi', :formats => [:html],
:locals => {:project => project,
:pbi => pbi,
:other_pbi_status_ids => other_pbi_status_ids,
:read_only => false} %>
<%- end -%>
<%- else -%>
<%- tasks[status.id].each do |task| -%>
<%= render :partial => 'post_its/sprint_board/task', :formats => [:html],
:locals => {:project => project,
:task => task,
:pbi_status_id => pbi_status_id,
:other_pbi_status_ids => other_pbi_status_ids,
:read_only => false} %>
<%- end -%>
<%- end -%>
</td>
<%= render :partial => 'post_its/sprint_board/pbi_status', :formats => [:js],
:locals => {:project => project,
:pbi_id => pbi.id,
:pbi_status_id => pbi_status_id,
:status => status} %>
<%- end -%>
</tr>
<%= javascript_tag do %>
$(document).ready(function() {
$("#<%= pbi_status_id %>").droppable({
accept: ".sprint-task,.sprint-pbi",
drop: function(event, ui) {
if ($.isFunction($.fn.setupAjaxIndicator)) {
setupAjaxIndicator();
}
if (!(ui.draggable.is($(this).children()))) {
$.ajax({
url: "<%= project_sprints_change_issue_status_path(@project) %>",
type: "POST",
data: {task: encodeURIComponent($(ui.draggable).attr("id")),
status: encodeURIComponent("<%= status.id %>")},
error: function() {
alert("<%= l(:error_changing_task_status) %>");
var task = $(ui.draggable);
task.detach();
task.appendTo($("#<%= "pbi_#{pbi_id}_status_" %>" + ui.draggable.attr("old-status")));
},
complete: function() {
if ($.isFunction($.fn.hideOnLoad)) {
hideOnLoad();
}
},
success: function() {
ui.draggable.attr("old-status", "<%= status.id %>");
window["container_" + $(ui.draggable).attr("id")] = "";
}
});
}
ui.draggable.appendTo($(this)).removeAttr("style");
ui.draggable.attr("style", "position: relative; " + ui.draggable.attr("style"));
}
});
});
<% end %>
\ No newline at end of file
<%- task_id = "task_#{task.id}" -%>
<table id="<%= task_id %>"
class="<%= task.post_it_css_class(:rotate => true,
:scale => true,
:draggable => true) %>"
old-status="<%= task.status.id %>">
<tr>
<td class="content" colspan="3">
<div class="content">
<%- if User.current.allowed_to?(:edit_sprint_board, project) and
User.current.allowed_to?(:edit_issues, project) and
task.editable? and !read_only -%>
<%= link_to('', edit_task_path(task), :remote => true, :method => 'GET',
:class => 'icon icon-edit float-icon', :title => l(:label_edit_task)) %>
<%- end -%>
<%- if User.current.allowed_to?(:log_time, project) -%>
<%= link_to('',
new_scrum_time_entry_path(task,
:pbi_status_id => pbi_status_id,
:other_pbi_status_ids => other_pbi_status_ids.join(','),
:task_id => task_id),
:remote => true, :method => 'GET',
:class => 'icon icon-log-time float-icon',
:title => l(:button_log_time)) %>
<%- end -%>
<%= render_issue_icons(task) %>
<%= render(:partial => 'post_its/sprint_board/fields', :formats => [:html],
:locals => {:project => project, :issue => task}) %>
<div>
<%= link_to_issue(task, :tracker => false, :truncate => 100,
:project => project != task.project) %>
</div>
<%= render(:partial => 'post_its/sprint_board/custom_fields', :formats => [:html],
:locals => {:project => project, :issue => task}) %>
<div class="doers-reviewers-post-its-container">
<%- task.doers.each do |doer| -%>
<div class="<%= Issue.doer_post_it_css_class %>">
<%= avatar(doer, :size => '14') %> <%= link_to_user doer %>
</div>
<%- end -%>
<%- task.reviewers.each do |reviewer| -%>
<div class="<%= Issue.reviewer_post_it_css_class %>">
<%= avatar(reviewer, :size => '14') %> <%= link_to_user reviewer %>
</div>
<%- end -%>
<%- if task.scrum_blocked? -%>
<div class="<%= Issue.blocked_post_it_css_class %>">
<%= l :label_blocked %>
</div>
<%- end -%>
</div>
</div>
</td>
</tr>
<tr>
<td class="estimation">
<%= render_hours(task.estimated_hours, :title => l(:field_estimated_hours)) %>
</td>
<td class="spent">
<%- if User.current.allowed_to?(:view_time_entries, project) -%>
<%= render_hours(task.spent_hours, :title => l(:label_spent_time), :ignore_zero => true,
:link => project_time_entries_path(task.project, :issue_id => task.id)) %>
<%- end -%>
</td>
<td class="pending">
<%= render(:partial => 'common/scrum_pending_effort', :formats => [:html],
:locals => {:project => project, :task => task}) %>
</td>
</tr>
</table>
<%= render(:partial => 'post_its/sprint_board/task', :formats => [:js],
:locals => {:project => project,
:other_pbi_status_ids => other_pbi_status_ids,
:task_id => task_id,
:task => task}) %>
<%- if User.current.allowed_to?(:edit_sprint_board, project) and task.editable? -%>
<%= javascript_tag do %>
<%- other_pbi_status_ids.each do |pbi_status_id| -%>
$(document).ready(function() {
$("#<%= task_id %>").draggable({
revert: "invalid",
axis: "x",
containment: "<%= pbi_status_id %>",
cursor: "ew-resize",
stack: "table"
});
draggableOnTouchScreen("<%= task_id %>");
});
<%- end -%>
<% end %>
<%- end -%>
\ No newline at end of file
<div>
<%- if User.current.allowed_to?(:add_issues, project) and
@product_backlog.open? -%>
<%- Tracker.pbi_trackers(project).each do |tracker| -%>
<span class="post-it settings-post-it <%= tracker.post_it_css_class %>">
<%= link_to(tracker.name,
new_pbi_path(@product_backlog, tracker, :top => top),
:remote => true, :method => 'GET', :class => 'icon icon-add',
:title => l(top ? :label_add_pbi_at_top : :label_add_pbi_at_bottom,
:tracker => tracker.name)) %>
</span>
<%- end -%>
<%- end -%>
<%= link_to(l(:label_check_dependencies),
check_dependencies_product_backlog_path(@product_backlog),
:remote => true, :method => 'GET', :class => 'icon icon-help') %>
</div>
<br />
<%= javascript_tag do %>
// Create the tooltip DOM element.
var toolTip = document.createElement("div"),
leftOffset = -(~~$("html").css("padding-left").replace("px", "") + ~~$("body").css("margin-left").replace("px", "")),
topOffset = -32;
toolTip.className = "graphic-tooltip";
document.body.appendChild(toolTip);
// Chart data.
var data = {
// Horizontal set of values (ordinal), vertical values (linear).
"xScale": "ordinal",
"yScale": "linear",
"yMin": 0,
"main": [
<%- serieIndex = 0
series.each do |serie| -%>
{
// Sprint story points serie.
"className": ".story-points-<%= serieIndex %>",
"data": [
<%- serie[:data].each_with_index do |value, i| -%>
{
"x": <%= i %>,
"y": <%= value[:pending_story_points] %>,
"tooltip": "<%= value[:story_points_tooltip] %>"
},
<%- end -%>
]
},
<%- serieIndex += 1 -%>
<%- end -%>
]
};
// X axis labels.
var x_axis_labels = [
<%- x_axis_labels.each do |label| -%>
'<%= label %>',
<%- end -%>
];
// Chart options.
var options = {
// Fill axis labels.
"tickFormatX": function(x) { return x_axis_labels[x]; },
// Render a tooltip when mouse goes over the values.
"mouseover": function(d, i) {
var pos = $(this).offset();
$(toolTip).text(d.tooltip)
.css({top: topOffset + pos.top, left: pos.left + leftOffset})
.show();
},
// Hide the tooltip when mouse exists the values.
"mouseout": function(x) {
$(toolTip).hide();
}
};
// Generate the chart.
new xChart("line-dotted", data, "#burndown-chart", options);
<% end %>
<table class="box attributes" width="100%">
<%= render :partial => 'post_its/product_backlog/head',
:locals => {:product_backlog => product_backlog,
:path => method(:burndown_product_backlog_path)} %>
<%= render :partial => 'scrum/velocity_form',
:locals => {:url => burndown_product_backlog_path(product_backlog),
:sprints_count => sprints_count,
:velocity => velocity,
:velocity_type => velocity_type} %>
</table>
<h3 class="title"><%= l(:label_check_dependencies) %></h3>
<%- if @pbis_dependencies.count <= 0 -%>
<%= l(:label_no_invalid_dependencies) %>
<%- else -%>
<p><%= l(:label_invalid_dependencies) %></p>
<ul>
<%- @pbis_dependencies.each do |pbi_dependencies| -%>
<%- if pbi_dependencies[:dependencies].count > 0 -%>
<li>
<%= raw l(:label_invalid_dependencies_for_pbi, :pbi => link_to_issue(pbi_dependencies[:pbi])) %>
<ul>
<%- pbi_dependencies[:dependencies].each do |dependency| -%>
<li><%= link_to_issue(dependency) %></li>
<%- end -%>
</ul>
</li>
<%- end -%>
<%- end -%>
</ul>
<%- end -%>
<%= render :partial => 'scrum/velocity_form',
:locals => {:url => release_plan_product_backlog_path(@product_backlog),
:sprints_count => @sprints_count,
:velocity => @velocity,
:velocity_type => @velocity_type} %>
<tr>
<td colspan="3">
<%= l(:label_release_plan_stats,
:sps => @total_story_points,
:pbis => @pbis_with_estimation,
:pbis_without_sps => @pbis_without_estimation) %>
</td>
</tr>
<%= render :partial => 'common/scrum_backlog_menu' %>
<h2><%= l(:label_product_backlog_burndown_chart) %></h2>
<% content_for :header_tags do %>
<%= javascript_include_tag 'd3.min.js', :plugin => 'scrum' %>
<%= javascript_include_tag 'xcharts.min.js', :plugin => 'scrum' %>
<%= stylesheet_link_tag 'xcharts.min.css', :plugin => 'scrum' %>
<% end %>
<div id="messages"></div>
<%= render :partial => 'burndown_head',
:locals => {:product_backlog => @product_backlog,
:sprints_count => @sprints_count,
:velocity => @velocity,
:velocity_type => @velocity_type} %>
<%- if @warning -%>
<p class="warning"><%= @warning %></p>
<%- end -%>
<div style="position: relative;">
<%- unless @only_one -%>
<svg height="500" width="350" class="xchart"
style="position: absolute; top: 0px; right: 0px;">
<%- i = 0 -%>
<%- row_space = 20 -%>
<%- @series.each do |serie| -%>
<g class="line color<%= i %>">
<path class="line" stroke-width="10" d="M5 <%= 15 + (i * row_space) %> H70" />
</g>
<g class="axis">
<text x="80" y="<%= 18 + (i * row_space) %>"><%= serie[:label] %></text>
</g>
<%- i += 1 -%>
<%- end -%>
</svg>
<%- end -%>
<figure id="burndown-chart"
style="left-margin: auto; right-margin:auto; width: 90%; height: 500px;" />
</div>
<%= render :partial => 'product_backlog/burndown', :formats => [:js],
:locals => {:series => @series, :x_axis_labels => @x_axis_labels} %>
<%= render :partial => 'common/scrum_footer' %>
$("#ajax-modal").html("<%= escape_javascript(render :partial => "product_backlog/check_dependencies") %>");
showModal("ajax-modal", "800px");
\ No newline at end of file
<%= render :partial => 'common/scrum_backlog_menu' %>
<h2>
<%= l(:label_release_plan) %>
</h2>
<table class="box attributes" width="100%">
<%= render :partial => 'post_its/product_backlog/head',
:locals => {:project => @project, :product_backlog => @product_backlog,
:path => method(:release_plan_product_backlog_path)} %>
<%= render :partial => 'release_plan_head',
:locals => {:sprints_count => @sprints_count,
:sps_per_sprint => @velocity,
:default_sps_per_sprint => @default_velocity,
:use_not_scheduled_pbis_for_velocity => @use_not_scheduled_pbis_for_velocity,
:total_story_points => @total_story_points,
:pbis_with_estimation => @pbis_with_estimation,
:pbis_without_estimation => @pbis_without_estimation} %>
</table>
<%= render :layout => 'common/fullscreen_content' do %>
<table class="sprint-board">
<tr class="sprint-board">
<th class="sprint-board"><%= l(:label_sprint_plural) %></th>
<th class="sprint-board"><%= l(:label_version_plural) %></th>
<th class="sprint-board"><%= l(:label_pbi_plural) %></th>
</tr>
<%- @sprints.each_with_index do |sprint, index| -%>
<tr class="sprint-board">
<td class="sprint-board" align="center">
<%= "#{l(:field_sprint)} +#{index + 1}" %>
</td>
<td class="sprint-board">
<%= raw(sprint[:versions].collect{|version| link_to_version(version)}.join(", ")) %>
</td>
<td class="sprint-board">
<ul id="pbis" class="pbis">
<%- sprint[:pbis].each do |pbi| -%>
<%= render :partial => 'post_its/product_backlog/pbi',
:locals => {:project => @project, :pbi => pbi, :read_only => true} %>
<%- end -%>
</ul>
</td>
</tr>
<%- end -%>
</table>
<% end %>
<%= render :partial => 'common/scrum_footer' %>
<%= render :partial => 'common/scrum_backlog_menu' %>
<h2><%= l(:label_product_backlog) %></h2>
<table class="box attributes" width="100%">
<%= render :partial => 'post_its/product_backlog/head',
:locals => {:project => @project, :product_backlog => @product_backlog,
:path => method(:product_backlog_path)} %>
</table>
<%= render :layout => 'common/fullscreen_content' do %>
<%= render :partial => 'product_backlog/actions',
:locals => {:project => @project, :top => true} %>
<div id="messages"></div>
<ul id="pbis" class="pbis">
<%- @product_backlog.pbis(@pbi_filter).each do |pbi| -%>
<%= render :partial => 'post_its/product_backlog/pbi',
:locals => {:project => @project, :pbi => pbi, :read_only => false} %>
<%- end -%>
</ul>
<%= render :partial => 'product_backlog/actions',
:locals => {:project => @project, :top => false} %>
<% end %>
<%- if User.current.allowed_to?(:edit_product_backlog, @project) -%>
<%= render :partial => 'post_its/product_backlog/pbis',
:formats => [:js],
:locals => {:project => @project, :product_backlog => @product_backlog} %>
<%- end -%>
<%= render :partial => 'common/scrum_footer' %>
<%- heads_for_wiki_formatter -%>
<div class="contextual scrum-menu">
<%= link_to(l(:label_product_backlog_new),
new_project_sprint_path(@project, :back_url => '', :create_product_backlog => true),
:class => 'icon icon-add') if User.current.allowed_to?(:manage_sprints, @project) %>
<%= render_scrum_help('product_backlogs') %>
</div>
<%- if @project.product_backlogs.empty? -%>
<p class="nodata"><%= l(:label_no_data) %></p>
<%- else -%>
<table class="list">
<thead>
<tr>
<th><%= l(:field_name) %></th>
<th><%= l(:field_status) %></th>
<th><%= l(:field_shared) %></th>
<th><%= l(:label_pbi_plural) %></th>
<th style="width:15%"></th>
</tr>
</thead>
<tbody>
<% for sprint in @project.product_backlogs %>
<tr class="version <%= cycle 'odd', 'even' %> <%= sprint.status %>">
<td><%= link_to(h(sprint.name), product_backlog_path(sprint)) %></td>
<td class="status"><%= l("label_sprint_status_#{sprint.status}") %></td>
<td align="center"><%= checked_image sprint.shared %></td>
<td align="center"><%= sprint.pbis.count %></td>
<td class="buttons">
<%- if User.current.allowed_to?(:manage_sprints, @project) -%>
<%= link_to l(:button_edit), edit_sprint_path(sprint),
:class => 'icon icon-edit' %>
<%= delete_link sprint_path(sprint) %>
<%- end -%>
</td>
</tr>
<% end; reset_cycle %>
<tbody>
</table>
<%- end -%>
<div class="contextual scrum-menu">
<%= link_to(l(:label_sprint_new), new_project_sprint_path(@project, :back_url => ''),
:class => 'icon icon-add') if User.current.allowed_to?(:manage_sprints, @project) %>
<%= render_scrum_help('sprints') %>
</div>
<%- if @project.sprints.empty? -%>
<p class="nodata"><%= l(:label_no_data) %></p>
<%- else -%>
<table class="list">
<thead>
<tr>
<th><%= l(:field_name) %></th>
<th><%= l(:field_start_date) %></th>
<th><%= l(:field_end_date) %></th>
<th><%= l(:field_status) %></th>
<th><%= l(:field_shared) %></th>
<th><%= l(:label_pbi_plural) %></th>
<th><%= l(:label_task_plural) %></th>
<th style="width:15%"></th>
</tr>
</thead>
<tbody>
<% for sprint in @project.sprints %>
<tr class="version <%= cycle 'odd', 'even' %> <%= sprint.status %>">
<td><%= link_to(h(sprint.name), sprint.is_product_backlog? ? product_backlog_path(sprint) : sprint) %></td>
<td align="center"><%= format_date(sprint.sprint_start_date) %></td>
<td align="center"><%= format_date(sprint.sprint_end_date) %></td>
<td class="status"><%= l("label_sprint_status_#{sprint.status}") %></td>
<td align="center"><%= checked_image sprint.shared %></td>
<td align="center"><%= sprint.pbis.count %></td>
<td align="center"><%= sprint.tasks.count %></td>
<td class="buttons">
<%- if User.current.allowed_to?(:manage_sprints, @project) -%>
<%= link_to l(:label_edit_effort), edit_effort_sprint_path(sprint),
:class => 'icon icon-time-add' %>
<%= link_to l(:button_edit), edit_sprint_path(sprint),
:class => 'icon icon-edit' %>
<%= delete_link sprint_path(sprint) %>
<%- end -%>
</td>
</tr>
<% end; reset_cycle %>
<tbody>
</table>
<%- end -%>
<h3 class="title"><%= l(:field_time_entries) %></h3>
<div id="time-entry-messages" />
<%= form_for :time_entry, :url => create_scrum_time_entry_path(@issue), :remote => true do |f| %>
<%= back_url_hidden_field_tag %>
<%= error_messages_for 'time_entry' %>
<%= hidden_field_tag :pbi_status_id, @pbi_status_id %>
<%= hidden_field_tag :other_pbi_status_ids, @other_pbi_status_ids %>
<%= hidden_field_tag :task_id, @task_id %>
<div class="box tabular">
<p>
<label><%= l(:field_hours) %>:</label>
<%= f.text_field :hours, :size => 15, :required => true %>
</p>
<p>
<label><%= l(:field_spent_on) %>:</label>
<%= f.text_field :spent_on, :size => 10, :value => Date.today, :required => true %>
</p>
<p>
<label><%= l(:field_comments) %>:</label>
<%= f.text_field :comments, :size => 15, :maxlength => 255 %>
</p>
<p>
<label><%= l(:field_activity) %>:</label>
<%= f.select :activity_id,
@project.activities.collect{|activity| [activity.name, activity.id]},
:required => true %>
</p>
<p>
<label><%= l(:field_user) %>:</label>
<%= f.select :user_id,
principals_options_for_select(@issue.assignable_users, @issue.assigned_to),
:required => true %>
</p>
<%- TimeEntry.new.custom_field_values.each do |value| -%>
<p>
<label><%= value.custom_field.name %>:</label>
<%= custom_field_tag :time_entry, value %>
</p>
<%- end -%>
<p>
<%= submit_tag l(:button_save) %>
</p>
</div>
<% end %>
<%= render :partial => "scrum/issue_attributes", :formats => [:html],
:locals => {:f => f, :issue => issue} %>
<p class="buttons">
<%= submit_tag l(:button_edit), :name => "edit", :disable_with => l(:label_loading) %>
<%= submit_tag l(:button_cancel), :name => nil, :onclick => "hideModal(this);", :type => "button" %>
</p>
\ No newline at end of file
<div id="main" class="nosidebar">
<div id="content">
<h3 class="title"><%= "#{@pbi.tracker.name} ##{@pbi.id}" %></h3>
<%- there_is_last_sprint = (!(@project.last_sprint.nil?))
there_is_product_backlog = (!(@project.product_backlogs.empty?))
render_move_to_last_sprint = (there_is_last_sprint and
((@pbi.sprint.is_product_backlog?) or (@pbi.sprint.id != @project.last_sprint.id)))
render_move_to_product_backlog = (there_is_product_backlog and
((!(@project.product_backlogs.include?(@pbi.sprint))) or
(@project.product_backlogs.size > 1)))
render_move_pbi = (there_is_product_backlog and (@pbi.sprint.id == @project.product_backlogs.first.id) and
User.current.allowed_to?(:sort_product_backlog, @project))
menu_needed = (render_move_to_last_sprint or render_move_to_product_backlog or render_move_pbi) -%>
<%- if menu_needed -%>
<div class="modal-issue-menu">
<%- if render_move_pbi -%>
<%= form_tag(move_pbi_path(@pbi, :before), :method => :get, :remote => true,
:id => 'move_pbi_before') do %>
<%= link_to(l(:label_move_pbi_before), '#', :onclick => "$('#move_pbi_before').submit(); return(false);") %>:
<%= text_field_tag(:before_other_pbi, '', :size => 3) %>
<%= javascript_tag "observeAutocompleteField('before_other_pbi', '#{escape_javascript auto_complete_issues_path(:project_id => @project, :scope => (Setting.cross_project_issue_relations? ? 'all' : nil))}')" %>
<% end %>
<%= form_tag(move_pbi_path(@pbi, :after), :method => :get, :remote => true,
:id => 'move_pbi_after') do %>
<%= link_to(l(:label_move_pbi_after), '#', :onclick => "$('#move_pbi_after').submit(); return(false);") %>:
<%= text_field_tag(:after_other_pbi, '', :size => 3) %>
<%= javascript_tag "observeAutocompleteField('after_other_pbi', '#{escape_javascript auto_complete_issues_path(:project_id => @project, :scope => (Setting.cross_project_issue_relations? ? 'all' : nil))}')" %>
<% end %>
<%- end -%>
<%- if render_move_to_last_sprint -%>
<%= link_to(l(:label_move_pbi_to_last_sprint), move_to_last_sprint_path(@pbi),
:remote => true, :method => :post, :class => "icon icon-sprint-board") %>
<%- end -%>
<%- if render_move_to_product_backlog -%>
<%- @project.product_backlogs.each do |product_backlog| -%>
<%= link_to(l(:label_move_pbi_to_name, :name => product_backlog.name),
move_to_product_backlog_path(@pbi, :id => product_backlog.id),
:remote => true, :method => :post, :class => "icon icon-product-backlog") %>
<%- end -%>
<%- end -%>
</div>
<%- end -%>
<div id="popup-messages"></div>
<%= labelled_form_for @pbi, :url => update_pbi_path(@pbi), :method => :post, :remote => true do |f| %>
<%= back_url_hidden_field_tag %>
<%= f.hidden_field :tracker_id, :value => @pbi.tracker_id %>
<%= f.hidden_field :sprint_id, :value => @pbi.sprint_id %>
<%= error_messages_for 'pbi' %>
<%= render :partial => 'scrum/edit_issue_attributes', :formats => [:html],
:locals => {:f => f, :issue => @pbi} %>
<% end %>
</div>
</div>
\ No newline at end of file
<div id="main" class="nosidebar">
<div id="content">
<h3 class="title"><%= @issue.tracker.name %></h3>
<div id="popup-messages"></div>
<%= labelled_form_for @issue, :url => update_task_path(@issue), :method => :post, :remote => true do |f| %>
<%= back_url_hidden_field_tag %>
<%= f.hidden_field :tracker_id, :value => @issue.tracker_id %>
<%= error_messages_for "task" %>
<%= render :partial => "scrum/edit_issue_attributes", :formats => [:html],
:locals => {:f => f, :issue => @issue} %>
<% end %>
</div>
</div>
<div class="box tabular">
<%- if issue.safe_attribute?(:subject) -%>
<p>
<%= f.text_field :subject, :size => 10, :maxlength => 255, :required => true %>
</p>
<%- end -%>
<%- if issue.safe_attribute?(:project_id) and
issue.sprint and issue.sprint.project and
issue.sprint.shared? and issue.sprint.project.children.any? -%>
<p>
<%= f.select :project_id, project_selector_tree(issue.sprint.project), {:required => true} %>
</p>
<%- end -%>
<%- allowed_statuses = issue.new_statuses_allowed_to(User.current) -%>
<%- if issue.safe_attribute?(:status_id) and allowed_statuses.present? -%>
<p>
<%= f.select :status_id, (allowed_statuses.collect {|status| [status.name, status.id]}), {:required => true} %>
</p>
<%- end -%>
<%- if issue.safe_attribute?(:priority_id) -%>
<p>
<%= f.select :priority_id, (IssuePriority.active.collect {|p| [p.name, p.id]}), {:required => true, :disabled => !issue.leaf?} %>
</p>
<%- end -%>
<%- if issue.field?(:assigned_to_id) -%>
<p>
<%= f.select :assigned_to_id, principals_options_for_select(issue.assignable_users, issue.assigned_to), :include_blank => true, :required => issue.required_attribute?('assigned_to_id') %>
</p>
<%- end -%>
<%- if issue.field?(:category_id) and issue.project.issue_categories.any? -%>
<p>
<%= f.select :category_id,
(issue.project.issue_categories.collect {|c| [c.name, c.id]}),
:include_blank => true,
:required => issue.required_attribute?(:category_id) %>
</p>
<% end %>
<%- if issue.field?(:fixed_version_id) and issue.assignable_versions.any? -%>
<p><%= f.select :fixed_version_id, version_options_for_select(issue.assignable_versions, issue.fixed_version), :include_blank => true, :required => issue.required_attribute?('fixed_version_id') %>
</p>
<%- end -%>
<%- method = (Redmine::VERSION::STRING < '3.3.0') ? :text_field : :date_field -%>
<%- if issue.field?(:start_date) -%>
<p>
<%= f.send method, :start_date, :size => 10, :disabled => !issue.leaf?, :required => issue.required_attribute?('start_date') %><%= calendar_for('issue_start_date') if issue.leaf? %>
</p>
<%- end -%>
<%- if issue.field?(:due_date) -%>
<p>
<%= f.send method, :due_date, :size => 10, :disabled => !issue.leaf?, :required => issue.required_attribute?('due_date') %><%= calendar_for('issue_due_date') if issue.leaf? %>
</p>
<%- end -%>
<%- if issue.field?(:estimated_hours) -%>
<p>
<%= f.text_field :estimated_hours, :size => 4, :maxlength => 10 %>
</p>
<%- end -%>
<%- if User.current.allowed_to?(:edit_pending_effort, issue.project) and
issue.is_task? -%>
<p>
<%= f.text_field :pending_effort, :size => 4, :maxlength => 10, :label => l(:field_pending_effort) %>
</p>
<%- end -%>
<%- if issue.field?(:done_ratio) and issue.leaf? and Issue.use_field_for_done_ratio? -%>
<p>
<%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r * 10} %", r * 10] }), :required => issue.required_attribute?('done_ratio') %>
</p>
<%- end -%>
<%- issue.editable_custom_field_values.each do |value| -%>
<%- if issue.custom_field?(value.custom_field) -%>
<p>
<%= custom_field_tag_with_label :issue, value, :required => value.custom_field.is_required %>
</p>
<%- end -%>
<%- end -%>
<%- if User.current.allowed_to?(:edit_pending_effort, issue.project) and
issue.is_pbi? and Scrum::Setting::use_remaining_story_points? -%>
<p>
<%= f.text_field :pending_effort, :size => 4, :maxlength => 10,
:label => l(:label_pending_sps) %>
</p>
<%- end -%>
<%- if issue.field?(:description) -%>
<p>
<%= f.label_for_field :description, :required => issue.required_attribute?(:description) %>
<%= content_tag :span, :id => "issue_description_and_toolbar" do %>
<%= f.text_area :description,
:rows => 8,
:accesskey => accesskey(:edit),
:class => "wiki-edit",
:no_label => true %>
<% end %>
</p>
<%= wikitoolbar_for 'issue_description' %>
<%- end -%>
<%- if issue.id -%>
<p>
<%= f.text_area :notes, :rows => 8, :class => "wiki-edit" %>
</p>
<%= wikitoolbar_for 'issue_notes' %>
<%- end -%>
</div>
<%= render :partial => "scrum/issue_attributes", :formats => [:js],
:locals => {:issue => issue} %>
<%- if issue.field?(:estimated_hours) and
issue.is_task? and
User.current.allowed_to?(:edit_pending_effort, issue.project) -%>
<%= javascript_tag do %>
var changeEstimatedHours = !$("#issue_estimated_hours").val();
var changePendingEffort = !$("#issue_pending_effort").val();
$("#issue_estimated_hours").change(function() {
if (changePendingEffort && $("#issue_estimated_hours").val()) {
$("#issue_pending_effort").val($("#issue_estimated_hours").val());
}
});
$("#issue_pending_effort").change(function() {
if (changeEstimatedHours && $("#issue_pending_effort").val()) {
$("#issue_estimated_hours").val($("#issue_pending_effort").val());
}
});
<% end %>
<%- end -%>
<%= javascript_tag do %>
$('#ajax-modal').on('click', 'div.jstTabs a.tab-preview', function(event){
var tab = $(event.target);
var url = tab.data('url');
var form = tab.parents('form');
var jstBlock = tab.parents('.jstBlock');
var element = encodeURIComponent(jstBlock.find('.wiki-edit').val());
var attachments = form.find('.attachments_fields input').serialize();
$.ajax({
url: url,
type: 'post',
data: "text=" + element + '&' + attachments,
success: function(data){
jstBlock.find('.wiki-preview').html(data);
}
});
});
<% end %>
<%= render :partial => "scrum/issue_attributes", :formats => [:html],
:locals => {:f => f, :issue => issue} %>
<p class="buttons">
<%= submit_tag l(:button_create_and_continue), :name => "create_and_continue", :disable_with => l(:label_loading) %>
<%= submit_tag l(:button_create), :name => "create", :disable_with => l(:label_loading) %>
<%= submit_tag l(:button_cancel), :name => nil, :onclick => "hideModal(this);", :type => "button" %>
</p>
\ No newline at end of file
<div id="main" class="nosidebar">
<div id="content">
<h3 class="title"><%= @pbi.tracker.name %></h3>
<div id="popup-messages"></div>
<%= labelled_form_for @pbi, :url => create_pbi_path(@sprint), :remote => true do |f| %>
<%= back_url_hidden_field_tag %>
<%= f.hidden_field :tracker_id, :value => @pbi.tracker_id %>
<%= f.hidden_field :sprint_id, :value => @pbi.sprint_id %>
<%= hidden_field_tag(:top, true) if @top %>
<%= error_messages_for "pbi" %>
<%= render :partial => "scrum/new_issue_attributes", :formats => [:html],
:locals => {:f => f, :issue => @pbi} %>
<% end %>
</div>
</div>
<div id="main" class="nosidebar">
<div id="content">
<h3 class="title"><%= @task.tracker.name %></h3>
<div id="popup-messages"></div>
<%= labelled_form_for @task, :url => create_task_path(@pbi), :remote => true do |f| %>
<%= back_url_hidden_field_tag %>
<%= f.hidden_field :tracker_id, :value => @task.tracker_id %>
<%= error_messages_for "task" %>
<%= render :partial => "scrum/new_issue_attributes", :formats => [:html],
:locals => {:f => f, :issue => @task} %>
<% end %>
</div>
</div>
<tr>
<td colspan="3">
<%= form_tag(url, :method => :get, :id => 'velocity_form') do %>
<%= hidden_field_tag(:filter_by_project, params[:filter_by_project]) %>
<%= label_tag(:velocity_type_all) do %>
<%= radio_button_tag(:velocity_type, 'all', 'all' == velocity_type) %>
<%= l(:label_velocity_all_pbis, :count => sprints_count) %>
<% end %>
&nbsp;&nbsp;&nbsp;
<%= label_tag(:velocity_type_only_scheduled) do %>
<%= radio_button_tag(:velocity_type, 'only_scheduled', 'only_scheduled' == velocity_type) %>
<%= content_tag('abbr', :title => l(:label_velocity_only_scheduled_pbis_hint)) do %>
<%= l(:label_velocity_only_scheduled_pbis) %>
<% end %>
<% end %>
&nbsp;&nbsp;&nbsp;
<%= label_tag(:velocity_type_custom) do %>
<%= radio_button_tag(:velocity_type, 'custom', 'custom' == velocity_type) %>
<%= l(:label_velocity_custom) %>
<% end %>
<%= text_field_tag(:custom_velocity, velocity, :size => 5,
:onclick => "$('#velocity_type_custom').prop('checked', true);") %>
&nbsp;&nbsp;&nbsp;
<%= button_tag(l(:button_update)) %>
<% end %>
</td>
</tr>
<%- if @continue or defined?(@exception) -%>
<%- if defined?(@exception) -%>
<%- message = l(:error_creating_pbi, :message => @exception.message)
message_class = "error" -%>
<%- else -%>
$("#new_issue").each (function() {
this.reset();
});
<%- message = l(:notice_pbi_created)
message_class = "notice" -%>
<%- end -%>
$("#popup-messages").html("<div class=\"flash <%= message_class %>\"><%= message %></div>");
<%- else -%>
$("#ajax-modal").dialog("close");
<%- end -%>
<%- if !defined?(@exception) -%>
if ($("#pbis").length > 0) {
<%- if @top -%>
$("#pbis").prepend("<%=
escape_javascript(render :partial => "post_its/product_backlog/pbi", :formats => [:html],
:locals => {:project => @project, :pbi => @pbi, :read_only => false}).html_safe
%>");
<%- else -%>
$("#pbis").append("<%=
escape_javascript(render :partial => "post_its/product_backlog/pbi", :formats => [:html],
:locals => {:project => @project, :pbi => @pbi, :read_only => false}).html_safe
%>");
<%- end -%>
} else if ($("#sprint_board").length > 0) {
$("#sprint_board").append("<%=
escape_javascript(render :partial => "post_its/sprint_board/pbi_row", :formats => [:html],
:locals => {:project => @project, :pbi => @pbi,
:task_statuses => IssueStatus.task_statuses}).html_safe
%>");
}
<%- if (!(IssueStatus.pbi_statuses.include?(@pbi.status))) -%>
$("#<%= @pbi.sprint.is_product_backlog? ? "pbi_#{@pbi.id}" : "pbi_#{@pbi.id}_row" %>").remove();
<%- end -%>
<%- end -%>
<%- if @continue or defined?(@exception) -%>
<%- if defined?(@exception) -%>
<%- message = l(:error_creating_task, :message => @exception.message)
message_class = "error" -%>
<%- else -%>
$("#new_issue").each (function() {
this.reset();
});
<%- message = l(:notice_task_created)
message_class = "notice" -%>
<%- end -%>
$("#popup-messages").html("<div class=\"flash <%= message_class %>\"><%= message %></div>");
<%- else -%>
$("#ajax-modal").dialog("close");
<%- end -%>
<%- if !defined?(@exception) -%>
<%- pbi_row = "pbi_#{@task.parent.id}_row" -%>
if ($("#<%= pbi_row %>").length > 0) {
$("#<%= pbi_row %>").replaceWith("<%=
escape_javascript(render :partial => "post_its/sprint_board/pbi_row", :formats => [:html],
:locals => {:project => @project, :pbi => @task.parent,
:task_statuses => IssueStatus.task_statuses}).html_safe
%>");
}
<%- end -%>
<%- if defined?(@exception) -%>
<%- message = l(:error_creating_time_entry, :message => @exception.message)
message_class = "error" -%>
$("#time-entry-messages").html("<div class=\"flash <%= message_class %>\"><%= message %></div>");
<%- else -%>
$("#ajax-modal").dialog("close");
<%- end -%>
<%- if !defined?(@exception) -%>
<%- pbi_row = "pbi_#{@issue.parent.id}_row" -%>
if ($("#<%= pbi_row %>").length > 0) {
$("#<%= pbi_row %>").replaceWith("<%=
escape_javascript(render :partial => "post_its/sprint_board/pbi_row", :formats => [:html],
:locals => {:project => @project, :pbi => @issue.parent,
:task_statuses => IssueStatus.task_statuses}).html_safe
%>");
}
<%- end -%>
$("#ajax-modal").html("<%= escape_javascript(render :partial => "scrum/edit_pbi_modal") %>");
showModal("ajax-modal", "880px");
\ No newline at end of file
$("#ajax-modal").html("<%= escape_javascript(render :partial => "scrum/edit_task_modal") %>");
showModal("ajax-modal", "880px");
\ No newline at end of file
<%- if defined?(@exception) -%>
<%- message = l(:error_updating_pbi, :message => @exception.message)
message_class = 'error' -%>
$("#popup-messages").html("<div class=\"flash <%= message_class %>\"><%= message %></div>");
<%- elsif @position == 'top' -%>
$("#popup-messages").html("");
<%- if @position == 'top' -%>
$("#<%= "pbi_#{@pbi.id}" %>").parent().prepend($("#<%= "pbi_#{@pbi.id}" %>"));
<%- elsif @position == 'bottom' -%>
$("#<%= "pbi_#{@pbi.id}" %>").parent().append($("#<%= "pbi_#{@pbi.id}" %>"));
<%- elsif @position == 'before' -%>
$("#ajax-modal").dialog("close");
$("#<%= "pbi_#{@other_pbi}" %>").before($("#<%= "pbi_#{@pbi.id}" %>"));
<%- elsif @position == 'after' -%>
$("#ajax-modal").dialog("close");
$("#<%= "pbi_#{@other_pbi}" %>").after($("#<%= "pbi_#{@pbi.id}" %>"));
<%- else -%>
console.log("Invalid position: <%= @position.inspect %>");
<%- end -%>
<%- end -%>
<%- if defined?(@exception) -%>
<%- message = l(:error_updating_pbi, :message => @exception.message)
message_class = "error" -%>
$("#popup-messages").html("<div class=\"flash <%= message_class %>\"><%= message %></div>");
<%- else -%>
$("#ajax-modal").dialog("close");
$("#<%= @previous_sprint.is_product_backlog? ? "pbi_#{@pbi.id}" : "pbi_#{@pbi.id}_row" %>").remove();
<%- end -%>
<%- if defined?(@exception) -%>
<%- message = l(:error_updating_pbi, :message => @exception.message)
message_class = "error" -%>
$("#popup-messages").html("<div class=\"flash <%= message_class %>\"><%= message %></div>");
<%- else -%>
$("#ajax-modal").dialog("close");
$("#<%= "pbi_#{@pbi.id}_row" %>").remove();
<%- end -%>
$("#ajax-modal").html("<%= escape_javascript(render :partial => "scrum/new_pbi_modal") %>");
showModal("ajax-modal", "800px");
\ No newline at end of file
$("#ajax-modal").html("<%= escape_javascript(render :partial => "scrum/new_task_modal") %>");
showModal("ajax-modal", "800px");
\ No newline at end of file
$("#ajax-modal").html("<%= escape_javascript(render(:partial => "scrum/create_time_entry")) %>");
showModal("ajax-modal", "600px");
\ No newline at end of file
<div class="contextual scrum-menu">
<%= link_to(l(:button_back), project_path(@project), :class => 'icon icon-cancel') %>
<%= render_scrum_help %>
</div>
<h2>
<%= l(:label_scrum_stats) %>
</h2>
<% content_for :header_tags do %>
<%= javascript_include_tag "d3.#{Rails.env.development? ? '' : 'min.'}js", :plugin => 'scrum' %>
<%= javascript_include_tag 'xcharts.min.js', :plugin => 'scrum' %>
<%= javascript_include_tag "nv.d3.#{Rails.env.development? ? '' : 'min.'}js", :plugin => 'scrum' %>
<%= stylesheet_link_tag 'xcharts.min.css', :plugin => 'scrum' %>
<%= stylesheet_link_tag "nv.d3.#{Rails.env.development? ? '' : 'min.'}css", :plugin => 'scrum' %>
<% end %>
<h3 align="center">
<%= l(:label_closed_story_points) %>
</h3>
<div id="<%= @closed_story_points_per_sprint_chart[:id] %>">
<svg style="height: <%= @closed_story_points_per_sprint_chart[:height] %>px;">
</svg>
</div>
<%= render :partial => 'common/bar_chart',
:formats => [:js],
:locals => {:graph => @closed_story_points_per_sprint_chart,
:element => {:label => l(:enumeration_activities), :name => :activity},
:rows => @closed_story_points_per_sprint} %>
<br />
<br />
<h3 align="center">
<%= l(:label_hours_per_story_point) %>
</h3>
<div id="<%= @hours_per_story_point_chart[:id] %>">
<svg style="height: <%= @hours_per_story_point_chart[:height] %>px;">
</svg>
</div>
<%= render :partial => 'common/bar_chart',
:formats => [:js],
:locals => {:graph => @hours_per_story_point_chart,
:element => {:label => l(:enumeration_activities), :name => :activity},
:rows => @hours_per_story_point} %>
<br />
<br />
<h3 align="center">
<%= l(:label_sps_by_pbi_category) %>
(<%= l(:label_sprint_plural) %> + <%= l(:label_product_backlog) %>)
</h3>
<%- if @sps_by_pbi_category.count < 1 -%>
<p class="nodata"><%= l(:label_no_data) %></p>
<%- else -%>
<%= render :partial => 'common/stat_graph_and_table',
:locals => {:graph => {:id => 'stats_sps_per_category', :width => 400, :height => 400},
:element => {:label => l(:label_issue_category_plural), :name => :category},
:unit => {:label => l(:label_story_point_unit), :plural_label => l(:label_story_point_plural)},
:rows => @sps_by_pbi_category,
:total => @sps_by_pbi_category_total} %>
<%- end -%>
<br />
<br />
<h3 align="center">
<%= l(:label_sps_by_pbi_type) %>
(<%= l(:label_sprint_plural) %> + <%= l(:label_product_backlog) %>)
</h3>
<%- if @sps_by_pbi_type.count < 1 -%>
<p class="nodata"><%= l(:label_no_data) %></p>
<%- else -%>
<%= render :partial => 'common/stat_graph_and_table',
:locals => {:graph => {:id => 'stats_sps_per_pbi_type', :width => 400, :height => 400},
:element => {:label => l(:label_tracker_plural), :name => :tracker},
:unit => {:label => l(:label_story_point_unit), :plural_label => l(:label_story_point_plural)},
:rows => @sps_by_pbi_type,
:total => @sps_by_pbi_type_total} %>
<%- end -%>
<br />
<br />
<h3 align="center">
<%= l(:label_time_by_activity) %>
(<%= l(:label_sprint_plural) %>)
</h3>
<%- if @effort_by_activity.count < 1 -%>
<p class="nodata"><%= l(:label_no_data) %></p>
<%- else -%>
<%= render :partial => 'common/stat_graph_and_table',
:locals => {:graph => {:id => 'stats_time_per_activity', :width => 400, :height => 400},
:element => {:label => l(:enumeration_activities), :name => :activity},
:unit => {:label => 'h', :plural_label => l(:label_time_entry_plural)},
:rows => @effort_by_activity,
:total => @effort_by_activity_total} %>
<%- end -%>
<% content_for :sidebar do %>
<%- if Redmine::VERSION::STRING < '3.4.0' -%>
<%= render :partial => 'projects/sidebar' %>
<%- else -%>
<%= call_hook(:view_projects_show_sidebar_bottom, :project => @project) %>
<%- end -%>
<% end %>
<%= render :partial => 'common/scrum_footer' %>
<%- if defined?(@exception) -%>
<%- message = l(:error_updating_task, :message => @exception.message)
message_class = "error" -%>
$("#popup-messages").html("<div class=\"flash <%= message_class %>\"><%= message %></div>");
<%- else -%>
if ($("#ajax-modal").length && $("#ajax-modal").hasClass("ui-dialog-content"))
{
$("#ajax-modal").dialog("close");
}
$("#<%= "pbi_#{@issue.parent_id}_row" %>").replaceWith("<%=
escape_javascript(render(:partial => 'post_its/sprint_board/pbi_row',
:formats => [:html],
:locals => {:project => (@issue.sprint.nil? ? @project : @issue.sprint.project),
:sprint_board_id => 'sprint_board',
:pbi => @issue.is_pbi? ? @issue : @issue.parent,
:task_statuses => IssueStatus.task_statuses})).html_safe
%>");
<%- # Scrum Board : do not throw an exception if new statut is not allowed -%>
<%- if @error_message -%>
alert('<%= @error_message %>');
<%- end -%>
<%- if @old_status != @issue.status -%>
var issue = $("#<%= "#{@issue.is_pbi? ? 'pbi' : 'task'}_#{@issue.id}" %>");
issue.detach();
issue.appendTo($("#<%= "pbi_#{@issue.is_pbi? ? @issue.id : @issue.parent_id}_status_#{@issue.status.id}" %>"));
<%- end -%>
<%- end -%>
<%- if defined?(@exception) -%>
<%- message = l(:error_updating_pbi, :message => @exception.message) -%>
<%- message_class = "error" -%>
$("#popup-messages").html("<div class=\"flash <%= message_class %>\"><%= message %></div>");
<%- else -%>
$("#ajax-modal").dialog("close");
<%- if @pbi.sprint.is_product_backlog? -%>
$("#<%= "pbi_#{@pbi.id}" %>").replaceWith("<%=
escape_javascript(render(:partial => 'post_its/product_backlog/pbi',
:formats => [:html],
:locals => {:project => @project,
:pbi => @pbi,
:read_only => false})).html_safe
%>");
<%- else -%>
<%- sprint_board_id = 'sprint_board' -%>
<%- task_statuses = IssueStatus.task_statuses -%>
$("#<%= "pbi_#{@pbi.id}_row" %>").replaceWith("<%=
escape_javascript(render(:partial => 'post_its/sprint_board/pbi_row',
:formats => [:html],
:locals => {:project => @project,
:sprint_board_id => sprint_board_id,
:pbi => @pbi,
:task_statuses => task_statuses})).html_safe
%>");
<%- end -%>
<%- if (!(IssueStatus.pbi_statuses.include?(@pbi.status))) -%>
$("#<%= @pbi.sprint.is_product_backlog? ? "pbi_#{@pbi.id}" : "pbi_#{@pbi.id}_row" %>").remove();
<%- end -%>
<%- end -%>
<%= stylesheet_link_tag "scrum", :plugin => "scrum", :media => "all" %>
<%= javascript_include_tag "scrum", :plugin => "scrum" %>
\ No newline at end of file
<%- tips = scrum_tips -%>
<%- unless tips.empty? -%>
<h3><%= l(:label_tip_title) %> <%= image_tag("help.png") %></h3>
<ul>
<%- tips.each do |tip| -%>
<li style="list-style: disc inside; margin-bottom: 0.5em;">
<%= raw(tip) %>
</li>
<%- end -%>
</ul>
<%- end -%>
\ No newline at end of file
<script language="javascript">
function AddSprintToDay(sprintName, sprintUrl, day, week, start)
{
var cellElement = null;
var cellElementWeekNumber = null;
var dayOneFound = false;
var elements = document.getElementsByTagName("*");
for (var i = 0; (cellElement == null) && (i < elements.length); i++)
{
if (elements[i].className == "day-num")
{
var oneDay = parseInt(elements[i].childNodes[0].nodeValue);
if ((!dayOneFound) && (oneDay == 1))
{
dayOneFound = true;
}
if (dayOneFound && (oneDay == day))
{
cellElement = elements[i].parentNode;
if (cellElement != null)
{
var cellParentTr = cellElement.parentNode;
if (cellParentTr != null)
{
var cellParentTrFirstId = cellParentTr.childNodes[1];
if (cellParentTrFirstId != null)
{
cellElementWeekNumber = parseInt(cellParentTrFirstId.innerText);
}
if ((cellElementWeekNumber != null) && (cellElementWeekNumber == week))
{
cellElement = elements[i].parentNode;
}
else
{
cellElement = null;
}
}
}
}
}
}
if (cellElement != null)
{
var link = document.createElement("a");
link.innerHTML = sprintName;
link.href = sprintUrl;
var sprint = document.createElement("span");
sprint.className = "icon icon-sprint-" + (start ? "start" : "end");
sprint.appendChild(link);
cellElement.appendChild(sprint);
}
}
<%- if sprints and sprints.any? -%>
<%- sprints.each do |sprint| -%>
AddSprintToDay("<%= sprint[:name] %>", "<%= sprint[:url] %>", <%= sprint[:day] %>,
<%= sprint[:week] %>, <%= sprint[:start] ? 'true' : 'false' %>);
<%- end -%>
<%- end -%>
</script>
<%= render :partial => 'common/scrum_footer' %>
<%- if defined?(@project) and !(@project.nil?) and @project.scrum? %>
<%- issue_ids = @issues.collect{ |issue| issue.id } -%>
<li class="folder">
<a href="#" class="submenu"><%= l(:field_sprint) %></a>
<ul>
<li><%= context_menu_link l(:label_none),
bulk_update_issues_path(:ids => issue_ids, :issue => {'sprint_id' => 'none'},
:back_url => @back),
:method => :post, :selected => (@issue && @issue.sprint.nil?),
:disabled => !@can[:edit] %></li>
<%- @project.all_open_sprints_and_product_backlogs.sort.each do |sprint| -%>
<li><%= context_menu_link sprint.name,
bulk_update_issues_path(:ids => issue_ids, :issue => {:sprint_id => sprint},
:back_url => @back),
:method => :post, :selected => (@issue && sprint == @issue.sprint),
:disabled => !@can[:edit] %></li>
<%- end -%>
</ul>
</li>
<%- end -%>
<%- if defined?(@project) and @project.scrum? -%>
<% if @safe_attributes.include?("sprint_id") -%>
<p>
<label for="sprint_id"><%= l(:field_sprint) %></label>
<%- sprints = @project.all_open_sprints_and_product_backlogs
options = content_tag("option", l(:label_no_change_option), :value => "") +
options_from_collection_for_select(sprints, :id, :name) -%>
<%= select_tag "issue[sprint_id]", options %>
</p>
<%- end -%>
<%- if User.current.allowed_to?(:edit_pending_effort, @project) -%>
<p>
<label for="pending_effort"><%= l(:field_pending_effort) %></label>
<%= text_field_tag "issue[pending_effort]", "", :size => 3 %>
</p>
<%- end -%>
<%- end -%>
<%- if User.current.allowed_to?(:edit_pending_effort, @issue.project) -%>
<div id="edit_pending_effort" class="scrum-dialog"
title="<%= l(@issue.is_task? ? :field_pending_effort : :label_remaining_story_point_plural) %>">
<%= form_tag(change_pending_efforts_path(issue), :method => :post) do %>
<table align="center">
<tr>
<th><%= l(:field_effective_date) %></th>
<th><%= l(@issue.is_task? ? :field_hours : :label_remaining_story_point_plural) %></th>
</tr>
<%- issue.pending_efforts.each_with_index do |pending_effort, index| -%>
<tr>
<th>
<%= format_date(pending_effort.date) %>
</th>
<td>
<%= text_field_tag "pending_efforts[#{pending_effort.id}]",
pending_effort.effort,
:size => 2 %>
</td>
</tr>
<%- end -%>
<tr>
<td colspan="2" align="left">
<%= submit_tag l(:button_edit), :name => 'edit', :disable_with => l(:label_loading) %>
</td>
</tr>
</table>
<% end %>
</div>
<%- end -%>
<%- assignable_sprints = @issue.assignable_sprints -%>
<%- if assignable_sprints.any? -%>
<div class="splitcontent">
<div class="splitcontentleft">
<p>
<%- selected = @issue.sprint ? @issue.sprint.id.to_s : nil -%>
<%= form.select :sprint_id,
options_from_collection_for_select(assignable_sprints, 'id', 'name', selected),
:include_blank => true %>
</p>
</div>
<%- if (@issue.is_task? or @issue.is_pbi?) and @issue.scrum? and
User.current.allowed_to?(:edit_pending_effort, @issue.project)-%>
<div class="splitcontentright">
<p>
<%= form.text_field :pending_effort, :size => 3,
:label => @issue.is_pbi? ? l(:label_remaining_story_point_plural) : nil %>
</p>
</div>
<%- end -%>
</div>
<%- end -%>
<%- if User.current.allowed_to?(:view_pending_effort, @issue.project) -%>
<div id="pending_effort" class="scrum-dialog"
title="<%= l(@issue.is_task? ? :field_pending_effort : :label_remaining_story_point_plural) %>">
<table align="center">
<tr>
<th><%= l(:field_effective_date) %></th>
<th><%= l(@issue.is_task? ? :field_hours : :label_remaining_story_point_plural) %></th>
</tr>
<%- issue.pending_efforts.each do |pending_effort| -%>
<tr>
<td align="center"><%= format_date(pending_effort.date) %></td>
<td align="center"><%= pending_effort.effort %></td>
</tr>
<%- end -%>
</table>
</div>
<%- end -%>
<%- if @issue.scrum? -%>
<%= issue_fields_rows do |rows|
if @issue.sprint.nil?
content = ''
else
if @issue.sprint.is_product_backlog
object = project_product_backlog_index_path(@issue.sprint.project)
else
object = @issue.sprint
end
content = link_to(h(@issue.sprint.name), object)
end
rows.left l(:label_sprint), raw(content), :class => 'sprint'
if @issue.is_pbi?
content = ''
if @issue.has_remaining_story_points?
if User.current.allowed_to?(:view_pending_effort, @issue.project)
content = render_sps(@issue.remaining_story_points) +
raw('&nbsp;') +
link_to('', '#', :class => 'icon icon-zoom-in',
:onclick => '$("#pending_effort").dialog({modal: true});') +
render(:partial => 'scrum_hooks/issues/pending_effort', :locals => {:issue => @issue})
end
if User.current.allowed_to?(:edit_pending_effort, @issue.project)
content += link_to('', '#', :class => 'icon icon-edit',
:onclick => '$("#edit_pending_effort").dialog({modal: true});') +
render(:partial => 'scrum_hooks/issues/edit_pending_effort', :locals => {:issue => @issue})
end
end
if Scrum::Setting.use_remaining_story_points? and
User.current.allowed_to?(:view_pending_effort, @issue.project) or
User.current.allowed_to?(:edit_pending_effort, @issue.project)
rows.right l(:label_remaining_story_point_plural), raw(content), :class => 'pending_effort'
end
elsif @issue.is_task?
content = ''
if @issue.has_pending_effort?
if User.current.allowed_to?(:view_pending_effort, @issue.project)
content = raw(@issue.pending_effort ? l('datetime.distance_in_words.x_hours', :count => @issue.pending_effort) : '') +
raw('&nbsp;') +
link_to('', '#', :class => 'icon icon-zoom-in',
:onclick => '$("#pending_effort").dialog({modal: true});') +
render(:partial => 'scrum_hooks/issues/pending_effort', :locals => {:issue => @issue})
end
if User.current.allowed_to?(:edit_pending_effort, @issue.project)
content += link_to('', '#', :class => 'icon icon-edit',
:onclick => '$("#edit_pending_effort").dialog({modal: true});') +
render(:partial => 'scrum_hooks/issues/edit_pending_effort', :locals => {:issue => @issue})
end
end
if User.current.allowed_to?(:view_pending_effort, @issue.project) or
User.current.allowed_to?(:edit_pending_effort, @issue.project)
rows.right l(:field_pending_effort), raw(content), :class => 'pending_effort'
end
end
end %>
<%- end -%>
<%- if @project and @project.module_enabled?(:scrum) -%>
<%- allow_scrum_stats = User.current.allowed_to?(:view_scrum_stats, @project) -%>
<%- if allow_scrum_stats -%>
<h3><%= l(:label_scrum) %></h3>
<%= link_to(l(:label_scrum_stats), project_scrum_stats_path(@project),
:class => 'icon icon-stats') if allow_scrum_stats %>
<%- end -%>
<%- end -%>
\ No newline at end of file
<%= link_to(l(:label_plugin_license), "#", :id => "licenseLink", :class => "icon icon-copy") %>
<div id="licenseDialog" style="overflow: hidden;">
<iframe id="licenseIframe" src="https://creativecommons.org/licenses/by-nd/4.0/"
frameborder="0" onload="resizeIframe(this)"></iframe>
</div>
<%= render :partial => "license", :formats => [:js] %>
<%= javascript_tag do %>
$("#licenseDialog").dialog({
autoOpen: false,
modal: true,
width: 850,
height: 600,
title: "<%= l(:label_plugin_license_title) %>"
});
$('#licenseLink').click(function() {
$('#licenseDialog').dialog('open');
return(false);
});
function resizeIframe(obj) {
obj.style.height = (obj.contentWindow.document.body.scrollHeight - 20) + 'px';
}
<% end %>
\ No newline at end of file
<div class="contextual scrum-menu">
<%= render :partial => 'license' %>
<%= render_scrum_help %>
</div>
<br />
<br />
<fieldset>
<legend><%= l(:label_settings) %></legend>
<p>
<label><%= l(:label_setting_render_plugin_tips) %>:</label>
<%= hidden_field_tag('settings[render_plugin_tips]', 0) %>
<%= check_box_tag('settings[render_plugin_tips]', 1,
Scrum::Setting.render_plugin_tips, :autofocus => 'autofocus') %>
</p>
<p>
<label><%= l(:label_setting_story_points_custom_field) %>:</label>
<%- custom_fields = CustomField.where(:type => 'IssueCustomField', :field_format => ['int', 'float', 'string', 'list'])
custom_fields = custom_fields.collect{|c| [c.name, c.id]}
options = options_for_select(custom_fields, Scrum::Setting.story_points_custom_field_id) -%>
<%= select_tag 'settings[story_points_custom_field_id]', options, :include_blank => true %>
</p>
<p>
<label><%= l(:label_setting_blocked_custom_field) %>:</label>
<%- custom_fields = CustomField.where(:type => 'IssueCustomField', :field_format => ['bool'])
custom_fields = custom_fields.collect{|c| [c.name, c.id]}
options = options_for_select(custom_fields, Scrum::Setting.blocked_custom_field_id) -%>
<%= select_tag 'settings[blocked_custom_field_id]', options, :include_blank => true %>
</p>
<p>
<label><%= l(:label_setting_simple_pbi_custom_field) %>:</label>
<%- custom_fields = CustomField.where(:type => 'IssueCustomField', :field_format => ['bool'])
custom_fields = custom_fields.collect{|c| [c.name, c.id]}
options = options_for_select(custom_fields, Scrum::Setting.simple_pbi_custom_field_id) -%>
<%= select_tag 'settings[simple_pbi_custom_field_id]', options, :include_blank => true %>
</p>
<%- trackers = Tracker.all.collect{|t| [t.name, t.id]} -%>
<p>
<label><%= l(:label_pbi_plural) %>:</label>
<%- options = options_for_select(trackers, Scrum::Setting.pbi_tracker_ids) -%>
<%= select_tag 'settings[pbi_tracker_ids]', options, :multiple => true %>
</p>
<p>
<label><%= l(:label_task_plural) %>:</label>
<%- options = options_for_select(trackers, Scrum::Setting.task_tracker_ids) -%>
<%= select_tag 'settings[task_tracker_ids]', options, :multiple => true %>
</p>
<%- Tracker.all.each do |tracker| -%>
<%- included = (Scrum::Setting.pbi_tracker_ids.include?(tracker.id) or
Scrum::Setting.task_tracker_ids.include?(tracker.id)) -%>
<p id="fields_tracker_<%= tracker.id %>"
style="display: <%= included ? 'block' : 'none' %>;">
<label><%= l(:label_setting_fields_on_tracker, :tracker => tracker.name) %></label>
<%- fields = tracker.core_fields.collect{|f| [l("field_#{f}".sub(/_id$/, '')), f]}
options = options_for_select(fields, Scrum::Setting.tracker_fields(tracker.id)) -%>
<%= select_tag "settings[tracker_#{tracker.id}_fields]", options, :multiple => true %>
<%- custom_fields = tracker.custom_fields.collect{|f| [f.name, f.id]}
options = options_for_select(custom_fields, Scrum::Setting.tracker_fields(tracker.id, Scrum::Setting::TrackerFields::CUSTOM_FIELDS)) -%>
<%= select_tag "settings[tracker_#{tracker.id}_custom_fields]", options, :multiple => true %>
</p>
<%- end -%>
<p>
<label><%= l(:label_setting_auto_update_pbi_status) %>:</label>
<%= hidden_field_tag('settings[auto_update_pbi_status]', 0) %>
<%= check_box_tag('settings[auto_update_pbi_status]', 1,
Scrum::Setting.auto_update_pbi_status) %>
<em><%= l(:label_setting_auto_update_pbi_status_explanation) %></em>
</p>
<p>
<label><%= l(:label_setting_update_pbi_status_if_all_tasks_are_closed) %>:</label>
<%- statuses = IssueStatus.all.collect{|s| [s.name, s.id]} -%>
<%- options = options_for_select(statuses, Scrum::Setting.closed_pbi_status_id) -%>
<%= select_tag 'settings[closed_pbi_status_id]', options, :include_blank => true %>
</p>
<p>
<label><%= l(:label_setting_pbi_is_closed_if_tasks_are_closed) %>:</label>
<%= hidden_field_tag('settings[pbi_is_closed_if_tasks_are_closed]', 0) %>
<%= check_box_tag('settings[pbi_is_closed_if_tasks_are_closed]', 1,
Scrum::Setting.pbi_is_closed_if_tasks_are_closed) %>
<em><%= l(:label_setting_pbi_is_closed_if_tasks_are_closed_explanation) %></em>
</p>
<p>
<label><%= l(:label_setting_use_remaining_story_points) %>:</label>
<%= hidden_field_tag('settings[use_remaining_story_points]', 0) %>
<%= check_box_tag('settings[use_remaining_story_points]', 1,
Scrum::Setting.use_remaining_story_points) %>
<em><%= l(:label_setting_use_remaining_story_points_explanation) %></em>
</p>
<p>
<label><%= l(:label_setting_default_sprint_name) %>:</label>
<%= text_field_tag('settings[default_sprint_name]',
Scrum::Setting.default_sprint_name) %>
</p>
<p>
<label><%= l(:label_setting_default_sprint_days) %>:</label>
<%= number_field_tag('settings[default_sprint_days]',
Scrum::Setting.default_sprint_days,
:min => 1, :max => 20) %>
<em><%= l(:label_setting_default_sprint_days_explanation) %></em>
</p>
<p>
<label><%= l(:label_setting_default_sprint_shared) %>:</label>
<%= hidden_field_tag('settings[default_sprint_shared]', 0) %>
<%= check_box_tag('settings[default_sprint_shared]', 1,
Scrum::Setting.default_sprint_shared) %>
<em><%= l(:label_setting_default_sprint_shared_explanation) %></em>
</p>
</fieldset>
<fieldset>
<legend><%= l(:label_post_it) %></legend>
<p>
<label><%= l(:label_setting_random_postit_rotation) %>:</label>
<%= hidden_field_tag('settings[random_posit_rotation]', 0) %>
<%= check_box_tag('settings[random_posit_rotation]', 1,
Scrum::Setting.random_posit_rotation) %>
</p>
<%- colors = {1 => 'post-it-color-1', 2 => 'post-it-color-2', 3 => 'post-it-color-3',
9 => 'post-it-color-9', 4 => 'post-it-color-4', 10 => 'post-it-color-10',
5 => 'post-it-color-5', 6 => 'post-it-color-6', 7 => 'post-it-color-7',
8 => 'post-it-color-8'}
mini_postit_content = raw('&nbsp;&nbsp;&nbsp;')
Tracker.all.each do |tracker| -%>
<%- included = (Scrum::Setting.pbi_tracker_ids.include?(tracker.id) or
Scrum::Setting.task_tracker_ids.include?(tracker.id)) -%>
<p id="tracker_<%= tracker.id %>_color"
style="display: <%= included ? 'block' : 'none' %>;">
<label><%= tracker.name %>:</label>
<nobr>
<%- colors.each_pair do |color_id, color_css_class| -%>
<span style="white-space: nowrap;">
<%- setting_name = "tracker_#{tracker.id.to_s}_color"
selected = (@settings[setting_name].nil? and (color_id == 1)) ? true : (@settings[setting_name] == color_css_class) -%>
<%= radio_button_tag("settings[#{setting_name}]", color_css_class, selected) %>
<span class="post-it settings-post-it <%= color_css_class %>">
<%= mini_postit_content %>
</span>
</span>
<%- end -%>
</nobr>
</p>
<%- end -%>
<p>
<label><%= l(:label_doer) %>:</label>
<%- colors.each_pair do |color_id, color_css_class| -%>
<span style="white-space: nowrap;">
<%- selected = (color_css_class == Scrum::Setting.doer_color) -%>
<%= radio_button_tag('settings[doer_color]', color_css_class, selected) %>
<span class="post-it settings-post-it <%= color_css_class %>">
<%= mini_postit_content %>
</span>
</span>
<%- end -%>
</p>
<p>
<label><%= l(:label_reviewer) %>:</label>
<%- colors.each_pair do |color_id, color_css_class| -%>
<span style="white-space: nowrap;">
<%- selected = (color_css_class == Scrum::Setting.reviewer_color) -%>
<%= radio_button_tag('settings[reviewer_color]', color_css_class, selected) %>
<span class="post-it settings-post-it <%= color_css_class %>">
<%= mini_postit_content %>
</span>
</span>
<%- end -%>
</p>
<p>
<label><%= l(:label_blocked) %>:</label>
<%- colors.each_pair do |color_id, color_css_class| -%>
<span style="white-space: nowrap;">
<%- selected = (color_css_class == Scrum::Setting.blocked_color) -%>
<%= radio_button_tag('settings[blocked_color]', color_css_class, selected) %>
<span class="post-it settings-post-it <%= color_css_class %>">
<%= mini_postit_content %>
</span>
</span>
<%- end -%>
</p>
</fieldset>
<fieldset>
<legend><%= l(:label_sprint_board) %></legend>
<p>
<label><%= l(:label_setting_task_statuses) %>:</label>
<%- statuses = IssueStatus.all.collect{|s| [s.name, s.id]} -%>
<%- options = options_for_select(statuses, Scrum::Setting.task_status_ids) -%>
<%= select_tag 'settings[task_status_ids]', options, :multiple => true %>
</p>
<p>
<label><%= l(:label_setting_clear_new_tasks_assignee) %>:</label>
<%= hidden_field_tag('settings[clear_new_tasks_assignee]', 0) %>
<%= check_box_tag('settings[clear_new_tasks_assignee]', 1,
Scrum::Setting.clear_new_tasks_assignee) %>
</p>
<p>
<label><%= l(:label_setting_pbi_statuses) %>:</label>
<%- statuses = IssueStatus.all.collect{|s| [s.name, s.id]} -%>
<%- options = options_for_select(statuses, Scrum::Setting.pbi_status_ids) -%>
<%= select_tag 'settings[pbi_status_ids]', options, :multiple => true %>
</p>
<p>
<label><%= l(:label_setting_verification_activities) %>:</label>
<%- activities = Enumeration.where(:type => 'TimeEntryActivity').collect{|s| [s.name, s.id]} -%>
<%- options = options_for_select(activities, Scrum::Setting.verification_activity_ids) -%>
<%= select_tag 'settings[verification_activity_ids]', options, :multiple => true %>
</p>
<p>
<label><%= l(:label_setting_inherit_pbi_attributes) %>:</label>
<%= hidden_field_tag('settings[inherit_pbi_attributes]', 0) %>
<%= check_box_tag('settings[inherit_pbi_attributes]', 1,
Scrum::Setting.inherit_pbi_attributes) %>
</p>
<p>
<label>
<%= link_to('', '#', :class => "icon #{ScrumHelper::LOWEST_SPEED_ICON}") %>
<%= l(:label_setting_lowest_speed) %>:
</label>
<%= text_field_tag('settings[lowest_speed]',
Scrum::Setting.lowest_speed) %>
</p>
<p>
<label>
<%= link_to('', '#', :class => "icon #{ScrumHelper::LOW_SPEED_ICON}") %>
<%= l(:label_setting_low_speed) %>:
</label>
<%= text_field_tag('settings[low_speed]',
Scrum::Setting.low_speed) %>
</p>
<p>
<label>
<%= link_to('', '#', :class => "icon #{ScrumHelper::HIGH_SPEED_ICON}") %>
<%= l(:label_setting_high_speed) %>:
</label>
<%= text_field_tag('settings[high_speed]',
Scrum::Setting.high_speed) %>
</p>
<p>
<label><%= l(:label_setting_render_pbis_speed) %>:</label>
<%= hidden_field_tag('settings[render_pbis_speed]', 0) %>
<%= check_box_tag('settings[render_pbis_speed]', 1,
Scrum::Setting.render_pbis_speed) %>
</p>
<p>
<label><%= l(:label_setting_render_tasks_speed) %>:</label>
<%= hidden_field_tag('settings[render_tasks_speed]', 0) %>
<%= check_box_tag('settings[render_tasks_speed]', 1,
Scrum::Setting.render_tasks_speed) %>
</p>
<%- Tracker.all.each do |tracker| -%>
<%- included = (Scrum::Setting.pbi_tracker_ids.include?(tracker.id) or
Scrum::Setting.task_tracker_ids.include?(tracker.id)) -%>
<p id="sprint_board_fields_tracker_<%= tracker.id %>"
style="display: <%= included ? 'block' : 'none' %>;">
<label><%= l(:label_setting_sprint_board_fields_on_tracker, :tracker => tracker.name) %></label>
<%- fields = tracker.core_fields.select{|f| Scrum::Setting::sprint_board_fields.include?(f.to_sym)}.collect{|f| [l("field_#{f}".sub(/_id$/, '')), f]}
values = Scrum::Setting.tracker_fields(tracker.id, Scrum::Setting::TrackerFields::SPRINT_BOARD_FIELDS)
options = options_for_select(fields, values) -%>
<%= select_tag "settings[tracker_#{tracker.id}_sprint_board_fields]", options, :multiple => true %>
<%- custom_fields = tracker.custom_fields.collect{|f| [f.name, f.id]}
values = Scrum::Setting.tracker_fields(tracker.id, Scrum::Setting::TrackerFields::SPRINT_BOARD_CUSTOM_FIELDS)
options = options_for_select(custom_fields, values) -%>
<%= select_tag "settings[tracker_#{tracker.id}_sprint_board_custom_fields]", options, :multiple => true %>
</p>
<%- end -%>
<p>
<label><%= l(:label_setting_show_project_totals) %>:</label>
<%= hidden_field_tag('settings[show_project_totals_on_sprint]', 0) %>
<%= check_box_tag('settings[show_project_totals_on_sprint]', 1,
Scrum::Setting.show_project_totals_on_sprint) %>
</p>
</fieldset>
<fieldset>
<legend><%= l(:label_sprint_burndown_chart) %></legend>
<p>
<label><%= l(:label_setting_sprint_burndown_day_zero) %>:</label>
<%= hidden_field_tag('settings[sprint_burndown_day_zero]', 0) %>
<%= check_box_tag('settings[sprint_burndown_day_zero]', 1,
Scrum::Setting.sprint_burndown_day_zero) %>
<em><%= l(:label_setting_sprint_burndown_day_zero_explanation) %></em>
</p>
</fieldset>
<fieldset>
<legend><%= l(:label_product_backlog) %></legend>
<p>
<label><%= l(:label_setting_create_journal_on_pbi_position_change) %>:</label>
<%= hidden_field_tag('settings[create_journal_on_pbi_position_change]', 0) %>
<%= check_box_tag('settings[create_journal_on_pbi_position_change]', 1,
Scrum::Setting.create_journal_on_pbi_position_change) %>
</p>
<p>
<label><%= l(:label_setting_check_dependencies_on_pbi_sorting) %>:</label>
<%= hidden_field_tag('settings[check_dependencies_on_pbi_sorting]', 0) %>
<%= check_box_tag('settings[check_dependencies_on_pbi_sorting]', 1,
Scrum::Setting.check_dependencies_on_pbi_sorting) %>
</p>
<p>
<label><%= l(:label_setting_render_position_on_pbi) %>:</label>
<%= hidden_field_tag('settings[render_position_on_pbi]', 0) %>
<%= check_box_tag('settings[render_position_on_pbi]', 1,
Scrum::Setting.render_position_on_pbi) %>
</p>
<p>
<label><%= l(:label_setting_render_category_on_pbi) %>:</label>
<%= hidden_field_tag('settings[render_category_on_pbi]', 0) %>
<%= check_box_tag('settings[render_category_on_pbi]', 1,
Scrum::Setting.render_category_on_pbi) %>
</p>
<p>
<label><%= l(:label_setting_render_version_on_pbi) %>:</label>
<%= hidden_field_tag('settings[render_version_on_pbi]', 0) %>
<%= check_box_tag('settings[render_version_on_pbi]', 1,
Scrum::Setting.render_version_on_pbi) %>
</p>
<p>
<label><%= l(:label_setting_render_author_on_pbi) %>:</label>
<%= hidden_field_tag('settings[render_author_on_pbi]', 0) %>
<%= check_box_tag('settings[render_author_on_pbi]', 1,
Scrum::Setting.render_author_on_pbi) %>
</p>
<p>
<label><%= l(:label_setting_render_assigned_to_on_pbi) %>:</label>
<%= hidden_field_tag('settings[render_assigned_to_on_pbi]', 0) %>
<%= check_box_tag('settings[render_assigned_to_on_pbi]', 1,
Scrum::Setting.render_assigned_to_on_pbi) %>
</p>
<p>
<label><%= l(:label_setting_render_updated_on_pbi) %>:</label>
<%= hidden_field_tag('settings[render_updated_on_pbi]', 0) %>
<%= check_box_tag('settings[render_updated_on_pbi]', 1,
Scrum::Setting.render_updated_on_pbi) %>
</p>
<p>
<label><%= l(:label_setting_show_project_totals) %>:</label>
<%= hidden_field_tag('settings[show_project_totals_on_backlog]', 0) %>
<%= check_box_tag('settings[show_project_totals_on_backlog]', 1,
Scrum::Setting.show_project_totals_on_backlog) %>
</p>
</fieldset>
<fieldset>
<legend><%= l(:label_product_backlog_burndown_chart) %></legend>
<p>
<label><%= l(:label_setting_product_burndown_sprints) %>:</label>
<%= number_field_tag('settings[product_burndown_sprints]',
Scrum::Setting.product_burndown_sprints,
:min => 0) %>
<em><%= l(:label_setting_product_burndown_sprints_explanation) %></em>
</p>
<p>
<label><%= l(:label_setting_product_burndown_extra_sprints) %>:</label>
<%= number_field_tag('settings[product_burndown_extra_sprints]',
Scrum::Setting.product_burndown_extra_sprints,
:min => 0) %>
<em><%= l(:label_setting_product_burndown_extra_sprints_explanation) %></em>
</p>
</fieldset>
<%= render :partial => 'common/scrum_footer' %>
<%= render :partial => 'scrum_settings', :formats => [:js] %>
<%= javascript_tag do %>
$("[id^='settings_'][id$='_tracker_ids']").change(function() {
$("[id^='fields_tracker_']").each(function(index, object) {
var tracker_id = object.id.split("fields_tracker_")[1];
var visible = (($("#settings_pbi_tracker_ids option[value='" + tracker_id + "']").prop("selected")) ||
($("#settings_task_tracker_ids option[value='" + tracker_id + "']").prop("selected")));
$("#fields_tracker_" + tracker_id + ", " +
"#tracker_" + tracker_id + "_color, " +
"#sprint_board_fields_tracker_" + tracker_id).css("display", visible ? "block" : "none");
});
});
<% end %>
<%= javascript_tag do %>
// Create the tooltip DOM element.
var toolTip = document.createElement("div"),
leftOffset = -(~~$("html").css("padding-left").replace("px", "") + ~~$("body").css("margin-left").replace("px", "")),
topOffset = -32;
toolTip.className = "graphic-tooltip";
document.body.appendChild(toolTip);
// Chart data.
var data = {
// Horizontal set of values (ordinal), vertical values (linear).
"xScale": "ordinal",
"yScale": "linear",
"yMin": 0,
"main": [
<%- serieIndex = 0
series.each do |serie| -%>
{
// Pending effort serie.
"className": ".effort-<%= serieIndex %>",
"data": [
<%- serie[:data].each_with_index do |value, i| -%>
<%- unless value[:effort].nil? -%>
{
"x": <%= i %>,
"y": <%= value[:effort] %>,
"tooltip": "<%= value[:tooltip] %>"
},
<%- end -%>
<%- end -%>
]
},
<%- serieIndex += 1 -%>
<%- end -%>
]
};
// X axis labels.
var x_axis_labels = [
<%- x_axis_labels.each do |label| -%>
'<%= label %>',
<%- end -%>
];
// Chart options.
var options = {
// Use data label as horizontal axis labels.
"tickFormatX": function(x) { return x_axis_labels[x]; },
// Render a tooltip when mouse goes over the values.
"mouseover": function(d, i) {
var pos = $(this).offset();
$(toolTip).text(d.tooltip)
.css({top: topOffset + pos.top, left: pos.left + leftOffset})
.show();
},
// Hide the tooltip when mouse exists the values.
"mouseout": function(x) {
$(toolTip).hide();
}
};
// Generate the chart.
new xChart("line-dotted", data, "#burndown-chart", options);
<% end %>
<%= javascript_tag do %>
// Create the tooltip DOM element.
var toolTip = document.createElement("div"),
leftOffset = -(~~$("html").css("padding-left").replace("px", "") + ~~$("body").css("margin-left").replace("px", "")),
topOffset = -32;
toolTip.className = "graphic-tooltip";
document.body.appendChild(toolTip);
// Chart data.
var data = {
// Horizontal set of values (ordinal), vertical values (linear).
"xScale": "ordinal",
"yScale": "linear",
"yMin": 0,
"main": [
<%- serieIndex = 0
series.each do |serie| -%>
{
// Pending SPs serie.
"className": ".pending-sps-<%= serieIndex %>",
"data": [
<%- serie[:data].each_with_index do |value, day| -%>
<%- unless value[:pending_sps].nil? -%>
{
"x": <%= day %>,
"y": <%= value[:pending_sps] %>,
"tooltip": "<%= value[:pending_sps_tooltip] %>"
},
<%- end -%>
<%- end -%>
]
},
<%- serieIndex += 1 -%>
<%- end -%>
]
};
// X axis labels.
var x_axis_labels = [
<%- x_axis_labels.each do |label| -%>
'<%= label %>',
<%- end -%>
];
// Chart options.
var options = {
// Use data label as horizontal axis labels.
"tickFormatX": function(x) { return x_axis_labels[x]; },
// Render a tooltip when mouse goes over the values.
"mouseover": function(d, i) {
var pos = $(this).offset();
$(toolTip).text(d.tooltip)
.css({top: topOffset + pos.top, left: pos.left + leftOffset})
.show();
},
// Hide the tooltip when mouse exists the values.
"mouseout": function(x) {
$(toolTip).hide();
}
};
console.log('options: ' + options);
// Generate the chart.
new xChart("line-dotted", data, "#burndown-chart", options);
<% end %>
<%= back_url_hidden_field_tag %>
<%= error_messages_for "sprint" %>
<div class="box tabular">
<p><%= f.text_field :name, :size => 60, :required => true %></p>
<p><%= f.text_area :description, :rows => 8, :class => 'wiki-edit' %></p>
<%= wikitoolbar_for 'sprint_description' %>
<%- unless @sprint.is_product_backlog? -%>
<%- method = (Redmine::VERSION::STRING < '3.3.0') ? :text_field : :date_field -%>
<p>
<%= f.send method, :sprint_start_date, :size => 10, :required => true, :label => l(:field_start_date) %>
<%= calendar_for "sprint_sprint_start_date" %>
</p>
<p>
<%= f.send method, :sprint_end_date, :size => 10, :required => true, :label => l(:field_end_date) %>
<%= calendar_for "sprint_sprint_end_date" %>
</p>
<%- end -%>
<p><%= f.select :status,
Sprint::SPRINT_STATUSES.collect { |status|
[l("label_sprint_status_#{status}"), status]
} %></p>
<p>
<%= f.check_box :shared, :label => l(:field_shared) %>
<em class="info"><%= l(:field_shared_note) %></em>
</p>
</div>
<%= javascript_tag do %>
var <%= graph[:id] %>_data = [
<%- rows.each do |row| -%>
{
key: "<%= row[element[:name]].nil? ? "---" : row[element[:name]] %>",
y: <%= row[:total] %>
},
<%- end -%>
];
nv.addGraph(function() {
var chart = nv.models.pieChart()
.x(function(d) { return d.key })
.y(function(d) { return d.y })
.showLabels(true)
.labelType("percent")
.color(d3.scale.category10().range())
.width(<%= graph[:width] %>)
.height(<%= graph[:height] %>);
d3.select("#<%= graph[:id] %>")
.datum(<%= graph[:id] %>_data)
.transition().duration(1200)
.attr('width', <%= graph[:width] %>)
.attr('height', <%= graph[:height] %>)
.call(chart);
chart.dispatch.on('stateChange', function(e) { nv.log('New State:', JSON.stringify(e)); });
return chart;
});
<% end %>
<%= javascript_tag do %>
$(document).ready(function() {
var $assignees = $("#tasks-assignees");
var users = { };
$assignees.append("<option value='all'><%= l(:label_any) %></option>");
$("table.sprint-board a.user").each(function(row_index, user_link) {
users[$.trim(user_link.text)] = row_index;
});
<% # put "Me" in first in the list %>
var current_user_name = '<%= User.current.name %>';
if (current_user_name in users) {
var label_me = '<< <%= l(:label_me) %> >>';
$assignees.append("<option value='" + current_user_name + "'>" + label_me + "</option>");
}
$.each(users, function(user, row_index) {
$assignees.append("<option value='" + user + "'>" + user + "</option>");
});
$assignees.change(function(user){
var user = $assignees.val();
if (user === "all") {
$("table.sprint-board tbody.sprint-board tr.sprint-board").show();
} else {
$("table.sprint-board tbody.sprint-board tr.sprint-board").hide();
$("table.sprint-board a.user:contains('" + user + "')").parents("tr.sprint-board").show();
}
});
<%- if User.current.allowed_to?(:sort_sprint_board, sprint.project) -%>
$("#<%= sprint_board_id %>").sortable({
handle: ".sprint-board-pbi-handle",
placeholder: "sprint-row-space",
update: function() {
if ($.isFunction($.fn.setupAjaxIndicator)) {
setupAjaxIndicator();
}
$.ajax({
url: "<%= sort_sprint_path(sprint) %>",
type: "POST",
data: $("#<%= sprint_board_id %>").sortable("serialize"),
dataType: "script",
error: function() {
alert("<%= l(:error_changing_pbi_order) %>");
location.reload(true);
},
complete: function() {
if ($.isFunction($.fn.hideOnLoad)) {
hideOnLoad();
}
}
});
}
});
<%- end -%>
});
<% end %>
<%= render :partial => "common/scrum_sprint_menu" %>
<h2>
<%= l((@type == :sps) ? :label_sprint_burndown_chart_sps : :label_sprint_burndown_chart_hours) %>
</h2>
<% content_for :header_tags do %>
<%= javascript_include_tag "d3.min.js", :plugin => "scrum" %>
<%= javascript_include_tag "xcharts.min.js", :plugin => "scrum" %>
<%= stylesheet_link_tag "xcharts.min.css", :plugin => "scrum" %>
<% end %>
<div id="messages"></div>
<%= render :partial => "post_its/sprint_board/head",
:locals => {:project => @project,
:sprint => @sprint,
:path => method(:burndown_sprint_path)} %>
<%- if @warning -%>
<p class="warning"><%= @warning %></p>
<%- end -%>
<div style="position: relative;">
<svg height="500" width="<%= @series.count <= 2 ? 300 : 400 %>" class="xchart"
style="position: absolute; top: 0px; right: 0px;">
<%- i = 0 -%>
<%- row_space = 20 -%>
<%- @series.each do |serie| -%>
<g class="line color<%= i %>">
<path class="line" stroke-width="10" d="M5 <%= 15 + (i * row_space) %> H70" />
</g>
<g class="axis">
<text x="80" y="<%= 18 + (i * row_space) %>"><%= serie[:label] %></text>
</g>
<%- i += 1 -%>
<%- end -%>
</svg>
<figure id="burndown-chart"
style="left-margin: auto; right-margin:auto; width: 90%; height: 500px;" />
</div>
<%= render :partial => (@type == :sps) ? 'sprints/burndown_by_sps' : 'sprints/burndown',
:formats => [:js],
:locals => {:series => @series,
:x_axis_labels => @x_axis_labels} %>
<%= render :partial => "common/scrum_footer" %>
<%= render :partial => @product_backlog ? 'common/scrum_backlog_menu' : 'common/scrum_sprint_menu' %>
<h2><%= l(:label_sprint) %></h2>
<%= labelled_form_for @sprint do |f| %>
<%= back_url_hidden_field_tag %>
<%= render :partial => "sprints/form", :locals => {:f => f} %>
<%= submit_tag l(:button_save) %>
<% end %>
<%= render :partial => "common/scrum_footer" %>
<%= render :partial => 'common/scrum_sprint_menu' %>
<h2><%= l(:label_edit_effort) %></h2>
<div class="box tabular">
<p>
<label><%= l(:label_sprint) %>:</label>
<%= @sprint.name %>
</p>
<p>
<label><%= l(:field_start_date) %>:</label>
<%= format_date @sprint.sprint_start_date %>
</p>
<p>
<label><%= l(:field_end_date) %>:</label>
<%= format_date @sprint.sprint_end_date %>
</p>
<%- if !(@sprint.description.blank?) -%>
<hr/>
<%= raw(Redmine::WikiFormatting::Textile::Formatter.new(@sprint.description).to_html(true)) %>
<%- end -%>
</div>
<%- if @project.sprints_and_product_backlogs.empty? -%>
<p class="nodata"><%= l(:label_no_data) %></p>
<%- else -%>
<%= form_tag(update_effort_sprint_path(@sprint)) do |f| %>
<%= back_url_hidden_field_tag %>
<%- days = (@sprint.sprint_end_date - @sprint.sprint_start_date).to_i + 1 -%>
<table class="list" style="display: block; overflow: auto;">
<thead>
<tr>
<th style="text-align: left;"><%= l(:field_user) %></th>
<%- ((@sprint.sprint_start_date)..(@sprint.sprint_end_date)).each do |date| -%>
<th style="text-align: center;"><%= I18n.l(date, :format => :scrum_day) %><br /><%= date.day %></th>
<%- end -%>
</tr>
</thead>
<tbody>
<%- @project.members.each do |member| -%>
<tr class="<%= cycle 'odd', 'even' %>">
<td style="text-align: left"><%= link_to_user(member.user) %></td>
<%- days.times do |day| -%>
<td style="text-align: center;">
<%- effort = @sprint.efforts.find_by_user_id_and_date(member.user.id,
@sprint.sprint_start_date + day) -%>
<%= text_field_tag "user[#{member.user.id}][#{day}]",
effort.nil? ? nil : effort.effort,
:size => 3 %>
</td>
<%- end -%>
</tr>
<% end; reset_cycle %>
<tbody>
</table>
<%= submit_tag l(:button_save) %>
<% end %>
<%- end -%>
<%= render :partial => "common/scrum_footer" %>
<div class="contextual scrum-menu"><%= render_scrum_help %></div>
<h2><%= l(@sprint.is_product_backlog? ? :label_product_backlog_new : :label_sprint_new) %></h2>
<%= labelled_form_for @sprint, :url => project_sprints_path(@project) do |f| %>
<%= back_url_hidden_field_tag %>
<%= render :partial => "sprints/form", :locals => {:f => f} %>
<%= hidden_field_tag :create_product_backlog, true if params[:create_product_backlog] %>
<%= submit_tag l(:button_create) %>
<% end %>
<%= render :partial => "common/scrum_footer" %>
<%= render :partial => 'common/scrum_sprint_menu' %>
<h2>
<%= l(:label_sprint_board) %>
</h2>
<div id="messages"></div>
<%= render :partial => 'post_its/sprint_board/head',
:locals => {:project => @project, :sprint => @sprint, :path => method(:sprint_path)} %>
<%- sprint_board_id = 'sprint_board' -%>
<%= render :layout => 'common/fullscreen_content' do %>
<table class="sprint-board">
<thead class="sprint-board">
<tr class="sprint-board">
<th></th>
<th class="sprint-board"><%= l(:label_pbi_plural) %></th>
<%- task_statuses = IssueStatus.task_statuses
task_statuses.each do |status| -%>
<th class="sprint-board"><%= status.name %></th>
<%- end -%>
</tr>
</thead>
<tbody id="<%= sprint_board_id %>" class="sprint-board">
<%- @sprint.pbis.each do |pbi| -%>
<%= render :partial => 'post_its/sprint_board/pbi_row', :formats => [:html],
:locals => {:project => @project,
:sprint_board_id => sprint_board_id,
:pbi => pbi,
:task_statuses => task_statuses} %>
<%- end -%>
</tbody>
</table>
<%= render :partial => 'post_its/sprint_board/footer',
:locals => {:project => @project, :sprint => @sprint, :path => method(:sprint_path)} %>
<%- if User.current.allowed_to?(:add_issues, @project) and
User.current.allowed_to?(:edit_sprint_board, @project) and
@sprint.open? -%>
<div>
<%- Tracker.pbi_trackers(@project).each do |tracker| -%>
<span class="post-it settings-post-it <%= tracker.post_it_css_class %>">
<%= link_to tracker.name, new_pbi_path(@sprint, tracker), :remote => true,
:method => 'GET', :class => 'icon icon-add' %>
</span>
<%- end -%>
</div>
<%- end -%>
<% end %>
<%= render :partial => 'sprints/show', :formats => [:js],
:locals => {:sprint => @sprint,
:sprint_board_id => sprint_board_id} %>
<%= render :partial => 'common/scrum_footer' %>
<%- heads_for_wiki_formatter -%>
<%= render :partial => 'common/scrum_sprint_menu' %>
<h2>
<%= l(:label_sprint_stats) %>
</h2>
<%= render :partial => 'post_its/sprint_board/head',
:locals => {:project => @project, :sprint => @sprint, :path => method(:stats_sprint_path)} %>
<% content_for :header_tags do %>
<%= javascript_include_tag "d3.#{Rails.env.development? ? '' : 'min.'}js", :plugin => 'scrum' %>
<%= javascript_include_tag 'xcharts.min.js', :plugin => 'scrum' %>
<%= javascript_include_tag "nv.d3.#{Rails.env.development? ? '' : 'min.'}js", :plugin => 'scrum' %>
<%= stylesheet_link_tag 'xcharts.min.css', :plugin => 'scrum' %>
<%= stylesheet_link_tag "nv.d3.#{Rails.env.development? ? '' : 'min.'}css", :plugin => 'scrum' %>
<% end %>
<h3 align="center"><%= l(:label_estimated_vs_done_effort) %></h3>
<%- if @members_efforts.count < 1 -%>
<p class="nodata"><%= l(:label_no_data) %></p>
<%- else -%>
<table class="scrum-stats-table">
<thead>
<tr class="double-row">
<th align="left"><%= l(:label_member) %></th>
<th></th>
<%- @days.each do |day| -%>
<th align="center"><%= day[:label] %></th>
<%- end -%>
<th align="center"><%= l(:label_total) %></th>
</tr>
</thead>
<tbody>
<%- @members_efforts.each do |member| -%>
<tr class="double-row">
<td align="left" rowspan="2"><%= link_to_user(member[:member]) %></td>
<td align="right" class="comment"><%= l(:label_estimated_effort) %></td>
<%- @days.each do |day| -%>
<td align="center">
<%- day_info = member[:estimated_efforts][:days][day[:date]] -%>
<%- if day_info -%>
<%= "#{day_info.round(2)} h" %>
<%- end -%>
</td>
<%- end -%>
<td align="center" class="total">
<%= member[:estimated_efforts][:total].round(2) %> h
</td>
</tr>
<tr class="double-row">
<td align="right" class="comment"><%= l(:label_done_effort) %></td>
<%- @days.each do |day| -%>
<td align="center">
<%- day_info = member[:done_efforts][:days][day[:date]] -%>
<%- if day_info -%>
<%= "#{day_info.round(2)} h" %>
<%- end -%>
</td>
<%- end -%>
<td align="center" class="total">
<%= member[:done_efforts][:total].round(2) %> h
</td>
</tr>
<%- end -%>
<%- if @members_efforts.count > 1 -%>
<tr class="double-row">
<td align="left" rowspan="2" class="total"><%= l(:label_total) %></td>
<td align="right" class="comment"><%= l(:label_estimated_effort) %></td>
<%- @days.each do |day| -%>
<td align="center" class="total">
<%- day_info = @estimated_efforts_totals[:days][day[:date]] -%>
<%- if day_info -%>
<%= "#{day_info.round(2)} h" %>
<%- end -%>
</td>
<%- end -%>
<td align="center" class="total">
<%= @estimated_efforts_totals[:total].round(2) %> h
</td>
</tr>
<tr class="double-row">
<td align="right" class="comment"><%= l(:label_done_effort) %></td>
<%- @days.each do |day| -%>
<td align="center" class="total">
<%- day_info = @done_efforts_totals[:days][day[:date]] -%>
<%- if day_info -%>
<%= "#{day_info.round(2)} h" %>
<%- end -%>
</td>
<%- end -%>
<td align="center" class="total">
<%= @done_efforts_totals[:total].round(2) %> h
</td>
</tr>
<%- end -%>
</tbody>
</table>
<%- end -%>
<br />
<br />
<h3 align="center"><%= l(:label_sps_by_pbi_category) %></h3>
<%- if @sps_by_pbi_category.count < 1 -%>
<p class="nodata"><%= l(:label_no_data) %></p>
<%- else -%>
<%= render :partial => 'common/stat_graph_and_table',
:locals => {:graph => {:id => 'stats_sps_per_category', :width => 400, :height => 400},
:element => {:label => l(:label_issue_category_plural), :name => :category},
:unit => {:label => l(:label_story_point_unit), :plural_label => l(:label_story_point_plural)},
:rows => @sps_by_pbi_category,
:total => @sps_by_pbi_category_total} %>
<%- end -%>
<br />
<br />
<h3 align="center"><%= l(:label_sps_by_pbi_type) %></h3>
<%- if @sps_by_pbi_type.count < 1 -%>
<p class="nodata"><%= l(:label_no_data) %></p>
<%- else -%>
<%= render :partial => 'common/stat_graph_and_table',
:locals => {:graph => {:id => 'stats_sps_per_pbi_type', :width => 400, :height => 400},
:element => {:label => l(:label_tracker_plural), :name => :tracker},
:unit => {:label => l(:label_story_point_unit), :plural_label => l(:label_story_point_plural)},
:rows => @sps_by_pbi_type,
:total => @sps_by_pbi_type_total} %>
<%- end -%>
<br />
<br />
<h3 align="center"><%= l(:label_sps_by_pbi_creation_date) %></h3>
<%- if @sps_by_pbi_creation_date.count < 1 -%>
<p class="nodata"><%= l(:label_no_data) %></p>
<%- else -%>
<%= render :partial => 'common/stat_graph_and_table',
:locals => {:graph => {:id => 'stats_sps_by_creation_date', :width => 400, :height => 400},
:element => {:label => l(:label_tracker_plural), :name => :created_on},
:unit => {:label => l(:label_story_point_unit), :plural_label => l(:label_story_point_plural)},
:rows => @sps_by_pbi_creation_date,
:total => @sps_by_pbi_creation_date_total} %>
<%- end -%>
<br />
<br />
<h3 align="center"><%= l(:label_time_by_activity) %></h3>
<%- if @effort_by_activity.count < 1 -%>
<p class="nodata"><%= l(:label_no_data) %></p>
<%- else -%>
<%= render :partial => 'common/stat_graph_and_table',
:locals => {:graph => {:id => 'stats_time_per_activity', :width => 400, :height => 400},
:element => {:label => l(:enumeration_activities), :name => :activity},
:unit => {:label => 'h', :plural_label => l(:label_time_entry_plural)},
:rows => @effort_by_activity,
:total => @effort_by_activity_total} %>
<%- end -%>
<%- if User.current.allowed_to?(:view_sprint_stats_by_member, @project) -%>
<br />
<br />
<h3 align="center"><%= l(:label_time_by_member_and_activity) %></h3>
<div id="<%= @efforts_by_member_and_activity_chart[:id] %>">
<svg style="height: <%= @efforts_by_member_and_activity_chart[:height] %>px;">
</svg>
</div>
<%= render :partial => 'common/bar_chart',
:formats => [:js],
:locals => {:graph => @efforts_by_member_and_activity_chart,
:element => {:label => l(:enumeration_activities), :name => :activity},
:rows => @efforts_by_member_and_activity} %>
<%- end -%>
<%= render :partial => 'common/scrum_footer' %>
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
function touchHandler(event) {
var touch = event.changedTouches[0];
var simulatedEvent = document.createEvent("MouseEvent");
simulatedEvent.initMouseEvent({
touchstart: "mousedown",
touchmove: "mousemove",
touchend: "mouseup"
}[event.type], true, true, window, 1,
touch.screenX, touch.screenY,
touch.clientX, touch.clientY, false,
false, false, false, 0, null);
touch.target.dispatchEvent(simulatedEvent);
if (!event) {
event = window.event;
}
var sender = event.srcElement || event.target;
if (sender &&
sender.nodeName.toLowerCase() != "a" &&
sender.nodeName.toLowerCase() != "input") {
event.preventDefault();
}
}
function draggableOnTouchScreen(element_id) {
var element = document.getElementById(element_id);
if (element) {
element.addEventListener("touchstart", touchHandler, true);
element.addEventListener("touchmove", touchHandler, true);
element.addEventListener("touchend", touchHandler, true);
element.addEventListener("touchcancel", touchHandler, true);
}
}
/*!
xCharts v0.3.0 Copyright (c) 2012, tenXer, Inc. All Rights Reserved.
@license MIT license. http://github.com/tenXer/xcharts for details
*/
(function(){var xChart,_vis={},_scales={},_visutils={};(function(){var n=this,t=n._,r={},e=Array.prototype,u=Object.prototype,i=Function.prototype,a=e.push,o=e.slice,c=e.concat,l=u.toString,f=u.hasOwnProperty,s=e.forEach,p=e.map,v=e.reduce,h=e.reduceRight,g=e.filter,d=e.every,m=e.some,y=e.indexOf,b=e.lastIndexOf,x=Array.isArray,_=Object.keys,j=i.bind,w=function(n){return n instanceof w?n:this instanceof w?(this._wrapped=n,void 0):new w(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=w),exports._=w):n._=w,w.VERSION="1.4.3";var A=w.each=w.forEach=function(n,t,e){if(null!=n)if(s&&n.forEach===s)n.forEach(t,e);else if(n.length===+n.length){for(var u=0,i=n.length;i>u;u++)if(t.call(e,n[u],u,n)===r)return}else for(var a in n)if(w.has(n,a)&&t.call(e,n[a],a,n)===r)return};w.map=w.collect=function(n,t,r){var e=[];return null==n?e:p&&n.map===p?n.map(t,r):(A(n,function(n,u,i){e[e.length]=t.call(r,n,u,i)}),e)};var O="Reduce of empty array with no initial value";w.reduce=w.foldl=w.inject=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),v&&n.reduce===v)return e&&(t=w.bind(t,e)),u?n.reduce(t,r):n.reduce(t);if(A(n,function(n,i,a){u?r=t.call(e,r,n,i,a):(r=n,u=!0)}),!u)throw new TypeError(O);return r},w.reduceRight=w.foldr=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),h&&n.reduceRight===h)return e&&(t=w.bind(t,e)),u?n.reduceRight(t,r):n.reduceRight(t);var i=n.length;if(i!==+i){var a=w.keys(n);i=a.length}if(A(n,function(o,c,l){c=a?a[--i]:--i,u?r=t.call(e,r,n[c],c,l):(r=n[c],u=!0)}),!u)throw new TypeError(O);return r},w.find=w.detect=function(n,t,r){var e;return E(n,function(n,u,i){return t.call(r,n,u,i)?(e=n,!0):void 0}),e},w.filter=w.select=function(n,t,r){var e=[];return null==n?e:g&&n.filter===g?n.filter(t,r):(A(n,function(n,u,i){t.call(r,n,u,i)&&(e[e.length]=n)}),e)},w.reject=function(n,t,r){return w.filter(n,function(n,e,u){return!t.call(r,n,e,u)},r)},w.every=w.all=function(n,t,e){t||(t=w.identity);var u=!0;return null==n?u:d&&n.every===d?n.every(t,e):(A(n,function(n,i,a){return(u=u&&t.call(e,n,i,a))?void 0:r}),!!u)};var E=w.some=w.any=function(n,t,e){t||(t=w.identity);var u=!1;return null==n?u:m&&n.some===m?n.some(t,e):(A(n,function(n,i,a){return u||(u=t.call(e,n,i,a))?r:void 0}),!!u)};w.contains=w.include=function(n,t){return null==n?!1:y&&n.indexOf===y?-1!=n.indexOf(t):E(n,function(n){return n===t})},w.invoke=function(n,t){var r=o.call(arguments,2);return w.map(n,function(n){return(w.isFunction(t)?t:n[t]).apply(n,r)})},w.pluck=function(n,t){return w.map(n,function(n){return n[t]})},w.where=function(n,t){return w.isEmpty(t)?[]:w.filter(n,function(n){for(var r in t)if(t[r]!==n[r])return!1;return!0})},w.max=function(n,t,r){if(!t&&w.isArray(n)&&n[0]===+n[0]&&65535>n.length)return Math.max.apply(Math,n);if(!t&&w.isEmpty(n))return-1/0;var e={computed:-1/0,value:-1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;a>=e.computed&&(e={value:n,computed:a})}),e.value},w.min=function(n,t,r){if(!t&&w.isArray(n)&&n[0]===+n[0]&&65535>n.length)return Math.min.apply(Math,n);if(!t&&w.isEmpty(n))return 1/0;var e={computed:1/0,value:1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;e.computed>a&&(e={value:n,computed:a})}),e.value},w.shuffle=function(n){var t,r=0,e=[];return A(n,function(n){t=w.random(r++),e[r-1]=e[t],e[t]=n}),e};var F=function(n){return w.isFunction(n)?n:function(t){return t[n]}};w.sortBy=function(n,t,r){var e=F(t);return w.pluck(w.map(n,function(n,t,u){return{value:n,index:t,criteria:e.call(r,n,t,u)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||void 0===r)return 1;if(e>r||void 0===e)return-1}return n.index<t.index?-1:1}),"value")};var k=function(n,t,r,e){var u={},i=F(t||w.identity);return A(n,function(t,a){var o=i.call(r,t,a,n);e(u,o,t)}),u};w.groupBy=function(n,t,r){return k(n,t,r,function(n,t,r){(w.has(n,t)?n[t]:n[t]=[]).push(r)})},w.countBy=function(n,t,r){return k(n,t,r,function(n,t){w.has(n,t)||(n[t]=0),n[t]++})},w.sortedIndex=function(n,t,r,e){r=null==r?w.identity:F(r);for(var u=r.call(e,t),i=0,a=n.length;a>i;){var o=i+a>>>1;u>r.call(e,n[o])?i=o+1:a=o}return i},w.toArray=function(n){return n?w.isArray(n)?o.call(n):n.length===+n.length?w.map(n,w.identity):w.values(n):[]},w.size=function(n){return null==n?0:n.length===+n.length?n.length:w.keys(n).length},w.first=w.head=w.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:o.call(n,0,t)},w.initial=function(n,t,r){return o.call(n,0,n.length-(null==t||r?1:t))},w.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:o.call(n,Math.max(n.length-t,0))},w.rest=w.tail=w.drop=function(n,t,r){return o.call(n,null==t||r?1:t)},w.compact=function(n){return w.filter(n,w.identity)};var R=function(n,t,r){return A(n,function(n){w.isArray(n)?t?a.apply(r,n):R(n,t,r):r.push(n)}),r};w.flatten=function(n,t){return R(n,t,[])},w.without=function(n){return w.difference(n,o.call(arguments,1))},w.uniq=w.unique=function(n,t,r,e){w.isFunction(t)&&(e=r,r=t,t=!1);var u=r?w.map(n,r,e):n,i=[],a=[];return A(u,function(r,e){(t?e&&a[a.length-1]===r:w.contains(a,r))||(a.push(r),i.push(n[e]))}),i},w.union=function(){return w.uniq(c.apply(e,arguments))},w.intersection=function(n){var t=o.call(arguments,1);return w.filter(w.uniq(n),function(n){return w.every(t,function(t){return w.indexOf(t,n)>=0})})},w.difference=function(n){var t=c.apply(e,o.call(arguments,1));return w.filter(n,function(n){return!w.contains(t,n)})},w.zip=function(){for(var n=o.call(arguments),t=w.max(w.pluck(n,"length")),r=Array(t),e=0;t>e;e++)r[e]=w.pluck(n,""+e);return r},w.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},w.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=w.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}if(y&&n.indexOf===y)return n.indexOf(t,r);for(;u>e;e++)if(n[e]===t)return e;return-1},w.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=null!=r;if(b&&n.lastIndexOf===b)return e?n.lastIndexOf(t,r):n.lastIndexOf(t);for(var u=e?r:n.length;u--;)if(n[u]===t)return u;return-1},w.range=function(n,t,r){1>=arguments.length&&(t=n||0,n=0),r=arguments[2]||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=0,i=Array(e);e>u;)i[u++]=n,n+=r;return i};var I=function(){};w.bind=function(n,t){var r,e;if(n.bind===j&&j)return j.apply(n,o.call(arguments,1));if(!w.isFunction(n))throw new TypeError;return r=o.call(arguments,2),e=function(){if(!(this instanceof e))return n.apply(t,r.concat(o.call(arguments)));I.prototype=n.prototype;var u=new I;I.prototype=null;var i=n.apply(u,r.concat(o.call(arguments)));return Object(i)===i?i:u}},w.bindAll=function(n){var t=o.call(arguments,1);return 0==t.length&&(t=w.functions(n)),A(t,function(t){n[t]=w.bind(n[t],n)}),n},w.memoize=function(n,t){var r={};return t||(t=w.identity),function(){var e=t.apply(this,arguments);return w.has(r,e)?r[e]:r[e]=n.apply(this,arguments)}},w.delay=function(n,t){var r=o.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},w.defer=function(n){return w.delay.apply(w,[n,1].concat(o.call(arguments,1)))},w.throttle=function(n,t){var r,e,u,i,a=0,o=function(){a=new Date,u=null,i=n.apply(r,e)};return function(){var c=new Date,l=t-(c-a);return r=this,e=arguments,0>=l?(clearTimeout(u),u=null,a=c,i=n.apply(r,e)):u||(u=setTimeout(o,l)),i}},w.debounce=function(n,t,r){var e,u;return function(){var i=this,a=arguments,o=function(){e=null,r||(u=n.apply(i,a))},c=r&&!e;return clearTimeout(e),e=setTimeout(o,t),c&&(u=n.apply(i,a)),u}},w.once=function(n){var t,r=!1;return function(){return r?t:(r=!0,t=n.apply(this,arguments),n=null,t)}},w.wrap=function(n,t){return function(){var r=[n];return a.apply(r,arguments),t.apply(this,r)}},w.compose=function(){var n=arguments;return function(){for(var t=arguments,r=n.length-1;r>=0;r--)t=[n[r].apply(this,t)];return t[0]}},w.after=function(n,t){return 0>=n?t():function(){return 1>--n?t.apply(this,arguments):void 0}},w.keys=_||function(n){if(n!==Object(n))throw new TypeError("Invalid object");var t=[];for(var r in n)w.has(n,r)&&(t[t.length]=r);return t},w.values=function(n){var t=[];for(var r in n)w.has(n,r)&&t.push(n[r]);return t},w.pairs=function(n){var t=[];for(var r in n)w.has(n,r)&&t.push([r,n[r]]);return t},w.invert=function(n){var t={};for(var r in n)w.has(n,r)&&(t[n[r]]=r);return t},w.functions=w.methods=function(n){var t=[];for(var r in n)w.isFunction(n[r])&&t.push(r);return t.sort()},w.extend=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]=t[r]}),n},w.pick=function(n){var t={},r=c.apply(e,o.call(arguments,1));return A(r,function(r){r in n&&(t[r]=n[r])}),t},w.omit=function(n){var t={},r=c.apply(e,o.call(arguments,1));for(var u in n)w.contains(r,u)||(t[u]=n[u]);return t},w.defaults=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)null==n[r]&&(n[r]=t[r])}),n},w.clone=function(n){return w.isObject(n)?w.isArray(n)?n.slice():w.extend({},n):n},w.tap=function(n,t){return t(n),n};var S=function(n,t,r,e){if(n===t)return 0!==n||1/n==1/t;if(null==n||null==t)return n===t;n instanceof w&&(n=n._wrapped),t instanceof w&&(t=t._wrapped);var u=l.call(n);if(u!=l.call(t))return!1;switch(u){case"[object String]":return n==t+"";case"[object Number]":return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case"[object Date]":case"[object Boolean]":return+n==+t;case"[object RegExp]":return n.source==t.source&&n.global==t.global&&n.multiline==t.multiline&&n.ignoreCase==t.ignoreCase}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]==n)return e[i]==t;r.push(n),e.push(t);var a=0,o=!0;if("[object Array]"==u){if(a=n.length,o=a==t.length)for(;a--&&(o=S(n[a],t[a],r,e)););}else{var c=n.constructor,f=t.constructor;if(c!==f&&!(w.isFunction(c)&&c instanceof c&&w.isFunction(f)&&f instanceof f))return!1;for(var s in n)if(w.has(n,s)&&(a++,!(o=w.has(t,s)&&S(n[s],t[s],r,e))))break;if(o){for(s in t)if(w.has(t,s)&&!a--)break;o=!a}}return r.pop(),e.pop(),o};w.isEqual=function(n,t){return S(n,t,[],[])},w.isEmpty=function(n){if(null==n)return!0;if(w.isArray(n)||w.isString(n))return 0===n.length;for(var t in n)if(w.has(n,t))return!1;return!0},w.isElement=function(n){return!(!n||1!==n.nodeType)},w.isArray=x||function(n){return"[object Array]"==l.call(n)},w.isObject=function(n){return n===Object(n)},A(["Arguments","Function","String","Number","Date","RegExp"],function(n){w["is"+n]=function(t){return l.call(t)=="[object "+n+"]"}}),w.isArguments(arguments)||(w.isArguments=function(n){return!(!n||!w.has(n,"callee"))}),w.isFunction=function(n){return"function"==typeof n},w.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},w.isNaN=function(n){return w.isNumber(n)&&n!=+n},w.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"==l.call(n)},w.isNull=function(n){return null===n},w.isUndefined=function(n){return void 0===n},w.has=function(n,t){return f.call(n,t)},w.noConflict=function(){return n._=t,this},w.identity=function(n){return n},w.times=function(n,t,r){for(var e=Array(n),u=0;n>u;u++)e[u]=t.call(r,u);return e},w.random=function(n,t){return null==t&&(t=n,n=0),n+(0|Math.random()*(t-n+1))};var T={escape:{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;","/":"&#x2F;"}};T.unescape=w.invert(T.escape);var M={escape:RegExp("["+w.keys(T.escape).join("")+"]","g"),unescape:RegExp("("+w.keys(T.unescape).join("|")+")","g")};w.each(["escape","unescape"],function(n){w[n]=function(t){return null==t?"":(""+t).replace(M[n],function(t){return T[n][t]})}}),w.result=function(n,t){if(null==n)return null;var r=n[t];return w.isFunction(r)?r.call(n):r},w.mixin=function(n){A(w.functions(n),function(t){var r=w[t]=n[t];w.prototype[t]=function(){var n=[this._wrapped];return a.apply(n,arguments),z.call(this,r.apply(w,n))}})};var N=0;w.uniqueId=function(n){var t=""+ ++N;return n?n+t:t},w.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var q=/(.)^/,B={"'":"'","\\":"\\","\r":"r","\n":"n"," ":"t","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\t|\u2028|\u2029/g;w.template=function(n,t,r){r=w.defaults({},r,w.templateSettings);var e=RegExp([(r.escape||q).source,(r.interpolate||q).source,(r.evaluate||q).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,a,o){return i+=n.slice(u,o).replace(D,function(n){return"\\"+B[n]}),r&&(i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'"),e&&(i+="'+\n((__t=("+e+"))==null?'':__t)+\n'"),a&&(i+="';\n"+a+"\n__p+='"),u=o+t.length,t}),i+="';\n",r.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var a=Function(r.variable||"obj","_",i)}catch(o){throw o.source=i,o}if(t)return a(t,w);var c=function(n){return a.call(this,n,w)};return c.source="function("+(r.variable||"obj")+"){\n"+i+"}",c},w.chain=function(n){return w(n).chain()};var z=function(n){return this._chain?w(n).chain():n};w.mixin(w),A(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=e[n];w.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!=n&&"splice"!=n||0!==r.length||delete r[0],z.call(this,r)}}),A(["concat","join","slice"],function(n){var t=e[n];w.prototype[n]=function(){return z.call(this,t.apply(this._wrapped,arguments))}}),w.extend(w.prototype,{chain:function(){return this._chain=!0,this},value:function(){return this._wrapped}})}).call(this);function getInsertionPoint(zIndex){return _.chain(_.range(zIndex,10)).reverse().map(function(z){return'g[data-index="'+z+'"]'}).value().join(", ")}function colorClass(el,i){var c=el.getAttribute("class");return(c!==null?c.replace(/color\d+/g,""):"")+" color"+i}_visutils={getInsertionPoint:getInsertionPoint,colorClass:colorClass};var local=this,defaultSpacing=.25;function _getDomain(data,axis){return _.chain(data).pluck("data").flatten().pluck(axis).uniq().filter(function(d){return d!==undefined&&d!==null}).value().sort(d3.ascending)}_scales.ordinal=function(data,axis,bounds,extents){var domain=_getDomain(data,axis);return d3.scale.ordinal().domain(domain).rangeRoundBands(bounds,defaultSpacing)};_scales.linear=function(data,axis,bounds,extents){return d3.scale.linear().domain(extents).nice().rangeRound(bounds)};_scales.exponential=function(data,axis,bounds,extents){return d3.scale.pow().exponent(.65).domain(extents).nice().rangeRound(bounds)};_scales.time=function(data,axis,bounds,extents){return d3.time.scale().domain(_.map(extents,function(d){return new Date(d)})).range(bounds)};function _extendDomain(domain,axis){var min=domain[0],max=domain[1],diff,e;if(min===max){e=Math.max(Math.round(min/10),4);min-=e;max+=e}diff=max-min;min=min?min-diff/10:min;min=domain[0]>0?Math.max(min,0):min;max=max?max+diff/10:max;max=domain[1]<0?Math.min(max,0):max;return[min,max]}function _getExtents(options,data,xType,yType){var extents,nData=_.chain(data).pluck("data").flatten().value();extents={x:d3.extent(nData,function(d){return d.x}),y:d3.extent(nData,function(d){return d.y})};_.each([xType,yType],function(type,i){var axis=i?"y":"x",extended;extents[axis]=d3.extent(nData,function(d){return d[axis]});if(type==="ordinal"){return}_.each([axis+"Min",axis+"Max"],function(minMax,i){if(type!=="time"){extended=_extendDomain(extents[axis])}if(options.hasOwnProperty(minMax)&&options[minMax]!==null){extents[axis][i]=options[minMax]}else if(type!=="time"){extents[axis][i]=extended[i]}})});return extents}_scales.xy=function(self,data,xType,yType){var o=self._options,extents=_getExtents(o,data,xType,yType),scales={},horiz=[o.axisPaddingLeft,self._width],vert=[self._height,o.axisPaddingTop],xScale,yScale;_.each([xType,yType],function(type,i){var axis=i===0?"x":"y",bounds=i===0?horiz:vert,fn=xChart.getScale(type);scales[axis]=fn(data,axis,bounds,extents[axis])});return scales};(function(){var zIndex=2,selector="g.bar",insertBefore=_visutils.getInsertionPoint(zIndex);function postUpdateScale(self,scaleData,mainData,compData){self.xScale2=d3.scale.ordinal().domain(d3.range(0,mainData.length)).rangeRoundBands([0,self.xScale.rangeBand()],.08)}function enter(self,storage,className,data,callbacks){var barGroups,bars,yZero=self.yZero;barGroups=self._g.selectAll(selector+className).data(data,function(d){return d.className});barGroups.enter().insert("g",insertBefore).attr("data-index",zIndex).style("opacity",0).attr("class",function(d,i){var cl=_.uniq((className+d.className).split(".")).join(" ");return cl+" bar "+_visutils.colorClass(this,i)}).attr("transform",function(d,i){return"translate("+self.xScale2(i)+",0)"});bars=barGroups.selectAll("rect").data(function(d){return d.data},function(d){return d.x});bars.enter().append("rect").attr("width",0).attr("rx",3).attr("ry",3).attr("x",function(d){return self.xScale(d.x)+self.xScale2.rangeBand()/2}).attr("height",function(d){return Math.abs(yZero-self.yScale(d.y))}).attr("y",function(d){return d.y<0?yZero:self.yScale(d.y)}).on("mouseover",callbacks.mouseover).on("mouseout",callbacks.mouseout).on("click",callbacks.click);storage.barGroups=barGroups;storage.bars=bars}function update(self,storage,timing){var yZero=self.yZero;storage.barGroups.attr("class",function(d,i){return _visutils.colorClass(this,i)}).transition().duration(timing).style("opacity",1).attr("transform",function(d,i){return"translate("+self.xScale2(i)+",0)"});storage.bars.transition().duration(timing).attr("width",self.xScale2.rangeBand()).attr("x",function(d){return self.xScale(d.x)}).attr("height",function(d){return Math.abs(yZero-self.yScale(d.y))}).attr("y",function(d){return d.y<0?yZero:self.yScale(d.y)})}function exit(self,storage,timing){storage.bars.exit().transition().duration(timing).attr("width",0).remove();storage.barGroups.exit().transition().duration(timing).style("opacity",0).remove()}function destroy(self,storage,timing){var band=self.xScale2?self.xScale2.rangeBand()/2:0;delete self.xScale2;storage.bars.transition().duration(timing).attr("width",0).attr("x",function(d){return self.xScale(d.x)+band})}_vis.bar={postUpdateScale:postUpdateScale,enter:enter,update:update,exit:exit,destroy:destroy}})();(function(){var zIndex=3,selector="g.line",insertBefore=_visutils.getInsertionPoint(zIndex);function enter(self,storage,className,data,callbacks){var inter=self._options.interpolation,x=function(d,i){if(!self.xScale2&&!self.xScale.rangeBand){return self.xScale(d.x)}return self.xScale(d.x)+self.xScale.rangeBand()/2},y=function(d){return self.yScale(d.y)},line=d3.svg.line().x(x).interpolate(inter),area=d3.svg.area().x(x).y1(self.yZero).interpolate(inter),container,fills,paths;function datum(d){return[d.data]}container=self._g.selectAll(selector+className).data(data,function(d){return d.className});container.enter().insert("g",insertBefore).attr("data-index",zIndex).attr("class",function(d,i){var cl=_.uniq((className+d.className).split(".")).join(" ");return cl+" line "+_visutils.colorClass(this,i)});fills=container.selectAll("path.fill").data(datum);fills.enter().append("path").attr("class","fill").style("opacity",0).attr("d",area.y0(y));paths=container.selectAll("path.line").data(datum);paths.enter().append("path").attr("class","line").style("opacity",0).attr("d",line.y(y));storage.lineContainers=container;storage.lineFills=fills;storage.linePaths=paths;storage.lineX=x;storage.lineY=y;storage.lineA=area;storage.line=line}function update(self,storage,timing){storage.lineContainers.attr("class",function(d,i){return _visutils.colorClass(this,i)});storage.lineFills.transition().duration(timing).style("opacity",1).attr("d",storage.lineA.y0(storage.lineY));storage.linePaths.transition().duration(timing).style("opacity",1).attr("d",storage.line.y(storage.lineY))}function exit(self,storage){storage.linePaths.exit().style("opacity",0).remove();storage.lineFills.exit().style("opacity",0).remove();storage.lineContainers.exit().remove()}function destroy(self,storage,timing){storage.linePaths.transition().duration(timing).style("opacity",0);storage.lineFills.transition().duration(timing).style("opacity",0)}_vis.line={enter:enter,update:update,exit:exit,destroy:destroy}})();(function(){var line=_vis.line;function enter(self,storage,className,data,callbacks){var circles;line.enter(self,storage,className,data,callbacks);circles=storage.lineContainers.selectAll("circle").data(function(d){return d.data},function(d){return d.x});circles.enter().append("circle").style("opacity",0).attr("cx",storage.lineX).attr("cy",storage.lineY).attr("r",5).on("mouseover",callbacks.mouseover).on("mouseout",callbacks.mouseout).on("click",callbacks.click);storage.lineCircles=circles}function update(self,storage,timing){line.update.apply(null,_.toArray(arguments));storage.lineCircles.transition().duration(timing).style("opacity",1).attr("cx",storage.lineX).attr("cy",storage.lineY)}function exit(self,storage){storage.lineCircles.exit().remove();line.exit.apply(null,_.toArray(arguments))}function destroy(self,storage,timing){line.destroy.apply(null,_.toArray(arguments));if(!storage.lineCircles){return}storage.lineCircles.transition().duration(timing).style("opacity",0)}_vis["line-dotted"]={enter:enter,update:update,exit:exit,destroy:destroy}})();(function(){var line=_vis["line-dotted"];function enter(self,storage,className,data,callbacks){line.enter(self,storage,className,data,callbacks)}function _accumulate_data(data){function reduce(memo,num){return memo+num.y}var nData=_.map(data,function(set){var i=set.data.length,d=_.clone(set.data);set=_.clone(set);while(i){i-=1;d[i]=_.clone(set.data[i]);d[i].y0=set.data[i].y;d[i].y=_.reduce(_.first(set.data,i),reduce,set.data[i].y)}return _.extend(set,{data:d})});return nData}function _resetData(self){if(!self.hasOwnProperty("cumulativeOMainData")){return}self._mainData=self.cumulativeOMainData;delete self.cumulativeOMainData;self._compData=self.cumulativeOCompData;delete self.cumulativeOCompData}function preUpdateScale(self,data){_resetData(self);self.cumulativeOMainData=self._mainData;self._mainData=_accumulate_data(self._mainData);self.cumulativeOCompData=self._compData;self._compData=_accumulate_data(self._compData)}function destroy(self,storage,timing){_resetData(self);line.destroy.apply(null,_.toArray(arguments))}_vis.cumulative={preUpdateScale:preUpdateScale,enter:enter,update:line.update,exit:line.exit,destroy:destroy}})();var emptyData=[[]],defaults={mouseover:function(data,i){},mouseout:function(data,i){},click:function(data,i){},axisPaddingTop:0,axisPaddingRight:0,axisPaddingBottom:5,axisPaddingLeft:20,paddingTop:0,paddingRight:0,paddingBottom:20,paddingLeft:60,tickHintX:10,tickFormatX:function(x){return x},tickHintY:10,tickFormatY:function(y){return y},xMin:null,xMax:null,yMin:null,yMax:null,dataFormatX:function(x){return x},dataFormatY:function(y){return y},unsupported:function(selector){d3.select(selector).text("SVG is not supported on your browser")},empty:function(self,selector,d){},notempty:function(self,selector){},timing:750,interpolation:"monotone",sortX:function(a,b){return!a.x&&!b.x?0:a.x<b.x?-1:1}};function svgEnabled(){var d=document;return!!d.createElementNS&&!!d.createElementNS("http://www.w3.org/2000/svg","svg").createSVGRect}function xChart(type,data,selector,options){var self=this,resizeLock;self._options=options=_.defaults(options||{},defaults);if(svgEnabled()===false){return options.unsupported(selector)}self._selector=selector;self._container=d3.select(selector);self._drawSvg();self._mainStorage={};self._compStorage={};data=_.clone(data);if(type&&!data.type){data.type=type}self.setData(data);d3.select(window).on("resize.for."+selector,function(){if(resizeLock){clearTimeout(resizeLock)}resizeLock=setTimeout(function(){resizeLock=null;self._resize()},500)})}xChart.setVis=function(type,vis){if(_vis.hasOwnProperty(type)){throw'Cannot override vis type "'+type+'".'}_vis[type]=vis};xChart.getVis=function(type){if(!_vis.hasOwnProperty(type)){throw'Vis type "'+type+'" does not exist.'}return _.clone(_vis[type])};xChart.setScale=function(name,fn){if(_scales.hasOwnProperty(name)){throw'Scale type "'+name+'" already exists.'}_scales[name]=fn};xChart.getScale=function(name){if(!_scales.hasOwnProperty(name)){throw'Scale type "'+name+'" does not exist.'}return _scales[name]};xChart.visutils=_visutils;_.defaults(xChart.prototype,{setType:function(type,skipDraw){var self=this;if(self._type&&type===self._type){return}if(!_vis.hasOwnProperty(type)){throw'Vis type "'+type+'" is not defined.'}if(self._type){self._destroy(self._vis,self._mainStorage)}self._type=type;self._vis=_vis[type];if(!skipDraw){self._draw()}},setData:function(data){var self=this,o=self._options,nData=_.clone(data);if(!data.hasOwnProperty("main")){throw'No "main" key found in given chart data.'}switch(data.type){case"bar":data.xScale="ordinal";break;case undefined:data.type=self._type;break}o.xMin=isNaN(parseInt(data.xMin,10))?o.xMin:data.xMin;o.xMax=isNaN(parseInt(data.xMax,10))?o.xMax:data.xMax;o.yMin=isNaN(parseInt(data.yMin,10))?o.yMin:data.yMin;o.yMax=isNaN(parseInt(data.yMax,10))?o.yMax:data.yMax;if(self._vis){self._destroy(self._vis,self._mainStorage)}self.setType(data.type,true);function _mapData(set){var d=_.map(_.clone(set.data),function(p){var np=_.clone(p);if(p.hasOwnProperty("x")){np.x=o.dataFormatX(p.x)}if(p.hasOwnProperty("y")){np.y=o.dataFormatY(p.y)}return np}).sort(o.sortX);return _.extend(_.clone(set),{data:d})}nData.main=_.map(nData.main,_mapData);self._mainData=nData.main;self._xScaleType=nData.xScale;self._yScaleType=nData.yScale;if(nData.hasOwnProperty("comp")){nData.comp=_.map(nData.comp,_mapData);self._compData=nData.comp}else{self._compData=[]}self._draw()},setScale:function(axis,type){var self=this;switch(axis){case"x":self._xScaleType=type;break;case"y":self._yScaleType=type;break;default:throw'Cannot change scale of unknown axis "'+axis+'".'}self._draw()},_drawSvg:function(){var self=this,c=self._container,options=self._options,width=parseInt(c.style("width").replace("px",""),10),height=parseInt(c.style("height").replace("px",""),10),svg,g,gScale;svg=c.selectAll("svg").data(emptyData);svg.enter().append("svg").attr("height",height).attr("width",width).attr("class","xchart");svg.transition().attr("width",width).attr("height",height);g=svg.selectAll("g").data(emptyData);g.enter().append("g").attr("transform","translate("+options.paddingLeft+","+options.paddingTop+")");gScale=g.selectAll("g.scale").data(emptyData);gScale.enter().append("g").attr("class","scale");self._svg=svg;self._g=g;self._gScale=gScale;self._height=height-options.paddingTop-options.paddingBottom-options.axisPaddingTop-options.axisPaddingBottom;self._width=width-options.paddingLeft-options.paddingRight-options.axisPaddingLeft-options.axisPaddingRight},_resize:function(event){var self=this;self._drawSvg();self._draw()},_drawAxes:function(){if(this._noData){return}var self=this,o=self._options,t=self._gScale.transition().duration(o.timing),xTicks=o.tickHintX,yTicks=o.tickHintY,bottom=self._height+o.axisPaddingTop+o.axisPaddingBottom,zeroLine=d3.svg.line().x(function(d){return d}),zLine,zLinePath,xAxis,xRules,yAxis,yRules,labels;xRules=d3.svg.axis().scale(self.xScale).ticks(xTicks).tickSize(-self._height).tickFormat(o.tickFormatX).orient("bottom");xAxis=self._gScale.selectAll("g.axisX").data(emptyData);xAxis.enter().append("g").attr("class","axis axisX").attr("transform","translate(0,"+bottom+")");xAxis.call(xRules);labels=self._gScale.selectAll(".axisX g")[0];if(labels.length>self._width/80){labels.sort(function(a,b){var r=/translate\(([^,)]+)/;a=a.getAttribute("transform").match(r);b=b.getAttribute("transform").match(r);return parseFloat(a[1],10)-parseFloat(b[1],10)});d3.selectAll(labels).filter(function(d,i){return i%(Math.ceil(labels.length/xTicks)+1)}).remove()}yRules=d3.svg.axis().scale(self.yScale).ticks(yTicks).tickSize(-self._width-o.axisPaddingRight-o.axisPaddingLeft).tickFormat(o.tickFormatY).orient("left");yAxis=self._gScale.selectAll("g.axisY").data(emptyData);yAxis.enter().append("g").attr("class","axis axisY").attr("transform","translate(0,0)");t.selectAll("g.axisY").call(yRules);zLine=self._gScale.selectAll("g.axisZero").data([[]]);zLine.enter().append("g").attr("class","axisZero");zLinePath=zLine.selectAll("line").data([[]]);zLinePath.enter().append("line").attr("x1",0).attr("x2",self._width+o.axisPaddingLeft+o.axisPaddingRight).attr("y1",self.yZero).attr("y2",self.yZero);zLinePath.transition().duration(o.timing).attr("y1",self.yZero).attr("y2",self.yZero)},_updateScale:function(){var self=this,_unionData=function(){return _.union(self._mainData,self._compData)},scaleData=_unionData(),vis=self._vis,scale,min;delete self.xScale;delete self.yScale;delete self.yZero;if(vis.hasOwnProperty("preUpdateScale")){vis.preUpdateScale(self,scaleData,self._mainData,self._compData)}scaleData=_unionData();scale=_scales.xy(self,scaleData,self._xScaleType,self._yScaleType);self.xScale=scale.x;self.yScale=scale.y;min=self.yScale.domain()[0];self.yZero=min>0?self.yScale(min):self.yScale(0);if(vis.hasOwnProperty("postUpdateScale")){vis.postUpdateScale(self,scaleData,self._mainData,self._compData)}},_enter:function(vis,storage,data,className){var self=this,callbacks={click:self._options.click,mouseover:self._options.mouseover,mouseout:self._options.mouseout};self._checkVisMethod(vis,"enter");vis.enter(self,storage,className,data,callbacks)},_update:function(vis,storage){var self=this;self._checkVisMethod(vis,"update");vis.update(self,storage,self._options.timing)},_exit:function(vis,storage){var self=this;self._checkVisMethod(vis,"exit");vis.exit(self,storage,self._options.timing)},_destroy:function(vis,storage){var self=this;self._checkVisMethod(vis,"destroy");try{vis.destroy(self,storage,self._options.timing)}catch(e){}},_draw:function(){var self=this,o=self._options,comp,compKeys;self._noData=_.flatten(_.pluck(self._mainData,"data").concat(_.pluck(self._compData,"data"))).length===0;self._updateScale();self._drawAxes();self._enter(self._vis,self._mainStorage,self._mainData,".main");self._exit(self._vis,self._mainStorage);self._update(self._vis,self._mainStorage);comp=_.chain(self._compData).groupBy(function(d){return d.type});compKeys=comp.keys();_.each(self._compStorage,function(d,key){if(-1===compKeys.indexOf(key).value()){var vis=_vis[key];self._enter(vis,d,[],".comp."+key.replace(/\W+/g,""));self._exit(vis,d)}});comp.each(function(d,key){var vis=_vis[key],storage;if(!self._compStorage.hasOwnProperty(key)){self._compStorage[key]={}}storage=self._compStorage[key];self._enter(vis,storage,d,".comp."+key.replace(/\W+/g,""));self._exit(vis,storage);self._update(vis,storage)});if(self._noData){o.empty(self,self._selector,self._mainData)}else{o.notempty(self,self._selector)}},_checkVisMethod:function(vis,method){var self=this;if(!vis[method]){throw'Required method "'+method+'" not found on vis type "'+self._type+'".'}}});if(typeof define==="function"&&define.amd&&typeof define.amd==="object"){define(function(){return xChart});return}window.xChart=xChart})();
\ No newline at end of file
/* nvd3 version 1.8.5 (https://github.com/novus/nvd3) 2016-12-01 */
.nvd3 .nv-axis {
pointer-events:none;
opacity: 1;
}
.nvd3 .nv-axis path {
fill: none;
stroke: #000;
stroke-opacity: .75;
shape-rendering: crispEdges;
}
.nvd3 .nv-axis path.domain {
stroke-opacity: .75;
}
.nvd3 .nv-axis.nv-x path.domain {
stroke-opacity: 0;
}
.nvd3 .nv-axis line {
fill: none;
stroke: #e5e5e5;
shape-rendering: crispEdges;
}
.nvd3 .nv-axis .zero line,
/*this selector may not be necessary*/ .nvd3 .nv-axis line.zero {
stroke-opacity: .75;
}
.nvd3 .nv-axis .nv-axisMaxMin text {
font-weight: bold;
}
.nvd3 .x .nv-axis .nv-axisMaxMin text,
.nvd3 .x2 .nv-axis .nv-axisMaxMin text,
.nvd3 .x3 .nv-axis .nv-axisMaxMin text {
text-anchor: middle;
}
.nvd3 .nv-axis.nv-disabled {
opacity: 0;
}
.nvd3 .nv-bars rect {
fill-opacity: .75;
transition: fill-opacity 250ms linear;
}
.nvd3 .nv-bars rect.hover {
fill-opacity: 1;
}
.nvd3 .nv-bars .hover rect {
fill: lightblue;
}
.nvd3 .nv-bars text {
fill: rgba(0,0,0,0);
}
.nvd3 .nv-bars .hover text {
fill: rgba(0,0,0,1);
}
.nvd3 .nv-multibar .nv-groups rect,
.nvd3 .nv-multibarHorizontal .nv-groups rect,
.nvd3 .nv-discretebar .nv-groups rect {
stroke-opacity: 0;
transition: fill-opacity 250ms linear;
}
.nvd3 .nv-multibar .nv-groups rect:hover,
.nvd3 .nv-multibarHorizontal .nv-groups rect:hover,
.nvd3 .nv-candlestickBar .nv-ticks rect:hover,
.nvd3 .nv-discretebar .nv-groups rect:hover {
fill-opacity: 1;
}
.nvd3 .nv-discretebar .nv-groups text,
.nvd3 .nv-multibarHorizontal .nv-groups text {
font-weight: bold;
fill: rgba(0,0,0,1);
stroke: rgba(0,0,0,0);
}
/* boxplot CSS */
.nvd3 .nv-boxplot circle {
fill-opacity: 0.5;
}
.nvd3 .nv-boxplot circle:hover {
fill-opacity: 1;
}
.nvd3 .nv-boxplot rect:hover {
fill-opacity: 1;
}
.nvd3 line.nv-boxplot-median {
stroke: black;
}
.nv-boxplot-tick:hover {
stroke-width: 2.5px;
}
/* bullet */
.nvd3.nv-bullet { font: 10px sans-serif; }
.nvd3.nv-bullet .nv-measure { fill-opacity: .8; }
.nvd3.nv-bullet .nv-measure:hover { fill-opacity: 1; }
.nvd3.nv-bullet .nv-marker { stroke: #000; stroke-width: 2px; }
.nvd3.nv-bullet .nv-markerTriangle { stroke: #000; fill: #fff; stroke-width: 1.5px; }
.nvd3.nv-bullet .nv-markerLine { stroke: #000; stroke-width: 1.5px; }
.nvd3.nv-bullet .nv-tick line { stroke: #666; stroke-width: .5px; }
.nvd3.nv-bullet .nv-range.nv-s0 { fill: #eee; }
.nvd3.nv-bullet .nv-range.nv-s1 { fill: #ddd; }
.nvd3.nv-bullet .nv-range.nv-s2 { fill: #ccc; }
.nvd3.nv-bullet .nv-title { font-size: 14px; font-weight: bold; }
.nvd3.nv-bullet .nv-subtitle { fill: #999; }
.nvd3.nv-bullet .nv-range {
fill: #bababa;
fill-opacity: .4;
}
.nvd3.nv-bullet .nv-range:hover {
fill-opacity: .7;
}
.nvd3.nv-candlestickBar .nv-ticks .nv-tick {
stroke-width: 1px;
}
.nvd3.nv-candlestickBar .nv-ticks .nv-tick.hover {
stroke-width: 2px;
}
.nvd3.nv-candlestickBar .nv-ticks .nv-tick.positive rect {
stroke: #2ca02c;
fill: #2ca02c;
}
.nvd3.nv-candlestickBar .nv-ticks .nv-tick.negative rect {
stroke: #d62728;
fill: #d62728;
}
.with-transitions .nv-candlestickBar .nv-ticks .nv-tick {
transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
}
.nvd3.nv-candlestickBar .nv-ticks line {
stroke: #333;
}
.nv-force-node {
stroke: #fff;
stroke-width: 1.5px;
}
.nv-force-link {
stroke: #999;
stroke-opacity: .6;
}
.nv-force-node text {
stroke-width: 0px;
}
.nvd3 .nv-legend .nv-disabled rect {
/*fill-opacity: 0;*/
}
.nvd3 .nv-check-box .nv-box {
fill-opacity:0;
stroke-width:2;
}
.nvd3 .nv-check-box .nv-check {
fill-opacity:0;
stroke-width:4;
}
.nvd3 .nv-series.nv-disabled .nv-check-box .nv-check {
fill-opacity:0;
stroke-opacity:0;
}
.nvd3 .nv-controlsWrap .nv-legend .nv-check-box .nv-check {
opacity: 0;
}
/* line plus bar */
.nvd3.nv-linePlusBar .nv-bar rect {
fill-opacity: .75;
}
.nvd3.nv-linePlusBar .nv-bar rect:hover {
fill-opacity: 1;
}
.nvd3 .nv-groups path.nv-line {
fill: none;
}
.nvd3 .nv-groups path.nv-area {
stroke: none;
}
.nvd3.nv-line .nvd3.nv-scatter .nv-groups .nv-point {
fill-opacity: 0;
stroke-opacity: 0;
}
.nvd3.nv-scatter.nv-single-point .nv-groups .nv-point {
fill-opacity: .5 !important;
stroke-opacity: .5 !important;
}
.with-transitions .nvd3 .nv-groups .nv-point {
transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
}
.nvd3.nv-scatter .nv-groups .nv-point.hover,
.nvd3 .nv-groups .nv-point.hover {
stroke-width: 7px;
fill-opacity: .95 !important;
stroke-opacity: .95 !important;
}
.nvd3 .nv-point-paths path {
stroke: #aaa;
stroke-opacity: 0;
fill: #eee;
fill-opacity: 0;
}
.nvd3 .nv-indexLine {
cursor: ew-resize;
}
/********************
* SVG CSS
*/
/********************
Default CSS for an svg element nvd3 used
*/
svg.nvd3-svg {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
display: block;
width:100%;
height:100%;
}
/********************
Box shadow and border radius styling
*/
.nvtooltip.with-3d-shadow, .with-3d-shadow .nvtooltip {
box-shadow: 0 5px 10px rgba(0,0,0,.2);
border-radius: 5px;
}
.nvd3 text {
font: normal 12px Arial, sans-serif;
}
.nvd3 .title {
font: bold 14px Arial, sans-serif;
}
.nvd3 .nv-background {
fill: white;
fill-opacity: 0;
}
.nvd3.nv-noData {
font-size: 18px;
font-weight: bold;
}
/**********
* Brush
*/
.nv-brush .extent {
fill-opacity: .125;
shape-rendering: crispEdges;
}
.nv-brush .resize path {
fill: #eee;
stroke: #666;
}
/**********
* Legend
*/
.nvd3 .nv-legend .nv-series {
cursor: pointer;
}
.nvd3 .nv-legend .nv-disabled circle {
fill-opacity: 0;
}
/* focus */
.nvd3 .nv-brush .extent {
fill-opacity: 0 !important;
}
.nvd3 .nv-brushBackground rect {
stroke: #000;
stroke-width: .4;
fill: #fff;
fill-opacity: .7;
}
/**********
* Print
*/
@media print {
.nvd3 text {
stroke-width: 0;
fill-opacity: 1;
}
}
.nvd3.nv-ohlcBar .nv-ticks .nv-tick {
stroke-width: 1px;
}
.nvd3.nv-ohlcBar .nv-ticks .nv-tick.hover {
stroke-width: 2px;
}
.nvd3.nv-ohlcBar .nv-ticks .nv-tick.positive {
stroke: #2ca02c;
}
.nvd3.nv-ohlcBar .nv-ticks .nv-tick.negative {
stroke: #d62728;
}
.nvd3 .background path {
fill: none;
stroke: #EEE;
stroke-opacity: .4;
shape-rendering: crispEdges;
}
.nvd3 .foreground path {
fill: none;
stroke-opacity: .7;
}
.nvd3 .nv-parallelCoordinates-brush .extent {
fill: #fff;
fill-opacity: .6;
stroke: gray;
shape-rendering: crispEdges;
}
.nvd3 .nv-parallelCoordinates .hover {
fill-opacity: 1;
stroke-width: 3px;
}
.nvd3 .missingValuesline line {
fill: none;
stroke: black;
stroke-width: 1;
stroke-opacity: 1;
stroke-dasharray: 5, 5;
}
.nvd3.nv-pie path {
stroke-opacity: 0;
transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear;
}
.nvd3.nv-pie .nv-pie-title {
font-size: 24px;
fill: rgba(19, 196, 249, 0.59);
}
.nvd3.nv-pie .nv-slice text {
stroke: #000;
stroke-width: 0;
}
.nvd3.nv-pie path {
stroke: #fff;
stroke-width: 1px;
stroke-opacity: 1;
}
.nvd3.nv-pie path {
fill-opacity: .7;
}
.nvd3.nv-pie .hover path {
fill-opacity: 1;
}
.nvd3.nv-pie .nv-label {
pointer-events: none;
}
.nvd3.nv-pie .nv-label rect {
fill-opacity: 0;
stroke-opacity: 0;
}
/* scatter */
.nvd3 .nv-groups .nv-point.hover {
stroke-width: 20px;
stroke-opacity: .5;
}
.nvd3 .nv-scatter .nv-point.hover {
fill-opacity: 1;
}
.nv-noninteractive {
pointer-events: none;
}
.nv-distx, .nv-disty {
pointer-events: none;
}
/* sparkline */
.nvd3.nv-sparkline path {
fill: none;
}
.nvd3.nv-sparklineplus g.nv-hoverValue {
pointer-events: none;
}
.nvd3.nv-sparklineplus .nv-hoverValue line {
stroke: #333;
stroke-width: 1.5px;
}
.nvd3.nv-sparklineplus,
.nvd3.nv-sparklineplus g {
pointer-events: all;
}
.nvd3 .nv-hoverArea {
fill-opacity: 0;
stroke-opacity: 0;
}
.nvd3.nv-sparklineplus .nv-xValue,
.nvd3.nv-sparklineplus .nv-yValue {
stroke-width: 0;
font-size: .9em;
font-weight: normal;
}
.nvd3.nv-sparklineplus .nv-yValue {
stroke: #f66;
}
.nvd3.nv-sparklineplus .nv-maxValue {
stroke: #2ca02c;
fill: #2ca02c;
}
.nvd3.nv-sparklineplus .nv-minValue {
stroke: #d62728;
fill: #d62728;
}
.nvd3.nv-sparklineplus .nv-currentValue {
font-weight: bold;
font-size: 1.1em;
}
/* stacked area */
.nvd3.nv-stackedarea path.nv-area {
fill-opacity: .7;
stroke-opacity: 0;
transition: fill-opacity 250ms linear, stroke-opacity 250ms linear;
}
.nvd3.nv-stackedarea path.nv-area.hover {
fill-opacity: .9;
}
.nvd3.nv-stackedarea .nv-groups .nv-point {
stroke-opacity: 0;
fill-opacity: 0;
}
.nvtooltip {
position: absolute;
background-color: rgba(255,255,255,1.0);
color: rgba(0,0,0,1.0);
padding: 1px;
border: 1px solid rgba(0,0,0,.2);
z-index: 10000;
display: block;
font-family: Arial, sans-serif;
font-size: 13px;
text-align: left;
pointer-events: none;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.nvtooltip {
background: rgba(255,255,255, 0.8);
border: 1px solid rgba(0,0,0,0.5);
border-radius: 4px;
}
/*Give tooltips that old fade in transition by
putting a "with-transitions" class on the container div.
*/
.nvtooltip.with-transitions, .with-transitions .nvtooltip {
transition: opacity 50ms linear;
transition-delay: 200ms;
}
.nvtooltip.x-nvtooltip,
.nvtooltip.y-nvtooltip {
padding: 8px;
}
.nvtooltip h3 {
margin: 0;
padding: 4px 14px;
line-height: 18px;
font-weight: normal;
background-color: rgba(247,247,247,0.75);
color: rgba(0,0,0,1.0);
text-align: center;
border-bottom: 1px solid #ebebeb;
border-radius: 5px 5px 0 0;
}
.nvtooltip p {
margin: 0;
padding: 5px 14px;
text-align: center;
}
.nvtooltip span {
display: inline-block;
margin: 2px 0;
}
.nvtooltip table {
margin: 6px;
border-spacing:0;
}
.nvtooltip table td {
padding: 2px 9px 2px 0;
vertical-align: middle;
}
.nvtooltip table td.key {
font-weight: normal;
}
.nvtooltip table td.key.total {
font-weight: bold;
}
.nvtooltip table td.value {
text-align: right;
font-weight: bold;
}
.nvtooltip table td.percent {
color: darkgray;
}
.nvtooltip table tr.highlight td {
padding: 1px 9px 1px 0;
border-bottom-style: solid;
border-bottom-width: 1px;
border-top-style: solid;
border-top-width: 1px;
}
.nvtooltip table td.legend-color-guide div {
width: 8px;
height: 8px;
vertical-align: middle;
}
.nvtooltip table td.legend-color-guide div {
width: 12px;
height: 12px;
border: 1px solid #999;
}
.nvtooltip .footer {
padding: 3px;
text-align: center;
}
.nvtooltip-pending-removal {
pointer-events: none;
display: none;
}
/****
Interactive Layer
*/
.nvd3 .nv-interactiveGuideLine {
pointer-events:none;
}
.nvd3 line.nv-guideline {
stroke: #ccc;
}
.nvd3 .nv-axis line,.nvd3 .nv-axis path{fill:none;shape-rendering:crispEdges}.nv-brush .extent,.nvd3 .background path,.nvd3 .nv-axis line,.nvd3 .nv-axis path{shape-rendering:crispEdges}.nv-distx,.nv-disty,.nv-noninteractive,.nvd3 .nv-axis,.nvd3.nv-pie .nv-label,.nvd3.nv-sparklineplus g.nv-hoverValue{pointer-events:none}.nvd3 .nv-axis{opacity:1}.nvd3 .nv-axis.nv-disabled,.nvd3 .nv-controlsWrap .nv-legend .nv-check-box .nv-check{opacity:0}.nvd3 .nv-axis path{stroke:#000;stroke-opacity:.75}.nvd3 .nv-axis path.domain{stroke-opacity:.75}.nvd3 .nv-axis.nv-x path.domain{stroke-opacity:0}.nvd3 .nv-axis line{stroke:#e5e5e5}.nvd3 .nv-axis .zero line, .nvd3 .nv-axis line.zero{stroke-opacity:.75}.nvd3 .nv-axis .nv-axisMaxMin text{font-weight:700}.nvd3 .x .nv-axis .nv-axisMaxMin text,.nvd3 .x2 .nv-axis .nv-axisMaxMin text,.nvd3 .x3 .nv-axis .nv-axisMaxMin text{text-anchor:middle}.nvd3 .nv-bars rect{fill-opacity:.75;transition:fill-opacity 250ms linear}.nvd3 .nv-bars rect.hover{fill-opacity:1}.nvd3 .nv-bars .hover rect{fill:#add8e6}.nvd3 .nv-bars text{fill:transparent}.nvd3 .nv-bars .hover text{fill:rgba(0,0,0,1)}.nvd3 .nv-discretebar .nv-groups rect,.nvd3 .nv-multibar .nv-groups rect,.nvd3 .nv-multibarHorizontal .nv-groups rect{stroke-opacity:0;transition:fill-opacity 250ms linear}.with-transitions .nv-candlestickBar .nv-ticks .nv-tick,.with-transitions .nvd3 .nv-groups .nv-point{transition:stroke-width 250ms linear,stroke-opacity 250ms linear}.nvd3 .nv-candlestickBar .nv-ticks rect:hover,.nvd3 .nv-discretebar .nv-groups rect:hover,.nvd3 .nv-multibar .nv-groups rect:hover,.nvd3 .nv-multibarHorizontal .nv-groups rect:hover{fill-opacity:1}.nvd3 .nv-discretebar .nv-groups text,.nvd3 .nv-multibarHorizontal .nv-groups text{font-weight:700;fill:rgba(0,0,0,1);stroke:transparent}.nvd3 .nv-boxplot circle{fill-opacity:.5}.nvd3 .nv-boxplot circle:hover,.nvd3 .nv-boxplot rect:hover{fill-opacity:1}.nvd3 line.nv-boxplot-median{stroke:#000}.nv-boxplot-tick:hover{stroke-width:2.5px}.nvd3.nv-bullet{font:10px sans-serif}.nvd3.nv-bullet .nv-measure{fill-opacity:.8}.nvd3.nv-bullet .nv-measure:hover{fill-opacity:1}.nvd3.nv-bullet .nv-marker{stroke:#000;stroke-width:2px}.nvd3.nv-bullet .nv-markerTriangle{stroke:#000;fill:#fff;stroke-width:1.5px}.nvd3.nv-bullet .nv-markerLine{stroke:#000;stroke-width:1.5px}.nvd3.nv-bullet .nv-tick line{stroke:#666;stroke-width:.5px}.nvd3.nv-bullet .nv-range.nv-s0{fill:#eee}.nvd3.nv-bullet .nv-range.nv-s1{fill:#ddd}.nvd3.nv-bullet .nv-range.nv-s2{fill:#ccc}.nvd3.nv-bullet .nv-title{font-size:14px;font-weight:700}.nvd3.nv-bullet .nv-subtitle{fill:#999}.nvd3.nv-bullet .nv-range{fill:#bababa;fill-opacity:.4}.nvd3.nv-bullet .nv-range:hover{fill-opacity:.7}.nvd3.nv-candlestickBar .nv-ticks .nv-tick{stroke-width:1px}.nvd3.nv-candlestickBar .nv-ticks .nv-tick.hover{stroke-width:2px}.nvd3.nv-candlestickBar .nv-ticks .nv-tick.positive rect{stroke:#2ca02c;fill:#2ca02c}.nvd3.nv-candlestickBar .nv-ticks .nv-tick.negative rect{stroke:#d62728;fill:#d62728}.nvd3.nv-candlestickBar .nv-ticks line{stroke:#333}.nv-force-node{stroke:#fff;stroke-width:1.5px}.nv-force-link{stroke:#999;stroke-opacity:.6}.nv-force-node text{stroke-width:0}.nvd3 .nv-check-box .nv-box{fill-opacity:0;stroke-width:2}.nvd3 .nv-check-box .nv-check{fill-opacity:0;stroke-width:4}.nvd3 .nv-series.nv-disabled .nv-check-box .nv-check{fill-opacity:0;stroke-opacity:0}.nvd3.nv-linePlusBar .nv-bar rect{fill-opacity:.75}.nvd3.nv-linePlusBar .nv-bar rect:hover{fill-opacity:1}.nvd3 .nv-groups path.nv-line{fill:none}.nvd3 .nv-groups path.nv-area{stroke:none}.nvd3.nv-line .nvd3.nv-scatter .nv-groups .nv-point{fill-opacity:0;stroke-opacity:0}.nvd3.nv-scatter.nv-single-point .nv-groups .nv-point{fill-opacity:.5!important;stroke-opacity:.5!important}.nvd3 .nv-groups .nv-point.hover,.nvd3.nv-scatter .nv-groups .nv-point.hover{stroke-width:7px;fill-opacity:.95!important;stroke-opacity:.95!important}.nvd3 .nv-point-paths path{stroke:#aaa;stroke-opacity:0;fill:#eee;fill-opacity:0}.nvd3 .nv-indexLine{cursor:ew-resize}svg.nvd3-svg{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;display:block;width:100%;height:100%}.nvtooltip.with-3d-shadow,.with-3d-shadow .nvtooltip{box-shadow:0 5px 10px rgba(0,0,0,.2);border-radius:5px}.nvd3 text{font:400 12px Arial,sans-serif}.nvd3 .title{font:700 14px Arial,sans-serif}.nvd3 .nv-background{fill:#fff;fill-opacity:0}.nvd3.nv-noData{font-size:18px;font-weight:700}.nv-brush .extent{fill-opacity:.125}.nv-brush .resize path{fill:#eee;stroke:#666}.nvd3 .nv-legend .nv-series{cursor:pointer}.nvd3 .nv-legend .nv-disabled circle{fill-opacity:0}.nvd3 .nv-brush .extent{fill-opacity:0!important}.nvd3 .nv-brushBackground rect{stroke:#000;stroke-width:.4;fill:#fff;fill-opacity:.7}@media print{.nvd3 text{stroke-width:0;fill-opacity:1}}.nvd3.nv-ohlcBar .nv-ticks .nv-tick{stroke-width:1px}.nvd3.nv-ohlcBar .nv-ticks .nv-tick.hover{stroke-width:2px}.nvd3.nv-ohlcBar .nv-ticks .nv-tick.positive{stroke:#2ca02c}.nvd3.nv-ohlcBar .nv-ticks .nv-tick.negative{stroke:#d62728}.nvd3 .background path{fill:none;stroke:#EEE;stroke-opacity:.4}.nvd3 .foreground path{fill:none;stroke-opacity:.7}.nvd3 .nv-parallelCoordinates-brush .extent{fill:#fff;fill-opacity:.6;stroke:gray;shape-rendering:crispEdges}.nvd3 .nv-parallelCoordinates .hover{fill-opacity:1;stroke-width:3px}.nvd3 .missingValuesline line{fill:none;stroke:#000;stroke-width:1;stroke-opacity:1;stroke-dasharray:5,5}.nvd3.nv-pie .nv-pie-title{font-size:24px;fill:rgba(19,196,249,.59)}.nvd3.nv-pie .nv-slice text{stroke:#000;stroke-width:0}.nvd3.nv-pie path{transition:fill-opacity 250ms linear,stroke-width 250ms linear,stroke-opacity 250ms linear;stroke:#fff;stroke-width:1px;stroke-opacity:1;fill-opacity:.7}.nvd3.nv-pie .hover path{fill-opacity:1}.nvd3.nv-pie .nv-label rect{fill-opacity:0;stroke-opacity:0}.nvd3 .nv-groups .nv-point.hover{stroke-width:20px;stroke-opacity:.5}.nvd3 .nv-scatter .nv-point.hover{fill-opacity:1}.nvd3.nv-sparkline path{fill:none}.nvd3.nv-sparklineplus .nv-hoverValue line{stroke:#333;stroke-width:1.5px}.nvd3.nv-sparklineplus,.nvd3.nv-sparklineplus g{pointer-events:all}.nvd3 .nv-interactiveGuideLine,.nvtooltip{pointer-events:none}.nvd3 .nv-hoverArea{fill-opacity:0;stroke-opacity:0}.nvd3.nv-sparklineplus .nv-xValue,.nvd3.nv-sparklineplus .nv-yValue{stroke-width:0;font-size:.9em;font-weight:400}.nvd3.nv-sparklineplus .nv-yValue{stroke:#f66}.nvd3.nv-sparklineplus .nv-maxValue{stroke:#2ca02c;fill:#2ca02c}.nvd3.nv-sparklineplus .nv-minValue{stroke:#d62728;fill:#d62728}.nvd3.nv-sparklineplus .nv-currentValue{font-weight:700;font-size:1.1em}.nvtooltip h3,.nvtooltip table td.key{font-weight:400}.nvd3.nv-stackedarea path.nv-area{fill-opacity:.7;stroke-opacity:0;transition:fill-opacity 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-stackedarea path.nv-area.hover{fill-opacity:.9}.nvd3.nv-stackedarea .nv-groups .nv-point{stroke-opacity:0;fill-opacity:0}.nvtooltip{position:absolute;color:rgba(0,0,0,1);padding:1px;z-index:10000;display:block;font-family:Arial,sans-serif;font-size:13px;text-align:left;white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background:rgba(255,255,255,.8);border:1px solid rgba(0,0,0,.5);border-radius:4px}.nvtooltip h3,.nvtooltip p{margin:0;text-align:center}.nvtooltip.with-transitions,.with-transitions .nvtooltip{transition:opacity 50ms linear;transition-delay:200ms}.nvtooltip.x-nvtooltip,.nvtooltip.y-nvtooltip{padding:8px}.nvtooltip h3{padding:4px 14px;line-height:18px;background-color:rgba(247,247,247,.75);color:rgba(0,0,0,1);border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.nvtooltip p{padding:5px 14px}.nvtooltip span{display:inline-block;margin:2px 0}.nvtooltip table{margin:6px;border-spacing:0}.nvtooltip table td{padding:2px 9px 2px 0;vertical-align:middle}.nvtooltip table td.key.total{font-weight:700}.nvtooltip table td.value{text-align:right;font-weight:700}.nvtooltip table td.percent{color:#a9a9a9}.nvtooltip table tr.highlight td{padding:1px 9px 1px 0;border-bottom-style:solid;border-bottom-width:1px;border-top-style:solid;border-top-width:1px}.nvtooltip table td.legend-color-guide div{vertical-align:middle;width:12px;height:12px;border:1px solid #999}.nvtooltip .footer{padding:3px;text-align:center}.nvtooltip-pending-removal{pointer-events:none;display:none}.nvd3 line.nv-guideline{stroke:#ccc}
/*# sourceMappingURL=nv.d3.min.css.map */
\ No newline at end of file
.icon-burndown {
background-image: url(../images/icon-burndown-16.png);
}
.icon-sprint-board {
background-image: url(../images/icon-sprint-board-16.png);
}
.icon-sprint-end {
background-image: url(../images/icon-sprint-end-16.png);
}
.icon-sprint-start {
background-image: url(../images/icon-sprint-start-16.png);
}
.icon-product-backlog {
background-image: url(../images/icon-product-backlog-16.png);
}
.icon-release-plan {
background-image: url(../images/icon-release-plan-16.png);
}
.icon-move-top {
background-image: url(../images/icon-move-top-16.png);
}
.icon-move-bottom {
background-image: url(../images/icon-move-bottom-16.png);
}
.icon-fullscreen {
background-image: url(../images/icon-fullscreen-16.png);
}
.icon-normal-screen {
background-image: url(../images/icon-normal-screen-16.png);
}
.icon-major-deviation {
background-image: url(../../../images/exclamation.png);
}
.icon-minor-deviation {
background-image: url(../../../images/warning.png);
}
.icon-below-deviation {
background-image: url(../../../images/lightning.png);
}
.icon-log-time {
background-image: url(../../../images/time_add.png);
}
.icon-vertical-drag-and-drop {
background-image: url(../../../images/reorder.png);
}
.float-icon {
float: right;
min-height: 16px;
padding-left: 16px;
padding-top: 0px;
padding-bottom: 0px;
}
.post-it, .post-it input {
}
.post-it {
overflow: hidden;
box-shadow: 4px 4px 7px rgba(0, 0, 0, 0.7);
-moz-box-shadow: 4px 4px 7px rgba(0, 0, 0, 0.7);
-o-box-shadow: 4px 4px 7px rgba(0, 0, 0, 0.7);
-webkit-box-shadow: 4px 4px 7px rgba(0, 0, 0, 0.7);
-transition: -transform 0.15s linear;
-moz-transition: -moz-transform 0.15s linear;
-o-transition: -o-transform 0.15s linear;
-webkit-transition: -webkit-transform 0.15s linear;
}
.post-it-scale:hover, .post-it-scale:hover {
z-index: 100;
}
.post-it-scale:hover {
-transform: scale(1.14);
-moz-transform: scale(1.14);
-o-transform: scale(1.14);
-webkit-transform: scale(1.14);
}
.post-it-small-scale:hover {
-transform: scale(1.02, 1.14);
-moz-transform: scale(1.02, 1.14);
-o-transform: scale(1.02, 1.14);
-webkit-transform: scale(1.02, 1.14);
}
.settings-post-it {
margin-right: 20px;
white-space: nowrap;
padding: 2px;
padding-right: 9px;
border-radius: 9px;
}
.post-it-color-1 {
background-image: -ms-linear-gradient(top, #FFFFE0 0%, #FFFE8D 100%);
background-image: -moz-linear-gradient(top, #FFFFE0 0%, #FFFE8D 100%);
background-image: -o-linear-gradient(top, #FFFFE0 0%, #FFFE8D 100%);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #FFFFE0), color-stop(1, #FFFE8D));
background-image: -webkit-linear-gradient(top, #FFFFE0 0%, #FFFE8D 100%);
background-image: linear-gradient(to bottom, #FFFFE0 0%, #FFFE8D 100%);
}
.post-it-color-2 {
background-image: -ms-linear-gradient(top, #F7D6FF 0%, #E9B1FF 100%);
background-image: -moz-linear-gradient(top, #F7D6FF 0%, #E9B1FF 100%);
background-image: -o-linear-gradient(top, #F7D6FF 0%, #E9B1FF 100%);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #F7D6FF), color-stop(1, #E9B1FF));
background-image: -webkit-linear-gradient(top, #F7D6FF 0%, #E9B1FF 100%);
background-image: linear-gradient(to bottom, #F7D6FF 0%, #E9B1FF 100%);
}
.post-it-color-3 {
background-image: -ms-linear-gradient(top, #C4FFCA 0%, #9CFFA4 100%);
background-image: -moz-linear-gradient(top, #C4FFCA 0%, #9CFFA4 100%);
background-image: -o-linear-gradient(top, #C4FFCA 0%, #9CFFA4 100%);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #C4FFCA), color-stop(1, #9CFFA4));
background-image: -webkit-linear-gradient(top, #C4FFCA 0%, #9CFFA4 100%);
background-image: linear-gradient(to bottom, #C4FFCA 0%, #9CFFA4 100%);
}
.post-it-color-4 {
background-image: -ms-linear-gradient(top, #C8E7FF 0%, #A3BFD7 100%);
background-image: -moz-linear-gradient(top, #C8E7FF 0%, #A3BFD7 100%);
background-image: -o-linear-gradient(top, #C8E7FF 0%, #A3BFD7 100%);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #C8E7FF), color-stop(1, #A3BFD7));
background-image: -webkit-linear-gradient(top, #C8E7FF 0%, #A3BFD7 100%);
background-image: linear-gradient(to bottom, #C8E7FF 0%, #A3BFD7 100%);
}
.post-it-color-5 {
background-image: -ms-linear-gradient(top, #FFE9C5 0%, #FFDC85 100%);
background-image: -moz-linear-gradient(top, #FFE9C5 0%, #FFDC85 100%);
background-image: -o-linear-gradient(top, #FFE9C5 0%, #FFDC85 100%);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #FFE9C5), color-stop(1, #FFDC85));
background-image: -webkit-linear-gradient(top, #FFE9C5 0%, #FFDC85 100%);
background-image: linear-gradient(to bottom, #FFE9C5 0%, #FFDC85 100%);
}
.post-it-color-6 {
background-image: -ms-linear-gradient(top, #FFCCCC 0%, #FFB3B8 100%);
background-image: -moz-linear-gradient(top, #FFCCCC 0%, #FFB3B8 100%);
background-image: -o-linear-gradient(top, #FFCCCC 0%, #FFB3B8 100%);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #FFCCCC), color-stop(1, #FFB3B8));
background-image: -webkit-linear-gradient(top, #FFCCCC 0%, #FFB3B8 100%);
background-image: linear-gradient(to bottom, #FFCCCC 0%, #FFB3B8 100%);
}
.post-it-color-7 {
background-image: -ms-linear-gradient(top, #FFCCF3 0%, #FFA4E5 100%);
background-image: -moz-linear-gradient(top, #FFCCF3 0%, #FFA4E5 100%);
background-image: -o-linear-gradient(top, #FFCCF3 0%, #FFA4E5 100%);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #FFCCF3), color-stop(1, #FFA4E5));
background-image: -webkit-linear-gradient(top, #FFCCF3 0%, #FFA4E5 100%);
background-image: linear-gradient(to bottom, #FFCCF3 0%, #FFA4E5 100%);
}
.post-it-color-8 {
background-image: -ms-linear-gradient(top, #FAFAFA 0%, #DADADA 100%);
background-image: -moz-linear-gradient(top, #FAFAFA 0%, #DADADA 100%);
background-image: -o-linear-gradient(top, #FAFAFA 0%, #DADADA 100%);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #FAFAFA), color-stop(1, #DADADA));
background-image: -webkit-linear-gradient(top, #FAFAFA 0%, #DADADA 100%);
background-image: linear-gradient(to bottom, #FAFAFA 0%, #DADADA 100%);
}
.post-it-color-9 {
background-image: -ms-linear-gradient(top, #B4FFB3 0%, #8CCD8B 100%);
background-image: -moz-linear-gradient(top, #B4FFB3 0%, #8CCD8B 100%);
background-image: -o-linear-gradient(top, #B4FFB3 0%, #8CCD8B 100%);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #B4FFB3), color-stop(1, #8CCD8B));
background-image: -webkit-linear-gradient(top, #B4FFB3 0%, #8CCD8B 100%);
background-image: linear-gradient(to bottom, #B4FFB3 0%, #8CCD8B 100%);
}
.post-it-color-10 {
background-image: -ms-linear-gradient(top, #C8C6FF 0%, #9A98CB 100%);
background-image: -moz-linear-gradient(top, #C8C6FF 0%, #9A98CB 100%);
background-image: -o-linear-gradient(top, #C8C6FF 0%, #9A98CB 100%);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #C8C6FF), color-stop(1, #9A98CB));
background-image: -webkit-linear-gradient(top, #C8C6FF 0%, #9A98CB 100%);
background-image: linear-gradient(to bottom, #C8C6FF 0%, #9A98CB 100%);
}
.post-it-rotation-0 {
}
.post-it-rotation-1 {
-o-transform: rotate(-0.5deg);
-webkit-transform: rotate(-0.5deg);
-moz-transform: rotate(-0.5deg);
}
.post-it-rotation-2 {
-o-transform: rotate(0.5deg);
-webkit-transform: rotate(0.5deg);
-moz-transform: rotate(0.5deg);
}
.post-it-rotation-3 {
-o-transform: rotate(-1deg);
-webkit-transform: rotate(-1deg);
-moz-transform: rotate(-1deg);
}
.post-it-rotation-4 {
-o-transform: rotate(1deg);
-webkit-transform: rotate(1deg);
-moz-transform: rotate(1deg);
}
.post-it-small-rotation-0 {
}
.post-it-small-rotation-1 {
-o-transform: rotate(-0.1deg);
-webkit-transform: rotate(-0.1deg);
-moz-transform: rotate(-0.1deg);
}
.post-it-small-rotation-2 {
-o-transform: rotate(0.1deg);
-webkit-transform: rotate(0.1deg);
-moz-transform: rotate(0.1deg);
}
.post-it-small-rotation-3 {
-o-transform: rotate(-0.2deg);
-webkit-transform: rotate(-0.2deg);
-moz-transform: rotate(-0.2deg);
}
.post-it-small-rotation-4 {
-o-transform: rotate(0.2deg);
-webkit-transform: rotate(0.2deg);
-moz-transform: rotate(0.2deg);
}
.post-it-horizontal-move-cursor {
cursor: ew-resize;
}
.post-it-vertical-move-cursor {
cursor: ns-resize;
}
.post-it-legend {
font-style: italic;
font-size: 80%;
}
ul.pbis {
padding: 10px 0px 15px 0px;
width: 90%;
margin: 0px auto 0px auto;
}
ul.pbis li {
list-style: none;
padding: 3px 20px 3px 20px;
margin-bottom: 10px;
}
ul.pbis li table {
width: 100%;
}
ul.pbis li table tr {
vertical-align: middle;
}
ul.pbis li table tr td.header-1, ul.pbis li table tr td.header-2 {
font-size: 0.8em;
font-style: italic;
color: grey;
width: 50%;
}
ul.pbis li table tr td.header-1 {
text-align: left;
}
.pb-pbi-mini-post-its-container {
font-size: 1.4em;
color: black;
}
ul.pbis li table tr td.header-2 {
text-align: right;
}
ul.pbis li table tr td.story-points {
width: 6em;
min-width: 6em;
font-style: italic;
color: darkgrey;
white-space: nowrap;
}
ul.pbis li table tr td.story-points input {
font-size: 2em;
font-style: bold;
text-align: right;
min-width: 2em;
}
ul.pbis li table tr td.story-points-legend {
font-style: italic;
color: darkgrey;
min-width: 20px;
}
ul.pbis li table tr td.content {
width: 100%;
font-size: 1.2em;
}
table.sprint-board {
border-collapse: collapse;
overflow: auto;
width: 90%;
margin: 0px auto 10px auto;
}
table.sprint-board tr.sprint-board {
vertical-align: top;
}
table.sprint-board tr.sprint-board th.sprint-board,
table.sprint-board tr.sprint-board td.sprint-board {
min-width: 13em;
padding-left: 0.8em;
padding-top: 0.8em;
}
table.sprint-board tr.sprint-board th.sprint-board {
border-bottom: 0.5em solid darkgrey;
}
table.sprint-board tr.sprint-board td.sprint-board {
border: 0.5em solid darkgrey;
}
.sprint-row-space {
border: 0.5em solid darkgrey;
height: 11em;
background-color: lightyellow;
}
table.sprint-pbi, table.sprint-task {
height: 9em;
margin-right: 0.8em;
margin-bottom: 0.8em;
}
table.sprint-pbi {
width: 16em;
}
table.sprint-task {
width: 13em;
float: left;
}
table.sprint-pbi tr, table.sprint-task tr {
vertical-align: top;
}
table.sprint-pbi tr td.content, table.sprint-task tr td.content,
table.sprint-pbi tr td.content div.content, table.sprint-task tr td.content div.content {
height: 100%;
position: relative;
}
table.sprint-pbi, table.sprint-task {
padding-left: 0.3em;
padding-top: 0.1em;
padding-right: 0.3em;
padding-bottom: 0;
}
table.sprint-pbi tr td.content, table.sprint-task tr td.content,
table.sprint-pbi tr td.content div.content, table.sprint-task tr td.content div.content,
table.sprint-pbi tr td.estimation, table.sprint-task tr td.estimation,
table.sprint-pbi tr td.spent, table.sprint-task tr td.spent,
table.sprint-pbi tr td.pending, table.sprint-task tr td.pending {
padding: 0px;
}
table.sprint-pbi tr td.estimation, table.sprint-task tr td.estimation {
}
table.sprint-pbi tr td.spent, table.sprint-task tr td.spent {
text-align: center;
}
table.sprint-pbi tr td.pending, table.sprint-task tr td.pending {
text-align: right;
}
table.sprint-pbi input, table.sprint-task input {
text-align: right;
width: 2em;
}
.editable-time {
font-size: 1.1em;
padding: 0px;
background: transparent;
border: none;
}
.doers-reviewers-post-its-container {
position: absolute;
right: 0px;
bottom: 0px;
overflow: visible;
max-width: 9em;
}
.doer-post-it, .reviewer-post-it, .blocked-post-it {
height: 1.3em;
font-size: 0.8em;
margin: 3px 0px 0px 0px;
border-radius: 3px;
padding: 1px 3px 1px 3px;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
}
.doer-post-it {
}
.reviewer-post-it {
}
.blocked-post-it {
}
.post-it .gravatar {
border: 0;
padding: 0;
}
.scrum-custom-menu {
display: none;
z-index: 1000;
position: absolute;
background-color: lightgrey;
border: 1px solid grey;
padding: 15px 5px 5px 5px;
-moz-border-radius: 5px;
border-radius: 5px;
}
.scrum-custom-menu .close-icon {
margin-top: -7px;
margin-right: -8px;
}
.scrum-custom-menu fieldset {
border: 1px solid white;
-moz-border-radius: 5px;
border-radius: 5px;
color: white;
margin-top: 5px;
}
.scrum-custom-menu fieldset legend, .scrum-custom-menu label {
font-style: italic;
color: grey;
}
.scrum-custom-menu label {
float: left;
}
.scrum-custom-menu p {
margin-left: 100px;
}
.rotate {
-webkit-transform: rotate(-90deg);
-moz-transform: rotate(-90deg);
-ms-transform: rotate(-90deg);
-o-transform: rotate(-90deg);
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3);
}
.burndown {
width: 90%;
text-align: center;
}
.scrum-dialog {
display: none;
}
.graphic-tooltip {
position: absolute;
background: #EEE;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
-ms-border-radius: 3px;
-o-border-radius: 3px;
border-radius: 3px;
padding: 5px;
-webkit-box-shadow: 0 1px 3px #000;
-moz-box-shadow: 0 1px 3px #000;
-ms-box-shadow: 0 1px 3px #000;
-o-box-shadow: 0 1px 3px #000;
box-shadow: 0 1px 3px #000;
border-collapse: separate;
display: none;
z-index: 10;
}
.modal-issue-menu {
text-align: right;
margin-bottom: 0.5em;
font-size: 0.9em;
}
#licenseIframe {
width: 820px;
height: 580px;
}
table.scrum-stats-table {
width: 90%;
margin-left: 5%;
margin-right: 5%;
border-collapse: collapse;
}
table.scrum-stats-table tbody tr.double-row:nth-child(4n+0),
table.scrum-stats-table tbody tr.double-row:nth-child(4n+3),
table.scrum-stats-table thead tr.double-row {
background: #B8D1F3;
}
table.scrum-stats-table tbody tr.double-row:nth-child(4n+1),
table.scrum-stats-table tbody tr.double-row:nth-child(4n+2) {
background: #DAE5F4;
}
table.scrum-stats-table tbody tr:nth-child(2n+0),
table.scrum-stats-table thead tr {
background: #B8D1F3;
}
table.scrum-stats-table tbody tr:nth-child(2n+1) {
background: #DAE5F4;
}
table.scrum-stats-table tbody tr td.total {
font-weight: bold;
}
table.scrum-stats-table tbody tr td.comment {
font-style: italic;
font-color: grey;
font-size: 80%;
}
table.scrum-stats-chart-table {
width: 90%;
margin-left: 5%;
margin-right: 5%;
border-collapse: collapse;
}
.scrum-menu a {
margin-left: 1em;
}
.scrum-content {
position: relative;
}
.scrum-content-fullscreen {
background-color: white;
z-index: 50;
width: 100%;
height: 100%;
position: fixed;
top: 0px;
right: 0px;
overflow: auto;
}
.scrum-contextual {
position: absolute;
top: 0px;
right: 0px;
}
.scrum-content .icon-normal-screen {
display: none;
}
.scrum-content-fullscreen .icon-normal-screen {
display: inline;
margin-top: 5px;
margin-right: 10px;
}
.scrum-content .icon-fullscreen {
display: inline;
margin-top: -5px;
}
.scrum-content-fullscreen .icon-fullscreen {
display: none;
}
.sprint-board-pbi-handle {
background-image: url(../../../images/reorder.png);
background-repeat: no-repeat;
background-position: center;
min-width: 16px;
}
.xchart .line{stroke-width:3px;fill:none}.xchart .fill{stroke-width:0}.xchart circle{stroke:#FFF;stroke-width:3px}.xchart .axis .domain{fill:none}.xchart .axis .tick line{stroke:#EEE;stroke-width:1px}.xchart .axis text{font-family:Helvetica,Arial,Verdana,sans-serif;fill:#666;font-size:12px}.xchart .color0 .line{stroke:#3880aa}.xchart .color0 .line .fill{pointer-events:none}.xchart .color0 rect,.xchart .color0 circle{fill:#3880aa}.xchart .color0 .fill{fill:none}.xchart .color0.comp .line{stroke:#89bbd8}.xchart .color0.comp rect{fill:#89bbd8}.xchart .color0.comp .fill{display:none}.xchart .color0.comp circle,.xchart .color0.comp .pointer{fill:#89bbd8}.xchart .color1 .line{stroke:#4da944}.xchart .color1 .line .fill{pointer-events:none}.xchart .color1 rect,.xchart .color1 circle{fill:#4da944}.xchart .color1 .fill{fill:none}.xchart .color1.comp .line{stroke:#9dd597}.xchart .color1.comp rect{fill:#9dd597}.xchart .color1.comp .fill{display:none}.xchart .color1.comp circle,.xchart .color1.comp .pointer{fill:#9dd597}.xchart .color2 .line{stroke:#f26522}.xchart .color2 .line .fill{pointer-events:none}.xchart .color2 rect,.xchart .color2 circle{fill:#f26522}.xchart .color2 .fill{fill:none}.xchart .color2.comp .line{stroke:#f9b99a}.xchart .color2.comp rect{fill:#f9b99a}.xchart .color2.comp .fill{display:none}.xchart .color2.comp circle,.xchart .color2.comp .pointer{fill:#f9b99a}.xchart .color3 .line{stroke:#c6080d}.xchart .color3 .line .fill{pointer-events:none}.xchart .color3 rect,.xchart .color3 circle{fill:#c6080d}.xchart .color3 .fill{fill:none}.xchart .color3.comp .line{stroke:#f8555a}.xchart .color3.comp rect{fill:#f8555a}.xchart .color3.comp .fill{display:none}.xchart .color3.comp circle,.xchart .color3.comp .pointer{fill:#f8555a}.xchart .color4 .line{stroke:#672d8b}.xchart .color4 .line .fill{pointer-events:none}.xchart .color4 rect,.xchart .color4 circle{fill:#672d8b}.xchart .color4 .fill{fill:none}.xchart .color4.comp .line{stroke:#a869ce}.xchart .color4.comp rect{fill:#a869ce}.xchart .color4.comp .fill{display:none}.xchart .color4.comp circle,.xchart .color4.comp .pointer{fill:#a869ce}.xchart .color5 .line{stroke:#ce1797}.xchart .color5 .line .fill{pointer-events:none}.xchart .color5 rect,.xchart .color5 circle{fill:#ce1797}.xchart .color5 .fill{fill:none}.xchart .color5.comp .line{stroke:#f075cb}.xchart .color5.comp rect{fill:#f075cb}.xchart .color5.comp .fill{display:none}.xchart .color5.comp circle,.xchart .color5.comp .pointer{fill:#f075cb}.xchart .color6 .line{stroke:#d9ce00}.xchart .color6 .line .fill{pointer-events:none}.xchart .color6 rect,.xchart .color6 circle{fill:#d9ce00}.xchart .color6 .fill{fill:none}.xchart .color6.comp .line{stroke:#fff75a}.xchart .color6.comp rect{fill:#fff75a}.xchart .color6.comp .fill{display:none}.xchart .color6.comp circle,.xchart .color6.comp .pointer{fill:#fff75a}.xchart .color7 .line{stroke:#754c24}.xchart .color7 .line .fill{pointer-events:none}.xchart .color7 rect,.xchart .color7 circle{fill:#754c24}.xchart .color7 .fill{fill:none}.xchart .color7.comp .line{stroke:#c98c50}.xchart .color7.comp rect{fill:#c98c50}.xchart .color7.comp .fill{display:none}.xchart .color7.comp circle,.xchart .color7.comp .pointer{fill:#c98c50}.xchart .color8 .line{stroke:#2eb9b4}.xchart .color8 .line .fill{pointer-events:none}.xchart .color8 rect,.xchart .color8 circle{fill:#2eb9b4}.xchart .color8 .fill{fill:none}.xchart .color8.comp .line{stroke:#86e1de}.xchart .color8.comp rect{fill:#86e1de}.xchart .color8.comp .fill{display:none}.xchart .color8.comp circle,.xchart .color8.comp .pointer{fill:#86e1de}.xchart .color9 .line{stroke:#0e2e42}.xchart .color9 .line .fill{pointer-events:none}.xchart .color9 rect,.xchart .color9 circle{fill:#0e2e42}.xchart .color9 .fill{fill:none}.xchart .color9.comp .line{stroke:#2477ab}.xchart .color9.comp rect{fill:#2477ab}.xchart .color9.comp .fill{display:none}.xchart .color9.comp circle,.xchart .color9.comp .pointer{fill:#2477ab}
\ No newline at end of file
# encoding: UTF-8
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
en:
error_changing_pbi_order: "Fail to sort the PBI, check its dependencies. The page will be reloaded."
error_changing_task_status: "Fail to change the task status."
error_changing_task_assigned_to: "Fail to change the task assignee."
error_changing_value: "Fail to change the value."
error_creating_pbi: "Fail to create a product backlog item (%{message})."
error_creating_task: "Fail to create a task (%{message})."
error_creating_time_entry: "Fail to create a time entry (%{message})."
error_new_status_no_allowed: "It is not allowed to go from status ’%{status_from}’ to status ’%{status_to}’"
error_no_product_backlog: "There is none product backlog yet, please ask this project administrator to create it in settings tab."
error_no_sprints: "There are none Sprints defined yet, please ask this project administrator to create them in settings tab."
error_sorting_other_issues_depends_on_issue: "Other issues (%{others}) depend on #%{id}, it cannot be sorted."
error_updating_pbi: "Error updating the PBI (%{message})"
error_updating_task: "Error updating the task (%{message})"
field_end_date: "End date"
field_pending_effort: "Pending effort"
field_position: "Position"
field_shared: "Shared"
field_shared_note: "A shared Sprint is visible in child projects, so you will be able to assign child projects issues to this Sprint."
field_sprint: "Sprint"
label_all_but_total: "all except total"
label_add_pbi_at_bottom: "Add %{tracker} at product backlog bottom"
label_add_pbi_at_top: "Add %{tracker} at product backlog top"
label_add_task: "Opens a pop-up windows to add a %{tracker} to this PBI"
label_begin: "Begin"
label_blocked: "Blocked"
label_burndown: "%{name} burndown"
label_check_dependencies: "Check dependencies"
label_closed_sps: "Closed SPs"
label_closed_story_points: "Closed story points"
label_create_subtask: "Create subtask"
label_date_previous_to: "Previous to %{date}"
label_doer: "Doer"
label_done_effort: "Done effort"
label_edit_effort: "Edit effort"
label_edit_pbi: "Opens a pop-up window to edit the PBI"
label_edit_task: "Opens a pop-up window to edit the task"
label_end: "End"
label_estimated_effort: "Estimated effort"
label_estimated_effort_tooltip: "Estimated effort (%{date}): %{hours} h"
label_estimated_vs_done_effort: "Estimated effort vs done effort"
label_exit_fullscreen: "Exit fullscreen"
label_filter_by_assignee: "Filter by assignee"
label_filter_by_project: "Filter by project"
label_fullscreen: "Fullscreen"
label_hours_per_story_point: "Hours per story point"
label_invalid_dependencies: "There are invalid dependencies for the following PBIs:"
label_invalid_dependencies_for_pbi: "%{pbi} blocks but it comes after:"
label_issue_deviation: "This issue has a deviation of %{deviation}%"
label_issue_speed: "The speed of this issue is %{speed}%"
label_limited_to_n_series: "Project series have been limited to %{n}. Criteria: not closed projects, pending SPs."
label_media_last_n_sprints: "Last %{n} Sprints media"
label_menu_product_backlog: "Backlog"
label_menu_sprint: "Sprint"
label_move_not_closed_pbis_to_last_sprint: "Move not closed PBIs to latest Sprint"
label_move_pbi_after: "Move after"
label_move_pbi_before: "Move before"
label_move_pbi_to_last_sprint: "Move to last Sprint"
label_move_pbi_to_name: "Move to %{name}"
label_no_invalid_dependencies: "There're not invalid dependencies."
label_nothing_to_move: "Nothing to move."
label_only_total: "only total"
label_pbi_plural: "Product backlog items"
label_pbi_post_it: "Product backlog item post-it"
label_pbi_status_auto_updated_all_tasks_closed: "PBI status auto updated to %{pbi_status} because all tasks were closed"
label_pbi_status_auto_updated_all_tasks_new: "PBI status auto updated to %{pbi_status} because all tasks were on %{task_status} status"
label_pbi_status_auto_updated_one_task_no_new: "PBI status auto updated to %{pbi_status} because at least one task wasn't on %{task_status} status"
label_pbis_count: "Closed/total PBIs"
label_pbis_moved: "Moved PBIs: %{pbis}."
label_pending_effort_tooltip: "Pending effort (%{date}): %{hours} h"
label_pending_sps: "Pending SPs"
label_pending_sps_tooltip: "Pending SPs (%{date}): %{sps}"
label_pending_story_points: "Pending story points %{pending_story_points} sp (%{sprint}: %{story_points} sp)"
label_percentage_closed_sps: "Closed/total SPs %"
label_plugin_license: "Plugin license"
label_plugin_license_title: "Plugin license, please read carefully"
label_post_it: "Post-it"
label_product_backlog: "Product backlog"
label_product_backlog_burndown_chart: "Product backlog burndown"
label_product_backlog_new: "New product backlog"
label_product_backlog_plural: "Product backlogs"
label_release_plan: "Release plan"
label_release_plan_name: "%{name} release plan"
label_release_plan_stats: "A total of %{sps} SPs and %{pbis} PBIs (there are other %{pbis_without_sps} PBIs without estimation)"
label_remaining_story_point_plural: "Remaining story points"
label_remaining_story_point_unit: "rsp"
label_reviewer: "Reviewer"
label_scrum_stats: "Scrum stats"
label_setting_auto_update_pbi_status: "Auto update parent PBIs"
label_setting_auto_update_pbi_status_explanation: "Change parent PBI status based on child tasks status."
label_setting_blocked_custom_field: "Blocked custom field"
label_setting_check_dependencies_on_pbi_sorting: "Check PBIs dependencies on sorting"
label_setting_clear_new_tasks_assignee: "Clear assignee for tasks in new status"
label_setting_create_journal_on_pbi_position_change: "Create journal on a product backlog item position change"
label_setting_default_sprint_days: "Default first Sprint length"
label_setting_default_sprint_days_explanation: "Default first Sprint length in non working days (i.e. 5 for 1 week, 10 for 2 weeks...)"
label_setting_default_sprint_name: "Default first Sprint name"
label_setting_default_sprint_shared: "Default first Sprint shared"
label_setting_default_sprint_shared_explanation: "Share by default the first Sprint with child projects"
label_setting_fields_on_tracker: "Fields to use with %{tracker}"
label_setting_high_speed: "High speed percentage"
label_setting_inherit_pbi_attributes: "When creating a task inherit parent product backlog item attributes"
label_setting_low_speed: "Low speed percentage"
label_setting_lowest_speed: "Lowest speed percentage"
label_setting_pbi_is_closed_if_tasks_are_closed: "PBI closed if children are closed"
label_setting_pbi_is_closed_if_tasks_are_closed_explanation: "This does not change PBI status but affects Scrum plugin features (boards, burndowns...)."
label_setting_pbi_statuses: "PBI statuses for Product backlog & Sprint board"
label_setting_product_burndown_extra_sprints: "Render extra Sprints for subprojects"
label_setting_product_burndown_extra_sprints_explanation: "How many Sprints are rendered after tatol serie finishes (0 for unlimit)."
label_setting_product_burndown_sprints: "Calculate it with this number of Sprints"
label_setting_product_burndown_sprints_explanation: "Put 0 to use all past Sprints in the speed calculation."
label_setting_random_postit_rotation: "Random post-it rotation"
label_setting_remaining_story_points_custom_field: "Remaining story points custom field"
label_setting_render_author_on_pbi: "Render author"
label_setting_render_assigned_to_on_pbi: "Render assigned to"
label_setting_render_category_on_pbi: "Render category"
label_setting_render_pbis_speed: "Render PBIs speed"
label_setting_render_plugin_tips: "Render Scrum plugin tips"
label_setting_render_position_on_pbi: "Render position"
label_setting_render_tasks_speed: "Render tasks speed"
label_setting_render_updated_on_pbi: "Render update timestamp"
label_setting_render_version_on_pbi: "Render version"
label_setting_show_project_totals: "Show project totals"
label_setting_simple_pbi_custom_field: "Simple PBI custom field"
label_setting_sprint_board_fields_on_tracker: "Fields to render with %{tracker}"
label_setting_sprint_burndown_day_zero: "Day 0"
label_setting_sprint_burndown_day_zero_explanation: "Use 'Begin' as an special date at the beginning of the Sprint burndown instead of 'End'."
label_setting_story_points_custom_field: "Story points custom field"
label_setting_task_statuses: "Task statuses for Sprint board"
label_setting_update_pbi_status_if_all_tasks_are_closed: "If all tasks are closed update PBI status to"
label_setting_use_remaining_story_points: "Use remaining SPs in PBIs"
label_setting_use_remaining_story_points_explanation: "Update remaining SPs for PBIs day by day will calculate a more precise Sprint burndown by SPs."
label_setting_verification_activities: "Verification activities"
label_scrum: "Scrum"
label_sprint: "Sprint"
label_sprint_board: "Sprint board"
label_sprint_burndown_chart: "Sprint burndown"
label_sprint_burndown_chart_hours: "Sprint burndown (hours)"
label_sprint_burndown_chart_sps: "Sprint burndown (SPs)"
label_sprint_burndown_chart_name: "%{name} burndown"
label_sprint_new: "New Sprint"
label_sprint_plural: "Sprints"
label_sprint_stats: "Sprint stats"
label_sprint_stats_name: "%{name} stats"
label_sprint_status_closed: "Closed"
label_sprint_status_open: "Open"
label_sps_by_pbi_category: "SPs by PBI category"
label_sps_by_pbi_creation_date: "SPs by PBI creation date"
label_sps_by_pbi_type: "SPs by PBI type"
label_sps_count: "Closed/total SPs"
label_sps_stat: "(%{sps} sp - %{percentage}%)"
label_sps_total: "Total (%{sps} sp)"
label_story_point_plural: "Story points"
label_story_point_unit: "sp"
label_task_plural: "Tasks"
label_time_by_activity: "Time by activity"
label_time_by_member_and_activity: "Time by member and activity"
label_time_stat: "(%{time} h - %{percentage}%)"
label_time_total: "Total (%{time} h)"
label_tip_new_product_backlog_link: "Settings » Sprints » New product backlog"
label_tip_new_version_link: "Settings » Versions » New version"
label_tip_new_sprint_link: "Settings » Sprints » New Sprint"
label_tip_no_permissions: "Plugin permissions aren't configured, you can configure them in %{link}"
label_tip_no_product_backlogs: "There aren't product backlogs, you can create them in %{link}"
label_tip_no_sprints: "There aren't Sprints, you can create them in %{link}"
label_tip_no_plugin_setting: "“%{setting}” plugin setting isn't configured, you can configure it in %{link}"
label_tip_permissions_link: "Administration » Roles & permissions » Permissions report"
label_tip_plugin_settings_link: "Administration » Plugins » Scrum Redmine plugin"
label_tip_product_backlog_link: "Backlog » Product backlog"
label_tip_product_backlog_without_pbis: "Product backlog without PBIs, you can add them in %{link}"
label_tip_project_members_link: "Settings » Members"
label_tip_project_without_members: "Project without members, you can add them in %{link}"
label_tip_project_without_versions: "Project without versions for the release plan, you can add them in %{link}"
label_tip_sprint_board_link: "Sprint » Sprint board"
label_tip_sprint_effort_link: "Settings » Sprints » Edit effort"
label_tip_sprint_with_orphan_tasks: "Sprint with orphan tasks (no parent PBI) %{link}"
label_tip_sprint_without_efforts: "Sprint without estimated effort, you can add it in %{link}"
label_tip_sprint_without_pbis: "Sprint without PBIs, you can add them in %{sprint_board_link} or moving them to the Sprint in %{product_backlog_link}"
label_tip_sprint_without_tasks: "Sprint without tasks, you can add them in %{link}"
label_tip_title: "Scrum plugin tips"
label_total_effort: "Total effort (spent + pending)"
label_velocity_all_pbis:
zero: "Use all PBIs from past Sprints to calculate the velocity"
one: "Use all PBIs from last Sprint to calculate the velocity"
other: "Use all PBIs from last %{count} Sprints to calculate the velocity"
label_velocity_custom: "Custom value:"
label_velocity_only_scheduled_pbis: "Only scheduled ones"
label_velocity_only_scheduled_pbis_hint: "This excludes from calculus any PBI created once the Sprint has started"
notice_pbi_created: "The product backlog item was successfully created"
notice_sprint_has_issues: "The Sprint has issues"
notice_task_created: "The task was successfully created"
notice_unable_delete_sprint: "Fail to delete Sprint"
project_module_scrum: "Scrum"
permission_edit_pending_effort: "Edit pending effort"
permission_edit_product_backlog: "Edit product backlog"
permission_edit_remaining_story_points: "Edit remaining story points"
permission_edit_sprint_board: "Edit Sprint board"
permission_manage_sprints: "Manage Sprints"
permission_sort_product_backlog: "Sort product backlog"
permission_sort_sprint_board: "Sort Sprint board"
permission_view_pending_effort: "View pending effort"
permission_view_product_backlog: "View product backlog"
permission_view_product_backlog_burndown: "View product backlog burndown"
permission_view_release_plan: "View release plan"
permission_view_remaining_story_points: "View remaining story points"
permission_view_scrum_stats: "View scrum stats"
permission_view_sprint_board: "View Sprint board"
permission_view_sprint_burndown: "View Sprint burndown"
permission_view_sprint_stats: "View Sprint stats"
permission_view_sprint_stats_by_member: "View Sprint stats by member"
date:
formats:
scrum_day: "%a"
# encoding: UTF-8
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
es:
error_changing_pbi_order: "No se pudo reordenar el EPP, compruebe las dependencias. La página se recargará."
error_changing_task_status: "No se pudo cambiar el estado de la tarea."
error_changing_task_assigned_to: "No se pudo cambiar el asignado a de la tarea."
error_changing_value: "No se pudo cambiar el valor."
error_creating_pbi: "No se pudo crear el elemento de la pila de producto (%{message})."
error_creating_task: "No se pudo crear la tarea (%{message})."
error_creating_time_entry: "No se pudo crear la imputación de tiempo (%{message})."
error_new_status_no_allowed: "No está permitido pasar del estado ’%{status_from}’ a estado ’%{status_to}’"
error_no_product_backlog: "No hay pila de producto definida todavía, por favor pide al administrador de este proyecto que la cree en la solapa de configuración."
error_no_sprints: "No hay Sprints definidos todavía, por favor pide al administrador de este proyecto que los cree en la solapa de configuración."
error_sorting_other_issues_depends_on_issue: "Otros peticiones (%{others}) dependen de #%{id}, no se puede ordenar."
error_updating_pbi: "Error actualizando el EPP (%{message})"
error_updating_task: "Error actualizando la tarea (%{message})"
field_end_date: "Fecha de fin"
field_pending_effort: "Esfuerzo pendiente"
field_position: "Posición"
field_shared: "Compartido"
field_shared_note: "Un Sprint compartido es visible en los proyectos hijos, con lo que podrás asignar peticiones de los proyectos hijos a este Sprint."
field_sprint: "Sprint"
label_all_but_total: "todos excepto el total"
label_add_pbi_at_bottom: "Añadir %{tracker} al final de la pila de producto"
label_add_pbi_at_top: "Añadir %{tracker} al inicio de la pila de producto"
label_add_task: "Abre una ventana emergente para añadir un nuevo/a %{tracker} a este EPP"
label_begin: "Inicio"
label_blocked: "Bloqueado"
label_burndown: "Burndown de %{name}"
label_check_dependencies: "Comprobar dependencias"
label_closed_sps: "PHs cerrados"
label_closed_story_points: "Puntos de historia cerrados"
label_create_subtask: "Crear subtarea"
label_date_previous_to: "Previos a %{date}"
label_doer: "Hacedor"
label_done_effort: "Esfuerzo hecho"
label_edit_effort: "Editar esfuerzo"
label_edit_pbi: "Abre una ventana emergente para editar el EPP"
label_edit_task: "Abre una ventana emergente para editar la tarea"
label_end: "Fin"
label_estimated_effort: "Esfuerzo estimado"
label_estimated_effort_tooltip: "Esfuerzo estimado (%{date}): %{hours} h"
label_estimated_vs_done_effort: "Esfuerzo estimado vs esfuerzo hecho"
label_exit_fullscreen: "Pantalla normal"
label_filter_by_assignee: "Filtrar por asignado"
label_filter_by_project: "Filtrar por proyecto"
label_fullscreen: "Pantalla completa"
label_hours_per_story_point: "Horas por punto de historia"
label_invalid_dependencies: "Hay dependencias inválidas para los siguientes EPPs:"
label_invalid_dependencies_for_pbi: "%{pbi} bloquea pero es posterior a:"
label_issue_deviation: "Esta petición tiene una desviación del %{deviation}%"
label_issue_speed: "La velocidad de esta petición es %{speed}%"
label_limited_to_n_series: "La serie de proyectos se ha limitado a %{n}. Criterios: proyectos no cerrados, PHs pendientes."
label_media_last_n_sprints: "Media últimos %{n} Sprints"
label_menu_product_backlog: "Pila"
label_menu_sprint: "Sprint"
label_move_not_closed_pbis_to_last_sprint: "Mover EPPs no cerrados al último Sprint"
label_move_pbi_after: "Mover después de"
label_move_pbi_before: "Mover antes de"
label_move_pbi_to_last_sprint: "Mover al último Sprint"
label_move_pbi_to_name: "Mover a %{name}"
label_no_invalid_dependencies: "No hay dependencias inválidas."
label_nothing_to_move: "Nada que mover."
label_only_total: "sólo el total"
label_pbi_plural: "Elementos de la pila de producto"
label_pbi_post_it: "Post-it del elemento de la pila de producto"
label_pbi_status_auto_updated_all_tasks_closed: "Estado del EPP auto actualizado a %{pbi_status} porque todas las tareas estaban cerradas"
label_pbi_status_auto_updated_all_tasks_new: "Estado del EPP auto actualizado a %{pbi_status} porque todas las tareas estaban en estado %{task_status}"
label_pbi_status_auto_updated_one_task_no_new: "Estado del EPP auto actualizado a %{pbi_status} porque al menos una tarea no estaba en estado %{task_status}"
label_pbis_count: "EPPs cerrados/total"
label_pbis_moved: "EPPs movidos: %{pbis}."
label_pending_effort_tooltip: "Esfuerzo pendiente (%{date}): %{hours} h"
label_pending_sps: "PHs pendientes"
label_pending_sps_tooltip: "PHs pendientes (%{date}): %{sps}"
label_pending_story_points: "Puntos de historia pendientes %{pending_story_points} ph (%{sprint}: %{story_points} ph)"
label_percentage_closed_sps: "% de PHs cerrados/total"
label_plugin_license: "Licencia del plugin"
label_plugin_license_title: "Licencia del plugin, por favor lee con atención"
label_post_it: "Post-it"
label_product_backlog: "Pila de producto"
label_product_backlog_burndown_chart: "Burndown de la pila de producto"
label_product_backlog_new: "Nueva pila de producto"
label_product_backlog_plural: "Pilas de producto"
label_release_plan: "Plan de liberaciones"
label_release_plan_name: "Plan de liberaciones de %{name}"
label_release_plan_stats: "Un total de %{sps} PHs y %{pbis} EPPs (hay otros %{pbis_without_sps} EPPs sin estimación)"
label_remaining_story_point_plural: "Puntos de historia pendientes"
label_remaining_story_point_unit: "php"
label_reviewer: "Revisor"
label_scrum_stats: "Estadísticas de Scrum"
label_setting_auto_update_pbi_status: "Auto actualizar EPPs padres"
label_setting_auto_update_pbi_status_explanation: "Cambiar estado de los EPPs padres basado en el estado de las tareas hijas."
label_setting_blocked_custom_field: "Campo personalizado para bloqueo"
label_setting_check_dependencies_on_pbi_sorting: "Comprobar las dependencias de los EPPs al ordenar"
label_setting_clear_new_tasks_assignee: "Vaciar asignado a para tareas en estado nuevo"
label_setting_create_journal_on_pbi_position_change: "Crear histórico cuando cambie la posición de un elemento de la pila de producto"
label_setting_default_sprint_days: "Duración por defecto del primer Sprint"
label_setting_default_sprint_days_explanation: "Duración por defecto del primer Sprint en días no laborables (e.j. 5 para 1 una semana, 10 para 2 semanas...)"
label_setting_default_sprint_name: "Nombre por defecto del primer Sprint"
label_setting_default_sprint_shared: "Compartir por defecto el primer Sprint"
label_setting_default_sprint_shared_explanation: "Compartir por defecto el primer Sprint con los proyectos hijos"
label_setting_fields_on_tracker: "Campos a usar con %{tracker}"
label_setting_high_speed: "Porcentaje para velocidad alta"
label_setting_inherit_pbi_attributes: "Al crear una nueva tarea heredar atributos del elemento de la pila de producto padre"
label_setting_low_speed: "Porcentaje para velocidad baja"
label_setting_lowest_speed: "Porcentaje para velocidad muy baja"
label_setting_pbi_is_closed_if_tasks_are_closed: "EPP cerrado si los hijos están cerrados"
label_setting_pbi_is_closed_if_tasks_are_closed_explanation: "Esto no cambia el estado del EPP pero afecta la funcionalidad del plugin de Scrum (tableros, burndowns...)."
label_setting_pbi_statuses: "Estado de los EPPs para la Pila de producto y el tablero del Sprint"
label_setting_product_burndown_extra_sprints: "Mostrar Sprints extra para subproyectos"
label_setting_product_burndown_extra_sprints_explanation: "How many Sprints are rendered after tatol serie finishes (0 for unlimit)."
label_setting_product_burndown_sprints: "Calcularlo con este número de Sprints"
label_setting_product_burndown_sprints_explanation: "Pon 0 para usar todos los Sprints pasados para calcular la velocidad."
label_setting_random_postit_rotation: "Rotación aleatoria de los post-its"
label_setting_remaining_story_points_custom_field: "Campo personalizado para puntos de historia pendientes"
label_setting_render_author_on_pbi: "Mostrar autor"
label_setting_render_assigned_to_on_pbi: "Mostrar asignado a"
label_setting_render_category_on_pbi: "Mostrar categoría"
label_setting_render_pbis_speed: "Mostrar velocidad de los EPPs"
label_setting_render_plugin_tips: "Mostrar consejos del plugin de Scrum"
label_setting_render_position_on_pbi: "Mostrar posición"
label_setting_render_tasks_speed: "Mostrar velocidad de las tareas"
label_setting_render_updated_on_pbi: "Mostrar fecha de actualización"
label_setting_render_version_on_pbi: "Mostrar versión"
label_setting_show_project_totals: "Mostrar totales de proyecto"
label_setting_simple_pbi_custom_field: "Campo personalizado para EPPs simples"
label_setting_sprint_board_fields_on_tracker: "Campos a mostrar con %{tracker}"
label_setting_sprint_burndown_day_zero: "Día 0"
label_setting_sprint_burndown_day_zero_explanation: "Usa 'Inicio' como fecha especial al principio del Sprint en vez de 'Fin'."
label_setting_story_points_custom_field: "Campo personalizado para puntos de historia"
label_setting_task_statuses: "Estados de las tareas para el tablero del Sprint"
label_setting_update_pbi_status_if_all_tasks_are_closed: "Si todas las tareas están cerradas cambiar estado del EPP a"
label_setting_use_remaining_story_points: "Usar PHs pendientes para los EPPs"
label_setting_use_remaining_story_points_explanation: "Actualiza los PHs pendientes para los EPPs día a día calculará un burndown de Sprint por PHs más preciso."
label_setting_verification_activities: "Actividades de verificación"
label_scrum: "Scrum"
label_sprint: "Sprint"
label_sprint_board: "Tablero del Sprint"
label_sprint_burndown_chart: "Burndown del Sprint"
label_sprint_burndown_chart_hours: "Burndown del Sprint (horas)"
label_sprint_burndown_chart_sps: "Burndown del Sprint (PHs)"
label_sprint_burndown_chart_name: "Burndown del %{name}"
label_sprint_new: "Nuevo Sprint"
label_sprint_plural: "Sprints"
label_sprint_stats: "Estadísticas del Sprint"
label_sprint_stats_name: "Estadísticas del %{name}"
label_sprint_status_closed: "Cerrado"
label_sprint_status_open: "Abierto"
label_sps_by_pbi_category: "PHs por categoría de EPP"
label_sps_by_pbi_creation_date: "PHs por fecha de creación de EPP"
label_sps_by_pbi_type: "PHs por tipo de EPP"
label_sps_count: "PHs cerrados/total"
label_sps_stat: "(%{sps} ph - %{percentage}%)"
label_sps_total: "Total (%{sps} ph)"
label_story_point_plural: "Puntos de historia"
label_story_point_unit: "ph"
label_task_plural: "Tareas"
label_time_by_activity: "Tiempo por actividad"
label_time_by_member_and_activity: "Tiempo por miembro y actividad"
label_time_stat: "(%{time} h - %{percentage}%)"
label_time_total: "Total (%{time} h)"
label_tip_new_product_backlog_link: "Configuración » Sprints » Nueva pila de producto"
label_tip_new_version_link: "Configuración » Versiones » Nueva versión"
label_tip_new_sprint_link: "Configuración » Sprints » Nuevo Sprint"
label_tip_no_permissions: "Los permisos del plugin no están configurados, puedes configurarlos en %{link}"
label_tip_no_product_backlogs: "No hay pilas de producto, puedes crearlas en %{link}"
label_tip_no_sprints: "No hay Sprints, puedes crearlos en %{link}"
label_tip_no_plugin_setting: "El parámetro del plugin <i>“%{setting}”</i> no está configurado, puedes configurarlo en %{link}"
label_tip_permissions_link: "Administración » Roles y permisos » Informe de permisos"
label_tip_plugin_settings_link: "Administración » Extensiones » Scrum Redmine plugin"
label_tip_product_backlog_link: "Pila » Pila de producto"
label_tip_product_backlog_without_pbis: "Pila de producto sin EPPs, puedes añadirlos en %{link}"
label_tip_project_members_link: "Configuración » Miembros"
label_tip_project_without_members: "Proyecto sin miembros, puedes añadirlos en %{link}"
label_tip_project_without_versions: "Proyecto sin versiones para el plan de liberaciones, puedes crearlas en %{link}"
label_tip_sprint_board_link: "Sprint » Tablero del Sprint"
label_tip_sprint_effort_link: "Configuración » Sprints » Editar esfuerzo"
label_tip_sprint_with_orphan_tasks: "Sprint con tareas huérfanas (sin EPP padre) %{link}"
label_tip_sprint_without_efforts: "Sprint sin esfuerzo planificado, puedes añadirlo en %{link}"
label_tip_sprint_without_pbis: "Sprint sin EPPs, puedes añadirlos en %{sprint_board_link} o moviéndolos al Sprint en %{product_backlog_link}"
label_tip_sprint_without_tasks: "Sprint sin tareas, puedes añadirlas en %{link}"
label_tip_title: "Consejos del plugin de Scrum"
label_total_effort: "Esfuerzo total (gastado + pendiente)"
label_velocity_all_pbis:
zero: "Usar todos los EPPs de todos los Sprints pasados para calcular la velocidad"
one: "Usar todos los EPPs del último Sprint para calcular la velocidad"
other: "Usar todos los EPPs de los últimos %{count} Sprints para calcular la velocidad"
label_velocity_custom: "Valor personalizado:"
label_velocity_only_scheduled_pbis: "Sólo los planificados"
label_velocity_only_scheduled_pbis_hint: "Esto excluye del cálculo cualquier EPP creado una vez que el Sprint ha comenzado"
notice_pbi_created: "El elemento de la pila de producto fue creado con éxito"
notice_sprint_has_issues: "El Sprint tiene peticiones"
notice_task_created: "La tarea fue creada con éxito"
notice_unable_delete_sprint: "Fallo al borrar Sprint"
project_module_scrum: "Scrum"
permission_edit_pending_effort: "Editar esfuerzo pendiente"
permission_edit_product_backlog: "Editar la pila de producto"
permission_edit_remaining_story_points: "Editar puntos de historia pendientes"
permission_edit_sprint_board: "Editar tablero del Sprint"
permission_manage_sprints: "Administrar Sprints"
permission_sort_product_backlog: "Ordenar la pila de producto"
permission_sort_sprint_board: "Ordenar tablero del Sprint"
permission_view_pending_effort: "Ver esfuerzo pendiente"
permission_view_product_backlog: "Ver pila de producto"
permission_view_product_backlog_burndown: "Ver burndown de la pila de producto"
permission_view_release_plan: "Ver plan de liberaciones"
permission_view_remaining_story_points: "Ver puntos de historia pendientes"
permission_view_scrum_stats: "Ver estadísticas de Scrum"
permission_view_sprint_board: "Ver tablero del Sprint"
permission_view_sprint_burndown: "Ver burndown del Sprint"
permission_view_sprint_stats: "Ver estadísticas del Sprint"
permission_view_sprint_stats_by_member: "Ver estadísticas del Sprint por miembro"
date:
formats:
scrum_day: "%a"
# encoding: UTF-8
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
fr:
error_changing_pbi_order: "Impossible de changer l’ordre de l’élement, Vérifiez ses dépendances. La page va être rechargée."
error_changing_task_status: "Erreur de changement du statut de la täche."
error_changing_task_assigned_to: "Erreur de changement de l’assignement de la täche."
error_changing_value: "Echec du changement de valeur."
error_creating_pbi: "Erreur à la création de l’élement de Product Backlog (%{message})."
error_creating_task: "Erreur à la création de la tâche (%{message})."
error_creating_time_entry: "Erreur à la création d’une saisie de temps (%{message})."
error_new_status_no_allowed: "Il n’est pas autorisé de passer du statut ’%{status_from}’ au statut ’%{status_to}’"
error_no_product_backlog: "Il n’y a aucun Product Backlog de défini pour le moment, merci de solliciter un administrateur du projet pour en créer dans l’onglet configuration."
error_no_sprints: "Il n’y a aucun Sprint de défini pour le moment, merci de solliciter un administrateur du projet pour en créer dans l’onglet configuration."
error_sorting_other_issues_depends_on_issue: "D’autres demandes (%{others}) dépendent de #%{id}, elle ne peut être ordonnée."
error_updating_pbi: "Erreur à la mise à jour du PBI (%{message})"
error_updating_task: "Erreur à la mise à jour de la tâche (%{message})"
field_end_date: "Fin"
field_pending_effort: "Effort Restant"
field_position: "Position"
field_shared: "Partagé"
field_shared_note: "Un sprint partagé est visible avec les sous projets enfants, vous pourrez ainsi ajouter les demandes des sous prjets à ce sprint."
field_sprint: "Sprint"
label_all_but_total: "tou(te)s excepté le total"
label_add_pbi_at_bottom: "Ajouter %{tracker} en haut du product backlog"
label_add_pbi_at_top: "Ajouter %{tracker} en bas du product backlog"
label_add_task: "Ouvre une pop-up pour ajouter un %{tracker} à ce PBI"
label_begin: "Début"
label_blocked: "Bloquée"
label_burndown: "%{name} burndown"
label_check_dependencies: "Vérifier les dépendances"
label_closed_sps: "SPs Fermés"
label_closed_story_points: "Story Points fermés"
label_create_subtask: "Créer une sous-tâche"
label_date_previous_to: "Avant %{date}"
label_doer: "Réalisateur"
label_done_effort: "Effort réalisé"
label_edit_effort: "Modifier l’effort"
label_edit_pbi: "Ouvre une pop-up pour modifier le PBI (Product Backlog Item)"
label_edit_task: "Ouvre une pop-up pour modifier la tâche"
label_end: "Fin"
label_estimated_effort: "Effort estimé"
label_estimated_effort_tooltip: "Effort estimé (%{date}): %{hours} h"
label_estimated_vs_done_effort: "Effort estimé vs effort réalisé"
label_exit_fullscreen: "Sortir du Plein-écran"
label_filter_by_assignee: "Filtrer par assigné à"
label_filter_by_project: "Filtrer par project"
label_fullscreen: "Plein-écran"
label_hours_per_story_point: "Nombre d’heures par point"
label_invalid_dependencies: "Il existe des dépendances invalides pour les PBIs suivantes:"
label_invalid_dependencies_for_pbi: "%{pbi} est bloquante mais vient après:"
label_issue_deviation: "Cette demande a une déviation de %{deviation}%"
label_issue_speed: "La vélocité de cette demande est de %{speed}%"
label_limited_to_n_series: "Les series du projet ont été limitées à %{n}. Criteria: projets non-fermés, SPs restant."
label_media_last_n_sprints: "Moyenne des %{n} derniers Sprints"
label_menu_product_backlog: "Backlog"
label_menu_sprint: "Sprint"
label_move_not_closed_pbis_to_last_sprint: "Déplacer les PBIs non-fermés vers le dernier Sprint"
label_move_pbi_after: "Déplacer après"
label_move_pbi_before: "Déplacer avant"
label_move_pbi_to_last_sprint: "Déplacer dans le dernier Sprint"
label_move_pbi_to_name: "Déplacer dans %{name}"
label_no_invalid_dependencies: "Il n’y a aucune dépendance invalide."
label_nothing_to_move: "Rien à déplacer."
label_only_total: "seulement le total"
label_pbi_plural: "Items du Product Backlog"
label_pbi_post_it: "Product Backlog Item post-it"
label_pbi_status_auto_updated_all_tasks_closed: "Statut du PBI mis-à-jour automatiquement à %{pbi_status} parceque toutes les tâches étaient fermées"
label_pbi_status_auto_updated_all_tasks_new: "Statut du PBI mis-à-jour automatiquement à %{pbi_status} parceque toutes les tâches avaient le statut %{task_status}"
label_pbi_status_auto_updated_one_task_no_new: "Statut du PBI mis-à-jour automatiquement à %{pbi_status} parceque au moins une tâches n’avait pas le statut %{task_status}"
label_pbis_count: "PBIs Fermés/total"
label_pbis_moved: "Moved PBIs: %{pbis}."
label_pending_effort_tooltip: "Effort Restant (%{date}): %{hours} h"
label_pending_sps: "Pending SPs"
label_pending_sps_tooltip: "Pending SPs (%{date}): %{sps}"
label_pending_story_points: "Points restant à faire %{pending_story_points} sp (%{sprint}: %{story_points} sp)"
label_percentage_closed_sps: "(%) SPs Fermés/total"
label_plugin_license: "Licence du plugin"
label_plugin_license_title: "Licence du plugin, merci de lire attentivement"
label_post_it: "Post-it"
label_product_backlog: "Product Backlog"
label_product_backlog_burndown_chart: "Burdown du Product Backlog "
label_product_backlog_new: "Nouveau Product Backlog"
label_product_backlog_plural: "Product Backlogs"
label_release_plan: "Plan de Release"
label_release_plan_name: "Plan de Release %{name}"
label_release_plan_stats: "Un total de %{sps} SPs et %{pbis} éments (il y’a %{pbis_without_sps} élements sans estimation)"
label_remaining_story_point_plural: "Story Points Restant"
label_remaining_story_point_unit: "rsp"
label_reviewer: "Vérificateur"
label_scrum_stats: "Stats Scrum "
label_setting_auto_update_pbi_status: "Mise-À-Jour auto des PBIs parents"
label_setting_auto_update_pbi_status_explanation: "Changer le statut du PBI parent en fonction du statut des tâches enfant."
label_setting_blocked_custom_field: "Champ personnalisé ’Bloqué(e)’"
label_setting_check_dependencies_on_pbi_sorting: "Vérifier les dépendances lors du tri des élements du Product Backlog"
label_setting_clear_new_tasks_assignee: "Effacer l’assigné à pour les tâches au statut nouveau"
label_setting_create_journal_on_pbi_position_change: "Créer un historique au changement de position d’un élement dans le Product Backlog"
label_setting_fields_on_tracker: "Champs à utiliser avec %{tracker}"
label_setting_high_speed: "Pourcentage haute vitesse"
label_setting_inherit_pbi_attributes: "Lors de la création d’une tâche, hériter les attributs du Product Backlog Item"
label_setting_low_speed: "Pourcentage faible vitesse"
label_setting_lowest_speed: "Plus faible pourcentage de vitesse"
label_setting_pbi_is_closed_if_tasks_are_closed: "PBI fermé si les enfants sont fermés"
label_setting_pbi_is_closed_if_tasks_are_closed_explanation: "Cela ne change pas le statut du PBI mais impacte des fonctionnalités du plugin Scrum (tableaux de bord, Burndowns...)."
label_setting_pbi_statuses: "Statuts des PBIs pour Product Backlog & Tableau de bord du Sprint"
label_setting_product_burndown_extra_sprints: "Afficher les Sprints supplémentaires pour les sous-projects"
label_setting_product_burndown_extra_sprints_explanation: "Combien de Sprints sont affichés après que la série totalle finisse (0 pour pas de limite)."
label_setting_product_burndown_sprints: "La calculer avec ce nombre de Sprints"
label_setting_product_burndown_sprints_explanation: "Mettre 0 pour utiliser tous les Sprints passés dans le calcul de la vitesse."
label_setting_random_postit_rotation: "Rotation aléatoire des post-its"
label_setting_remaining_story_points_custom_field: "Champ personnalisé Story points restant"
label_setting_render_author_on_pbi: "Afficher l’auteur"
label_setting_render_assigned_to_on_pbi: "Afficher l’assigné à"
label_setting_render_category_on_pbi: "Afficher la catégorie"
label_setting_render_pbis_speed: "Afficher la vélocité des PBIs"
label_setting_render_plugin_tips: "Afficher les alertes du plugin Scrum"
label_setting_render_position_on_pbi: "Afficher la position"
label_setting_render_tasks_speed: "Afficher la vélocité des tâches"
label_setting_render_updated_on_pbi: "Afficher le timestamp de mise à jour"
label_setting_render_version_on_pbi: "Afficher la version"
label_setting_show_project_totals: "Afficher les totaux du projet"
label_setting_simple_pbi_custom_field: "Champ personnalisé PBI simple"
label_setting_sprint_board_fields_on_tracker: "Champs à afficher pour %{tracker}"
label_setting_sprint_burndown_day_zero: "Jour 0"
label_setting_sprint_burndown_day_zero_explanation: "Utiliser ’Début’ comme une date spéciale au commencement du Sprint burndown à la place de ’Fin’."
label_setting_story_points_custom_field: "Champ personnalisé Story points (Points Scrum)"
label_setting_task_statuses: "Statuts des tâches pour le Tableau de bord du Sprint (Sprint board)"
label_setting_update_pbi_status_if_all_tasks_are_closed: "Si toutes les tâches sont fermées mettre à jour le statut du PBI"
label_setting_use_remaining_story_points: "Utiliser les SPs restant dans les PBIs"
label_setting_use_remaining_story_points_explanation: "Mettre à jour les SPs restant pour les PBIs au jour le jour permettra de calculer un Sprint burndown par SPs plus précis."
label_setting_verification_activities: "Vérification des activités"
label_scrum: "Scrum"
label_sprint: "Sprint"
label_sprint_board: "Tableau de bord du Sprint"
label_sprint_burndown_chart: "Sprint burndown"
label_sprint_burndown_chart_hours: "Sprint Burndown (heures)"
label_sprint_burndown_chart_sps: "Sprint Burndown (SPs)"
label_sprint_burndown_chart_name: "Burndown {name}"
label_sprint_new: "Nouveau Sprint"
label_sprint_plural: "Sprints"
label_sprint_stats: "Stats du Sprint "
label_sprint_stats_name: "Stats %{name}"
label_sprint_status_closed: "Fermé"
label_sprint_status_open: "Ouvert"
label_sps_by_pbi_category: "SPs par catégorie des élements"
label_sps_by_pbi_creation_date: "SPs par date de création des élements"
label_sps_by_pbi_type: "SPs par type de PBI"
label_sps_count: "SPs Fermés/total"
label_sps_stat: "(%{sps} sp - %{percentage}%)"
label_sps_total: "Total (%{sps} sp)"
label_story_point_plural: "Story points"
label_story_point_unit: "sp"
label_task_plural: "Tâches"
label_time_by_activity: "Temps par activité"
label_time_by_member_and_activity: "Temps par membre et activité"
label_time_stat: "(%{time} h - %{percentage}%)"
label_time_total: "Total (%{time} h)"
label_tip_new_product_backlog_link: "Configuration » Sprints » Nouveau Product Backlog"
label_tip_new_version_link: "Configuration » Versions » Nouvelle version"
label_tip_new_sprint_link: "Configuration » Sprints » Nouveau Sprint"
label_tip_no_permissions: "Les permissions du plugin ne sont pas définies, vous pouvez les configurer dans %{link}"
label_tip_no_product_backlogs: "Il n’existe pas de Product Backlog, vous pouvez en créer un dans %{link}"
label_tip_no_sprints: "Il n’existe aucun sprint, vous pouvez en créér dans %{link}"
label_tip_no_plugin_setting: "Le paramètre “%{setting}” n’est pas défini, vous pouvez le configurer dans %{link}"
label_tip_permissions_link: "Administration » Roles & permissions » Permissions report"
label_tip_plugin_settings_link: "Administration » Plugins » Plugin Redmine Scrum"
label_tip_product_backlog_link: "Backlog » Product Backlog"
label_tip_product_backlog_without_pbis: "Product Backlog sans élement, vous pouvez en ajouter dans %{link}"
label_tip_project_members_link: "Configuration » Membres"
label_tip_project_without_members: "Projet sans membre, vous pouvez en ajouter dans %{link}"
label_tip_project_without_versions: "Projet sans version pour le plan de Release, vous pouvez en ajouter dans %{link}"
label_tip_sprint_board_link: "Sprint » Tableau de bord du sprint"
label_tip_sprint_effort_link: "Configuration » Sprints » Modifier l’effort"
label_tip_sprint_with_orphan_tasks: "Sprint avec des tâches orphelines (pas de PBI parent) %{link}"
label_tip_sprint_without_efforts: "Sprint sans effort estimé, vous pouvez l’ajouter dans %{link}"
label_tip_sprint_without_pbis: "Sprint sans élement, vous pouvez en ajouter dans %{sprint_board_link} ou les déplacer dans le Sprint via %{product_backlog_link}"
label_tip_sprint_without_tasks: "Sprint sans tâche, vous pouvez en ajouter dans %{link}"
label_tip_title: "Alertes du plugin Scrum"
label_total_effort: "Effort total (passé + restant)"
label_velocity_all_pbis:
zero: "Utiliser tous les PBIs des Sprints passés pour calculer la vélocité"
one: "Utiliser tous les élements du PB du dernier Sprint pour calculer la vélocité"
other: "Utiliser tous les élements du PB depuis les %{count} derniers Sprints pour calculer la vélocité"
label_velocity_custom: "Valeur personnalisée:"
label_velocity_only_scheduled_pbis: "Uniquement ceux planifiés"
label_velocity_only_scheduled_pbis_hint: "Cela exclut du calcul tout PBI créé après que le Sprint ait commencé"
notice_pbi_created: "L’élement du product backlog a été créé avec succès"
notice_sprint_has_issues: "Le sprint possède des demandes"
notice_task_created: "La tâche a été créée avec succès"
notice_unable_delete_sprint: "Echec de la suppression du Sprint"
project_module_scrum: "Scrum"
permission_edit_pending_effort: "Éditer pending effort"
permission_edit_product_backlog: "Modifier le Product Backlog"
permission_edit_remaining_story_points: "Éditer les story points restant"
permission_edit_sprint_board: "Modifier le tableau de board du Sprint"
permission_manage_sprints: "Configurer les Sprints"
permission_sort_product_backlog: "Ordonner le Product Backlog"
permission_sort_sprint_board: "Ordonner le tableau de board du Sprint"
permission_view_pending_effort: "Voir pending effort"
permission_view_product_backlog: "Voir le Product Backlog"
permission_view_product_backlog_burndown: "Voir le burndown chart du Product Backlog"
permission_view_release_plan: "Voir le plan de Release"
permission_view_remaining_story_points: "Voir les story points restant"
permission_view_scrum_stats: "Voir les stats scrum"
permission_view_sprint_board: "Voir le tableau de bord Sprint"
permission_view_sprint_burndown: "Voir le burndown chart du Sprint"
permission_view_sprint_stats: "Voir les stats du Sprint"
permission_view_sprint_stats_by_member: "Voir les stats du Sprint par membre"
date:
formats:
scrum_day: "%a"
# encoding: UTF-8
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
zh:
error_changing_pbi_order: "无法排序 PBIs,请检查对应的依赖关系。当前页将被重载。"
error_changing_task_status: "不能更新任务状态。"
error_changing_task_assigned_to: "不能更新任务执行人。"
error_changing_value: "不能更新值。"
error_creating_pbi: "不能新建PBI(%{message})。"
error_creating_task: "不能新建任务(%{message})。"
error_creating_time_entry: "不能新建工时(%{message})。"
error_no_product_backlog: "当前尚不存在 Product Backlog,请联系项目管理员通过设置页创建 Product Backlog。"
error_no_sprints: "当前尚未定义 Sprints,请联系系统管理员通过设置页创建 Sprints。"
error_updating_pbi: "更新PBI报错:(%{message})"
error_updating_task: "更新任务报错:(%{message})"
field_end_date: "结束日期"
field_pending_effort: "预计耗时"
field_position: "位置"
field_shared: "已共享"
field_shared_note: "已共享的 Sprint 可在子项目中被检索到,您也可以将子项目中的问题直接派发到已分享的 Sprint 中。"
field_sprint: "Sprint"
label_add_task: "打开弹窗给当前PBI添加一个跟踪:%{tracker} "
label_blocked: "已锁定"
label_burndown: "%{name} 燃尽图"
label_check_dependencies: "检查依赖"
label_create_subtask: "新建子任务"
label_date_previous_to: "%{date}之前"
label_doer: "执行人"
label_done_effort: "已记录耗时"
label_edit_effort: "编辑耗时"
label_edit_pbi: "编辑PBI时,打开弹出窗口"
label_edit_task: "编辑任务时,打开弹出窗口"
label_end: "End"
label_estimated_effort: "预估工作量"
label_estimated_effort_tooltip: "预估工作量 (%{date})天 %{hours} 小时"
label_estimated_vs_done_effort: "预估工作量 VS 已完成工作量"
label_filter_by_assignee: "按分配对象过滤"
label_hours_per_story_point: "耗时(小时)/ SP"
label_invalid_dependencies: "以下 PBIs 不存在有效的依赖关系:"
label_invalid_dependencies_for_pbi: "%{pbi} 已锁定, but it comes after:"
label_issue_deviation: "当前问题偏向于:%{deviation}%"
label_issue_speed: "当前问题的速度为 %{speed}%"
label_media_last_n_sprints: " %{n} Sprints 的中位数"
label_menu_product_backlog: "Backlog"
label_menu_sprint: "Sprint"
label_move_pbi_after: "移动到...之后"
label_move_pbi_before: "移动到...之前"
label_move_pbi_to_last_sprint: "移动到最近一个 Sprint"
label_move_pbi_to_name: "移动到 %{name}"
label_no_invalid_dependencies: "当前无有效的依赖关系。"
label_pbi_plural: "Product Backlog Items(PBIs)"
label_pbi_post_it: "PBI 便利贴"
label_pending_effort_tooltip: "预计耗时:(%{date})天 - %{hours}小时)"
label_pending_sps: "待定 SPs"
label_pending_sps_tooltip: "待定 SPs (%{date}): %{sps}"
label_pending_story_points: "待定故事点 %{pending_story_points} SP (%{sprint}: %{story_points} SP)"
label_plugin_license: "插件许可证"
label_plugin_license_title: "插件许可证,请务必仔细阅读。"
label_post_it: "便利贴"
label_product_backlog: "产品 Backlog "
label_product_backlog_burndown_chart: "产品 Backlog 燃尽图"
label_product_backlog_new: "新建产品 Backlog"
label_release_plan: "发布计划"
label_release_plan_name: "%{name} 发布计划"
label_release_plan_stats: "总计: %{sps} SPs / %{pbis} PBIs (目前仍有 %{pbis_without_sps} PBIs 未进行预估)"
label_reviewer: "评审人"
label_scrum_stats: "Scrum 统计"
label_setting_auto_update_pbi_status: "自动更新上级 PBIs"
label_setting_auto_update_pbi_status_explanation: "基于子任务状态,更新父任务(PBI)装填"
label_setting_blocked_custom_field: "锁定的自定义字段"
label_setting_check_dependencies_on_pbi_sorting: "在排序时检查 PBIs 的依赖情况"
label_setting_clear_new_tasks_assignee: "在新建状态时清除任务的指派对象"
label_setting_create_journal_on_pbi_position_change: " PBI 的位置变更时创建日志"
label_setting_fields_on_tracker: "用于在跟踪:%{tracker} 中的字段"
label_setting_high_speed: "高速率临界值"
label_setting_inherit_pbi_attributes: "当创建新任务时继承父 PBI 的相关属性值"
label_setting_low_speed: "低速率临界值"
label_setting_lowest_speed: "极低速率临界值"
label_setting_pbi_statuses: "用于 Product Backlog Sprint 看板的 PBI 状态"
label_setting_product_burndown_sprints: "用如下数量的 Sprints 计算燃尽速率"
label_setting_random_postit_rotation: "便利贴随机回转动画"
label_setting_render_author_on_pbi: "显示作者"
label_setting_render_category_on_pbi: "显示类别"
label_setting_render_pbis_speed: "显示 PBIs 速度"
label_setting_render_plugin_tips: "显示 Scrum 插件提示"
label_setting_render_position_on_pbi: "显示 PBI 位置"
label_setting_render_tasks_speed: "显示任务速度"
label_setting_render_updated_on_pbi: "显示更新时间戳"
label_setting_render_version_on_pbi: "显示目标版本"
label_setting_sprint_board_fields_on_tracker: "显示在跟踪:%{tracker} 中使用的字段"
label_setting_story_points_custom_field: "SP 自定义字段"
label_setting_task_statuses: "Sprint 看板的任务状态"
label_setting_verification_activities: "验证活动"
label_scrum: "Scrum"
label_sprint: "Sprint"
label_sprint_board: "Sprint 看板"
label_sprint_burndown_chart: "Sprint 燃尽图"
label_sprint_burndown_chart_hours: "Sprint 燃尽图(按工时)"
label_sprint_burndown_chart_sps: "Sprint 燃尽图(按 SPs)"
label_sprint_burndown_chart_name: "%{name} 燃尽图"
label_sprint_new: "新建 Sprint"
label_sprint_plural: "Sprints"
label_sprint_stats: "Sprint 统计"
label_sprint_stats_name: "%{name} 统计"
label_sprint_status_closed: "已关闭"
label_sprint_status_open: "进行中"
label_sps_by_pbi_category: "SPs(按 PBI 对应的类别)"
label_sps_by_pbi_creation_date: "SPs(按 PBI 对应的创建日期)"
label_sps_by_pbi_type: "SPs(按 PBI 对应的跟踪类型)"
label_sps_stat: "(%{sps} SP - %{percentage}%)"
label_sps_total: "总计(%{sps} SP)"
label_story_point_plural: "故事点"
label_story_point_unit: "SP"
label_task_plural: "任务"
label_time_by_activity: "工时(按活动)"
label_time_by_member_and_activity: "工时(按成员及其活动)"
label_time_stat: "(%{time} 小时 - %{percentage}%)"
label_time_total: "总计(%{time} 小时)"
label_tip_new_product_backlog_link: "设置 » Sprints » 新建产品 Backlog"
label_tip_new_version_link: "设置 » 版本 » 新建版本"
label_tip_new_sprint_link: "设置 » Sprints » 新建 Sprint"
label_tip_no_permissions: "插件权限未配置。请在使用前,先通过如下链接配置相关权限:%{link} "
label_tip_no_product_backlogs: "当前不存在 Product Backlogs,请通过如下链接添加: %{link} "
label_tip_no_sprints: "当前不存在 Sprints,请先通过 %{link} 创建。"
label_tip_no_plugin_setting: "“%{setting}” 插件未配置,请通过如下链接设置:%{link} "
label_tip_permissions_link: "管理 » 角色和权限 » 权限报表"
label_tip_plugin_settings_link: "管理 » 插件 » Scrum Redmine 插件"
label_tip_product_backlog_link: "Backlog » 产品 Backlog"
label_tip_product_backlog_without_pbis: "Product backlog 中不包含 PBIs,请通过如下链接添加:%{link} "
label_tip_project_members_link: "设置 » 成员"
label_tip_project_without_members: "项目不包含成员,请通过如下链接添加:%{link} "
label_tip_project_without_versions: "在项目发布计划中不含版本,请通过如下链接添加:%{link} "
label_tip_sprint_board_link: "Sprint » Sprint 看板"
label_tip_sprint_effort_link: "设置 » Sprints » 编辑耗时"
label_tip_sprint_with_orphan_tasks: "含独立任务(无上级 PBI)的 Sprint %{link} "
label_tip_sprint_without_efforts: "Sprint 中不含预估工作量,请通过如下链接配置:%{link} "
label_tip_sprint_without_pbis: "Sprint 未包含PBIs,请通过Sprint看板: %{sprint_board_link} 添加;或在PBI列表:%{product_backlog_link} 中将对应的PBIs移动到Sprint。"
label_tip_sprint_without_tasks: "Sprint未包含具体任务,请通过如下链接添加:%{link} "
label_tip_title: "Scrum 插件提示"
label_total_effort: "总计工作量(已用 + 待定)"
label_velocity_all_pbis:
one: "从上次的 Sprint 中的 PBIs 推算速率"
other: "从上 %{count} 次的 Sprints 中的 PBIs 推算速率"
label_velocity_custom: "自定义值:"
label_velocity_only_scheduled_pbis: "仅限在 Sprint 计划中提前纳入的 PBIs"
label_velocity_only_scheduled_pbis_hint: " Sprint 开始之后所创建的 PBI 不纳入计算范围"
notice_pbi_created: "PBI 已创建。"
notice_sprint_has_issues: "Sprint 已包含任务。"
notice_task_created: "任务已创建。"
notice_unable_delete_sprint: "不能删除 Sprint。"
project_module_scrum: "Scrum"
permission_edit_product_backlog: "编辑产品 Backlog"
permission_edit_sprint_board: "编辑 Sprint 看板"
permission_manage_sprints: "管理 Sprints"
permission_sort_product_backlog: "排序产品 Backlog "
permission_sort_sprint_board: "排序Sprint看板"
permission_view_product_backlog: "查看产品 Backlog "
permission_view_product_backlog_burndown: "查看产品 Backlog 燃尽图"
permission_view_release_plan: "查看发布计划"
permission_view_scrum_stats: "查看 Scrum 统计"
permission_view_sprint_board: "查看 Sprint 看板"
permission_view_sprint_burndown: "查看 Sprint 燃尽图"
permission_view_sprint_stats: "查看 Sprint 统计"
permission_view_sprint_stats_by_member: "按成员查看 Sprint 统计"
date:
formats:
scrum_day: "%a"
# Plugin's routes
# See: http://guides.rubyonrails.org/routing.html
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
resources :projects do
resources :sprints, :shallow => true do
member do
get :edit_effort
post :update_effort
get :burndown
get :burndown_graph
get :stats
post :sort
end
collection do
get :burndown_index
get :stats_index
end
end
post "sprints/change_issue_status",
:controller => :sprints, :action => :change_issue_status,
:as => :sprints_change_issue_status
resources :product_backlog, :shallow => true do
member do
post :sort
post :create_pbi
get :burndown
get :burndown_graph
get :check_dependencies
get :release_plan
end
end
get "product_backlog/new_pbi/:tracker_id",
:controller => :product_backlog, :action => :new_pbi,
:as => :product_backlog_new_pbi
get "scrum/stats",
:controller => :scrum, :action => :stats,
:as => :scrum_stats
end
post "issues/:id/story_points",
:controller => :scrum, :action => :change_story_points,
:as => :change_story_points
post "issues/:id/remaining_story_points",
:controller => :scrum, :action => :change_remaining_story_points,
:as => :change_remaining_story_points
post "issues/:id/pending_effort",
:controller => :scrum, :action => :change_pending_effort,
:as => :change_pending_effort
post "issues/:id/pending_efforts",
:controller => :scrum, :action => :change_pending_efforts,
:as => :change_pending_efforts
post "issues/:id/assigned_to",
:controller => :scrum, :action => :change_assigned_to,
:as => :change_assigned_to
get "issues/:id/time_entry",
:controller => :scrum, :action => :new_time_entry,
:as => :new_scrum_time_entry
post "issues/:id/time_entry",
:controller => :scrum, :action => :create_time_entry,
:as => :create_scrum_time_entry
get "scrum/:sprint_id/new_pbi/:tracker_id",
:controller => :scrum, :action => :new_pbi,
:as => :new_pbi
post "scrum/:sprint_id/create_pbi",
:controller => :scrum, :action => :create_pbi,
:as => :create_pbi
get "scrum/:pbi_id/new/:tracker_id",
:controller => :scrum, :action => :new_task,
:as => :new_task
post "scrum/:pbi_id/create_task",
:controller => :scrum, :action => :create_task,
:as => :create_task
get "scrum/:pbi_id/edit_pbi",
:controller => :scrum, :action => :edit_pbi,
:as => :edit_pbi
post "scrum/:pbi_id/update_pbi",
:controller => :scrum, :action => :update_pbi,
:as => :update_pbi
get "scrum/:pbi_id/move/:position",
:controller => :scrum, :action => :move_pbi,
:as => :move_pbi
get "scrum/:id/edit_task",
:controller => :scrum, :action => :edit_task,
:as => :edit_task
post "scrum/:id/update_task",
:controller => :scrum, :action => :update_task,
:as => :update_task
post "scrum/:pbi_id/move_to_last_sprint",
:controller => :scrum, :action => :move_to_last_sprint,
:as => :move_to_last_sprint
post "scrum/:sprint_id/move_not_closed_pbis_to_last_sprint",
:controller => :scrum, :action => :move_not_closed_pbis_to_last_sprint,
:as => :move_not_closed_pbis_to_last_sprint
post "scrum/:pbi_id/move_to_product_backlog",
:controller => :scrum, :action => :move_to_product_backlog,
:as => :move_to_product_backlog
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
class CreateSprints < ActiveRecord::Migration[4.2]
def self.up
create_table :sprints, :force => true do |t|
t.column :name, :string, :null => false
t.column :description, :text
t.column :start_date, :date, :null => false
t.column :end_date, :date, :null => false
t.column :user_id, :integer, :null => false
t.column :project_id, :integer, :null => false
t.column :created_on, :datetime
t.column :updated_on, :datetime
end
add_index :sprints, [:name], :name => "sprints_name"
add_index :sprints, [:user_id], :name => "sprints_user"
add_index :sprints, [:project_id], :name => "sprints_project"
end
def self.down
drop_table :sprints
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
class AddIssuesSprintId < ActiveRecord::Migration[4.2]
def self.up
add_column :issues, :sprint_id, :integer
add_index :issues, [:sprint_id], :name => "issues_sprint_id"
end
def self.down
remove_column :issues, :sprint_id
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
class AddIssuesPosition < ActiveRecord::Migration[4.2]
def self.up
add_column :issues, :position, :integer
add_index :issues, [:position], :name => "issues_position"
end
def self.down
remove_column :issues, :position
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
class AddProjectsProductBacklogId < ActiveRecord::Migration[4.2]
def self.up
add_column :projects, :product_backlog_id, :integer
add_index :projects, [:product_backlog_id], :name => "projects_product_backlog_id"
end
def self.down
remove_column :projects, :product_backlog_id
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
class AddSprintsIsProductBacklog < ActiveRecord::Migration[4.2]
class Sprint < ActiveRecord::Base
belongs_to :project
end
class Project < ActiveRecord::Base
belongs_to :product_backlog, :class_name => "Sprint"
has_many :sprints
end
def self.up
add_column :sprints, :is_product_backlog, :boolean, :default => false
add_index :sprints, [:is_product_backlog], :name => "sprints_is_product_backlog"
Sprint.all.each do |sprint|
is_product_backlog = (!(sprint.project.nil?))
is_product_backlog &&= (!(sprint.project.product_backlog.nil?))
is_product_backlog &&= (sprint.project.product_backlog == sprint)
sprint.is_product_backlog = is_product_backlog
sprint.save!
end
end
def self.down
remove_column :sprints, :is_product_backlog
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
class CreateSprintEfforts < ActiveRecord::Migration[4.2]
def self.up
create_table :sprint_efforts, :force => true do |t|
t.column :sprint_id, :integer, :null => false
t.column :user_id, :integer, :null => false
t.column :date, :date, :null => false
t.column :effort, :integer
end
add_index :sprint_efforts, [:sprint_id], :name => "sprint_efforts_sprint"
add_index :sprint_efforts, [:user_id], :name => "sprint_efforts_user"
add_index :sprint_efforts, [:date], :name => "sprint_efforts_date"
end
def self.down
drop_table :sprint_efforts
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
class CreatePendingEfforts < ActiveRecord::Migration[4.2]
class Issue < ActiveRecord::Base
end
class CustomValue < ActiveRecord::Base
end
class PendingEffort < ActiveRecord::Base
end
def self.up
create_table :pending_efforts, :force => true do |t|
t.column :issue_id, :integer, :null => false
t.column :date, :date, :null => false
t.column :effort, :integer
end
add_index :pending_efforts, [:issue_id], :name => "pending_efforts_issue"
add_index :pending_efforts, [:date], :name => "pending_efforts_date"
if !((custom_field_id = Setting.plugin_scrum[:pending_effort_custom_field]).nil?)
Issue.all.each do |issue|
values = CustomValue.where(:customized_type => "Issue",
:customized_id => issue.id,
:custom_field_id => custom_field_id)
if values.count == 1
effort = PendingEffort.new(:issue_id => issue.id,
:date => issue.updated_on,
:effort => values.first.value.to_i)
effort.save!
end
end
end
end
def self.down
drop_table :pending_efforts
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
class ChangePendingEffortsEffort < ActiveRecord::Migration[4.2]
def self.up
change_column :pending_efforts, :effort, :float
end
def self.down
change_column :pending_efforts, :effort, :integer
end
end
\ No newline at end of file
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
class ChangeSprintsDates < ActiveRecord::Migration[4.2]
def self.up
rename_column :sprints, :start_date, :sprint_start_date
rename_column :sprints, :end_date, :sprint_end_date
end
def self.down
rename_column :sprints, :sprint_start_date, :start_date
rename_column :sprints, :sprint_end_date, :end_date
end
end
\ No newline at end of file
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
class ChangeSprintEffortsEffort < ActiveRecord::Migration[4.2]
def self.up
change_column :sprint_efforts, :effort, :float
end
def self.down
change_column :sprint_efforts, :effort, :integer
end
end
\ No newline at end of file
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
class RenameDeviationToSpeedSettings < ActiveRecord::Migration[4.2]
def self.up
change_speed_settings render_pbis_deviations: :render_pbis_speed,
render_tasks_deviations: :render_tasks_speed,
major_deviation_ratio: :lowest_speed,
minor_deviation_ratio: :low_speed,
below_deviation_ratio: :high_speed
end
def self.down
change_speed_settings render_pbis_speed: :render_pbis_deviations,
render_tasks_speed: :render_tasks_deviations,
lowest_speed: :major_deviation_ratio,
low_speed: :minor_deviation_ratio,
high_speed: :below_deviation_ratio
end
private
def self.change_speed_settings(settings)
if (plugin_settings = Setting.where(name: 'plugin_scrum').first)
if (values = plugin_settings.value)
settings.each_pair { |key, value|
change_speed_setting(values, key, value)
}
plugin_settings.value = values
plugin_settings.save!
end
end
end
def self.change_speed_setting(settings, old_setting, new_setting)
if settings[old_setting]
old_setting_value = settings[old_setting].to_i
if old_setting_value > 0
settings[new_setting] = (10000 / old_setting_value).to_s
settings.delete(old_setting)
end
end
end
end
\ No newline at end of file
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
class AddSprintsStatus < ActiveRecord::Migration[4.2]
def self.up
add_column :sprints, :status, :string, :limit => 10, :default => "open"
add_index :sprints, [:status], :name => "sprints_status"
end
def self.down
remove_column :sprints, :status
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
class AddSprintsShared < ActiveRecord::Migration[4.2]
def self.up
add_column :sprints, :shared, :boolean, :default => false
add_index :sprints, [:shared], :name => "sprints_shared"
end
def self.down
remove_column :sprints, :shared
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
class UpdateSprintsDateConstraints < ActiveRecord::Migration[4.2]
def self.up
change_column :sprints, :sprint_start_date, :date, :null => true
change_column :sprints, :sprint_end_date, :date, :null => true
Sprint.where(:is_product_backlog => true).update_all(:sprint_start_date => nil, :sprint_end_date => nil)
end
def self.down
Sprint.where(:sprint_start_date => nil).update_all(:sprint_start_date => Time.now)
Sprint.where(:sprint_end_date => nil).update_all(:sprint_end_date => Time.now)
change_column :sprints, :sprint_start_date, :date, :null => false
change_column :sprints, :sprint_end_date, :date, :null => false
end
end
# encoding: UTF-8
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
# This plugin should be reloaded in development mode.
if (Rails.env == 'development')
ActiveSupport::Dependencies.autoload_once_paths.reject!{|x| x =~ /^#{Regexp.escape(File.dirname(__FILE__))}/}
end
ApplicationHelper.send(:include, Scrum::ApplicationHelperPatch)
CalendarsController.send(:include, Scrum::CalendarsControllerPatch)
Issue.send(:include, Scrum::IssuePatch)
IssueQuery.send(:include, Scrum::IssueQueryPatch)
IssuesController.send(:include, Scrum::IssuesControllerPatch)
IssueStatus.send(:include, Scrum::IssueStatusPatch)
Journal.send(:include, Scrum::JournalPatch)
Project.send(:include, Scrum::ProjectPatch)
ProjectsHelper.send(:include, Scrum::ProjectsHelperPatch)
Query.send(:include, Scrum::QueryPatch)
Tracker.send(:include, Scrum::TrackerPatch)
User.send(:include, Scrum::UserPatch)
require_dependency 'scrum/helper_hooks'
require_dependency 'scrum/view_hooks'
Redmine::Plugin.register :scrum do
name 'Scrum Redmine plugin'
author 'Emilio González Montaña'
description 'This plugin for Redmine allows to follow Scrum methodology with Redmine projects'
version '0.20.0'
url 'https://redmine.ociotec.com/projects/redmine-plugin-scrum'
author_url 'http://ociotec.com'
requires_redmine :version_or_higher => '4.0.0'
project_module :scrum do
permission :manage_sprints,
{:sprints => [:new, :create, :edit, :update, :destroy, :edit_effort, :update_effort]},
:require => :member
permission :view_sprint_board,
{:sprints => [:index, :show]}
permission :edit_sprint_board,
{:sprints => [:change_issue_status, :sort],
:scrum => [:change_story_points, :change_remaining_story_points,
:change_pending_effort, :change_assigned_to,
:new_pbi, :create_pbi, :edit_pbi, :update_pbi,
:new_task, :create_task, :edit_task, :update_task]},
:require => :member
permission :sort_sprint_board,
{:sprints => [:sort]},
:require => :member
permission :view_sprint_burndown,
{:sprints => [:burndown_index, :burndown]}
permission :view_sprint_stats, {:sprints => [:stats_index, :stats]}
permission :view_sprint_stats_by_member, {}
permission :view_product_backlog,
{:product_backlog => [:index, :show, :check_dependencies]}
permission :edit_product_backlog,
{:product_backlog => [:new_pbi, :create_pbi],
:scrum => [:edit_pbi, :update_pbi]},
:require => :member
permission :sort_product_backlog,
{:product_backlog => [:sort],
:scrum => [:move_pbi]},
:require => :member
permission :view_product_backlog_burndown,
{:product_backlog => [:burndown]}
permission :view_release_plan,
{:product_backlog => [:release_plan]}
permission :view_scrum_stats,
{:scrum => [:stats]}
permission :view_pending_effort,
{}
permission :edit_pending_effort,
{:scrum => [:change_pending_effort, :change_pending_efforts,
:change_story_points, :change_remaining_story_points]},
:require => :member
permission :view_remaining_story_points,
{}
permission :edit_remaining_story_points,
{:scrum => [:change_remaining_story_points]},
:require => :member
end
menu :project_menu, :product_backlog, {:controller => :product_backlog, :action => :index},
:caption => :label_menu_product_backlog, :after => :activity, :param => :project_id
menu :project_menu, :sprint, {:controller => :sprints, :action => :index},
:caption => :label_menu_sprint, :after => :activity, :param => :project_id
settings :default => {:create_journal_on_pbi_position_change => '0',
:doer_color => 'post-it-color-5',
:pbi_status_ids => [],
:pbi_tracker_ids => [],
:reviewer_color => 'post-it-color-3',
:blocked_color => 'post-it-color-6',
:story_points_custom_field_id => nil,
:blocked_custom_field_id => nil,
:simple_pbi_custom_field_id => nil,
:task_status_ids => [],
:task_tracker_ids => [],
:auto_update_pbi_status => '1',
:closed_pbi_status_id => nil,
:clear_new_tasks_assignee => '1',
:verification_activity_ids => [],
:inherit_pbi_attributes => '1',
:random_posit_rotation => '1',
:render_position_on_pbi => '0',
:render_category_on_pbi => '1',
:render_version_on_pbi => '1',
:render_author_on_pbi => '1',
:render_assigned_to_on_pbi => '0',
:render_updated_on_pbi => '0',
:check_dependencies_on_pbi_sorting => '0',
:product_burndown_sprints => '4',
:render_pbis_speed => '1',
:render_tasks_speed => '1',
:lowest_speed => 70,
:low_speed => 80,
:high_speed => 140,
:render_plugin_tips => '1',
:sprint_burndown_day_zero => '1',
:pbi_is_closed_if_tasks_are_closed => '0',
:show_project_totals_on_sprint => '0',
:show_project_totals_on_backlog => '0',
:use_remaining_story_points => '0',
:product_burndown_extra_sprints => 3,
:default_sprint_name => 'Sprint 1',
:default_sprint_days => 10,
:default_sprint_shared => '1'},
:partial => 'settings/scrum_settings'
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
require_dependency 'application_helper'
module Scrum
module ApplicationHelperPatch
def self.included(base)
base.class_eval do
def link_to_sprint(sprint, include_project_prefix = false)
if sprint.is_product_backlog?
path = product_backlog_path(sprint)
else
path = sprint
end
label = h(sprint.name)
label = "#{sprint.project.name}:#{label}" if include_project_prefix
return link_to(label, path)
end
def link_to_sprint_stats(sprint, include_project_prefix = false)
if sprint.is_product_backlog?
return nil
else
path = stats_sprint_path(sprint)
label = l(:label_sprint_stats_name, :name => sprint.name)
label = "#{sprint.project.name}:#{label}" if include_project_prefix
return link_to(label, path)
end
end
def link_to_sprint_burndown(sprint, include_project_prefix = false)
if sprint.is_product_backlog?
label = l(:label_burndown, :name => sprint.name)
path = burndown_product_backlog_path(sprint)
else
label = l(:label_sprint_burndown_chart_name, :name => sprint.name)
path = burndown_sprint_path(sprint)
end
label = "#{sprint.project.name}:#{label}" if include_project_prefix
return link_to(label, path)
end
def link_to_release_plan(sprint, include_project_prefix = false)
unless sprint.is_product_backlog?
return nil
end
label = l(:label_release_plan_name, :name => sprint.name)
label = "#{project.name}:#{label}" if include_project_prefix
return link_to(label, release_plan_product_backlog_path(sprint))
end
alias_method :parse_redmine_links_without_scrum, :parse_redmine_links
def parse_redmine_links(text, default_project, obj, attr, only_path, options)
result = parse_redmine_links_without_scrum(text, default_project, obj, attr, only_path, options)
text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(sprint|burndown|stats|product\-backlog|release\-plan)?((#)((\d*)|(current|latest)))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
leading, project_identifier, element_type, separator, element_id_text = $1, $4, $5, $7, $8
link = nil
project = default_project
if project_identifier
project = Project.visible.find_by_identifier(project_identifier)
end
if project and element_type and element_id_text
element_id = element_id_text.to_i
include_project_prefix = (project != default_project)
case element_type
when 'sprint', 'burndown', 'stats', 'product-backlog', 'release-plan'
if ((element_id_text == 'latest') or (element_id_text == 'current'))
sprint = project.last_sprint
else
sprint = project.sprints_and_product_backlogs.find_by_id(element_id)
end
end
unless sprint.nil?
case element_type
when 'sprint', 'product-backlog'
link = link_to_sprint(sprint, include_project_prefix)
when 'burndown'
link = link_to_sprint_burndown(sprint, include_project_prefix)
when 'stats'
link = link_to_sprint_stats(sprint, include_project_prefix)
when 'release-plan'
link = link_to_release_plan(sprint, include_project_prefix)
end
end
end
(leading + (link || "#{project_identifier}#{element_type}#{separator}#{element_id_text}"))
end
return result
end
def scrum_tips
tips = []
if Scrum::Setting.render_plugin_tips
back_url = url_for(params.permit!)
# Plugin permissions check.
unless @project and !(@project.module_enabled?(:scrum))
scrum_permissions = Redmine::AccessControl.modules_permissions(['scrum']).select{|p| p.project_module}.collect{|p| p.name}
active_scrum_permissions = Role.all.collect{|r| r.permissions & scrum_permissions}.flatten
if active_scrum_permissions.empty?
tips << l(:label_tip_no_permissions,
:link => link_to(l(:label_tip_permissions_link), permissions_roles_path))
end
end
# Minimal plugin settings check.
plugin_settings_link = link_to(l(:label_tip_plugin_settings_link),
plugin_settings_path(:id => :scrum))
if Scrum::Setting.story_points_custom_field_id.blank?
tips << l(:label_tip_no_plugin_setting, :link => plugin_settings_link,
:setting => l(:label_setting_story_points_custom_field))
end
if Scrum::Setting.pbi_tracker_ids.empty?
tips << l(:label_tip_no_plugin_setting, :link => plugin_settings_link,
:setting => l(:label_pbi_plural))
end
if Scrum::Setting.task_tracker_ids.empty?
tips << l(:label_tip_no_plugin_setting, :link => plugin_settings_link,
:setting => l(:label_task_plural))
end
if Scrum::Setting.task_status_ids.empty?
tips << l(:label_tip_no_plugin_setting, :link => plugin_settings_link,
:setting => l(:label_setting_task_statuses))
end
if Scrum::Setting.pbi_status_ids.empty?
tips << l(:label_tip_no_plugin_setting, :link => plugin_settings_link,
:setting => l(:label_setting_pbi_statuses))
end
# Project configuration checks.
if @project and @project.persisted? and @project.module_enabled?(:scrum)
product_backlog_link = link_to(l(:label_tip_product_backlog_link),
project_product_backlog_index_path(@project))
# At least one PB check.
if @project.product_backlogs.empty?
tips << l(:label_tip_no_product_backlogs,
:link => link_to(l(:label_tip_new_product_backlog_link),
new_project_sprint_path(@project, :create_product_backlog => true,
:back_url => back_url)))
end
# At least one Sprint check.
if @project.sprints.empty?
tips << l(:label_tip_no_sprints,
:link => link_to(l(:label_tip_new_sprint_link),
new_project_sprint_path(@project, :back_url => back_url)))
end
# Product backlog (+release plan) checks.
if @product_backlog and @product_backlog.persisted?
# No PBIs check.
if @product_backlog.pbis.empty?
tips << l(:label_tip_product_backlog_without_pbis, :link => product_backlog_link)
end
# Release plan checks.
if params[:controller] == 'scrum' and params[:action] == 'release_plan'
# No versions check.
if @project.versions.empty?
tips << l(:label_tip_project_without_versions,
:link => link_to(l(:label_tip_new_version_link),
new_project_version_path(@project, :back_url => back_url)))
end
end
end
# Sprint checks.
if @sprint and @sprint.persisted? and !(@sprint.is_product_backlog?)
sprint_board_link = link_to(l(:label_tip_sprint_board_link),
sprint_path(@sprint))
# No PBIs check.
if @sprint.pbis.empty?
tips << l(:label_tip_sprint_without_pbis, :sprint_board_link => sprint_board_link,
:product_backlog_link => product_backlog_link)
end
# No tasks check.
if @sprint.tasks.empty?
tips << l(:label_tip_sprint_without_tasks, :link => sprint_board_link)
end
# Orphan tasks check.
if (orphan_tasks = @sprint.orphan_tasks).any?
issues_link = orphan_tasks.collect{ |task|
link_to_issue(task, :subject => false, :tracker => false)
}.join(', ')
tips << l(:label_tip_sprint_with_orphan_tasks, :link => issues_link)
end
# No estimated effort check.
if @sprint.efforts.empty?
tips << l(:label_tip_sprint_without_efforts,
:link => link_to(l(:label_tip_sprint_effort_link),
edit_effort_sprint_path(@sprint, :back_url => back_url)))
end
# No project members on edit Sprint effort view.
if @project.members.empty? and params[:action].to_s == 'edit_effort'
tips << l(:label_tip_project_without_members,
:link => link_to(l(:label_tip_project_members_link),
settings_project_path(@project, :tab => :members)))
end
end
end
end
return tips
end
def render_time(time, unit, options = {})
if time.nil?
''
else
if time.is_a?(Integer)
text = ("%d" % time) unless options[:ignore_zero] and time == 0
elsif time.is_a?(Float)
text = ("%g" % time) unless options[:ignore_zero] and time == 0.0
else
text = time unless options[:ignore_zero] and (time.blank? or (time == '0'))
end
unless text.blank?
text = "#{text}#{options[:space_unit] ? ' ' : ''}#{unit}"
unless options[:link].nil?
text = link_to(text, options[:link])
end
render :inline => "<span title=\"#{options[:title]}\">#{text}</span>"
end
end
end
def render_hours(hours, options = {})
render_time(hours, 'h', options)
end
def render_sps(sps, options = {})
render_time(sps, l(:label_story_point_unit), options)
end
def render_scrum_help(unique_id = nil)
template = nil
case params[:controller].to_sym
when :sprints
case params[:action].to_sym
when :show
template = 'sprint/board'
when :burndown
template = (params[:type] and (params[:type] == 'sps')) ?
'sprint/burndown_sps' : 'sprint/burndown_effort'
when :stats
template = 'sprint/stats'
when :new, :edit
template = params[:create_product_backlog] ? 'product_backlog/form' : 'sprint/form'
when :edit_effort
template = 'sprint/edit_effort'
end
when :product_backlog
case params[:action].to_sym
when :show
template = 'product_backlog/board'
when :burndown
template = 'product_backlog/burndown'
when :release_plan
template = 'product_backlog/release_plan'
end
when :scrum
case params[:action].to_sym
when :stats
template = 'scrum/stats'
end
when :projects
case params[:action].to_sym
when :settings
if unique_id == 'sprints'
template = 'project_settings/sprints'
elsif unique_id == 'product_backlogs'
template = 'project_settings/product_backlogs'
end
end
when :settings
case params[:action].to_sym
when :plugin
case params[:id].to_sym
when :scrum
template = 'scrum/settings'
end
end
end
unless template.nil?
links = {}
links[:plugin_settings] = link_to(l(:label_tip_plugin_settings_link),
plugin_settings_path(:id => :scrum))
links[:permissions] = link_to(l(:label_tip_permissions_link),
permissions_roles_path)
links[:sprint_effort] = link_to(l(:label_tip_sprint_effort_link),
edit_effort_sprint_path(@sprint,
:back_url => url_for(params.permit!))) if @sprint and not @sprint.new_record?
end
return template.nil? ? '' : render(:partial => 'help/help',
:formats => [:html],
:locals => {:template => template,
:unique_id => unique_id,
:links => links})
end
end
end
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
require_dependency 'calendars_controller'
module Scrum
module CalendarsControllerPatch
def self.included(base)
base.class_eval do
around_action :add_sprints, :only => [:show]
def add_sprints
yield
view = ActionView::Base.new(File.join(File.dirname(__FILE__), '..', '..', 'app', 'views'))
view.class_eval do
include ApplicationHelper
end
sprints = []
query_sprints(sprints, @query, @calendar, true)
query_sprints(sprints, @query, @calendar, false)
response.body += view.render(:partial => 'scrum_hooks/calendars/sprints',
:locals => {:sprints => sprints})
end
private
def query_sprints(sprints, query, calendar, start)
date_field = start ? 'sprint_start_date' : 'sprint_end_date'
query.sprints.where(date_field => calendar.startdt..calendar.enddt,
is_product_backlog: false).each do |sprint|
sprints << {:name => sprint.name,
:url => url_for(:controller => :sprints,
:action => :show,
:id => sprint.id,
:only_path => true),
:day => sprint.send(date_field).day,
:week => sprint.send(date_field).cweek,
:start => start}
end
end
end
end
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
module Scrum
class HelperHooks < Redmine::Hook::Listener
def helper_issues_show_detail_after_setting(context)
case context[:detail].property
when "attr"
case context[:detail].prop_key
when "sprint_id"
context[:detail][:value] = get_sprint_name(context[:detail].value)
context[:detail][:old_value] = get_sprint_name(context[:detail].old_value)
end
end
end
private
def get_sprint_name(id)
sprint = Sprint.find(id.to_i)
return sprint.name
rescue
return id
end
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
require_dependency 'issue'
module Scrum
module IssuePatch
def self.included(base)
base.class_eval do
belongs_to :sprint
has_many :pending_efforts, -> { order('date ASC') }
safe_attributes :sprint_id, :if => lambda { |issue, user|
issue.project and issue.project.scrum? and user.allowed_to?(:edit_issues, issue.project)
}
before_save :update_position, :if => lambda { |issue|
issue.project and issue.project.scrum? and issue.sprint_id_changed? and issue.is_pbi?
}
before_save :update_pending_effort, :if => lambda { |issue|
issue.project and issue.project.scrum? and issue.status_id_changed? and issue.is_task?
}
before_save :update_assigned_to, :if => lambda { |issue|
issue.project and issue.project.scrum? and issue.status_id_changed? and issue.is_task?
}
before_save :update_parent_pbi, :if => lambda { |issue|
issue.project and issue.project.scrum? and Scrum::Setting.auto_update_pbi_status and
(issue.status_id_changed? or issue.new_record?) and
issue.is_task? and !issue.parent_id.nil?
}
before_save :update_parent_pbi_on_closed_tasks, :if => lambda { |issue|
issue.project and issue.project.scrum? and Scrum::Setting.closed_pbi_status_id and
(issue.status_id_changed? or issue.new_record?) and
issue.is_task? and !issue.parent_id.nil? and !issue.parent.closed?
}
def scrum_closed?
closed = self.closed?
if !closed and is_pbi? and self.children.any? and
Scrum::Setting.pbi_is_closed_if_tasks_are_closed?
closed = true
self.children.each do |task|
if !task.closed?
closed = false
break # at least a task opened, no need to go further
end
end
end
return closed
end
def closed_on_for_burndown
completed_date = nil
closed_statuses = IssueStatus::closed_status_ids
if self.closed?
self.journals.order('created_on DESC').each do |journal|
journal.details.where(:prop_key => 'status_id',
:value => closed_statuses).each do |detail|
completed_date = journal.created_on
end
break unless completed_date.nil?
end
end
if self.is_pbi? and self.children.any? and
Scrum::Setting.pbi_is_closed_if_tasks_are_closed
all_tasks_closed = true
last_closed_task_date = completed_date
self.children.each do |task|
if all_tasks_closed and task.closed?
task_closed_on = task.closed_on_for_burndown
if task_closed_on
if last_closed_task_date.nil? or
last_closed_task_date > task_closed_on
last_closed_task_date = task_closed_on
end
end
else
all_tasks_closed = false
end
end
if all_tasks_closed and last_closed_task_date
completed_date = last_closed_task_date
end
end
return completed_date
end
def has_story_points?
((!((custom_field_id = Scrum::Setting.story_points_custom_field_id).nil?)) and
visible_custom_field_values.collect{|value| value.custom_field.id.to_s}.include?(custom_field_id) and
self.is_pbi?)
end
def story_points
if has_story_points? and
!((custom_field_id = Scrum::Setting.story_points_custom_field_id).nil?) and
!((custom_value = self.custom_value_for(custom_field_id)).nil?) and
!((value = custom_value.value).blank?)
# Replace invalid float number separator (i.e. 0,5) with valid separator (i.e. 0.5)
value.gsub(',', '.').to_f
end
end
def story_points=(value)
if has_story_points? and
!((custom_field_id = Scrum::Setting.story_points_custom_field_id).nil?) and
!((custom_value = self.custom_value_for(custom_field_id)).nil?) and
custom_value.custom_field.valid_field_value?(value)
custom_value.value = value
custom_value.save!
else
raise
end
end
def closed_story_points
value = 0.0
if self.scrum_closed? and self.has_story_points?
value = self.story_points.to_f
elsif self.has_story_points? and
self.has_remaining_story_points? and
sps = self.story_points
remaining_sps = self.remaining_story_points
unless sps.nil? or remaining_sps.nil?
value = (sps - remaining_sps).to_f
value = 0.0 if value < 0
end
end
return value
end
def scheduled?
is_scheduled = false
if created_on and sprint and sprint.sprint_start_date
if is_pbi?
is_scheduled = created_on < sprint.sprint_start_date
elsif is_task?
is_scheduled = created_on <= sprint.sprint_start_date
end
end
return is_scheduled
end
def use_in_burndown?
is_task? and IssueStatus.task_statuses.include?(status) and
parent and parent.is_pbi? and IssueStatus.pbi_statuses.include?(parent.status)
end
def is_pbi?
tracker.is_pbi?
end
def is_simple_pbi?
is_pbi? and
!((custom_field_id = Scrum::Setting.simple_pbi_custom_field_id).nil?) and
!((custom_value = self.custom_value_for(custom_field_id)).nil?) and
(custom_value.value == '1') and
self.children.empty?
end
def is_task?
tracker.is_task?
end
def tasks_by_status_id
raise 'Issue is not an user story' unless is_pbi?
statuses = {}
IssueStatus.task_statuses.each do |status|
statuses[status.id] = children.select{|issue| (issue.status == status) and issue.visible?}
end
statuses
end
def doers
users = []
users << assigned_to unless assigned_to.nil?
time_entries = TimeEntry.where(:issue_id => id,
:activity_id => Issue.doing_activities_ids)
users.concat(time_entries.collect{|t| t.user}).uniq.sort
end
def reviewers
users = []
time_entries = TimeEntry.where(:issue_id => id,
:activity_id => Issue.reviewing_activities_ids)
users.concat(time_entries.collect{|t| t.user}).uniq.sort
end
def sortable?()
is_sortable = false
if is_pbi? and editable? and sprint and
((User.current.allowed_to?(:edit_product_backlog, project) and (sprint.is_product_backlog?)) or
(User.current.allowed_to?(:edit_sprint_board, project) and (!(sprint.is_product_backlog?))))
is_sortable = true
elsif is_task? and editable? and sprint and
User.current.allowed_to?(:edit_sprint_board, project) and !sprint.is_product_backlog?
is_sortable = true
end
return is_sortable
end
def post_it_css_class(options = {})
classes = ['post-it', 'big-post-it', tracker.post_it_css_class]
if is_pbi?
classes << 'sprint-pbi'
if options[:draggable] and editable? and sprint
if User.current.allowed_to?(:edit_product_backlog, project) and sprint.is_product_backlog?
classes << 'post-it-vertical-move-cursor'
elsif User.current.allowed_to?(:edit_sprint_board, project) and !(sprint.is_product_backlog?) and
is_simple_pbi?
classes << 'post-it-horizontal-move-cursor'
end
end
elsif is_task?
classes << 'sprint-task'
if options[:draggable] and editable? and sprint and
User.current.allowed_to?(:edit_sprint_board, project) and !sprint.is_product_backlog?
classes << 'post-it-horizontal-move-cursor'
end
end
if Scrum::Setting.random_posit_rotation
classes << "post-it-rotation-#{rand(5)}" if options[:rotate]
classes << "post-it-small-rotation-#{rand(5)}" if options[:small_rotate]
end
classes << 'post-it-scale' if options[:scale]
classes << 'post-it-small-scale' if options[:small_scale]
classes.join(' ')
end
def self.doer_post_it_css_class
doer_or_reviewer_post_it_css_class(:doer)
end
def self.reviewer_post_it_css_class
doer_or_reviewer_post_it_css_class(:reviewer)
end
def has_pending_effort?
is_task? and pending_efforts.any?
end
def pending_effort
value = nil
if self.is_task? and self.has_pending_effort?
value = pending_efforts.last.effort
elsif self.is_pbi?
if Scrum::Setting.use_remaining_story_points?
if self.has_remaining_story_points?
value = pending_efforts.last.effort
end
else
value = self.children.collect{|task| task.pending_effort}.compact.sum
end
end
return value
end
def pending_effort_children
value = nil
if self.is_pbi? and self.children.any?
value = self.children.collect{|task| task.pending_effort}.compact.sum
end
return value
end
def pending_effort=(new_effort)
if is_task?
self.any_pending_effort = new_effort
elsif self.is_pbi? and Scrum::Setting.use_remaining_story_points?
self.any_pending_effort = new_effort
end
end
def has_remaining_story_points?
Scrum::Setting.use_remaining_story_points? and is_pbi? and pending_efforts.any?
end
def remaining_story_points
if has_remaining_story_points?
return pending_efforts.last.effort
end
end
def remaining_story_points=(new_sps_value)
if is_pbi?
self.any_pending_effort = new_sps_value
end
end
def story_points_for_burdown(day)
value = nil
if self.has_remaining_story_points?
values = self.pending_efforts.where(['date <= ?', day])
value = values.last.effort if values.any?
end
if value.nil?
closed_on = self.closed_on_for_burndown
value = (closed_on.nil? or closed_on.beginning_of_day > day) ? self.story_points : 0.0
end
return value
end
def init_from_params(params)
end
def inherit_from_issue(source_issue)
[:priority_id, :category_id, :fixed_version_id, :start_date, :due_date].each do |attribute|
self.copy_attribute(source_issue, attribute)
end
self.custom_field_values = source_issue.custom_field_values.inject({}){|h, v| h[v.custom_field_id] = v.value; h}
end
def field?(field)
included = self.tracker.field?(field)
if (Redmine::VERSION::STRING < '3.4.0') and (field.to_sym == :description)
included = true
end
self.safe_attribute?(field) and (included or self.required_attribute?(field))
end
def custom_field?(custom_field)
self.tracker.custom_field?(custom_field)
end
def set_on_top
@set_on_top = true
end
def total_time
# Cache added
unless defined?(@total_time)
if self.is_pbi?
the_pending_effort = self.pending_effort_children
the_spent_hours = self.children.collect{|task| task.spent_hours}.compact.sum
elsif self.is_task?
the_pending_effort = self.pending_effort
the_spent_hours = self.spent_hours
end
the_pending_effort = the_pending_effort.nil? ? 0.0 : the_pending_effort
the_spent_hours = the_spent_hours.nil? ? 0.0 : the_spent_hours
@total_time = (the_pending_effort + the_spent_hours)
end
return @total_time
end
def speed
if (self.is_pbi? or self.is_task?) and (self.total_time > 0.0)
the_estimated_hours = (!defined?(self.total_estimated_hours) or self.total_estimated_hours.nil?) ?
0.0 : self.total_estimated_hours
return ((the_estimated_hours * 100.0) / self.total_time).round
else
return nil
end
end
def has_blocked_field?
return ((!((custom_field_id = Scrum::Setting.blocked_custom_field_id).nil?)) and
visible_custom_field_values.collect{|value| value.custom_field.id.to_s}.include?(custom_field_id))
end
def scrum_blocked?
if has_blocked_field? and
!((custom_field_id = Scrum::Setting.blocked_custom_field_id).nil?) and
!((custom_value = self.custom_value_for(custom_field_id)).nil?) and
!((value = custom_value.value).blank?)
return (value == '1')
end
end
def self.blocked_post_it_css_class
return doer_or_reviewer_post_it_css_class(:blocked)
end
def move_pbi_to(position, other_pbi_id = nil)
if !(sprint.nil?) and is_pbi?
case position
when 'top'
move_issue_to_the_begin_of_the_sprint
check_bad_dependencies
save!
when 'bottom'
move_issue_to_the_end_of_the_sprint
check_bad_dependencies
save!
when 'before', 'after'
if other_pbi_id.nil? or (other_pbi = Issue.find(other_pbi_id)).nil?
raise "Other PBI ID ##{other_pbi_id} is invalid"
elsif !(other_pbi.is_pbi?)
raise "Issue ##{other_pbi_id} is not a PBI"
elsif (other_pbi.sprint_id != sprint_id)
raise "Other PBI ID ##{other_pbi_id} is not in this product backlog"
else
move_issue_respecting_to_pbi(other_pbi, position == 'after')
end
end
end
end
def is_first_pbi?
min = min_position
return ((!(position.nil?)) and (!(min.nil?)) and (position <= min))
end
def is_last_pbi?
max = max_position
return ((!(position.nil?)) and (!(max.nil?)) and (position >= max))
end
def assignable_sprints
unless @assignable_sprints
sprints = project.all_open_sprints_and_product_backlogs.to_a
sprints << sprint unless sprint.nil? or sprint_id_changed?
@assignable_sprints = sprints.uniq.sort
end
return @assignable_sprints if @assignable_sprints
end
def scrum?
enabled = false
if project
enabled = true if project.scrum?
end
if sprint and sprint.project
enabled = true if sprint.project.scrum?
end
return enabled
end
def get_dependencies
dependencies = []
unless sprint.nil?
sprint.pbis(:position_bellow => position).each do |other_pbi|
if self != other_pbi
if self.respond_to?(:all_dependent_issues)
# Old Redmine API (<3.3.0).
is_dependent = all_dependent_issues.include?(other_pbi)
elsif self.respond_to?(:would_reschedule?) and self.respond_to?(:blocks?)
# New Redmine API (>=3.3.0).
is_dependent = (would_reschedule?(other_pbi) or blocks?(other_pbi))
else
is_dependent = false
end
dependencies << other_pbi if is_dependent
end
end
end
return dependencies
end
def check_bad_dependencies(raise_exception = true)
message = nil
if Scrum::Setting.check_dependencies_on_pbi_sorting
dependencies = get_dependencies
if dependencies.count > 0
others = dependencies.collect{ |issue| "##{issue.id}" }.join(', ')
message = l(:error_sorting_other_issues_depends_on_issue, :id => id, :others => others)
end
end
raise message if !(message.nil?) and raise_exception
return message
end
protected
def copy_attribute(source_issue, attribute)
if self.safe_attribute?(attribute) and source_issue.safe_attribute?(attribute)
self.send("#{attribute}=", source_issue.send("#{attribute}"))
end
end
private
def update_position
if sprint_id_was.blank?
# New PBI into PB or Sprint
if @set_on_top
move_issue_to_the_begin_of_the_sprint
else
move_issue_to_the_end_of_the_sprint
end
elsif sprint and (old_sprint = Sprint.find_by_id(sprint_id_was))
if old_sprint.is_product_backlog
# From PB to Sprint
move_issue_to_the_end_of_the_sprint
elsif sprint.is_product_backlog
# From Sprint to PB
move_issue_to_the_begin_of_the_sprint
else
# From Sprint to Sprint
move_issue_to_the_end_of_the_sprint
end
end
end
def update_pending_effort
self.pending_effort = 0 if self.closed?
end
def update_assigned_to
new_status = IssueStatus.task_statuses.first
if new_status
if self.status == new_status
if Scrum::Setting.clear_new_tasks_assignee and !(new_record?)
self.assigned_to = nil
end
elsif self.assigned_to.nil?
self.assigned_to = User.current
end
end
end
def update_parent_pbi
new_status = IssueStatus.task_statuses.first
in_progress_status = IssueStatus.task_statuses.second
if new_status && in_progress_status
pbi = self.parent
if pbi and pbi.is_pbi?
all_tasks_new = (self.status == new_status)
pbi.children.each do |task|
if task.is_task?
task = self if task.id == self.id
if task.status != new_status
all_tasks_new = false
break # at least a task not new, no need to go further
end
end
end
if pbi.status == new_status and !all_tasks_new
pbi.init_journal(User.current,
l(:label_pbi_status_auto_updated_one_task_no_new,
:pbi_status => in_progress_status.name,
:task_status => new_status.name))
pbi.status = in_progress_status
pbi.save!
elsif pbi.status != new_status and all_tasks_new
pbi.init_journal(User.current,
l(:label_pbi_status_auto_updated_all_tasks_new,
:pbi_status => new_status.name,
:task_status => new_status.name))
pbi.status = new_status
pbi.save!
end
end
end
end
def update_parent_pbi_on_closed_tasks
statuses = IssueStatus.where(:id => Scrum::Setting.closed_pbi_status_id).order("position ASC")
pbi = self.parent
if statuses.length == 1 and pbi and pbi.is_pbi?
pbi_status_to_set = statuses.first
all_tasks_closed = self.closed?
pbi.children.each do |task|
if task.is_task?
task = self if task.id == self.id
unless task.closed?
all_tasks_closed = false
break # at least a task opened, no need to go further
end
end
end
if all_tasks_closed and pbi.status != pbi_status_to_set
pbi.init_journal(User.current,
l(:label_pbi_status_auto_updated_all_tasks_closed,
:pbi_status => pbi_status_to_set.name))
pbi.status = pbi_status_to_set
pbi.save!
end
end
end
def min_position
min = nil
unless sprint.nil?
sprint.pbis.each do |pbi|
min = pbi.position if min.nil? or ((!pbi.position.nil?) and (pbi.position < min))
end
end
return min
end
def max_position
max = nil
unless sprint.nil?
sprint.pbis.each do |pbi|
max = pbi.position if max.nil? or ((!pbi.position.nil?) and (pbi.position > max))
end
end
return max
end
def move_issue_to_the_begin_of_the_sprint
min = min_position
self.position = min.nil? ? 1 : (min - 1)
end
def move_issue_to_the_end_of_the_sprint
max = max_position
self.position = max.nil? ? 1 : (max + 1)
end
def move_issue_respecting_to_pbi(other_pbi, after)
self.position = other_pbi.position
self.position += 1 if after
sprint.pbis(:position_above => after ? self.position : self.position - 1).each do |next_pbi|
if next_pbi.id != self.id
next_pbi.position += 1
end
end
self.check_bad_dependencies
sprint.pbis(:position_above => after ? self.position : self.position - 1).each do |next_pbi|
if next_pbi.id != self.id
next_pbi.check_bad_dependencies
end
end
self.save!
sprint.pbis(:position_above => after ? self.position : self.position - 1).each do |next_pbi|
if next_pbi.id != self.id
next_pbi.save!
end
end
end
def self.doer_or_reviewer_post_it_css_class(type)
classes = ['post-it']
case type
when :doer
classes << 'doer-post-it'
classes << Scrum::Setting.doer_color
when :reviewer
classes << 'reviewer-post-it'
classes << Scrum::Setting.reviewer_color
when :blocked
classes << 'blocked-post-it'
classes << Scrum::Setting.blocked_color
end
if Scrum::Setting.random_posit_rotation
classes << "post-it-rotation-#{rand(5)}"
end
classes.join(' ')
end
@@activities = nil
def self.activities
unless @@activities
@@activities = Enumeration.where(:type => 'TimeEntryActivity')
end
@@activities
end
@@reviewing_activities_ids = nil
def self.reviewing_activities_ids
unless @@reviewing_activities_ids
@@reviewing_activities_ids = Scrum::Setting.verification_activity_ids
end
@@reviewing_activities_ids
end
@@doing_activities_ids = nil
def self.doing_activities_ids
unless @@doing_activities_ids
reviewing_activities = Enumeration.where(:id => reviewing_activities_ids)
doing_activities = activities - reviewing_activities
@@doing_activities_ids = doing_activities.collect{|a| a.id}
end
@@doing_activities_ids
end
def any_pending_effort=(new_effort)
if id and new_effort
effort = PendingEffort.where(:issue_id => id, :date => Date.today).first
# Replace invalid float number separator (i.e. 0,5) with valid separator (i.e. 0.5)
new_effort.gsub!(',', '.') if new_effort.is_a?(String)
if effort.nil?
date = (pending_efforts.empty? and sprint and sprint.sprint_start_date and sprint.sprint_start_date < Date.today) ? sprint.sprint_start_date : Date.today
effort = PendingEffort.new(:issue_id => id, :date => date, :effort => new_effort)
else
effort.effort = new_effort
end
effort.save!
end
end
end
end
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
require_dependency 'issue_query'
module Scrum
module IssueQueryPatch
def self.included(base)
base.class_eval do
self.available_columns << QueryColumn.new(:sprint,
:sortable => lambda {Sprint.fields_for_order_statement},
:groupable => true)
self.available_columns << QueryColumn.new(:position,
:sortable => "#{Issue.table_name}.position")
alias_method :initialize_available_filters_without_scrum, :initialize_available_filters
def initialize_available_filters
filters = initialize_available_filters_without_scrum
if project
sprints = project.sprints_and_product_backlogs
if sprints.any?
add_available_filter 'sprint_id',
:type => :list_optional,
:values => sprints.sort.collect{|s| [s.name, s.id.to_s]}
add_available_filter 'position',
:type => :integer
add_associations_custom_fields_filters :sprint
end
end
filters
end
alias_method :issues_without_scrum, :issues
def issues(options = {})
options[:include] ||= []
options[:include] << :sprint
issues_without_scrum(options)
end
alias_method :issue_ids_without_scrum, :issue_ids
def issue_ids(options = {})
options[:include] ||= []
options[:include] << :sprint
issue_ids_without_scrum(options)
end
alias_method :available_columns_without_scrum, :available_columns
def available_columns
if !@available_columns
@available_columns = available_columns_without_scrum
index = nil
@available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
index = (index ? index + 1 : -1)
# insert the column after estimated_hours or at the end
@available_columns.insert index, QueryColumn.new(:pending_effort,
:sortable => "COALESCE(("\
"SELECT effort FROM #{PendingEffort.table_name} "\
"WHERE #{PendingEffort.table_name}.issue_id = #{Issue.table_name}.id "\
"ORDER BY #{PendingEffort.table_name}.date DESC LIMIT 1"\
"), 0)",
:default_order => 'desc',
:totalable => true
)
end
return @available_columns
end
def total_for_pending_effort(scope)
total = scope.joins(:pending_efforts).
sum("#{PendingEffort.table_name}.effort")
map_total(total) {|effort| effort.to_f.round(2)}
end
end
end
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
require_dependency "issue_status"
module Scrum
module IssueStatusPatch
def self.included(base)
base.class_eval do
def self.task_statuses
IssueStatus.where(:id => Scrum::Setting.task_status_ids).order("position ASC")
end
def self.pbi_statuses
IssueStatus.where(:id => Scrum::Setting.pbi_status_ids).order("position ASC")
end
def self.closed_status_ids
IssueStatus.where(:is_closed => true).collect{|status| status.id}
end
end
end
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
require_dependency "issues_controller"
module Scrum
module IssuesControllerPatch
def self.included(base)
base.class_eval do
after_action :save_pending_effort, :only => [:create, :update]
before_action :add_default_sprint, :only => [:new, :update_form]
private
def save_pending_effort
if @issue.scrum? and @issue.id and params[:issue] and params[:issue][:pending_effort]
if @issue.is_task?
@issue.pending_effort = params[:issue][:pending_effort]
elsif @issue.is_pbi?
@issue.remaining_story_points = params[:issue][:pending_effort]
end
end
end
def add_default_sprint
if @issue.id.nil?
@issue.sprint = nil
unless @project.nil?
if @project.scrum?
if @issue.is_task? and (current_sprint = @project.current_sprint)
@issue.sprint = current_sprint
elsif @issue.is_pbi? and (product_backlog = @project.product_backlogs.first)
@issue.sprint = product_backlog
end
end
end
end
end
end
end
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
require_dependency "journal"
module Scrum
module JournalPatch
def self.included(base)
base.class_eval do
private
alias_method :add_attribute_detail_without_scrum, :add_attribute_detail
def add_attribute_detail(attribute, old_value, value)
if Scrum::Setting.create_journal_on_pbi_position_change or (attribute != 'position')
add_attribute_detail_without_scrum(attribute, old_value, value)
end
end
end
end
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
require_dependency "project"
module Scrum
module ProjectPatch
def self.included(base)
base.class_eval do
has_many :product_backlogs, -> { where(:is_product_backlog => true).order('name ASC') },
:class_name => 'Sprint'
has_many :sprints, -> { where(:is_product_backlog => false).order('sprint_start_date ASC, name ASC') },
:dependent => :destroy
has_many :sprints_and_product_backlogs, -> { order('sprint_start_date ASC, name ASC') },
:class_name => 'Sprint', :dependent => :destroy
has_many :open_sprints_and_product_backlogs, -> { where(:status => 'open').order('sprint_start_date ASC, name ASC') },
:class_name => 'Sprint', :dependent => :destroy
def last_sprint
sprints.last
end
def current_sprint
today = Date.today
current_sprint = sprints.where('sprint_start_date <= ?', today)
.where('sprint_end_date >= ?', today).last
current_sprint ? current_sprint : last_sprint
end
def story_points_per_sprint(options = {})
max_sprints_count = self.sprints.length
last_sprints_count = Scrum::Setting.product_burndown_sprints
last_sprints_count = max_sprints_count if last_sprints_count == 0
i = max_sprints_count - 1
sprints_count = 0
story_points_per_sprint = 0.0
scheduled_story_points_per_sprint = 0.0
today = Date.today
while (sprints_count < last_sprints_count and i >= 0)
story_points = self.sprints[i].story_points(options)
scheduled_story_points = self.sprints[i].scheduled_story_points(options)
sprint_end_date = self.sprints[i].sprint_end_date
unless story_points.nil? or scheduled_story_points.nil? or (sprint_end_date >= today)
story_points_per_sprint += story_points
scheduled_story_points_per_sprint += scheduled_story_points
sprints_count += 1
end
i -= 1
end
story_points_per_sprint = filter_story_points(story_points_per_sprint, sprints_count)
scheduled_story_points_per_sprint = filter_story_points(scheduled_story_points_per_sprint, sprints_count)
return [story_points_per_sprint, scheduled_story_points_per_sprint,
(Scrum::Setting.product_burndown_sprints == 0) ? 0 : sprints_count]
end
def closed_story_points_per_sprint
return value_per_sprint(:closed_story_points,
:label_closed_story_points)
end
def hours_per_story_point
return value_per_sprint(:hours_per_story_point,
:label_hours_per_story_point)
end
def sps_by_category
sps_by_pbi_field(:category, :sps_by_pbi_category)
end
def sps_by_pbi_type
sps_by_pbi_field(:tracker, :sps_by_pbi_type)
end
def effort_by_activity
sps_by_pbi_field(:activity, :time_entries_by_activity)
end
def all_open_sprints_and_product_backlogs(only_shared = false)
# Get this project Sprints.
conditions = {}
conditions[:shared] = true if only_shared
all_sprints = scrum? ? open_sprints_and_product_backlogs.where(conditions).to_a : []
# If parent try to recursively add shared Sprints from parents.
unless parent.nil?
all_sprints += parent.all_open_sprints_and_product_backlogs(true)
end
return all_sprints
end
def scrum?
is_scrum = module_enabled?(:scrum)
is_scrum = parent.scrum? unless is_scrum or parent.nil?
return is_scrum
end
def pbis_count(filters)
return pbis(filters).count
end
def closed_pbis_count(filters)
return pbis(filters).select {|pbi| pbi.scrum_closed?}.count
end
def total_sps(filters)
return pbis(filters).collect {|pbi| pbi.story_points.to_f || 0.0}.sum
end
def closed_sps(filters)
return pbis(filters).collect {|pbi| pbi.closed_story_points}.sum
end
private
def pbis(filters)
the_filters = filters ? filters.clone : {}
the_filters[:filter_by_project] = Integer(the_filters[:filter_by_project]) rescue nil
filter_conditions = {}
filter_conditions[:project_id] = the_filters[:filter_by_project] if the_filters[:filter_by_project]
Issue.visible.includes(:custom_values).where(pbis_conditons(the_filters)).where(filter_conditions)
end
def pbis_conditons(filters)
conditions = []
conditions << self.project_condition(::Setting.display_subprojects_issues?) unless filters[:filter_by_project]
conditions << "(tracker_id IN (#{Tracker.pbi_trackers(self).collect {|tracker| tracker.id}.join(', ')}))"
return conditions.join(' AND ')
end
def filter_story_points(story_points, sprints_count)
story_points /= sprints_count if story_points > 0 and sprints_count > 0
story_points = 1 if story_points == 0
story_points = story_points.round(2)
return story_points
end
def sps_by_pbi_field(field, method)
results = {}
total = 0.0
all_sprints = sprints_and_product_backlogs
all_sprints.each do |sprint|
sprint_results, sprint_total = sprint.send(method)
sprint_results.each do |result|
if !results.key?(result[field])
results[result[field]] = 0.0
end
results[result[field]] += result[:total]
end
total += sprint_total
end
new_results = []
results.each_pair{|key, value|
new_results << {field => key,
:total => value,
:percentage => total ? ((value * 100.0) / total).round(2) : 0.0}
}
[new_results, total]
end
def value_per_sprint(sprint_method, label_value)
results = {}
media = 0.0
sprints_to_use = sprints
max_sprints_count = sprints_to_use.count
last_sprints_count = Scrum::Setting.product_burndown_sprints
last_sprints_count = max_sprints_count if last_sprints_count == 0
last_sprints_count = sprints_to_use.count if last_sprints_count > sprints_to_use.count
sprints_to_use.each_with_index { |sprint, i|
value = sprint.send(sprint_method)
results[sprint.name] = value
if i >= max_sprints_count - last_sprints_count
media += value
end
}
media = (media / last_sprints_count).round(2) if last_sprints_count > 0
results[l(:label_media_last_n_sprints, :n => last_sprints_count)] = media
return {l(label_value) => results}
end
end
end
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
require_dependency "projects_helper"
module Scrum
module ProjectsHelperPatch
def self.included(base)
base.class_eval do
alias_method :project_settings_tabs_without_scrum, :project_settings_tabs
def project_settings_tabs
tabs = project_settings_tabs_without_scrum
if User.current.allowed_to?(:manage_sprints, @project)
options = {:name => 'versions', :action => :manage_versions,
:partial => 'projects/settings/versions',
:label => :label_version_plural}
index = tabs.index(options)
unless index # Needed for Redmine v3.4.x
options[:url] = {:tab => 'versions',
:version_status => params[:version_status],
:version_name => params[:version_name]}
index = tabs.index(options)
end
if index
tabs.insert(index,
{:name => 'product_backlogs', :action => :edit_sprints,
:partial => 'projects/settings/product_backlogs',
:label => :label_product_backlog_plural})
tabs.insert(index,
{:name => 'sprints', :action => :edit_sprints,
:partial => 'projects/settings/sprints',
:label => :label_sprint_plural})
tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
end
end
return(tabs)
end
end
end
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
require_dependency 'query'
module Scrum
module QueryPatch
def self.included(base)
base.class_eval do
def sprints(options = {})
Sprint
.joins(:project)
.includes(:project)
.where(Query.scrum_merge_conditions(project_statement, options[:conditions]))
rescue ::ActiveRecord::StatementInvalid => e
raise StatementInvalid.new(e.message)
end
# Deprecated method from Rails 2.3.X.
def self.scrum_merge_conditions(*conditions)
segments = []
conditions.each do |condition|
unless condition.blank?
sql = sanitize_sql(condition)
segments << sql unless sql.blank?
end
end
"(#{segments.join(') AND (')})" unless segments.empty?
end
end
end
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
module Scrum
class Setting
%w(auto_update_pbi_status
check_dependencies_on_pbi_sorting
clear_new_tasks_assignee
create_journal_on_pbi_position_change
inherit_pbi_attributes
pbi_is_closed_if_tasks_are_closed
random_posit_rotation
render_author_on_pbi
render_category_on_pbi
render_pbis_speed
render_plugin_tips
render_position_on_pbi
render_tasks_speed
render_updated_on_pbi
render_version_on_pbi
render_assigned_to_on_pbi
show_project_totals_on_sprint
show_project_totals_on_backlog
sprint_burndown_day_zero
use_remaining_story_points
default_sprint_shared).each do |setting|
src = <<-END_SRC
def self.#{setting}
setting_or_default_boolean(:#{setting})
end
def self.#{setting}?
setting_or_default_boolean(:#{setting})
end
END_SRC
class_eval src, __FILE__, __LINE__
end
%w(blocked_color
doer_color
reviewer_color
default_sprint_name).each do |setting|
src = <<-END_SRC
def self.#{setting}
setting_or_default(:#{setting})
end
END_SRC
class_eval src, __FILE__, __LINE__
end
%w(pbi_status_ids
pbi_tracker_ids
task_status_ids
task_tracker_ids
verification_activity_ids).each do |setting|
src = <<-END_SRC
def self.#{setting}
collect_ids(:#{setting})
end
END_SRC
class_eval src, __FILE__, __LINE__
end
%w(blocked_custom_field_id
closed_pbi_status_id
simple_pbi_custom_field_id
story_points_custom_field_id).each do |setting|
src = <<-END_SRC
def self.#{setting}
::Setting.plugin_scrum[:#{setting}.to_s]
end
END_SRC
class_eval src, __FILE__, __LINE__
end
module TrackerFields
FIELDS = 'fields'
CUSTOM_FIELDS = 'custom_fields'
SPRINT_BOARD_FIELDS = 'sprint_board_fields'
SPRINT_BOARD_CUSTOM_FIELDS = 'sprint_board_custom_fields'
end
def self.tracker_fields(tracker, type = TrackerFields::FIELDS)
collect("tracker_#{tracker}_#{type}")
end
def self.tracker_field?(tracker, field, type = TrackerFields::FIELDS)
tracker_fields(tracker, type).include?(field.to_s)
end
def self.sprint_board_fields
[:status_id, :category_id, :fixed_version_id]
end
def self.task_tracker
Tracker.all(task_tracker_ids)
end
def self.tracker_id_color(id)
setting_or_default("tracker_#{id.to_s}_color")
end
def self.product_burndown_sprints
setting_or_default_integer(:product_burndown_sprints, :min => 0)
end
def self.product_burndown_extra_sprints
setting_or_default_integer(:product_burndown_extra_sprints, :min => 0)
end
def self.lowest_speed
setting_or_default_integer(:lowest_speed, :min => 0, :max => 99)
end
def self.low_speed
setting_or_default_integer(:low_speed, :min => 0, :max => 99)
end
def self.high_speed
setting_or_default_integer(:high_speed, :min => 101, :max => 10000)
end
def self.default_sprint_days
setting_or_default_integer(:default_sprint_days, :min => 1, :max => 20)
end
private
def self.setting_or_default(setting)
::Setting.plugin_scrum[setting.to_s] || Redmine::Plugin::registered_plugins[:scrum].settings[:default][setting]
end
def self.setting_or_default_boolean(setting)
setting_or_default(setting) == '1'
end
def self.setting_or_default_integer(setting, options = {})
value = setting_or_default(setting).to_i
value = options[:min] if options[:min] and value < options[:min]
value = options[:max] if options[:max] and value > options[:max]
value
end
def self.collect_ids(setting)
(::Setting.plugin_scrum[setting.to_s] || []).collect{|value| value.to_i}
end
def self.collect(setting)
(::Setting.plugin_scrum[setting.to_s] || [])
end
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
require_dependency "tracker"
module Scrum
module TrackerPatch
def self.included(base)
base.class_eval do
def self.pbi_trackers_ids
Scrum::Setting.pbi_tracker_ids
end
def self.pbi_trackers(project = nil)
trackers_ids = pbi_trackers_ids
trackers_ids &= project.trackers.collect{ |tracker| tracker.id } if project
Tracker.where(:id => trackers_ids).sort
end
def is_pbi?
Scrum::Setting.pbi_tracker_ids.include?(id)
end
def self.task_trackers_ids
Scrum::Setting.task_tracker_ids
end
def self.task_trackers
Tracker.where(:id => task_trackers_ids)
end
def is_task?
Scrum::Setting.task_tracker_ids.include?(id)
end
def post_it_css_class
Scrum::Setting.tracker_id_color(id)
end
def field?(field)
Scrum::Setting.tracker_field?(self.id, field)
end
def custom_field?(custom_field)
Scrum::Setting.tracker_field?(self.id, custom_field.id, Scrum::Setting::TrackerFields::CUSTOM_FIELDS) or custom_field.is_required
end
end
end
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
require_dependency "user"
module Scrum
module UserPatch
def self.included(base)
base.class_eval do
has_many :sprint_efforts, :dependent => :destroy
end
end
end
end
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
module Scrum
class ViewHooks < Redmine::Hook::ViewListener
render_on(:view_issues_bulk_edit_details_bottom, :partial => 'scrum_hooks/issues/bulk_edit')
render_on(:view_issues_context_menu_start, :partial => 'scrum_hooks/context_menus/issues')
render_on(:view_issues_form_details_bottom, :partial => 'scrum_hooks/issues/form')
render_on(:view_issues_show_details_bottom, :partial => 'scrum_hooks/issues/show')
render_on(:view_layouts_base_html_head, :partial => 'scrum_hooks/head')
render_on(:view_layouts_base_sidebar, :partial => 'scrum_hooks/scrum_tips')
render_on(:view_projects_show_sidebar_bottom, :partial => 'scrum_hooks/projects/show_sidebar')
end
end
Attribution-NoDerivatives 4.0 International
=======================================================================
Creative Commons Corporation ("Creative Commons") is not a law firm and
does not provide legal services or legal advice. Distribution of
Creative Commons public licenses does not create a lawyer-client or
other relationship. Creative Commons makes its licenses and related
information available on an "as-is" basis. Creative Commons gives no
warranties regarding its licenses, any material licensed under their
terms and conditions, or any related information. Creative Commons
disclaims all liability for damages resulting from their use to the
fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and
conditions that creators and other rights holders may use to share
original works of authorship and other material subject to copyright
and certain other rights specified in the public license below. The
following considerations are for informational purposes only, are not
exhaustive, and do not form part of our licenses.
Considerations for licensors: Our public licenses are
intended for use by those authorized to give the public
permission to use material in ways otherwise restricted by
copyright and certain other rights. Our licenses are
irrevocable. Licensors should read and understand the terms
and conditions of the license they choose before applying it.
Licensors should also secure all rights necessary before
applying our licenses so that the public can reuse the
material as expected. Licensors should clearly mark any
material not subject to the license. This includes other CC-
licensed material, or material used under an exception or
limitation to copyright. More considerations for licensors:
wiki.creativecommons.org/Considerations_for_licensors
Considerations for the public: By using one of our public
licenses, a licensor grants the public permission to use the
licensed material under specified terms and conditions. If
the licensor's permission is not necessary for any reason--for
example, because of any applicable exception or limitation to
copyright--then that use is not regulated by the license. Our
licenses grant only permissions under copyright and certain
other rights that a licensor has authority to grant. Use of
the licensed material may still be restricted for other
reasons, including because others have copyright or other
rights in the material. A licensor may make special requests,
such as asking that all changes be marked or described.
Although not required by our licenses, you are encouraged to
respect those requests where reasonable. More_considerations
for the public:
wiki.creativecommons.org/Considerations_for_licensees
=======================================================================
Creative Commons Attribution-NoDerivatives 4.0 International Public
License
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
Attribution-NoDerivatives 4.0 International Public License ("Public
License"). To the extent this Public License may be interpreted as a
contract, You are granted the Licensed Rights in consideration of Your
acceptance of these terms and conditions, and the Licensor grants You
such rights in consideration of benefits the Licensor receives from
making the Licensed Material available under these terms and
conditions.
Section 1 -- Definitions.
a. Adapted Material means material subject to Copyright and Similar
Rights that is derived from or based upon the Licensed Material
and in which the Licensed Material is translated, altered,
arranged, transformed, or otherwise modified in a manner requiring
permission under the Copyright and Similar Rights held by the
Licensor. For purposes of this Public License, where the Licensed
Material is a musical work, performance, or sound recording,
Adapted Material is always produced where the Licensed Material is
synched in timed relation with a moving image.
b. Copyright and Similar Rights means copyright and/or similar rights
closely related to copyright including, without limitation,
performance, broadcast, sound recording, and Sui Generis Database
Rights, without regard to how the rights are labeled or
categorized. For purposes of this Public License, the rights
specified in Section 2(b)(1)-(2) are not Copyright and Similar
Rights.
c. Effective Technological Measures means those measures that, in the
absence of proper authority, may not be circumvented under laws
fulfilling obligations under Article 11 of the WIPO Copyright
Treaty adopted on December 20, 1996, and/or similar international
agreements.
d. Exceptions and Limitations means fair use, fair dealing, and/or
any other exception or limitation to Copyright and Similar Rights
that applies to Your use of the Licensed Material.
e. Licensed Material means the artistic or literary work, database,
or other material to which the Licensor applied this Public
License.
f. Licensed Rights means the rights granted to You subject to the
terms and conditions of this Public License, which are limited to
all Copyright and Similar Rights that apply to Your use of the
Licensed Material and that the Licensor has authority to license.
g. Licensor means the individual(s) or entity(ies) granting rights
under this Public License.
h. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such
as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
available to the public including in ways that members of the
public may access the material from a place and at a time
individually chosen by them.
i. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and of
the Council of 11 March 1996 on the legal protection of databases,
as amended and/or succeeded, as well as other essentially
equivalent rights anywhere in the world.
j. You means the individual or entity exercising the Licensed Rights
under this Public License. Your has a corresponding meaning.
Section 2 -- Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License,
the Licensor hereby grants You a worldwide, royalty-free,
non-sublicensable, non-exclusive, irrevocable license to
exercise the Licensed Rights in the Licensed Material to:
a. reproduce and Share the Licensed Material, in whole or
in part; and
b. produce and reproduce, but not Share, Adapted Material.
2. Exceptions and Limitations. For the avoidance of doubt, where
Exceptions and Limitations apply to Your use, this Public
License does not apply, and You do not need to comply with
its terms and conditions.
3. Term. The term of this Public License is specified in Section
6(a).
4. Media and formats; technical modifications allowed. The
Licensor authorizes You to exercise the Licensed Rights in
all media and formats whether now known or hereafter created,
and to make technical modifications necessary to do so. The
Licensor waives and/or agrees not to assert any right or
authority to forbid You from making technical modifications
necessary to exercise the Licensed Rights, including
technical modifications necessary to circumvent Effective
Technological Measures. For purposes of this Public License,
simply making modifications authorized by this Section 2(a)
(4) never produces Adapted Material.
5. Downstream recipients.
a. Offer from the Licensor -- Licensed Material. Every
recipient of the Licensed Material automatically
receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this
Public License.
b. No downstream restrictions. You may not offer or impose
any additional or different terms or conditions on, or
apply any Effective Technological Measures to, the
Licensed Material if doing so restricts exercise of the
Licensed Rights by any recipient of the Licensed
Material.
6. No endorsement. Nothing in this Public License constitutes or
may be construed as permission to assert or imply that You
are, or that Your use of the Licensed Material is, connected
with, or sponsored, endorsed, or granted official status by,
the Licensor or others designated to receive attribution as
provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not
licensed under this Public License, nor are publicity,
privacy, and/or other similar personality rights; however, to
the extent possible, the Licensor waives and/or agrees not to
assert any such rights held by the Licensor to the limited
extent necessary to allow You to exercise the Licensed
Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this
Public License.
3. To the extent possible, the Licensor waives any right to
collect royalties from You for the exercise of the Licensed
Rights, whether directly or through a collecting society
under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly
reserves any right to collect such royalties.
Section 3 -- License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the
following conditions.
a. Attribution.
1. If You Share the Licensed Material, You must:
a. retain the following if it is supplied by the Licensor
with the Licensed Material:
i. identification of the creator(s) of the Licensed
Material and any others designated to receive
attribution, in any reasonable manner requested by
the Licensor (including by pseudonym if
designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of
warranties;
v. a URI or hyperlink to the Licensed Material to the
extent reasonably practicable;
b. indicate if You modified the Licensed Material and
retain an indication of any previous modifications; and
c. indicate the Licensed Material is licensed under this
Public License, and include the text of, or the URI or
hyperlink to, this Public License.
For the avoidance of doubt, You do not have permission under
this Public License to Share Adapted Material.
2. You may satisfy the conditions in Section 3(a)(1) in any
reasonable manner based on the medium, means, and context in
which You Share the Licensed Material. For example, it may be
reasonable to satisfy the conditions by providing a URI or
hyperlink to a resource that includes the required
information.
3. If requested by the Licensor, You must remove any of the
information required by Section 3(a)(1)(A) to the extent
reasonably practicable.
Section 4 -- Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that
apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
to extract, reuse, reproduce, and Share all or a substantial
portion of the contents of the database, provided You do not Share
Adapted Material;
b. if You include all or a substantial portion of the database
contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database
Rights (but not its individual contents) is Adapted Material; and
c. You must comply with the conditions in Section 3(a) if You Share
all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not
replace Your obligations under this Public License where the Licensed
Rights include other Copyright and Similar Rights.
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
c. The disclaimer of warranties and limitation of liability provided
above shall be interpreted in a manner that, to the extent
possible, most closely approximates an absolute disclaimer and
waiver of all liability.
Section 6 -- Term and Termination.
a. This Public License applies for the term of the Copyright and
Similar Rights licensed here. However, if You fail to comply with
this Public License, then Your rights under this Public License
terminate automatically.
b. Where Your right to use the Licensed Material has terminated under
Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided
it is cured within 30 days of Your discovery of the
violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any
right the Licensor may have to seek remedies for Your violations
of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the
Licensed Material under separate terms or conditions or stop
distributing the Licensed Material at any time; however, doing so
will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
License.
Section 7 -- Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different
terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the
Licensed Material not stated herein are separate from and
independent of the terms and conditions of this Public License.
Section 8 -- Interpretation.
a. For the avoidance of doubt, this Public License does not, and
shall not be interpreted to, reduce, limit, restrict, or impose
conditions on any use of the Licensed Material that could lawfully
be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is
deemed unenforceable, it shall be automatically reformed to the
minimum extent necessary to make it enforceable. If the provision
cannot be reformed, it shall be severed from this Public License
without affecting the enforceability of the remaining terms and
conditions.
c. No term or condition of this Public License will be waived and no
failure to comply consented to unless expressly agreed to by the
Licensor.
d. Nothing in this Public License constitutes or may be interpreted
as a limitation upon, or waiver of, any privileges and immunities
that apply to the Licensor or You, including from the legal
processes of any jurisdiction or authority.
=======================================================================
Creative Commons is not a party to its public licenses.
Notwithstanding, Creative Commons may elect to apply one of its public
licenses to material it publishes and in those instances will be
considered the "Licensor." Except for the limited purpose of indicating
that material is shared under a Creative Commons public license or as
otherwise permitted by the Creative Commons policies published at
creativecommons.org/policies, Creative Commons does not authorize the
use of the trademark "Creative Commons" or any other trademark or logo
of Creative Commons without its prior written consent including,
without limitation, in connection with any unauthorized modifications
to any of its public licenses or any other arrangements,
understandings, or agreements concerning use of licensed material. For
the avoidance of doubt, this paragraph does not form part of the public
licenses.
Creative Commons may be contacted at creativecommons.org.
\ No newline at end of file
# encoding: UTF-8
# Copyright © Emilio González Montaña
# Licence: Attribution & no derivatives
# * Attribution to the plugin web page URL should be done if you want to use it.
# https://redmine.ociotec.com/projects/localizable
# * No derivatives of this plugin (or partial) are allowed.
# Take a look to licence.txt file at plugin root folder for further details.
# Load the Redmine helper
require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper')
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment