mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-11-29 10:24:20 +01:00
Merge branch 'dev' for release 5.6.6
This commit is contained in:
commit
2afdb2824a
15
CHANGELOG.md
15
CHANGELOG.md
@ -1,5 +1,18 @@
|
||||
# Changelog Fab-manager
|
||||
|
||||
## v5.6.6 2023 January 23
|
||||
|
||||
- Add more context data to sentry reports
|
||||
- Improved SSO testing
|
||||
- Ability to map the external ID from the SSO
|
||||
- Ability to soft-destroy a reserved event
|
||||
- Fix a bug: unable to run task fix_invoice_item when some invoice items are associated with errors
|
||||
- Fix a bug: invalid event date reported when the timezone in before UTC
|
||||
- Fix a bug: unable to run accounting export if a line label was not defined
|
||||
- Fix a security issue: updated rack to 2.2.6.2 to fix [CVE-2022-44571](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-44571)
|
||||
- Fix a security issue: updated globalid to 1.0.1 to fix [CVE-2023-22799](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-22799)
|
||||
- [TODO DEPLOY] `rails fablab:fix:invoice_items_in_error` THEN `rails fablab:fix_invoice_items` THEN `rails db:migrate`
|
||||
|
||||
## v5.6.5 2023 January 9
|
||||
|
||||
- Moved the buttons to create a new machine or availability to the admin section
|
||||
@ -24,7 +37,7 @@
|
||||
- Fix a bug: cryptic error message when failed to create a manager
|
||||
- Fix a bug: unable to restore accounting periods closed by a deleted admin
|
||||
- Fix a bug: unable to build an accounting archive if the operator was deleted
|
||||
- Fix a bug: unable to udpate an event category
|
||||
- Fix a bug: unable to update an event category
|
||||
|
||||
## v5.6.1 2023 January 6
|
||||
|
||||
|
@ -160,7 +160,7 @@ GEM
|
||||
fugit (1.5.3)
|
||||
et-orbi (~> 1, >= 1.2.7)
|
||||
raabro (~> 1.4)
|
||||
globalid (1.0.0)
|
||||
globalid (1.0.1)
|
||||
activesupport (>= 5.0)
|
||||
hashdiff (1.0.1)
|
||||
hashery (2.1.2)
|
||||
@ -222,7 +222,7 @@ GEM
|
||||
mini_magick (4.10.1)
|
||||
mini_mime (1.1.2)
|
||||
mini_portile2 (2.8.0)
|
||||
minitest (5.16.2)
|
||||
minitest (5.17.0)
|
||||
minitest-reporters (1.4.2)
|
||||
ansi
|
||||
builder
|
||||
@ -303,7 +303,7 @@ GEM
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.6.1)
|
||||
rack (2.2.4)
|
||||
rack (2.2.6.2)
|
||||
rack-oauth2 (1.19.0)
|
||||
activesupport
|
||||
attr_required
|
||||
|
@ -3,7 +3,6 @@
|
||||
# API Controller for resources of type AuthProvider
|
||||
# AuthProvider are used to connect users through single-sign on systems
|
||||
class API::AuthProvidersController < API::ApiController
|
||||
|
||||
before_action :set_provider, only: %i[show update destroy]
|
||||
def index
|
||||
@providers = policy_scope(AuthProvider)
|
||||
@ -64,13 +63,13 @@ class API::AuthProvidersController < API::ApiController
|
||||
user = User.find_by('lower(email) = ?', params[:email]&.downcase)
|
||||
|
||||
if user&.auth_token
|
||||
if AuthProvider.active.providable_type != DatabaseProvider.name
|
||||
if AuthProvider.active.providable_type == DatabaseProvider.name
|
||||
render json: { status: 'error', error: I18n.t('members.current_authentication_method_no_code') }, status: :bad_request
|
||||
else
|
||||
NotificationCenter.call type: 'notify_user_auth_migration',
|
||||
receiver: user,
|
||||
attached_object: user
|
||||
render json: { status: 'processing' }, status: :ok
|
||||
else
|
||||
render json: { status: 'error', error: I18n.t('members.current_authentication_method_no_code') }, status: :bad_request
|
||||
end
|
||||
else
|
||||
render json: { status: 'error', error: I18n.t('members.requested_account_does_not_exists') }, status: :bad_request
|
||||
@ -92,18 +91,18 @@ class API::AuthProvidersController < API::ApiController
|
||||
providable_attributes: %i[id base_url token_endpoint authorization_endpoint
|
||||
profile_url client_id client_secret scopes],
|
||||
auth_provider_mappings_attributes: [:id, :local_model, :local_field, :api_field, :api_endpoint, :api_data_type,
|
||||
:_destroy, transformation: [:type, :format, :true_value, :false_value,
|
||||
mapping: %i[from to]]])
|
||||
:_destroy, { transformation: [:type, :format, :true_value, :false_value,
|
||||
{ mapping: %i[from to] }] }])
|
||||
elsif params['auth_provider']['providable_type'] == OpenIdConnectProvider.name
|
||||
params.require(:auth_provider)
|
||||
.permit(:id, :name, :providable_type,
|
||||
providable_attributes: [:id, :issuer, :discovery, :client_auth_method, :prompt, :send_scope_to_token_endpoint,
|
||||
:client__identifier, :client__secret, :client__authorization_endpoint, :client__token_endpoint,
|
||||
:client__userinfo_endpoint, :client__jwks_uri, :client__end_session_endpoint, :profile_url,
|
||||
scope: []],
|
||||
{ scope: [] }],
|
||||
auth_provider_mappings_attributes: [:id, :local_model, :local_field, :api_field, :api_endpoint, :api_data_type,
|
||||
:_destroy, transformation: [:type, :format, :true_value, :false_value,
|
||||
mapping: %i[from to]]])
|
||||
:_destroy, { transformation: [:type, :format, :true_value, :false_value,
|
||||
{ mapping: %i[from to] }] }])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -36,7 +36,8 @@ class API::EventsController < API::ApiController
|
||||
limit = params[:limit]
|
||||
@events = Event.includes(:event_image, :event_files, :availability, :category)
|
||||
.where('events.nb_total_places != -1 OR events.nb_total_places IS NULL')
|
||||
.order('availabilities.start_at ASC').references(:availabilities)
|
||||
.where(deleted_at: nil)
|
||||
.order('availabilities.start_at').references(:availabilities)
|
||||
.limit(limit)
|
||||
|
||||
@events = case Setting.get('upcoming_events_shown')
|
||||
@ -49,7 +50,9 @@ class API::EventsController < API::ApiController
|
||||
end
|
||||
end
|
||||
|
||||
def show; end
|
||||
def show
|
||||
head :not_found if @event.deleted_at
|
||||
end
|
||||
|
||||
def create
|
||||
authorize Event
|
||||
|
@ -8,6 +8,7 @@ class OpenAPI::V1::EventsController < OpenAPI::V1::BaseController
|
||||
|
||||
def index
|
||||
@events = Event.includes(:event_image, :event_files, :availability, :category)
|
||||
.where(deleted_at: nil)
|
||||
@events = if upcoming
|
||||
@events.references(:availabilities)
|
||||
.where('availabilities.end_at >= ?', DateTime.current)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import moment, { unitOfTime } from 'moment';
|
||||
import { IFablab } from '../models/fablab';
|
||||
import { TDateISO, TDateISODate, THours, TMinutes } from '../typings/date-iso';
|
||||
import { TDateISO, TDateISODate, TDateISOShortTime } from '../typings/date-iso';
|
||||
|
||||
declare let Fablab: IFablab;
|
||||
|
||||
@ -17,7 +17,15 @@ export default class FormatLib {
|
||||
*/
|
||||
static isDateISO = (value: string): boolean => {
|
||||
if (typeof value !== 'string') return false;
|
||||
return !!value?.match(/^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d.\d\d\d/);
|
||||
return !!value?.match(/^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d/);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the provided variable is string representing a short date, according to ISO 8601 (e.g. 2023-01-12)
|
||||
*/
|
||||
static isShortDateISO = (value: string): boolean => {
|
||||
if (typeof value !== 'string') return false;
|
||||
return !!value.match(/^\d\d\d\d-\d\d-\d\d$/);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -32,7 +40,36 @@ export default class FormatLib {
|
||||
* Return the formatted localized date for the given date
|
||||
*/
|
||||
static date = (date: Date|TDateISO|TDateISODate): string => {
|
||||
return Intl.DateTimeFormat().format(moment(date).toDate());
|
||||
let tempDate: Date;
|
||||
if (FormatLib.isShortDateISO(date as string) || FormatLib.isDateISO(date as string)) {
|
||||
tempDate = FormatLib.parseISOdate(date as TDateISO);
|
||||
} else {
|
||||
tempDate = moment(date).toDate();
|
||||
}
|
||||
return Intl.DateTimeFormat(Fablab.intl_locale).format(tempDate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse the provided datetime or date string (as ISO8601 format) and return the equivalent Date object
|
||||
*/
|
||||
private static parseISOdate = (date: TDateISO|TDateISODate, res: Date = new Date()): Date => {
|
||||
const isoDateMatch = (date as string)?.match(/^(\d\d\d\d)-(\d\d)-(\d\d)/);
|
||||
res.setFullYear(parseInt(isoDateMatch[1], 10));
|
||||
res.setMonth(parseInt(isoDateMatch[2], 10) - 1);
|
||||
res.setDate(parseInt(isoDateMatch[3], 10));
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse the provided datetime or time string (as ISO8601 format) and return the equivalent Date object
|
||||
*/
|
||||
private static parseISOtime = (date: TDateISO|TDateISOShortTime, res: Date = new Date()): Date => {
|
||||
const isoTimeMatch = (date as string)?.match(/(^|T)(\d\d):(\d\d)/);
|
||||
res.setHours(parseInt(isoTimeMatch[2], 10));
|
||||
res.setMinutes(parseInt(isoTimeMatch[3], 10));
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -45,13 +82,10 @@ export default class FormatLib {
|
||||
/**
|
||||
* Return the formatted localized time for the given date
|
||||
*/
|
||||
static time = (date: Date|TDateISO|`${THours}:${TMinutes}`): string => {
|
||||
static time = (date: Date|TDateISO|TDateISOShortTime): string => {
|
||||
let tempDate: Date;
|
||||
if (FormatLib.isShortTimeISO(date as string)) {
|
||||
const isoTimeMatch = (date as string)?.match(/^(\d\d):(\d\d)$/);
|
||||
tempDate = new Date();
|
||||
tempDate.setHours(parseInt(isoTimeMatch[1], 10));
|
||||
tempDate.setMinutes(parseInt(isoTimeMatch[2], 10));
|
||||
if (FormatLib.isShortTimeISO(date as string) || FormatLib.isDateISO(date as string)) {
|
||||
tempDate = FormatLib.parseISOtime(date as TDateISOShortTime);
|
||||
} else {
|
||||
tempDate = moment(date).toDate();
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
// from https://gist.github.com/MrChocolatine/367fb2a35d02f6175cc8ccb3d3a20054
|
||||
// inspired from https://gist.github.com/MrChocolatine/367fb2a35d02f6175cc8ccb3d3a20054
|
||||
|
||||
type TYear = `${number}${number}${number}${number}`;
|
||||
type TMonth = `${number}${number}`;
|
||||
@ -13,10 +13,15 @@ type TMilliseconds = `${number}${number}${number}`;
|
||||
*/
|
||||
type TDateISODate = `${TYear}-${TMonth}-${TDay}`;
|
||||
|
||||
/**
|
||||
* Represent a string like `14:42`
|
||||
*/
|
||||
type TDateISOShortTime = `${THours}:${TMinutes}`;
|
||||
|
||||
/**
|
||||
* Represent a string like `14:42:34.678`
|
||||
*/
|
||||
type TDateISOTime = `${THours}:${TMinutes}:${TSeconds}`|`${THours}:${TMinutes}:${TSeconds}.${TMilliseconds}`;
|
||||
type TDateISOTime = `${TDateISOShortTime}:${TSeconds}`|`${TDateISOShortTime}:${TSeconds}.${TMilliseconds}`;
|
||||
|
||||
/**
|
||||
* Represent a timezone like `+0100`
|
||||
|
@ -53,13 +53,12 @@ class Event < ApplicationRecord
|
||||
.references(:availabilities)
|
||||
end
|
||||
|
||||
def safe_destroy
|
||||
reservations = Reservation.where(reservable_type: 'Event', reservable_id: id)
|
||||
if reservations.size.zero?
|
||||
destroy
|
||||
else
|
||||
false
|
||||
end
|
||||
def destroyable?
|
||||
Reservation.where(reservable_type: 'Event', reservable_id: id).count.zero?
|
||||
end
|
||||
|
||||
def soft_destroy!
|
||||
update(deleted_at: DateTime.current)
|
||||
end
|
||||
|
||||
##
|
||||
|
@ -28,7 +28,7 @@ class Profile < ApplicationRecord
|
||||
blacklist = %w[id user_id created_at updated_at]
|
||||
# model-relationships must be added manually
|
||||
additional = [%w[avatar string], %w[address string], %w[organization_name string], %w[organization_address string],
|
||||
%w[gender boolean], %w[birthday date]]
|
||||
%w[gender boolean], %w[birthday date], %w[external_id string]]
|
||||
Profile.columns_hash
|
||||
.map { |k, v| [k, v.type.to_s] }
|
||||
.delete_if { |col| blacklist.include?(col[0]) }
|
||||
|
@ -6,12 +6,16 @@ class EventPolicy < ApplicationPolicy
|
||||
class Scope < Scope
|
||||
def resolve
|
||||
if user.nil? || (user && !user.admin? && !user.manager?)
|
||||
scope.includes(:event_image, :event_files, :availability, :category, :event_price_categories, :age_range, :events_event_themes, :event_themes)
|
||||
scope.includes(:event_image, :event_files, :availability, :category, :event_price_categories, :age_range, :events_event_themes,
|
||||
:event_themes)
|
||||
.where('availabilities.start_at >= ?', DateTime.current)
|
||||
.where(deleted_at: nil)
|
||||
.order('availabilities.start_at ASC')
|
||||
.references(:availabilities)
|
||||
else
|
||||
scope.includes(:event_image, :event_files, :availability, :category, :event_price_categories, :age_range, :events_event_themes, :event_themes)
|
||||
scope.includes(:event_image, :event_files, :availability, :category, :event_price_categories, :age_range, :events_event_themes,
|
||||
:event_themes)
|
||||
.where(deleted_at: nil)
|
||||
.references(:availabilities)
|
||||
end
|
||||
end
|
||||
|
@ -62,9 +62,9 @@ class Accounting::AccountingExportService
|
||||
when 'date'
|
||||
row << line.date&.strftime(date_format)
|
||||
when 'account_code'
|
||||
row << line.account_code
|
||||
row << line.account_code.to_s
|
||||
when 'account_label'
|
||||
row << line.account_label
|
||||
row << line.account_label.to_s
|
||||
when 'piece'
|
||||
row << line.invoice.reference
|
||||
when 'line_label'
|
||||
|
@ -70,8 +70,9 @@ class EventService
|
||||
end
|
||||
|
||||
events.each do |e|
|
||||
# we use double negation because safe_destroy can return either a boolean (false) or an Availability (in case of delete success)
|
||||
results.push status: !!e.safe_destroy, event: e # rubocop:disable Style/DoubleNegation
|
||||
method = e.destroyable? ? :destroy : :soft_destroy!
|
||||
# we use double negation because destroy can return either a boolean (false) or an Event (in case of delete success)
|
||||
results.push status: !!e.send(method), event: e # rubocop:disable Style/DoubleNegation
|
||||
end
|
||||
results
|
||||
end
|
||||
|
@ -40,6 +40,11 @@ class UserSetterService
|
||||
@user.statistic_profile.birthday = data
|
||||
end
|
||||
|
||||
def assign_external_id(data)
|
||||
@user.invoicing_profile ||= InvoicingProfile.new
|
||||
@user.invoicing_profile.external_id = data
|
||||
end
|
||||
|
||||
def assign_profile_attribute(attribute, data)
|
||||
@user.profile[attribute[8..].to_sym] = data
|
||||
end
|
||||
@ -65,6 +70,8 @@ class UserSetterService
|
||||
assign_gender(data)
|
||||
when 'profile.birthday'
|
||||
assign_birthday(data)
|
||||
when 'profile.external_id'
|
||||
assign_external_id(data)
|
||||
else
|
||||
assign_profile_attribute(attribute, data)
|
||||
end
|
||||
|
@ -1,5 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'version'
|
||||
|
||||
Sentry.init do |config|
|
||||
config.excluded_exceptions += ['Pundit::NotAuthorizedError']
|
||||
|
||||
@ -20,6 +22,11 @@ Sentry.init do |config|
|
||||
# Set traces_sample_rate to 1.0 to capture 100%
|
||||
# of transactions for performance monitoring.
|
||||
# We recommend adjusting this value in production.
|
||||
config.traces_sample_rate = 0.01
|
||||
config.traces_sample_rate = 0.1
|
||||
config.environment = Rails.env
|
||||
config.release = Version.current
|
||||
end
|
||||
|
||||
Sentry.configure_scope do |scope|
|
||||
scope.set_tags(instance: ENV.fetch('DEFAULT_HOST'))
|
||||
end
|
||||
|
12
db/migrate/20230119143245_add_deleted_at_to_event.rb
Normal file
12
db/migrate/20230119143245_add_deleted_at_to_event.rb
Normal file
@ -0,0 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Allow soft destroy of events.
|
||||
# Events with existing reservation cannot be destroyed because we need them for rebuilding invoices, statistics, etc.
|
||||
# This attribute allows to make a "soft destroy" of an Event, marking it as destroyed so it doesn't appear anymore in
|
||||
# the interface (as if it was destroyed) but still lives in the database so we can use it to build data.
|
||||
class AddDeletedAtToEvent < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_column :events, :deleted_at, :datetime
|
||||
add_index :events, :deleted_at
|
||||
end
|
||||
end
|
22
db/schema.rb
22
db/schema.rb
@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2023_01_06_081943) do
|
||||
ActiveRecord::Schema.define(version: 2023_01_19_143245) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "fuzzystrmatch"
|
||||
@ -19,8 +19,8 @@ ActiveRecord::Schema.define(version: 2023_01_06_081943) do
|
||||
enable_extension "unaccent"
|
||||
|
||||
create_table "abuses", id: :serial, force: :cascade do |t|
|
||||
t.integer "signaled_id"
|
||||
t.string "signaled_type"
|
||||
t.integer "signaled_id"
|
||||
t.string "first_name"
|
||||
t.string "last_name"
|
||||
t.string "email"
|
||||
@ -68,8 +68,8 @@ ActiveRecord::Schema.define(version: 2023_01_06_081943) do
|
||||
t.string "locality"
|
||||
t.string "country"
|
||||
t.string "postal_code"
|
||||
t.integer "placeable_id"
|
||||
t.string "placeable_type"
|
||||
t.integer "placeable_id"
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
end
|
||||
@ -93,8 +93,8 @@ ActiveRecord::Schema.define(version: 2023_01_06_081943) do
|
||||
end
|
||||
|
||||
create_table "assets", id: :serial, force: :cascade do |t|
|
||||
t.integer "viewable_id"
|
||||
t.string "viewable_type"
|
||||
t.integer "viewable_id"
|
||||
t.string "attachment"
|
||||
t.string "type"
|
||||
t.datetime "created_at"
|
||||
@ -176,8 +176,8 @@ ActiveRecord::Schema.define(version: 2023_01_06_081943) do
|
||||
end
|
||||
|
||||
create_table "credits", id: :serial, force: :cascade do |t|
|
||||
t.integer "creditable_id"
|
||||
t.string "creditable_type"
|
||||
t.integer "creditable_id"
|
||||
t.integer "plan_id"
|
||||
t.integer "hours"
|
||||
t.datetime "created_at"
|
||||
@ -226,8 +226,10 @@ ActiveRecord::Schema.define(version: 2023_01_06_081943) do
|
||||
t.integer "recurrence_id"
|
||||
t.integer "age_range_id"
|
||||
t.integer "category_id"
|
||||
t.datetime "deleted_at"
|
||||
t.index ["availability_id"], name: "index_events_on_availability_id"
|
||||
t.index ["category_id"], name: "index_events_on_category_id"
|
||||
t.index ["deleted_at"], name: "index_events_on_deleted_at"
|
||||
t.index ["recurrence_id"], name: "index_events_on_recurrence_id"
|
||||
end
|
||||
|
||||
@ -417,15 +419,15 @@ ActiveRecord::Schema.define(version: 2023_01_06_081943) do
|
||||
|
||||
create_table "notifications", id: :serial, force: :cascade do |t|
|
||||
t.integer "receiver_id"
|
||||
t.integer "attached_object_id"
|
||||
t.string "attached_object_type"
|
||||
t.integer "attached_object_id"
|
||||
t.integer "notification_type_id"
|
||||
t.boolean "is_read", default: false
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.string "receiver_type"
|
||||
t.boolean "is_send", default: false
|
||||
t.jsonb "meta_data", default: {}
|
||||
t.jsonb "meta_data", default: "{}"
|
||||
t.index ["notification_type_id"], name: "index_notifications_on_notification_type_id"
|
||||
t.index ["receiver_id"], name: "index_notifications_on_receiver_id"
|
||||
end
|
||||
@ -665,8 +667,8 @@ ActiveRecord::Schema.define(version: 2023_01_06_081943) do
|
||||
create_table "prices", id: :serial, force: :cascade do |t|
|
||||
t.integer "group_id"
|
||||
t.integer "plan_id"
|
||||
t.integer "priceable_id"
|
||||
t.string "priceable_type"
|
||||
t.integer "priceable_id"
|
||||
t.integer "amount"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
@ -867,8 +869,8 @@ ActiveRecord::Schema.define(version: 2023_01_06_081943) do
|
||||
t.text "message"
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.integer "reservable_id"
|
||||
t.string "reservable_type"
|
||||
t.integer "reservable_id"
|
||||
t.integer "nb_reserve_places"
|
||||
t.integer "statistic_profile_id"
|
||||
t.index ["reservable_type", "reservable_id"], name: "index_reservations_on_reservable_type_and_reservable_id"
|
||||
@ -877,8 +879,8 @@ ActiveRecord::Schema.define(version: 2023_01_06_081943) do
|
||||
|
||||
create_table "roles", id: :serial, force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.integer "resource_id"
|
||||
t.string "resource_type"
|
||||
t.integer "resource_id"
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id"
|
||||
|
@ -177,7 +177,7 @@ namespace :fablab do
|
||||
|
||||
desc '[release 4.4.3] fix duration of recurring availabilities'
|
||||
task availabilities_duration: :environment do
|
||||
Availability.select(:occurrence_id).where(is_recurrent: true).group(:occurrence_id).each do |a|
|
||||
Availability.select('occurrence_id').where(is_recurrent: true).group('occurrence_id').each do |a|
|
||||
occurrences = Availability.where(occurrence_id: a.occurrence_id)
|
||||
next unless occurrences.map(&:slot_duration).uniq.size > 1
|
||||
|
||||
@ -231,7 +231,7 @@ namespace :fablab do
|
||||
statistic_profile_ids = StatisticProfilePrepaidPack.all.map(&:statistic_profile_id).uniq
|
||||
# find the reservations that use prepaid pack by machine_ids, members and preriod
|
||||
reservations = Reservation.where(reservable_type: 'Machine', reservable_id: machine_ids, statistic_profile_id: statistic_profile_ids,
|
||||
created_at: start_date..end_date).order(statistic_profile_id: 'ASC', created_at: 'ASC')
|
||||
created_at: start_date..end_date).order(statistic_profile_id: :asc, created_at: :asc)
|
||||
infos = []
|
||||
reservations.each do |reservation|
|
||||
# find pack by pack's created_at before reservation's create_at and pack's expries_at before start_date
|
||||
@ -243,7 +243,7 @@ namespace :fablab do
|
||||
.where(statistic_profile_id: reservation.statistic_profile_id)
|
||||
.where('statistic_profile_prepaid_packs.created_at <= ?', reservation.created_at)
|
||||
.where('expires_at is NULL or expires_at > ?', start_date)
|
||||
.order(created_at: 'ASC')
|
||||
.order(created_at: :asc)
|
||||
|
||||
# passe reservation if cannot find any pack
|
||||
next if packs.empty?
|
||||
@ -279,5 +279,15 @@ namespace :fablab do
|
||||
puts i
|
||||
end
|
||||
end
|
||||
|
||||
desc '[release 5.6.6] fix invoice items in error'
|
||||
task invoice_items_in_error: :environment do
|
||||
next if InvoiceItem.where(object_type: 'Error').count.zero?
|
||||
|
||||
InvoiceItem.where(object_type: 'Error').update_all(object_id: 0) # rubocop:disable Rails/SkipsModelValidations
|
||||
|
||||
Fablab::Application.load_tasks if Rake::Task.tasks.empty?
|
||||
Rake::Task['fablab:chain:invoices_items'].invoke
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -36,13 +36,13 @@ namespace :fablab do
|
||||
puts "Operator: #{invoice.operator_profile&.user&.profile&.full_name} (#{invoice.operator_profile&.user&.email})"
|
||||
puts "Date: #{invoice.created_at}"
|
||||
puts '=============================================='
|
||||
puts "Concerned item: #{ii.id}"
|
||||
puts "Concerned item: #{ii.id} (#{ii.object_type} #{ii.object_id})"
|
||||
puts "Item subject: #{ii.description}."
|
||||
other_items.find_each do |oii|
|
||||
puts '=============================================='
|
||||
puts "Other item: #{oii.description} (#{oii.id})"
|
||||
puts "Other item object: #{oii.object_type} #{oii.object_id}"
|
||||
puts "Other item slots: #{oii.object.try(:slots)&.map { |s| "#{s.start_at} - #{s.end_at}" }}"
|
||||
puts "Other item slots: #{oii.object.try(:slots)&.map { |s| "#{s.start_at} - #{s.end_at}" }}" if oii.object_type == 'Reservation'
|
||||
print "\e[1;34m[ ? ]\e[0m Associate the item with #{oii.object_type} #{oii.object_id} ? (y/N) > "
|
||||
confirm = $stdin.gets.chomp
|
||||
if confirm == 'y'
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fab-manager",
|
||||
"version": "5.6.5",
|
||||
"version": "5.6.6",
|
||||
"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",
|
||||
|
36
test/frontend/lib/format.test.ts
Normal file
36
test/frontend/lib/format.test.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import FormatLib from 'lib/format';
|
||||
import { IFablab } from 'models/fablab';
|
||||
|
||||
declare const Fablab: IFablab;
|
||||
describe('FormatLib', () => {
|
||||
test('format a date', () => {
|
||||
Fablab.intl_locale = 'fr-FR';
|
||||
const str = FormatLib.date(new Date('2023-01-12T12:00:00+0100'));
|
||||
expect(str).toBe('12/01/2023');
|
||||
});
|
||||
test('format an iso8601 short date', () => {
|
||||
Fablab.intl_locale = 'fr-FR';
|
||||
const str = FormatLib.date('2023-01-12');
|
||||
expect(str).toBe('12/01/2023');
|
||||
});
|
||||
test('format an iso8601 date', () => {
|
||||
Fablab.intl_locale = 'fr-CA';
|
||||
const str = FormatLib.date('2023-01-12T23:59:14-0500');
|
||||
expect(str).toBe('2023-01-12');
|
||||
});
|
||||
test('format a time', () => {
|
||||
Fablab.intl_locale = 'fr-FR';
|
||||
const str = FormatLib.time(new Date('2023-01-12T23:59:14+0100'));
|
||||
expect(str).toBe('23:59');
|
||||
});
|
||||
test('format an iso8601 short time', () => {
|
||||
Fablab.intl_locale = 'fr-FR';
|
||||
const str = FormatLib.time('23:59');
|
||||
expect(str).toBe('23:59');
|
||||
});
|
||||
test('format an iso8601 time', () => {
|
||||
Fablab.intl_locale = 'fr-CA';
|
||||
const str = FormatLib.time('2023-01-12T23:59:14-0500');
|
||||
expect(str).toBe('23 h 59');
|
||||
});
|
||||
});
|
@ -2,7 +2,7 @@
|
||||
"references": [
|
||||
{ "path": "../../" }
|
||||
],
|
||||
"include": ["components/**/*", "__fixtures__/**/*", "__lib__/**/*"],
|
||||
"include": ["components/**/*", "lib/**/*", "__fixtures__/**/*", "__lib__/**/*"],
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"target": "ES2015",
|
||||
|
35
test/helpers/auth_provider_helper.rb
Normal file
35
test/helpers/auth_provider_helper.rb
Normal file
@ -0,0 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Provides methods to help authentication providers
|
||||
module AuthProviderHelper
|
||||
def github_provider_params(name)
|
||||
{
|
||||
name: name,
|
||||
providable_type: 'OAuth2Provider',
|
||||
providable_attributes: {
|
||||
authorization_endpoint: 'authorize',
|
||||
token_endpoint: 'access_token',
|
||||
base_url: 'https://github.com/login/oauth/',
|
||||
profile_url: 'https://github.com/settings/profile',
|
||||
client_id: ENV.fetch('OAUTH_CLIENT_ID', 'github-oauth-app-id'),
|
||||
client_secret: ENV.fetch('OAUTH_CLIENT_SECRET', 'github-oauth-app-secret')
|
||||
},
|
||||
auth_provider_mappings_attributes: [
|
||||
{
|
||||
api_data_type: 'json',
|
||||
api_endpoint: 'https://api.github.com/user',
|
||||
api_field: 'id',
|
||||
local_field: 'uid',
|
||||
local_model: 'user'
|
||||
},
|
||||
{
|
||||
api_data_type: 'json',
|
||||
api_endpoint: 'https://api.github.com/user',
|
||||
api_field: 'html_url',
|
||||
local_field: 'github',
|
||||
local_model: 'profile'
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
end
|
@ -1,45 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
require 'helpers/auth_provider_helper'
|
||||
|
||||
class AuthProvidersTest < ActionDispatch::IntegrationTest
|
||||
include AuthProviderHelper
|
||||
|
||||
def setup
|
||||
@admin = User.find_by(username: 'admin')
|
||||
login_as(@admin, scope: :user)
|
||||
Fablab::Application.load_tasks if Rake::Task.tasks.empty?
|
||||
end
|
||||
|
||||
test 'create an auth external provider and activate it' do
|
||||
name = 'GitHub'
|
||||
post '/api/auth_providers',
|
||||
params: {
|
||||
auth_provider: {
|
||||
name: name,
|
||||
providable_type: 'OAuth2Provider',
|
||||
providable_attributes: {
|
||||
authorization_endpoint: 'authorize',
|
||||
token_endpoint: 'access_token',
|
||||
base_url: 'https://github.com/login/oauth/',
|
||||
profile_url: 'https://github.com/settings/profile',
|
||||
client_id: ENV.fetch('OAUTH_CLIENT_ID', 'github-oauth-app-id'),
|
||||
client_secret: ENV.fetch('OAUTH_CLIENT_SECRET', 'github-oauth-app-secret')
|
||||
},
|
||||
auth_provider_mappings_attributes: [
|
||||
{
|
||||
api_data_type: 'json',
|
||||
api_endpoint: 'https://api.github.com/user',
|
||||
api_field: 'id',
|
||||
local_field: 'uid',
|
||||
local_model: 'user'
|
||||
},
|
||||
{
|
||||
api_data_type: 'json',
|
||||
api_endpoint: 'https://api.github.com/user',
|
||||
api_field: 'html_url',
|
||||
local_field: 'github',
|
||||
local_model: 'profile'
|
||||
}
|
||||
]
|
||||
}
|
||||
auth_provider: github_provider_params(name)
|
||||
}.to_json,
|
||||
headers: default_headers
|
||||
|
||||
@ -58,8 +35,7 @@ class AuthProvidersTest < ActionDispatch::IntegrationTest
|
||||
assert_equal 2, provider[:auth_provider_mappings_attributes].length
|
||||
|
||||
# now let's activate this new provider
|
||||
Fablab::Application.load_tasks if Rake::Task.tasks.empty?
|
||||
Rake::Task['fablab:auth:switch_provider'].invoke(name)
|
||||
Rake::Task['fablab:auth:switch_provider'].execute(Rake::TaskArguments.new([:provider], [name]))
|
||||
|
||||
db_provider&.reload
|
||||
assert_equal 'active', db_provider&.status
|
||||
@ -68,4 +44,111 @@ class AuthProvidersTest < ActionDispatch::IntegrationTest
|
||||
assert_not_nil u.auth_token
|
||||
end
|
||||
end
|
||||
|
||||
test 'update an authentication provider' do
|
||||
provider = AuthProvider.create!(github_provider_params('GitHub'))
|
||||
patch "/api/auth_providers/#{provider.id}",
|
||||
params: {
|
||||
auth_provider: {
|
||||
providable_type: 'OAuth2Provider',
|
||||
auth_provider_mappings_attributes: [
|
||||
{ api_data_type: 'json', api_endpoint: 'https://api.github.com/user',
|
||||
api_field: 'avatar_url', local_field: 'avatar', local_model: 'profile' }
|
||||
]
|
||||
}
|
||||
}.to_json,
|
||||
headers: default_headers
|
||||
|
||||
# Check response format & status
|
||||
assert_equal 200, response.status, response.body
|
||||
assert_equal Mime[:json], response.content_type
|
||||
|
||||
provider.reload
|
||||
|
||||
# Check the provider was updated
|
||||
res = json_response(response.body)
|
||||
assert_equal provider.id, res[:id]
|
||||
assert_equal 3, provider.auth_provider_mappings.count
|
||||
assert_not_nil provider.auth_provider_mappings.find_by(api_field: 'avatar_url')
|
||||
end
|
||||
|
||||
test 'build an oauth2 strategy name' do
|
||||
get '/api/auth_providers/strategy_name?providable_type=OAuth2Provider&name=Sleede'
|
||||
|
||||
assert_response :success
|
||||
assert_equal 'oauth2-sleede', response.body
|
||||
end
|
||||
|
||||
test 'build an openid strategy name' do
|
||||
get '/api/auth_providers/strategy_name?providable_type=OpenIdConnectProvider&name=Sleede'
|
||||
|
||||
assert_response :success
|
||||
assert_equal 'openidconnect-sleede', response.body
|
||||
end
|
||||
|
||||
test 'show an authentication provider' do
|
||||
provider = AuthProvider.first
|
||||
get "/api/auth_providers/#{provider.id}"
|
||||
|
||||
# Check response format & status
|
||||
assert_equal 200, response.status, response.body
|
||||
assert_equal Mime[:json], response.content_type
|
||||
|
||||
# Check the provider was updated
|
||||
res = json_response(response.body)
|
||||
assert_equal provider.id, res[:id]
|
||||
assert_equal provider.providable_type, res[:providable_type]
|
||||
end
|
||||
|
||||
test 'show fields available for mapping' do
|
||||
get '/api/auth_providers/mapping_fields'
|
||||
|
||||
assert_equal 200, response.status, response.body
|
||||
assert_equal Mime[:json], response.content_type
|
||||
|
||||
# Check the returned fields
|
||||
res = json_response(response.body)
|
||||
assert_not_empty res[:user]
|
||||
assert_not_empty res[:profile]
|
||||
assert_not res[:user].map(&:first).include?('encrypted_password')
|
||||
assert(res[:user].map(&:last).all? { |type| %w[string boolean integer datetime].include?(type) })
|
||||
end
|
||||
|
||||
test 'get the current active provider' do
|
||||
get '/api/auth_providers/active'
|
||||
|
||||
assert_equal 200, response.status, response.body
|
||||
assert_equal Mime[:json], response.content_type
|
||||
|
||||
# Check the returned fields
|
||||
res = json_response(response.body)
|
||||
assert_equal AuthProvider.active.id, res[:id]
|
||||
assert_nil res[:previous_provider]
|
||||
end
|
||||
|
||||
test 'send auth migration token' do
|
||||
# create an enable an oauth2 provider
|
||||
name = 'TokenTest'
|
||||
AuthProvider.create!(github_provider_params(name))
|
||||
Rake::Task['fablab:auth:switch_provider'].execute(Rake::TaskArguments.new([:provider], [name]))
|
||||
|
||||
# send the migration token
|
||||
user = User.find(10)
|
||||
post '/api/auth_providers/send_code',
|
||||
params: {
|
||||
email: user.email
|
||||
}.to_json,
|
||||
headers: default_headers
|
||||
|
||||
assert_equal 200, response.status, response.body
|
||||
assert_equal Mime[:json], response.content_type
|
||||
|
||||
# check resulting notification
|
||||
notification = Notification.find_by(
|
||||
notification_type_id: NotificationType.find_by_name('notify_user_auth_migration'), # rubocop:disable Rails/DynamicFindBy
|
||||
attached_object_type: 'User',
|
||||
attached_object_id: user.id
|
||||
)
|
||||
assert_not_nil notification, 'user notification was not created'
|
||||
end
|
||||
end
|
||||
|
66
test/integration/events/delete_test.rb
Normal file
66
test/integration/events/delete_test.rb
Normal file
@ -0,0 +1,66 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
module Events; end
|
||||
|
||||
class Events::DeleteTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
admin = User.with_role(:admin).first
|
||||
login_as(admin, scope: :user)
|
||||
end
|
||||
|
||||
test 'delete an event' do
|
||||
event = Event.first
|
||||
delete "/api/events/#{event.id}?mode=single", headers: default_headers
|
||||
|
||||
# Check response format & status
|
||||
assert_response :success
|
||||
assert_equal Mime[:json], response.content_type
|
||||
res = json_response(response.body)
|
||||
assert_equal 1, res[:deleted]
|
||||
|
||||
# Check the event was correctly deleted
|
||||
assert_raise ActiveRecord::RecordNotFound do
|
||||
event.reload
|
||||
end
|
||||
end
|
||||
|
||||
test 'soft delete an event' do
|
||||
event = Event.first
|
||||
|
||||
# Make a reservation on this event
|
||||
post '/api/local_payment/confirm_payment',
|
||||
params: {
|
||||
customer_id: User.find_by(username: 'pdurand').id,
|
||||
items: [
|
||||
{
|
||||
reservation: {
|
||||
reservable_id: event.id,
|
||||
reservable_type: 'Event',
|
||||
nb_reserve_places: 2,
|
||||
slots_reservations_attributes: [
|
||||
{
|
||||
slot_id: event.availability.slots.first&.id,
|
||||
offered: false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}.to_json,
|
||||
headers: default_headers
|
||||
|
||||
# Check response format & status
|
||||
assert_equal 201, response.status, response.body
|
||||
|
||||
assert_not event.destroyable?
|
||||
delete "/api/events/#{event.id}?mode=single", headers: default_headers
|
||||
assert_response :success
|
||||
res = json_response(response.body)
|
||||
assert_equal 1, res[:deleted]
|
||||
|
||||
event.reload
|
||||
assert_not_nil event.deleted_at
|
||||
end
|
||||
end
|
@ -69,9 +69,13 @@ class MachinesTest < ActionDispatch::IntegrationTest
|
||||
end
|
||||
|
||||
test 'delete a machine' do
|
||||
delete '/api/machines/3', headers: default_headers
|
||||
machine = Machine.find(3)
|
||||
delete "/api/machines/#{machine.id}", headers: default_headers
|
||||
assert_response :success
|
||||
assert_empty response.body
|
||||
assert_raise ActiveRecord::RecordNotFound do
|
||||
machine.reload
|
||||
end
|
||||
end
|
||||
|
||||
test 'soft delete a machine' do
|
||||
|
Loading…
Reference in New Issue
Block a user