1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-19 13:54:25 +01:00

Ability to configure reservation slot restrict for plans

This commit is contained in:
Du Peng 2020-02-07 17:37:00 +01:00
parent 767a8cd332
commit 74cc69d1bd
24 changed files with 232 additions and 27 deletions

View File

@ -1,5 +1,6 @@
# Changelog Fab Manager
- Ability to configure reservation slot restrict for plans
- Ability to create and delete periodic calendar availabilities (recurrence)
- Ability to fully customize the home page
- Automated setup assistant
@ -140,7 +141,7 @@
## v4.0.4 2019 August 14
- Fix a bug: #140 VAT rate is erroneous in invoices.
Note: this bug was introduced in v4.0.3 and requires (if you are on v4.0.3) to regenerate the invoices since August 1st (if
Note: this bug was introduced in v4.0.3 and requires (if you are on v4.0.3) to regenerate the invoices since August 1st (if
- [TODO DEPLOY] `rake fablab:maintenance:regenerate_invoices[2019,8]`
## v4.0.3 2019 August 01
@ -184,7 +185,7 @@
- Refactored user's profile to keep invoicing data after an user was deleted
- Refactored user's profile to keep statistical data after an user was deleted
- Ability to delete an user (fixes #129 and #120)
- Ask user acceptance before deposing analytics cookies
- Ask user acceptance before deposing analytics cookies
- Fix a bug: (spanish) some translations are not loaded correctly
- Fix a bug: some users may not appear in the admin's general listing
- Fix a bug: Availabilities export report an erroneous number of reservations for machine availabilities (#131)
@ -381,8 +382,8 @@
- Fix a security issue: sprockets < 2.12.5 has a security vulnerability as described in [CVE-2018-3760](https://nvd.nist.gov/vuln/detail/CVE-2018-3760)
- Ensure elasticSearch indices are started with green status on new installations
- Refactored User.to_json to remove code duplication
- Fixed syntax and typos in README
- [TODO DEPLOY] **IMPORTANT** Please read [elastic_upgrade.md](doc/elastic_upgrade.md) for instructions on upgrading ElasticSearch.
- Fixed syntax and typos in README
- [TODO DEPLOY] **IMPORTANT** Please read [elastic_upgrade.md](doc/elastic_upgrade.md) for instructions on upgrading ElasticSearch.
- [TODO DEPLOY] `rake fablab:fix:categories_slugs`
- [TODO DEPLOY] -> (only dev) `bundle install`
- [TODO DEPLOY] `rake db:seed`
@ -395,7 +396,7 @@
- Set Stripe API version, all fab-managers has to use this version because codebase relies on it
- Fix a security issue: OmniAuth < 1.3.2 has a security vulnerability described in [CVE-2017-18076](https://nvd.nist.gov/vuln/detail/CVE-2017-18076)
- Fix a security issue: rack-protection < 1.5.5 has a security vulnerability described in [CVE-2018-1000119](https://nvd.nist.gov/vuln/detail/CVE-2018-1000119)
- Fix a security issue: http gem < 0.7.3 has a security vulnerability described in [CVE-2015-1828](https://nvd.nist.gov/vuln/detail/CVE-2015-1828), updates twitter gem as a dependency
- Fix a security issue: http gem < 0.7.3 has a security vulnerability described in [CVE-2015-1828](https://nvd.nist.gov/vuln/detail/CVE-2015-1828), updates twitter gem as a dependency
## v2.6.3 2018 January 2
@ -451,12 +452,12 @@
## v2.5.13 2017 September 11
- Fix a bug: ActiveRecord::RecordNotFound when running rake task fix:recursive_events_over_DST with recursive events which the initial event was deleted
- Fix a bug: ActiveRecord::RecordNotFound when running rake task fix:recursive_events_over_DST with recursive events which the initial event was deleted
## v2.5.12 2017 September 11
- Fix a bug: Long words overflow from homepage's events blocks
- Fix a bug: ActiveRecord::RecordNotFound when running rake task fix:recursive_events_over_DST with non-recursive events
- Fix a bug: ActiveRecord::RecordNotFound when running rake task fix:recursive_events_over_DST with non-recursive events
## v2.5.11 2017 September 7

View File

@ -297,7 +297,9 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state
machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }],
trainingsPromise: ['Training', function (Training) { return Training.query().$promise; }],
spacesPromise: ['Space', function (Space) { return Space.query().$promise; }],
tagsPromise: ['Tag', function (Tag) { return Tag.query().$promise; }]
tagsPromise: ['Tag', function (Tag) { return Tag.query().$promise; }],
plansPromise: ['Plan', function (Plan) { return Plan.query().$promise; }],
groupsPromise: ['Group', function (Group) { return Group.query().$promise; }]
} });
// when the modal is closed, we send the slot to the server for saving
modalInstance.result.then(
@ -394,8 +396,8 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state
/**
* Controller used in the slot creation modal window
*/
Application.Controllers.controller('CreateEventModalController', ['$scope', '$uibModalInstance', '$sce', 'moment', 'start', 'end', 'machinesPromise', 'Availability', 'trainingsPromise', 'spacesPromise', 'tagsPromise', 'growl', '_t',
function ($scope, $uibModalInstance, $sce, moment, start, end, machinesPromise, Availability, trainingsPromise, spacesPromise, tagsPromise, growl, _t) {
Application.Controllers.controller('CreateEventModalController', ['$scope', '$uibModalInstance', '$sce', 'moment', 'start', 'end', 'machinesPromise', 'Availability', 'trainingsPromise', 'spacesPromise', 'tagsPromise', 'plansPromise', 'groupsPromise', 'growl', '_t',
function ($scope, $uibModalInstance, $sce, moment, start, end, machinesPromise, Availability, trainingsPromise, spacesPromise, tagsPromise, plansPromise, groupsPromise, growl, _t) {
// $uibModal parameter
$scope.start = start;
@ -414,6 +416,21 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui
// all tags list
$scope.tags = tagsPromise;
$scope.isOnlySubscriptions = false;
$scope.selectedPlans = [];
$scope.selectedPlansBinding = {};
// list of plans, classified by group
$scope.plansClassifiedByGroup = [];
for (let group of Array.from(groupsPromise)) {
const groupObj = { id: group.id, name: group.name, plans: [] };
for (let plan of Array.from(plansPromise)) {
if (plan.group_id === group.id) { groupObj.plans.push(plan); }
}
if (groupObj.plans.length > 0) {
$scope.plansClassifiedByGroup.push(groupObj);
}
}
// machines associated with the created slot
$scope.selectedMachines = [];
$scope.selectedMachinesBinding = {};
@ -463,6 +480,9 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui
// localized name(s) of the selected tag(s)
$scope.tagsName = '';
// localized name(s) of the selected plan(s)
$scope.plansName = '';
/**
* Adds or removes the provided machine from the current slot
* @param machine {Object}
@ -491,6 +511,34 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui
}
}
/**
* Adds or removes the provided plan from the current slot
* @param plan {Object}
*/
$scope.toggleSelectPlan = function (plan) {
const index = $scope.selectedPlans.indexOf(plan);
if (index > -1) {
return $scope.selectedMachines.splice(index, 1);
} else {
return $scope.selectedMachines.push(machine);
}
};
/**
* Select/unselect all the plans
*/
$scope.toggleAllPlans = function() {
const count = $scope.selectedPlans.length;
$scope.selectedPlans = [];
$scope.selectedPlansBinding = {};
if (count == 0) {
plansPromise.forEach(function (plan) {
$scope.selectedPlans.push(plan);
$scope.selectedPlansBinding[plan.id] = true;
})
}
};
/**
* Callback for the modal window validation: save the slot and closes the modal
*/
@ -510,6 +558,9 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui
if ($scope.availability.is_recurrent) {
$scope.availability.occurrences = $scope.occurrences;
}
if ($scope.isOnlySubscriptions && $scope.selectedPlans.length > 0) {
$scope.availability.plan_ids = $scope.selectedPlans.map(function (p) { return p.id; });
}
return Availability.save(
{ availability: $scope.availability },
function (availability) { $uibModalInstance.close(availability); }
@ -560,6 +611,14 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui
$scope.selectedSpace = $scope.spaces[0];
}
// when disable is only subscriptions option, reset all selected plans
$scope.$watch('isOnlySubscriptions', function(value) {
if (!value) {
$scope.selectedPlans = [];
$scope.selectedPlansBinding = {};
}
});
// When we configure a machine/space availability, do not let the user change the end time, as the total
// time must be dividable by Fablab.slotDuration minutes (base slot duration). For training availabilities, the user
// can configure any duration as it does not matters.
@ -685,6 +744,9 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui
return $scope.availability.tag_ids.indexOf(t.id) > -1;
})
$scope.tagsName = localizedList(tags);
if ($scope.isOnlySubscriptions && $scope.selectedPlans.length > 0) {
$scope.plansName = localizedList($scope.selectedPlans);
}
}
const localizedList = function (items) {

View File

@ -367,6 +367,8 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$stat
$scope.settings = settingsPromise;
// list of plans, classified by group
$scope.groups = groupsPromise;
$scope.plans = plansPromise;
$scope.plansClassifiedByGroup = [];
for (let group of Array.from(groupsPromise)) {
const groupObj = { id: group.id, name: group.name, plans: [] };

View File

@ -23,6 +23,8 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
plan: '=',
planSelectionTime: '=',
settings: '=',
plans: '=',
groups: '=',
onSlotAddedToCart: '=',
onSlotRemovedFromCart: '=',
onSlotStartToModify: '=',
@ -134,6 +136,27 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
$scope.payCart = function () {
// first, we check that a user was selected
if (Object.keys($scope.user).length > 0) {
// check user was selected a plan if slot is restricted for subcriptions
const slotHasPlan = [];
$scope.events.reserved.forEach(slot => {
if (slot.plan_ids.length > 0) {
if (
($scope.selectedPlan && _.include(slot.plan_ids, $scope.selectedPlan.id)) ||
($scope.user.subscribed_plan && _.include(slot.plan_ids, $scope.user.subscribed_plan.id))
) {
slotHasPlan.push(true);
} else {
slotHasPlan.push(false);
}
}
});
const hasPlanForSlot = slotHasPlan.every(a => a);
if (!hasPlanForSlot) {
return growl.error(_t('app.shared.cart.slot_restrict_subcriptions_must_select_plan'));
}
const reservation = mkReservation($scope.user, $scope.events.reserved, $scope.selectedPlan);
return Wallet.getWalletByUser({ user_id: $scope.user.id }, function (wallet) {
@ -262,6 +285,21 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
*/
var slotSelectionChanged = function () {
if ($scope.slot) {
// build a list of plans if this slot is restricted for subcriptions
if ($scope.slot.plan_ids.length > 0) {
const _plans = _.filter($scope.plans, p => _.include($scope.slot.plan_ids, p.id));
$scope.slot.plansGrouped = [];
for (let group of Array.from($scope.groups)) {
const groupObj = { id: group.id, name: group.name, plans: [] };
for (let plan of Array.from(_plans)) {
if (plan.group_id === group.id) { groupObj.plans.push(plan); }
}
if (groupObj.plans.length > 0) {
$scope.slot.plansGrouped.push(groupObj);
}
}
}
if (!$scope.slot.is_reserved && !$scope.events.modifiable && !$scope.slot.is_completed) {
// slot is not reserved and we are not currently modifying a slot
// -> can be added to cart or removed if already present

View File

@ -93,12 +93,41 @@
</div>
</div>
</div>
<div class="m-t-sm">
<p class="text-center font-sbold" translate>{{ 'app.admin.calendar.restrict_this_slot_for_subcriptions_optional' }}</p>
<div class="row">
<div class="form-group col-md-12">
<label for="is_only_subscriptions" translate>{{ 'app.admin.calendar.enabled' }}</label>
<input bs-switch
ng-model="isOnlySubscriptions"
id="is_only_subscriptions"
type="checkbox"
class="form-control"
switch-on-text="{{ 'app.shared.buttons.yes' | translate }}"
switch-off-text="{{ 'app.shared.buttons.no' | translate }}"
switch-animate="true"/>
</div>
<div class="col-md-12" ng-show="isOnlySubscriptions">
<p class="font-sbold m-t-sm">{{ 'app.admin.calendar.select_some_plans' | translate }}</p>
<div class="form-group">
<button class="btn btn-default pull-right m-t-n-xl" ng-click="toggleAllPlans()" translate>{{ selectedPlans.length == 0 ? 'app.admin.calendar.select_all' : 'app.admin.calendar.select_none' }}</button>
<div ng-repeat="group in plansClassifiedByGroup">
<div class="text-center font-sbold">{{::group.name}}</div>
<label class="checkbox m-l-md" ng-repeat="plan in group.plans">
<input type="checkbox" ng-click="toggleSelectPlan(plan)" ng-model="selectedPlansBinding[plan.id]"> {{::plan.name}}</span>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-body m-h" ng-show="step === 4">
<div class="m-t-sm">
<p class="text-center font-sbold" translate>{{ 'app.admin.calendar.recurrence' }}</p>
<div class="row">
<div class="form-group">
<div class="form-group col-md-12">
<label for="is_recurrent" translate>{{ 'app.admin.calendar.enabled' }}</label>
<input bs-switch
ng-model="availability.is_recurrent"
@ -167,6 +196,10 @@
<span class="underline" translate>{{ 'app.admin.calendar.labels' }}</span>
<span ng-bind-html="tagsName"></span>
</div>
<div class="m-t" ng-show="isOnlySubscriptions">
<span class="underline" translate>{{ 'app.admin.calendar.plans' }}</span>
<span ng-bind-html="plansName"></span>
</div>
</div>
</div>
</div>

View File

@ -37,6 +37,8 @@
plan="selectedPlan"
plan-selection-time="planSelectionTime"
settings="settings"
plans="plans"
groups="groups"
on-slot-added-to-cart="markSlotAsAdded"
on-slot-removed-from-cart="markSlotAsRemoved"
on-slot-start-to-modify="markSlotAsModifying"

View File

@ -28,6 +28,15 @@
switch-animate="true"
switch-readonly="{{slot.isValid}}"/>
</div>
<div class="alert alert-warning m-t-sm m-b-none" ng-show="slot.plansGrouped.length">
<div class="font-sbold text-u-c" translate>{{ 'app.shared.cart.slot_restrict_plans' }}</div>
<div ng-repeat="group in slot.plansGrouped">
<div class="font-sbold">{{::group.name}}</div>
<ul class="m-n" ng-repeat="plan in group.plans">
<li>{{::plan.name}}</li>
</ul>
</div>
</div>
</div>
<div>
<button class="btn btn-valid btn-warning btn-block text-u-c r-b" ng-click="validateSlot(slot)" ng-if="!slot.isValid" translate>{{ 'app.shared.cart.confirm_this_slot' }}</button>

View File

@ -148,8 +148,8 @@ class API::AvailabilitiesController < API::ApiController
def availability_params
params.require(:availability).permit(:start_at, :end_at, :available_type, :machine_ids, :training_ids, :nb_total_places,
:is_recurrent, :period, :nb_periods, :end_date,
machine_ids: [], training_ids: [], space_ids: [], tag_ids: [],
machines_attributes: %i[id _destroy])
machine_ids: [], training_ids: [], space_ids: [], tag_ids: [], plan_ids: [],
machines_attributes: %i[id _destroy], plans_attributes: %i[id _destroy])
end
def lock_params

View File

@ -4,7 +4,6 @@
# Eg. a 3D printer will be reservable on thursday from 9 to 11 pm
# Availabilities may be subdivided into Slots (of 1h), for some types of reservables (eg. Machine)
class Availability < ActiveRecord::Base
# elastic initialisations
include Elasticsearch::Model
index_name 'fablab'
@ -29,6 +28,10 @@ class Availability < ActiveRecord::Base
has_many :tags, through: :availability_tags
accepts_nested_attributes_for :tags, allow_destroy: true
has_many :plans_availabilities, dependent: :destroy
has_many :plans, through: :plans_availabilities
accepts_nested_attributes_for :plans, allow_destroy: true
scope :machines, -> { where(available_type: 'machines') }
scope :trainings, -> { includes(:trainings).where(available_type: 'training') }
scope :spaces, -> { includes(:spaces).where(available_type: 'space') }
@ -86,7 +89,7 @@ class Availability < ActiveRecord::Base
def available_space_places
return unless available_type == 'space'
((end_at - start_at)/ApplicationHelper::SLOT_DURATION.minutes).to_i * nb_total_places
((end_at - start_at) / ApplicationHelper::SLOT_DURATION.minutes).to_i * nb_total_places
end
def title(filter = {})
@ -166,5 +169,4 @@ class Availability < ActiveRecord::Base
errors.add(:machine_ids, I18n.t('availabilities.must_be_associated_with_at_least_1_machine'))
end
end

View File

@ -25,7 +25,6 @@ class Plan < ActiveRecord::Base
after_create :create_statistic_type
after_create :set_name
validates :amount, :group, :base_name, presence: true
validates :interval_count, numericality: { only_integer: true, greater_than_or_equal_to: 1 }
validates :interval_count, numericality: { less_than: 13 }, if: proc { |plan| plan.interval == 'month' }

View File

@ -0,0 +1,6 @@
# frozen_string_literal: true
class PlansAvailability < ActiveRecord::Base
belongs_to :plan
belongs_to :availability
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
json.array!(@slots) do |slot|
json.id slot.id if slot.id
json.can_modify slot.can_modify
@ -14,13 +16,16 @@ json.array!(@slots) do |slot|
json.name slot.machine.name
end
# the user who booked the slot ...
json.user do
json.id slot.reservation.user.id
json.name slot.reservation.user.profile.full_name
end if @current_user_role == 'admin' and slot.reservation # ... if the slot was reserved
if (@current_user_role == 'admin') && slot.reservation
json.user do
json.id slot.reservation.user.id
json.name slot.reservation.user.profile.full_name
end
end # ... if the slot was reserved
json.tag_ids slot.availability.tag_ids
json.tags slot.availability.tags do |t|
json.id t.id
json.name t.name
end
json.plan_ids slot.availability.plan_ids
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
json.array!(@slots) do |slot|
json.id slot.id if slot.id
json.can_modify slot.can_modify
@ -15,13 +17,16 @@ json.array!(@slots) do |slot|
json.name slot.space.name
end
# the user who booked the slot ...
json.user do
json.id slot.reservation.user.id
json.name slot.reservation.user.profile.full_name
end if @current_user_role == 'admin' and slot.reservation # ... if the slot was reserved
if (@current_user_role == 'admin') && slot.reservation
json.user do
json.id slot.reservation.user.id
json.name slot.reservation.user.profile.full_name
end
end # ... if the slot was reserved
json.tag_ids slot.availability.tag_ids
json.tags slot.availability.tags do |t|
json.id t.id
json.name t.name
end
json.plan_ids slot.availability.plan_ids
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
json.array!(@availabilities) do |a|
json.id a.slot_id if a.slot_id
if a.is_reserved
@ -31,4 +33,5 @@ json.array!(@availabilities) do |a|
json.id t.id
json.name t.name
end
json.plan_ids slot.availability.plan_ids
end

View File

@ -46,6 +46,9 @@ en:
adjust_the_opening_hours: "Adjust the opening hours"
to_time: "to" # context: time. eg. 'from 18:00 to 21:00'
restrict_this_slot_with_labels_optional: "Restrict this slot with labels (optional)"
restrict_this_slot_for_subcriptions_optional: "Restrict this slot for subscription users (optional)"
select_some_plans: "Select some plans"
plans: "Plan(s):"
recurrence: "Recurrence"
enabled: "Enabled"
period: "Period"

View File

@ -44,6 +44,9 @@ es:
adjust_the_opening_hours: "Ajustar el horario de apertura"
to_time: a
restrict_this_slot_with_labels_optional: "Restringir este horario con etiquetas (opcional)"
restrict_this_slot_for_subcriptions_optional: "Restrict this slot for subscription users (optional)"
select_some_plans: "Select some plans"
plans: "Plan(s):"
recurrence: "Recurrencia"
enabled: "Activa"
period: "Período"

View File

@ -46,6 +46,9 @@ fr:
adjust_the_opening_hours: "Ajuster l'horaire"
to_time: "à" # context: time. eg. 'from 18:00 to 21:00'
restrict_this_slot_with_labels_optional: "Restreindre ce créneau avec des étiquettes (optionnel)"
restrict_this_slot_for_subcriptions_optional: "Restreindre ce créneau pour les abonnements (optionnel)"
select_some_plans: "Sélectionnez des formules d'abonnement"
plans: "Abonnement(s):"
recurrence: "Récurrence"
enabled: "Activée"
period: "Période"

View File

@ -46,6 +46,9 @@ pt:
adjust_the_opening_hours: "Ajustar o horário de funcionamento"
to_time: "ás" # context: time. eg. 'from 18:00 to 21:00'
restrict_this_slot_with_labels_optional: "Restrinja este slot com etiquetas (opcional)"
restrict_this_slot_for_subcriptions_optional: "Restrict this slot for subscription users (optional)"
select_some_plans: "Select some plans"
plans: "Plan(s):"
recurrence: "Recurrence"
enabled: "Enabled"
period: "Period"

View File

@ -439,3 +439,5 @@ en:
a_problem_occurred_during_the_payment_process_please_try_again_later: "A problem occurred during the payment process. Please try again later."
none: "None"
online_payment_disabled: "Online payment is not available. Please contact the Fablab reception directly."
slot_restrict_plans: "This slot is restricted for the plans below:"
slot_restrict_subcriptions_must_select_plan: "The slot is restricted for subscriptions, please select a plan first."

View File

@ -416,3 +416,5 @@ es:
a_problem_occurred_during_the_payment_process_please_try_again_later: "A problem occurred during the payment process. Please try again later."
none: "Ninguno"
online_payment_disabled: "El pago en línea no está disponible. Póngase en contacto directamente con la recepción de Fablab."
slot_restrict_plans: "This slot is restricted for the plans below:"
slot_restrict_subcriptions_must_select_plan: "The slot is restricted for subscriptions, please select a plan first."

View File

@ -439,3 +439,5 @@ fr:
a_problem_occurred_during_the_payment_process_please_try_again_later: "Il y a eu un problème lors de la procédure de paiement. Veuillez réessayer plus tard."
none: "Aucune"
online_payment_disabled: "Le payment par carte bancaire n'est pas disponible. Merci de contacter directement l'accueil du Fablab."
slot_restrict_plans: "Ce créneau est restreint pour les formules d'abonnement ci-desous:"
slot_restrict_subcriptions_must_select_plan: "Le créneau est restreint pour les abonnements, Veuillez tout d'abord sélectionner un formule d'abonnement"

View File

@ -439,3 +439,5 @@ pt:
a_problem_occurred_during_the_payment_process_please_try_again_later: "Um problema ocorreu durante o processo de pagamento. Por favor tente novamente mais tarde."
none: "Vazio"
online_payment_disabled: "O pagamento online não está disponível. Entre em contato diretamente com a recepção do Fablab."
slot_restrict_plans: "This slot is restricted for the plans below:"
slot_restrict_subcriptions_must_select_plan: "The slot is restricted for subscriptions, please select a plan first."

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
class CreatePlansAvailabilities < ActiveRecord::Migration
def change
create_table :plans_availabilities do |t|
t.belongs_to :plan, index: true
t.belongs_to :availability, index: true
end
end
end

View File

@ -11,12 +11,12 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20200127111404) do
ActiveRecord::Schema.define(version: 20200206132857) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
enable_extension "pg_trgm"
enable_extension "unaccent"
enable_extension "pg_trgm"
create_table "abuses", force: :cascade do |t|
t.integer "signaled_id"
@ -465,6 +465,14 @@ ActiveRecord::Schema.define(version: 20200127111404) do
add_index "plans", ["group_id"], name: "index_plans_on_group_id", using: :btree
create_table "plans_availabilities", force: :cascade do |t|
t.integer "plan_id"
t.integer "availability_id"
end
add_index "plans_availabilities", ["availability_id"], name: "index_plans_availabilities_on_availability_id", using: :btree
add_index "plans_availabilities", ["plan_id"], name: "index_plans_availabilities_on_plan_id", using: :btree
create_table "price_categories", force: :cascade do |t|
t.string "name"
t.text "conditions"