1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-11-28 09:24:24 +01:00

Merge branch 'dev' for release 6.3.1

This commit is contained in:
Nicolas Florentin 2023-11-10 17:09:21 +01:00
commit 4979accf11
34 changed files with 204 additions and 138 deletions

View File

@ -1,5 +1,16 @@
# Changelog Fab-manager
## v6.3.1 2023 November 10
- Fix a bug: statistic_sub_type.label of plan was nil
- adds a migrations to fix all statistic_sub_types of plans having label = nil
- Fix a bug: unable to show wallet payment mean for avoir
- updates spanish translations and adds translations
- Fix a bug: avoids crash due to oidc config with scope = nil
- Fix a bug: unable to see value for input group with long label on eventModal
- Improvement: when deleting an event, all reservations are canceled
- Improvement: replace original image by large generated version (event, machine, space, training)
## v6.3.0 2023 November 3
- Fix a bug: fix all failing tasks of rake task file chain.rake
@ -13,6 +24,7 @@
- Fix a bug: replaces custom ServerLocale middleware with sidekiq i18n middleware
- adds a rake task to erase all reservations and invoices (fablab:maintenance:delete_all_reservations_and_invoices)
- improvement: dynamic label (i18n) for stats structure tables
- [TODO DEPLOY] upgrade to v6.2.0 BEFORE upgrading to v6.3.0 !!!
## v6.2.0 2023 October 13
@ -22,6 +34,7 @@
- Fix a bug: fix members tour (help), a selector was not valid anymore
- Fix a bug: unable to save OpenID extra_authorize_params as json
- Fix machine list bug : when there is no user logged in and access machines list with at least one machine associated to a space
- [TODO DEPLOY] `rails db:seed`
## v6.1.2 2023 October 2

View File

@ -77,6 +77,21 @@
}
}
.modal-footer.btn-stack {
display: flex;
flex-direction: column;
gap: 1rem;
&::after,
&::before {
content: none;
}
& > * {
margin: 0 !important;
}
}
.modal-backdrop {
height: 100%;
}

View File

@ -82,18 +82,18 @@
<p class="text-center font-sbold" translate>{{ 'app.admin.calendar.divide_this_availability' }}</p>
<div class="row">
<div class="col-md-5">
<div class="input-group">
<input type="number" class="form-control" ng-model="slots_nb" step="1" min="1" required="true" />
<span class="input-group-addon" translate>{{ 'app.admin.calendar.slots' }}</span>
</div>
<div class="form-group">
<input id="slots_nb" type="number" class="form-control" ng-model="slots_nb" step="1" min="1" required="true" />
<label for="slots_nb" translate>{{ 'app.admin.calendar.slots' }}</label>
</div>
</div>
<p class="col-md-2 middle-of-inputs" translate>
{{ 'app.admin.calendar.slots_of' }}
</p>
<div class="col-md-5">
<div class="input-group">
<input type="number" class="form-control" ng-model="availability.slot_duration" min="1" required="true" />
<span class="input-group-addon" translate>{{ 'app.admin.calendar.minutes' }}</span>
<div class="form-group">
<input id="slot_duration" type="number" class="form-control" ng-model="availability.slot_duration" min="1" required="true" />
<label for="slot_duration" translate>{{ 'app.admin.calendar.minutes' }}</label>
</div>
</div>
</div>
@ -223,7 +223,7 @@
<p class="text-center font-sbold" translate>{{ 'app.admin.calendar.summary' }}</p>
<div class="row">
<span>{{ 'app.admin.calendar.about_to_create' | translate:{NUMBER:occurrences.length,TYPE:availability.available_type} }}</span>
<ul>
<ul style="max-height: 25vh; overflow: auto;">
<li ng-repeat="slot in occurrences">{{slot.start_at | amDateFormat:'L LT'}} - {{slot.end_at | amDateFormat:'LT'}}</li>
</ul>
<div class="alert alert-info text-xs" ng-show="availability.slot_duration">

View File

@ -17,37 +17,37 @@
</section>
</div>
</div>
<section class="m-lg events-management"
ui-tour="events"
ui-tour-backdrop="true"
ui-tour-template-url="'/shared/tour-step-template.html'"
ui-tour-use-hotkeys="true"
ui-tour-scroll-parent-id="content-main"
post-render="setupEventsTour">
<div class="row">
<div class="col-md-12" ng-if="isAuthorized('admin')">
<uib-tabset justified="true" active="tabs.active">
<uib-tab heading="{{ 'app.admin.events.settings' | translate }}" index="0">
<events-settings on-error="onError" on-success="onSuccess" ui-router="uiRouter"></events-settings>
</uib-tab>
<uib-tab heading="{{ 'app.admin.events.events_monitoring' | translate }}" index="1">
<ng-include src="'/admin/events/monitoring.html'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'app.admin.events.manage_filters' | translate }}" index="2">
<ng-include src="'/admin/events/filters.html'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'app.admin.events.manage_prices_categories' | translate }}" index="3" class="prices-tab">
<ng-include src="'/admin/events/prices.html'"></ng-include>
</uib-tab>
</uib-tabset>
</div>
<div class="col-md-12" ng-if="isAuthorized('manager')">
<ng-include src="'/admin/events/monitoring.html'"></ng-include>
</div>
</div>
</section>
</section>
<section class="m-lg events-management"
ui-tour="events"
ui-tour-backdrop="true"
ui-tour-template-url="'/shared/tour-step-template.html'"
ui-tour-use-hotkeys="true"
ui-tour-scroll-parent-id="content-main"
post-render="setupEventsTour">
<div class="row">
<div class="col-md-12" ng-if="isAuthorized('admin')">
<uib-tabset justified="true" active="tabs.active">
<uib-tab heading="{{ 'app.admin.events.settings' | translate }}" index="0">
<events-settings on-error="onError" on-success="onSuccess" ui-router="uiRouter"></events-settings>
</uib-tab>
<uib-tab heading="{{ 'app.admin.events.events_monitoring' | translate }}" index="1">
<ng-include src="'/admin/events/monitoring.html'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'app.admin.events.manage_filters' | translate }}" index="2">
<ng-include src="'/admin/events/filters.html'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'app.admin.events.manage_prices_categories' | translate }}" index="3" class="prices-tab">
<ng-include src="'/admin/events/prices.html'"></ng-include>
</uib-tab>
</uib-tabset>
</div>
<div class="col-md-12" ng-if="isAuthorized('manager')">
<ng-include src="'/admin/events/monitoring.html'"></ng-include>
</div>
</div>
</section>

View File

@ -5,6 +5,7 @@
<div class="modal-body">
<p ng-hide="isRecurrent" translate>{{ 'app.public.events_show.do_you_really_want_to_delete_this_event' }}</p>
<p ng-show="isRecurrent" translate>{{ 'app.public.events_show.delete_recurring_event' }}</p>
<p translate>{{ 'app.public.events_show.all_reservations_for_this_event_will_be_canceled' }}</p>
<div ng-show="isRecurrent" class="form-group">
<label class="checkbox">
<input type="radio" name="delete_mode" ng-model="deleteMode" value="single" required/>

View File

@ -17,61 +17,60 @@
</section>
</div>
</div>
</section>
<section class="events-list-page">
<events-editorial-block on-error="onError"></events-editorial-block>
<section class="events-list-page">
<events-editorial-block on-error="onError"></events-editorial-block>
<div class="row">
<div class="col-md-3" ng-show="categories.length > 0">
<select ng-model="filters.category_id" ng-change="filterEvents()" class="form-control" ng-options="c.id as c.name for c in categories">
<option value="" translate>{{ 'app.public.events_list.all_categories' }}</option>
</select>
</div>
<div class="col-md-3" ng-show="themes.length > 0">
<select ng-model="filters.theme_id" ng-change="filterEvents()" class="form-control" ng-options="t.id as t.name for t in themes">
<option value="" translate>{{ 'app.public.events_list.all_themes' }}</option>
</select>
</div>
<div class="col-md-3" ng-show="ageRanges.length > 0">
<select ng-model="filters.age_range_id" ng-change="filterEvents()" class="form-control" ng-options="a.id as a.name for a in ageRanges">
<option value="" translate>{{ 'app.public.events_list.for_all' }}</option>
</select>
</div>
<div class="row">
<div class="col-md-3" ng-show="categories.length > 0">
<select ng-model="filters.category_id" ng-change="filterEvents()" class="form-control" ng-options="c.id as c.name for c in categories">
<option value="" translate>{{ 'app.public.events_list.all_categories' }}</option>
</select>
</div>
<div class="event-focus" ng-if="featuredEevent && (!currentUser || currentUser.role === 'member')">
<div class="col-md-3" ng-show="themes.length > 0">
<select ng-model="filters.theme_id" ng-change="filterEvents()" class="form-control" ng-options="t.id as t.name for t in themes">
<option value="" translate>{{ 'app.public.events_list.all_themes' }}</option>
</select>
</div>
<div class="col-md-3" ng-show="ageRanges.length > 0">
<select ng-model="filters.age_range_id" ng-change="filterEvents()" class="form-control" ng-options="a.id as a.name for a in ageRanges">
<option value="" translate>{{ 'app.public.events_list.for_all' }}</option>
</select>
</div>
</div>
<div class="event-focus" ng-if="featuredEevent && (!currentUser || currentUser.role === 'member')">
<event-card style="display: contents"
event="featuredEevent"
card-type="'lg'"
ui-sref="app.public.events_show({id: featuredEevent.id})">
</event-card>
</div>
<div ng-repeat="month in monthOrder">
<h1>{{monthNames[month.split(',')[0] - 1]}}, {{month.split(',')[1]}}</h1>
<div class="event-monthList">
<event-card style="display: contents"
event="featuredEevent"
card-type="'lg'"
ui-sref="app.public.events_show({id: featuredEevent.id})">
event="event"
ng-repeat="event in eventsGroupByMonth[month]"
card-type="'sm'"
ng-if="isAuthorized(['admin', 'manager']) || event.id !== featuredEevent.id"
ng-class="{'featured-event': event.id === featuredEevent.id}"
ui-sref="app.public.events_show({id: event.id})">
</event-card>
</div>
<div ng-repeat="month in monthOrder">
<h1>{{monthNames[month.split(',')[0] - 1]}}, {{month.split(',')[1]}}</h1>
</div>
<div class="event-monthList">
<event-card style="display: contents"
event="event"
ng-repeat="event in eventsGroupByMonth[month]"
card-type="'sm'"
ng-if="isAuthorized(['admin', 'manager']) || event.id !== featuredEevent.id"
ng-class="{'featured-event': event.id === featuredEevent.id}"
ui-sref="app.public.events_show({id: event.id})">
</event-card>
</div>
<div class="row">
<div class="col-lg-12 text-center m-t-md">
<a class="btn btn-warning" ng-click="loadMoreEvents()" ng-hide="noMoreResults" translate>{{ 'app.public.events_list.load_the_next_events' }}</a>
</div>
</div>
<div class="row">
<div class="col-lg-12 text-center m-t-md">
<a class="btn btn-warning" ng-click="loadMoreEvents()" ng-hide="noMoreResults" translate>{{ 'app.public.events_list.load_the_next_events' }}</a>
</div>
</div>
</section>
</section>

View File

@ -7,7 +7,7 @@
<p ng-show="object.user && currentUser.id !== object.user.id" translate translate-values="{NAME: object.user.name}">{{ 'app.shared.confirm_modify_slot_modal.do_you_want_to_change_NAME_s_booking_slot_initially_planned_at' }}</p>
<p><strong>{{object.start | amDateFormat: 'LL'}} : {{object.start | amDateFormat:'LT'}} - {{object.end | amDateFormat:'LT'}}</strong></p>
</div>
<div class="modal-footer">
<div class="modal-footer btn-stack">
<button class="btn btn-warning" ng-class="{'m-b':object.cancelable&&object.movable}" ng-click="ok('cancel')" ng-show="object.cancelable" translate>{{ 'app.shared.confirm_modify_slot_modal.cancel_this_reservation' }}</button>
<button class="btn btn-info" ng-click="ok('move')" ng-show="object.movable" translate>{{ 'app.shared.confirm_modify_slot_modal.i_want_to_change_date' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>

View File

@ -1,6 +1,6 @@
<div class="" id="loginModal">
<div class="modal-header">
<img ng-src="{{logoBlack.custom_asset_file_attributes.attachment_url}}" alt="{{logo.custom_asset_file_attributes.attachment}}" class="modal-logo"/>
<img ng-src="{{logoBlack.custom_asset_file_attributes.attachment_url}}" alt="" class="modal-logo"/>
<i class="fa fa-times close-modal-button" ng-click="dismiss()"></i>
<h1 translate translate-default="Login">
{{ 'app.public.common.connection' }}

View File

@ -160,7 +160,9 @@ class Invoice < PaymentDocument
# return a summary of the payment means used
def payment_means
res = []
res.push(means: :wallet, amount: wallet_amount) if paid_by_wallet?
res.push(means: :wallet, amount: wallet_amount || total) if paid_by_wallet?
return res if is_a?(Avoir)
if paid_by_card?
res.push(means: :card, amount: amount_paid)
else
@ -195,7 +197,7 @@ class Invoice < PaymentDocument
end
def paid_by_wallet?
wallet_transaction && wallet_amount.positive?
(wallet_transaction && wallet_amount.positive?) || payment_method == 'wallet'
end
def render_resource

View File

@ -35,8 +35,8 @@ class Plan < ApplicationRecord
after_create :create_machines_prices
after_create :create_spaces_prices
after_create :create_statistic_type
after_create :set_name
after_create :create_statistic_type
after_create :update_gateway_product
after_update :update_gateway_product, if: :saved_change_to_base_name?

View File

@ -71,7 +71,16 @@ class EventService
events.each do |e|
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
ActiveRecord::Base.transaction do
status = !!e.send(method)
if status
e.reservations.preload(:slots_reservations).map(&:slots_reservations).flatten.map do |slots_reservation|
SlotsReservationsService.cancel(slots_reservation)
end
end
results.push status: status, event: e # rubocop:disable Style/DoubleNegation
end
end
results
end

View File

@ -5,7 +5,7 @@ class SlotsReservationsService
class << self
def cancel(slot_reservation)
# first we mark ths slot reservation as cancelled in DB, to free a ticket
slot_reservation.update(canceled_at: Time.current)
slot_reservation.update!(canceled_at: Time.current)
# then we try to remove this reservation from ElasticSearch, to keep the statistics up-to-date
model_name = slot_reservation.reservation.reservable.class.name

View File

@ -7,7 +7,7 @@ if event.event_image
json.event_image_attributes do
json.id event.event_image.id
json.attachment_name event.event_image.attachment_identifier
json.attachment_url "#{event.event_image.attachment_url}?#{event.event_image.updated_at.to_i}"
json.attachment_url "#{event.event_image.attachment.large.url}?#{event.event_image.updated_at.to_i}"
end
end
json.event_files_attributes event.event_files do |f|

View File

@ -6,7 +6,7 @@ if machine.machine_image
json.machine_image_attributes do
json.id machine.machine_image.id
json.attachment_name machine.machine_image.attachment_identifier
json.attachment_url machine.machine_image.attachment.url
json.attachment_url machine.machine_image.attachment.large.url
end
end

View File

@ -5,7 +5,7 @@ if space.space_image
json.space_image_attributes do
json.id space.space_image.id
json.attachment_name space.space_image.attachment_identifier
json.attachment_url space.space_image.attachment.url
json.attachment_url space.space_image.attachment.large.url
end
end

View File

@ -7,7 +7,7 @@ if training.training_image
json.training_image_attributes do
json.id training.training_image.id
json.attachment_name training.training_image.attachment_identifier
json.attachment_url training.training_image.attachment.url
json.attachment_url training.training_image.attachment.large.url
end
end

View File

@ -75,10 +75,10 @@ es:
associated_machines: "Máquinas asociadas"
associated_machines_help: "Si asocia una máquina a esta formación, los miembros deberán superar con éxito esta formación antes de poder reservar la máquina."
default_seats: "Número predeterminado de asientos"
public_page: "Mostrar en la lista de formaciones"
public_help: "Si esta opción no está seleccionada, la formación no aparecerá en la lista de formaciones."
public_page: "Mostrar en la lista de capacitaciones"
public_help: "Si esta opción no está seleccionada, la capacitación no aparecerá en la lista de capacitaciones."
disable_training: "Desactivar la formación"
disabled_help: "Si está desactivada, la formación no se podrá reservar y no aparecerá por defecto en la lista de formaciones."
disabled_help: "Si está desactivada, la capacitación no se podrá reservar y no aparecerá por defecto en la lista de capacitaciones."
automatic_cancellation: "Cancelación automática"
automatic_cancellation_info: "Si edita condiciones específicas aquí, las condiciones generales de cancelación dejarán de tenerse en cuenta. Se le notificará si se cancela una sesión. Los abonos y reembolsos serán automáticos si el monedero está activado. En caso contrario, tendrás que hacerlo manualmente."
automatic_cancellation_switch: "Activar la cancelación automática para esta formación"
@ -337,7 +337,7 @@ es:
select_none: "No"
manage_machines: "Haga clic aquí para añadir o eliminar máquinas."
manage_spaces: "Haga clic aquí para añadir o eliminar espacios."
manage_trainings: "Haga clic aquí para añadir o eliminar formaciones."
manage_trainings: "Haga clic aquí para añadir o eliminar capacitaciones."
number_of_tickets: "Número de tickets: "
adjust_the_opening_hours: "Ajustar el horario de apertura"
to_time: "a" #e.g. from 18:00 to 21:00
@ -372,7 +372,7 @@ es:
min_slot_duration: "Debe especificar una duración válida para las franjas horarias."
export_is_running_you_ll_be_notified_when_its_ready: "La exportación se está ejecutando. Se le notificará cuando esté listo."
actions: "Acciones"
block_reservations: "Reservas de bloques"
block_reservations: "Bloquear las reservas"
do_you_really_want_to_block_this_slot: "¿Realmente desea bloquear nuevas reservas en esta franja horaria? Se volverá invisible para los usuarios."
locking_success: "Franja horaria correctamente bloqueada, ya no aparecerá en el calendario del usuario"
locking_failed: "Ocurrió un error. El bloqueo de la franja horaria ha fallado"
@ -401,7 +401,7 @@ es:
minutes: "minutos"
deleted_user: "Usuario suprimido"
select_type: "Seleccione un tipo para continuar"
no_modules_available: "No hay ningún módulo reservable disponible. Habilite al menos un módulo (máquinas, espacios o formaciones) en la sección Personalización."
no_modules_available: "No hay ningún módulo reservable disponible. Habilite al menos un módulo (máquinas, espacios o capacitaciones) en la sección Personalización."
#import external iCal calendar
icalendar:
icalendar_import: "Importar iCalendar"
@ -489,7 +489,7 @@ es:
#track and monitor the trainings
trainings:
trainings_monitoring: "Seguimiento de la formación"
all_trainings: "Todas las formaciones"
all_trainings: "Todas las capacitaciones"
add_a_new_training: "Añadir una nueva formación"
name: "Nombre"
associated_machines: "Máquinas asociadas"
@ -533,9 +533,9 @@ es:
add_a_new_training: "Añadir una nueva formación"
trainings_settings:
title: "Configuración"
automatic_cancellation: "Cancelación automática de formaciones"
automatic_cancellation: "Cancelación automática de capacitaciones"
automatic_cancellation_info: "Se requiere un número mínimo de participantes para mantener una sesión. Se le notificará si se cancela una sesión. Los abonos y reembolsos serán automáticos si el monedero está habilitado. De lo contrario, tendrá que hacerlo manualmente."
automatic_cancellation_switch: "Activar la cancelación automática de todos las formaciones"
automatic_cancellation_switch: "Activar la cancelación automática de todos las capacitaciones"
automatic_cancellation_threshold: "Número mínimo de inscripciones para mantener una sesión"
must_be_positive: "Debe especificar un número superior o igual a 0"
automatic_cancellation_deadline: "Fecha límite, en horas antes de la cancelación automática"
@ -549,7 +549,7 @@ es:
validation_rule_switch: "Activar la regla de validación"
validation_rule_period: "Límite de tiempo en meses"
generic_text_block: "Bloque de texto editorial"
generic_text_block_info: "Muestra un bloque editorial encima de la lista de formaciones visibles para los miembros."
generic_text_block_info: "Muestra un bloque editorial encima de la lista de capacitaciones visibles para los miembros."
generic_text_block_switch: "Mostrar bloque editorial"
cta_switch: "Mostrar un botón"
cta_label: "Etiqueta del botón"
@ -1266,7 +1266,7 @@ es:
#members bulk import
members_import:
import_members: "Importar miembros"
info: "Puede cargar un archivo CSV para crear nuevos miembros o actualizar los existentes. Su archivo debe utilizar los identificadores siguientes para especificar el grupo, las formaciones y las etiquetas de los miembros."
info: "Puede cargar un archivo CSV para crear nuevos miembros o actualizar los existentes. Su archivo debe utilizar los identificadores siguientes para especificar el grupo, las capacitaciones y las etiquetas de los miembros."
required_fields: "Su fichero debe contener, al menos, los siguientes datos para cada usuario a crear: email, nombre, apellidos y grupo. Si la contraseña está vacía, se generará. En las actualizaciones, los campos vacíos se mantendrán tal cual."
about_example_html: "Consulte el archivo de ejemplo proporcionado para generar un archivo CSV correcto.<br>Este ejemplo:<ol><li>creará un nuevo miembro (Jean Dupont) con una contraseña generada</li><li>actualizará la contraseña de un miembro existente (ID 43) con la nueva contraseña generada.</li></ol><br>Tenga cuidado de utilizar la codificación <strong>Unicode UTF-8</strong>."
groups: "Grupos"
@ -1645,8 +1645,8 @@ es:
customize_information_messages: "Personalizar mensajes de información"
message_of_the_machine_booking_page: "Mensaje de la página de reserva de la máquina:"
type_the_message_content: "Escriba el contenido del mensaje"
warning_message_of_the_training_booking_page: "Mensaje de advertencia de la pagina de reservación de las formaciones:"
information_message_of_the_training_reservation_page: "Mensaje de información en la página de reservación de las formaciones:"
warning_message_of_the_training_booking_page: "Mensaje de advertencia de la pagina de reservación de las capacitaciones:"
information_message_of_the_training_reservation_page: "Mensaje de información en la página de reservación de las capacitaciones:"
message_of_the_subscriptions_page: "Mensaje de advertencia de la página de reserva de formación:"
message_of_the_events_page: "Mensaje de la página de eventos:"
message_of_the_spaces_page: "Mensaje de la página de espacios:"
@ -1797,7 +1797,7 @@ es:
enable_plans: "Activar los planes"
plans_module: "módulo de planes"
trainings: "Formaciones"
trainings_info_html: "<p>Las formaciones están totalmente integradas en la agenda del gestor de Fab. Si la activas, tus miembros podrán reservar y pagar formaciones.</p><p>Las formaciones permiten evitar que los miembros reserven algunas máquinas si no han realizado el curso previo.</p>"
trainings_info_html: "<p>Las capacitaciones están totalmente integradas en la agenda del gestor de Fab. Si la activas, tus miembros podrán reservar y pagar capacitaciones.</p><p>Las capacitaciones permiten evitar que los miembros reserven algunas máquinas si no han realizado el curso previo.</p>"
enable_trainings: "Activar la formación"
trainings_module: "módulo de formación"
store: "Tienda"
@ -1828,7 +1828,7 @@ es:
recaptcha_site_key: "Clave del sitio reCAPTCHA"
recaptcha_secret_key: "Clave secreta de reCAPTCHA"
feature_tour_display: "activar la visita guiada"
email_from: "dirección del expedidor"
email_from: "dirección del remitente"
disqus_shortname: "Nombre corto de Disqus"
COUNT_items_removed: "{COUNT, plural, one {}=1{Un elemento eliminado} other{{COUNT} elementos eliminados}}"
item_added: "Un elemento añadido"
@ -1916,7 +1916,7 @@ es:
notifications: "Notificaciones"
email: "Email"
email_info: "La dirección de email desde la que se enviarán las notificaciones. Puede utilizar una dirección inexistente (como noreply@...) o una dirección existente si desea permitir a sus miembros responder a las notificaciones que reciban."
email_from: "Dirección del expedidor"
email_from: "Dirección del remitente"
wallet: "Cartera"
wallet_info_html: "<p>La cartera virtual permite asignar una suma de dinero a los usuarios. Luego, pueden gastar este dinero como deseen, en Fab-manager.</p><p>los miembros no pueden acreditar su cartera a sí mismos, es un privilegio de los gestores y administradores.</p>"
enable_wallet: "Activar cartera"
@ -2099,12 +2099,12 @@ es:
trainings:
welcome:
title: "Formaciones"
content: "Aquí puede crear, modificar y eliminar formaciones. También es el lugar donde puede validar los cursos de formación seguidos por sus miembros."
content: "Aquí puede crear, modificar y eliminar capacitaciones. También es el lugar donde puede validar los cursos de capacitación seguidos por sus miembros."
welcome_manager:
title: "Formaciones"
content: "Este es el lugar donde puedes ver las formaciones y sus asociaciones con las máquinas. También es el lugar donde puede validar las formaciones seguidas por sus miembros."
content: "Este es el lugar donde puedes ver las capacitaciones y sus asociaciones con las máquinas. También es el lugar donde puede validar las capacitaciones seguidas por sus miembros."
trainings:
title: "Administrar formaciones"
title: "Administrar capacitaciones"
content: "<p>A cada formación se le asocia un número de plazas por defecto. Sin embargo, el número de plazas reales puede modificarse para cada sesión.</p><p>Las sesiones de formación se programan desde la pestaña del administrador \"Calendario\".</p><p>Además, una formación puede asociarse a una o varias máquinas. Además, una formación puede estar asociada a una o varias máquinas.</p>"
filter:
title: "Filtro"

View File

@ -119,9 +119,9 @@ es:
collaborators: "Colaboradores"
#dashboard: my trainings
trainings:
your_next_trainings: "Sus próximas formaciones"
your_previous_trainings: "Sus formaciones anteriores"
your_approved_trainings: "Sus formaciones aprobadas"
your_next_trainings: "Sus próximas capacitaciones"
your_previous_trainings: "Sus capacitaciones anteriores"
your_approved_trainings: "Sus capacitaciones aprobadas"
no_trainings: "Ninguna formación"
your_training_credits: "Tus créditos de entrenamiento"
subscribe_for_credits: "Suscríbete para beneficiarte de entrenamientos gratuitos"
@ -264,7 +264,7 @@ es:
trainings_reserve:
trainings_planning: "Plan de la formación"
planning_of: "Plan de " #eg. Planning of 3d printer training
all_trainings: "Todas las formaciones"
all_trainings: "Todas las capacitaciones"
cancel_my_selection: "Cancelar mi selección"
i_change: "Cambio"
i_shift: "Cambio"

View File

@ -352,6 +352,7 @@ de:
you_can_shift_this_reservation_on_the_following_slots: "Sie können diese Reservierung auf die folgenden Slots verschieben:"
confirmation_required: "Bestätigung erforderlich"
do_you_really_want_to_delete_this_event: "Möchten Sie diese Veranstaltung wirklich löschen?"
all_reservations_for_this_event_will_be_canceled: All reservations for this event will be canceled.
delete_recurring_event: "Sie sind dabei, eine wiederkehrende Veranstaltung zu löschen. Was möchten Sie tun?"
delete_this_event: "Nur diese Veranstaltung"
delete_this_and_next: "Diese Veranstaltung sowie die folgenden"

View File

@ -352,6 +352,7 @@ en:
you_can_shift_this_reservation_on_the_following_slots: "You can shift this reservation on the following slots:"
confirmation_required: "Confirmation required"
do_you_really_want_to_delete_this_event: "Do you really want to delete this event?"
all_reservations_for_this_event_will_be_canceled: All reservations for this event will be canceled.
delete_recurring_event: "You're about to delete a periodic event. What do you want to do?"
delete_this_event: "Only this event"
delete_this_and_next: "This event and the following"

View File

@ -13,12 +13,12 @@ es:
decline: "Rechazar"
#dashboard sections
dashboard: "Panel"
my_profile: "My Perfil"
my_profile: "Mi Perfil"
my_children: "Mis hijos"
my_settings: "Mis ajustes"
my_supporting_documents_files: "Mis documentos justificativos"
my_projects: "Mis proyectos"
my_trainings: "Mis formaciones"
my_trainings: "Mis capacitaciones"
my_reservations: "Mis reservas"
my_events: "Mis eventos"
my_invoices: "Mis facturas"
@ -39,7 +39,7 @@ es:
#left menu (public)
home: "Menú principal"
reserve_a_machine: "Reservar una máquina"
trainings_registrations: "Registro de formaciones"
trainings_registrations: "Registro de capacitaciones"
events_registrations: "Registro de eventos"
reserve_a_space: "Reservar un espacio"
projects_gallery: "Galería de proyectos"
@ -259,7 +259,7 @@ es:
#list of trainings
trainings_list:
book: "Reservar"
the_trainings: "Lista de formaciones"
the_trainings: "Lista de capacitaciones"
#details of a training
training_show:
book_this_training: "Reservar la formación"
@ -352,6 +352,7 @@ es:
you_can_shift_this_reservation_on_the_following_slots: "Puede cambiar la reserva en los siguientes campos:"
confirmation_required: "Confirmación requerida"
do_you_really_want_to_delete_this_event: "¿Realmente desea eliminar este evento?"
all_reservations_for_this_event_will_be_canceled: Todas las reservas de este evento serán canceladas.
delete_recurring_event: "Estás a punto de eliminar un evento periódico. ¿Qué quieres hacer?"
delete_this_event: "Solo este evento"
delete_this_and_next: "Este evento y lo siguiente"
@ -376,7 +377,7 @@ es:
#public calendar
calendar:
calendar: "Calendario"
show_unavailables: "Mostrar campos inválidos"
show_unavailables: "Mostrar las franjas horarias indisponibles"
filter_calendar: "Filtrar calendario"
trainings: "Formaciones"
machines: "Máquinas"
@ -537,7 +538,7 @@ es:
content: "<p>Esta página le permitirá consultar la lista de todas las máquinas y reservar una franja horaria en nombre de un miembro.</p><p>Una máquina puede ser, por ejemplo, una impresora 3D.</p><p>Los miembros también pueden acceder a esta página y reservar ellos mismos una máquina, si está habilitado el pago con tarjeta de crédito o si algunos precios son iguales a 0.</p>"
trainings:
title: "Formaciones"
content: "<p>Esta página le permitirá consultar la lista de todas las sesiones de formación e inscribir a un miembro en una sesión de formación.</p><p>Las formaciones pueden establecerse como requisitos previos antes de permitir la reserva de determinadas máquinas.</p><p>Los miembros también pueden acceder a esta página e inscribirse ellos mismos en una sesión de formación, si está habilitado el pago con tarjeta de crédito o si algunos precios son iguales a 0.</p>"
content: "<p>Esta página le permitirá consultar la lista de todas las sesiones de capacitación e inscribir a un miembro en una sesión de capacitación.</p><p>Las capacitaciones pueden establecerse como requisitos previos antes de permitir la reserva de determinadas máquinas.</p><p>Los miembros también pueden acceder a esta página e inscribirse ellos mismos en una sesión de capacitación, si está habilitado el pago con tarjeta de crédito o si algunos precios son iguales a 0.</p>"
spaces:
title: "Espacios"
content: "<p>Esta página le permitirá consultar la lista de todos los espacios disponibles y reservar una plaza en una franja horaria, en nombre de un miembro.</p><p>Un espacio puede ser, por ejemplo, un taller de carpintería o una sala de reuniones.</p><p>Su particularidad es que pueden ser reservados por varias personas al mismo tiempo.</p><p>Los miembros también pueden acceder a esta página y reservar ellos mismos una máquina, si está habilitado el pago con tarjeta de crédito o si algunos precios son iguales a 0.</p>"

View File

@ -352,6 +352,7 @@ fr:
you_can_shift_this_reservation_on_the_following_slots: "Vous pouvez déplacer cette réservation sur les créneaux suivants :"
confirmation_required: "Confirmation requise"
do_you_really_want_to_delete_this_event: "Voulez-vous vraiment supprimer cet événement ?"
all_reservations_for_this_event_will_be_canceled: Toutes les réservations de cet événement seront annulées.
delete_recurring_event: "Vous êtes sur le point de supprimer un événement périodique. Que voulez-vous supprimer ?"
delete_this_event: "Uniquement cet événement"
delete_this_and_next: "Cet événement et tous les suivants"

View File

@ -352,6 +352,7 @@ it:
you_can_shift_this_reservation_on_the_following_slots: "È possibile spostare questa prenotazione sui seguenti slot:"
confirmation_required: "Conferma richiesta"
do_you_really_want_to_delete_this_event: "Volete davvero cancellare questo evento?"
all_reservations_for_this_event_will_be_canceled: All reservations for this event will be canceled.
delete_recurring_event: "Stai per eliminare un evento multiplo. Cosa vuoi fare?"
delete_this_event: "Solo questo evento"
delete_this_and_next: "Questo evento e il successivo"

View File

@ -352,6 +352,7 @@
you_can_shift_this_reservation_on_the_following_slots: "Du kan flytte denne reservasjonen til følgende tidsluke:"
confirmation_required: "Bekreftelse påkrevd"
do_you_really_want_to_delete_this_event: "Vil du virkelig slette denne hendelsen?"
all_reservations_for_this_event_will_be_canceled: All reservations for this event will be canceled.
delete_recurring_event: "Du er ferd med å oppdatere en gjentakende hendelse. Hva vil du gjøre?"
delete_this_event: "Bare denne hendelsen"
delete_this_and_next: "Denne hendelsen og de følgende"

View File

@ -352,6 +352,7 @@ pt:
you_can_shift_this_reservation_on_the_following_slots: "Você pode alterar essa reserva nos campos a seguir:"
confirmation_required: "Confirmação obrigatória"
do_you_really_want_to_delete_this_event: "Vocêrealmente deseja remover este evento?"
all_reservations_for_this_event_will_be_canceled: All reservations for this event will be canceled.
delete_recurring_event: "Você está prestes a excluir um evento periódico. O que você deseja fazer?"
delete_this_event: "Apenas este evento"
delete_this_and_next: "Este evento e os seguintes"

View File

@ -352,6 +352,7 @@ zu:
you_can_shift_this_reservation_on_the_following_slots: "crwdns28338:0crwdne28338:0"
confirmation_required: "crwdns28340:0crwdne28340:0"
do_you_really_want_to_delete_this_event: "crwdns28342:0crwdne28342:0"
all_reservations_for_this_event_will_be_canceled: crwdns38132:0crwdne38132:0
delete_recurring_event: "crwdns28344:0crwdne28344:0"
delete_this_event: "crwdns28346:0crwdne28346:0"
delete_this_and_next: "crwdns28348:0crwdne28348:0"

View File

@ -540,7 +540,7 @@ es:
stock_external: "Stock público"
calendar:
calendar: "Agenda"
show_unavailables: "Mostrar todas las franjas horarias"
show_unavailables: "Mostrar las franjas horarias llenas"
filter_calendar: "Filtrar calendario"
trainings: "Formaciones"
machines: "Máquinas"

View File

@ -383,7 +383,7 @@ es:
statistics_machine: "de estadísticas sobre slots de máquina"
statistics_project: "de estadísticas sobre proyectos"
statistics_subscription: "de estadísticas de suscripción"
statistics_training: "de estadísticas de formaciones"
statistics_training: "de estadísticas de capacitaciones"
statistics_space: "de estadísticas sobre espacios"
statistics_order: "de estadísticas sobre los pedidos de la tienda"
users_members: "de la lista de miembros"
@ -672,7 +672,7 @@ es:
recaptcha_site_key: "clave del sitio reCAPTCHA"
recaptcha_secret_key: "clave secreta de reCAPTCHA"
feature_tour_display: "Modo de visualización de la visita guiada"
email_from: "Dirección del expedidor"
email_from: "Dirección del remitente"
disqus_shortname: "Nombre corto de Disqus"
allowed_cad_extensions: "Extensiones de archivos CAD permitidas"
allowed_cad_mime_types: "Archivos CAD permitidos tipos MIME"
@ -729,11 +729,11 @@ es:
external_id: "identificador externo"
prevent_invoices_zero: "impedir la creación de facturas a 0"
invoice_VAT-name: "Nombre IVA"
trainings_auto_cancel: "Cancelación automática de formaciones"
trainings_auto_cancel: "Cancelación automática de capacitaciones"
trainings_auto_cancel_threshold: "Mínimo de participantes para la cancelación automática"
trainings_auto_cancel_deadline: "Plazo de cancelación automática"
trainings_authorization_validity: "Período de validez de las formaciones"
trainings_authorization_validity_duration: "Duración del periodo de validez de las formaciones"
trainings_authorization_validity: "Período de validez de las capacitaciones"
trainings_authorization_validity_duration: "Duración del periodo de validez de las capacitaciones"
trainings_invalidation_rule: "Formaciones invalidación automática"
trainings_invalidation_rule_period: "Periodo de gracia antes de invalidar una formación"
projects_list_member_filter_presence: "Presencia del filtro de miembros en la lista de proyectos"

View File

@ -15,7 +15,7 @@ es:
account_name: "Nombre de usuario:"
password: "Contraseña:"
temporary_password: "Esta es una contraseña temporal, puede modificarla en la pagina «Mi cuenta»."
keep_advantages: "Con esta cuenta, guarda todas las ventajas relacionadas con su perfil de usuario de Fab Lab (formaciones, planes de suscripción)."
keep_advantages: "Con esta cuenta, guarda todas las ventajas relacionadas con su perfil de usuario de Fab Lab (capacitaciones, planes de suscripción)."
to_use_platform: "Para usar el sitio web, por favor"
logon_or_login: "crea una nueva cuenta o inicia sesión haciendo clic aquí."
token_if_link_problem: "Si experimenta problemas con el enlace, puede introducir el siguiente código en el primer intento de conexión:"
@ -330,7 +330,7 @@ es:
notify_admin_objects_stripe_sync:
subject: "Sincronización de Stripe"
body:
objects_sync: "Todos los miembros, cupones, máquinas, formaciones, espacios y planes se sincronizaron correctamente en Stripe."
objects_sync: "Todos los miembros, cupones, máquinas, capacitaciones, espacios y planes se sincronizaron correctamente en Stripe."
notify_admin_order_is_paid:
subject: "Nuevo pedido"
body:

View File

@ -0,0 +1,13 @@
class FixPlanStatisticSubTypes < ActiveRecord::Migration[7.0]
def up
StatisticSubType.joins(statistic_types: :statistic_index).where(statistic_indices: { es_type_key: "subscription" }, label: nil).each do |statistic_sub_type|
plan = Plan.find_by(slug: statistic_sub_type.key)
if plan
statistic_sub_type.update_column(:label, plan.name)
end
end
end
def down
end
end

View File

@ -9203,6 +9203,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20230831103208'),
('20230901090637'),
('20230907124230'),
('20231103093436');
('20231103093436'),
('20231108094433');

View File

@ -20,7 +20,7 @@ class ProviderConfig
(@config[:providable_attributes].keys.filter { |n| !n.start_with?('client__') && n != 'profile_url' }.map do |n|
val = @config[:providable_attributes][n]
val.join(' ') if n == 'scope'
val&.join(' ') if n == 'scope'
[n, val]
end).push(
['client_options', @config[:providable_attributes].keys.filter { |n| n.start_with?('client__') }.to_h do |n|

View File

@ -1,6 +1,6 @@
{
"name": "fab-manager",
"version": "6.3.0",
"version": "6.3.1",
"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",

View File

@ -5,6 +5,8 @@ require 'test_helper'
module Events; end
class Events::DeleteTest < ActionDispatch::IntegrationTest
include ActionMailer::TestHelper
setup do
admin = User.with_role(:admin).first
login_as(admin, scope: :user)
@ -55,7 +57,10 @@ class Events::DeleteTest < ActionDispatch::IntegrationTest
assert_equal 201, response.status, response.body
assert_not event.destroyable?
delete "/api/events/#{event.id}?mode=single", headers: default_headers
assert_enqueued_emails 2 do
delete "/api/events/#{event.id}?mode=single", headers: default_headers
end
assert event.availability.slots_reservations.all? { |sr| sr.canceled_at }
assert_response :success
res = json_response(response.body)
assert_equal 1, res[:deleted]