class Period < ActiveRecord::Base extend UserSystem include UserSystem belongs_to :party acts_as_list :scope => :party, :order => :position has_many :tasks, :order => 'position, finished_at', :dependent => :destroy validates_associated :party validates_uniqueness_of :position, :scope => :party_id def validate errors.add "A period cannot end before it starts" unless end_on >= start_on end def self.find_active_or_future(check_tasks = false) periods = find(:all, :conditions => "end_on >= '#{Date.today.strftime '%Y-%m-%d'}'") if (check_tasks) ended_periods_with_open_tasks = Task.find(:all, :conditions => ['finished_at IS NULL AND period_id NOT IN (?)', periods.map{|p| p.id}]).map {|t| t.period} periods + ended_periods_with_open_tasks end periods.select {|p| p.party.includes?(current_user)} end def speed start_todo = estimate_data(start_on, true) end_todo = estimate_data(end_on, true) days = end_on - start_on return (start_todo - end_todo).to_f / days end def completed_tasks tasks.select {|task| task.completed?} end def open_tasks tasks.select {|task| task.active?} end def most_frequent_backlog all_backlogs = tasks.map {|t| t.backlog}.compact return nil if all_backlogs.empty? freq = {} all_backlogs.each do |b| freq[b.id] ||= 0 freq[b.id] += 1 end freq.to_a.sort_by {|backlog, count| -count}.first[0] end def active?(check_tasks = false) start_on <= Date.today && (end_on >= Date.today || (check_tasks && active_tasks?)) end def active_tasks? not tasks.select {|task| task.active?}.empty? end def future? start_on >= Date.today end def active_or_future?(check_tasks = false) end_on >= Date.today || (check_tasks && active_tasks?) end def passed? end_on < Date.today end def estimate_data(date, actual=false) total = BigDecimal('0') tasks.each do |task| total += task.estimate_data(date, actual) end total end def work_data(date) total = BigDecimal('0') tasks.each do |task| total += task.work_data(date) end total end def required_speed todo = estimate_data(Date.today) remaining_days = end_on - Date.today if todo == 0 0 elsif remaining_days > 0 todo / remaining_days elsif remaining_days == 0 todo else todo * (-remaining_days) end end def to_s name end def name "#{party.name}##{position}" end def dates all_dates = [] (start_on-1).upto(end_on) {|date| all_dates << date} return all_dates end def recorded_dates dates = [] if start_on > Date.today dates << (start_on-1) elsif end_on < Date.today (start_on-1).upto(end_on) {|date| dates << date} else (start_on-1).upto(Date.today) {|date| dates << date} end return dates end # Return an array with a work object per day: # [ # [, , , , , nil, nil], # [, nil, , , , nil, nil] # ] def works_for_week(week_no, with_empty_works = false, user_id = nil) first = Date.commercial(Date.today.year, week_no, 1) last = first + 6 works = tasks.map {|t| t.works_with_children}.flatten works.reject! {|work| work.hours == 0} unless with_empty_works works.reject! {|work| work.user_id != user_id} if user_id && backlog.enable_users by_day = (0..6).map do |i| works.select {|w| w.completed_at && w.completed_at.to_date == first + i}.sort do |w1, w2| if w1.completed_at != w2.completed_at w1.completed_at <=> w2.completed_at elsif w1.started_at && w2.started_at w1.started_at <=> w2.started_at elsif w1.started_at 1 else -1 end end end by_day.each {|d| d[works.size] = d[works.size]} by_line = by_day.transpose by_line = by_line.select {|l| l.compact.size > 0} by_line end def estimates? not backlogs.find {|backlog| backlog.track_todo?}.nil? end def track_times? not backlogs.find {|backlog| backlog.track_times?}.nil? end def enable_subtasks? not backlogs.find {|backlog| backlog.enable_subtasks?}.nil? end def invoice? not backlogs.find {|backlog| backlog.enable_invoicing?}.nil? end def backlogs tasks.map {|task| task.backlog}.uniq end def burn_down_graph(size) begin require 'gruff' rescue MissingSourceFile => e return File.read("public/images/rmagick_#{size}.gif") if File.exists? "public/images/rmagick_#{size}.gif" return File.read("public/images/rmagick.gif") end g = Gruff::Line.new(size) g.theme_37signals g.title = l(:burn_down_chart) + " " + name g.font = '/usr/share/fonts/bitstream-vera/Vera.ttf' g.legend_font_size = 14 g.hide_dots = true g.colors = %w{#0000ff #ff8800} if track_work? g.colors += %w{#00ff00} end if previous_period = higher_item g.colors += %w{#cccccc #cccccc} end g.colors += %w{#8888ff #d7a790} recorded_dates = self.recorded_dates observed_todo_data = get_todo_data(recorded_dates) g.data("#{l :todo} (#{l :obs})", observed_todo_data) g.data(l(:projection), projection_data(observed_todo_data)) actual_todo_data = get_todo_data(recorded_dates, true) g.data("#{l :todo}", actual_todo_data) g.data(l(:projection), projection_data(actual_todo_data)) g.data(l(:done), get_work_data(recorded_dates)) if track_work? if previous_period = higher_item g.data("#{l :previous_abr} #{l :obs}", previous_period.get_todo_data(previous_period.dates)) g.data("#{l :previous_abr}", previous_period.get_todo_data(previous_period.dates, true)) end g.minimum_value = 0 all_dates = dates labels = {1 => all_dates[1].to_s, all_dates.length-1 => all_dates.last.to_s} labels.merge({all_dates.index(Date.today) => Date.today.to_s}) if all_dates.index(Date.today) && (all_dates.index(Date.today) / all_dates.length) > 0.10 g.labels = labels # g.draw_vertical_legend g.maximum_value = (g.maximum_value.to_s[0..0].to_i + 1) * (10**Math::log10(g.maximum_value.to_i).to_i) if g.maximum_value > 0 g.to_blob end def get_todo_data(dates, actual=false) dates.map { |date| estimate_data(date, actual) } end def get_work_data(dates) dates.map { |date| work_data(date) } end def projection_data(observed_todo_data) if observed_todo_data.length <= 1 velocity = party.current_speed else velocity = (observed_todo_data[0] - observed_todo_data[-1]).to_f / (observed_todo_data.length-1) end projection_data = dates.map do |date| if date < Date.today nil else value = observed_todo_data[0] - (date-start_on+1).to_f*velocity value >= 0 ? value : 0 end end end end