Last active
December 23, 2015 09:09
-
-
Save josephlord/6612292 to your computer and use it in GitHub Desktop.
Dashboard schedule move/create task/task_group API
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # == 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