mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-12-01 12:24:28 +01:00
Merge branch 'dev' for release 5.6.11
This commit is contained in:
commit
07e6fee403
@ -1,5 +1,11 @@
|
||||
# Changelog Fab-manager
|
||||
|
||||
## v5.6.11 2023 February 07
|
||||
|
||||
- OpenAPI endpoint to fetch subscription data
|
||||
- Fix a bug: invalid date display in negative timezones
|
||||
- Fix a bug: unable to get latest payment_gateway_object for plan/machine/training/space
|
||||
|
||||
## v5.6.10 2023 February 02
|
||||
|
||||
- Optimized memory consumption in statistics fetcher service
|
||||
|
43
app/controllers/open_api/v1/subscriptions_controller.rb
Normal file
43
app/controllers/open_api/v1/subscriptions_controller.rb
Normal file
@ -0,0 +1,43 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# authorized 3rd party softwares can fetch the subscriptions through the OpenAPI
|
||||
class OpenAPI::V1::SubscriptionsController < OpenAPI::V1::BaseController
|
||||
extend OpenAPI::ApiDoc
|
||||
include Rails::Pagination
|
||||
expose_doc
|
||||
|
||||
def index
|
||||
@subscriptions = Subscription.order(created_at: :desc)
|
||||
.includes(:plan, statistic_profile: :user)
|
||||
.references(:statistic_profile, :plan)
|
||||
|
||||
@subscriptions = @subscriptions.where('created_at >= ?', DateTime.parse(params[:after])) if params[:after].present?
|
||||
@subscriptions = @subscriptions.where('created_at <= ?', DateTime.parse(params[:before])) if params[:before].present?
|
||||
@subscriptions = @subscriptions.where(plan_id: may_array(params[:plan_id])) if params[:plan_id].present?
|
||||
@subscriptions = @subscriptions.where(statistic_profiles: { user_id: may_array(params[:user_id]) }) if params[:user_id].present?
|
||||
|
||||
@subscriptions = @subscriptions.page(page).per(per_page)
|
||||
@pageination_meta = pageination_meta
|
||||
paginate @subscriptions, per_page: per_page
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def page
|
||||
params[:page] || 1
|
||||
end
|
||||
|
||||
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
|
56
app/doc/open_api/v1/subscriptions_doc.rb
Normal file
56
app/doc/open_api/v1/subscriptions_doc.rb
Normal file
@ -0,0 +1,56 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# openAPI documentation for subscriptions endpoints
|
||||
class OpenAPI::V1::SubscriptionsDoc < OpenAPI::V1::BaseDoc
|
||||
resource_description do
|
||||
short 'Subscriptions'
|
||||
desc 'Subscriptions'
|
||||
formats FORMATS
|
||||
api_version API_VERSION
|
||||
end
|
||||
|
||||
include OpenAPI::V1::Concerns::ParamGroups
|
||||
|
||||
doc_for :index do
|
||||
api :GET, "/#{API_VERSION}/subscriptions", 'Subscriptions index'
|
||||
description "Index of users' subscriptions, with optional pagination. 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": [
|
||||
{
|
||||
"id": 2809,
|
||||
"user_id": 211,
|
||||
"created_at": "2022-08-26T09:41:02.426+02:00",
|
||||
"expiration_date": "2022-09-26T09:41:02.427+02:00",
|
||||
"canceled_at": null,
|
||||
"plan_id": 1
|
||||
},
|
||||
{
|
||||
"id": 2783,
|
||||
"user_id": 211,
|
||||
"created_at": "2022-06-06T20:03:33.470+02:00",
|
||||
"expiration_date": "2022-07-06T20:03:33.470+02:00",
|
||||
"canceled_at": null,
|
||||
"plan_id": 1
|
||||
},
|
||||
{
|
||||
"id": 2773,
|
||||
"user_id": 211,
|
||||
"created_at": "2021-12-23T19:26:36.852+01:00",
|
||||
"expiration_date": "2022-01-23T19:26:36.852+01:00",
|
||||
"canceled_at": null,
|
||||
"plan_id": 1
|
||||
}
|
||||
],
|
||||
"total_pages": 3,
|
||||
"total_count": 9,
|
||||
"page": 1,
|
||||
"page_siez": 3
|
||||
}
|
||||
SUBSCRIPTIONS
|
||||
end
|
||||
end
|
@ -1,4 +1,4 @@
|
||||
import moment, { unitOfTime } from 'moment';
|
||||
import moment, { unitOfTime } from 'moment-timezone';
|
||||
import { IFablab } from '../models/fablab';
|
||||
import { TDateISO, TDateISODate, TDateISOShortTime } from '../typings/date-iso';
|
||||
|
||||
@ -46,30 +46,23 @@ export default class FormatLib {
|
||||
} else {
|
||||
tempDate = moment(date).toDate();
|
||||
}
|
||||
return Intl.DateTimeFormat(Fablab.intl_locale).format(tempDate);
|
||||
return Intl.DateTimeFormat(Fablab.intl_locale, { timeZone: Fablab.timezone }).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.setDate(parseInt(isoDateMatch[3], 10));
|
||||
res.setMonth(parseInt(isoDateMatch[2], 10) - 1);
|
||||
res.setFullYear(parseInt(isoDateMatch[1], 10));
|
||||
|
||||
return res;
|
||||
private static parseISOdate = (date: TDateISO|TDateISODate): Date => {
|
||||
const isoDateMatch = (date as string)?.match(/^(\d\d\d\d-\d\d-\d\d)/);
|
||||
return new Date(`${isoDateMatch[1]}T12:00:00${Fablab.timezone_offset}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
private static parseISOtime = (date: TDateISO|TDateISOShortTime): Date => {
|
||||
const isoTimeMatch = (date as string)?.match(/(^|T)(\d\d:\d\d)/);
|
||||
return new Date(`1970-01-01T${isoTimeMatch[2]}:00${Fablab.timezone_offset}`);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -89,7 +82,7 @@ export default class FormatLib {
|
||||
} else {
|
||||
tempDate = moment(date).toDate();
|
||||
}
|
||||
return Intl.DateTimeFormat(Fablab.intl_locale, { hour: 'numeric', minute: 'numeric' }).format(tempDate);
|
||||
return Intl.DateTimeFormat(Fablab.intl_locale, { hour: 'numeric', minute: 'numeric', timeZone: Fablab.timezone }).format(tempDate);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { IANATimeZone, TTimezoneISO } from '../typings/date-iso';
|
||||
|
||||
export interface IFablab {
|
||||
plansModule: boolean,
|
||||
spacesModule: boolean,
|
||||
@ -13,7 +15,8 @@ export interface IFablab {
|
||||
fullcalendar_locale: string,
|
||||
intl_locale: string,
|
||||
intl_currency: string,
|
||||
timezone: string,
|
||||
timezone: IANATimeZone,
|
||||
timezone_offset: TTimezoneISO,
|
||||
weekStartingDay: string,
|
||||
d3DateFormat: string,
|
||||
uibDateFormat: string,
|
||||
|
@ -26,7 +26,7 @@ type TDateISOTime = `${TDateISOShortTime}:${TSeconds}`|`${TDateISOShortTime}:${T
|
||||
/**
|
||||
* Represent a timezone like `+0100`
|
||||
*/
|
||||
type TTimezoneISO = `+${THours}${TMinutes}`|`-${THours}${TMinutes}`|'Z'
|
||||
type TTimezoneISO = `+${THours}${TMinutes}`|`-${THours}${TMinutes}`|`+${THours}:${TMinutes}`|`-${THours}:${TMinutes}`|'Z'
|
||||
|
||||
/**
|
||||
* Represent a string like `2021-01-08T14:42:34.678Z` (format: ISO 8601).
|
||||
@ -36,3 +36,5 @@ type TTimezoneISO = `+${THours}${TMinutes}`|`-${THours}${TMinutes}`|'Z'
|
||||
* "Expression produces a union type that is too complex to represent. ts(2590)
|
||||
*/
|
||||
export type TDateISO = `${TDateISODate}T${TDateISOTime}${TTimezoneISO}`;
|
||||
|
||||
export type IANATimeZone = `${string}/${string}` | 'UTC';
|
||||
|
@ -29,7 +29,7 @@ class Machine < ApplicationRecord
|
||||
has_many :credits, as: :creditable, dependent: :destroy
|
||||
has_many :plans, through: :credits
|
||||
|
||||
has_one :payment_gateway_object, as: :item, dependent: :destroy
|
||||
has_one :payment_gateway_object, -> { order id: :desc }, inverse_of: :machine, as: :item, dependent: :destroy
|
||||
|
||||
has_many :machines_products, dependent: :destroy
|
||||
has_many :products, through: :machines_products
|
||||
|
@ -13,7 +13,7 @@ class Plan < ApplicationRecord
|
||||
has_many :subscriptions, dependent: :nullify
|
||||
has_one :plan_file, as: :viewable, dependent: :destroy
|
||||
has_many :prices, dependent: :destroy
|
||||
has_one :payment_gateway_object, as: :item, dependent: :destroy
|
||||
has_one :payment_gateway_object, -> { order id: :desc }, inverse_of: :plan, as: :item, dependent: :destroy
|
||||
|
||||
extend FriendlyId
|
||||
friendly_id :base_name, use: :slugged
|
||||
|
@ -26,7 +26,7 @@ class Space < ApplicationRecord
|
||||
has_many :prepaid_packs, as: :priceable, dependent: :destroy
|
||||
has_many :credits, as: :creditable, dependent: :destroy
|
||||
|
||||
has_one :payment_gateway_object, as: :item, dependent: :destroy
|
||||
has_one :payment_gateway_object, -> { order id: :desc }, inverse_of: :space, as: :item, dependent: :destroy
|
||||
|
||||
has_one :advanced_accounting, as: :accountable, dependent: :destroy
|
||||
accepts_nested_attributes_for :advanced_accounting, allow_destroy: true
|
||||
|
@ -27,7 +27,7 @@ class Training < ApplicationRecord
|
||||
has_many :credits, as: :creditable, dependent: :destroy
|
||||
has_many :plans, through: :credits
|
||||
|
||||
has_one :payment_gateway_object, as: :item, dependent: :destroy
|
||||
has_one :payment_gateway_object, -> { order id: :desc }, inverse_of: :training, as: :item, dependent: :destroy
|
||||
|
||||
has_one :advanced_accounting, as: :accountable, dependent: :destroy
|
||||
accepts_nested_attributes_for :advanced_accounting, allow_destroy: true
|
||||
|
@ -54,6 +54,7 @@
|
||||
Fablab.intl_locale = "<%= Rails.application.secrets.intl_locale %>";
|
||||
Fablab.intl_currency = "<%= Rails.application.secrets.intl_currency %>";
|
||||
Fablab.timezone = "<%= Time.zone.tzinfo.name %>";
|
||||
Fablab.timezone_offset = "<%= Time.zone.formatted_offset %>";
|
||||
Fablab.translations = {
|
||||
app: {
|
||||
shared: {
|
||||
|
10
app/views/open_api/v1/subscriptions/index.json.jbuilder
Normal file
10
app/views/open_api/v1/subscriptions/index.json.jbuilder
Normal file
@ -0,0 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.data @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]
|
@ -282,6 +282,7 @@ Rails.application.routes.draw do
|
||||
resources :events
|
||||
resources :availabilities
|
||||
resources :accounting
|
||||
resources :subscriptions
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fab-manager",
|
||||
"version": "5.6.10",
|
||||
"version": "5.6.11",
|
||||
"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",
|
||||
|
@ -24,6 +24,7 @@ global.Fablab.fullcalendar_locale = 'fr';
|
||||
global.Fablab.intl_locale = 'fr-FR';
|
||||
global.Fablab.intl_currency = 'EUR';
|
||||
global.Fablab.timezone = 'Europe/Paris';
|
||||
global.Fablab.timezone_offset = '+01:00';
|
||||
global.Fablab.translations = {
|
||||
app: {
|
||||
shared: {
|
||||
|
@ -3,38 +3,73 @@ import { IFablab } from 'models/fablab';
|
||||
|
||||
declare const Fablab: IFablab;
|
||||
describe('FormatLib', () => {
|
||||
test('format a date', () => {
|
||||
test('format a Date object in french format', () => {
|
||||
Fablab.intl_locale = 'fr-FR';
|
||||
const str = FormatLib.date(new Date('2023-01-12T12:00:00+0100'));
|
||||
Fablab.timezone = 'Europe/Paris';
|
||||
Fablab.timezone_offset = '+01:00';
|
||||
const str = FormatLib.date(new Date('2023-01-12T23:59:00+0100'));
|
||||
expect(str).toBe('12/01/2023');
|
||||
});
|
||||
test('format a Date object in canadian format', () => {
|
||||
Fablab.intl_locale = 'fr-CA';
|
||||
Fablab.timezone = 'America/Toronto';
|
||||
Fablab.timezone_offset = '-05:00';
|
||||
const str = FormatLib.date(new Date('2023-01-12T23:59:00-0500'));
|
||||
expect(str).toBe('2023-01-12');
|
||||
});
|
||||
test('format an iso8601 short date in french format', () => {
|
||||
Fablab.intl_locale = 'fr-FR';
|
||||
Fablab.timezone = 'Europe/Paris';
|
||||
Fablab.timezone_offset = '+01:00';
|
||||
const str = FormatLib.date('2023-01-12');
|
||||
expect(str).toBe('12/01/2023');
|
||||
});
|
||||
test('format an iso8601 short date in canadian format', () => {
|
||||
Fablab.intl_locale = 'fr-CA';
|
||||
Fablab.timezone = 'America/Toronto';
|
||||
Fablab.timezone_offset = '-05:00';
|
||||
const str = FormatLib.date('2023-02-27');
|
||||
expect(str).toBe('2023-02-27');
|
||||
});
|
||||
test('format an iso8601 date', () => {
|
||||
test('format an iso8601 date in french format', () => {
|
||||
Fablab.intl_locale = 'fr-FR';
|
||||
Fablab.timezone = 'Europe/Paris';
|
||||
Fablab.timezone_offset = '+01:00';
|
||||
const str = FormatLib.date('2023-01-12T23:59:14+0100');
|
||||
expect(str).toBe('12/01/2023');
|
||||
});
|
||||
test('format an iso8601 date in canadian format', () => {
|
||||
Fablab.intl_locale = 'fr-CA';
|
||||
Fablab.timezone = 'America/Toronto';
|
||||
Fablab.timezone_offset = '-05:00';
|
||||
const str = FormatLib.date('2023-01-12T23:59:14-0500');
|
||||
expect(str).toBe('2023-01-12');
|
||||
});
|
||||
test('format a time', () => {
|
||||
test('format a time from a Date object', () => {
|
||||
Fablab.intl_locale = 'fr-FR';
|
||||
Fablab.timezone = 'Europe/Paris';
|
||||
Fablab.timezone_offset = '+01:00';
|
||||
const str = FormatLib.time(new Date('2023-01-12T23:59:14+0100'));
|
||||
expect(str).toBe('23:59');
|
||||
});
|
||||
test('format a time from a Date object in canadian format', () => {
|
||||
Fablab.intl_locale = 'fr-CA';
|
||||
Fablab.timezone = 'America/Toronto';
|
||||
Fablab.timezone_offset = '-05:00';
|
||||
const str = FormatLib.time(new Date('2023-01-12T23:59:14-0500'));
|
||||
expect(str).toBe('23 h 59');
|
||||
});
|
||||
test('format an iso8601 short time', () => {
|
||||
Fablab.intl_locale = 'fr-FR';
|
||||
Fablab.timezone = 'Europe/Paris';
|
||||
Fablab.timezone_offset = '+01:00';
|
||||
const str = FormatLib.time('23:59');
|
||||
expect(str).toBe('23:59');
|
||||
});
|
||||
test('format an iso8601 time', () => {
|
||||
Fablab.intl_locale = 'fr-CA';
|
||||
Fablab.timezone = 'America/Toronto';
|
||||
Fablab.timezone_offset = '-05:00';
|
||||
const str = FormatLib.time('2023-01-12T23:59:14-0500');
|
||||
expect(str).toBe('23 h 59');
|
||||
});
|
||||
|
36
test/integration/open_api/subscriptions_test.rb
Normal file
36
test/integration/open_api/subscriptions_test.rb
Normal file
@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
module OpenApi; end
|
||||
|
||||
class OpenApi::SubscriptionsTest < ActionDispatch::IntegrationTest
|
||||
def setup
|
||||
@token = OpenAPI::Client.find_by(name: 'minitest').token
|
||||
end
|
||||
|
||||
test 'list all subscriptions' do
|
||||
get '/open_api/v1/subscriptions', headers: open_api_headers(@token)
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test 'list all subscriptions with pagination' do
|
||||
get '/open_api/v1/subscriptions?page=1&per_page=5', headers: open_api_headers(@token)
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test 'list all subscriptions for a user' do
|
||||
get '/open_api/v1/subscriptions?user_id=3', headers: open_api_headers(@token)
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test 'list all subscriptions for a user with pagination' do
|
||||
get '/open_api/v1/subscriptions?user_id=3&page=1&per_page=5', headers: open_api_headers(@token)
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test 'list all subscriptions for a plan with pagination' do
|
||||
get '/open_api/v1/subscriptions?plan_id=1&page=1&per_page=5', headers: open_api_headers(@token)
|
||||
assert_response :success
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue
Block a user