OpenProject is the leading open source project management software.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
openproject/modules/backlogs/app/models/sprint.rb

170 lines
6.0 KiB

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
renamed start_date, burndown generation, parameter quotation * start_date was added to the core Issue model, overriding our start_date. The core start_date is just an alias for the effective_date, so it's not useful to us. * burndown generation. Sprint.generate_burndown will generate the burndown for all current sprints by calling +generate+ on each of them. +generate+ picks what it can from the cache and interpolates the rest. The cache can be primed by just calling +generate+ at least once a day. Calling it multiple times will have no effect _unless_ you've changed a story or task, which will cause today's cache entry to be discarded. This way you will always have the latest data. The charts being generated: - points committed: the total of points included in the sprint. This would typically always be the same from the start, but will change if stories are added or removed mid-sprint - points resolved: when a story goes to 100% completed (typically by setting its tasks to closed), the story is deemed resolved. - points accepted: when a story is closed, it is deemed to be accepted by the PO - remaining hours: total of hours remaining on all tasks - required burn rate in points/hours: the number of points/hours the team will have to burn down each day to complete all the work at the end of the sprint. A flat line is perfect, if the line starts dropping it means the team is ahead of the curve, if the line starts climbing it means the team is running behind * parameter quotation added to raw sql
15 years ago
require 'date'
15 years ago
class Sprint < Version
scope :open_sprints, lambda { |project|
where(["versions.status = 'open' and versions.project_id = ?", project.id])
.order(Arel.sql("COALESCE(start_date, CAST('4000-12-30' as date)) ASC, COALESCE(effective_date, CAST('4000-12-30' as date)) ASC"))
}
15 years ago
11 years ago
# null last ordering
scope :order_by_date, -> {
order Arel.sql("COALESCE(start_date, CAST('4000-12-30' as date)) ASC, COALESCE(effective_date, CAST('4000-12-30' as date)) ASC")
}
scope :order_by_name, -> {
order Arel.sql("#{Version.table_name}.name ASC")
}
scope :apply_to, lambda { |project|
where("#{Version.table_name}.project_id = #{project.id}" +
" OR (#{Project.table_name}.active = #{true} AND (" +
" #{Version.table_name}.sharing = 'system'" +
" OR (#{Project.table_name}.lft >= #{project.root.lft} AND #{Project.table_name}.rgt <= #{project.root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
" OR (#{Project.table_name}.lft < #{project.lft} AND #{Project.table_name}.rgt > #{project.rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
" OR (#{Project.table_name}.lft > #{project.lft} AND #{Project.table_name}.rgt < #{project.rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
'))')
.includes(:project)
.references(:projects)
}
scope :displayed_left, lambda { |project|
joins(sanitize_sql_array([
"LEFT OUTER JOIN (SELECT * from #{VersionSetting.table_name}" +
' WHERE project_id = ? ) version_settings' +
' ON version_settings.version_id = versions.id',
project.id])
)
.where([
'(version_settings.project_id = ? AND version_settings.display = ?)' +
' OR (version_settings.project_id is NULL)',
project.id,
VersionSetting::DISPLAY_LEFT
])
.joins("
LEFT OUTER JOIN (SELECT * FROM #{VersionSetting.table_name}) AS vs
ON vs.version_id = #{Version.table_name}.id AND vs.project_id = #{Version.table_name}.project_id
") # next take only those versions which define 'display left' in their home project or the given project (or don't define anything)
.where(
"(version_settings.display = ? OR vs.display = ? OR vs.display IS NULL)",
VersionSetting::DISPLAY_LEFT,
VersionSetting::DISPLAY_LEFT
)
}
scope :displayed_right, lambda { |project|
where(['version_settings.project_id = ? AND version_settings.display = ?',
project.id, VersionSetting::DISPLAY_RIGHT])
.includes(:version_settings)
.references(:version_settings)
}
def stories(project, options = {})
Story.sprint_backlog(project, self, options)
end
def points
stories.inject(0) { |sum, story| sum + story.story_points.to_i }
end
def has_wiki_page
return false if wiki_page_title.blank?
page = project.wiki.find_page(wiki_page_title)
return false if !page
template = project.wiki.find_page(Setting.plugin_openproject_backlogs['wiki_template'])
return false if template && page.text == template.text
15 years ago
true
end
15 years ago
def wiki_page
return '' unless project.wiki
15 years ago
update_attribute(:wiki_page_title, name) if wiki_page_title.blank?
15 years ago
page = project.wiki.find_page(wiki_page_title)
template = project.wiki.find_page(Setting.plugin_openproject_backlogs['wiki_template'])
if template and not page
page = project.wiki.pages.build(title: wiki_page_title)
page.build_content(text: "h1. #{name}\n\n#{template.text}")
page.save!
end
wiki_page_title
end
def days(cutoff = nil, alldays = false)
11 years ago
# TODO: Assumes mon-fri are working days, sat-sun are not. This assumption
# is not globally right, we need to make this configurable.
cutoff = effective_date if cutoff.nil?
(start_date..cutoff).select { |d| alldays || (d.wday > 0 and d.wday < 6) }
end
def has_burndown?
!!(effective_date and start_date)
end
15 years ago
def activity
bd = burndown('up')
return false if bd.blank?
11 years ago
# Assume a sprint is active if it's only 2 days old
return true if bd.remaining_hours.size <= 2
15 years ago
WorkPackage.exists?(['fixed_version_id = ? and ((updated_on between ? and ?) or (created_on between ? and ?))',
id, -2.days.from_now, Time.now, -2.days.from_now, Time.now])
end
15 years ago
def burndown(project, burn_direction = nil)
return nil unless self.has_burndown?
renamed start_date, burndown generation, parameter quotation * start_date was added to the core Issue model, overriding our start_date. The core start_date is just an alias for the effective_date, so it's not useful to us. * burndown generation. Sprint.generate_burndown will generate the burndown for all current sprints by calling +generate+ on each of them. +generate+ picks what it can from the cache and interpolates the rest. The cache can be primed by just calling +generate+ at least once a day. Calling it multiple times will have no effect _unless_ you've changed a story or task, which will cause today's cache entry to be discarded. This way you will always have the latest data. The charts being generated: - points committed: the total of points included in the sprint. This would typically always be the same from the start, but will change if stories are added or removed mid-sprint - points resolved: when a story goes to 100% completed (typically by setting its tasks to closed), the story is deemed resolved. - points accepted: when a story is closed, it is deemed to be accepted by the PO - remaining hours: total of hours remaining on all tasks - required burn rate in points/hours: the number of points/hours the team will have to burn down each day to complete all the work at the end of the sprint. A flat line is perfect, if the line starts dropping it means the team is ahead of the curve, if the line starts climbing it means the team is running behind * parameter quotation added to raw sql
15 years ago
@cached_burndown ||= Burndown.new(self, project, burn_direction)
end
renamed start_date, burndown generation, parameter quotation * start_date was added to the core Issue model, overriding our start_date. The core start_date is just an alias for the effective_date, so it's not useful to us. * burndown generation. Sprint.generate_burndown will generate the burndown for all current sprints by calling +generate+ on each of them. +generate+ picks what it can from the cache and interpolates the rest. The cache can be primed by just calling +generate+ at least once a day. Calling it multiple times will have no effect _unless_ you've changed a story or task, which will cause today's cache entry to be discarded. This way you will always have the latest data. The charts being generated: - points committed: the total of points included in the sprint. This would typically always be the same from the start, but will change if stories are added or removed mid-sprint - points resolved: when a story goes to 100% completed (typically by setting its tasks to closed), the story is deemed resolved. - points accepted: when a story is closed, it is deemed to be accepted by the PO - remaining hours: total of hours remaining on all tasks - required burn rate in points/hours: the number of points/hours the team will have to burn down each day to complete all the work at the end of the sprint. A flat line is perfect, if the line starts dropping it means the team is ahead of the curve, if the line starts climbing it means the team is running behind * parameter quotation added to raw sql
15 years ago
def self.generate_burndown(only_current = true)
if only_current
conditions = ['? BETWEEN start_date AND effective_date', Date.today]
else
conditions = '1 = 1'
renamed start_date, burndown generation, parameter quotation * start_date was added to the core Issue model, overriding our start_date. The core start_date is just an alias for the effective_date, so it's not useful to us. * burndown generation. Sprint.generate_burndown will generate the burndown for all current sprints by calling +generate+ on each of them. +generate+ picks what it can from the cache and interpolates the rest. The cache can be primed by just calling +generate+ at least once a day. Calling it multiple times will have no effect _unless_ you've changed a story or task, which will cause today's cache entry to be discarded. This way you will always have the latest data. The charts being generated: - points committed: the total of points included in the sprint. This would typically always be the same from the start, but will change if stories are added or removed mid-sprint - points resolved: when a story goes to 100% completed (typically by setting its tasks to closed), the story is deemed resolved. - points accepted: when a story is closed, it is deemed to be accepted by the PO - remaining hours: total of hours remaining on all tasks - required burn rate in points/hours: the number of points/hours the team will have to burn down each day to complete all the work at the end of the sprint. A flat line is perfect, if the line starts dropping it means the team is ahead of the curve, if the line starts climbing it means the team is running behind * parameter quotation added to raw sql
15 years ago
end
Version.where(conditions).each(&:burndown)
end
def impediments(project)
# for reasons beyond me,
# the default_scope needs to be explicitly applied.
Impediment.default_scope.where(fixed_version_id: self, project_id: project)
end
15 years ago
end