Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ gem "json", "2.21.2"

gem "active_model_serializers", "~> 0.10.16"

gem "sidekiq", "~> 8.0"

gem "sidekiq-cron"
# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin Ajax possible
# gem "rack-cors"

Expand Down
33 changes: 33 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ GEM
concurrent-ruby (1.3.8)
connection_pool (3.0.2)
crass (1.0.7)
cronex (0.15.0)
tzinfo
unicode (>= 0.4.4.5)
date (3.5.1)
debug (1.11.1)
irb (~> 1.10)
Expand All @@ -117,13 +120,18 @@ GEM
ed25519 (1.4.0)
erb (6.0.7)
erubi (1.13.1)
et-orbi (1.4.2)
tzinfo
factory_bot (6.6.0)
activesupport (>= 6.1.0)
factory_bot_rails (6.5.1)
factory_bot (~> 6.5)
railties (>= 6.1.0)
faker (3.8.0)
i18n (>= 1.8.11, < 2)
fugit (1.14.0)
et-orbi (~> 1.4)
raabro (~> 1.4)
globalid (1.4.0)
activesupport (>= 6.1)
i18n (1.15.2)
Expand Down Expand Up @@ -200,6 +208,7 @@ GEM
prism (1.9.0)
puma (8.0.2)
nio4r (~> 2.0)
raabro (1.5.0)
racc (1.8.1)
rack (3.2.7)
rack-session (2.1.2)
Expand Down Expand Up @@ -253,6 +262,8 @@ GEM
prism (>= 1.6.0)
rbs (>= 4.0.0)
tsort
redis-client (0.31.0)
connection_pool
regexp_parser (2.12.0)
reline (0.7.0)
io-console (~> 0.5)
Expand Down Expand Up @@ -306,6 +317,17 @@ GEM
rubocop-rails (>= 2.30)
ruby-progressbar (1.13.0)
securerandom (0.4.1)
sidekiq (8.1.7)
connection_pool (>= 3.0.0)
json (>= 2.16.0)
logger (>= 1.7.0)
rack (>= 3.2.0)
redis-client (>= 0.29.0)
sidekiq-cron (2.4.0)
cronex (>= 0.13.0)
fugit (~> 1.8, >= 1.11.1)
globalid (>= 1.0.1)
sidekiq (>= 6.5.0)
solid_cache (1.0.10)
activejob (>= 7.2)
activerecord (>= 7.2)
Expand All @@ -326,6 +348,7 @@ GEM
concurrent-ruby (~> 1.0)
tzinfo-data (1.2026.4)
tzinfo (>= 1.0.0)
unicode (0.4.4.5)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.2.0)
Expand Down Expand Up @@ -362,6 +385,8 @@ DEPENDENCIES
rails-i18n
rspec-rails
rubocop-rails-omakase
sidekiq (~> 8.0)
sidekiq-cron
solid_cache
thruster
tzinfo-data
Expand Down Expand Up @@ -393,6 +418,7 @@ CHECKSUMS
concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
crass (1.0.7) sha256=94868719948664c89ddcaf0a37c65048413dfcb1c869470a5f7a7ceb5390b295
cronex (0.15.0) sha256=21c794e085fad2951c4f2e279f440340a35ba2297e0b738f22f263f69fbe2186
date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6
devise (5.0.4) sha256=d605f2b85854e74e56ee789e2d398702bc2d06e6bcd894717a670a3199c74cc1
Expand All @@ -403,9 +429,11 @@ CHECKSUMS
ed25519 (1.4.0) sha256=16e97f5198689a154247169f3453ef4cfd3f7a47481fde0ae33206cdfdcac506
erb (6.0.7) sha256=c5ca6dc25b0ef974a44dc8f59fe847577122483b1968a38dec305c60bf91ee92
erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
et-orbi (1.4.2) sha256=bb555dae668419cb24caa2a293a170e58be6d4df1e017c51f5030bdc133cd20c
factory_bot (6.6.0) sha256=1fc1b3b5620ec980a6a27aec1b6ec8c250ca82962e970e8a40f93e8d388d4b89
factory_bot_rails (6.5.1) sha256=d3cc4851eae4dea8a665ec4a4516895045e710554d2b5ac9e68b94d351bc6d68
faker (3.8.0) sha256=c147b308df73a90f27a4fc84f18d4c22ef0ad9c2a64b2b61c86fd0ca71753efc
fugit (1.14.0) sha256=e311a9cc85a54bb67649df45060e721282a1e2c8ebad40e41874a3a7d576f37e
globalid (1.4.0) sha256=037f12fbf1d9d7a014d501c2d5c77356fd4ddd96d7a7991d6700bba96706f427
i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5
io-console (0.9.4) sha256=a403304f32928ee85e56d457b4612ca43f2703b561a3258844992bd24085fbb1
Expand Down Expand Up @@ -443,6 +471,7 @@ CHECKSUMS
prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
puma (8.0.2) sha256=c8ed871dfbbe66448ea9ffd46692342d9804d4071522b52b5331b7b6e7b686fb
raabro (1.5.0) sha256=3f998a7bc84f9c84df3ab580634d2e0a5bda4f0841168d56035f529c9877440a
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
rack (3.2.7) sha256=93e13e1c24f93556671d85d2d79fa228c3485815c50d7e2f265b5330c6528fb7
rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8
Expand All @@ -457,6 +486,7 @@ CHECKSUMS
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
rbs (4.2.0) sha256=51f7b886dcc05bc09e10b901daa6a81829f6adc03101d6ca9ea4aac6103e0674
rdoc (8.1.0) sha256=befc37880a5cfd36dcb176c748b5fd5ee39dd3536a75e81824e4972ce230f60a
redis-client (0.31.0) sha256=6820cb2a21e6795a823141f50fd25e655de2b8dc8ff833b43509a27b0263fd75
regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d
responders (3.2.1) sha256=c2772970cbda654c0e772d852e5a851e82cd8e96f5fa958650861ba75b8ffa84
Expand All @@ -472,6 +502,8 @@ CHECKSUMS
rubocop-rails-omakase (1.1.0) sha256=2af73ac8ee5852de2919abbd2618af9c15c19b512c4cfc1f9a5d3b6ef009109d
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
sidekiq (8.1.7) sha256=91ad48c7e6c32e1846bf1d5b74c27948758049b93fed0cb539f8d586c7de3415
sidekiq-cron (2.4.0) sha256=25595c5aee0c16d13a89466e5108af047fa4cfbf388199b39e1a600ca39472d4
solid_cache (1.0.10) sha256=bc05a2fb3ac78a6f43cbb5946679cf9db67dd30d22939ededc385cb93e120d41
sshkit (1.25.1) sha256=be3f10b9d6eb0b44d5eaba3f7cbe41bc6bb894bce4339688ac20124391455b78
thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
Expand All @@ -481,6 +513,7 @@ CHECKSUMS
tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b
tzinfo-data (1.2026.4) sha256=22f5f02608f14938ab8b78513a70a22f0b0277614a07273d3bedb3ea45ece104
unicode (0.4.4.5) sha256=42f294bfc8e186d29da89d1f766071505a20a22776168a31bb3408e03fa7a9d7
unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/api/v1/groups_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def update
end

def destroy
Groups::Delete.call(id: params[:id])
Groups::Deactivate.call(id: params[:id])
head :no_content
end

Expand Down
38 changes: 38 additions & 0 deletions app/controllers/api/v1/occupations_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
module Api
module V1
class OccupationsController < BaseController
before_action :authenticate_request!

def create
group = Groups::Find.call(id: occupation_params[:group_id])
destination = Terrains::Find.call(id: occupation_params[:terrain_id])

occupation = Occupations::Move.call(group:, destination:)

render json: occupation, serializer: OccupationSerializer, status: :created
end

def by_group
group = Groups::Find.call(id: params[:id])
result = Occupations::HistoryByGroup.call(group:, page: params[:page], per_page: params[:per_page])
paginated = PaginatedCollectionSerializer.call(result:, serializer: GroupOccupationHistorySerializer)

render json: { group: GroupSummarySerializer.new(group).attributes, **paginated }
end

def by_terrain
terrain = Terrains::Find.call(id: params[:id])
result = Occupations::HistoryByTerrain.call(terrain:, page: params[:page], per_page: params[:per_page])
paginated = PaginatedCollectionSerializer.call(result:, serializer: TerrainOccupationHistorySerializer)

render json: { terrain: TerrainSummarySerializer.new(terrain).attributes, **paginated }
end

private

def occupation_params
params.expect(occupation: [ :group_id, :terrain_id ])
end
end
end
end
2 changes: 1 addition & 1 deletion app/controllers/api/v1/terrains_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def update
end

def destroy
Terrains::Delete.call(id: params[:id])
Terrains::Deactivate.call(id: params[:id])
head :no_content
end

Expand Down
4 changes: 4 additions & 0 deletions app/errors/occupations/deactivation_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module Occupations
class DeactivationError < ApplicationError
end
end
4 changes: 4 additions & 0 deletions app/errors/occupations/movement_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module Occupations
class MovementError < ApplicationError
end
end
9 changes: 9 additions & 0 deletions app/jobs/terrains/release_resting_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Terrains
class ReleaseRestingJob < ApplicationJob
queue_as :default

def perform
Terrains::ReleaseResting.call
end
end
end
10 changes: 10 additions & 0 deletions app/models/group.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class Group < ApplicationRecord
has_many :occupations, dependent: :restrict_with_error

validates :name, presence: true
validates :animal_count, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }

Expand All @@ -12,4 +14,12 @@ def deleted?
def soft_delete!
update!(deleted_at: Time.current)
end

def current_occupation
occupations.active.first
end

def current_terrain
current_occupation&.terrain
end
end
27 changes: 27 additions & 0 deletions app/models/occupation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class Occupation < ApplicationRecord
belongs_to :group
belongs_to :terrain

validates :started_at, presence: true
validate :ended_at_not_before_started_at

scope :active, -> { where(ended_at: nil) }
scope :finished, -> { where.not(ended_at: nil) }

def active?
ended_at.nil?
end

def finished?
!active?
end

private

def ended_at_not_before_started_at
return if ended_at.blank? || started_at.blank?
return if ended_at >= started_at

errors.add(:ended_at, :greater_than_or_equal_to, count: started_at)
end
end
47 changes: 47 additions & 0 deletions app/models/terrain.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class Terrain < ApplicationRecord
has_many :occupations, dependent: :restrict_with_error

enum :status, {
available: "available",
occupied: "occupied",
Expand All @@ -19,4 +21,49 @@ def deleted?
def soft_delete!
update!(deleted_at: Time.current)
end

def current_occupation
occupations.active.first
end

def current_group
current_occupation&.group
end

def last_occupation
occupations.finished.order(ended_at: :desc).first
end

def last_group
last_occupation&.group
end

def remaining_rest_days
return 0 unless resting?

occupation = last_occupation
return 0 unless occupation

elapsed_days = (Date.current - occupation.ended_at.to_date).to_i

[ rest_days - elapsed_days, 0 ].max
end

def days_free
return 0 unless available?

occupation = last_occupation

return (Date.current - created_at.to_date).to_i unless occupation

(Date.current - occupation.ended_at.to_date).to_i
end

def rest_completed?
return false unless resting?

occupation = last_occupation
return false unless occupation
occupation.ended_at.to_date + rest_days <= Date.current
end
end
5 changes: 5 additions & 0 deletions app/serializers/group_occupation_history_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class GroupOccupationHistorySerializer < ActiveModel::Serializer
attributes :id, :started_at, :ended_at

belongs_to :terrain, serializer: TerrainSummarySerializer
end
2 changes: 2 additions & 0 deletions app/serializers/group_serializer.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
class GroupSerializer < ActiveModel::Serializer
attributes :id, :name, :animal_count, :created_at, :updated_at

belongs_to :current_terrain, serializer: TerrainSummarySerializer
end
3 changes: 3 additions & 0 deletions app/serializers/group_summary_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class GroupSummarySerializer < ActiveModel::Serializer
attributes :id, :name
end
6 changes: 6 additions & 0 deletions app/serializers/occupation_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class OccupationSerializer < ActiveModel::Serializer
attributes :id, :started_at, :ended_at

belongs_to :group, serializer: GroupSummarySerializer
belongs_to :terrain, serializer: TerrainSummarySerializer
end
5 changes: 5 additions & 0 deletions app/serializers/terrain_occupation_history_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class TerrainOccupationHistorySerializer < ActiveModel::Serializer
attributes :id, :started_at, :ended_at

belongs_to :group, serializer: GroupSummarySerializer
end
Loading
Loading