ari/jobsworth

View on GitHub
app/models/abstract_task.rb

Summary

Maintainability
F
4 days
Test Coverage
# encoding: UTF-8
require 'active_record_extensions'
# this is abstract class for Task and Template
class AbstractTask < ActiveRecord::Base
  self.table_name = 'tasks'

  OPEN=0
  CLOSED=1
  WILL_NOT_FIX=2
  INVALID=3
  DUPLICATE=4
  MAX_STATUS=4

  belongs_to :company
  belongs_to :project
  belongs_to :milestone

  has_many :users, :through => :task_users, :source => :user
  has_many :owners, :through => :task_owners, :source => :user
  has_many :watchers, :through => :task_watchers, :source => :user

  #task_watcher and task_owner is subclasses of task_user
  has_many :task_users, :dependent => :destroy, :foreign_key => 'task_id'
  has_many :task_watchers, :dependent => :destroy, :foreign_key => 'task_id'
  has_many :task_owners, :dependent => :destroy, :foreign_key => 'task_id'

  has_and_belongs_to_many :dependencies, -> { order('dependency_id') }, :class_name => 'AbstractTask', :join_table => 'dependencies', :association_foreign_key => 'dependency_id', :foreign_key => 'task_id', :select => 'tasks.*'
  has_and_belongs_to_many :dependants, -> { order('task_id') }, :class_name => 'AbstractTask', :join_table => 'dependencies', :association_foreign_key => 'task_id', :foreign_key => 'dependency_id', :select => 'tasks.*'

  has_many :attachments, :class_name => 'ProjectFile', :dependent => :destroy, :foreign_key => 'task_id'
  has_many :scm_changesets, -> { where('task_id IS NOT NULL') }, :dependent => :destroy, :foreign_key => 'task_id'

  belongs_to :creator, :class_name => 'User', :foreign_key => 'creator_id'
  belongs_to :old_owner, :class_name => 'User', :foreign_key => 'user_id'

  has_and_belongs_to_many :tags, :join_table => 'task_tags', :foreign_key => 'task_id'

  has_many :task_property_values, -> { includes(:property) }, :dependent => :destroy, :foreign_key => 'task_id'
  accepts_nested_attributes_for :task_property_values, :allow_destroy => true

  has_many :task_customers, :dependent => :destroy, :foreign_key => 'task_id'
  has_many :customers, -> { order('customers.name asc') }, :through => :task_customers
  adds_and_removes_using_params :customers

  has_many :todos, -> { order('completed_at IS NULL desc, completed_at desc, position') }, :dependent => :destroy, :foreign_key => 'task_id'
  accepts_nested_attributes_for :todos

  has_and_belongs_to_many :resources, :join_table => 'resources_tasks', :foreign_key => 'task_id'

  has_many :work_logs, -> { order('started_at asc') }, :dependent => :destroy, :foreign_key => 'task_id'
  has_many :event_logs, :as => :target

  has_many :sheets, :foreign_key => 'task_id'
  has_one :ical_entry, :foreign_key => 'task_id'

  has_and_belongs_to_many :email_addresses, :join_table => 'email_address_tasks', :foreign_key => 'task_id'

  validates_length_of :name, :maximum => 200, :allow_nil => true
  validates_presence_of :name
  validates_presence_of :company
  validates_presence_of :project_id
  validates_uniqueness_of :task_num, :scope => 'company_id', :on => :update
  validate :validate_properties
  
  before_validation :set_default_properties_for_new_task
  before_create :set_task_num
  after_create :schedule_tasks
  after_save :reschedule_tasks

  def self.accessed_by(user)
    readonly(false).joins(
        'join projects on
        tasks.project_id = projects.id
       join project_permissions on
        project_permissions.project_id = projects.id
      join users on
        project_permissions.user_id = users.id'
    ).where(
        'users.id = ? and
      (
        project_permissions.can_see_unwatched = ? or
        users.id in
          (select task_users.user_id from task_users where task_users.task_id=tasks.id)
      )',
        user.id,
        true
    )
  end

  def self.all_accessed_by(user)
    readonly(false).joins(
        'join project_permissions on
        project_permissions.project_id = tasks.project_id
      join users as project_permission_users on
        project_permissions.user_id = project_permission_users.id'
    ).where(
        'project_permission_users.id= ? and
      (
        project_permissions.can_see_unwatched = ? or
        project_permission_users.id in
          (select task_users.user_id from task_users where task_users.task_id=tasks.id)
      )',
        user.id,
        true
    )
  end

  #let children redefine read statuses
  def set_task_read(user, status=true)
    ;
  end

  def unread?(user)
    ;
  end

  def has_milestone?
    self.milestone_id != nil and self.milestone_id != 0
  end

  def escape_twice(attr)
    h(String.new(h(attr)))
  end

  def billing_enabled?
    # fallback to value from company on tasks/new where project is not already assigned.
    if new_record? && project.blank?
      company.try :use_billing
    else
      project.try :billing_enabled?
    end
  end

  def to_tip(options = {})
    user = options[:user]
    utz = user.tz

    unless @tip
      owners = t('tasks.no_one')
      owners = self.users.collect { |u| u.name }.to_sentence if self.users.present?

      res = "<table id=\"task_tooltip\" cellpadding=0 cellspacing=0>"
      res << "<tr><th>#{human_name(:summary)}</td><td>#{escape_twice(self.name)}</tr>"
      res << "<tr><th>#{human_name(:project)}</td><td>#{escape_twice(self.project.full_name)}</td></tr>"
      res << "<tr><th>#{human_name(:Tags)}</td><td>#{escape_twice(self.full_tags_without_links)}</td></tr>" unless self.full_tags_without_links.blank?
      res << "<tr><th>#{human_name(:assigned_to)}</td><td>#{escape_twice(owners)}</td></tr>"
      res << "<tr><th>#{human_name(:resolution)}</td><td>#{human_value(self.status_type)}</td></tr>"
      res << "<tr><th>#{human_name(:milestone)}</td><td>#{escape_twice(self.milestone.name)}</td></tr>" if self.milestone_id.to_i > 0
      res << "<tr><th>#{human_name(:completed)}</td><td>#{I18n.l(utz.utc_to_local(self.completed_at), format: user.date_format)}</td></tr>" if self.completed_at
      res << "<tr><th>#{human_name(:due_date)}</td><td>#{I18n.l(utz.utc_to_local(due), format: user.date_format)}</td></tr>" if self.due
      unless self.dependencies.empty?
        res << "<tr><th valign=\"top\">#{human_name(:dependencies)}</td><td>#{self.dependencies.collect { |t| escape_twice(t.issue_name) }.join('<br />')}</td></tr>"
      end
      unless self.dependants.empty?
        res << "<tr><th valign=\"top\">#{human_name(:depended_on_by)}</td><td>#{self.dependants.collect { |t| escape_twice(t.issue_name) }.join('<br />')}</td></tr>"
      end
      res << "<tr><th>#{human_name(:progress)}</td><td>#{TimeParser.format_duration(self.worked_minutes)} / #{TimeParser.format_duration(self.duration.to_i)}</tr>"
      res << "<tr><th>#{human_name(:description)}</th><td class=\"tip_description\">#{escape_twice(self.description_wrapped).gsub(/\n/, '<br/>').gsub(/\"/, '&quot;')}</td></tr>" unless self.description.blank?
      res << '</table>'
      @tip = res.gsub(/\"/, '&quot;')
    end
    @tip
  end

  def resolved?
    status != 0
  end

  def open_or_closed
    resolved? ? 'closed' : 'open'
  end

  # define open?, closed?, will_not_fix?, invalid?, duplicate? predicates
  ['OPEN', 'CLOSED', 'WILL_NOT_FIX', 'INVALID', 'DUPLICATE'].each do |status_name|
    define_method(status_name.downcase + '?') { status == self.class.const_get(status_name) }
  end

  def done?
    self.resolved? && self.completed_at != nil
  end

  def overdue?
    self.due_date ? (self.due_date.to_time <= Time.now.utc) : false
  end

  ###
  # This method return due_date - duration
  # It used only to display task in calendar. User should not start work on task when start_date come.
  # For date when user should start work on task we have schedule controller.
  # Again, do not use this method outside calendar view. And this method should be removed when schedule code will be fixed.
  ###
  def start_date
    return due_date if (duration.nil? or due_date.nil?)
    due_date - (duration/(60*8)).to_i.days
  end

  def target_date
    due_at || milestone.try(:due_at)
  end

  alias_method :due_date, :target_date
  alias_method :due, :due_date

  def full_name
    if self.project
      [ERB::Util.h(self.project.full_name), full_tags].join(' / ').html_safe
    else
      ''
    end
  end

  def full_name_without_links
    [self.project.full_name, self.full_tags_without_links].join(' / ')
  end

  def full_tags_without_links
    self.tags.collect { |t| t.name.capitalize }.join(' / ')
  end

  def issue_name
    "[##{self.task_num}] #{self[:name]}"
  end

  def issue_num
    if self.status > 0
      "<strike>##{self.task_num}</strike>".html_safe
    else
      "##{self.task_num}"
    end
  end

  def status_name
    "#{self.issue_num} #{self.name}"
  end

  def status_type
    self.company.statuses[self.status].name
  end

  def self.status_types
    Company.first.statuses.all.collect { |a| a.name }
  end

  def owners_to_display
    o = self.owners.collect { |u| u.name }.join(', ')
    o = 'Unassigned' if o.nil? || o == ''
    o
  end

  def set_tags=(tagstring)
    return false if (tagstring.nil? or tagstring.gsub(' ', '') == self.tagstring.gsub(' ', ''))
    self.tags.clear
    tagstring.split(',').each do |t|
      tag_name = t.downcase.strip

      if tag_name.length == 0
        next
      end

      tag = Company.find(self.company_id).tags.find_or_create_by(name: tag_name)
      self.tags << tag unless self.tags.include?(tag)
    end
    self.company.tags.first.save unless self.company.tags.first.nil? #ugly, trigger tag save callback, needed to cache sweeper
    true
  end

  def tagstring
    tags.map { |t| t.name }.join(', ')
  end

  def default_duration
    self.project.nil? ? 60 : (self.project.default_estimate * 60).to_i
  end

  def to_s
    self.name
  end

  def description_wrapped
    if description.blank?
      nil
    else
      truncate(word_wrap(self.description, :line_width => 80), :length => 1000)
    end
  end

  def css_classes
    unless @css
      @css= if self.open?
              ''
            elsif self.closed?
              ' closed'
            else
              ' invalid'
            end
    end
    @css
  end

  def todo_status
    if todos.empty?
      I18n.t 'tasks.no_todos'
    else
      "[#{sprintf('%.2f%%', todos.select { |t| t.completed_at }.size / todos.size.to_f * 100.0)}]"
    end
  end

  # Sets up custom properties using the given form params
  def properties=(params)
    ids=[]
    attributes= params.collect { |prop_id, val_id|
      # task_property_values may be changed elsewhere
      # discards the cached copy of task_property_values
      # reload from the database to avoid duplicate insert conflicts
      task_property_value= task_property_values(true).find_by(:property_id => prop_id)
      if task_property_value.nil?
        hash = {:property_id => prop_id, :property_value_id => val_id}
      else
        ids << task_property_value.id
        hash = {:id => task_property_value.id}
        if val_id.blank?
          hash[:_destroy] = 1
        else
          hash[:property_id] = prop_id
          hash[:property_value_id] = val_id
        end
      end
      hash
    }
    attributes += (self.task_property_values.collect(&:id) - ids).collect { |id| {:id => id, :_destroy => 1} }
    self.task_property_values_attributes = attributes
  end

  #set default properties for new task
  def set_default_properties
    task_property_values.clear
    self.company.properties.each do |property|
      task_property_values.build(:property_id => property.id, :property_value_id => property.default_value.id) unless property.default_value.nil?
    end
  end

  def set_property_value(property, property_value)
    # remove the current one if it exists
    existing = task_property_values.detect { |tpv| tpv.property == property }
    if existing and existing.property_value != property_value
      task_property_values.delete(existing)
    end

    if property_value
      # only create a new one if property_value is set
      task_property_values.create(:property_id => property.id, :property_value_id => property_value.id)
    end
  end

  # Returns the value of the given property for this task
  def property_value(property)
    return unless property

    tpv = task_property_values.detect { |tpv| tpv.property.id == property.id }
    tpv.property_value if tpv
  end

  def set_users_dependencies_resources(params, current_user)
    set_users(params)
    set_dependency_attributes(params[:dependencies], current_user)
    set_resource_attributes(params[:resource])
    self.attachments.find(params[:delete_files]).each { |file| file.destroy } rescue nil
    self.updated_by_id = current_user.email_addresses.first.id
    self.creator_id = current_user.id if creator_id.nil?
  end

  # Custom validation for tasks.
  def validate_properties
    company.properties.mandatory.each do |p|
      unless property_value(p)
        message = [p.name, I18n.t('activerecord.errors.messages.blank')].join ' '
        errors.add(:base, message)
      end
    end
  end

  def create_attachments(files_array, current_user)
    attachments =
        if files_array.blank? || files_array.reject(&:blank?).empty?
          []
        else
          files_array.map do |tmp_file|
            next if tmp_file.is_a?(String)
            normalize_filename(tmp_file)
            add_attachment(tmp_file, current_user)
          end.compact
        end

    attachments
  end

  def add_attachment(file, user)
    uri = Digest::MD5.hexdigest(file.read)
    if self.attachments.where(:uri => uri).count == 0
      self.attachments.create(
          :company => self.company,
          :customer => self.project.customer,
          :project => self.project,
          :user => user,
          :file => file,
          :uri => uri
      )
    end
  end

  def statuses_for_select_list
    company.statuses.collect { |s| [s.name] }.each_with_index { |s, i| s<< i }
  end

  def unknown_emails
    email_addresses.map { |ea| ea.email }.join(', ')
  end

  def unknown_emails=(emails)
    email_addresses.clear
    (emails || '').split(/$| |,/).map { |email| email.strip.empty? ? nil : email.strip }.compact.each { |email|
      ea= EmailAddress.find_or_create_by(:email => email)
      self.email_addresses<< ea
    }
  end

  def task_due_calculation(due_at, user)
    begin
      # Only care about the date part, parse the input date string into DateTime in UTC.
      # Later, the date part will be converted from DateTime to string display in UTC, so that it doesn't change.
      format = "#{user.date_format}"
      due_date = DateTime.strptime(due_at, format).ago(-12.hours)
    rescue
    end
    self.due_at = due_date unless due_date.nil?
  end

  # TODO: WTF is this motherfucking huge shit montain?
  # log task changes, worktimes, comments and update task
  def self.update(task, params, user)
    old_tags = task.tags.collect { |t| t.name }.sort.join(', ')
    old_deps = task.dependencies.collect { |t| "[#{t.issue_num}] #{t.name}" }.sort.join(', ')
    old_owner = task.owners.first
    old_users = task.owners.collect { |u| u.id }.sort.join(',')
    old_users ||= '0'
    old_project_id = task.project_id
    old_project_name = task.project.name
    old_task = task.dup

    task.send(:do_update, params, user)

    # event_log stores task property changes
    event_log = EventLog.new(:event_type => EventLog::TASK_MODIFIED, :user => user, :company => user.company, :project => task.project)

    body = ''
    body << ((old_task[:name] != task[:name]) ? ('- Name:'.html_safe + "#{old_task[:name]} " + '->'.html_safe + " #{task[:name]}\n") : '')
    body << ((old_task.description != task.description) ? "- Description changed\n".html_safe : '')

    assigned_ids = (params[:assigned] || [])
    assigned_ids = assigned_ids.uniq.collect { |u| u.to_i }.sort.join(',')
    if old_users != assigned_ids
      task.users.reload
      new_name = task.owners.empty? ? 'Unassigned' : task.owners.collect { |u| u.name }.join(', ')
      body << "- Assignment: #{new_name}\n"
      event_log.event_type = EventLog::TASK_ASSIGNED

      task.mark_as_unread(user)

      # re-schedule old owner and new owner's task list in case of assignee change
      #
      # NOTE: Normally one task only have one owner. If a task has more than one user, only re-schedule tasks of the first user.
      task.owners.reload.first.update_column(:need_schedule, true) if task.owners.count > 0
      old_owner.update_column(:need_schedule, true) if old_owner
    end

    if old_project_id != task.project_id
      body << "- Project: #{old_project_name} -> #{task.project.name}\n"
      WorkLog.where("task_id = #{task.id}").update_all("customer_id = #{task.project.customer_id}, project_id = #{task.project_id}")
      ProjectFile.where("task_id = #{task.id}").update_all("customer_id = #{task.project.customer_id}, project_id = #{task.project_id}")
    end

    old_duration = TimeParser.format_duration(old_task.duration)
    new_duration = TimeParser.format_duration(task.duration)

    body << ((old_task.duration != task.duration) ? "- Estimate: #{old_duration} -> #{new_duration}\n".html_safe : '')

    if old_task.milestone != task.milestone
      old_name = 'None'
      unless old_task.milestone.nil?
        old_name = old_task.milestone.name
        old_task.milestone.update_counts
        old_task.milestone.update_status
      end

      new_name = 'None'
      new_name = task.milestone.name unless task.milestone.nil?
      body << "- Milestone: #{old_name} -> #{new_name}\n"
    end

    if old_task.due_at != task.due_at
      old_name = new_name = 'None'
      old_name = I18n.l(user.tz.utc_to_local(old_task.due_at), format: '%A, %d %B %Y') unless old_task.due_at.nil?
      new_name = I18n.l(user.tz.utc_to_local(task.due_at), format: '%A, %d %B %Y') unless task.due_at.nil?

      body << "- Due: #{old_name} -> #{new_name}\n".html_safe
    end

    new_tags = task.tags.collect { |t| t.name }.sort.join(', ')
    if old_tags != new_tags
      body << "- Tags: #{new_tags}\n"
    end

    new_deps = task.dependencies.collect { |t| "[#{t.issue_num}] #{t.name}" }.sort.join(', ')
    if old_deps != new_deps
      body << "- Dependencies: #{(new_deps.length > 0) ? new_deps : I18n.t('shared.none')}"
    end

    if old_task.status != task.status
      body << "- Resolution: #{old_task.status_type} -> #{task.status_type}\n"

      if task.resolved? && old_task.status != task.status
        event_log.event_type = EventLog::TASK_MODIFIED
      end

      if task.completed_at && old_task.completed_at.nil?
        event_log.event_type = EventLog::TASK_COMPLETED
      end

      if !task.resolved? && old_task.resolved?
        event_log.event_type = EventLog::TASK_REVERTED
      end

      # task resolution change, reschedule user's task list
      task.owners.first.update_column(:need_schedule, true) if task.owners.count > 0
    end

    files = task.create_attachments(params['tmp_files'], user)
    files.each do |file|
      body << "- Attached: #{file.file_file_name}\n"
    end
    event_log.body = body
    event_log.target = task
    event_log.save! unless event_log.body.blank?

    params_for_work_log_and_comment = ActionController::Parameters.new(
        {
            work_log: params.fetch(:work_log, {}).permit(:started_at, :customer_id, :duration, :body, :access_level_id),
            comment: params[:comment]
        }
    )
    # work_log stores worktime & comment
    work_log = WorkLog.build_work_added_or_comment(task, user, params_for_work_log_and_comment)
    if work_log
      work_log.event_log.event_type = event_log.event_type unless event_log.body.blank?
      work_log.save!
      work_log.notify(files) if work_log.comment?
    end
  end

  def time_total
    duration + worked_minutes
  end

  private

  def full_tags
    self.tags.collect { |t| "<a href=\"/tasks?tag=#{ERB::Util.h t.name}\" class=\"description\">#{ERB::Util.h t.name.capitalize.gsub(/\"/, '&quot;'.html_safe)}</a>" }.join(' / ').html_safe
  end

  def set_task_num
    self.task_num = nil
    max = self.class.connection.execute("SELECT * FROM (SELECT 1 + coalesce((SELECT max(task_num) FROM tasks WHERE company_id ='#{self.company_id}'), 0)) AS max").first.first
    # The value of above statement varies across database drivers,
    # for mysql2, max is already at the value, for other drivers,
    # we'll need to call `last`.
    max = max.last if max.is_a? Array
    self.task_num = max
  end

  ###
  # Sets the owners/watchers of this task from ids.
  # Existing records WILL  be cleared by this method.
  ###
  def set_user_ids(relation, ids)
    return if ids.nil?

    relation.destroy_all

    ids.each do |o|
      next if o.to_i == 0
      u = company.users.find(o.to_i)
      relation.create(:user => u, :task => self)
    end
  end

  ###
  # Sets up any task owners or watchers from the given params.
  # Any existings ones not in the given params will be removed.
  ###
  def set_users(params)
    all_users = params[:users] || []
    owners = params[:assigned] || []
    emails = params[:unknowns] || []
    watchers = all_users - owners
    set_user_ids(self.task_owners, owners)
    set_user_ids(self.task_watchers, watchers)
    self.unknown_emails = emails.join(',')
  end

  ###
  # Sets up any links to resources that should be attached to this
  # task.
  # Clears any existings links to resources.
  ###
  def set_resource_attributes(params)
    return unless params

    resources.clear

    ids = params[:name].split(',')
    ids += params[:ids] if params[:ids] and params[:ids].any?

    ids.each do |id|
      self.resources << company.resources.find(id)
    end
  end

  ###
  # Sets the dependencies of this this from dependency_params.
  # Existing and unused dependencies WILL be cleared by this method,
  # only if user has access to this dependencies
  ###
  def set_dependency_attributes(dependency_params, user)
    return if dependency_params.nil?

    new_dependencies = []
    dependency_params.each do |d|
      d.split(',').each do |dep|
        dep.strip!
        next if dep.to_i == 0
        t = self.class.accessed_by(user).find_by(:task_num => dep)
        new_dependencies << t if t
      end
    end

    removed = self.dependencies.accessed_by(user) - new_dependencies
    self.dependencies.delete(removed)

    new_dependencies.each do |t|
      existing = self.dependencies.detect { |d| d.id == t.id }
      self.dependencies << t unless existing
    end

    self.save
  end

  def normalize_filename(file)
    file.original_filename.gsub!(' ', '_')
    file.original_filename.gsub!(/[^a-zA-Z0-9_\.]/, '')
  end

  # update task from params
  def do_update(params, user)
    if self.wait_for_customer and !params[:comment].blank?
      self.wait_for_customer = false
      params[:task].delete(:wait_for_customer)
    end

    self.attributes = params[:task]

    if self.service_id == -1
      self.isQuoted = true
      self.service_id = nil
    else
      self.isQuoted = false
    end

    self.task_due_calculation(params, self)
    self.duration = TimeParser.parse_time(params[:task][:duration]) if (params[:task] && params[:task][:duration])

    if self.resolved? && self.completed_at.nil?
      self.completed_at = Time.now.utc
    end

    if !self.resolved? && !self.completed_at.nil?
      self.completed_at = nil
    end

    self.set_users_dependencies_resources(params, user)

    self.save!

    self
  end

  # new task added, re-schedule user's task list
  def schedule_tasks
    unless self.owners.count > 0 and !self.resolved?
      self.estimate_date = nil
      return
    end

    # add a delayed job to schedule tasks
    self.owners.first.update_column(:need_schedule, true) if self.owners.any?
  end

  def reschedule_tasks
    if status == 1
      # run rescheduling for all dependencies of updated task if it was closed
      dependencies.includes(:owners).each do |dependency|
        dependency.owners.includes(:work_plan).each do |owner|
          owner.schedule_tasks(save: true)
        end
      end
    end
    # run rescheduling only for updated task if it isn't closed
    owners.includes(:work_plan).each do |owner|
      owner.schedule_tasks(save: true)
    end
  end

  def set_default_properties_for_new_task
    if new_record? && !task_property_values.present?
      company.properties.mandatory.each do |property|
        task_property_values.build(property_id: property.id,
                                   property_value_id: property.default_value.id)
      end
    end
  end

end


# == Schema Information
#
# Table name: tasks
#
#  id                 :integer(4)      not null, primary key
#  name               :string(200)     default(""), not null
#  project_id         :integer(4)      default(0), not null
#  position           :integer(4)      default(0), not null
#  created_at         :datetime        not null
#  due_at             :datetime
#  updated_at         :datetime        not null
#  completed_at       :datetime
#  duration           :integer(4)      default(1)
#  hidden             :integer(4)      default(0)
#  milestone_id       :integer(4)
#  description        :text
#  company_id         :integer(4)
#  priority           :integer(4)      default(0)
#  updated_by_id      :integer(4)
#  severity_id        :integer(4)      default(0)
#  type_id            :integer(4)      default(0)
#  task_num           :integer(4)      default(0)
#  status             :integer(4)      default(0)
#  creator_id         :integer(4)
#  hide_until         :datetime
#  worked_minutes     :integer(4)      default(0)
#  type               :string(255)     default("Task")
#  weight             :integer(4)      default(0)
#  weight_adjustment  :integer(4)      default(0)
#  wait_for_customer  :boolean(1)      default(FALSE)
#
# Indexes
#
#  index_tasks_on_type_and_task_num_and_company_id  (type,task_num,company_id) UNIQUE
#  tasks_company_id_index                           (company_id)
#  tasks_due_at_idx                                 (due_at)
#  index_tasks_on_milestone_id                      (milestone_id)
#  tasks_project_completed_index                    (project_id,completed_at)
#  tasks_project_id_index                           (project_id,milestone_id)
#