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:
commit
4228b44226
21
CHANGELOG.md
21
CHANGELOG.md
@ -1,10 +1,29 @@
|
||||
# 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
|
||||
|
||||
- Fix a bug: unable to update recurrent events
|
||||
- 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
|
||||
|
||||
|
@ -19,14 +19,16 @@ class OpenAPI::V1::EventsController < OpenAPI::V1::BaseController
|
||||
|
||||
@events = @events.where(id: may_array(params[:id])) if params[:id].present?
|
||||
|
||||
return if params[:page].blank?
|
||||
|
||||
@events = @events.page(params[:page]).per(per_page)
|
||||
@events = @events.page(page).per(per_page)
|
||||
paginate @events, per_page: per_page
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def page
|
||||
params[:page] || 1
|
||||
end
|
||||
|
||||
def per_page
|
||||
params[:per_page] || 20
|
||||
end
|
||||
|
@ -8,13 +8,11 @@ class OpenAPI::V1::InvoicesController < OpenAPI::V1::BaseController
|
||||
|
||||
def index
|
||||
@invoices = Invoice.order(created_at: :desc)
|
||||
.includes(invoicing_profile: :user)
|
||||
.includes(:payment_gateway_object, :invoicing_profile)
|
||||
.references(:invoicing_profiles)
|
||||
|
||||
@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)
|
||||
paginate @invoices, per_page: per_page
|
||||
end
|
||||
|
@ -8,16 +8,14 @@ class OpenAPI::V1::ReservationsController < OpenAPI::V1::BaseController
|
||||
|
||||
def index
|
||||
@reservations = Reservation.order(created_at: :desc)
|
||||
.includes(statistic_profile: :user)
|
||||
.includes(slots_reservations: :slot, statistic_profile: :user)
|
||||
.references(:statistic_profiles)
|
||||
|
||||
@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_id: may_array(params[:reservable_id])) if params[:reservable_id].present?
|
||||
|
||||
return if params[:page].blank?
|
||||
|
||||
@reservations = @reservations.page(params[:page]).per(per_page)
|
||||
@reservations = @reservations.page(page).per(per_page)
|
||||
paginate @reservations, per_page: per_page
|
||||
end
|
||||
|
||||
@ -27,6 +25,10 @@ class OpenAPI::V1::ReservationsController < OpenAPI::V1::BaseController
|
||||
type.singularize.classify
|
||||
end
|
||||
|
||||
def page
|
||||
params[:page] || 1
|
||||
end
|
||||
|
||||
def per_page
|
||||
params[:per_page] || 20
|
||||
end
|
||||
|
@ -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.page(page).per(per_page)
|
||||
@pageination_meta = pageination_meta
|
||||
paginate @subscriptions, per_page: per_page
|
||||
end
|
||||
|
||||
@ -30,14 +29,4 @@ class OpenAPI::V1::SubscriptionsController < OpenAPI::V1::BaseController
|
||||
def per_page
|
||||
params[:per_page] || 20
|
||||
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
|
||||
|
@ -18,12 +18,16 @@ class OpenAPI::V1::UsersController < OpenAPI::V1::BaseController
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def page
|
||||
params[:page] || 1
|
||||
end
|
||||
|
||||
def per_page
|
||||
params[:per_page] || 20
|
||||
end
|
||||
|
@ -16,7 +16,7 @@ class OpenAPI::V1::EventsDoc < OpenAPI::V1::BaseDoc
|
||||
param_group :pagination
|
||||
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.'
|
||||
description 'Events index. Order by *created_at* desc.'
|
||||
description 'Events index, pagniated. Ordered by *created_at* desc.'
|
||||
example <<-EVENTS
|
||||
# /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",
|
||||
"nb_total_places": 18,
|
||||
"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": {
|
||||
"normal": {
|
||||
"name": "Plein tarif",
|
||||
"amount": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": "https://example.com/#!/events/183"
|
||||
},
|
||||
{
|
||||
"id": 182,
|
||||
@ -44,6 +53,19 @@ class OpenAPI::V1::EventsDoc < OpenAPI::V1::BaseDoc
|
||||
"created_at": "2016-04-11T17:40:15.146+02:00",
|
||||
"nb_total_places": 8,
|
||||
"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": {
|
||||
"normal": {
|
||||
"name": "Plein tarif",
|
||||
@ -53,7 +75,8 @@ class OpenAPI::V1::EventsDoc < OpenAPI::V1::BaseDoc
|
||||
"name": "Tarif réduit",
|
||||
"amount": 4000
|
||||
},
|
||||
}
|
||||
},
|
||||
"url": "https://example.com/#!/events/182"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ class OpenAPI::V1::InvoicesDoc < OpenAPI::V1::BaseDoc
|
||||
|
||||
doc_for :index do
|
||||
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 :user_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various users.'
|
||||
example <<-INVOICES
|
||||
|
@ -13,7 +13,7 @@ class OpenAPI::V1::ReservationsDoc < OpenAPI::V1::BaseDoc
|
||||
|
||||
doc_for :index do
|
||||
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 :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.'
|
||||
@ -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 !",
|
||||
"updated_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,
|
||||
@ -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 !",
|
||||
"updated_at": "2016-05-03T13:53:47.172+02: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,
|
||||
@ -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 !",
|
||||
"updated_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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -13,14 +13,14 @@ class OpenAPI::V1::SubscriptionsDoc < OpenAPI::V1::BaseDoc
|
||||
|
||||
doc_for :index do
|
||||
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 :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.'
|
||||
example <<-SUBSCRIPTIONS
|
||||
# /open_api/v1/subscriptions?user_id=211&page=1&per_page=3
|
||||
{
|
||||
"data": [
|
||||
"subscriptions": [
|
||||
{
|
||||
"id": 2809,
|
||||
"user_id": 211,
|
||||
@ -45,11 +45,7 @@ class OpenAPI::V1::SubscriptionsDoc < OpenAPI::V1::BaseDoc
|
||||
"canceled_at": null,
|
||||
"plan_id": 1
|
||||
}
|
||||
],
|
||||
"total_pages": 3,
|
||||
"total_count": 9,
|
||||
"page": 1,
|
||||
"page_siez": 3
|
||||
]
|
||||
}
|
||||
SUBSCRIPTIONS
|
||||
end
|
||||
|
@ -13,7 +13,7 @@ class OpenAPI::V1::UsersDoc < OpenAPI::V1::BaseDoc
|
||||
|
||||
doc_for :index do
|
||||
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 :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.'
|
||||
|
@ -17,6 +17,7 @@ import * as React from 'react';
|
||||
import { User } from '../../../models/user';
|
||||
import PrepaidPackAPI from '../../../api/prepaid-pack';
|
||||
import { PrepaidPack } from '../../../models/prepaid-pack';
|
||||
import { HtmlTranslate } from '../../base/html-translate';
|
||||
|
||||
interface PrepaidPacksPanelProps {
|
||||
user: User,
|
||||
@ -159,7 +160,10 @@ const PrepaidPacksPanel: React.FC<PrepaidPacksPanelProps> = ({ user, onError })
|
||||
onDecline={togglePacksModal}
|
||||
onSuccess={onPackBoughtSuccess} />}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
@ -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);
|
||||
}, []);
|
||||
|
||||
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
|
||||
*/
|
||||
@ -278,12 +291,14 @@ export const EventForm: React.FC<EventFormProps> = ({ action, event, onError, on
|
||||
type="number"
|
||||
tooltip={t('app.admin.event_form.seats_help')} />
|
||||
<FormInput register={register}
|
||||
id="amount"
|
||||
formState={formState}
|
||||
rules={{ required: true }}
|
||||
label={t('app.admin.event_form.standard_rate')}
|
||||
tooltip={t('app.admin.event_form.0_equal_free')}
|
||||
addOn={FormatLib.currencySymbol()} />
|
||||
type="number"
|
||||
id="amount"
|
||||
formState={formState}
|
||||
rules={{ required: true, min: 0 }}
|
||||
nullable
|
||||
label={t('app.admin.event_form.standard_rate')}
|
||||
tooltip={t('app.admin.event_form.0_equal_free')}
|
||||
addOn={FormatLib.currencySymbol()} />
|
||||
|
||||
{priceCategoriesOptions && <div className="additional-prices">
|
||||
{fields.map((price, index) => (
|
||||
@ -293,14 +308,16 @@ export const EventForm: React.FC<EventFormProps> = ({ action, event, onError, on
|
||||
id={`event_price_categories_attributes.${index}.price_category_id`}
|
||||
rules={{ required: true }}
|
||||
formState={formState}
|
||||
disabled={() => index < fields.length - 1}
|
||||
label={t('app.admin.event_form.fare_class')} />
|
||||
<FormInput id={`event_price_categories_attributes.${index}.amount`}
|
||||
register={register}
|
||||
type="number"
|
||||
rules={{ required: true }}
|
||||
formState={formState}
|
||||
label={t('app.admin.event_form.price')}
|
||||
addOn={FormatLib.currencySymbol()} />
|
||||
register={register}
|
||||
type="number"
|
||||
rules={{ required: true, min: 0 }}
|
||||
nullable
|
||||
formState={formState}
|
||||
label={t('app.admin.event_form.price')}
|
||||
addOn={FormatLib.currencySymbol()} />
|
||||
<FabButton className="remove-price is-main" onClick={() => handlePriceRemove(price, index)} icon={<Trash size={20} />} />
|
||||
</div>
|
||||
))}
|
||||
|
@ -136,7 +136,7 @@ export const FormMultiSelect = <TFieldValues extends FieldValues, TContext exten
|
||||
if (creatable) {
|
||||
Object.assign(selectProps, {
|
||||
formatCreateLabel,
|
||||
onCreateOption: inputValue => handleCreate(inputValue, value, rhfOnChange)
|
||||
onCreateOption: inputValue => handleCreate(inputValue, value || [], rhfOnChange)
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -66,7 +66,8 @@ export const FormSelect = <TFieldValues extends FieldValues, TContext extends ob
|
||||
placeholder={placeholder}
|
||||
isDisabled={isDisabled}
|
||||
isClearable={clearable}
|
||||
options={options} />
|
||||
options={options}
|
||||
isOptionDisabled={(option) => option.disabled}/>
|
||||
} />
|
||||
</AbstractFormItem>
|
||||
);
|
||||
|
@ -113,7 +113,7 @@ const StripeKeysForm: React.FC<StripeKeysFormProps> = ({ onValidKeys, onInvalidK
|
||||
}, reason => {
|
||||
if (!mounted.current) return;
|
||||
|
||||
if (reason.response.status === 401) {
|
||||
if (reason.response?.status === 401) {
|
||||
setSecretKeyAddOn(<i className="fa fa-times" />);
|
||||
setSecretKeyAddOnClassName('key-invalid');
|
||||
}
|
||||
|
@ -105,7 +105,7 @@ Application.Controllers.controller('AdminEventsController', ['$scope', '$state',
|
||||
{ selected: '' };
|
||||
|
||||
// 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
|
||||
|
@ -69,7 +69,7 @@ export interface Event {
|
||||
export interface EventDecoration {
|
||||
id?: number,
|
||||
name: string,
|
||||
related_to?: number
|
||||
related_to?: number // report the count of events related to the given decoration
|
||||
}
|
||||
|
||||
export type EventTheme = EventDecoration;
|
||||
|
@ -2,7 +2,7 @@
|
||||
* Option format, expected by 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
|
||||
|
@ -41,10 +41,10 @@ class AuthProvider < ApplicationRecord
|
||||
provider = find_by(status: 'active')
|
||||
return local if provider.nil?
|
||||
|
||||
return provider
|
||||
provider
|
||||
rescue ActiveRecord::StatementInvalid
|
||||
# we fall here on database creation because the table "active_providers" still does not exists at the moment
|
||||
return local
|
||||
local
|
||||
end
|
||||
end
|
||||
|
||||
@ -59,7 +59,7 @@ class AuthProvider < ApplicationRecord
|
||||
|
||||
parsed = /^([^-]+)-(.+)$/.match(strategy_name)
|
||||
ret = nil
|
||||
all.each do |strategy|
|
||||
all.find_each do |strategy|
|
||||
if strategy.provider_type == parsed[1] && strategy.name.downcase.parameterize == parsed[2]
|
||||
ret = strategy
|
||||
break
|
||||
@ -70,13 +70,13 @@ class AuthProvider < ApplicationRecord
|
||||
|
||||
## Return the name that should be registered in OmniAuth for the corresponding strategy
|
||||
def strategy_name
|
||||
provider_type + '-' + name.downcase.parameterize
|
||||
"#{provider_type}-#{name.downcase.parameterize}"
|
||||
end
|
||||
|
||||
## Return the provider type name without the "Provider" part.
|
||||
## eg. DatabaseProvider will return 'database'
|
||||
def provider_type
|
||||
providable_type[0..-9].downcase
|
||||
providable_type[0..-9]&.downcase
|
||||
end
|
||||
|
||||
## Return the user's profile fields that are currently managed from the SSO
|
||||
@ -84,7 +84,7 @@ class AuthProvider < ApplicationRecord
|
||||
def sso_fields
|
||||
fields = []
|
||||
auth_provider_mappings.each do |mapping|
|
||||
fields.push(mapping.local_model + '.' + mapping.local_field)
|
||||
fields.push("#{mapping.local_model}.#{mapping.local_field}")
|
||||
end
|
||||
fields
|
||||
end
|
||||
@ -96,10 +96,10 @@ class AuthProvider < ApplicationRecord
|
||||
end
|
||||
|
||||
def safe_destroy
|
||||
if status != 'active'
|
||||
destroy
|
||||
else
|
||||
if status == 'active'
|
||||
false
|
||||
else
|
||||
destroy
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -3,6 +3,5 @@
|
||||
# OAuth2Provider is a special type of AuthProvider which provides authentication through an external SSO server using
|
||||
# the oAuth 2.0 protocol.
|
||||
class OAuth2Provider < ApplicationRecord
|
||||
has_one :auth_provider, as: :providable
|
||||
|
||||
has_one :auth_provider, as: :providable, dependent: :destroy
|
||||
end
|
||||
|
@ -3,7 +3,7 @@
|
||||
# OpenIdConnectProvider is a special type of AuthProvider which provides authentication through an external SSO server using
|
||||
# the OpenID Connect protocol.
|
||||
class OpenIdConnectProvider < ApplicationRecord
|
||||
has_one :auth_provider, as: :providable
|
||||
has_one :auth_provider, as: :providable, dependent: :destroy
|
||||
|
||||
validates :issuer, presence: true
|
||||
validates :client__identifier, presence: true
|
||||
@ -28,8 +28,8 @@ class OpenIdConnectProvider < ApplicationRecord
|
||||
end
|
||||
|
||||
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)]
|
||||
end.to_h
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -10,7 +10,7 @@ json.is_completed slot.full?(reservable)
|
||||
json.backgroundColor 'white'
|
||||
|
||||
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.tags slot.availability.tags do |t|
|
||||
|
@ -7,7 +7,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<% 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">
|
||||
<%= hidden_field_tag :authenticity_token, @authentication_token %>
|
||||
<noscript>
|
||||
|
@ -1 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.extract! event, :id, :title, :description, :updated_at, :created_at
|
||||
|
@ -1,8 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.events @events do |event|
|
||||
json.partial! 'open_api/v1/events/event', event: event
|
||||
json.extract! event, :nb_total_places, :nb_free_places
|
||||
json.start_at event.availability.start_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
|
||||
json.event_image do
|
||||
json.large_url root_url.chomp('/') + event.event_image.attachment.large.url
|
||||
@ -23,4 +28,5 @@ json.events @events do |event|
|
||||
end
|
||||
end
|
||||
end
|
||||
json.url URI.join("#{ENV.fetch('DEFAULT_PROTOCOL')}://#{ENV.fetch('DEFAULT_HOST')}", "/#!/events/#{event.id}")
|
||||
end
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
json.invoices @invoices do |invoice|
|
||||
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
|
||||
json.payment_gateway_object do
|
||||
json.id invoice.payment_gateway_object.gateway_object_id
|
||||
|
@ -13,12 +13,18 @@ json.reservations @reservations do |reservation|
|
||||
end
|
||||
|
||||
json.reservable do
|
||||
if reservation.reservable_type == 'Training'
|
||||
case reservation.reservable_type
|
||||
when 'Training'
|
||||
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
|
||||
elsif reservation.reservable_type == 'Event'
|
||||
when 'Event'
|
||||
json.partial! 'open_api/v1/events/event', event: reservation.reservable
|
||||
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
|
||||
|
@ -1,10 +1,6 @@
|
||||
# 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.user_id subscription.statistic_profile.user_id
|
||||
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]
|
||||
|
@ -1 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.extract! training, :id, :name, :slug, :disabled, :updated_at, :created_at
|
||||
|
@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.trainings @trainings do |training|
|
||||
json.partial! 'open_api/v1/trainings/training', training: training
|
||||
json.extract! training, :nb_total_places, :description
|
||||
|
@ -12,6 +12,7 @@ Apipie.configure do |config|
|
||||
config.app_info['v1'] = <<-RDOC
|
||||
= 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).
|
||||
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*.
|
||||
|
@ -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."
|
||||
select_machine: "Select a machine"
|
||||
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
|
||||
members_show:
|
||||
members_list: "Mitgliederliste"
|
||||
|
@ -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."
|
||||
select_machine: "Select a machine"
|
||||
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
|
||||
members_show:
|
||||
members_list: "Members list"
|
||||
|
@ -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."
|
||||
select_machine: "Select a machine"
|
||||
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
|
||||
members_show:
|
||||
members_list: "Lista de miembros"
|
||||
|
@ -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."
|
||||
select_machine: "Sélectionnez une machine"
|
||||
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
|
||||
members_show:
|
||||
members_list: "Liste des membres"
|
||||
|
@ -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."
|
||||
select_machine: "Select a machine"
|
||||
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
|
||||
members_show:
|
||||
members_list: "Medlemsliste"
|
||||
|
@ -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."
|
||||
select_machine: "Select a machine"
|
||||
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
|
||||
members_show:
|
||||
members_list: "Lista de membros"
|
||||
|
@ -170,6 +170,8 @@ zu:
|
||||
cta_info: "crwdns37065:0crwdne37065:0"
|
||||
select_machine: "crwdns37067:0crwdne37067: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
|
||||
members_show:
|
||||
members_list: "crwdns27656:0crwdne27656:0"
|
||||
|
12
db/migrate/20230302120458_add_uniqueness_constraints.rb
Normal file
12
db/migrate/20230302120458_add_uniqueness_constraints.rb
Normal 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
|
@ -10,7 +10,7 @@
|
||||
#
|
||||
# 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
|
||||
enable_extension "fuzzystrmatch"
|
||||
@ -122,6 +122,7 @@ ActiveRecord::Schema.define(version: 2023_02_13_134954) do
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "providable_type"
|
||||
t.integer "providable_id"
|
||||
t.index ["name"], name: "index_auth_providers_on_name", unique: true
|
||||
end
|
||||
|
||||
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|
|
||||
t.integer "booked"
|
||||
t.bigint "event_price_category_id"
|
||||
t.bigint "cart_item_event_reservation_id"
|
||||
t.datetime "created_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 ["event_price_category_id"], name: "index_cart_item_tickets_on_event_price_category"
|
||||
end
|
||||
@ -287,6 +288,7 @@ ActiveRecord::Schema.define(version: 2023_02_13_134954) do
|
||||
t.integer "hours"
|
||||
t.datetime "created_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"
|
||||
end
|
||||
|
||||
@ -787,6 +789,8 @@ ActiveRecord::Schema.define(version: 2023_02_13_134954) do
|
||||
t.text "conditions"
|
||||
t.datetime "created_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
|
||||
|
||||
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.integer "duration", default: 60
|
||||
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 ["priceable_type", "priceable_id"], name: "index_prices_on_priceable_type_and_priceable_id"
|
||||
end
|
||||
|
@ -15,6 +15,8 @@ STRIPE_PUBLISHABLE_KEY=
|
||||
# oAuth SSO keys for tests
|
||||
OAUTH_CLIENT_ID=github-oauth-app-id
|
||||
OAUTH_CLIENT_SECRET=github-oauth-app-secret
|
||||
OIDC_CLIENT_ID=oidc-client-id
|
||||
OIDC_CLIENT_SECRET=oidc-client-secret
|
||||
|
||||
# Configure carefully!
|
||||
DEFAULT_HOST=localhost:5000
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"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.",
|
||||
"keywords": [
|
||||
"fablab",
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/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`.
|
||||
|
||||
stripe_public_key=$(RAILS_ENV='test' bin/rails runner "puts ENV['STRIPE_PUBLISHABLE_KEY']")
|
||||
|
@ -49,10 +49,10 @@ ALLOW_INSECURE_HTTP=false
|
||||
ENABLE_SENTRY=false
|
||||
|
||||
# 5242880 = 5 megabytes
|
||||
MAX_IMPORT_SIZE='5242880'
|
||||
MAX_IMPORT_SIZE=5242880
|
||||
# 10485760 = 10 megabytes
|
||||
MAX_IMAGE_SIZE='10485760'
|
||||
MAX_IMAGE_SIZE=10485760
|
||||
# 20971520 = 20 megabytes
|
||||
MAX_CAO_SIZE='20971520'
|
||||
MAX_CAO_SIZE=20971520
|
||||
# 5242880 = 5 megabytes
|
||||
MAX_SUPPORTING_DOCUMENT_FILE_SIZE='5242880'
|
||||
MAX_SUPPORTING_DOCUMENT_FILE_SIZE=5242880
|
||||
|
4
test/fixtures/history_values.yml
vendored
4
test/fixtures/history_values.yml
vendored
@ -648,7 +648,7 @@ history_value_66:
|
||||
history_value_67:
|
||||
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'
|
||||
updated_at: '2021-05-31 15:00:37.210049'
|
||||
footprint: 4984215605d9f30ac4f9594bc0d552d6b5e280f650801399b698aa43188001a5
|
||||
@ -657,7 +657,7 @@ history_value_67:
|
||||
history_value_68:
|
||||
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'
|
||||
updated_at: '2021-05-31 15:00:37.280668'
|
||||
footprint: 48db504877d3329e39d1e816b243629c44b47be9f2837e2e4af4f30ca7cbd3e8
|
||||
|
8
test/frontend/__fixtures__/age_ranges.ts
Normal file
8
test/frontend/__fixtures__/age_ranges.ts
Normal 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;
|
8
test/frontend/__fixtures__/event_categories.ts
Normal file
8
test/frontend/__fixtures__/event_categories.ts
Normal 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;
|
8
test/frontend/__fixtures__/event_price_categories.ts
Normal file
8
test/frontend/__fixtures__/event_price_categories.ts
Normal 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;
|
8
test/frontend/__fixtures__/event_themes.ts
Normal file
8
test/frontend/__fixtures__/event_themes.ts
Normal 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;
|
@ -13,6 +13,10 @@ import spaces from '../__fixtures__/spaces';
|
||||
import statuses from '../__fixtures__/statuses';
|
||||
import notificationTypes from '../__fixtures__/notification_types';
|
||||
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 = {
|
||||
init: () => {
|
||||
@ -33,7 +37,11 @@ const FixturesLib = {
|
||||
spaces: JSON.parse(JSON.stringify(spaces)),
|
||||
statuses: JSON.parse(JSON.stringify(statuses)),
|
||||
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))
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -113,10 +113,22 @@ export const server = setupServer(
|
||||
return res(ctx.json({ status }));
|
||||
}),
|
||||
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) => {
|
||||
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));
|
||||
})
|
||||
);
|
||||
|
||||
|
68
test/frontend/components/events/event-form.test.tsx
Normal file
68
test/frontend/components/events/event-form.test.tsx
Normal 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');
|
||||
});
|
||||
});
|
@ -32,4 +32,32 @@ module AuthProviderHelper
|
||||
]
|
||||
}
|
||||
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
|
||||
|
@ -9,12 +9,27 @@ class OpenApi::EventsTest < ActionDispatch::IntegrationTest
|
||||
@token = OpenAPI::Client.find_by(name: 'minitest').token
|
||||
end
|
||||
|
||||
test 'list all events' do
|
||||
test 'list events' do
|
||||
get '/open_api/v1/events', headers: open_api_headers(@token)
|
||||
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
|
||||
|
||||
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)
|
||||
assert_response :success
|
||||
end
|
||||
@ -29,7 +44,7 @@ class OpenApi::EventsTest < ActionDispatch::IntegrationTest
|
||||
assert_response :success
|
||||
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)
|
||||
assert_response :success
|
||||
end
|
||||
|
@ -9,22 +9,25 @@ class OpenApi::InvoicesTest < ActionDispatch::IntegrationTest
|
||||
@token = OpenAPI::Client.find_by(name: 'minitest').token
|
||||
end
|
||||
|
||||
test 'list all invoices' do
|
||||
test 'list invoices' do
|
||||
get '/open_api/v1/invoices', headers: open_api_headers(@token)
|
||||
assert_response :success
|
||||
assert_equal Mime[:json], response.content_type
|
||||
|
||||
assert_not_empty json_response(response.body)[:invoices]
|
||||
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)
|
||||
assert_response :success
|
||||
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)
|
||||
assert_response :success
|
||||
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)
|
||||
assert_response :success
|
||||
end
|
||||
@ -32,6 +35,6 @@ class OpenApi::InvoicesTest < ActionDispatch::IntegrationTest
|
||||
test 'download an invoice' do
|
||||
get '/open_api/v1/invoices/3/download', headers: open_api_headers(@token)
|
||||
assert_response :success
|
||||
assert_match /^inline; filename=/, response.headers['Content-Disposition']
|
||||
assert_match(/^inline; filename=/, response.headers['Content-Disposition'])
|
||||
end
|
||||
end
|
||||
|
@ -9,12 +9,12 @@ class OpenApi::ReservationsTest < ActionDispatch::IntegrationTest
|
||||
@token = OpenAPI::Client.find_by(name: 'minitest').token
|
||||
end
|
||||
|
||||
test 'list all reservations' do
|
||||
test 'list reservations ' do
|
||||
get '/open_api/v1/reservations', headers: open_api_headers(@token)
|
||||
assert_response :success
|
||||
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
|
||||
|
||||
test 'list all reservations with pagination' do
|
||||
|
@ -9,12 +9,15 @@ class OpenApi::SubscriptionsTest < ActionDispatch::IntegrationTest
|
||||
@token = OpenAPI::Client.find_by(name: 'minitest').token
|
||||
end
|
||||
|
||||
test 'list all subscriptions' do
|
||||
test 'list subscriptions' do
|
||||
get '/open_api/v1/subscriptions', headers: open_api_headers(@token)
|
||||
assert_response :success
|
||||
assert_equal Mime[:json], response.content_type
|
||||
|
||||
assert_not_empty json_response(response.body)[:subscriptions]
|
||||
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)
|
||||
assert_response :success
|
||||
end
|
||||
|
59
test/integration/open_id_connect_test.rb
Normal file
59
test/integration/open_id_connect_test.rb
Normal 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
|
@ -19,7 +19,11 @@ VCR.configure do |config|
|
||||
config.hook_into :webmock
|
||||
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.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
|
||||
|
||||
Sidekiq::Testing.fake!
|
||||
@ -43,7 +47,7 @@ class ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
def upload_headers
|
||||
{ 'Accept' => Mime[:json], 'Content-Type' => 'multipart/form-data' }
|
||||
{ 'Accept' => Mime[:json], 'Content-Type' => Mime[:multipart_form].to_s }
|
||||
end
|
||||
|
||||
def open_api_headers(token)
|
||||
|
Loading…
x
Reference in New Issue
Block a user