1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-17 11:54:22 +01:00

Merge branch 'training' into dev

This commit is contained in:
Sylvain 2016-07-13 18:15:14 +02:00
parent f5b435895e
commit 90142ae3bb
38 changed files with 737 additions and 172 deletions

@ -1,7 +1,148 @@
'use strict' 'use strict'
Application.Controllers.controller "TrainingsController", ["$scope", "$state", "$uibModal", 'Training', 'trainingsPromise', 'machinesPromise', '_t', 'growl' ### COMMON CODE ###
, ($scope, $state, $uibModal, Training, trainingsPromise, machinesPromise, _t, growl) ->
##
# 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 # Removes the newly inserted but not saved training / Cancel the current training modification
# @param rowform {Object} see http://vitalets.github.io/angular-xeditable/ # @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
@ -138,6 +250,12 @@ Application.Controllers.controller "TrainingsController", ["$scope", "$state", "
# @param training {Object} training to delete # @param training {Object} training to delete
## ##
$scope.removeTraining = (index, training)-> $scope.removeTraining = (index, training)->
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_delete_this_training')
, -> # deletion confirmed
training.$delete -> training.$delete ->
$scope.trainings.splice(index, 1) $scope.trainings.splice(index, 1)
growl.info(_t('training_successfully_deleted')) growl.info(_t('training_successfully_deleted'))
@ -146,25 +264,6 @@ Application.Controllers.controller "TrainingsController", ["$scope", "$state", "
##
# 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
]
)
## ##
# Takes a month number and return its localized literal name # Takes a month number and return its localized literal name
# @param {Number} from 0 to 11 # @param {Number} from 0 to 11

@ -19,7 +19,7 @@ Application.Controllers.controller "MainNavController", ["$scope", "$location",
linkIcon: 'calendar' linkIcon: 'calendar'
} }
{ {
state: 'app.logged.trainings_reserve' state: 'app.public.trainings_list'
linkText: 'trainings_registrations' linkText: 'trainings_registrations'
linkIcon: 'graduation-cap' linkIcon: 'graduation-cap'
} }

@ -1,13 +1,58 @@
'use strict' '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. # 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 # 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). # 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', 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, $uibModal, Auth, dialogs, $timeout, Price, Availability, Slot, Member, Setting, CustomAsset, $compile, availabilityTrainingsPromise, plansPromise, groupsPromise, growl, settingsPromise, _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:{}} ## Once a training reservation was modified, will contains {newReservedSlot:{}, oldReservedSlot:{}}
$scope.modifiedSlots = null $scope.modifiedSlots = null
## Selected training unless 'all' trainings are displayed
$scope.training = trainingPromise
## fullCalendar (v2) configuration ## fullCalendar (v2) configuration
$scope.calendarConfig = $scope.calendarConfig =
timezone: Fablab.timezone timezone: Fablab.timezone
@ -125,7 +173,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
if $scope.ctrl.member if $scope.ctrl.member
Member.get {id: $scope.ctrl.member.id}, (member) -> Member.get {id: $scope.ctrl.member.id}, (member) ->
$scope.ctrl.member = 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.calendar.fullCalendar 'removeEvents'
$scope.eventSources.push $scope.eventSources.push
events: trainings events: trainings
@ -441,7 +489,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
## ##
eventRenderCb = (event, element, view)-> eventRenderCb = (event, element, view)->
element.attr( element.attr(
'uib-popover': event.training.description 'uib-popover': $filter('humanize')($filter('simpleText')(event.training.description), 70)
'popover-trigger': 'mouseenter' 'popover-trigger': 'mouseenter'
) )
$compile(element)($scope) $compile(element)($scope)

@ -115,6 +115,8 @@ Application.Filters.filter "simpleText", [ ->
if text != undefined if text != undefined
text = text.replace(/<br\s*\/?>/g, '\n') text = text.replace(/<br\s*\/?>/g, '\n')
text.replace(/<\/?\w+[^>]*>/g, '') text.replace(/<\/?\w+[^>]*>/g, '')
else
""
] ]
Application.Filters.filter "toTrusted", [ "$sce", ($sce) -> 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 Translations.query(['app.admin.machines_edit', 'app.shared.machine']).$promise
] ]
# trainings # 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', .state 'app.logged.trainings_reserve',
url: '/trainings/reserve' url: '/trainings/:id/reserve'
views: views:
'main@': 'main@':
templateUrl: '<%= asset_path "trainings/reserve.html" %>' templateUrl: '<%= asset_path "trainings/reserve.html" %>'
@ -379,8 +405,11 @@ angular.module('application.router', ['ui.router']).
groupsPromise: ['Group', (Group)-> groupsPromise: ['Group', (Group)->
Group.query().$promise Group.query().$promise
] ]
availabilityTrainingsPromise: ['Availability', (Availability)-> availabilityTrainingsPromise: ['Availability', '$stateParams', (Availability, $stateParams)->
Availability.trainings().$promise Availability.trainings({trainingId: $stateParams.id}).$promise
]
trainingPromise: ['Training', '$stateParams', (Training, $stateParams)->
Training.get({id: $stateParams.id}).$promise unless $stateParams.id == 'all'
] ]
settingsPromise: ['Setting', (Setting)-> settingsPromise: ['Setting', (Setting)->
Setting.query(names: "['booking_window_start', Setting.query(names: "['booking_window_start',
@ -511,7 +540,7 @@ angular.module('application.router', ['ui.router']).
views: views:
'main@': 'main@':
templateUrl: '<%= asset_path "admin/trainings/index.html" %>' templateUrl: '<%= asset_path "admin/trainings/index.html" %>'
controller: 'TrainingsController' controller: 'TrainingsAdminController'
resolve: resolve:
trainingsPromise: ['Training', (Training)-> trainingsPromise: ['Training', (Training)->
Training.query().$promise Training.query().$promise
@ -520,9 +549,37 @@ angular.module('application.router', ['ui.router']).
Machine.query().$promise Machine.query().$promise
] ]
translations: [ 'Translations', (Translations) -> 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 # events
.state 'app.admin.events', .state 'app.admin.events',
url: '/admin/events' url: '/admin/events'

@ -14,7 +14,8 @@ Application.Services.factory 'Availability', ["$resource", ($resource)->
isArray: true isArray: true
trainings: trainings:
method: 'GET' method: 'GET'
url: '/api/availabilities/trainings' url: '/api/availabilities/trainings/:trainingId'
params: {trainingId: "@trainingId"}
isArray: true isArray: true
update: update:
method: 'PUT' method: 'PUT'

@ -23,7 +23,7 @@
<ng-include src="'<%= asset_path 'admin/plans/_form.html' %>'"></ng-include> <ng-include src="'<%= asset_path 'admin/plans/_form.html' %>'"></ng-include>
<div class="panel-footer no-padder"> <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> </div>
</form> </form>

@ -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:&#xf03e;/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>

@ -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"> <div class="col-md-12">
<uib-tabset justified="true"> <uib-tabset justified="true">
<uib-tab heading="{{ 'trainings' | translate }}"> <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> <button type="button" class="btn btn-warning m-t m-b" ui-sref="app.admin.trainings_new" 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>
<table class="table"> <table class="table">
<thead> <thead>
@ -38,35 +34,12 @@
</thead> </thead>
<tbody> <tbody>
<tr ng-repeat="training in trainings"> <tr ng-repeat="training in trainings">
<td>{{ training.name }}</td>
<td>{{ showMachines(training) }}</td>
<td>{{ training.nb_total_places }}</td>
<td> <td>
<span editable-text="training.name" e-name="name" e-form="rowform" e-required> <div class="buttons">
{{ training.name }} <button class="btn btn-default" ui-sref="app.admin.trainings_edit({id:training.id})">
</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()">
<i class="fa fa-edit"></i> {{ 'edit' | translate }} <i class="fa fa-edit"></i> {{ 'edit' | translate }}
</button> </button>
<button class="btn btn-danger" ng-click="removeTraining($index, training)"> <button class="btn btn-danger" ng-click="removeTraining($index, training)">

@ -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"> <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}"> <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> <label for="name" class="col-sm-2 control-label">{{ 'name' | translate }} *</label>
<div class="col-sm-4"> <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> <span class="help-block" ng-show="machineForm['machine[name]'].$dirty && machineForm['machine[name]'].$error.required" translate>{{ 'name_is_required' }}</span>
</div> </div>
</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}"> <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> <label for="description" class="col-sm-2 control-label">{{ 'description' | translate }} *</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="hidden" name="machine[description]" ng-value="machine.description" /> <input type="hidden"
<summernote ng-model="machine.description" id="machine_description" placeholder="" config="summernoteOpts" name="machine[description]" required></summernote> 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> <span class="help-block" ng-show="machineForm['machine[description]'].$dirty && machineForm['machine[description]'].$error.required" translate>{{ 'description_is_required' }}</span>
</div> </div>
</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}"> <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> <label for="spec" class="col-sm-2 control-label">{{ 'technical_specifications' | translate }} *</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="hidden" name="machine[spec]" ng-value="machine.spec" /> <input type="hidden"
<summernote ng-model="machine.spec" id="machine_spec" placeholder="" config="summernoteOpts" name="machine[spec]" required></summernote> 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> <span class="help-block" ng-show="machineForm['machine[spec]'].$dirty && machineForm['machine[spec]'].$error.required" translate>{{ 'technical_specifications_are_required' }}</span>
</div> </div>
</div> </div>
@ -85,7 +114,10 @@
</div> <!-- ./panel-body --> </div> <!-- ./panel-body -->
<div class="panel-footer no-padder"> <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> </div>
</section> </section>
</form> </form>

@ -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:&#xf03e;/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> <a href="#" ng-click="backPrevLocation($event)"><i class="fa fa-long-arrow-left "></i></a>
</section> </section>
</div> </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"> <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> </section>
</div> </div>
</div> </div>
</section> </section>
@ -24,6 +33,10 @@
<div class="col-sm-12 col-md-12 col-lg-3"> <div class="col-sm-12 col-md-12 col-lg-3">
<div class="text-center m-t">
</div>
<div ng-if="currentUser.role === 'admin'"> <div ng-if="currentUser.role === 'admin'">
<select-member></select-member> <select-member></select-member>
</div> </div>

@ -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 @user = current_user
end end
@slots = [] @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? 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 else
end_at = 1.month.since end_at = 1.month.since
end_at = 3.months.since if can_show_slot_plus_three_months(@user) 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 end
# finally, we merge the availabilities with the reservations
@availabilities.each do |a| @availabilities.each do |a|
a = verify_training_is_reserved(a, @reservations) a = verify_training_is_reserved(a, @reservations)
end end

@ -35,10 +35,15 @@ class API::TrainingsController < API::ApiController
members.each do |m| members.each do |m|
m.trainings << @training m.trainings << @training
end end
else
@training.update(training_params)
end
head :no_content head :no_content
else
if @training.update(training_params)
render :show, status: :ok, location: @training
else
render json: @training.errors, status: :unprocessable_entity
end
end
end end
def destroy def destroy
@ -57,6 +62,6 @@ class API::TrainingsController < API::ApiController
end end
def training_params 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
end end

@ -2,6 +2,9 @@ class Training < ActiveRecord::Base
extend FriendlyId extend FriendlyId
friendly_id :name, use: :slugged 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_and_belongs_to_many :machines, join_table: :trainings_machines
has_many :trainings_availabilities has_many :trainings_availabilities
@ -23,8 +26,6 @@ class Training < ActiveRecord::Base
after_update :update_statistic_subtype, if: :name_changed? after_update :update_statistic_subtype, if: :name_changed?
after_destroy :remove_statistic_subtype after_destroy :remove_statistic_subtype
validates :description, length: { maximum: 255 }
def amount_by_group(group) def amount_by_group(group)
trainings_pricings.where(group_id: group).first trainings_pricings.where(group_id: group).first
end end

@ -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.cache! [@trainings, role] do
json.array!(@trainings) do |training| json.array!(@trainings) do |training|
json.id training.id json.extract! training, :id, :name, :description, :machine_ids, :nb_total_places
json.name training.name json.training_image training.training_image.attachment.large.url if training.training_image
json.description training.description
json.machine_ids training.machine_ids
json.nb_total_places training.nb_total_places
json.plan_ids training.plan_ids if role === 'admin' json.plan_ids training.plan_ids if role === 'admin'
end end
end end

@ -1,5 +1,6 @@
json.extract! @training, :id, :name, :machine_ids, :nb_total_places json.extract! @training, :id, :name, :description, :machine_ids, :nb_total_places
json.availabilities @training.availabilities do |a| 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.id a.id
json.start_at a.start_at.iso8601 json.start_at a.start_at.iso8601
json.end_at a.end_at.iso8601 json.end_at a.end_at.iso8601

@ -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: trainings:
# track and monitor the 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" training: "Training"
year_NUMBER: "Year {{NUMBER}}" # angular interpolation year_NUMBER: "Year {{NUMBER}}" # angular interpolation
month_of_NAME: "Month of {{NAME}}" # angular interpolation month_of_NAME: "Month of {{NAME}}" # angular interpolation
@ -67,6 +62,12 @@ en:
description_was_successfully_saved: "Description was successfully saved." description_was_successfully_saved: "Description was successfully saved."
training_successfully_deleted: "Training successfully deleted." 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." 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: events:
# courses and workshops tracking and management # courses and workshops tracking and management

@ -46,11 +46,6 @@ fr:
trainings: trainings:
# suivre et surveiller les formations # 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" training: "Formation"
year_NUMBER: "Année {{NUMBER}}" # angular interpolation year_NUMBER: "Année {{NUMBER}}" # angular interpolation
month_of_NAME: "Mois de {{NAME}}" # 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." description_was_successfully_saved: "La description a bien été enregistrée."
training_successfully_deleted: "La formation a bien été supprimé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." 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: events:
# gestion et suivi des stages et ateliers # gestion et suivi des stages et ateliers

@ -128,6 +128,8 @@ en:
trainings_reserve: trainings_reserve:
# book a training # book a training
trainings_planning: "Trainings planning" 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" select_a_slot_in_the_calendar: "Select a slot in the calendar"
you_ve_just_selected_the_slot: "You've just selected the slot:" 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 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: trainings_reserve:
# réserver une formation # 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" 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 :" 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 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 # list of machines
the_fablab_s_machines: "The FabLab's machines" the_fablab_s_machines: "The FabLab's machines"
add_a_machine: "Add a machine" add_a_machine: "Add a machine"
book: "Book"
_or_the_: " or the " _or_the_: " or the "
machines_show: machines_show:
@ -173,6 +172,14 @@ en:
unauthorized_operation: "Unauthoried operation" 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." 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: plans:
# summary of the subscriptions # summary of the subscriptions
subcriptions: "Subscriptions" subcriptions: "Subscriptions"

@ -161,7 +161,6 @@ fr:
# liste des machines # liste des machines
the_fablab_s_machines: "Les machines du FabLab" the_fablab_s_machines: "Les machines du FabLab"
add_a_machine: "Ajouter une machine" add_a_machine: "Ajouter une machine"
book: "Réserver"
_or_the_: " ou la " _or_the_: " ou la "
machines_show: machines_show:
@ -173,6 +172,16 @@ fr:
unauthorized_operation: "Opération non autorisée" 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." 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: plans:
# page récapitulative des abonnements # page récapitulative des abonnements
subcriptions: "Les abonnements" subcriptions: "Les abonnements"

@ -87,6 +87,11 @@ en:
_disconnect_then_reconnect_: "disconnect then reconnect" _disconnect_then_reconnect_: "disconnect then reconnect"
_for_your_changes_to_take_effect: "for your changes to take effect." _for_your_changes_to_take_effect: "for your changes to take effect."
add_a_project: "Add a project" 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: messages:
you_will_lose_any_unsaved_modification_if_you_quit_this_page: "You will lose any unsaved modification if you quit this page" 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:
# project edition form # project edition form
name_is_required: "Name is required."
illustration: "Illustration" illustration: "Illustration"
add_an_illustration: "Add an illustration" add_an_illustration: "Add an illustration"
CAD_file: "CAD file" CAD_file: "CAD file"
add_a_new_file: "Add a new file" add_a_new_file: "Add a new file"
description_is_required: "Description is required."
steps: "Steps" steps: "Steps"
step_title: "Step title" step_title: "Step title"
add_a_picture: "Add a picture" add_a_picture: "Add a picture"
@ -130,10 +133,6 @@ en:
machine: machine:
# machine edition form # 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." technical_specifications_are_required: "Technical specifications are required."
attached_files_(pdf): "Attached files (pdf)" attached_files_(pdf): "Attached files (pdf)"
attach_a_file: "Attach a file" attach_a_file: "Attach a file"
@ -168,7 +167,6 @@ en:
title_is_required: "Title is required." title_is_required: "Title is required."
matching_visual: "Matching visual" matching_visual: "Matching visual"
choose_a_picture: "Choose a picture" choose_a_picture: "Choose a picture"
description_is_required: "Description is required."
attachments: "Attachments" attachments: "Attachments"
add_a_new_file: "Add a new file" add_a_new_file: "Add a new file"
event_type: "Event type" event_type: "Event type"
@ -188,7 +186,6 @@ en:
plan: plan:
# subscription plan edition form # subscription plan edition form
general_informations: "General informations" 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." name_length_must_be_less_than_24_characters: "Name length must be less than 24 characters."
type_is_required: "Type is required." type_is_required: "Type is required."
group: "Group" group: "Group"
@ -213,6 +210,13 @@ en:
new_partner: "New partner" new_partner: "New partner"
email_address_is_required: "Email address is required." 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: user_admin:
# partial form to edit/create an user (admin view) # partial form to edit/create an user (admin view)
group: "Group" group: "Group"

@ -87,6 +87,11 @@ fr:
_disconnect_then_reconnect_: "déconnectez-vous puis re-connectez vous" _disconnect_then_reconnect_: "déconnectez-vous puis re-connectez vous"
_for_your_changes_to_take_effect: "pour que les modifications soient prises en compte." _for_your_changes_to_take_effect: "pour que les modifications soient prises en compte."
add_a_project: "Ajouter un projet" 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: messages:
you_will_lose_any_unsaved_modification_if_you_quit_this_page: "Vous perdrez les modifications non enregistrées si vous quittez cette page" 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: project:
# formulaire d'étition d'un projet # formulaire d'étition d'un projet
name_is_required: "Le nom est requis."
illustration: "Illustration" illustration: "Illustration"
add_an_illustration: "Ajouter un visuel" add_an_illustration: "Ajouter un visuel"
CAD_file: "Fichier CAO" CAD_file: "Fichier CAO"
add_a_new_file: "Ajouter un nouveau fichier" add_a_new_file: "Ajouter un nouveau fichier"
description_is_required: "La description est requise."
steps: "Étapes" steps: "Étapes"
step_title: "Titre de l'étape" step_title: "Titre de l'étape"
add_a_picture: "Ajouter une image" add_a_picture: "Ajouter une image"
@ -130,10 +133,6 @@ fr:
machine: machine:
# formulaire d'édition d'une 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." technical_specifications_are_required: "Les caractéristiques techniques sont requises."
attached_files_(pdf): "Pièces jointes (pdf)" attached_files_(pdf): "Pièces jointes (pdf)"
attach_a_file: "Joindre un fichier" attach_a_file: "Joindre un fichier"
@ -168,7 +167,6 @@ fr:
title_is_required: "Le titre est requis." title_is_required: "Le titre est requis."
matching_visual: "Visuel associé" matching_visual: "Visuel associé"
choose_a_picture: "Choisir une image" choose_a_picture: "Choisir une image"
description_is_required: "La description est requise."
attachments: "Pièces jointes" attachments: "Pièces jointes"
add_a_new_file: "Ajouter un nouveau fichier" add_a_new_file: "Ajouter un nouveau fichier"
event_type: "Type d'évènement" event_type: "Type d'évènement"
@ -188,7 +186,6 @@ fr:
plan: plan:
# formulaire d'édition d'une formule d'abonnement # formulaire d'édition d'une formule d'abonnement
general_informations: "Informations générales" 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." name_length_must_be_less_than_24_characters: "Le nom doit faire moins de 24 caractères."
type_is_required: "Le type est requis." type_is_required: "Le type est requis."
group: "Groupe" group: "Groupe"
@ -213,6 +210,13 @@ fr:
new_partner: "Nouveau partenaire" new_partner: "Nouveau partenaire"
email_address_is_required: "L'adresse e-mail est requise." 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: user_admin:
# formulaire partiel d'édition/création utilisateur (vue admin) # formulaire partiel d'édition/création utilisateur (vue admin)
group: "Groupe" group: "Groupe"

@ -64,7 +64,7 @@ Rails.application.routes.draw do
resources :availabilities do resources :availabilities do
get 'machines/:machine_id', action: 'machine', on: :collection get 'machines/:machine_id', action: 'machine', on: :collection
get 'trainings', on: :collection get 'trainings/:training_id', action: 'trainings', on: :collection
get 'reservations', on: :member get 'reservations', on: :member
end end

@ -131,11 +131,11 @@ end
if Training.count == 0 if Training.count == 0
Training.create!([ Training.create!([
{name: "Formation Imprimante 3D"}, {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"}, {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"}, {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"}, {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"} {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| TrainingsPricing.all.each do |p|

@ -25,7 +25,7 @@
"tag": "0.14.3", "tag": "0.14.3",
"commit": "306d1a30b4a8e8144741bb9c0126331ac884126a" "commit": "306d1a30b4a8e8144741bb9c0126331ac884126a"
}, },
"_source": "git://github.com/angular-ui/bootstrap-bower.git", "_source": "https://github.com/angular-ui/bootstrap-bower.git",
"_target": ">=0.13.1", "_target": "~0.14.3",
"_originalSource": "angular-bootstrap" "_originalSource": "angular-bootstrap"
} }

@ -1,6 +1,6 @@
{ {
"name": "ngUpload", "name": "ngUpload",
"version": "0.5.17", "version": "0.5.18",
"main": "ng-upload.js", "main": "ng-upload.js",
"ignore": [ "ignore": [
"node_modules", "node_modules",
@ -20,13 +20,13 @@
"angular": ">=1.0.4" "angular": ">=1.0.4"
}, },
"homepage": "https://github.com/twilson63/ngUpload", "homepage": "https://github.com/twilson63/ngUpload",
"_release": "0.5.17", "_release": "0.5.18",
"_resolution": { "_resolution": {
"type": "version", "type": "version",
"tag": "v0.5.17", "tag": "v0.5.18",
"commit": "df9f3edfdbcd1ca6d3f365ff85e32de229df3af1" "commit": "da7fe2bb94eb6adb2cd26ab4f6f979aa020baf9c"
}, },
"_source": "git://github.com/twilson63/ngUpload.git", "_source": "https://github.com/twilson63/ngUpload.git",
"_target": ">=0.5.11", "_target": ">=0.5.11",
"_originalSource": "ngUpload" "_originalSource": "ngUpload"
} }

@ -1,6 +1,6 @@
{ {
"name": "ngUpload", "name": "ngUpload",
"version": "0.5.17", "version": "0.5.18",
"main": "ng-upload.js", "main": "ng-upload.js",
"ignore": [ "ignore": [
"node_modules", "node_modules",

@ -146,6 +146,9 @@ angular.module('ngUpload', [])
} }
// perform check before submit file // perform check before submit file
if (options.beforeSubmit && options.beforeSubmit(scope, {}) === false) { if (options.beforeSubmit && options.beforeSubmit(scope, {}) === false) {
if(!scope.$$phase){
scope.$apply();
}
$event.preventDefault(); $event.preventDefault();
return false; 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)})}}}]);

@ -133,7 +133,7 @@ angular.module('app', ['ngUpload'])
* Working in IE * 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 ## Directive Options