hicknhack-software/redmine_hourglass

View on GitHub
app/models/hourglass/time_log.rb

Summary

Maintainability
A
1 hr
Test Coverage
module Hourglass
  class AlreadyBookedException < StandardError
  end

  class TimeLog < ApplicationRecord
    include Namespace

    belongs_to :user
    has_one :time_booking, dependent: :destroy
    has_one :time_entry, through: :time_booking

    before_save :remove_seconds

    validates_presence_of :user, :start, :stop
    validates_length_of :comments, maximum: 1024, allow_blank: true
    validate :stop_is_valid
    validate :does_not_overlap_with_other, if: [:user, :start?, :stop?]

    delegate :project, to: :time_booking, allow_nil: true

    scope :booked_on_project, lambda { |project_id|
      joins(:time_entry).where(time_entries: {project_id: project_id})
    }
    scope :with_start_in_interval, lambda { |floor, ceiling|
      where(arel_table[:start].gt(floor).and(arel_table[:start].lt(ceiling)))
    }

    scope :overlaps_with, lambda { |start, stop|
      where(arel_table[:start].lt(stop).and(arel_table[:stop].gt(start)))
    }

    def build_time_booking(args = {})
      super time_booking_arguments default_booking_arguments.merge args
    end

    def update(attributes)
      round = attributes.delete :round
      ActiveRecord::Base.transaction do
        result = super attributes
        if booked?
          DateTimeCalculations.booking_process user, start: start, stop: stop, project_id: time_booking.project_id, round: round do |options|
            time_booking.update start: options[:start], stop: options[:stop], time_entry_attributes: {hours: DateTimeCalculations.time_diff_in_hours(options[:start], options[:stop])}
            time_booking
          end
          raise ActiveRecord::Rollback unless time_booking.persisted?
        end
        result
      end
    end

    def book(attributes)
      raise AlreadyBookedException if booked?
      DateTimeCalculations.booking_process user, default_booking_arguments.merge(attributes.except(:start, :stop)) do |options|
        create_time_booking time_booking_arguments options
      end
    end

    def split(args)
      split_at = args[:split_at].change(sec: 0)
      insert_new_before, round = args.values_at :insert_new_before, :round
      return if start >= split_at || split_at >= stop
      old_time = insert_new_before ? start : stop
      ActiveRecord::Base.transaction do
        update insert_new_before ? {start: split_at, round: round} : {stop: split_at, round: round}
        new_time_log_args = insert_new_before ? {start: old_time, stop: split_at} : {start: split_at, stop: old_time}
        self.class.create new_time_log_args.merge user: user, comments: comments
      end
    end

    def join_with(other)
      return false unless joinable? other
      new_stop = other.stop
      ActiveRecord::Base.transaction do
        other.destroy
        update stop: new_stop
      end
      true
    end

    def hours
      DateTimeCalculations.time_diff_in_hours start, stop
    end

    def booked?
      time_booking.present? && time_booking.persisted?
    end

    def bookable?
      !booked?
    end

    def as_json(args = {})
      super args.deep_merge methods: :hours
    end

    def joinable?(other)
      user_id == other.user_id && stop == other.start && bookable? && other.bookable?
    end

    def self.joinable?(*ids)
      where(id: ids).order(start: :asc).reduce do |previous, time_log|
        return false unless previous.joinable?(time_log)
        time_log
      end
      true
    end

    private
    def default_booking_arguments
      {start: start, stop: stop, comments: comments, time_log_id: id, user: user}.with_indifferent_access
    end

    def time_booking_arguments(options)
      options
          .slice(:start, :stop, :time_log_id)
          .merge time_entry_attributes: time_entry_arguments(options)
    end

    def time_entry_arguments(options)
      options
          .slice(:project_id, :issue_id, :comments, :activity_id, :user, :custom_field_values)
          .merge spent_on: User.current.time_to_date(options[:start]), hours: DateTimeCalculations.time_diff_in_hours(options[:start], options[:stop])
    end

    def stop_is_valid
      errors.add :stop, :invalid if stop.present? && start.present? && stop <= start
    end

    def does_not_overlap_with_other
      errors.add :base, :overlaps unless user.hourglass_time_logs.where.not(id: id).overlaps_with(start, stop).empty?
    end

    def remove_seconds
      self.start = start.change(sec: 0) if start
      self.stop = stop.change(sec: 0) if stop
    end
  end
end