1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-29 18:52:22 +01:00

(api) availabilities

This commit is contained in:
Sylvain 2023-03-30 13:22:36 +02:00
parent 7945895c1e
commit 3811e7a6d5
12 changed files with 424 additions and 1 deletions

View File

@ -23,6 +23,8 @@
- Fill the holes in the logical sequence of invoices references with nil invoices
- Use a cached configuration file to read the authentification provider settings
- Order numbers are now saved in database instead of generated on-the-fly
- OpenAPI availabilities endpoint
- Ability to filter OpenAPI reservations endpoint by availability_id
- Support for ARM64 CPU architecture
- Fix a bug: broken display after a plan category was deleted
- Fix a security issue: updated json5 to 2.2.2 to fix [CVE-2022-46175](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-46175)

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
require_relative 'concerns/reservations_filters_concern'
# public API controller for resources of type Reservation
class OpenAPI::V1::AvailabilitiesController < OpenAPI::V1::BaseController
extend OpenAPI::APIDoc
include Rails::Pagination
include OpenAPI::V1::Concerns::AvailabilitiesFiltersConcern
expose_doc
def index
@availabilities = Availability.order(start_at: :desc)
.includes(:slots)
@availabilities = filter_by_after(@availabilities, params)
@availabilities = filter_by_before(@availabilities, params)
@availabilities = filter_by_id(@availabilities, params)
@availabilities = filter_by_available_type(@availabilities, params)
@availabilities = filter_by_available_id(@availabilities, params)
@availabilities = @availabilities.page(page).per(per_page)
paginate @availabilities, per_page: per_page
end
private
def page
params[:page] || 1
end
def per_page
params[:per_page] || 20
end
end

View File

@ -0,0 +1,85 @@
# frozen_string_literal: true
# Filter the list of availabilities by the given parameters
module OpenAPI::V1::Concerns::AvailabilitiesFiltersConcern
extend ActiveSupport::Concern
included do
# @param availabilities [ActiveRecord::Relation<Availability>]
# @param filters [ActionController::Parameters]
def filter_by_id(availabilities, filters)
return availabilities if filters[:id].blank?
availabilities.where(id: may_array(filters[:id]))
end
# @param availabilities [ActiveRecord::Relation<Availability>]
# @param filters [ActionController::Parameters]
def filter_by_after(availabilities, filters)
return availabilities if filters[:after].blank?
availabilities.where('availabilities.start_at >= ?', Time.zone.parse(filters[:after]))
end
# @param availabilities [ActiveRecord::Relation<Availability>]
# @param filters [ActionController::Parameters]
def filter_by_before(availabilities, filters)
return availabilities if filters[:before].blank?
availabilities.where('availabilities.end_at <= ?', Time.zone.parse(filters[:before]))
end
# @param availabilities [ActiveRecord::Relation<Availability>]
# @param filters [ActionController::Parameters]
def filter_by_available_type(availabilities, filters)
return availabilities if filters[:available_type].blank?
availabilities.where(available_type: format_type(filters[:available_type]))
end
# @param availabilities [ActiveRecord::Relation<Availability>]
# @param filters [ActionController::Parameters]
def filter_by_available_id(availabilities, filters)
return availabilities if filters[:available_id].blank? || filters[:available_type].blank?
join_table = join_table(filters)
availabilities.joins(join_table).where(join_table => { where_clause(filters) => may_array(filters[:available_id]) })
end
# @param type [ActionController::Parameters]
# @return [String]
def format_type(type)
types = {
'Machine' => 'machines',
'Space' => 'space',
'Training' => 'training',
'Event' => 'event'
}
types[type]
end
# @param filters [ActionController::Parameters]
# @return [Symbol]
def join_table(filters)
tables = {
'Machine' => :machines_availabilities,
'Space' => :spaces_availabilities,
'Training' => :trainings_availabilities,
'Event' => :event
}
tables[filters[:available_type]]
end
# @param filters [ActionController::Parameters]
# @return [Symbol]
def where_clause(filters)
clauses = {
'Machine' => :machine_id,
'Space' => :space_id,
'Training' => :training_id,
'Event' => :id
}
clauses[filters[:available_type]]
end
end
end

View File

@ -45,6 +45,15 @@ module OpenAPI::V1::Concerns::ReservationsFiltersConcern
reservations.where(reservable_id: may_array(filters[:reservable_id]))
end
# @param reservations [ActiveRecord::Relation<Reservation>]
# @param filters [ActionController::Parameters]
def filter_by_availability_id(reservations, filters)
return reservations if filters[:availability_id].blank?
reservations.joins(:slots_reservations, :slots)
.where(slots_reservations: { slots: { availability_id: may_array(filters[:availability_id]) } })
end
# @param type [String]
def format_type(type)
type.singularize.classify

View File

@ -19,6 +19,7 @@ class OpenAPI::V1::ReservationsController < OpenAPI::V1::BaseController
@reservations = filter_by_user(@reservations, params)
@reservations = filter_by_reservable_type(@reservations, params)
@reservations = filter_by_reservable_id(@reservations, params)
@reservations = filter_by_availability_id(@reservations, params)
@reservations = @reservations.page(page).per(per_page)
paginate @reservations, per_page: per_page

View File

@ -0,0 +1,163 @@
# frozen_string_literal: true
# openAPI documentation for reservations endpoint
class OpenAPI::V1::AvailabilitiesDoc < OpenAPI::V1::BaseDoc
resource_description do
short 'Availabilities'
desc 'Slots availables for reservation'
formats FORMATS
api_version API_VERSION
end
include OpenAPI::V1::Concerns::ParamGroups
doc_for :index do
api :GET, "/#{API_VERSION}/availabilities", 'Availabilities index'
description 'Index of reservable availabilities and their slots, paginated. Ordered by *start_at* descendant.'
param_group :pagination
param :after, DateTime, optional: true, desc: 'Filter availabilities to those starting after the given date.'
param :before, DateTime, optional: true, desc: 'Filter availabilities to those ending before the given date.'
param :user_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various users.'
param :available_type, %w[Event Machine Space Training], optional: true, desc: 'Scope the request to a specific type of reservable.'
param :available_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various reservables. <br>' \
'<b>WARNING</b>: filtering by <i>available_id</i> is only available if ' \
'filter <i>available_type</i> is provided'
example <<-AVAILABILITIES
# /open_api/v1/availabilities?available_type=Machine&page=1&per_page=3
{
"availabilities": [
{
"id": 5115,
"start_at": "2023-07-13T14:00:00.000+02:00",
"end_at": "2023-07-13T18:00:00.000+02:00",
"created_at": "2023-01-24T12:28:25.905+01:00",
"available_type": "Machine",
"available_ids": [
5,
9,
10,
15,
8,
12,
17,
16,
3,
2,
14,
18
],
"slots": [
{
"id": 17792,
"start_at": "2023-07-13T14:00:00.000+02:00",
"end_at": "2023-07-13T15:00:00.000+02:00"
},
{
"id": 17793,
"start_at": "2023-07-13T15:00:00.000+02:00",
"end_at": "2023-07-13T16:00:00.000+02:00"
},
{
"id": 17794,
"start_at": "2023-07-13T16:00:00.000+02:00",
"end_at": "2023-07-13T17:00:00.000+02:00"
},
{
"id": 17795,
"start_at": "2023-07-13T17:00:00.000+02:00",
"end_at": "2023-07-13T18:00:00.000+02:00"
}
]
},
{
"id": 5112,
"start_at": "2023-07-07T14:00:00.000+02:00",
"end_at": "2023-07-07T18:00:00.000+02:00",
"created_at": "2023-01-24T12:26:45.997+01:00",
"available_type": "Machine",
"available_ids": [
5,
9,
10,
15,
8,
12,
17,
16,
3,
2,
14,
18
],
"slots": [
{
"id": 17786,
"start_at": "2023-07-07T14:00:00.000+02:00",
"end_at": "2023-07-07T15:00:00.000+02:00"
},
{
"id": 17787,
"start_at": "2023-07-07T15:00:00.000+02:00",
"end_at": "2023-07-07T16:00:00.000+02:00"
},
{
"id": 17788,
"start_at": "2023-07-07T16:00:00.000+02:00",
"end_at": "2023-07-07T17:00:00.000+02:00"
},
{
"id": 17789,
"start_at": "2023-07-07T17:00:00.000+02:00",
"end_at": "2023-07-07T18:00:00.000+02:00"
}
]
},
{
"id": 5111,
"start_at": "2023-07-06T14:00:00.000+02:00",
"end_at": "2023-07-06T18:00:00.000+02:00",
"created_at": "2023-01-24T12:26:37.189+01:00",
"available_type": "Machine",
"available_ids": [
5,
9,
10,
15,
8,
12,
17,
16,
3,
2,
14,
18
],
"slots": [
{
"id": 17782,
"start_at": "2023-07-06T14:00:00.000+02:00",
"end_at": "2023-07-06T15:00:00.000+02:00"
},
{
"id": 17783,
"start_at": "2023-07-06T15:00:00.000+02:00",
"end_at": "2023-07-06T16:00:00.000+02:00"
},
{
"id": 17784,
"start_at": "2023-07-06T16:00:00.000+02:00",
"end_at": "2023-07-06T17:00:00.000+02:00"
},
{
"id": 17785,
"start_at": "2023-07-06T17:00:00.000+02:00",
"end_at": "2023-07-06T18:00:00.000+02:00"
}
]
}
]
}
AVAILABILITIES
end
end

View File

@ -20,6 +20,7 @@ class OpenAPI::V1::ReservationsDoc < OpenAPI::V1::BaseDoc
param :user_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various users.'
param :reservable_type, %w[Event Machine Space Training], optional: true, desc: 'Scope the request to a specific type of reservable.'
param :reservable_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various reservables.'
param :availability_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various availabilities.'
example <<-RESERVATIONS
# /open_api/v1/reservations?reservable_type=Event&page=1&per_page=3
@ -48,6 +49,7 @@ class OpenAPI::V1::ReservationsDoc < OpenAPI::V1::BaseDoc
"reserved_slots": [
{
"canceled_at": "2016-05-20T09:40:12.201+01:00",
"availability_id": 5200,
"start_at": "2016-06-03T14:00:00.000+01:00",
"end_at": "2016-06-03T15:00:00.000+01:00"
}
@ -76,6 +78,7 @@ class OpenAPI::V1::ReservationsDoc < OpenAPI::V1::BaseDoc
"reserved_slots": [
{
"canceled_at": null,
"availability_id": 5199,
"start_at": "2016-06-02T16:00:00.000+01:00",
"end_at": "2016-06-02T17:00:00.000+01:00"
}
@ -104,6 +107,7 @@ class OpenAPI::V1::ReservationsDoc < OpenAPI::V1::BaseDoc
"reserved_slots": [
{
"canceled_at": null,
"availability_id": 5066,
"start_at": "2016-06-03T14:00:00.000+01:00",
"end_at": "2016-06-03T15:00:00.000+01:00"
}

View File

@ -113,6 +113,22 @@ class Availability < ApplicationRecord
end
end
# @return [Array<Integer>]
def available_ids
case available_type
when 'training'
training_ids
when 'machines'
machine_ids
when 'event'
[event&.id]
when 'space'
space_ids
else
[]
end
end
# check if the reservations are complete?
# if a nb_total_places hasn't been defined, then places are unlimited
# @return [Boolean]

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
json.availabilities @availabilities do |availability|
json.extract! availability, :id, :start_at, :end_at, :created_at
json.available_type availability.available_type.classify
json.available_ids availability.available_ids
json.slots availability.slots do |slot|
json.extract! slot, :id, :start_at, :end_at
end
end

View File

@ -25,6 +25,6 @@ json.reservations @reservations do |reservation|
json.reserved_slots reservation.slots_reservations do |slot_reservation|
json.extract! slot_reservation, :canceled_at
json.extract! slot_reservation.slot, :start_at, :end_at
json.extract! slot_reservation.slot, :availability_id, :start_at, :end_at
end
end

View File

@ -0,0 +1,87 @@
# frozen_string_literal: true
require 'test_helper'
module OpenApi; end
class OpenApi::AvailabilitiesTest < ActionDispatch::IntegrationTest
def setup
@token = OpenAPI::Client.find_by(name: 'minitest').token
end
test 'list availabilities' do
get '/open_api/v1/availabilities', headers: open_api_headers(@token)
assert_response :success
availabilities = json_response(response.body)
assert_not_empty availabilities[:availabilities]
assert(availabilities[:availabilities].none? { |a| a[:id].blank? })
assert(availabilities[:availabilities].none? { |a| a[:start_at].blank? })
assert(availabilities[:availabilities].none? { |a| a[:end_at].blank? })
assert(availabilities[:availabilities].none? { |a| a[:available_type].blank? })
assert(availabilities[:availabilities].none? { |a| a[:available_ids].empty? })
assert(availabilities[:availabilities].none? { |a| a[:created_at].blank? })
assert(availabilities[:availabilities].none? { |a| a[:slots].empty? })
assert(availabilities[:availabilities].pluck(:slots).flatten.none? { |s| s[:id].blank? })
assert(availabilities[:availabilities].pluck(:slots).flatten.none? { |s| s[:start_at].blank? })
assert(availabilities[:availabilities].pluck(:slots).flatten.none? { |s| s[:end_at].blank? })
end
test 'list availabilities with pagination details' do
get '/open_api/v1/availabilities?page=1&per_page=5', headers: open_api_headers(@token)
assert_response :success
availabilities = json_response(response.body)
assert_equal 5, availabilities[:availabilities].count
end
test 'list availabilities for given IDs' do
get '/open_api/v1/availabilities?id=[3,4]', headers: open_api_headers(@token)
assert_response :success
availabilities = json_response(response.body)
assert_not_empty availabilities[:availabilities]
assert(availabilities[:availabilities].all? { |a| [3, 4].include?(a[:id]) })
end
test 'list availabilities for given type' do
get '/open_api/v1/availabilities?available_type=Machine', headers: open_api_headers(@token)
assert_response :success
availabilities = json_response(response.body)
assert_not_empty availabilities[:availabilities]
assert(availabilities[:availabilities].all? { |a| a[:available_type] == 'Machine' })
end
test 'list availabilities for given type and IDs' do
get '/open_api/v1/availabilities?available_type=Machine&available_id[]=1&available_id[]=2', headers: open_api_headers(@token)
assert_response :success
availabilities = json_response(response.body)
assert_not_empty availabilities[:availabilities]
assert(availabilities[:availabilities].all? { |a| a[:available_type] == 'Machine' })
assert(availabilities[:availabilities].all? { |a| a[:available_ids].any? { |id| [1, 2].include?(id) } })
end
test 'list availabilities with given available_id but no available_type does not filter by id' do
get '/open_api/v1/availabilities?&available_id=1', headers: open_api_headers(@token)
assert_response :success
availabilities = json_response(response.body)
assert_not_empty availabilities[:availabilities]
assert(availabilities[:availabilities].any? { |a| a[:available_ids] != 1 })
end
test 'list availabilities with date filtering' do
get '/open_api/v1/availabilities?after=2016-04-01T00:00:00+01:00&before=2016-05-31T23:59:59+02:00', headers: open_api_headers(@token)
assert_response :success
availabilities = json_response(response.body)
assert_not_empty availabilities[:availabilities]
assert(availabilities[:availabilities].all? do |a|
start = Time.zone.parse(a[:start_at])
ending = Time.zone.parse(a[:end_at])
start >= '2016-04-01'.to_date && ending <= '2016-05-31'.to_date
end)
end
end

View File

@ -79,4 +79,14 @@ class OpenApi::ReservationsTest < ActionDispatch::IntegrationTest
assert_not_empty reservations[:reservations]
assert_equal [2], reservations[:reservations].pluck(:reservable_id).uniq
end
test 'list reservations filtered by availability' do
get '/open_api/v1/reservations?availability_id=13', headers: open_api_headers(@token)
assert_response :success
assert_match Mime[:json].to_s, response.content_type
reservations = json_response(response.body)
assert_not_empty reservations[:reservations]
assert_equal [13], reservations[:reservations].pluck(:reserved_slots).flatten.pluck(:availability_id).uniq
end
end