fernandokosh/redmine_time_tracker

View on GitHub
app/models/time_booking.rb

Summary

Maintainability
A
2 hrs
Test Coverage
require 'redmine/i18n'
class TimeBooking < ActiveRecord::Base
  include Redmine::I18n
  unloadable

  attr_accessible :started_on, :stopped_at, :time_entry_id, :time_log_id, :project, :project_id
  belongs_to :project
  belongs_to :time_log
  belongs_to :time_entry, :dependent => :delete
  has_one :issue, :through => :time_entry
  has_one :activity, :through => :time_entry
  has_one :fixed_version, :through => :issue, :class_name => 'Version', :foreign_key => 'fixed_version_id'

  validates_presence_of :time_log_id
  validates :time_entry_id, :presence => true

  scope :visible, lambda {
    ca = []
    permission_list = [:tt_view_bookings, :tt_book_time, :tt_edit_own_bookings, :tt_edit_bookings]
    permission_list.each { |permission|
      ca << [Project.allowed_to_condition(User.current, permission, {})]
    }
    cond = [ca.map { |c| c[0] }.join(" OR ")]

    {:include => :project,
     :conditions => cond}
  }

  # check user-permissions. in some cass we need to prevent some or all of his actions
  before_update do
    # if the object changed and the user has not the permission to change every TimeLog (includes active trackers), we
    # have to change for special permissions in detail before saving the changes or undo them
    if self.changed?
      if (self.changed - ['comments', 'issue', 'project_id', 'time_entry_id', 'activity_id']).empty?
        unless permission_level > 0
          raise StandardError, l(:tt_error_not_allowed_to_change_booking) if self.user == User.current
          raise StandardError, l(:tt_error_not_allowed_to_change_foreign_booking)
        end
        # want to change more than comments only? => needs more permission!
      else
        unless permission_level > 1
          raise StandardError, l(:tt_error_not_allowed_to_change_booking) if self.user == User.current
          raise StandardError, l(:tt_error_not_allowed_to_change_foreign_booking)
        end
      end
      # special checks for project-changes
      if self.changed.include?('project_id')
        old_project = Project.where(:id => self.project_id_was).first
        new_project = Project.where(:id => self.project_id).first
        # user tries to switch the time from one project to another, so we have to check his permissions on both projects before starting the update
        raise StandardError, l(:tt_error_not_allowed_to_change_booking) unless help.permission_checker([:tt_book_time, :tt_edit_bookings], old_project) && help.permission_checker([:tt_book_time, :tt_edit_bookings], new_project) ||
            self.user == User.current && User.current.allowed_to?(:tt_edit_own_bookings, old_project) && User.current.allowed_to?(:tt_edit_own_bookings, new_project)
      end
    end
  end

  after_destroy do
    self.time_log.check_bookable
  end

  def permission_level
    case
      when User.current.allowed_to?(:tt_edit_bookings, self.project) ||
          self.user == User.current && User.current.allowed_to?(:tt_edit_own_bookings, self.project)
        2
      when self.user == User.current && User.current.allowed_to?(:tt_book_time, self.project)
        1
      else
        0
    end
  end

  def initialize(args = {}, options = {})
    ActiveRecord::Base.transaction do
      proj = help.project_from_id(args[:project_id])
      raise StandardError, l(:tt_error_not_allowed_to_book_without_project) if proj.nil?
      if help.permission_checker([:tt_book_time, :tt_edit_own_bookings, :tt_edit_bookings], proj)
        # TODO check for user-specific setup (limitations for bookable times etc)
        time_entry = create_time_entry({:project => proj, :issue => args[:issue], :user_id => args[:user_id], :comments => args[:comments], :started_on => args[:started_on], :activity_id => args[:activity_id], :hours => args[:hours]})
        super({:time_entry_id => time_entry.id, :time_log_id => args[:time_log_id], :started_on => args[:started_on], :stopped_at => args[:stopped_at], :project_id => proj.id})
      else
        raise StandardError, l(:tt_error_not_allowed_to_book_on_project)
      end
    end
  end

  # returns the hours between two timestamps
  def hours_spent(time1 = started_on, time2 = stopped_at)
    ((time2.to_i - time1.to_i) / 3600.0).to_f
  end

  def get_formatted_time
    help.time_dist2string((hours_spent*60).to_i)
  end

  def get_formatted_start_time
    format_time self.started_on, false unless self.started_on.nil?
  end

  def get_formatted_stop_time
    format_time self.stopped_at, false unless self.stopped_at.nil?
  end

  # we have to redefine some setters, to ensure a convenient way to update these attributes

  def issue=(issue)
    return if issue == self.issue # no validation or permission checks necessary if there are no changes!

    # check if project has to be updated also!
    unless issue.nil?
      if issue.project != self.project
        # if the new issue is part of another project we first have to nullify the issue, change the project and than set
        # the new issue_id otherwise updating the redmine time_entry will fail
        self.time_entry.update_attributes! :issue => nil
        self.project = issue.project
      end
    end
    # workaround to get dirty-flag working even for associated fields!
    @changed_attributes['issue'] = self.issue unless issue == self.issue
    self.time_entry.update_attributes! :issue => issue #also update TimeEntry
  end

  def issue
    if self.time_entry.issue.nil?
      l(:time_tracker_label_none)
    else
      self.time_entry.issue
    end
  end

  def issue_id
    if self.time_entry.issue.nil?
      l(:time_tracker_label_none)
    else
      self.time_entry.issue.id.to_s
    end
  end

  def fixed_version
    if self.time_entry.issue.nil? || self.time_entry.issue.fixed_version.nil?
      l(:time_tracker_label_none)
    else
      self.time_entry.issue.fixed_version
    end
  end

  def project=(project)
    return if project == self.project # no validation or permission checks necessary if there are no changes!
    raise StandardError, l(:tt_error_not_allowed_to_book_without_project) if project.nil?

    # workaround to get dirty-flag working even for associated fields!
    unless project.id == self.project_id
      if self.project.nil?
        @changed_attributes['project_id'] = nil
      else
        @changed_attributes['project_id'] = self.project.id
      end
    end
    write_attribute(:project_id, project.id)
    self.time_entry.update_attributes! :project => project #also update TimeEntry
  end

  # this method is necessary to change start and stop at the same time without leaving boundaries
  def update_time(start, stop)
    return if start == self.started_on && stop == self.stopped_at # no validation or permission checks necessary if there are no changes!

    raise StandardError, l(:error_booking_to_much_time) if start < self.time_log.started_on || start >= self.time_log.stopped_at || stop <= self.time_log.started_on || stop > self.time_log.stopped_at || start == stop

    write_attribute(:started_on, start)
    write_attribute(:stopped_at, stop)
    self.time_entry.update_attributes!(:spent_on => start, :hours => self.hours_spent) #also update TimeEntry
  end

  def user_id
    self.time_entry.user_id
  end

  def activity_id
    self.time_entry.activity_id
  end

  def activity_id=(activity_id)
    # workaround to get dirty-flag working even for associated fields!
    @changed_attributes['activity_id'] = self.activity_id unless activity_id == self.activity_id
    self.time_entry.update_attributes! :activity_id => activity_id
  end

  # following methods are necessary to use the query_patch, so we can use the powerful filter options of redmine
  # to show our booking lists => which will be the base for our invoices

  def comments
    self.time_entry.comments
  end

  def comments=(comments)
    # workaround to get dirty-flag working even for associated fields!
    @changed_attributes['comments'] = self.comments unless comments == self.comments
    self.time_entry.update_attributes! :comments => comments
  end

  def tt_booking_date
    format_date(help.in_user_time_zone self.started_on) unless self.started_on.nil?
  end

  def user
    if self.time_log.nil?
      nil
    else
      self.time_log.user
    end
  end

  def virtual?
    self.time_entry.issue.nil?
  end

  private

  def create_time_entry(args ={})
    # TODO check for user-specific setup (limitations for bookable times etc)
    # create a timeBooking to combine a timeLog-entry and a timeEntry
    time_entry = TimeEntry.create({:project => args[:project], :issue => args[:issue], :hours => args[:hours], :comments => args[:comments], :spent_on => args[:started_on], :activity_id => args[:activity_id]})
    # due to the mass-assignment security, we have to set the user_id extra
    time_entry.user_id = args[:user_id]
    time_entry.save
    time_entry
  end
end