mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
Add working hours and non working days for users
This commit is contained in:
@@ -60,6 +60,12 @@ class User < Principal
|
||||
has_one :rss_token, class_name: "::Token::RSS", dependent: :destroy
|
||||
has_many :api_tokens, class_name: "::Token::API", dependent: :destroy
|
||||
has_many :oauth_client_tokens, dependent: :destroy
|
||||
has_many :working_hours, class_name: "UserWorkingHours",
|
||||
dependent: :destroy,
|
||||
inverse_of: :user
|
||||
has_many :non_working_days, class_name: "UserNonWorkingDay",
|
||||
dependent: :destroy,
|
||||
inverse_of: :user
|
||||
|
||||
# The user might have one invitation token
|
||||
has_one :invitation_token, class_name: "::Token::Invitation", dependent: :destroy
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
class UserNonWorkingDay < ApplicationRecord
|
||||
belongs_to :user, inverse_of: :non_working_days
|
||||
|
||||
validates :date, presence: true, uniqueness: { scope: :user_id }
|
||||
|
||||
scope :for_year, ->(year) { where(date: Date.new(year, 1, 1)..Date.new(year, 12, 31)) }
|
||||
|
||||
scope :for_user, ->(user) { where(user:) }
|
||||
|
||||
scope :visible, ->(user = User.current) do
|
||||
if user.allowed_globally?(:manage_working_times)
|
||||
all
|
||||
else
|
||||
where(user:)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,72 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
class UserWorkingHours < ApplicationRecord
|
||||
belongs_to :user, inverse_of: :working_hours
|
||||
|
||||
validates :valid_from, presence: true
|
||||
validates :monday, :tuesday, :wednesday, :thursday, :friday, :saturday, :sunday,
|
||||
presence: true,
|
||||
numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 24 * 60 }
|
||||
validates :availability_factor,
|
||||
presence: true,
|
||||
numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }
|
||||
|
||||
scope :for_user, ->(user) { where(user:) }
|
||||
|
||||
scope :past, -> { where(valid_from: ...Date.current).order(valid_from: :desc) }
|
||||
scope :upcoming, -> { where(valid_from: Date.current..).order(valid_from: :asc) }
|
||||
|
||||
def self.valid_for_date(date)
|
||||
where(valid_from: ..date).order(valid_from: :desc).first
|
||||
end
|
||||
|
||||
def self.current
|
||||
valid_for_date(Date.current)
|
||||
end
|
||||
|
||||
scope :visible, ->(user = User.current) do
|
||||
if user.allowed_globally?(:manage_working_times)
|
||||
all
|
||||
else
|
||||
where(user:)
|
||||
end
|
||||
end
|
||||
|
||||
%i[monday tuesday wednesday thursday friday saturday sunday].each do |day|
|
||||
define_method("#{day}_hours") do
|
||||
public_send(day) / 60.0
|
||||
end
|
||||
|
||||
define_method("#{day}_hours=") do |hours|
|
||||
public_send("#{day}=", (hours.to_f * 60).round)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -286,6 +286,14 @@ Rails.application.reloader.to_prepare do
|
||||
{},
|
||||
permissible_on: :project_query,
|
||||
require: :loggedin
|
||||
|
||||
map.permission :manage_own_working_times,
|
||||
{},
|
||||
permissible_on: :global
|
||||
|
||||
map.permission :manage_working_times,
|
||||
{},
|
||||
permissible_on: :global
|
||||
end
|
||||
|
||||
map.project_module :work_package_tracking, order: 90 do |wpt|
|
||||
|
||||
@@ -1748,6 +1748,25 @@ en:
|
||||
users/invitation/form_model:
|
||||
principal_type: "Invitation type"
|
||||
id_or_email: "Name or email address"
|
||||
user_non_working_days:
|
||||
date: "Date"
|
||||
user_working_hours:
|
||||
valid_from: "Valid from"
|
||||
monday: "Monday"
|
||||
monday_hours: "Monday hours"
|
||||
tuesday: "Tuesday"
|
||||
tuesday_hours: "Tuesday hours"
|
||||
wednesday: "Wednesday"
|
||||
wednesday_hours: "Wednesday hours"
|
||||
thursday: "Thursday"
|
||||
thursday_hours: "Thursday hours"
|
||||
friday: "Friday"
|
||||
friday_hours: "Friday hours"
|
||||
saturday: "Saturday"
|
||||
saturday_hours: "Saturday hours"
|
||||
sunday: "Sunday"
|
||||
sunday_hours: "Sunday hours"
|
||||
availability_factor: "Availability factor"
|
||||
version:
|
||||
effective_date: "Finish date"
|
||||
sharing: "Sharing"
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddUserWorkingHours < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
create_table :user_working_hours do |t|
|
||||
t.references :user, null: false, foreign_key: true
|
||||
|
||||
t.date :valid_from, null: false, index: true
|
||||
t.integer :monday, null: false
|
||||
t.integer :tuesday, null: false
|
||||
t.integer :wednesday, null: false
|
||||
t.integer :thursday, null: false
|
||||
t.integer :friday, null: false
|
||||
t.integer :saturday, null: false
|
||||
t.integer :sunday, null: false
|
||||
t.integer :availability_factor, null: false, default: 100
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddUserNonWorkingDays < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
create_table :user_non_working_days do |t|
|
||||
t.references :user, null: false, foreign_key: true
|
||||
|
||||
t.date :date, null: false, index: true
|
||||
|
||||
t.timestamps
|
||||
|
||||
t.index %i[user_id date], unique: true
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
FactoryBot.define do
|
||||
factory :user_non_working_day do
|
||||
user
|
||||
sequence(:date) { |n| Date.current + n.days }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,44 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
FactoryBot.define do
|
||||
factory :user_working_hours do
|
||||
user
|
||||
sequence(:valid_from) { |n| Date.current + n.days }
|
||||
monday { 480 }
|
||||
tuesday { 480 }
|
||||
wednesday { 480 }
|
||||
thursday { 480 }
|
||||
friday { 480 }
|
||||
saturday { 0 }
|
||||
sunday { 0 }
|
||||
availability_factor { 100 }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,116 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe UserNonWorkingDay do
|
||||
subject(:non_working_day) { build(:user_non_working_day) }
|
||||
|
||||
describe "validations" do
|
||||
it { is_expected.to be_valid }
|
||||
|
||||
it { is_expected.to validate_presence_of(:date) }
|
||||
|
||||
it "validates uniqueness of date scoped to user" do
|
||||
existing = create(:user_non_working_day)
|
||||
duplicate = build(:user_non_working_day, user: existing.user, date: existing.date)
|
||||
|
||||
expect(duplicate).not_to be_valid
|
||||
expect(duplicate.errors[:date]).to be_present
|
||||
end
|
||||
|
||||
it "allows the same date for different users" do
|
||||
existing = create(:user_non_working_day)
|
||||
other_user = create(:user)
|
||||
other = build(:user_non_working_day, user: other_user, date: existing.date)
|
||||
|
||||
expect(other).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
describe ".for_year" do
|
||||
let(:user) { create(:user) }
|
||||
let!(:day_in_year) { create(:user_non_working_day, user:, date: Date.new(2025, 6, 15)) }
|
||||
let!(:day_at_start) { create(:user_non_working_day, user:, date: Date.new(2025, 1, 1)) }
|
||||
let!(:day_at_end) { create(:user_non_working_day, user:, date: Date.new(2025, 12, 31)) }
|
||||
let!(:day_outside_year) { create(:user_non_working_day, user:, date: Date.new(2024, 12, 31)) }
|
||||
|
||||
it "returns records within the given year" do
|
||||
expect(described_class.for_user(user).for_year(2025)).to contain_exactly(day_in_year, day_at_start, day_at_end)
|
||||
end
|
||||
|
||||
it "excludes records outside the given year" do
|
||||
expect(described_class.for_user(user).for_year(2025)).not_to include(day_outside_year)
|
||||
end
|
||||
end
|
||||
|
||||
describe ".for_user" do
|
||||
let(:user) { create(:user) }
|
||||
let(:other_user) { create(:user) }
|
||||
let!(:user_day) { create(:user_non_working_day, user:) }
|
||||
let!(:other_day) { create(:user_non_working_day, user: other_user) }
|
||||
|
||||
it "returns only records for the given user" do
|
||||
expect(described_class.for_user(user)).to contain_exactly(user_day)
|
||||
end
|
||||
|
||||
it "excludes records for other users" do
|
||||
expect(described_class.for_user(user)).not_to include(other_day)
|
||||
end
|
||||
end
|
||||
|
||||
describe ".visible" do
|
||||
let(:user) { create(:user) }
|
||||
let(:other_user) { create(:user) }
|
||||
let!(:user_day) { create(:user_non_working_day, user:) }
|
||||
let!(:other_day) { create(:user_non_working_day, user: other_user) }
|
||||
|
||||
context "when the viewer has :manage_working_times permission" do
|
||||
let(:viewer) { create(:user, global_permissions: [:manage_working_times]) }
|
||||
|
||||
it "returns all records" do
|
||||
expect(described_class.visible(viewer)).to contain_exactly(user_day, other_day)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the viewer has no special permissions" do
|
||||
let(:viewer) { create(:user) }
|
||||
let!(:viewer_day) { create(:user_non_working_day, user: viewer) }
|
||||
|
||||
it "returns only their own records" do
|
||||
expect(described_class.visible(viewer)).to contain_exactly(viewer_day)
|
||||
end
|
||||
|
||||
it "excludes other users' records" do
|
||||
expect(described_class.visible(viewer)).not_to include(user_day, other_day)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,199 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe UserWorkingHours do
|
||||
subject(:working_hours) { build(:user_working_hours) }
|
||||
|
||||
describe "validations" do
|
||||
it { is_expected.to be_valid }
|
||||
|
||||
it { is_expected.to validate_presence_of(:valid_from) }
|
||||
|
||||
%i[monday tuesday wednesday thursday friday saturday sunday].each do |day|
|
||||
it { is_expected.to validate_presence_of(day) }
|
||||
|
||||
it do
|
||||
expect(subject).to validate_numericality_of(day).only_integer
|
||||
.is_greater_than_or_equal_to(0)
|
||||
.is_less_than_or_equal_to(24 * 60)
|
||||
end
|
||||
end
|
||||
|
||||
it { is_expected.to validate_presence_of(:availability_factor) }
|
||||
|
||||
it do
|
||||
expect(subject).to validate_numericality_of(:availability_factor).only_integer
|
||||
.is_greater_than_or_equal_to(0)
|
||||
.is_less_than_or_equal_to(100)
|
||||
end
|
||||
end
|
||||
|
||||
describe "hours accessors" do
|
||||
subject(:working_hours) { build(:user_working_hours, monday: 480, tuesday: 90, wednesday: 0) }
|
||||
|
||||
%i[monday tuesday wednesday thursday friday saturday sunday].each do |day|
|
||||
describe "##{day}_hours" do
|
||||
it "returns the minutes value converted to hours" do
|
||||
working_hours.public_send("#{day}=", 150)
|
||||
expect(working_hours.public_send("#{day}_hours")).to eq(2.5)
|
||||
end
|
||||
end
|
||||
|
||||
describe "##{day}_hours=" do
|
||||
it "stores the hours value converted to minutes" do
|
||||
working_hours.public_send("#{day}_hours=", 7.5)
|
||||
expect(working_hours.public_send(day)).to eq(450)
|
||||
end
|
||||
|
||||
it "rounds fractional minutes" do
|
||||
working_hours.public_send("#{day}_hours=", 1.0 / 3)
|
||||
expect(working_hours.public_send(day)).to eq(20)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "returns 8.0 hours for a full work day of 480 minutes" do
|
||||
expect(working_hours.monday_hours).to eq(8.0)
|
||||
end
|
||||
|
||||
it "returns 1.5 hours for 90 minutes" do
|
||||
expect(working_hours.tuesday_hours).to eq(1.5)
|
||||
end
|
||||
|
||||
it "returns 0.0 for a non-working day" do
|
||||
expect(working_hours.wednesday_hours).to eq(0.0)
|
||||
end
|
||||
end
|
||||
|
||||
describe ".valid_for_date" do
|
||||
let(:user) { create(:user) }
|
||||
let!(:old_hours) { create(:user_working_hours, user:, valid_from: 30.days.ago) }
|
||||
let!(:recent_hours) { create(:user_working_hours, user:, valid_from: 10.days.ago) }
|
||||
let!(:future_hours) { create(:user_working_hours, user:, valid_from: 10.days.from_now) }
|
||||
|
||||
it "returns the most recent record valid on the given date" do
|
||||
expect(described_class.for_user(user).valid_for_date(Date.current)).to eq(recent_hours)
|
||||
end
|
||||
|
||||
it "returns the correct record for a past date" do
|
||||
expect(described_class.for_user(user).valid_for_date(20.days.ago.to_date)).to eq(old_hours)
|
||||
end
|
||||
|
||||
it "returns nil when no record is valid for the given date" do
|
||||
expect(described_class.for_user(user).valid_for_date(31.days.ago.to_date)).to be_nil
|
||||
end
|
||||
|
||||
it "does not return future records" do
|
||||
expect(described_class.for_user(user).valid_for_date(Date.current)).not_to eq(future_hours)
|
||||
end
|
||||
end
|
||||
|
||||
describe ".current" do
|
||||
let(:user) { create(:user) }
|
||||
let!(:past_hours) { create(:user_working_hours, user:, valid_from: 10.days.ago) }
|
||||
let!(:future_hours) { create(:user_working_hours, user:, valid_from: 10.days.from_now) }
|
||||
|
||||
it "returns the currently valid record" do
|
||||
expect(described_class.for_user(user).current).to eq(past_hours)
|
||||
end
|
||||
|
||||
it "does not return future records" do
|
||||
expect(described_class.for_user(user).current).not_to eq(future_hours)
|
||||
end
|
||||
end
|
||||
|
||||
describe ".past" do
|
||||
let(:user) { create(:user) }
|
||||
let!(:older_hours) { create(:user_working_hours, user:, valid_from: 20.days.ago) }
|
||||
let!(:recent_past_hours) { create(:user_working_hours, user:, valid_from: 5.days.ago) }
|
||||
let!(:future_hours) { create(:user_working_hours, user:, valid_from: 5.days.from_now) }
|
||||
|
||||
it "returns records with valid_from before today" do
|
||||
expect(described_class.for_user(user).past).to contain_exactly(older_hours, recent_past_hours)
|
||||
end
|
||||
|
||||
it "orders results descending by valid_from" do
|
||||
expect(described_class.for_user(user).past).to eq([recent_past_hours, older_hours])
|
||||
end
|
||||
|
||||
it "excludes future records" do
|
||||
expect(described_class.for_user(user).past).not_to include(future_hours)
|
||||
end
|
||||
end
|
||||
|
||||
describe ".upcoming" do
|
||||
let(:user) { create(:user) }
|
||||
let!(:past_hours) { create(:user_working_hours, user:, valid_from: 5.days.ago) }
|
||||
let!(:near_future_hours) { create(:user_working_hours, user:, valid_from: 5.days.from_now) }
|
||||
let!(:far_future_hours) { create(:user_working_hours, user:, valid_from: 20.days.from_now) }
|
||||
|
||||
it "returns records with valid_from from today onwards" do
|
||||
expect(described_class.for_user(user).upcoming).to contain_exactly(near_future_hours, far_future_hours)
|
||||
end
|
||||
|
||||
it "orders results ascending by valid_from" do
|
||||
expect(described_class.for_user(user).upcoming).to eq([near_future_hours, far_future_hours])
|
||||
end
|
||||
|
||||
it "excludes past records" do
|
||||
expect(described_class.for_user(user).upcoming).not_to include(past_hours)
|
||||
end
|
||||
end
|
||||
|
||||
describe ".visible" do
|
||||
let(:user) { create(:user) }
|
||||
let(:other_user) { create(:user) }
|
||||
let!(:user_hours) { create(:user_working_hours, user:) }
|
||||
let!(:other_hours) { create(:user_working_hours, user: other_user) }
|
||||
|
||||
context "when the viewer has :manage_working_times permission" do
|
||||
let(:viewer) { create(:user, global_permissions: [:manage_working_times]) }
|
||||
|
||||
it "returns all records" do
|
||||
expect(described_class.visible(viewer)).to contain_exactly(user_hours, other_hours)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the viewer has no special permissions" do
|
||||
let(:viewer) { create(:user) }
|
||||
let!(:viewer_hours) { create(:user_working_hours, user: viewer) }
|
||||
|
||||
it "returns only their own records" do
|
||||
expect(described_class.visible(viewer)).to contain_exactly(viewer_hours)
|
||||
end
|
||||
|
||||
it "excludes other users' records" do
|
||||
expect(described_class.visible(viewer)).not_to include(user_hours, other_hours)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user