1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-26 20:54:21 +01:00

Merge branch 'dev' for release 5.8.0

This commit is contained in:
Sylvain 2023-03-03 15:48:09 +01:00
commit 4228b44226
60 changed files with 464 additions and 109 deletions

View File

@ -1,10 +1,29 @@
# Changelog Fab-manager # Changelog Fab-manager
## v5.8.0 2023 March 03
- OpenAPI events endpoint returns category, theme and age_range
- OpenAPI reservation endpoint will return details for the reserved slots
- Display info messages if the user cannot buy prepaid packs
- Fix a bug: some OpenAPI endpoints struggle and expire with timeout
- Fix a bug: OpenAPI events endpoint documentation does not refect the returned data
- Fix a bug: members can't change/cancel their reservations
- Fix a bug: admin events view should default to the list tab
- Fix a bug: event creation form should not allow setting multiple times the same price category
- Fix a bug: MAX_SIZE env varibles should not be quoted (#438)
- Fix a bug: unable to add OIDC scopes without discovery
- [BREAKING CHANGE] GET `open_api/v1/events` will necessarily be paginated
- [BREAKING CHANGE] GET `open_api/v1/invoices` will necessarily be paginated
- [BREAKING CHANGE] GET `open_api/v1/reservations` will necessarily be paginated
- [BREAKING CHANGE] GET `open_api/v1/users` will necessarily be paginated
- [BREAKING CHANGE] GET `open_api/v1/subscriptions` won't return `total_count`, `total_pages`, `page` or `page_siez` anymore. RFC-5988 headers (*Link*, *Total* and *Per-Page*) will continue to provide these same data.
- [BREAKING CHANGE] GET `open_api/v1/subscriptions` will return a `subscriptions` array instead of a `data` array.
## v5.7.2 2023 February 24 ## v5.7.2 2023 February 24
- Fix a bug: unable to update recurrent events - Fix a bug: unable to update recurrent events
- Fix a bug: invalid border color for slots - Fix a bug: invalid border color for slots
- Fix a bug: members can change/cancel their reservations - Fix a bug: members can't change/cancel their reservations
## v5.7.1 2023 February 20 ## v5.7.1 2023 February 20

View File

@ -19,14 +19,16 @@ class OpenAPI::V1::EventsController < OpenAPI::V1::BaseController
@events = @events.where(id: may_array(params[:id])) if params[:id].present? @events = @events.where(id: may_array(params[:id])) if params[:id].present?
return if params[:page].blank? @events = @events.page(page).per(per_page)
@events = @events.page(params[:page]).per(per_page)
paginate @events, per_page: per_page paginate @events, per_page: per_page
end end
private private
def page
params[:page] || 1
end
def per_page def per_page
params[:per_page] || 20 params[:per_page] || 20
end end

View File

@ -8,13 +8,11 @@ class OpenAPI::V1::InvoicesController < OpenAPI::V1::BaseController
def index def index
@invoices = Invoice.order(created_at: :desc) @invoices = Invoice.order(created_at: :desc)
.includes(invoicing_profile: :user) .includes(:payment_gateway_object, :invoicing_profile)
.references(:invoicing_profiles) .references(:invoicing_profiles)
@invoices = @invoices.where(invoicing_profiles: { user_id: may_array(params[:user_id]) }) if params[:user_id].present? @invoices = @invoices.where(invoicing_profiles: { user_id: may_array(params[:user_id]) }) if params[:user_id].present?
return if params[:page].blank?
@invoices = @invoices.page(params[:page]).per(per_page) @invoices = @invoices.page(params[:page]).per(per_page)
paginate @invoices, per_page: per_page paginate @invoices, per_page: per_page
end end

View File

@ -8,16 +8,14 @@ class OpenAPI::V1::ReservationsController < OpenAPI::V1::BaseController
def index def index
@reservations = Reservation.order(created_at: :desc) @reservations = Reservation.order(created_at: :desc)
.includes(statistic_profile: :user) .includes(slots_reservations: :slot, statistic_profile: :user)
.references(:statistic_profiles) .references(:statistic_profiles)
@reservations = @reservations.where(statistic_profiles: { user_id: may_array(params[:user_id]) }) if params[:user_id].present? @reservations = @reservations.where(statistic_profiles: { user_id: may_array(params[:user_id]) }) if params[:user_id].present?
@reservations = @reservations.where(reservable_type: format_type(params[:reservable_type])) if params[:reservable_type].present? @reservations = @reservations.where(reservable_type: format_type(params[:reservable_type])) if params[:reservable_type].present?
@reservations = @reservations.where(reservable_id: may_array(params[:reservable_id])) if params[:reservable_id].present? @reservations = @reservations.where(reservable_id: may_array(params[:reservable_id])) if params[:reservable_id].present?
return if params[:page].blank? @reservations = @reservations.page(page).per(per_page)
@reservations = @reservations.page(params[:page]).per(per_page)
paginate @reservations, per_page: per_page paginate @reservations, per_page: per_page
end end
@ -27,6 +25,10 @@ class OpenAPI::V1::ReservationsController < OpenAPI::V1::BaseController
type.singularize.classify type.singularize.classify
end end
def page
params[:page] || 1
end
def per_page def per_page
params[:per_page] || 20 params[:per_page] || 20
end end

View File

@ -17,7 +17,6 @@ class OpenAPI::V1::SubscriptionsController < OpenAPI::V1::BaseController
@subscriptions = @subscriptions.where(statistic_profiles: { user_id: may_array(params[:user_id]) }) if params[:user_id].present? @subscriptions = @subscriptions.where(statistic_profiles: { user_id: may_array(params[:user_id]) }) if params[:user_id].present?
@subscriptions = @subscriptions.page(page).per(per_page) @subscriptions = @subscriptions.page(page).per(per_page)
@pageination_meta = pageination_meta
paginate @subscriptions, per_page: per_page paginate @subscriptions, per_page: per_page
end end
@ -30,14 +29,4 @@ class OpenAPI::V1::SubscriptionsController < OpenAPI::V1::BaseController
def per_page def per_page
params[:per_page] || 20 params[:per_page] || 20
end end
def pageination_meta
total_count = Subscription.count
{
total_count: total_count,
total_pages: (total_count / per_page.to_f).ceil,
page: page.to_i,
page_size: per_page.to_i
}
end
end end

View File

@ -18,12 +18,16 @@ class OpenAPI::V1::UsersController < OpenAPI::V1::BaseController
return if params[:page].blank? return if params[:page].blank?
@users = @users.page(params[:page]).per(per_page) @users = @users.page(page).per(per_page)
paginate @users, per_page: per_page paginate @users, per_page: per_page
end end
private private
def page
params[:page] || 1
end
def per_page def per_page
params[:per_page] || 20 params[:per_page] || 20
end end

View File

@ -16,7 +16,7 @@ class OpenAPI::V1::EventsDoc < OpenAPI::V1::BaseDoc
param_group :pagination param_group :pagination
param :id, [Integer, Array], optional: true, desc: 'Scope the request to one or various events.' param :id, [Integer, Array], optional: true, desc: 'Scope the request to one or various events.'
param :upcoming, [FalseClass, TrueClass], optional: true, desc: 'Scope for the upcoming events.' param :upcoming, [FalseClass, TrueClass], optional: true, desc: 'Scope for the upcoming events.'
description 'Events index. Order by *created_at* desc.' description 'Events index, pagniated. Ordered by *created_at* desc.'
example <<-EVENTS example <<-EVENTS
# /open_api/v1/events?page=1&per_page=2 # /open_api/v1/events?page=1&per_page=2
{ {
@ -29,12 +29,21 @@ class OpenAPI::V1::EventsDoc < OpenAPI::V1::BaseDoc
"created_at": "2016-04-25T10:49:40.055+02:00", "created_at": "2016-04-25T10:49:40.055+02:00",
"nb_total_places": 18, "nb_total_places": 18,
"nb_free_places": 16, "nb_free_places": 16,
"start_at": "2016-05-02T18:00:00.000+02:00",
"end_at": "2016-05-02T22:00:00.000+02:00",
"category": "Openlab",
"event_image": {
"large_url": "https://example.com/uploads/event_image/3454/large_event_image.jpg",
"medium_url": "https://example.com/uploads/event_image/3454/medium_event_image.jpg",
"small_url": "https://example.com/uploads/event_image/3454/small_event_image.jpg"
},
"prices": { "prices": {
"normal": { "normal": {
"name": "Plein tarif", "name": "Plein tarif",
"amount": 0 "amount": 0
} }
} },
"url": "https://example.com/#!/events/183"
}, },
{ {
"id": 182, "id": 182,
@ -44,6 +53,19 @@ class OpenAPI::V1::EventsDoc < OpenAPI::V1::BaseDoc
"created_at": "2016-04-11T17:40:15.146+02:00", "created_at": "2016-04-11T17:40:15.146+02:00",
"nb_total_places": 8, "nb_total_places": 8,
"nb_free_places": 0, "nb_free_places": 0,
"start_at": "2016-05-02T18:00:00.000+01:00",
"end_at": "2026-05-02T22:00:00.000+01:00",
"category": "Atelier",
"themes": [
"DIY",
"Sport"
],
"age_range": "14 - 18 ans",
"event_image": {
"large_url": "https://example.com/uploads/event_image/3453/large_event_image.jpg",
"medium_url": "https://example.com/uploads/event_image/3453/medium_event_image.jpg",
"small_url": "https://example.com/uploads/event_image/3453/small_event_image.jpg"
},
"prices": { "prices": {
"normal": { "normal": {
"name": "Plein tarif", "name": "Plein tarif",
@ -53,7 +75,8 @@ class OpenAPI::V1::EventsDoc < OpenAPI::V1::BaseDoc
"name": "Tarif réduit", "name": "Tarif réduit",
"amount": 4000 "amount": 4000
}, },
} },
"url": "https://example.com/#!/events/182"
} }
] ]
} }

View File

@ -13,7 +13,7 @@ class OpenAPI::V1::InvoicesDoc < OpenAPI::V1::BaseDoc
doc_for :index do doc_for :index do
api :GET, "/#{API_VERSION}/invoices", 'Invoices index' api :GET, "/#{API_VERSION}/invoices", 'Invoices index'
description "Index of users' invoices, with optional pagination. Order by *created_at* descendant." description 'Index of invoices, paginated. Ordered by *created_at* descendant.'
param_group :pagination param_group :pagination
param :user_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various users.' param :user_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various users.'
example <<-INVOICES example <<-INVOICES

View File

@ -13,7 +13,7 @@ class OpenAPI::V1::ReservationsDoc < OpenAPI::V1::BaseDoc
doc_for :index do doc_for :index do
api :GET, "/#{API_VERSION}/reservations", 'Reservations index' api :GET, "/#{API_VERSION}/reservations", 'Reservations index'
description 'Index of reservations made by users, with optional pagination. Order by *created_at* descendant.' description 'Index of reservations made by users, paginated. Ordered by *created_at* descendant.'
param_group :pagination param_group :pagination
param :user_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various users.' 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_type, %w[Event Machine Space Training], optional: true, desc: 'Scope the request to a specific type of reservable.'
@ -42,7 +42,14 @@ class OpenAPI::V1::ReservationsDoc < OpenAPI::V1::BaseDoc
"description": "A partir de 15 ans : \r\n\r\nDécouvrez le Fab Lab, familiarisez-vous avec les découpeuses laser, les imprimantes 3D, la découpeuse vinyle ... ! Fabriquez un objet simple, à ramener chez vous ! \r\n\r\nAdoptez la Fab Lab attitude !", "description": "A partir de 15 ans : \r\n\r\nDécouvrez le Fab Lab, familiarisez-vous avec les découpeuses laser, les imprimantes 3D, la découpeuse vinyle ... ! Fabriquez un objet simple, à ramener chez vous ! \r\n\r\nAdoptez la Fab Lab attitude !",
"updated_at": "2016-03-21T15:55:56.306+01:00", "updated_at": "2016-03-21T15:55:56.306+01:00",
"created_at": "2016-03-21T15:55:56.306+01:00" "created_at": "2016-03-21T15:55:56.306+01:00"
},
"reserved_slots": [
{
"canceled_at": "2016-05-20T09:40:12.201+01:00",
"start_at": "2016-06-03T14:00:00.000+01:00",
"end_at": "2016-06-03T15:00:00.000+01:00"
} }
]
}, },
{ {
"id": 3252, "id": 3252,
@ -63,7 +70,14 @@ class OpenAPI::V1::ReservationsDoc < OpenAPI::V1::BaseDoc
"description": "A partir de 15 ans : \r\n\r\nDécouvrez le Fab Lab, familiarisez-vous avec les découpeuses laser, les imprimantes 3D, la découpeuse vinyle ... ! Fabriquez un objet simple, à ramener chez vous ! \r\n\r\nAdoptez la Fab Lab attitude !", "description": "A partir de 15 ans : \r\n\r\nDécouvrez le Fab Lab, familiarisez-vous avec les découpeuses laser, les imprimantes 3D, la découpeuse vinyle ... ! Fabriquez un objet simple, à ramener chez vous ! \r\n\r\nAdoptez la Fab Lab attitude !",
"updated_at": "2016-05-03T13:53:47.172+02:00", "updated_at": "2016-05-03T13:53:47.172+02:00",
"created_at": "2016-03-07T15:58:14.113+01:00" "created_at": "2016-03-07T15:58:14.113+01:00"
},
"reserved_slots": [
{
"canceled_at": null,
"start_at": "2016-06-02T16:00:00.000+01:00",
"end_at": "2016-06-02T17:00:00.000+01:00"
} }
]
}, },
{ {
"id": 3251, "id": 3251,
@ -84,7 +98,14 @@ class OpenAPI::V1::ReservationsDoc < OpenAPI::V1::BaseDoc
"description": "A partir de 15 ans : \r\n\r\nDécouvrez le Fab Lab, familiarisez-vous avec les découpeuses laser, les imprimantes 3D, la découpeuse vinyle ... ! Fabriquez un objet simple, à ramener chez vous ! \r\n\r\nAdoptez la Fab Lab attitude !", "description": "A partir de 15 ans : \r\n\r\nDécouvrez le Fab Lab, familiarisez-vous avec les découpeuses laser, les imprimantes 3D, la découpeuse vinyle ... ! Fabriquez un objet simple, à ramener chez vous ! \r\n\r\nAdoptez la Fab Lab attitude !",
"updated_at": "2016-03-21T15:55:56.306+01:00", "updated_at": "2016-03-21T15:55:56.306+01:00",
"created_at": "2016-03-21T15:55:56.306+01:00" "created_at": "2016-03-21T15:55:56.306+01:00"
},
"reserved_slots": [
{
"canceled_at": null,
"start_at": "2016-06-03T14:00:00.000+01:00",
"end_at": "2016-06-03T15:00:00.000+01:00"
} }
]
} }
] ]
} }

View File

@ -13,14 +13,14 @@ class OpenAPI::V1::SubscriptionsDoc < OpenAPI::V1::BaseDoc
doc_for :index do doc_for :index do
api :GET, "/#{API_VERSION}/subscriptions", 'Subscriptions index' api :GET, "/#{API_VERSION}/subscriptions", 'Subscriptions index'
description "Index of users' subscriptions, with optional pagination. Order by *created_at* descendant." description "Index of users' subscriptions, paginated. Order by *created_at* descendant."
param_group :pagination param_group :pagination
param :user_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various users.' param :user_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various users.'
param :plan_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various plans.' param :plan_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various plans.'
example <<-SUBSCRIPTIONS example <<-SUBSCRIPTIONS
# /open_api/v1/subscriptions?user_id=211&page=1&per_page=3 # /open_api/v1/subscriptions?user_id=211&page=1&per_page=3
{ {
"data": [ "subscriptions": [
{ {
"id": 2809, "id": 2809,
"user_id": 211, "user_id": 211,
@ -45,11 +45,7 @@ class OpenAPI::V1::SubscriptionsDoc < OpenAPI::V1::BaseDoc
"canceled_at": null, "canceled_at": null,
"plan_id": 1 "plan_id": 1
} }
], ]
"total_pages": 3,
"total_count": 9,
"page": 1,
"page_siez": 3
} }
SUBSCRIPTIONS SUBSCRIPTIONS
end end

View File

@ -13,7 +13,7 @@ class OpenAPI::V1::UsersDoc < OpenAPI::V1::BaseDoc
doc_for :index do doc_for :index do
api :GET, "/#{API_VERSION}/users", 'Users index' api :GET, "/#{API_VERSION}/users", 'Users index'
description 'Users index, with optional pagination. Order by *created_at* descendant.' description 'Users index, paginated. Ordered by *created_at* descendant.'
param_group :pagination param_group :pagination
param :email, [String, Array], optional: true, desc: 'Filter users by *email* using strict matching.' param :email, [String, Array], optional: true, desc: 'Filter users by *email* using strict matching.'
param :user_id, [Integer, Array], optional: true, desc: 'Filter users by *id* using strict matching.' param :user_id, [Integer, Array], optional: true, desc: 'Filter users by *id* using strict matching.'

View File

@ -17,6 +17,7 @@ import * as React from 'react';
import { User } from '../../../models/user'; import { User } from '../../../models/user';
import PrepaidPackAPI from '../../../api/prepaid-pack'; import PrepaidPackAPI from '../../../api/prepaid-pack';
import { PrepaidPack } from '../../../models/prepaid-pack'; import { PrepaidPack } from '../../../models/prepaid-pack';
import { HtmlTranslate } from '../../base/html-translate';
interface PrepaidPacksPanelProps { interface PrepaidPacksPanelProps {
user: User, user: User,
@ -159,7 +160,10 @@ const PrepaidPacksPanel: React.FC<PrepaidPacksPanelProps> = ({ user, onError })
onDecline={togglePacksModal} onDecline={togglePacksModal}
onSuccess={onPackBoughtSuccess} />} onSuccess={onPackBoughtSuccess} />}
</div>} </div>}
{packs.length === 0 && <p>{t('app.logged.dashboard.reservations_dashboard.prepaid_packs_panel.no_packs')}</p>}
{(packsForSubscribers && user.subscribed_plan == null && packs.length > 0) &&
<HtmlTranslate trKey={'app.logged.dashboard.reservations_dashboard.prepaid_packs_panel.reserved_for_subscribers_html'} options={{ LINK: '#!/plans' }} />
}
</FabPanel> </FabPanel>
); );
}; };

View File

@ -71,6 +71,19 @@ export const EventForm: React.FC<EventFormProps> = ({ action, event, onError, on
SettingAPI.get('advanced_accounting').then(res => setIsActiveAccounting(res.value === 'true')).catch(onError); SettingAPI.get('advanced_accounting').then(res => setIsActiveAccounting(res.value === 'true')).catch(onError);
}, []); }, []);
useEffect(() => {
// When a new custom price is added to the current event, we mark it as disabled to prevent setting the same category twice
const selectedCategoriesId = output.event_price_categories_attributes
?.filter(epc => !epc._destroy && epc.price_category_id)
?.map(epc => epc.price_category_id) || [];
setPriceCategoriesOptions(priceCategoriesOptions?.map(pco => {
return {
...pco,
disabled: selectedCategoriesId.includes(pco.value)
};
}));
}, [output.event_price_categories_attributes]);
/** /**
* Callback triggered when the user clicks on the 'remove' button, in the additional prices area * Callback triggered when the user clicks on the 'remove' button, in the additional prices area
*/ */
@ -278,9 +291,11 @@ export const EventForm: React.FC<EventFormProps> = ({ action, event, onError, on
type="number" type="number"
tooltip={t('app.admin.event_form.seats_help')} /> tooltip={t('app.admin.event_form.seats_help')} />
<FormInput register={register} <FormInput register={register}
type="number"
id="amount" id="amount"
formState={formState} formState={formState}
rules={{ required: true }} rules={{ required: true, min: 0 }}
nullable
label={t('app.admin.event_form.standard_rate')} label={t('app.admin.event_form.standard_rate')}
tooltip={t('app.admin.event_form.0_equal_free')} tooltip={t('app.admin.event_form.0_equal_free')}
addOn={FormatLib.currencySymbol()} /> addOn={FormatLib.currencySymbol()} />
@ -293,11 +308,13 @@ export const EventForm: React.FC<EventFormProps> = ({ action, event, onError, on
id={`event_price_categories_attributes.${index}.price_category_id`} id={`event_price_categories_attributes.${index}.price_category_id`}
rules={{ required: true }} rules={{ required: true }}
formState={formState} formState={formState}
disabled={() => index < fields.length - 1}
label={t('app.admin.event_form.fare_class')} /> label={t('app.admin.event_form.fare_class')} />
<FormInput id={`event_price_categories_attributes.${index}.amount`} <FormInput id={`event_price_categories_attributes.${index}.amount`}
register={register} register={register}
type="number" type="number"
rules={{ required: true }} rules={{ required: true, min: 0 }}
nullable
formState={formState} formState={formState}
label={t('app.admin.event_form.price')} label={t('app.admin.event_form.price')}
addOn={FormatLib.currencySymbol()} /> addOn={FormatLib.currencySymbol()} />

View File

@ -136,7 +136,7 @@ export const FormMultiSelect = <TFieldValues extends FieldValues, TContext exten
if (creatable) { if (creatable) {
Object.assign(selectProps, { Object.assign(selectProps, {
formatCreateLabel, formatCreateLabel,
onCreateOption: inputValue => handleCreate(inputValue, value, rhfOnChange) onCreateOption: inputValue => handleCreate(inputValue, value || [], rhfOnChange)
}); });
} }

View File

@ -66,7 +66,8 @@ export const FormSelect = <TFieldValues extends FieldValues, TContext extends ob
placeholder={placeholder} placeholder={placeholder}
isDisabled={isDisabled} isDisabled={isDisabled}
isClearable={clearable} isClearable={clearable}
options={options} /> options={options}
isOptionDisabled={(option) => option.disabled}/>
} /> } />
</AbstractFormItem> </AbstractFormItem>
); );

View File

@ -113,7 +113,7 @@ const StripeKeysForm: React.FC<StripeKeysFormProps> = ({ onValidKeys, onInvalidK
}, reason => { }, reason => {
if (!mounted.current) return; if (!mounted.current) return;
if (reason.response.status === 401) { if (reason.response?.status === 401) {
setSecretKeyAddOn(<i className="fa fa-times" />); setSecretKeyAddOn(<i className="fa fa-times" />);
setSecretKeyAddOnClassName('key-invalid'); setSecretKeyAddOnClassName('key-invalid');
} }

View File

@ -105,7 +105,7 @@ Application.Controllers.controller('AdminEventsController', ['$scope', '$state',
{ selected: '' }; { selected: '' };
// default tab: events list // default tab: events list
$scope.tabs = { active: 0 }; $scope.tabs = { active: 1 };
/** /**
* Adds a bucket of events to the bottom of the page, grouped by month * Adds a bucket of events to the bottom of the page, grouped by month

View File

@ -69,7 +69,7 @@ export interface Event {
export interface EventDecoration { export interface EventDecoration {
id?: number, id?: number,
name: string, name: string,
related_to?: number related_to?: number // report the count of events related to the given decoration
} }
export type EventTheme = EventDecoration; export type EventTheme = EventDecoration;

View File

@ -2,7 +2,7 @@
* Option format, expected by react-select * Option format, expected by react-select
* @see https://github.com/JedWatson/react-select * @see https://github.com/JedWatson/react-select
*/ */
export type SelectOption<TOptionValue, TOptionLabel = string> = { value: TOptionValue, label: TOptionLabel } export type SelectOption<TOptionValue, TOptionLabel = string> = { value: TOptionValue, label: TOptionLabel, disabled?: boolean }
/** /**
* Checklist Option format * Checklist Option format

View File

@ -41,10 +41,10 @@ class AuthProvider < ApplicationRecord
provider = find_by(status: 'active') provider = find_by(status: 'active')
return local if provider.nil? return local if provider.nil?
return provider provider
rescue ActiveRecord::StatementInvalid rescue ActiveRecord::StatementInvalid
# we fall here on database creation because the table "active_providers" still does not exists at the moment # we fall here on database creation because the table "active_providers" still does not exists at the moment
return local local
end end
end end
@ -59,7 +59,7 @@ class AuthProvider < ApplicationRecord
parsed = /^([^-]+)-(.+)$/.match(strategy_name) parsed = /^([^-]+)-(.+)$/.match(strategy_name)
ret = nil ret = nil
all.each do |strategy| all.find_each do |strategy|
if strategy.provider_type == parsed[1] && strategy.name.downcase.parameterize == parsed[2] if strategy.provider_type == parsed[1] && strategy.name.downcase.parameterize == parsed[2]
ret = strategy ret = strategy
break break
@ -70,13 +70,13 @@ class AuthProvider < ApplicationRecord
## Return the name that should be registered in OmniAuth for the corresponding strategy ## Return the name that should be registered in OmniAuth for the corresponding strategy
def strategy_name def strategy_name
provider_type + '-' + name.downcase.parameterize "#{provider_type}-#{name.downcase.parameterize}"
end end
## Return the provider type name without the "Provider" part. ## Return the provider type name without the "Provider" part.
## eg. DatabaseProvider will return 'database' ## eg. DatabaseProvider will return 'database'
def provider_type def provider_type
providable_type[0..-9].downcase providable_type[0..-9]&.downcase
end end
## Return the user's profile fields that are currently managed from the SSO ## Return the user's profile fields that are currently managed from the SSO
@ -84,7 +84,7 @@ class AuthProvider < ApplicationRecord
def sso_fields def sso_fields
fields = [] fields = []
auth_provider_mappings.each do |mapping| auth_provider_mappings.each do |mapping|
fields.push(mapping.local_model + '.' + mapping.local_field) fields.push("#{mapping.local_model}.#{mapping.local_field}")
end end
fields fields
end end
@ -96,10 +96,10 @@ class AuthProvider < ApplicationRecord
end end
def safe_destroy def safe_destroy
if status != 'active' if status == 'active'
destroy
else
false false
else
destroy
end end
end end

View File

@ -3,6 +3,5 @@
# OAuth2Provider is a special type of AuthProvider which provides authentication through an external SSO server using # OAuth2Provider is a special type of AuthProvider which provides authentication through an external SSO server using
# the oAuth 2.0 protocol. # the oAuth 2.0 protocol.
class OAuth2Provider < ApplicationRecord class OAuth2Provider < ApplicationRecord
has_one :auth_provider, as: :providable has_one :auth_provider, as: :providable, dependent: :destroy
end end

View File

@ -3,7 +3,7 @@
# OpenIdConnectProvider is a special type of AuthProvider which provides authentication through an external SSO server using # OpenIdConnectProvider is a special type of AuthProvider which provides authentication through an external SSO server using
# the OpenID Connect protocol. # the OpenID Connect protocol.
class OpenIdConnectProvider < ApplicationRecord class OpenIdConnectProvider < ApplicationRecord
has_one :auth_provider, as: :providable has_one :auth_provider, as: :providable, dependent: :destroy
validates :issuer, presence: true validates :issuer, presence: true
validates :client__identifier, presence: true validates :client__identifier, presence: true
@ -28,8 +28,8 @@ class OpenIdConnectProvider < ApplicationRecord
end end
def client_config def client_config
OpenIdConnectProvider.columns.map(&:name).filter { |n| n.start_with?('client__') }.map do |n| OpenIdConnectProvider.columns.map(&:name).filter { |n| n.start_with?('client__') }.to_h do |n|
[n.sub('client__', ''), send(n)] [n.sub('client__', ''), send(n)]
end.to_h end
end end
end end

View File

@ -10,7 +10,7 @@ json.is_completed slot.full?(reservable)
json.backgroundColor 'white' json.backgroundColor 'white'
json.availability_id slot.availability_id json.availability_id slot.availability_id
json.slots_reservations_ids Slots::ReservationsService.user_reservations(slot, user, reservable)[:reservations] json.slots_reservations_ids Slots::ReservationsService.user_reservations(slot, user, reservable)[:reservations].map(&:id)
json.tag_ids slot.availability.tag_ids json.tag_ids slot.availability.tag_ids
json.tags slot.availability.tags do |t| json.tags slot.availability.tags do |t|

View File

@ -7,7 +7,7 @@
</head> </head>
<body> <body>
<% param = @authorization_token ? "?auth_token=#{@authorization_token}" : '' %> <% param = @authorization_token ? "?auth_token=#{@authorization_token}" : '' %>
<% url_path = File.join(root_url, "users/auth/#{@active_provider.strategy_name}#{param}") %> <% url_path = URI.join("#{ENV.fetch('DEFAULT_PROTOCOL')}://#{ENV.fetch('DEFAULT_HOST')}", "users/auth/#{@active_provider.strategy_name}#{param}") %>
<form id="redirect-form" action="<%=url_path%>" method="post" target="_self"> <form id="redirect-form" action="<%=url_path%>" method="post" target="_self">
<%= hidden_field_tag :authenticity_token, @authentication_token %> <%= hidden_field_tag :authenticity_token, @authentication_token %>
<noscript> <noscript>

View File

@ -1 +1,3 @@
# frozen_string_literal: true
json.extract! event, :id, :title, :description, :updated_at, :created_at json.extract! event, :id, :title, :description, :updated_at, :created_at

View File

@ -1,8 +1,13 @@
# frozen_string_literal: true
json.events @events do |event| json.events @events do |event|
json.partial! 'open_api/v1/events/event', event: event json.partial! 'open_api/v1/events/event', event: event
json.extract! event, :nb_total_places, :nb_free_places json.extract! event, :nb_total_places, :nb_free_places
json.start_at event.availability.start_at json.start_at event.availability.start_at
json.end_at event.availability.end_at json.end_at event.availability.end_at
json.category event.category.name
json.themes event.event_themes&.map(&:name)
json.age_range event.age_range&.name
if event.event_image if event.event_image
json.event_image do json.event_image do
json.large_url root_url.chomp('/') + event.event_image.attachment.large.url json.large_url root_url.chomp('/') + event.event_image.attachment.large.url
@ -23,4 +28,5 @@ json.events @events do |event|
end end
end end
end end
json.url URI.join("#{ENV.fetch('DEFAULT_PROTOCOL')}://#{ENV.fetch('DEFAULT_HOST')}", "/#!/events/#{event.id}")
end end

View File

@ -2,7 +2,7 @@
json.invoices @invoices do |invoice| json.invoices @invoices do |invoice|
json.extract! invoice, :id, :reference, :total, :type, :description json.extract! invoice, :id, :reference, :total, :type, :description
json.user_id invoice.statistic_profile.user_id json.user_id invoice.invoicing_profile.user_id
if invoice.payment_gateway_object if invoice.payment_gateway_object
json.payment_gateway_object do json.payment_gateway_object do
json.id invoice.payment_gateway_object.gateway_object_id json.id invoice.payment_gateway_object.gateway_object_id

View File

@ -13,12 +13,18 @@ json.reservations @reservations do |reservation|
end end
json.reservable do json.reservable do
if reservation.reservable_type == 'Training' case reservation.reservable_type
when 'Training'
json.partial! 'open_api/v1/trainings/training', training: reservation.reservable json.partial! 'open_api/v1/trainings/training', training: reservation.reservable
elsif reservation.reservable_type == 'Machine' when 'Machine'
json.partial! 'open_api/v1/machines/machine', machine: reservation.reservable json.partial! 'open_api/v1/machines/machine', machine: reservation.reservable
elsif reservation.reservable_type == 'Event' when 'Event'
json.partial! 'open_api/v1/events/event', event: reservation.reservable json.partial! 'open_api/v1/events/event', event: reservation.reservable
end end
end end
json.reserved_slots reservation.slots_reservations do |slot_reservation|
json.extract! slot_reservation, :canceled_at
json.extract! slot_reservation.slot, :start_at, :end_at
end
end end

View File

@ -1,10 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
json.data @subscriptions do |subscription| json.subscriptions @subscriptions do |subscription|
json.extract! subscription, :id, :created_at, :expiration_date, :canceled_at, :plan_id json.extract! subscription, :id, :created_at, :expiration_date, :canceled_at, :plan_id
json.user_id subscription.statistic_profile.user_id json.user_id subscription.statistic_profile.user_id
end end
json.total_pages @pageination_meta[:total_pages]
json.total_count @pageination_meta[:total_count]
json.page @pageination_meta[:page]
json.page_siez @pageination_meta[:page_size]

View File

@ -1 +1,3 @@
# frozen_string_literal: true
json.extract! training, :id, :name, :slug, :disabled, :updated_at, :created_at json.extract! training, :id, :name, :slug, :disabled, :updated_at, :created_at

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
json.trainings @trainings do |training| json.trainings @trainings do |training|
json.partial! 'open_api/v1/trainings/training', training: training json.partial! 'open_api/v1/trainings/training', training: training
json.extract! training, :nb_total_places, :description json.extract! training, :nb_total_places, :description

View File

@ -12,6 +12,7 @@ Apipie.configure do |config|
config.app_info['v1'] = <<-RDOC config.app_info['v1'] = <<-RDOC
= Pagination = Pagination
--- ---
Some endpoints are paginated, because they provides many data. Other provides optional pagination.
You can ask for pagination on your requests, by providing the GET parameters *page* and *per_page* (when it's available). You can ask for pagination on your requests, by providing the GET parameters *page* and *per_page* (when it's available).
The meta-data about pagination will be returned in the headers, following RFC-5988 standard for web linking. The meta-data about pagination will be returned in the headers, following RFC-5988 standard for web linking.
It uses headers *Link*, *Total* and *Per-Page*. It uses headers *Link*, *Total* and *Per-Page*.

View File

@ -170,6 +170,8 @@ de:
cta_info: "You can buy prepaid hours packs to book machines and benefit from discounts. Choose a machine to buy a corresponding pack." cta_info: "You can buy prepaid hours packs to book machines and benefit from discounts. Choose a machine to buy a corresponding pack."
select_machine: "Select a machine" select_machine: "Select a machine"
cta_button: "Buy a pack" cta_button: "Buy a pack"
no_packs: "No prepaid packs available for sale"
reserved_for_subscribers_html: 'The purchase of prepaid packs is reserved for subscribers. <a href="{LINK}">Subscribe now</a> to benefit.'
#public profil of a member #public profil of a member
members_show: members_show:
members_list: "Mitgliederliste" members_list: "Mitgliederliste"

View File

@ -170,6 +170,8 @@ en:
cta_info: "You can buy prepaid hours packs to book machines and benefit from discounts. Choose a machine to buy a corresponding pack." cta_info: "You can buy prepaid hours packs to book machines and benefit from discounts. Choose a machine to buy a corresponding pack."
select_machine: "Select a machine" select_machine: "Select a machine"
cta_button: "Buy a pack" cta_button: "Buy a pack"
no_packs: "No prepaid packs available for sale"
reserved_for_subscribers_html: 'The purchase of prepaid packs is reserved for subscribers. <a href="{LINK}">Subscribe now</a> to benefit.'
#public profil of a member #public profil of a member
members_show: members_show:
members_list: "Members list" members_list: "Members list"

View File

@ -170,6 +170,8 @@ es:
cta_info: "You can buy prepaid hours packs to book machines and benefit from discounts. Choose a machine to buy a corresponding pack." cta_info: "You can buy prepaid hours packs to book machines and benefit from discounts. Choose a machine to buy a corresponding pack."
select_machine: "Select a machine" select_machine: "Select a machine"
cta_button: "Buy a pack" cta_button: "Buy a pack"
no_packs: "No prepaid packs available for sale"
reserved_for_subscribers_html: 'The purchase of prepaid packs is reserved for subscribers. <a href="{LINK}">Subscribe now</a> to benefit.'
#public profil of a member #public profil of a member
members_show: members_show:
members_list: "Lista de miembros" members_list: "Lista de miembros"

View File

@ -170,6 +170,8 @@ fr:
cta_info: "Vous pouvez acheter des packs d'heures prépayées pour réserver des machines et bénéficier de réductions. Choisissez une machine pour acheter un pack correspondant." cta_info: "Vous pouvez acheter des packs d'heures prépayées pour réserver des machines et bénéficier de réductions. Choisissez une machine pour acheter un pack correspondant."
select_machine: "Sélectionnez une machine" select_machine: "Sélectionnez une machine"
cta_button: "Acheter un pack" cta_button: "Acheter un pack"
no_packs: "Aucun pack prépayé disponible à la vente"
reserved_for_subscribers_html: 'L''achat de packs prépayés est réservé aux abonnés. <a href="{LINK}">Abonnez-vous maintenant</a> pour en bénéficier.'
#public profil of a member #public profil of a member
members_show: members_show:
members_list: "Liste des membres" members_list: "Liste des membres"

View File

@ -170,6 +170,8 @@
cta_info: "You can buy prepaid hours packs to book machines and benefit from discounts. Choose a machine to buy a corresponding pack." cta_info: "You can buy prepaid hours packs to book machines and benefit from discounts. Choose a machine to buy a corresponding pack."
select_machine: "Select a machine" select_machine: "Select a machine"
cta_button: "Buy a pack" cta_button: "Buy a pack"
no_packs: "No prepaid packs available for sale"
reserved_for_subscribers_html: 'The purchase of prepaid packs is reserved for subscribers. <a href="{LINK}">Subscribe now</a> to benefit.'
#public profil of a member #public profil of a member
members_show: members_show:
members_list: "Medlemsliste" members_list: "Medlemsliste"

View File

@ -170,6 +170,8 @@ pt:
cta_info: "You can buy prepaid hours packs to book machines and benefit from discounts. Choose a machine to buy a corresponding pack." cta_info: "You can buy prepaid hours packs to book machines and benefit from discounts. Choose a machine to buy a corresponding pack."
select_machine: "Select a machine" select_machine: "Select a machine"
cta_button: "Buy a pack" cta_button: "Buy a pack"
no_packs: "No prepaid packs available for sale"
reserved_for_subscribers_html: 'The purchase of prepaid packs is reserved for subscribers. <a href="{LINK}">Subscribe now</a> to benefit.'
#public profil of a member #public profil of a member
members_show: members_show:
members_list: "Lista de membros" members_list: "Lista de membros"

View File

@ -170,6 +170,8 @@ zu:
cta_info: "crwdns37065:0crwdne37065:0" cta_info: "crwdns37065:0crwdne37065:0"
select_machine: "crwdns37067:0crwdne37067:0" select_machine: "crwdns37067:0crwdne37067:0"
cta_button: "crwdns37069:0crwdne37069:0" cta_button: "crwdns37069:0crwdne37069:0"
no_packs: "crwdns37389:0crwdne37389:0"
reserved_for_subscribers_html: 'crwdns37391:0{LINK}crwdne37391:0'
#public profil of a member #public profil of a member
members_show: members_show:
members_list: "crwdns27656:0crwdne27656:0" members_list: "crwdns27656:0crwdne27656:0"

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
# Add uniqueness constraint at database level
class AddUniquenessConstraints < ActiveRecord::Migration[5.2]
def change
add_index :credits, %i[plan_id creditable_id creditable_type], unique: true
add_index :prices, %i[plan_id priceable_id priceable_type group_id duration], unique: true,
name: 'index_prices_on_plan_priceable_group_and_duration'
add_index :price_categories, :name, unique: true
add_index :auth_providers, :name, unique: true
end
end

View File

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2023_02_13_134954) do ActiveRecord::Schema.define(version: 2023_03_02_120458) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "fuzzystrmatch" enable_extension "fuzzystrmatch"
@ -122,6 +122,7 @@ ActiveRecord::Schema.define(version: 2023_02_13_134954) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.string "providable_type" t.string "providable_type"
t.integer "providable_id" t.integer "providable_id"
t.index ["name"], name: "index_auth_providers_on_name", unique: true
end end
create_table "availabilities", id: :serial, force: :cascade do |t| create_table "availabilities", id: :serial, force: :cascade do |t|
@ -163,10 +164,10 @@ ActiveRecord::Schema.define(version: 2023_02_13_134954) do
create_table "cart_item_event_reservation_tickets", force: :cascade do |t| create_table "cart_item_event_reservation_tickets", force: :cascade do |t|
t.integer "booked" t.integer "booked"
t.bigint "event_price_category_id"
t.bigint "cart_item_event_reservation_id" t.bigint "cart_item_event_reservation_id"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.bigint "event_price_category_id"
t.index ["cart_item_event_reservation_id"], name: "index_cart_item_tickets_on_cart_item_event_reservation" t.index ["cart_item_event_reservation_id"], name: "index_cart_item_tickets_on_cart_item_event_reservation"
t.index ["event_price_category_id"], name: "index_cart_item_tickets_on_event_price_category" t.index ["event_price_category_id"], name: "index_cart_item_tickets_on_event_price_category"
end end
@ -287,6 +288,7 @@ ActiveRecord::Schema.define(version: 2023_02_13_134954) do
t.integer "hours" t.integer "hours"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.index ["plan_id", "creditable_id", "creditable_type"], name: "index_credits_on_plan_id_and_creditable_id_and_creditable_type", unique: true
t.index ["plan_id"], name: "index_credits_on_plan_id" t.index ["plan_id"], name: "index_credits_on_plan_id"
end end
@ -787,6 +789,8 @@ ActiveRecord::Schema.define(version: 2023_02_13_134954) do
t.text "conditions" t.text "conditions"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index "btrim(lower((name)::text))", name: "index_price_categories_on_TRIM_BOTH_FROM_LOWER_name", unique: true
t.index ["name"], name: "index_price_categories_on_name", unique: true
end end
create_table "prices", id: :serial, force: :cascade do |t| create_table "prices", id: :serial, force: :cascade do |t|
@ -799,6 +803,7 @@ ActiveRecord::Schema.define(version: 2023_02_13_134954) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.integer "duration", default: 60 t.integer "duration", default: 60
t.index ["group_id"], name: "index_prices_on_group_id" t.index ["group_id"], name: "index_prices_on_group_id"
t.index ["plan_id", "priceable_id", "priceable_type", "group_id", "duration"], name: "index_prices_on_plan_priceable_group_and_duration", unique: true
t.index ["plan_id"], name: "index_prices_on_plan_id" t.index ["plan_id"], name: "index_prices_on_plan_id"
t.index ["priceable_type", "priceable_id"], name: "index_prices_on_priceable_type_and_priceable_id" t.index ["priceable_type", "priceable_id"], name: "index_prices_on_priceable_type_and_priceable_id"
end end

View File

@ -15,6 +15,8 @@ STRIPE_PUBLISHABLE_KEY=
# oAuth SSO keys for tests # oAuth SSO keys for tests
OAUTH_CLIENT_ID=github-oauth-app-id OAUTH_CLIENT_ID=github-oauth-app-id
OAUTH_CLIENT_SECRET=github-oauth-app-secret OAUTH_CLIENT_SECRET=github-oauth-app-secret
OIDC_CLIENT_ID=oidc-client-id
OIDC_CLIENT_SECRET=oidc-client-secret
# Configure carefully! # Configure carefully!
DEFAULT_HOST=localhost:5000 DEFAULT_HOST=localhost:5000

View File

@ -1,6 +1,6 @@
{ {
"name": "fab-manager", "name": "fab-manager",
"version": "5.7.2", "version": "5.8.0",
"description": "Fab-manager is the FabLab management solution. It provides a comprehensive, web-based, open-source tool to simplify your administrative tasks and your marker's projects.", "description": "Fab-manager is the FabLab management solution. It provides a comprehensive, web-based, open-source tool to simplify your administrative tasks and your marker's projects.",
"keywords": [ "keywords": [
"fablab", "fablab",

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Use this script to safely run the test suite. # Use this script to safely run the test suite after any database changes.
# This must be preferred over `rails test`. # This must be preferred over `rails test`.
stripe_public_key=$(RAILS_ENV='test' bin/rails runner "puts ENV['STRIPE_PUBLISHABLE_KEY']") stripe_public_key=$(RAILS_ENV='test' bin/rails runner "puts ENV['STRIPE_PUBLISHABLE_KEY']")

View File

@ -49,10 +49,10 @@ ALLOW_INSECURE_HTTP=false
ENABLE_SENTRY=false ENABLE_SENTRY=false
# 5242880 = 5 megabytes # 5242880 = 5 megabytes
MAX_IMPORT_SIZE='5242880' MAX_IMPORT_SIZE=5242880
# 10485760 = 10 megabytes # 10485760 = 10 megabytes
MAX_IMAGE_SIZE='10485760' MAX_IMAGE_SIZE=10485760
# 20971520 = 20 megabytes # 20971520 = 20 megabytes
MAX_CAO_SIZE='20971520' MAX_CAO_SIZE=20971520
# 5242880 = 5 megabytes # 5242880 = 5 megabytes
MAX_SUPPORTING_DOCUMENT_FILE_SIZE='5242880' MAX_SUPPORTING_DOCUMENT_FILE_SIZE=5242880

View File

@ -648,7 +648,7 @@ history_value_66:
history_value_67: history_value_67:
id: 67 id: 67
setting_id: 67 setting_id: 67
value: pk_test_aScrMu3y4AocfCN5XLJjGzmQ value: <%=ENV.fetch('STRIPE_PUBLISHABLE_KEY', 'pk_test_faketestfaketestfaketest') %>
created_at: '2020-06-08 17:12:16.846525' created_at: '2020-06-08 17:12:16.846525'
updated_at: '2021-05-31 15:00:37.210049' updated_at: '2021-05-31 15:00:37.210049'
footprint: 4984215605d9f30ac4f9594bc0d552d6b5e280f650801399b698aa43188001a5 footprint: 4984215605d9f30ac4f9594bc0d552d6b5e280f650801399b698aa43188001a5
@ -657,7 +657,7 @@ history_value_67:
history_value_68: history_value_68:
id: 68 id: 68
setting_id: 68 setting_id: 68
value: sk_test_mGokO9TGtrVxMOyK4yZiktBE value: <%=ENV.fetch('STRIPE_API_KEY', 'sk_test_testfaketestfaketestfake') %>
created_at: '2020-06-08 17:12:16.846525' created_at: '2020-06-08 17:12:16.846525'
updated_at: '2021-05-31 15:00:37.280668' updated_at: '2021-05-31 15:00:37.280668'
footprint: 48db504877d3329e39d1e816b243629c44b47be9f2837e2e4af4f30ca7cbd3e8 footprint: 48db504877d3329e39d1e816b243629c44b47be9f2837e2e4af4f30ca7cbd3e8

View File

@ -0,0 +1,8 @@
import { AgeRange } from 'models/event';
const ageRanges: Array<AgeRange> = [
{ id: 1, name: 'Children' },
{ id: 2, name: 'Over 18' }
];
export default ageRanges;

View File

@ -0,0 +1,8 @@
import { EventCategory } from 'models/event';
const categories: Array<EventCategory> = [
{ id: 1, name: 'Workshop' },
{ id: 2, name: 'Internship' }
];
export default categories;

View File

@ -0,0 +1,8 @@
import { EventPriceCategory } from 'models/event';
const categories: Array<EventPriceCategory> = [
{ id: 1, name: 'Students' },
{ id: 2, name: 'Partners' }
];
export default categories;

View File

@ -0,0 +1,8 @@
import { EventTheme } from 'models/event';
const themes: Array<EventTheme> = [
{ id: 1, name: 'Fabric week' },
{ id: 2, name: 'Everyone at the Fablab' }
];
export default themes;

View File

@ -13,6 +13,10 @@ import spaces from '../__fixtures__/spaces';
import statuses from '../__fixtures__/statuses'; import statuses from '../__fixtures__/statuses';
import notificationTypes from '../__fixtures__/notification_types'; import notificationTypes from '../__fixtures__/notification_types';
import notifications from '../__fixtures__/notifications'; import notifications from '../__fixtures__/notifications';
import eventCategories from '../__fixtures__/event_categories';
import eventThemes from '../__fixtures__/event_themes';
import ageRanges from '../__fixtures__/age_ranges';
import eventPriceCategories from '../__fixtures__/event_price_categories';
const FixturesLib = { const FixturesLib = {
init: () => { init: () => {
@ -33,7 +37,11 @@ const FixturesLib = {
spaces: JSON.parse(JSON.stringify(spaces)), spaces: JSON.parse(JSON.stringify(spaces)),
statuses: JSON.parse(JSON.stringify(statuses)), statuses: JSON.parse(JSON.stringify(statuses)),
notificationTypes: JSON.parse(JSON.stringify(notificationTypes)), notificationTypes: JSON.parse(JSON.stringify(notificationTypes)),
notifications: JSON.parse(JSON.stringify(notifications)) notifications: JSON.parse(JSON.stringify(notifications)),
eventCategories: JSON.parse(JSON.stringify(eventCategories)),
eventThemes: JSON.parse(JSON.stringify(eventThemes)),
ageRanges: JSON.parse(JSON.stringify(ageRanges)),
eventPriceCategories: JSON.parse(JSON.stringify(eventPriceCategories))
}; };
} }
}; };

View File

@ -113,10 +113,22 @@ export const server = setupServer(
return res(ctx.json({ status })); return res(ctx.json({ status }));
}), }),
rest.get('/api/notification_types', (req, res, ctx) => { rest.get('/api/notification_types', (req, res, ctx) => {
return res(ctx.json(fixtures.notification_types)); return res(ctx.json(fixtures.notificationTypes));
}), }),
rest.get('/api/notifications', (req, res, ctx) => { rest.get('/api/notifications', (req, res, ctx) => {
return res(ctx.json(fixtures.notifications)); return res(ctx.json(fixtures.notifications));
}),
rest.get('/api/categories', (req, res, ctx) => {
return res(ctx.json(fixtures.eventCategories));
}),
rest.get('/api/event_themes', (req, res, ctx) => {
return res(ctx.json(fixtures.eventThemes));
}),
rest.get('/api/age_ranges', (req, res, ctx) => {
return res(ctx.json(fixtures.ageRanges));
}),
rest.get('/api/price_categories', (req, res, ctx) => {
return res(ctx.json(fixtures.eventPriceCategories));
}) })
); );

View File

@ -0,0 +1,68 @@
import { EventForm } from 'components/events/event-form';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import selectEvent from 'react-select-event';
import eventPriceCategories from '../../__fixtures__/event_price_categories';
describe('EventForm', () => {
const onError = jest.fn();
const onSuccess = jest.fn();
test('render create EventForm', async () => {
render(<EventForm action="create" onError={onError} onSuccess={onSuccess} />);
await waitFor(() => screen.getByRole('combobox', { name: /app.admin.event_form.event_category/ }));
expect(screen.getByLabelText(/app.admin.event_form.title/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.matching_visual/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.description/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.event_category/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.event_themes/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.age_range/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.start_date/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.end_date/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.all_day/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.start_time/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.end_time/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.recurrence/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form._and_ends_on/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.seats_available/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.standard_rate/)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /app.admin.event_form.add_price/ })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /app.admin.event_form.add_a_new_file/ })).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.advanced_accounting_form.code/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.advanced_accounting_form.analytical_section/)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /app.admin.event_form.save/ })).toBeInTheDocument();
});
test('all day event hides the time inputs', async () => {
render(<EventForm action="create" onError={onError} onSuccess={onSuccess} />);
await waitFor(() => screen.getByRole('combobox', { name: /app.admin.event_form.event_category/ }));
const user = userEvent.setup();
await user.click(screen.getByLabelText(/app.admin.event_form.all_day/));
expect(screen.queryByLabelText(/app.admin.event_form.start_time/)).toBeNull();
expect(screen.queryByLabelText(/app.admin.event_form.end_time/)).toBeNull();
});
test('recurrent event requires end date', async () => {
render(<EventForm action="create" onError={onError} onSuccess={onSuccess} />);
await waitFor(() => screen.getByRole('combobox', { name: /app.admin.event_form.event_category/ }));
await selectEvent.select(screen.getByLabelText(/app.admin.event_form.recurrence/), 'app.admin.event_form.recurring.every_week');
expect(screen.getByLabelText(/app.admin.event_form._and_ends_on/).closest('label')).toHaveClass('is-required');
});
test('adding a second custom rate', async () => {
render(<EventForm action="create" onError={onError} onSuccess={onSuccess} />);
await waitFor(() => screen.getByRole('combobox', { name: /app.admin.event_form.event_category/ }));
// add a first category
fireEvent.click(screen.getByRole('button', { name: /app.admin.event_form.add_price/ }));
expect(screen.getByLabelText(/app.admin.event_form.fare_class/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.price/)).toBeInTheDocument();
await selectEvent.select(screen.getByLabelText(/app.admin.event_form.fare_class/), eventPriceCategories[0].name);
fireEvent.change(screen.getByLabelText(/app.admin.event_form.price/), { target: { value: 10 } });
// add a second category
fireEvent.click(screen.getByRole('button', { name: /app.admin.event_form.add_price/ }));
expect(screen.getAllByLabelText(/app.admin.event_form.fare_class/)[0]).toBeDisabled();
await selectEvent.openMenu(screen.getAllByLabelText(/app.admin.event_form.fare_class/)[1]);
expect(screen.getAllByText(eventPriceCategories[0].name).find(element => element.classList.contains('rs__option'))).toHaveAttribute('aria-disabled', 'true');
});
});

View File

@ -32,4 +32,32 @@ module AuthProviderHelper
] ]
} }
end end
def keycloak_provider_params(name)
{
name: name,
providable_type: 'OpenIdConnectProvider',
providable_attributes: {
issuer: 'https://sso.sleede.dev/auth/realms/master',
discovery: true,
client_auth_method: 'basic',
scope: %w[openid profile email toto],
prompt: 'consent',
send_scope_to_token_endpoint: true,
profile_url: 'https://sso.sleede.dev/auth/realms/master/account/',
client__identifier: ENV.fetch('OIDC_CLIENT_ID', 'oidc-client-id'),
client__secret: ENV.fetch('OIDC_CLIENT_SECRET', 'oidc-client-secret'),
client__authorization_endpoint: '',
client__token_endpoint: '',
client__userinfo_endpoint: '',
client__end_session_endpoint: ''
},
auth_provider_mappings_attributes: [
{ id: '', local_model: 'user', local_field: 'uid', api_endpoint: 'user_info', api_data_type: 'json', api_field: 'sub' },
{ id: '', local_model: 'user', local_field: 'email', api_endpoint: 'user_info', api_data_type: 'json', api_field: 'email' },
{ id: '', local_model: 'profile', local_field: 'first_name', api_endpoint: 'user_info', api_data_type: 'json', api_field: 'given_name' },
{ id: '', local_model: 'profile', local_field: 'last_name', api_endpoint: 'user_info', api_data_type: 'json', api_field: 'family_name' }
]
}
end
end end

View File

@ -9,12 +9,27 @@ class OpenApi::EventsTest < ActionDispatch::IntegrationTest
@token = OpenAPI::Client.find_by(name: 'minitest').token @token = OpenAPI::Client.find_by(name: 'minitest').token
end end
test 'list all events' do test 'list events' do
get '/open_api/v1/events', headers: open_api_headers(@token) get '/open_api/v1/events', headers: open_api_headers(@token)
assert_response :success assert_response :success
events = json_response(response.body)
assert_not_empty events[:events]
assert(events[:events].all? { |event| !event[:id].nil? })
assert(events[:events].all? { |event| !event[:title].nil? })
assert(events[:events].all? { |event| !event[:description].nil? })
assert(events[:events].all? { |event| !event[:updated_at].nil? })
assert(events[:events].all? { |event| !event[:created_at].nil? })
assert(events[:events].all? { |event| !event[:nb_total_places].nil? })
assert(events[:events].all? { |event| !event[:nb_free_places].nil? })
assert(events[:events].all? { |event| !event[:start_at].nil? })
assert(events[:events].all? { |event| !event[:end_at].nil? })
assert(events[:events].all? { |event| !event[:category].nil? })
assert(events[:events].all? { |event| !event[:prices][:normal].nil? })
assert(events[:events].all? { |event| !event[:url].nil? })
end end
test 'list all events with pagination' do test 'list events with pagination details' do
get '/open_api/v1/events?page=1&per_page=5', headers: open_api_headers(@token) get '/open_api/v1/events?page=1&per_page=5', headers: open_api_headers(@token)
assert_response :success assert_response :success
end end
@ -29,7 +44,7 @@ class OpenApi::EventsTest < ActionDispatch::IntegrationTest
assert_response :success assert_response :success
end end
test 'list all upcoming events with pagination' do test 'list all upcoming events with pagination details' do
get '/open_api/v1/events?upcoming=true&page=1&per_page=5', headers: open_api_headers(@token) get '/open_api/v1/events?upcoming=true&page=1&per_page=5', headers: open_api_headers(@token)
assert_response :success assert_response :success
end end

View File

@ -9,22 +9,25 @@ class OpenApi::InvoicesTest < ActionDispatch::IntegrationTest
@token = OpenAPI::Client.find_by(name: 'minitest').token @token = OpenAPI::Client.find_by(name: 'minitest').token
end end
test 'list all invoices' do test 'list invoices' do
get '/open_api/v1/invoices', headers: open_api_headers(@token) get '/open_api/v1/invoices', headers: open_api_headers(@token)
assert_response :success assert_response :success
assert_equal Mime[:json], response.content_type
assert_not_empty json_response(response.body)[:invoices]
end end
test 'list all invoices with pagination' do test 'list invoices with pagination details' do
get '/open_api/v1/invoices?page=1&per_page=5', headers: open_api_headers(@token) get '/open_api/v1/invoices?page=1&per_page=5', headers: open_api_headers(@token)
assert_response :success assert_response :success
end end
test 'list all invoices for a user' do test 'list invoices for a user' do
get '/open_api/v1/invoices?user_id=3', headers: open_api_headers(@token) get '/open_api/v1/invoices?user_id=3', headers: open_api_headers(@token)
assert_response :success assert_response :success
end end
test 'list all invoices for a user with pagination' do test 'list invoices for a user with pagination details' do
get '/open_api/v1/invoices?user_id=3&page=1&per_page=5', headers: open_api_headers(@token) get '/open_api/v1/invoices?user_id=3&page=1&per_page=5', headers: open_api_headers(@token)
assert_response :success assert_response :success
end end
@ -32,6 +35,6 @@ class OpenApi::InvoicesTest < ActionDispatch::IntegrationTest
test 'download an invoice' do test 'download an invoice' do
get '/open_api/v1/invoices/3/download', headers: open_api_headers(@token) get '/open_api/v1/invoices/3/download', headers: open_api_headers(@token)
assert_response :success assert_response :success
assert_match /^inline; filename=/, response.headers['Content-Disposition'] assert_match(/^inline; filename=/, response.headers['Content-Disposition'])
end end
end end

View File

@ -9,12 +9,12 @@ class OpenApi::ReservationsTest < ActionDispatch::IntegrationTest
@token = OpenAPI::Client.find_by(name: 'minitest').token @token = OpenAPI::Client.find_by(name: 'minitest').token
end end
test 'list all reservations' do test 'list reservations ' do
get '/open_api/v1/reservations', headers: open_api_headers(@token) get '/open_api/v1/reservations', headers: open_api_headers(@token)
assert_response :success assert_response :success
assert_equal Mime[:json], response.content_type assert_equal Mime[:json], response.content_type
assert_equal Reservation.count, json_response(response.body)[:reservations].length assert_not_empty json_response(response.body)[:reservations]
end end
test 'list all reservations with pagination' do test 'list all reservations with pagination' do

View File

@ -9,12 +9,15 @@ class OpenApi::SubscriptionsTest < ActionDispatch::IntegrationTest
@token = OpenAPI::Client.find_by(name: 'minitest').token @token = OpenAPI::Client.find_by(name: 'minitest').token
end end
test 'list all subscriptions' do test 'list subscriptions' do
get '/open_api/v1/subscriptions', headers: open_api_headers(@token) get '/open_api/v1/subscriptions', headers: open_api_headers(@token)
assert_response :success assert_response :success
assert_equal Mime[:json], response.content_type
assert_not_empty json_response(response.body)[:subscriptions]
end end
test 'list all subscriptions with pagination' do test 'list subscriptions with pagination' do
get '/open_api/v1/subscriptions?page=1&per_page=5', headers: open_api_headers(@token) get '/open_api/v1/subscriptions?page=1&per_page=5', headers: open_api_headers(@token)
assert_response :success assert_response :success
end end

View File

@ -0,0 +1,59 @@
# frozen_string_literal: true
require 'test_helper'
require 'helpers/auth_provider_helper'
class OpenIdConnectTest < ActionDispatch::IntegrationTest
include AuthProviderHelper
setup do
@admin = User.find_by(username: 'admin')
login_as(@admin, scope: :user)
Fablab::Application.load_tasks if Rake::Task.tasks.empty?
end
test 'create and activate an OIDC provider' do
name = 'Sleede'
post '/api/auth_providers',
params: {
auth_provider: keycloak_provider_params(name)
}.to_json,
headers: default_headers
# Check response format & status
assert_equal 201, response.status, response.body
assert_equal Mime[:json], response.content_type
# Check the provider was correctly created
db_provider = OpenIdConnectProvider.includes(:auth_provider).where('auth_providers.name': name).first&.auth_provider
assert_not_nil db_provider
provider = json_response(response.body)
assert_equal name, provider[:name]
assert_equal db_provider&.id, provider[:id]
assert_equal 'pending', provider[:status]
assert_equal 4, provider[:auth_provider_mappings_attributes].length
# now let's activate this new provider
Rake::Task['fablab:auth:switch_provider'].execute(Rake::TaskArguments.new([:provider], [name]))
# Check it is correctly activated
db_provider&.reload
assert_equal 'active', db_provider&.status
assert_equal AuthProvider.active.id, db_provider&.id
# TODO, login with the SSO (need debugging)
## The following doesn't work but I can't find out why... Maybe configuring Devise like this is not the right way,
## but when testing the process with Capybara, I always fall with the message "Not found. Authentication passthru."
# Simulate an application restart (reload routes and change devise setup)
# logout
# Devise.setup do |config|
# require_relative '../../lib/omni_auth/openid_connect'
# config.omniauth OmniAuth::Strategies::SsoOpenidConnectProvider.name&.to_sym,
# db_provider&.providable&.config
# end
# User.devise :omniauthable, omniauth_providers: [db_provider&.strategy_name&.to_sym]
# Rails.application.reload_routes!
end
end

View File

@ -19,7 +19,11 @@ VCR.configure do |config|
config.hook_into :webmock config.hook_into :webmock
config.filter_sensitive_data('sk_test_testfaketestfaketestfake') { Setting.get('stripe_secret_key') } config.filter_sensitive_data('sk_test_testfaketestfaketestfake') { Setting.get('stripe_secret_key') }
config.filter_sensitive_data('pk_test_faketestfaketestfaketest') { Setting.get('stripe_public_key') } config.filter_sensitive_data('pk_test_faketestfaketestfaketest') { Setting.get('stripe_public_key') }
config.ignore_request { |req| URI(req.uri).port == 9200 } config.filter_sensitive_data('github-oauth-app-id') { ENV.fetch('OAUTH_CLIENT_ID') }
config.filter_sensitive_data('github-oauth-app-secret') { ENV.fetch('OAUTH_CLIENT_SECRET') }
config.filter_sensitive_data('oidc-client-id') { ENV.fetch('OIDC_CLIENT_ID') }
config.filter_sensitive_data('oidc-client-secret') { ENV.fetch('OIDC_CLIENT_SECRET') }
config.ignore_request { |req| URI(req.uri).port == 9200 || URI(req.uri).host == '127.0.0.1' }
end end
Sidekiq::Testing.fake! Sidekiq::Testing.fake!
@ -43,7 +47,7 @@ class ActiveSupport::TestCase
end end
def upload_headers def upload_headers
{ 'Accept' => Mime[:json], 'Content-Type' => 'multipart/form-data' } { 'Accept' => Mime[:json], 'Content-Type' => Mime[:multipart_form].to_s }
end end
def open_api_headers(token) def open_api_headers(token)