Skip to content

Instantly share code, notes, and snippets.

@josephlord
Last active December 23, 2015 09:09
Show Gist options
  • Select an option

  • Save josephlord/6612292 to your computer and use it in GitHub Desktop.

Select an option

Save josephlord/6612292 to your computer and use it in GitHub Desktop.
Dashboard schedule move/create task/task_group API
params: {
regular schedule element params: (name / description etc.) ...
create_task_group: {
task_group: {task group object},
tg_position: y
},
move_task_group: {
at_position: x,
to_position: y
},
create_task: {
task: {task object},
in_task_group_at_position: x,
at_position_in_task_group: a
}
move_task: {
from_task_group_at_position: x,
to_task_group_at_position: y,
from_position_in_task_group: a,
to_position_in_destination_task_group: b
}
}
# == Schema Information
# Schema version: 20130621212950
#
# Table name: project_schedules
#
# id :integer not null, primary key
# schedule_id :integer not null
# pricing_ratecard_id :integer not null
# name :string(200) not null
# description :string(255) default(""), not null
# version :integer not null
# active_from_time :datetime not null
# replaced_at_time :datetime not null
# created_by_user_id :integer not null
# lead_consultant_user_id :integer not null
# read_access_group_id :integer not null
# write_access_group_id :integer not null
# schedule_stage_id :integer not null
# outcomes :string(255) default(""), not null
# evidence :string(255) default(""), not null
# schedule_type_id :integer not null
# schedule_cost_cap :integer not null
# created_at :datetime not null
# updated_at :datetime not null
# editable :boolean default(TRUE), not null
# previous_version :integer
# project_id :integer not null
#
# Indexes
#
# index_project_schedules_on_active_from_time (active_from_time)
# index_project_schedules_on_replaced_at_time (replaced_at_time)
# index_project_schedules_on_schedule_id (schedule_id)
# index_project_schedules_on_schedule_id_and_version (schedule_id,version) UNIQUE
# index_project_schedules_on_schedule_type_id (schedule_type_id)
#
require 'utilities/time_constants'
include Utilities
class Project::Schedule < ActiveRecord::Base
include ActiveModel::ForbiddenAttributesProtection
validates :schedule_id, presence: true
validates :lead_consultant, presence: true
validates :created_by, presence: true
validates :schedule_type, presence: true
validates :stage, presence: true
validates :read_access_group, presence: true
validates :write_access_group, presence: true
validates :ratecard, presence: true
validates :name, presence: true
validates :version, presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :active_from_time, presence: true
validates :replaced_at_time, presence: true
validate :active_time_validation
validate :version_unique_for_schedule
validate :schedule_id_validation
validate :edit_lock
# validates :version, uniqueness: true, if: same_schedule?
# has_one created_by, class: User::User
belongs_to :project, class_name: 'Project::Project', foreign_key: :project_id
belongs_to :lead_consultant, class_name: 'User::User', foreign_key: :lead_consultant_user_id
belongs_to :created_by, class_name: 'User::User', foreign_key: :created_by_user_id
belongs_to :schedule_type, class_name: 'Project::ScheduleType', foreign_key: :schedule_type_id
belongs_to :stage, class_name: 'Project::ScheduleStage', foreign_key: :schedule_stage_id
belongs_to :read_access_group, class_name: 'User::AccessGroup', foreign_key: :read_access_group_id
belongs_to :write_access_group, class_name: 'User::AccessGroup', foreign_key: :write_access_group_id
belongs_to :ratecard, class_name: 'Pricing::Ratecard', foreign_key: :pricing_ratecard_id
has_many :task_groups, -> { order :schedule_position }, class_name: 'Project::TaskGroup', foreign_key: :project_schedule_id
has_many :tasks, through: :task_groups
has_many :work_records, through: :tasks
has_many :task_resource_plans, through: :tasks
#Scope
def self.current_versions
where("active_from_time <= now()").where("replaced_at_time > now()").order(:name)
end
require 'pry'
# Returns the absolute value for schedule_position to be set on a task group to insert it
# in the requested position
def make_room_for_task_group(position)
current_occupant = task_groups(true)[position]
current_preceding = position == 0 ? nil : task_groups[position - 1]
if (current_occupant && current_preceding &&
current_preceding.schedule_position < current_occupant.schedule_position - 1)
actual_position = current_occupant.schedule_position - 1
elsif current_occupant # Need to shift following positions away
actual_position = current_occupant.schedule_position
binding.pry
self.class.connection.execute(
"UPDATE project_task_groups
SET schedule_position = schedule_position + #{task_groups[-1].schedule_position}
WHERE schedule_position >= #{actual_position}")
task_groups(true) # Reload the cache with updated positions
end
actual_position ||= task_groups[-1].schedule_position + 1
end
def move_task_group(task_group, to_position)
current_occupant = task_groups[to_position]
if task_group.schedule_position < current_occupant.schedule_position
to_position += 1 # Need to account for the fact it will no longer be before this position
end
task_group.schedule_position = make_room_for_task_group(to_position)
task_group.save!
end
def lock_and_create_new_version!
# This function will return a duplicated record with incremented version number and dates set to NEVER
save!
transaction do
ret_val = self.dup
ret_val.id = nil
ret_val.previous_version = version
ret_val.version = Project::Schedule.where('schedule_id = ?',schedule_id).order(:version).reverse.first.version + 1
ret_val.active_from_time = TimeConstants.NEVER
ret_val.replaced_at_time = TimeConstants.NEVER
ret_val.editable = true
ret_val.save!
self.editable = false
save!
# TODO Duplicate taskgroups retaining same tasks associated with this schedule where work has already started and creating
# new deep copy versions of the tasks where work has not started so that the task resource plans can be updated
# TODO Tests for the same
ret_val
end
end
def self.get_active_from_schedule_id(schedule_id)
Project::Schedule.where('schedule_id = ?', schedule_id).order(:replaced_at_time).reverse.first
end
def get_active
Project::Schedule.get_active_from_schedule_id(schedule_id)
end
def set_active!
#This will get the time and set it as the replaced at of the now replaced version
#It will also trigger a save of this schedule and will therefore also trigger validation before the commit happens
Project::Schedule.transaction do
current_active = get_active
# Use database time in case there is a difference between it and the host running the frontend.
change_time = ActiveRecord::Base.connection.select_value('SELECT now()').to_datetime
unless current_active.nil? || current_active.replaced_at_time < change_time
if current_active.active_from_time > change_time
raise 'Potential Database corruption or wrong clock. Existing schedule becomes active in the future - Please contact an administrator'
end
current_active.replaced_at_time = change_time
current_active.save!(validate:false) # Known minor change to times so should not need full validity check.
end
self.active_from_time = change_time
self.replaced_at_time = TimeConstants.FOREVER
save!
self.editable = false
save!
end
end
def self.new_schedule_id
ActiveRecord::Base.connection.select_value("SELECT nextval('schedule_id_sequence')").to_i
end
def schedule_id_validation
current_max_id = ActiveRecord::Base.connection.select_value("SELECT last_value FROM schedule_id_sequence").to_i
return unless !schedule_id || schedule_id > current_max_id
errors.add(:schedule_id, "Exceeds the sequence it should be generated from")
end
def self.new_schedule
#This will create a new schedule record with a unique schedule_id. Appropriate defaults will be set where appropriate
ret_val = self.new schedule_id: new_schedule_id
ret_val.active_from_time = TimeConstants.NEVER
ret_val.replaced_at_time = TimeConstants.NEVER
ret_val.version = 0
ret_val
end
def active_time_validation
return unless active_from_time && replaced_at_time
return if active_from_time == TimeConstants.NEVER
if active_from_time > replaced_at_time
errors.add(:active_from_time, "can't be greater than replaced_at_time")
end
if active_from_time > 1.minutes.from_now
errors.add(:active_from_time, "can't be in the future")
end
# May skip at this level if performance is too low
if active_from_time != replaced_at_time
overlapping = Project::Schedule.where('schedule_id =?', schedule_id ).where('replaced_at_time > ?', active_from_time ).where('active_from_time < ?', replaced_at_time)
overlapping = overlapping.where('id != ?', id) unless id.nil?
errors.add(:active_from_time, "active times overlap") if overlapping.exists?
end
end
def version_unique_for_schedule
return if schedule_id.nil? || version.nil?
duplicates = Project::Schedule.where('schedule_id = ?', schedule_id).where('version = ?', version)
duplicates = duplicates.where('id != ?', id) unless id.nil?
if duplicates.exists?
errors.add(:version, "duplicate version numbers within schedule_id")
end
end
def edit_lock
permittedChanges = ['replaced_at_time', 'editable']
# ensure that all changed elements are on the white list for this state.
unless editable || changed & permittedChanges == changed
# add an error for each changed attribute absent from the white list for this state.
(changed - permittedChanges).each do |attr|
errors.add attr, "Locked while not editable"
end
end
end
def active?
# TODO make this a DB transaction to use DB time
reload
active_from_time <= 0.seconds.ago && 0.seconds.ago < replaced_at_time
end
def task_group_list
self.task_groups.order(:schedule_position)
end
end
#TODO Constrain in DB so that only one version can be active - by times. Postpone, may become easier in Postgres 9.2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment