class Timesheet

File

timesheet.rb

(C)

Hipposoft 2008

Purpose

Describe the behaviour of Timesheet objects. See below for more details.


07-Jan-2008 (ADH): Created.

Constants

AUTO_SORT_FIELD_LIMIT
DEFAULT_SORT_COLUMN
DEFAULT_SORT_DIRECTION
DEFAULT_SORT_ORDER
USED_RANGE_COLUMN

Public Class Methods

allowed_range( accurate = false ) click to toggle source

Return a range of years allowed for a timesheet. Optionally pass ‘true’ if you want an actual Date object range rather than just a year range.

# File app/models/timesheet.rb, line 50
def self.allowed_range( accurate = false )
  if ( WorkPacket.significant.count.zero? )
    range = ( Date.current.year - 2 )..( Date.current.year + 2 )
  else
    range = self.used_range
    range = ( range.first - 2 )..( range.last + 2 )
  end

  if ( accurate )
    ( Date.new( range.first, 1, 1 ) )..( Date.new( range.last, 12, 31 ) )
  else
    range
  end
end
date_for( year, week_number, day_number, as_date = false ) click to toggle source

Class method; as #date_for, but pass explicitly the year, week number and day number of interest. If an optional fourth parameter is ‘true’, returns a Date object rather than a string.

# File app/models/timesheet.rb, line 360
def self.date_for( year, week_number, day_number, as_date = false )

  # Get the date of Monday, week 1 in this timesheet's year.
  # Add as many days as needed to get to Monday of the week
  # for this timesheet.

  date = Timesheet.get_first_week_start( year )
  date = date + ( ( week_number - 1 ) * 7 )

  # Add in the day number offset.

  date += TimesheetRow::DAY_ORDER.index( day_number )

  # Return in DD-Mth-YYYY format, or as a Date object?

  if ( as_date )
    return date
  else
    return date.strftime( '%d-%b-%Y' ) # Or ISO: '%Y-%m-%d'
  end
end
get_first_week_start( year ) click to toggle source

Get the date of the first day of week 1 in the given year. Note that sometimes, this can be in December the previous year. Works on commercial weeks (Mon->Sun). Returns a Date.

# File app/models/timesheet.rb, line 250
def self.get_first_week_start( year )

  # Is Jan 1st already in week 1?

  date = Date.new( year, 1, 1 )

  if ( date.cweek == 1 )

    # Yes. Check December of the previous year.

    31.downto( 25 ) do | day |
      date = Date.new( year - 1, 12, day )

      # If we encounter a date in the previous year which has a week
      # number > 1, then that's the last week of the previous year. If
      # we're on Dec 31st that means that week 1 started on Jan 1st,
      # else in December.

      if ( date.cweek > 1 )
        return ( day == 31 ? Date.new( year, 1, 1 ) : Date.new( year - 1, 12, day + 1 ) )
      end
    end

  else

    # No. Walk forward through January until we reach week 1.

    2.upto( 7 ) do | day |
      date = Date.new( year, 1, day )
      return date if ( date.cweek == 1 )
    end
  end
end
get_last_week_end( year ) click to toggle source

Get the date of the last day of the last week in the given year. Note that sometimes, this can be in January in the following year. Works on commercial weeks (Mon->Sun). Returns a Date.

# File app/models/timesheet.rb, line 288
def self.get_last_week_end( year )

  # Is Dec 31st already in week 1 for the next year?

  date = Date.new( year, 12, 31 )

  if ( date.cweek == 1 )

    # Yes. Check backwards through December to find the last day
    # in the higher week number.

    30.downto( 25 ) do | day |
      date = Date.new( year, 12, day )
      return Date.new( year, 12, day ) if ( date.cweek > 1 )
    end

  else

    # No. Check January of the following year to find the end
    # of the highest numbered week.

    1.upto( 6 ) do | day |
      date = Date.new( year + 1, 1, day )
      if ( date.cweek == 1 )
        return ( day == 1 ? Date.new( year, 12, 31 ) : Date.new( year + 1, 1, day - 1 ) )
      end
    end
  end
end
get_last_week_number( year ) click to toggle source

Get the number of the last commercial week (Mon->Sun) in the given year. This is usually 52, but is 53 for some years.

# File app/models/timesheet.rb, line 321
def self.get_last_week_number( year )

  # Is Dec 31st already in week 1 for the next year?

  date = Date.new( year, 12, 31 )

  if ( date.cweek == 1 )

    # Yes. Check backwards through December to find the last day
    # in the higher week number.

    30.downto( 25 ) do | day |
      date = Date.new( year, 12, day )
      return date.cweek if ( date.cweek > 1 )
    end

  else

    # No, so we have the highest week already.

    return date.cweek
  end
end
used_range( accurate = false ) click to toggle source

Return a range of years used by all current timesheets, or the allowed range (see above) if there are no work packets. Optionally pass ‘true’ if you want an actual Date object range rather than just a year range.

# File app/models/timesheet.rb, line 69
def self.used_range( accurate = false )
  return self.allowed_range( accurate ) if WorkPacket.significant.count.zero?

  first = WorkPacket.find_earliest_by_tasks()
  last  = WorkPacket.find_latest_by_tasks()

  if accurate
    ( first.date )..( last.date )
  else
    ( first.date.year )..( last.date.year )
  end
end

Public Instance Methods

add_row( task ) click to toggle source

Add a row to the timesheet using the given task object. Does nothing if a row containing that task is already present. The updated timesheet is not saved - the caller must do this. The new, added timesheet row object is returned, unless the task is already included in the timesheet, in which case the method returns ‘nil’.

# File app/models/timesheet.rb, line 200
def add_row( task )
  unless self.tasks.include?( task )
    timesheet_row      = TimesheetRow.new
    timesheet_row.task = task

    self.timesheet_rows.push( timesheet_row )

    return timesheet_row
  else
    return nil
  end
end
can_be_modified_by?( user ) click to toggle source

Is the given user permitted to update this timesheet? Admins can modify anything. Managers can modify their own timesheets whether committed or not, or any other timesheet provided it is not committed. Normal users can only modify their own timesheets when not committed.

Note there is no special status awarded to admin-owned timesheets; a manager can modify any not committed timesheet. This keeps the model simple. Managers are trusted to only modify timesheets they don’t own when really necessary, but they can’t revise history by changing committed data.

# File app/models/timesheet.rb, line 143
def can_be_modified_by?( user )
  if ( user.admin? )
    true
  elsif ( user.manager? )
    ( user.id == self.user.id ) or ( not self.committed )
  else
    ( user.id == self.user.id ) and ( not self.committed )
  end
end
column_sum( day_number ) click to toggle source

Count the hours across all rows on the given day number; 0 is Sunday, 1-6 Monday to Saturday.

# File app/models/timesheet.rb, line 216
def column_sum( day_number )
  sum = 0.0

  # [TODO] Slow. Surely there's a better way...?

  self.timesheet_rows.all.each do | timesheet_row |
    work_packet = WorkPacket.find_by_timesheet_row_id(
      timesheet_row.id,
      :conditions => { :day_number => day_number }
    )

    sum += work_packet.worked_hours if work_packet
  end

  return sum
end
date_for( day_number, as_date = false ) click to toggle source

Return a date string representing this timesheet on the given day number. Day numbers are odd - 0 = Sunday at the end of this timesheet’s week, while 1-6 = Monday at the start of the week through to Saturday inclusive (aligning with Ruby “cweek”). If an optional second parameter is ‘true’, returns a Date object rather than a string.

# File app/models/timesheet.rb, line 352
def date_for( day_number, as_date = false )
  Timesheet.date_for( self.year, self.week_number, day_number, as_date )
end
editable_week( nextweek ) click to toggle source

Return the next (pass ‘true’) or previous (pass ‘false’) editable week after this one, as a hash with properties ‘week_number’ and ‘timesheet’. The latter will be populated with a timesheet if there is a not committed item in the found week, or nil if the week has no associated timesheet yet. Returns nil altogether if no editable week can be found (e.g. ask for previous from week 1, or all previous weeks have committed timesheets on them).

This operation may involve many database queries so is relatively slow.

# File app/models/timesheet.rb, line 178
def editable_week( nextweek )
  discover_week( nextweek ) do | timesheet |
    ( timesheet.nil? or not timesheet.committed )
  end
end
is_permitted_for?( user ) click to toggle source

Is the given user permitted to do anything with this timesheet? Admins and managers can view anything. Normal users can only view their own timesheets.

# File app/models/timesheet.rb, line 127
def is_permitted_for?( user )
  ( user.id == self.user.id ) or ( user.privileged? )
end
showable_week( nextweek ) click to toggle source

As #editable_week, but returns weeks for ‘showable’ weeks - that is, only weeks where a timesheet owned by the current user already exists.

# File app/models/timesheet.rb, line 187
def showable_week( nextweek )
  discover_week( nextweek ) do | timesheet |
    ( not timesheet.nil? )
  end
end
start_day() click to toggle source

Return the date of the first day for this timesheet as a string augmented with week number for display purposes.

# File app/models/timesheet.rb, line 242
def start_day()
  return "#{ self.date_for( TimesheetRow::FIRST_DAY ) } (week #{ self.week_number })"
end
total_sum() click to toggle source

Count the total number of worked hours in the whole timesheet.

# File app/models/timesheet.rb, line 235
def total_sum()
  return self.work_packets.sum( :worked_hours )
end
unused_weeks() click to toggle source

Return a sorted array of week numbers which can be assigned to the timesheet. Includes the current timesheet’s already allocated week.

# File app/models/timesheet.rb, line 157
def unused_weeks()
  timesheets = Timesheet.where( :user_id => self.user_id, :year => self.year )
  used_weeks = timesheets.select( :week_number ).map( &:week_number )

  range        = 1..Timesheet.get_last_week_number( self.year )
  unused_weeks = ( range.to_a - used_weeks )
  unused_weeks.push( self.week_number ) unless ( self.week_number.nil? )

  return unused_weeks.sort()
end