mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-17 06:52:27 +01:00
Merge branch 'training' into dev
This commit is contained in:
parent
f5b435895e
commit
90142ae3bb
@ -1,7 +1,148 @@
|
||||
'use strict'
|
||||
|
||||
Application.Controllers.controller "TrainingsController", ["$scope", "$state", "$uibModal", 'Training', 'trainingsPromise', 'machinesPromise', '_t', 'growl'
|
||||
, ($scope, $state, $uibModal, Training, trainingsPromise, machinesPromise, _t, growl) ->
|
||||
### COMMON CODE ###
|
||||
|
||||
##
|
||||
# Provides a set of common callback methods to the $scope parameter. These methods are used
|
||||
# in the various trainings' admin controllers.
|
||||
#
|
||||
# Provides :
|
||||
# - $scope.submited(content)
|
||||
# - $scope.fileinputClass(v)
|
||||
#
|
||||
# Requires :
|
||||
# - $state (Ui-Router) [ 'app.admin.trainings' ]
|
||||
##
|
||||
class TrainingsController
|
||||
constructor: ($scope, $state) ->
|
||||
|
||||
##
|
||||
# For use with ngUpload (https://github.com/twilson63/ngUpload).
|
||||
# Intended to be the callback when the upload is done: any raised error will be stacked in the
|
||||
# $scope.alerts array. If everything goes fine, the user is redirected to the trainings list.
|
||||
# @param content {Object} JSON - The upload's result
|
||||
##
|
||||
$scope.submited = (content) ->
|
||||
if !content.id?
|
||||
$scope.alerts = []
|
||||
angular.forEach content, (v, k)->
|
||||
angular.forEach v, (err)->
|
||||
$scope.alerts.push
|
||||
msg: k+': '+err
|
||||
type: 'danger'
|
||||
else
|
||||
$state.go('app.admin.trainings')
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Changes the current user's view, redirecting him to the machines list
|
||||
##
|
||||
$scope.cancel = ->
|
||||
$state.go('app.admin.trainings')
|
||||
|
||||
|
||||
|
||||
##
|
||||
# For use with 'ng-class', returns the CSS class name for the uploads previews.
|
||||
# The preview may show a placeholder or the content of the file depending on the upload state.
|
||||
# @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules)
|
||||
##
|
||||
$scope.fileinputClass = (v)->
|
||||
if v
|
||||
'fileinput-exists'
|
||||
else
|
||||
'fileinput-new'
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Controller used in the training creation page (admin)
|
||||
##
|
||||
Application.Controllers.controller "NewTrainingController", [ '$scope', '$state', 'machinesPromise', 'CSRF'
|
||||
, ($scope, $state, machinesPromise, CSRF) ->
|
||||
|
||||
|
||||
|
||||
### PUBLIC SCOPE ###
|
||||
|
||||
## Form action on the following URL
|
||||
$scope.method = 'post'
|
||||
|
||||
## API URL where the form will be posted
|
||||
$scope.actionUrl = '/api/trainings/'
|
||||
|
||||
## list of machines
|
||||
$scope.machines = machinesPromise
|
||||
|
||||
|
||||
|
||||
### PRIVATE SCOPE ###
|
||||
|
||||
##
|
||||
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||
##
|
||||
initialize = ->
|
||||
CSRF.setMetaTags()
|
||||
|
||||
## Using the TrainingsController
|
||||
new TrainingsController($scope, $state)
|
||||
|
||||
|
||||
## !!! MUST BE CALLED AT THE END of the controller
|
||||
initialize()
|
||||
]
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Controller used in the training edition page (admin)
|
||||
##
|
||||
Application.Controllers.controller "EditTrainingController", [ '$scope', '$state', '$stateParams', 'trainingPromise', 'machinesPromise', 'CSRF'
|
||||
, ($scope, $state, $stateParams, trainingPromise, machinesPromise, CSRF) ->
|
||||
|
||||
|
||||
|
||||
### PUBLIC SCOPE ###
|
||||
|
||||
## Form action on the following URL
|
||||
$scope.method = 'patch'
|
||||
|
||||
## API URL where the form will be posted
|
||||
$scope.actionUrl = '/api/trainings/' + $stateParams.id
|
||||
|
||||
## Details of the training to edit (id in URL)
|
||||
$scope.training = trainingPromise
|
||||
|
||||
## list of machines
|
||||
$scope.machines = machinesPromise
|
||||
|
||||
|
||||
|
||||
### PRIVATE SCOPE ###
|
||||
|
||||
##
|
||||
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||
##
|
||||
initialize = ->
|
||||
CSRF.setMetaTags()
|
||||
|
||||
## Using the TrainingsController
|
||||
new TrainingsController($scope, $state)
|
||||
|
||||
|
||||
## !!! MUST BE CALLED AT THE END of the controller
|
||||
initialize()
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Controller used in the trainings management page, allowing admins users to see and manage the list of trainings and reservations.
|
||||
##
|
||||
Application.Controllers.controller "TrainingsAdminController", ["$scope", "$state", "$uibModal", 'Training', 'trainingsPromise', 'machinesPromise', '_t', 'growl', 'dialogs'
|
||||
, ($scope, $state, $uibModal, Training, trainingsPromise, machinesPromise, _t, growl, dialogs) ->
|
||||
|
||||
|
||||
|
||||
@ -40,35 +181,6 @@ Application.Controllers.controller "TrainingsController", ["$scope", "$state", "
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Create a new empty training object and append it to the $scope.trainings list
|
||||
##
|
||||
$scope.addTraining = ->
|
||||
$scope.inserted =
|
||||
name: ''
|
||||
machine_ids: []
|
||||
$scope.trainings.push($scope.inserted)
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Saves a new training / Update an existing training to the server (form validation callback)
|
||||
# @param data {Object} training name, associated machine(s) and default places number
|
||||
# @param id {number} training id, in case of update
|
||||
##
|
||||
$scope.saveTraining = (data, id) ->
|
||||
if id?
|
||||
Training.update {id: id},
|
||||
training: data
|
||||
else
|
||||
Training.save
|
||||
training: data
|
||||
, (resp) ->
|
||||
$scope.trainings[$scope.trainings.length-1] = resp
|
||||
console.log(resp)
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Removes the newly inserted but not saved training / Cancel the current training modification
|
||||
# @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
|
||||
@ -138,30 +250,17 @@ Application.Controllers.controller "TrainingsController", ["$scope", "$state", "
|
||||
# @param training {Object} training to delete
|
||||
##
|
||||
$scope.removeTraining = (index, training)->
|
||||
training.$delete ->
|
||||
$scope.trainings.splice(index, 1)
|
||||
growl.info(_t('training_successfully_deleted'))
|
||||
, (error)->
|
||||
growl.warning(_t('unable_to_delete_the_training_because_some_users_alredy_booked_it'))
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Open the modal to edit description of the training
|
||||
# @param training {Object} Training to edit description
|
||||
##
|
||||
$scope.openModalToSetDescription = (training)->
|
||||
$uibModal.open(
|
||||
templateUrl: "<%= asset_path 'admin/trainings/modal_edit.html' %>"
|
||||
controller: ['$scope', '$uibModalInstance', 'Training', 'growl', ($scope, $uibModalInstance, Training, growl)->
|
||||
$scope.training = training
|
||||
$scope.save = ->
|
||||
Training.update id: training.id, { training: { description: $scope.training.description } }, (training)->
|
||||
$uibModalInstance.close()
|
||||
growl.success(_t('description_was_successfully_saved'))
|
||||
return
|
||||
]
|
||||
)
|
||||
dialogs.confirm
|
||||
resolve:
|
||||
object: ->
|
||||
title: _t('confirmation_required')
|
||||
msg: _t('do_you_really_want_to_delete_this_training')
|
||||
, -> # deletion confirmed
|
||||
training.$delete ->
|
||||
$scope.trainings.splice(index, 1)
|
||||
growl.info(_t('training_successfully_deleted'))
|
||||
, (error)->
|
||||
growl.warning(_t('unable_to_delete_the_training_because_some_users_alredy_booked_it'))
|
||||
|
||||
|
||||
|
||||
|
@ -19,7 +19,7 @@ Application.Controllers.controller "MainNavController", ["$scope", "$location",
|
||||
linkIcon: 'calendar'
|
||||
}
|
||||
{
|
||||
state: 'app.logged.trainings_reserve'
|
||||
state: 'app.public.trainings_list'
|
||||
linkText: 'trainings_registrations'
|
||||
linkIcon: 'graduation-cap'
|
||||
}
|
||||
|
@ -1,13 +1,58 @@
|
||||
'use strict'
|
||||
|
||||
##
|
||||
# Public listing of the trainings
|
||||
##
|
||||
Application.Controllers.controller "TrainingsController", ['$scope', '$state', 'trainingsPromise', ($scope, $state, trainingsPromise) ->
|
||||
|
||||
## List of trainings
|
||||
$scope.trainings = trainingsPromise
|
||||
|
||||
##
|
||||
# Callback for the 'reserve' button
|
||||
##
|
||||
$scope.reserveTraining = (training, event) ->
|
||||
$state.go('app.logged.trainings_reserve', {id: training.id})
|
||||
|
||||
##
|
||||
# Callback for the 'show' button
|
||||
##
|
||||
$scope.showTraining = (training) ->
|
||||
$state.go('app.public.training_show', {id: training.id})
|
||||
]
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Public view of a specific training
|
||||
##
|
||||
Application.Controllers.controller "ShowTrainingController", ['$scope', '$state', 'trainingPromise', ($scope, $state, trainingPromise) ->
|
||||
|
||||
## Current training
|
||||
$scope.training = trainingPromise
|
||||
|
||||
##
|
||||
# Callback for the 'reserve' button
|
||||
##
|
||||
$scope.reserveTraining = (training, event) ->
|
||||
$state.go('app.logged.trainings_reserve', {id: training.id})
|
||||
|
||||
##
|
||||
# Revert view to the full list of trainings ("<-" button)
|
||||
##
|
||||
$scope.cancel = (event) ->
|
||||
$state.go('app.public.trainings_list')
|
||||
]
|
||||
|
||||
|
||||
##
|
||||
# Controller used in the training reservation agenda page.
|
||||
# This controller is very similar to the machine reservation controller with one major difference: here, ONLY ONE
|
||||
# training can be reserved during the reservation process (the shopping cart may contains only one training and a subscription).
|
||||
##
|
||||
|
||||
Application.Controllers.controller "ReserveTrainingController", ["$scope", "$state", '$stateParams', "$uibModal", 'Auth', 'dialogs', '$timeout', 'Price', 'Availability', 'Slot', 'Member', 'Setting', 'CustomAsset', '$compile', 'availabilityTrainingsPromise', 'plansPromise', 'groupsPromise', 'growl', 'settingsPromise', '_t',
|
||||
($scope, $state, $stateParams, $uibModal, Auth, dialogs, $timeout, Price, Availability, Slot, Member, Setting, CustomAsset, $compile, availabilityTrainingsPromise, plansPromise, groupsPromise, growl, settingsPromise, _t) ->
|
||||
Application.Controllers.controller "ReserveTrainingController", ["$scope", "$state", '$stateParams', '$filter', '$compile', "$uibModal", 'Auth', 'dialogs', '$timeout', 'Price', 'Availability', 'Slot', 'Member', 'Setting', 'CustomAsset', 'availabilityTrainingsPromise', 'plansPromise', 'groupsPromise', 'growl', 'settingsPromise', 'trainingPromise', '_t'
|
||||
, ($scope, $state, $stateParams, $filter, $compile, $uibModal, Auth, dialogs, $timeout, Price, Availability, Slot, Member, Setting, CustomAsset, availabilityTrainingsPromise, plansPromise, groupsPromise, growl, settingsPromise, trainingPromise, _t) ->
|
||||
|
||||
|
||||
|
||||
@ -71,6 +116,9 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
|
||||
## Once a training reservation was modified, will contains {newReservedSlot:{}, oldReservedSlot:{}}
|
||||
$scope.modifiedSlots = null
|
||||
|
||||
## Selected training unless 'all' trainings are displayed
|
||||
$scope.training = trainingPromise
|
||||
|
||||
## fullCalendar (v2) configuration
|
||||
$scope.calendarConfig =
|
||||
timezone: Fablab.timezone
|
||||
@ -125,7 +173,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
|
||||
if $scope.ctrl.member
|
||||
Member.get {id: $scope.ctrl.member.id}, (member) ->
|
||||
$scope.ctrl.member = member
|
||||
Availability.trainings {member_id: $scope.ctrl.member.id}, (trainings) ->
|
||||
Availability.trainings {trainingId: $stateParams.id, member_id: $scope.ctrl.member.id}, (trainings) ->
|
||||
$scope.calendar.fullCalendar 'removeEvents'
|
||||
$scope.eventSources.push
|
||||
events: trainings
|
||||
@ -441,7 +489,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
|
||||
##
|
||||
eventRenderCb = (event, element, view)->
|
||||
element.attr(
|
||||
'uib-popover': event.training.description
|
||||
'uib-popover': $filter('humanize')($filter('simpleText')(event.training.description), 70)
|
||||
'popover-trigger': 'mouseenter'
|
||||
)
|
||||
$compile(element)($scope)
|
||||
|
@ -115,6 +115,8 @@ Application.Filters.filter "simpleText", [ ->
|
||||
if text != undefined
|
||||
text = text.replace(/<br\s*\/?>/g, '\n')
|
||||
text.replace(/<\/?\w+[^>]*>/g, '')
|
||||
else
|
||||
""
|
||||
]
|
||||
|
||||
Application.Filters.filter "toTrusted", [ "$sce", ($sce) ->
|
||||
|
@ -363,8 +363,34 @@ angular.module('application.router', ['ui.router']).
|
||||
Translations.query(['app.admin.machines_edit', 'app.shared.machine']).$promise
|
||||
]
|
||||
# trainings
|
||||
.state 'app.public.trainings_list',
|
||||
url: '/trainings'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "trainings/index.html" %>'
|
||||
controller: 'TrainingsController'
|
||||
resolve:
|
||||
trainingsPromise: ['Training', (Training)->
|
||||
Training.query().$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query(['app.public.trainings_list']).$promise
|
||||
]
|
||||
.state 'app.public.training_show',
|
||||
url: '/trainings/:id'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "trainings/show.html" %>'
|
||||
controller: 'ShowTrainingController'
|
||||
resolve:
|
||||
trainingPromise: ['Training', '$stateParams', (Training, $stateParams)->
|
||||
Training.get({id: $stateParams.id}).$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query(['app.public.training_show']).$promise
|
||||
]
|
||||
.state 'app.logged.trainings_reserve',
|
||||
url: '/trainings/reserve'
|
||||
url: '/trainings/:id/reserve'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "trainings/reserve.html" %>'
|
||||
@ -379,8 +405,11 @@ angular.module('application.router', ['ui.router']).
|
||||
groupsPromise: ['Group', (Group)->
|
||||
Group.query().$promise
|
||||
]
|
||||
availabilityTrainingsPromise: ['Availability', (Availability)->
|
||||
Availability.trainings().$promise
|
||||
availabilityTrainingsPromise: ['Availability', '$stateParams', (Availability, $stateParams)->
|
||||
Availability.trainings({trainingId: $stateParams.id}).$promise
|
||||
]
|
||||
trainingPromise: ['Training', '$stateParams', (Training, $stateParams)->
|
||||
Training.get({id: $stateParams.id}).$promise unless $stateParams.id == 'all'
|
||||
]
|
||||
settingsPromise: ['Setting', (Setting)->
|
||||
Setting.query(names: "['booking_window_start',
|
||||
@ -511,7 +540,7 @@ angular.module('application.router', ['ui.router']).
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "admin/trainings/index.html" %>'
|
||||
controller: 'TrainingsController'
|
||||
controller: 'TrainingsAdminController'
|
||||
resolve:
|
||||
trainingsPromise: ['Training', (Training)->
|
||||
Training.query().$promise
|
||||
@ -520,9 +549,37 @@ angular.module('application.router', ['ui.router']).
|
||||
Machine.query().$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query('app.admin.trainings').$promise
|
||||
Translations.query(['app.admin.trainings', 'app.shared.trainings']).$promise
|
||||
]
|
||||
.state 'app.admin.trainings_new',
|
||||
url: '/admin/trainings/new'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "admin/trainings/new.html" %>'
|
||||
controller: 'NewTrainingController'
|
||||
resolve:
|
||||
machinesPromise: ['Machine', (Machine)->
|
||||
Machine.query().$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query(['app.admin.trainings_new', 'app.shared.trainings']).$promise
|
||||
]
|
||||
.state 'app.admin.trainings_edit',
|
||||
url: '/admin/trainings/:id/edit'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "admin/trainings/edit.html" %>'
|
||||
controller: 'EditTrainingController'
|
||||
resolve:
|
||||
trainingPromise: ['Training', '$stateParams', (Training, $stateParams)->
|
||||
Training.get(id: $stateParams.id).$promise
|
||||
]
|
||||
machinesPromise: ['Machine', (Machine)->
|
||||
Machine.query().$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query('app.shared.trainings').$promise
|
||||
]
|
||||
|
||||
# events
|
||||
.state 'app.admin.events',
|
||||
url: '/admin/events'
|
||||
|
@ -14,7 +14,8 @@ Application.Services.factory 'Availability', ["$resource", ($resource)->
|
||||
isArray: true
|
||||
trainings:
|
||||
method: 'GET'
|
||||
url: '/api/availabilities/trainings'
|
||||
url: '/api/availabilities/trainings/:trainingId'
|
||||
params: {trainingId: "@trainingId"}
|
||||
isArray: true
|
||||
update:
|
||||
method: 'PUT'
|
||||
|
@ -23,7 +23,7 @@
|
||||
<ng-include src="'<%= asset_path 'admin/plans/_form.html' %>'"></ng-include>
|
||||
|
||||
<div class="panel-footer no-padder">
|
||||
<input type="submit" value="Enregistrer" class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c" ng-disabled="planForm.$invalid || !partnerIsValid()"/>
|
||||
<input type="submit" value="{{ 'save' | translate }}" class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c" ng-disabled="planForm.$invalid || !partnerIsValid()"/>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
104
app/assets/templates/admin/trainings/_form.html.erb
Normal file
104
app/assets/templates/admin/trainings/_form.html.erb
Normal file
@ -0,0 +1,104 @@
|
||||
<form role="form"
|
||||
name="trainingForm"
|
||||
class="form-horizontal"
|
||||
ng-attr-action="{{ actionUrl }}"
|
||||
ng-upload="submited(content)"
|
||||
upload-options-enable-rails-csrf="true"
|
||||
unsaved-warning-form
|
||||
novalidate>
|
||||
|
||||
<input name="_method" type="hidden" ng-value="method">
|
||||
|
||||
<section class="panel panel-default bg-light m-lg">
|
||||
<div class="panel-body m-r">
|
||||
|
||||
<uib-alert ng-repeat="alert in alerts" type="{{alert.type}}" close="closeAlert($index)">{{alert.msg}}</uib-alert>
|
||||
|
||||
<div class="form-group m-b-lg" ng-class="{'has-error': trainingForm['training[name]'].$dirty && trainingForm['training[name]'].$invalid}">
|
||||
<label for="name" class="col-sm-2 control-label">{{ 'name' | translate }} *</label>
|
||||
<div class="col-sm-4">
|
||||
<input name="training[name]"
|
||||
ng-model="training.name"
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="training_name"
|
||||
placeholder="{{'name' | translate}}"
|
||||
required/>
|
||||
<span class="help-block" ng-show="trainingForm['training[name]'].$dirty && trainingForm['training[name]'].$error.required" translate>{{ 'name_is_required' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group m-b-lg">
|
||||
<label for="training_image" class="col-sm-2 control-label">{{ 'illustration' | translate }} *</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="fileinput" data-provides="fileinput" ng-class="fileinputClass(training.training_image)">
|
||||
<div class="fileinput-new thumbnail" style="width: 334px; height: 250px;">
|
||||
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:/font:FontAwesome/icon" bs-holder ng-if="!training.training_image">
|
||||
</div>
|
||||
<div class="fileinput-preview fileinput-exists thumbnail" style="max-width: 334px;">
|
||||
<img ng-src="{{ training.training_image }}" alt="" />
|
||||
</div>
|
||||
<div>
|
||||
<span class="btn btn-default btn-file">
|
||||
<span class="fileinput-new">{{ 'add_an_illustration' | translate }} <i class="fa fa-upload fa-fw"></i></span>
|
||||
<span class="fileinput-exists" translate>{{ 'change' }}</span>
|
||||
<input type="file"
|
||||
ng-model="training.training_image"
|
||||
name="training[training_image_attributes][attachment]"
|
||||
accept="image/*"
|
||||
required
|
||||
bs-jasny-fileinput>
|
||||
</span>
|
||||
<a href="#" class="btn btn-danger fileinput-exists" data-dismiss="fileinput" translate>{{ 'delete' }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group m-b-xl" ng-class="{'has-error': trainingForm['training[description]'].$dirty && trainingForm['training[description]'].$invalid}">
|
||||
<label for="training_description" class="col-sm-2 control-label">{{ 'description' | translate }} *</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="hidden" name="training[description]" ng-value="training.description" />
|
||||
<summernote ng-model="training.description" id="training_description" placeholder="" config="summernoteOpts" name="training[description]" required></summernote>
|
||||
<span class="help-block" ng-show="trainingForm['training[description]'].$dirty && trainingForm['training[description]'].$error.required" translate>{{ 'description_is_required' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group m-b-lg" ng-class="{'has-error': trainingForm['training[machine_ids]'].$dirty && trainingForm['training[machine_ids]'].$invalid}">
|
||||
<label for="training_machines" class="col-sm-2 control-label">{{ 'associated_machines' | translate }}</label>
|
||||
<div class="col-sm-4">
|
||||
<ui-select multiple ng-model="training.machine_ids" class="form-control" id="training_machines">
|
||||
<ui-select-match>
|
||||
<span ng-bind="$item.name"></span>
|
||||
<input type="hidden" name="training[machine_ids][]" value="{{$item.id}}" />
|
||||
</ui-select-match>
|
||||
<ui-select-choices repeat="m.id as m in (machines | filter: $select.search)">
|
||||
<span ng-bind-html="m.name | highlight: $select.search"></span>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group m-b-lg" ng-class="{'has-error': trainingForm['training[nb_total_places]'].$dirty && trainingForm['training[nb_total_places]'].$invalid}">
|
||||
<label for="training_nb_total_places" class="col-sm-2 control-label">{{ 'number_of_tickets' | translate }}</label>
|
||||
<div class="col-sm-4">
|
||||
<input ng-model="training.nb_total_places"
|
||||
type="number"
|
||||
min="0"
|
||||
name="training[nb_total_places]"
|
||||
class="form-control"
|
||||
id="training_nb_total_places">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- ./panel-body -->
|
||||
|
||||
<div class="panel-footer no-padder">
|
||||
<input type="submit"
|
||||
value="{{ 'validate_your_training' | translate }}"
|
||||
class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c"
|
||||
ng-disabled="trainingForm.$invalid"/>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
27
app/assets/templates/admin/trainings/edit.html.erb
Normal file
27
app/assets/templates/admin/trainings/edit.html.erb
Normal file
@ -0,0 +1,27 @@
|
||||
<section class="heading b-b">
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a ng-click="cancel()"><i class="fa fa-long-arrow-left"></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
|
||||
<section class="heading-title">
|
||||
<h1>{{ training.name }}</h1>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md">
|
||||
<section class="heading-actions wrapper">
|
||||
<div class="btn btn-lg btn-block btn-default rounded m-t-xs" ng-click="cancel()" translate>{{ 'cancel' }}</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<div class="row no-gutter">
|
||||
<div class="col-sm-12 col-md-12 col-lg-9 b-r-lg nopadding">
|
||||
<ng-include src="'<%= asset_path 'admin/trainings/_form.html' %>'"></ng-include>
|
||||
</div>
|
||||
</div>
|
@ -21,11 +21,7 @@
|
||||
<div class="col-md-12">
|
||||
<uib-tabset justified="true">
|
||||
<uib-tab heading="{{ 'trainings' | translate }}">
|
||||
<button type="button" class="btn btn-warning m-t m-b" ng-click="addTraining()" translate>{{ 'add_a_new_training' }}</button>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
{{ 'beware_when_creating_a_training_its_reservation_prices_are_initialized_to_zero' | translate }}
|
||||
{{ 'dont_forget_to_change_them_before_creating_slots_for_this_training' | translate }}
|
||||
</div>
|
||||
<button type="button" class="btn btn-warning m-t m-b" ui-sref="app.admin.trainings_new" translate>{{ 'add_a_new_training' }}</button>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
@ -38,35 +34,12 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="training in trainings">
|
||||
<td>{{ training.name }}</td>
|
||||
<td>{{ showMachines(training) }}</td>
|
||||
<td>{{ training.nb_total_places }}</td>
|
||||
<td>
|
||||
<span editable-text="training.name" e-name="name" e-form="rowform" e-required>
|
||||
{{ training.name }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span editable-checklist="training.machine_ids" e-ng-options="m.id as m.name for m in machines" e-name="machine_ids" e-form="rowform">
|
||||
{{ showMachines(training) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span editable-number="training.nb_total_places" e-name="nb_total_places" e-form="rowform" e-required>
|
||||
{{ training.nb_total_places }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<form editable-form name="rowform" onbeforesave="saveTraining($data, training.id)" ng-show="rowform.$visible" class="form-buttons form-inline" shown="inserted == training">
|
||||
<button type="submit" ng-disabled="rowform.$waiting" class="btn btn-warning">
|
||||
<i class="fa fa-check"></i>
|
||||
</button>
|
||||
<button type="button" ng-disabled="rowform.$waiting" ng-click="cancelTraining(rowform, $index)" class="btn btn-default">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</form>
|
||||
<div class="buttons" ng-show="!rowform.$visible">
|
||||
<button ng-click="openModalToSetDescription(training)" class="btn btn-default">
|
||||
<i class="fa fa-comment-o"></i>
|
||||
</button>
|
||||
<button class="btn btn-default" ng-click="rowform.$show()">
|
||||
<div class="buttons">
|
||||
<button class="btn btn-default" ui-sref="app.admin.trainings_edit({id:training.id})">
|
||||
<i class="fa fa-edit"></i> {{ 'edit' | translate }}
|
||||
</button>
|
||||
<button class="btn btn-danger" ng-click="removeTraining($index, training)">
|
||||
|
35
app/assets/templates/admin/trainings/new.html.erb
Normal file
35
app/assets/templates/admin/trainings/new.html.erb
Normal file
@ -0,0 +1,35 @@
|
||||
<section class="heading b-b">
|
||||
<div class="row no-gutter">
|
||||
<div class="col-md-1 hidden-xs">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="cancel()"><i class="fa fa-long-arrow-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-md-8 b-l b-r">
|
||||
<section class="heading-title">
|
||||
<h1 translate>{{ 'add_a_new_training' }}</h1>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
<div class="row no-gutter" >
|
||||
|
||||
<div class="col-md-9 b-r nopadding">
|
||||
|
||||
<div class="alert alert-warning m-lg" role="alert">
|
||||
{{ 'beware_when_creating_a_training_its_reservation_prices_are_initialized_to_zero' | translate }}
|
||||
{{ 'dont_forget_to_change_them_before_creating_slots_for_this_training' | translate }}
|
||||
</div>
|
||||
|
||||
<ng-include src="'<%= asset_path 'admin/trainings/_form.html' %>'"></ng-include>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<!-- <button class="btn">TEST</button> -->
|
||||
</div>
|
||||
</div>
|
@ -1,4 +1,11 @@
|
||||
<form role="form" name="machineForm" class="form-horizontal" novalidate action="{{ actionUrl }}" ng-upload="submited(content)" upload-options-enable-rails-csrf="true" unsaved-warning-form>
|
||||
<form role="form"
|
||||
name="machineForm"
|
||||
class="form-horizontal"
|
||||
action="{{ actionUrl }}"
|
||||
ng-upload="submited(content)"
|
||||
upload-options-enable-rails-csrf="true"
|
||||
unsaved-warning-form
|
||||
novalidate>
|
||||
|
||||
<input name="_method" type="hidden" ng-value="method">
|
||||
|
||||
@ -10,7 +17,13 @@
|
||||
<div class="form-group m-b-lg" ng-class="{'has-error': machineForm['machine[name]'].$dirty && machineForm['machine[name]'].$invalid}">
|
||||
<label for="name" class="col-sm-2 control-label">{{ 'name' | translate }} *</label>
|
||||
<div class="col-sm-4">
|
||||
<input ng-model="machine.name" type="text" name="machine[name]" class="form-control" id="machine_name" placeholder="Nom :" required>
|
||||
<input ng-model="machine.name"
|
||||
type="text"
|
||||
name="machine[name]"
|
||||
class="form-control"
|
||||
id="machine_name"
|
||||
placeholder="{{'name' | translate}}"
|
||||
required>
|
||||
<span class="help-block" ng-show="machineForm['machine[name]'].$dirty && machineForm['machine[name]'].$error.required" translate>{{ 'name_is_required' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -46,8 +59,16 @@
|
||||
<div class="form-group m-b-xl" ng-class="{'has-error': machineForm['machine[description]'].$dirty && machineForm['machine[description]'].$invalid}">
|
||||
<label for="description" class="col-sm-2 control-label">{{ 'description' | translate }} *</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="hidden" name="machine[description]" ng-value="machine.description" />
|
||||
<summernote ng-model="machine.description" id="machine_description" placeholder="" config="summernoteOpts" name="machine[description]" required></summernote>
|
||||
<input type="hidden"
|
||||
name="machine[description]"
|
||||
ng-value="machine.description" />
|
||||
<summernote ng-model="machine.description"
|
||||
id="machine_description"
|
||||
placeholder=""
|
||||
config="summernoteOpts"
|
||||
name="machine[description]"
|
||||
required>
|
||||
</summernote>
|
||||
<span class="help-block" ng-show="machineForm['machine[description]'].$dirty && machineForm['machine[description]'].$error.required" translate>{{ 'description_is_required' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -55,8 +76,16 @@
|
||||
<div class="form-group m-b-xl" ng-class="{'has-error': machineForm['machine[spec]'].$dirty && machineForm['machine[spec]'].$invalid}">
|
||||
<label for="spec" class="col-sm-2 control-label">{{ 'technical_specifications' | translate }} *</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="hidden" name="machine[spec]" ng-value="machine.spec" />
|
||||
<summernote ng-model="machine.spec" id="machine_spec" placeholder="" config="summernoteOpts" name="machine[spec]" required></summernote>
|
||||
<input type="hidden"
|
||||
name="machine[spec]"
|
||||
ng-value="machine.spec" />
|
||||
<summernote ng-model="machine.spec"
|
||||
id="machine_spec"
|
||||
placeholder=""
|
||||
config="summernoteOpts"
|
||||
name="machine[spec]"
|
||||
required>
|
||||
</summernote>
|
||||
<span class="help-block" ng-show="machineForm['machine[spec]'].$dirty && machineForm['machine[spec]'].$error.required" translate>{{ 'technical_specifications_are_required' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -85,7 +114,10 @@
|
||||
</div> <!-- ./panel-body -->
|
||||
|
||||
<div class="panel-footer no-padder">
|
||||
<input type="submit" value="{{ 'validate_your_machine' | translate }}" class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c" ng-disabled="machineForm.$invalid"/>
|
||||
<input type="submit"
|
||||
value="{{ 'validate_your_machine' | translate }}"
|
||||
class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c"
|
||||
ng-disabled="machineForm.$invalid"/>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
|
61
app/assets/templates/trainings/index.html.erb
Normal file
61
app/assets/templates/trainings/index.html.erb
Normal file
@ -0,0 +1,61 @@
|
||||
<section class="heading b-b">
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fa fa-long-arrow-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
|
||||
<section class="heading-title">
|
||||
<h1 translate>{{ 'the_trainings' }}</h1>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="m-lg">
|
||||
|
||||
<div class="row" ng-repeat="training in (trainings.length/3 | array)">
|
||||
|
||||
<div class="col-xs-12 col-sm-6 col-md-4" ng-repeat="training in trainings.slice(3*$index, 3*$index + 3)">
|
||||
|
||||
|
||||
<div class="widget panel panel-default">
|
||||
<div class="panel-heading picture" ng-if="!training.training_image">
|
||||
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:/font:FontAwesome/icon" bs-holder class="img-responsive">
|
||||
</div>
|
||||
<div class="panel-heading picture" style="background-image:url({{training.training_image}})" ng-if="training.training_image">
|
||||
</div>
|
||||
<div class="panel-body" style="heigth:170px;">
|
||||
<h1 class="m-b">{{training.name}}</h1>
|
||||
<div ng-if="training.description">
|
||||
<p ng-bind-html="training.description | simpleText | humanize : 140 | breakFilter"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-footer no-padder">
|
||||
|
||||
<div class="text-center clearfix">
|
||||
<div class="col-sm-6 b-r no-padder">
|
||||
<div class="btn btn-default btn-block no-b padder-v red" ng-click="reserveTraining(training, $event)">
|
||||
<i class="fa fa-bookmark"></i> {{ 'book' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 no-padder">
|
||||
<div class="btn btn-default btn-block padder-v no-b red" ng-click="showTraining(training)">
|
||||
<i class="fa fa-eye"></i> {{ 'consult' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</section>
|
@ -5,11 +5,20 @@
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fa fa-long-arrow-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
|
||||
<section class="heading-title">
|
||||
<h1 translate>{{ 'trainings_planning' }}</h1>
|
||||
<h1 ng-hide="training" translate>{{ 'trainings_planning' }}</h1>
|
||||
<h1 ng-show="training"><span translate>{{ 'planning_of' }}</span> {{training.name}}</h1>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md">
|
||||
<section class="heading-actions wrapper">
|
||||
<a class="btn btn-lg btn-warning bg-white b-2x rounded m-t-xs"
|
||||
ui-sref="app.logged.trainings_reserve({id:'all'})"
|
||||
ng-show="training"
|
||||
role="button"
|
||||
translate>{{ 'all_trainings' }}</a>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -24,6 +33,10 @@
|
||||
|
||||
<div class="col-sm-12 col-md-12 col-lg-3">
|
||||
|
||||
<div class="text-center m-t">
|
||||
|
||||
</div>
|
||||
|
||||
<div ng-if="currentUser.role === 'admin'">
|
||||
<select-member></select-member>
|
||||
</div>
|
||||
|
42
app/assets/templates/trainings/show.html.erb
Normal file
42
app/assets/templates/trainings/show.html.erb
Normal file
@ -0,0 +1,42 @@
|
||||
<div>
|
||||
|
||||
<section class="heading b-b">
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="cancel($event)"><i class="fa fa-long-arrow-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-7 b-l b-r-md">
|
||||
<section class="heading-title">
|
||||
<h1>{{ training.name }}</h1>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 col-sm-12 col-md-4 b-t hide-b-md">
|
||||
<section class="heading-actions wrapper">
|
||||
<a ng-click="reserveTraining(training, $event)" class="btn btn-lg btn-warning bg-white b-2x rounded m-t-xs" translate>{{ 'book_this_training' }}</a>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="row no-gutter">
|
||||
<div class="col-sm-12 col-md-12 col-lg-8 b-r-lg">
|
||||
|
||||
<div class="article wrapper-lg" >
|
||||
|
||||
<div class="article-thumbnail" ng-if="training.training_image">
|
||||
<img ng-src="{{training.training_image}}" alt="{{training.name}}" class="img-responsive">
|
||||
</div>
|
||||
|
||||
<p class="intro" ng-bind-html="training.description | breakFilter"></p>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
@ -78,14 +78,33 @@ class API::AvailabilitiesController < API::ApiController
|
||||
@user = current_user
|
||||
end
|
||||
@slots = []
|
||||
@reservations = @user.reservations.includes(:slots).references(:slots).where("reservable_type = 'Training' AND slots.start_at > ?", Time.now)
|
||||
|
||||
# first, we get the already-made reservations
|
||||
@reservations = @user.reservations.where("reservable_type = 'Training'")
|
||||
@reservations = @reservations.where('reservable_id = :id', id: params[:training_id].to_i) if params[:training_id].is_number?
|
||||
@reservations = @reservations.joins(:slots).where('slots.start_at > ?', Time.now)
|
||||
|
||||
# what is requested?
|
||||
# 1) a single training
|
||||
if params[:training_id].is_number?
|
||||
@availabilities = Training.find(params[:training_id]).availabilities
|
||||
# 2) all trainings
|
||||
else
|
||||
@availabilities = Availability.trainings
|
||||
end
|
||||
|
||||
# who made the request?
|
||||
# 1) an admin (he can see all future availabilities)
|
||||
if @user.is_admin?
|
||||
@availabilities = Availability.includes(:tags, :slots, trainings: [:machines]).trainings.where('availabilities.start_at > ?', Time.now)
|
||||
@availabilities = @availabilities.includes(:tags, :slots, trainings: [:machines]).where('availabilities.start_at > ?', Time.now)
|
||||
# 2) an user (he cannot see availabilities further than 1 (or 3) months)
|
||||
else
|
||||
end_at = 1.month.since
|
||||
end_at = 3.months.since if can_show_slot_plus_three_months(@user)
|
||||
@availabilities = Availability.includes(:tags, :slots, trainings: [:machines]).trainings.where('availabilities.start_at > ? AND availabilities.start_at < ?', Time.now, end_at).where('availability_tags.tag_id' => @user.tag_ids.concat([nil]))
|
||||
@availabilities = @availabilities.includes(:tags, :slots, :availability_tags, trainings: [:machines]).where('availabilities.start_at > ? AND availabilities.start_at < ?', Time.now, end_at).where('availability_tags.tag_id' => @user.tag_ids.concat([nil]))
|
||||
end
|
||||
|
||||
# finally, we merge the availabilities with the reservations
|
||||
@availabilities.each do |a|
|
||||
a = verify_training_is_reserved(a, @reservations)
|
||||
end
|
||||
|
@ -35,10 +35,15 @@ class API::TrainingsController < API::ApiController
|
||||
members.each do |m|
|
||||
m.trainings << @training
|
||||
end
|
||||
|
||||
head :no_content
|
||||
else
|
||||
@training.update(training_params)
|
||||
if @training.update(training_params)
|
||||
render :show, status: :ok, location: @training
|
||||
else
|
||||
render json: @training.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
head :no_content
|
||||
end
|
||||
|
||||
def destroy
|
||||
@ -57,6 +62,6 @@ class API::TrainingsController < API::ApiController
|
||||
end
|
||||
|
||||
def training_params
|
||||
params.require(:training).permit(:id, :name, :description, :machine_ids, :plan_ids, :nb_total_places, machine_ids: [], plan_ids: [])
|
||||
params.require(:training).permit(:id, :name, :description, :machine_ids, :plan_ids, :nb_total_places, training_image_attributes: [:attachment], machine_ids: [], plan_ids: [])
|
||||
end
|
||||
end
|
||||
|
@ -2,6 +2,9 @@ class Training < ActiveRecord::Base
|
||||
extend FriendlyId
|
||||
friendly_id :name, use: :slugged
|
||||
|
||||
has_one :training_image, as: :viewable, dependent: :destroy
|
||||
accepts_nested_attributes_for :training_image, allow_destroy: true
|
||||
|
||||
has_and_belongs_to_many :machines, join_table: :trainings_machines
|
||||
|
||||
has_many :trainings_availabilities
|
||||
@ -23,8 +26,6 @@ class Training < ActiveRecord::Base
|
||||
after_update :update_statistic_subtype, if: :name_changed?
|
||||
after_destroy :remove_statistic_subtype
|
||||
|
||||
validates :description, length: { maximum: 255 }
|
||||
|
||||
def amount_by_group(group)
|
||||
trainings_pricings.where(group_id: group).first
|
||||
end
|
||||
|
4
app/models/training_image.rb
Normal file
4
app/models/training_image.rb
Normal file
@ -0,0 +1,4 @@
|
||||
|
||||
class TrainingImage < Asset
|
||||
mount_uploader :attachment, MachineImageUploader
|
||||
end
|
@ -2,12 +2,8 @@ role = (current_user and current_user.is_admin?) ? 'admin' : 'user'
|
||||
|
||||
json.cache! [@trainings, role] do
|
||||
json.array!(@trainings) do |training|
|
||||
json.id training.id
|
||||
json.name training.name
|
||||
json.description training.description
|
||||
json.machine_ids training.machine_ids
|
||||
json.nb_total_places training.nb_total_places
|
||||
|
||||
json.extract! training, :id, :name, :description, :machine_ids, :nb_total_places
|
||||
json.training_image training.training_image.attachment.large.url if training.training_image
|
||||
json.plan_ids training.plan_ids if role === 'admin'
|
||||
end
|
||||
end
|
||||
|
@ -1,5 +1,6 @@
|
||||
json.extract! @training, :id, :name, :machine_ids, :nb_total_places
|
||||
json.availabilities @training.availabilities do |a|
|
||||
json.extract! @training, :id, :name, :description, :machine_ids, :nb_total_places
|
||||
json.training_image @training.training_image.attachment.large.url if @training.training_image
|
||||
json.availabilities @training.availabilities.order('start_at DESC') do |a|
|
||||
json.id a.id
|
||||
json.start_at a.start_at.iso8601
|
||||
json.end_at a.end_at.iso8601
|
||||
|
12
config/initializers/is_number.rb
Normal file
12
config/initializers/is_number.rb
Normal file
@ -0,0 +1,12 @@
|
||||
## Helper method: will return true if the current string
|
||||
## can be parsed as a number (float or integer), false otherwise
|
||||
# exemples:
|
||||
# "2" => true
|
||||
# "4.5" => true
|
||||
# "hello" => false
|
||||
# "" => false
|
||||
class String
|
||||
def is_number?
|
||||
true if Float(self) rescue false
|
||||
end
|
||||
end
|
@ -46,11 +46,6 @@ en:
|
||||
|
||||
trainings:
|
||||
# track and monitor the trainings
|
||||
add_a_new_training: "Add a new training"
|
||||
beware_when_creating_a_training_its_reservation_prices_are_initialized_to_zero: "Beware, when creating a training, its reservation prices are initialized at zero."
|
||||
dont_forget_to_change_them_before_creating_slots_for_this_training: "Don't forget to change them before creating slots for this training."
|
||||
associated_machines: "Associated machines"
|
||||
number_of_tickets: "Number of tickets"
|
||||
training: "Training"
|
||||
year_NUMBER: "Year {{NUMBER}}" # angular interpolation
|
||||
month_of_NAME: "Month of {{NAME}}" # angular interpolation
|
||||
@ -67,6 +62,12 @@ en:
|
||||
description_was_successfully_saved: "Description was successfully saved."
|
||||
training_successfully_deleted: "Training successfully deleted."
|
||||
unable_to_delete_the_training_because_some_users_alredy_booked_it: "Unable to delete the training because some users already booked it."
|
||||
do_you_really_want_to_delete_this_training: "Do you really want to delete this training?"
|
||||
|
||||
trainings_new:
|
||||
# create a new training
|
||||
beware_when_creating_a_training_its_reservation_prices_are_initialized_to_zero: "Beware, when creating a training, its reservation prices are initialized at zero."
|
||||
dont_forget_to_change_them_before_creating_slots_for_this_training: "Don't forget to change them before creating slots for this training."
|
||||
|
||||
events:
|
||||
# courses and workshops tracking and management
|
||||
|
@ -46,11 +46,6 @@ fr:
|
||||
|
||||
trainings:
|
||||
# suivre et surveiller les formations
|
||||
add_a_new_training: "Ajouter une nouvelle formation"
|
||||
beware_when_creating_a_training_its_reservation_prices_are_initialized_to_zero: "Attention, lors de la création d'une formation, ses tarifs de réservation sont initialisés à zero."
|
||||
dont_forget_to_change_them_before_creating_slots_for_this_training: "Pensez à les modifier avant de créer des créneaux pour cette formation."
|
||||
associated_machines: "Machines associées"
|
||||
number_of_tickets: "Nombre de places"
|
||||
training: "Formation"
|
||||
year_NUMBER: "Année {{NUMBER}}" # angular interpolation
|
||||
month_of_NAME: "Mois de {{NAME}}" # angular interpolation
|
||||
@ -67,6 +62,12 @@ fr:
|
||||
description_was_successfully_saved: "La description a bien été enregistrée."
|
||||
training_successfully_deleted: "La formation a bien été supprimée."
|
||||
unable_to_delete_the_training_because_some_users_alredy_booked_it: "La formation ne peut pas être supprimée car elle a déjà été réservée par des utilisateurs."
|
||||
do_you_really_want_to_delete_this_training: "Êtes-vous sur de vouloir supprimer cette formation ?"
|
||||
|
||||
trainings_new:
|
||||
# créer une nouvelle formation
|
||||
beware_when_creating_a_training_its_reservation_prices_are_initialized_to_zero: "Attention, lors de la création d'une formation, ses tarifs de réservation sont initialisés à zero."
|
||||
dont_forget_to_change_them_before_creating_slots_for_this_training: "Pensez à les modifier avant de créer des créneaux pour cette formation."
|
||||
|
||||
events:
|
||||
# gestion et suivi des stages et ateliers
|
||||
|
@ -128,6 +128,8 @@ en:
|
||||
trainings_reserve:
|
||||
# book a training
|
||||
trainings_planning: "Trainings planning"
|
||||
planning_of: "Planning of" # followed by the training name (eg. "Planning of 3d printer training")
|
||||
all_trainings: "All trainings"
|
||||
select_a_slot_in_the_calendar: "Select a slot in the calendar"
|
||||
you_ve_just_selected_the_slot: "You've just selected the slot:"
|
||||
datetime_to_time: "{{START_DATETIME}} to {{END_TIME}}" # angular interpolation, eg: Thursday, September 4 1986 8:30 PM to 10:00 PM
|
||||
|
@ -127,7 +127,9 @@ fr:
|
||||
|
||||
trainings_reserve:
|
||||
# réserver une formation
|
||||
trainings_planning: "Planning formation"
|
||||
trainings_planning: "Planning formations"
|
||||
planning_of: "Planning de la" # suivi du nom de la formation (eg. "Planning de la formation imprimante 3d")
|
||||
all_trainings: "Toutes les formations"
|
||||
select_a_slot_in_the_calendar: "Sélectionnez un créneau dans le calendrier"
|
||||
you_ve_just_selected_the_slot: "Vous venez de sélectionner le créneau :"
|
||||
datetime_to_time: "{{START_DATETIME}} à {{END_TIME}}" # angular interpolation, eg: Thursday, September 4 1986 8:30 PM to 10:00 PM
|
||||
|
@ -161,7 +161,6 @@ en:
|
||||
# list of machines
|
||||
the_fablab_s_machines: "The FabLab's machines"
|
||||
add_a_machine: "Add a machine"
|
||||
book: "Book"
|
||||
_or_the_: " or the "
|
||||
|
||||
machines_show:
|
||||
@ -173,6 +172,14 @@ en:
|
||||
unauthorized_operation: "Unauthoried operation"
|
||||
the_machine_cant_be_deleted_because_it_is_already_reserved_by_some_users: "The machine can't be deleted because it's already reserved by some users."
|
||||
|
||||
trainings_list:
|
||||
# list of trainings
|
||||
the_trainings: "The trainings"
|
||||
|
||||
training_show:
|
||||
# details of a training
|
||||
book_this_training: "Book this training"
|
||||
|
||||
plans:
|
||||
# summary of the subscriptions
|
||||
subcriptions: "Subscriptions"
|
||||
|
@ -161,7 +161,6 @@ fr:
|
||||
# liste des machines
|
||||
the_fablab_s_machines: "Les machines du FabLab"
|
||||
add_a_machine: "Ajouter une machine"
|
||||
book: "Réserver"
|
||||
_or_the_: " ou la "
|
||||
|
||||
machines_show:
|
||||
@ -173,6 +172,16 @@ fr:
|
||||
unauthorized_operation: "Opération non autorisée"
|
||||
the_machine_cant_be_deleted_because_it_is_already_reserved_by_some_users: "La machine ne peut pas être supprimée car elle a déjà été réservée par des utilisateurs."
|
||||
|
||||
|
||||
trainings_list:
|
||||
# liste des formations
|
||||
the_trainings: "Les formations"
|
||||
|
||||
training_show:
|
||||
# détails d'une formation
|
||||
book_this_training: "Réserver cette formation"
|
||||
|
||||
|
||||
plans:
|
||||
# page récapitulative des abonnements
|
||||
subcriptions: "Les abonnements"
|
||||
|
@ -87,6 +87,11 @@ en:
|
||||
_disconnect_then_reconnect_: "disconnect then reconnect"
|
||||
_for_your_changes_to_take_effect: "for your changes to take effect."
|
||||
add_a_project: "Add a project"
|
||||
illustration: "Illustration"
|
||||
add_an_illustration: "Add an illustration."
|
||||
book: "Book"
|
||||
description_is_required: "Description is required."
|
||||
name_is_required: "Name is required."
|
||||
|
||||
messages:
|
||||
you_will_lose_any_unsaved_modification_if_you_quit_this_page: "You will lose any unsaved modification if you quit this page"
|
||||
@ -110,12 +115,10 @@ en:
|
||||
|
||||
project:
|
||||
# project edition form
|
||||
name_is_required: "Name is required."
|
||||
illustration: "Illustration"
|
||||
add_an_illustration: "Add an illustration"
|
||||
CAD_file: "CAD file"
|
||||
add_a_new_file: "Add a new file"
|
||||
description_is_required: "Description is required."
|
||||
steps: "Steps"
|
||||
step_title: "Step title"
|
||||
add_a_picture: "Add a picture"
|
||||
@ -130,10 +133,6 @@ en:
|
||||
|
||||
machine:
|
||||
# machine edition form
|
||||
name_is_required: "The name is required."
|
||||
illustration: "Illustration"
|
||||
add_an_illustration: "Add an illustration."
|
||||
description_is_required: "Description is required."
|
||||
technical_specifications_are_required: "Technical specifications are required."
|
||||
attached_files_(pdf): "Attached files (pdf)"
|
||||
attach_a_file: "Attach a file"
|
||||
@ -168,7 +167,6 @@ en:
|
||||
title_is_required: "Title is required."
|
||||
matching_visual: "Matching visual"
|
||||
choose_a_picture: "Choose a picture"
|
||||
description_is_required: "Description is required."
|
||||
attachments: "Attachments"
|
||||
add_a_new_file: "Add a new file"
|
||||
event_type: "Event type"
|
||||
@ -188,7 +186,6 @@ en:
|
||||
plan:
|
||||
# subscription plan edition form
|
||||
general_informations: "General informations"
|
||||
name_is_required: "Name is required."
|
||||
name_length_must_be_less_than_24_characters: "Name length must be less than 24 characters."
|
||||
type_is_required: "Type is required."
|
||||
group: "Group"
|
||||
@ -213,6 +210,13 @@ en:
|
||||
new_partner: "New partner"
|
||||
email_address_is_required: "Email address is required."
|
||||
|
||||
trainings:
|
||||
# training edition form
|
||||
add_a_new_training: "Add a new training"
|
||||
validate_your_training: "Validate your training"
|
||||
associated_machines: "Associated machines"
|
||||
number_of_tickets: "Number of tickets"
|
||||
|
||||
user_admin:
|
||||
# partial form to edit/create an user (admin view)
|
||||
group: "Group"
|
||||
|
@ -87,6 +87,11 @@ fr:
|
||||
_disconnect_then_reconnect_: "déconnectez-vous puis re-connectez vous"
|
||||
_for_your_changes_to_take_effect: "pour que les modifications soient prises en compte."
|
||||
add_a_project: "Ajouter un projet"
|
||||
illustration: "Visuel"
|
||||
add_an_illustration: "Ajouter un visuel"
|
||||
book: "Réserver"
|
||||
description_is_required: "La description est requise."
|
||||
name_is_required: "Le nom est requis."
|
||||
|
||||
messages:
|
||||
you_will_lose_any_unsaved_modification_if_you_quit_this_page: "Vous perdrez les modifications non enregistrées si vous quittez cette page"
|
||||
@ -110,12 +115,10 @@ fr:
|
||||
|
||||
project:
|
||||
# formulaire d'étition d'un projet
|
||||
name_is_required: "Le nom est requis."
|
||||
illustration: "Illustration"
|
||||
add_an_illustration: "Ajouter un visuel"
|
||||
CAD_file: "Fichier CAO"
|
||||
add_a_new_file: "Ajouter un nouveau fichier"
|
||||
description_is_required: "La description est requise."
|
||||
steps: "Étapes"
|
||||
step_title: "Titre de l'étape"
|
||||
add_a_picture: "Ajouter une image"
|
||||
@ -130,10 +133,6 @@ fr:
|
||||
|
||||
machine:
|
||||
# formulaire d'édition d'une machine
|
||||
name_is_required: "Le nom est requis."
|
||||
illustration: "Visuel"
|
||||
add_an_illustration: "Ajouter un visuel"
|
||||
description_is_required: "La description est requise."
|
||||
technical_specifications_are_required: "Les caractéristiques techniques sont requises."
|
||||
attached_files_(pdf): "Pièces jointes (pdf)"
|
||||
attach_a_file: "Joindre un fichier"
|
||||
@ -168,7 +167,6 @@ fr:
|
||||
title_is_required: "Le titre est requis."
|
||||
matching_visual: "Visuel associé"
|
||||
choose_a_picture: "Choisir une image"
|
||||
description_is_required: "La description est requise."
|
||||
attachments: "Pièces jointes"
|
||||
add_a_new_file: "Ajouter un nouveau fichier"
|
||||
event_type: "Type d'évènement"
|
||||
@ -188,7 +186,6 @@ fr:
|
||||
plan:
|
||||
# formulaire d'édition d'une formule d'abonnement
|
||||
general_informations: "Informations générales"
|
||||
name_is_required: "Le nom est requis."
|
||||
name_length_must_be_less_than_24_characters: "Le nom doit faire moins de 24 caractères."
|
||||
type_is_required: "Le type est requis."
|
||||
group: "Groupe"
|
||||
@ -213,6 +210,13 @@ fr:
|
||||
new_partner: "Nouveau partenaire"
|
||||
email_address_is_required: "L'adresse e-mail est requise."
|
||||
|
||||
trainings:
|
||||
# formulaire d'édition d'une formation
|
||||
add_a_new_training: "Ajouter une nouvelle formation"
|
||||
validate_your_training: "Valider votre formation"
|
||||
associated_machines: "Machines associées"
|
||||
number_of_tickets: "Nombre de places"
|
||||
|
||||
user_admin:
|
||||
# formulaire partiel d'édition/création utilisateur (vue admin)
|
||||
group: "Groupe"
|
||||
|
@ -64,7 +64,7 @@ Rails.application.routes.draw do
|
||||
|
||||
resources :availabilities do
|
||||
get 'machines/:machine_id', action: 'machine', on: :collection
|
||||
get 'trainings', on: :collection
|
||||
get 'trainings/:training_id', action: 'trainings', on: :collection
|
||||
get 'reservations', on: :member
|
||||
end
|
||||
|
||||
|
10
db/seeds.rb
10
db/seeds.rb
@ -131,11 +131,11 @@ end
|
||||
|
||||
if Training.count == 0
|
||||
Training.create!([
|
||||
{name: "Formation Imprimante 3D"},
|
||||
{name: "Formation Laser / Vinyle"},
|
||||
{name: "Formation Petite fraiseuse numerique"},
|
||||
{name: "Formation Shopbot Grande Fraiseuse"},
|
||||
{name: "Formation logiciel 2D"}
|
||||
{name: "Formation Imprimante 3D", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."},
|
||||
{name: "Formation Laser / Vinyle", description: "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."},
|
||||
{name: "Formation Petite fraiseuse numerique", description: "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur."},
|
||||
{name: "Formation Shopbot Grande Fraiseuse", description: "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."},
|
||||
{name: "Formation logiciel 2D", description: "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo."}
|
||||
])
|
||||
|
||||
TrainingsPricing.all.each do |p|
|
||||
|
@ -25,7 +25,7 @@
|
||||
"tag": "0.14.3",
|
||||
"commit": "306d1a30b4a8e8144741bb9c0126331ac884126a"
|
||||
},
|
||||
"_source": "git://github.com/angular-ui/bootstrap-bower.git",
|
||||
"_target": ">=0.13.1",
|
||||
"_source": "https://github.com/angular-ui/bootstrap-bower.git",
|
||||
"_target": "~0.14.3",
|
||||
"_originalSource": "angular-bootstrap"
|
||||
}
|
10
vendor/assets/components/ngUpload/.bower.json
vendored
10
vendor/assets/components/ngUpload/.bower.json
vendored
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ngUpload",
|
||||
"version": "0.5.17",
|
||||
"version": "0.5.18",
|
||||
"main": "ng-upload.js",
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
@ -20,13 +20,13 @@
|
||||
"angular": ">=1.0.4"
|
||||
},
|
||||
"homepage": "https://github.com/twilson63/ngUpload",
|
||||
"_release": "0.5.17",
|
||||
"_release": "0.5.18",
|
||||
"_resolution": {
|
||||
"type": "version",
|
||||
"tag": "v0.5.17",
|
||||
"commit": "df9f3edfdbcd1ca6d3f365ff85e32de229df3af1"
|
||||
"tag": "v0.5.18",
|
||||
"commit": "da7fe2bb94eb6adb2cd26ab4f6f979aa020baf9c"
|
||||
},
|
||||
"_source": "git://github.com/twilson63/ngUpload.git",
|
||||
"_source": "https://github.com/twilson63/ngUpload.git",
|
||||
"_target": ">=0.5.11",
|
||||
"_originalSource": "ngUpload"
|
||||
}
|
2
vendor/assets/components/ngUpload/bower.json
vendored
2
vendor/assets/components/ngUpload/bower.json
vendored
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ngUpload",
|
||||
"version": "0.5.17",
|
||||
"version": "0.5.18",
|
||||
"main": "ng-upload.js",
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
|
@ -146,6 +146,9 @@ angular.module('ngUpload', [])
|
||||
}
|
||||
// perform check before submit file
|
||||
if (options.beforeSubmit && options.beforeSubmit(scope, {}) === false) {
|
||||
if(!scope.$$phase){
|
||||
scope.$apply();
|
||||
}
|
||||
$event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
angular.module("ngUpload",[]).directive("uploadSubmit",["$parse",function(){function n(t,e){t=angular.element(t);var a=t.parent();return e=e.toLowerCase(),a&&a[0].tagName.toLowerCase()===e?a:a?n(a,e):null}return{restrict:"AC",link:function(t,e){e.bind("click",function(t){if(t&&(t.preventDefault(),t.stopPropagation()),!e.attr("disabled")){var a=n(e,"form");a.triggerHandler("submit"),a[0].submit()}})}}}]).directive("ngUpload",["$log","$parse","$document",function(n,t,e){function a(n){var t,a=e.find("head");return angular.forEach(a.find("meta"),function(e){e.getAttribute("name")===n&&(t=e)}),angular.element(t)}var r=1;return{restrict:"AC",link:function(e,o,i){function l(n){e.$isUploading=n}function u(){s.unbind("load"),e.$$phase?l(!1):e.$apply(function(){l(!1)});try{var t,a=(s[0].contentDocument||s[0].contentWindow.document).body;try{t=angular.fromJson(a.innerText||a.textContent),e.$$phase?p(e,{content:t}):e.$apply(function(){p(e,{content:t})})}catch(r){t=a.innerHTML;var o="ng-upload: Response is not valid JSON";n.warn(o),f&&(e.$$phase?f(e,{error:o}):e.$apply(function(){f(e,{error:o})}))}}catch(o){n.warn("ng-upload: Server error"),f&&(e.$$phase?f(e,{error:o}):e.$apply(function(){f(e,{error:o})}))}}r++;var d={},p=i.ngUpload?t(i.ngUpload):null,f=i.errorCatcher?t(i.errorCatcher):null,c=i.ngUploadLoading?t(i.ngUploadLoading):null;i.hasOwnProperty("uploadOptionsConvertHidden")&&(d.convertHidden="false"!=i.uploadOptionsConvertHidden),i.hasOwnProperty("uploadOptionsEnableRailsCsrf")&&(d.enableRailsCsrf="false"!=i.uploadOptionsEnableRailsCsrf),i.hasOwnProperty("uploadOptionsBeforeSubmit")&&(d.beforeSubmit=t(i.uploadOptionsBeforeSubmit)),o.attr({target:"upload-iframe-"+r,method:"post",enctype:"multipart/form-data",encoding:"multipart/form-data"});var s=angular.element('<iframe name="upload-iframe-'+r+'" '+'border="0" width="0" height="0" '+'style="width:0px;height:0px;border:none;display:none">');if(d.enableRailsCsrf){var m=angular.element("<input />");m.attr("class","upload-csrf-token"),m.attr("type","hidden"),m.attr("name",a("csrf-param").attr("content")),m.val(a("csrf-token").attr("content")),o.append(m)}o.after(s),l(!1),o.bind("submit",function(n){var t=e[i.name];return t&&t.$invalid?(n.preventDefault(),!1):d.beforeSubmit&&d.beforeSubmit(e,{})===!1?(n.preventDefault(),!1):(s.bind("load",u),d.convertHidden&&angular.forEach(o.find("input"),function(n){var t=angular.element(n);t.attr("ng-model")&&t.attr("type")&&"hidden"==t.attr("type")&&t.attr("value",e.$eval(t.attr("ng-model")))}),e.$$phase?(c&&c(e),l(!0)):e.$apply(function(){c&&c(e),l(!0)}),void 0)})}}}]);
|
||||
angular.module("ngUpload",[]).directive("uploadSubmit",["$parse",function(){function n(t,e){t=angular.element(t);var a=t.parent();return e=e.toLowerCase(),a&&a[0].tagName.toLowerCase()===e?a:a?n(a,e):null}return{restrict:"AC",link:function(t,e){e.bind("click",function(t){if(t&&(t.preventDefault(),t.stopPropagation()),!e.attr("disabled")){var a=n(e,"form");a.triggerHandler("submit"),a[0].submit()}})}}}]).directive("ngUpload",["$log","$parse","$document",function(n,t,e){function a(n){var t,a=e.find("head");return angular.forEach(a.find("meta"),function(e){e.getAttribute("name")===n&&(t=e)}),angular.element(t)}var r=1;return{restrict:"AC",link:function(e,o,i){function l(n){e.$isUploading=n}function p(){c.unbind("load"),e.$$phase?l(!1):e.$apply(function(){l(!1)});try{var t,a=(c[0].contentDocument||c[0].contentWindow.document).body;try{t=angular.fromJson(a.innerText||a.textContent),e.$$phase?d(e,{content:t}):e.$apply(function(){d(e,{content:t})})}catch(r){t=a.innerHTML;var o="ng-upload: Response is not valid JSON";n.warn(o),f&&(e.$$phase?f(e,{error:o}):e.$apply(function(){f(e,{error:o})}))}}catch(o){n.warn("ng-upload: Server error"),f&&(e.$$phase?f(e,{error:o}):e.$apply(function(){f(e,{error:o})}))}}r++;var u={},d=i.ngUpload?t(i.ngUpload):null,f=i.errorCatcher?t(i.errorCatcher):null,s=i.ngUploadLoading?t(i.ngUploadLoading):null;i.hasOwnProperty("uploadOptionsConvertHidden")&&(u.convertHidden="false"!=i.uploadOptionsConvertHidden),i.hasOwnProperty("uploadOptionsEnableRailsCsrf")&&(u.enableRailsCsrf="false"!=i.uploadOptionsEnableRailsCsrf),i.hasOwnProperty("uploadOptionsBeforeSubmit")&&(u.beforeSubmit=t(i.uploadOptionsBeforeSubmit)),o.attr({target:"upload-iframe-"+r,method:"post",enctype:"multipart/form-data",encoding:"multipart/form-data"});var c=angular.element('<iframe name="upload-iframe-'+r+'" '+'border="0" width="0" height="0" '+'style="width:0px;height:0px;border:none;display:none">');if(u.enableRailsCsrf){var m=angular.element("<input />");m.attr("class","upload-csrf-token"),m.attr("type","hidden"),m.attr("name",a("csrf-param").attr("content")),m.val(a("csrf-token").attr("content")),o.append(m)}o.after(c),l(!1),o.bind("submit",function(n){var t=e[i.name];return t&&t.$invalid?(n.preventDefault(),!1):u.beforeSubmit&&u.beforeSubmit(e,{})===!1?(e.$$phase||e.$apply(),n.preventDefault(),!1):(c.bind("load",p),u.convertHidden&&angular.forEach(o.find("input"),function(n){var t=angular.element(n);t.attr("ng-model")&&t.attr("type")&&"hidden"==t.attr("type")&&t.attr("value",e.$eval(t.attr("ng-model")))}),e.$$phase?(s&&s(e),l(!0)):e.$apply(function(){s&&s(e),l(!0)}),void 0)})}}}]);
|
2
vendor/assets/components/ngUpload/readme.md
vendored
2
vendor/assets/components/ngUpload/readme.md
vendored
@ -133,7 +133,7 @@ angular.module('app', ['ngUpload'])
|
||||
|
||||
* Working in IE
|
||||
|
||||
In order, for ngUpload to respond correctly for IE, your server needs to return the response back as `html/text` not `application/json`
|
||||
In order, for ngUpload to respond correctly for IE, your server needs to return the response back as `text/html` not `application/json`
|
||||
|
||||
|
||||
## Directive Options
|
||||
|
Loading…
x
Reference in New Issue
Block a user