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

Merge remote-tracking branch 'LaCasemate/master'

This commit is contained in:
siteswapjuggler 2017-09-10 12:01:59 +02:00
commit 52071c7b83
64 changed files with 609 additions and 88 deletions

View File

@ -1 +1 @@
2.5.10
2.5.11

View File

@ -1,5 +1,24 @@
# Changelog Fab Manager
## v2.5.11 2017 September 7
- Added tooltip concerning images insertion while configuring the about page
- Ability for admins to configure the maximum visibility for availabilities reservation
- Administrators isolation in a special group
- In login modal, displays an alert if Caps lock key is pressed
- Prevent creation of irregular yearly plans (eg. 12 months)
- Ability to lock machine, space or training availability slots, to prevent new reservations on them
- Fix a bug: admins cannot see all availabilities for spaces in reservation calendar when a user is selected
- Fix a bug: missing translation after payment in english and portuguese
- Fix a bug: invalid notification when sending monetary coupon to users
- Fix a bug: unable to delete group "standard"
- Fix a bug: recursive events crossing Daylight Saving Time period changes are shifted by 1 hour
- Fix a bug: unable to see availabilities in the public calendar when browsing as a visitor (non-connected)
- Updated puma for compatibility with openSSL > 1.0
- Documented installation on ArchLinux
- [TODO DEPLOY] `rake db:seed` then `rake fablab:fix:migrate_admins_group`
- [TODO DEPLOY] `rake fablab:fix:recursive_events_over_DST`
## v2.5.10 2017 August 16
- Updated axlsx gem for excel files generation, possible fix for #489

View File

@ -43,7 +43,7 @@ group :development do
gem 'mailcatcher'
gem 'awesome_print'
gem "puma"
gem 'puma'
gem 'foreman'
gem 'capistrano'

View File

@ -301,8 +301,7 @@ GEM
prawn-table (0.2.1)
protected_attributes (1.1.3)
activemodel (>= 4.0.1, < 5.0)
puma (2.11.1)
rack (>= 1.1, < 2.0)
puma (3.10.0)
pundit (1.0.0)
activesupport (>= 3.0.0)
rack (1.6.8)
@ -568,4 +567,4 @@ DEPENDENCIES
webmock
BUNDLED WITH
1.15.3
1.15.4

View File

@ -71,6 +71,7 @@ In you only intend to run fab-manager on your local machine for testing purposes
1. Install RVM with the ruby version specified in the [.ruby-version file](.ruby-version).
For more details about the process, Please read the [official RVM documentation](http://rvm.io/rvm/install).
If you're using ArchLinux, you may have to [read this](doc/archlinux_readme.md) before.
2. Retrieve the project from Git

View File

@ -20,7 +20,7 @@ angular.module('application', ['ngCookies', 'ngResource', 'ngSanitize', 'ngCooki
'ui.select', 'ui.calendar', 'angularMoment', 'Devise', 'DeviseModal', 'angular-growl', 'xeditable',
'checklist-model', 'unsavedChanges', 'angular-loading-bar', 'ngTouch', 'angular-google-analytics',
'angularUtils.directives.dirDisqus', 'summernote', 'elasticsearch', 'angular-medium-editor', 'naif.base64',
'minicolors', 'pascalprecht.translate', 'ngFitText', 'ngAside']).
'minicolors', 'pascalprecht.translate', 'ngFitText', 'ngAside', 'ngCapsLock']).
config(['$httpProvider', 'AuthProvider', "growlProvider", "unsavedWarningsConfigProvider", "AnalyticsProvider", "uibDatepickerPopupConfig", "$provide", "$translateProvider",
function($httpProvider, AuthProvider, growlProvider, unsavedWarningsConfigProvider, AnalyticsProvider, uibDatepickerPopupConfig, $provide, $translateProvider) {

View File

@ -68,6 +68,7 @@
//= require angular-translate-interpolation-messageformat/angular-translate-interpolation-messageformat
//= require ngFitText/dist/ng-FitText.min
//= require angular-aside/dist/js/angular-aside
//= require ngCapsLock/ng-caps-lock
//= require_tree ./controllers
//= require_tree ./services
//= require_tree ./directives

View File

@ -132,6 +132,40 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
##
# Mark the selected slot as unavailable for new reservations or allow reservations again on it
##
$scope.toggleLockReservations = ->
# first, define a shortcut to the lock property
locked = $scope.availability.lock
# then check if we'll allow reservations locking
prevent = !locked # if currently locked, allow unlock anyway
if (!locked)
prevent = false
angular.forEach $scope.reservations, (r) ->
if r.canceled_at == null
prevent = true # if currently unlocked and has any non-cancelled reservation, disallow locking
if (!prevent)
# open a confirmation dialog
dialogs.confirm
resolve:
object: ->
title: _t('admin_calendar.confirmation_required')
msg: if locked then _t("admin_calendar.do_you_really_want_to_allow_reservations") else _t("admin_calendar.do_you_really_want_to_block_this_slot")
, ->
# the admin has confirmed, lock/unlock the slot
Availability.lock {id: $scope.availability.id}, {lock: !locked}
, (data) -> # success
$scope.availability = data
growl.success(if locked then _t('admin_calendar.unlocking_success') else _t('admin_calendar.locking_success') )
uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents'
, (error) -> # failed
growl.error(if locked then _t('admin_calendar.unlocking_failed') else _t('admin_calendar.locking_failed'))
else
growl.error(_t('admin_calendar.unlockable_because_reservations'))
### PRIVATE SCOPE ###
##

View File

@ -25,7 +25,7 @@ class MembersController
## Retrieve the profiles groups (eg. students ...)
Group.query (groups) ->
$scope.groups = groups
$scope.groups = groups.filter (g) -> g.slug != 'admins'
## Retrieve the list the available trainings
Training.query().$promise.then (data)->

View File

@ -13,7 +13,7 @@ class PlanController
## groups list
$scope.groups = groups
$scope.groups = groups.filter (g) -> g.slug != 'admins'
## users with role 'partner', notifiables for a partner plan
$scope.partners = partners.users

View File

@ -17,7 +17,7 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
$scope.plans = plans
## List of groups (eg. normal, student ...)
$scope.groups = groups
$scope.groups = groups.filter (g) -> g.slug != 'admins'
## Associate free machine hours with subscriptions
$scope.machineCredits = machineCreditsPromise

View File

@ -66,7 +66,7 @@ Application.Controllers.controller "SettingsController", ["$scope", 'Setting', '
$scope.moveDelay =
name: 'booking_move_delay'
value: parseInt(settingsPromise.booking_move_delay)
value: parseInt(settingsPromise.booking_move_delay, 10)
$scope.enableCancel =
name: 'booking_cancel_enable'
@ -74,7 +74,7 @@ Application.Controllers.controller "SettingsController", ["$scope", 'Setting', '
$scope.cancelDelay =
name: 'booking_cancel_delay'
value: parseInt(settingsPromise.booking_cancel_delay)
value: parseInt(settingsPromise.booking_cancel_delay, 10)
$scope.enableReminder =
name: 'reminder_enable'
@ -82,7 +82,15 @@ Application.Controllers.controller "SettingsController", ["$scope", 'Setting', '
$scope.reminderDelay =
name: 'reminder_delay'
value: parseInt(settingsPromise.reminder_delay)
value: parseInt(settingsPromise.reminder_delay, 10)
$scope.visibilityYearly =
name: 'visibility_yearly'
value: parseInt(settingsPromise.visibility_yearly, 10)
$scope.visibilityOthers =
name: 'visibility_others'
value: parseInt(settingsPromise.visibility_others, 10)

View File

@ -256,6 +256,9 @@ Application.Controllers.controller "ShowMachineController", ['$scope', '$state',
$state.go('app.public.machines_list')
, (error)->
growl.warning(_t('the_machine_cant_be_deleted_because_it_is_already_reserved_by_some_users'))
##
# Callback to book a reservation for the current machine
##
@ -412,6 +415,7 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", '$stat
updateCalendar()
##
# When modifying an already booked reservation, callback when the modification was successfully done.
##

View File

@ -8,7 +8,7 @@ Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScop
### PUBLIC SCOPE ###
## list of groups
$scope.groups = groupsPromise
$scope.groups = groupsPromise.filter (g) -> g.slug != 'admins'
## default : do not show the group changing form
## group ID of the current/selected user
@ -18,7 +18,7 @@ Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScop
## list of plans, classified by group
$scope.plansClassifiedByGroup = []
for group in groupsPromise
for group in $scope.groups
groupObj = { id: group.id, name: group.name, plans: [] }
for plan in plansPromise
groupObj.plans.push(plan) if plan.group_id == group.id

View File

@ -1156,7 +1156,9 @@ angular.module('application.router', ['ui.router']).
'fablab_name',
'name_genre',
'reminder_enable',
'reminder_delay'
'reminder_delay',
'visibility_yearly',
'visibility_others'
]").$promise
]
cguFile: ['CustomAsset', (CustomAsset) ->

View File

@ -24,4 +24,7 @@ Application.Services.factory 'Availability', ["$resource", ($resource)->
isArray: true
update:
method: 'PUT'
lock:
method: 'PUT'
url: '/api/availabilities/:id/lock'
]

View File

@ -360,6 +360,10 @@ body.container{
}
.reservations-locked {
background-color: #f5f5f5;
}
.reservation-canceled {
color: #606060;
border-radius: 0.2em;

View File

@ -42,11 +42,12 @@
</a>
<iframe name="export-frame" height="0" width="0" class="none"></iframe>
</div>
<div class="widget panel b-a m m-t-lg">
<div class="widget panel b-a m m-t-lg" ng-if="availability">
<div class="panel-heading b-b small">
<h3 translate>{{ 'admin_calendar.ongoing_reservations' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper">
<div class="widget-content no-bg auto wrapper" ng-class="{'reservations-locked': availability.lock}">
<ul class="list-unstyled" ng-if="reservations.length > 0">
<li ng-repeat="r in reservations" class="m-b-xs" ng-class="{'reservation-canceled':r.canceled_at}">
{{r.user.name}}
@ -55,13 +56,12 @@
<span class="btn btn-warning btn-xs" ng-click="cancelBooking(r)" ng-if="!r.canceled_at"><i class="fa fa-times red"></i></span>
</li>
</ul>
<div ng-if="reservations.length == 0" translate>{{ 'admin_calendar.no_reservations' }}</div>
</div>
<div ng-show="reservations.length == 0" translate>{{ 'admin_calendar.no_reservations' }}</div>
<div class="m-t" ng-show="availability.lock"><i class="fa fa-ban"/> <span class="m-l-xs" translate>{{ 'admin_calendar.reservations_locked' }}</span></div>
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-3" ng-if="availability.machine_ids.length > 0">
<div class="widget panel b-a m m-t-lg">
<div class="widget panel b-a m m-t-lg" ng-if="availability.machine_ids.length > 0">
<div class="panel-heading b-b small">
<h3 translate>{{ 'admin_calendar.machines' }}</h3>
</div>
@ -74,6 +74,24 @@
</ul>
</div>
</div>
<div class="widget panel b-a m m-t-lg" ng-if="availability">
<div class="panel-heading b-b small">
<h3 translate>{{ 'admin_calendar.actions' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper">
<button class="btn btn-default" ng-click="toggleLockReservations()">
<span ng-hide="availability.lock">
<i class="fa fa-stop" />
<span class="m-l-xs" translate>{{ 'admin_calendar.block_reservations' }}</span>
</span>
<span ng-show="availability.lock">
<i class="fa fa-play" />
<span class="m-l-xs" translate>{{ 'admin_calendar.allow_reservations' }}</span>
</span>
</button>
</div>
</div>
</div>
</section>

View File

@ -23,7 +23,7 @@
<i class="fa fa-times"></i>
</button>
</form>
<div class="buttons" ng-show="!rowform.$visible">
<div class="buttons" ng-show="!rowform.$visible" ng-hide="group.slug === 'admins'">
<button class="btn btn-default" ng-click="rowform.$show()">
<i class="fa fa-edit"></i> <span class="hidden-xs hidden-sm" translate>{{ 'edit' }}</span>
</button>

View File

@ -44,13 +44,13 @@
<tr>
<th style="width:20%"><a href="" ng-click="setOrder('name')">{{ 'name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': order == 'name', 'fa fa-sort-alpha-desc': order == '-name', 'fa fa-arrows-v': order }"></i></a></th>
<th style="width:10%"><a href="" ng-click="setOrder('calls_count')">{{ 'calls_count' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': order == 'calls_count', 'fa fa-sort-numeric-desc': order == '-calls_count', 'fa fa-arrows-v': order }"></i></a></th>
<th style="width:15%"><a href="" ng-click="setOrder('calls_count')">{{ 'calls_count' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': order == 'calls_count', 'fa fa-sort-numeric-desc': order == '-calls_count', 'fa fa-arrows-v': order }"></i></a></th>
<th style="width:20%"><a href="">{{ 'token' | translate }}</a></th>
<th style="width:20%"><a href="" ng-click="setOrder('created_at')">{{ 'created_at' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': order == 'created_at', 'fa fa-sort-numeric-desc': order == '-created_at', 'fa fa-arrows-v': order }"></i></a></th>
<th style="width:30%"></th>
<th style="width:25%"></th>
</tr>
</thead>
<tbody>

View File

@ -17,6 +17,7 @@
}'>
</div>
<span class="help-block text-info text-xs"><i class="fa fa-lightbulb-o"></i> {{ 'settings.drag_and_drop_to_insert_images' | translate }}</span>
<button name="button" class="btn btn-warning" ng-click="save(aboutBodySetting)" translate>{{ 'save' }}</button>
</div>
<div class="col-md-4 col-md-offset-2">

View File

@ -22,7 +22,34 @@
</div>
</div>
<div class="row">
<h3 class="m-l" translate>{{ 'settings.ability_for_the_users_to_move_their_reservations' }}</h3>
<h3 class="m-l m-t-lg" translate>{{ 'settings.max_visibility' }}</h3>
<form class="col-md-4" name="visibilityYearlyForm">
<label for="yearlySubscribers" class="control-label m-r" translate>{{ 'settings.visibility_for_yearly_members' }}</label>
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">
<i class="fa fa-calendar"></i>
</div>
<input type="number" class="form-control" id="yearlySubscribers" ng-model="visibilityYearly.value" min="1" required>
</div>
</div>
<button name="button" class="btn btn-warning" ng-click="save(visibilityYearly)" ng-disabled="visibilityYearlyForm.$invalid" translate>{{ 'save' }}</button>
</form>
<form class="col-md-4 col-md-offset-2" name="visibilityOthersForm">
<label for="others" class="control-label m-r" translate>{{ 'settings.visibility_for_other_members' }}</label>
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">
<i class="fa fa-calendar"></i>
</div>
<input type="number" class="form-control" id="others" ng-model="visibilityOthers.value" min="1" required>
</div>
</div>
<button name="button" class="btn btn-warning" ng-click="save(visibilityOthers)" ng-disabled="visibilityOthersForm.$invalid" translate>{{ 'save' }}</button>
</form>
</div>
<div class="row">
<h3 class="m-l m-t-lg" translate>{{ 'settings.ability_for_the_users_to_move_their_reservations' }}</h3>
<div class="form-group m-l">
<label for="enableMove" class="control-label m-r" translate>{{ 'settings.reservations_shifting' }}</label>
<input bs-switch
@ -51,7 +78,7 @@
</form>
</div>
<div class="row">
<h3 class="m-l" translate>{{ 'settings.ability_for_the_users_to_cancel_their_reservations' }}</h3>
<h3 class="m-l m-t-lg" translate>{{ 'settings.ability_for_the_users_to_cancel_their_reservations' }}</h3>
<div class="form-group m-l">
<label for="enableCancel" class="control-label m-r" translate>{{ 'settings.reservations_cancelling' }}</label>
<input bs-switch

View File

@ -22,7 +22,12 @@
<uib-alert type="warning">
<span class="text-black font-sbold">{{getUserGroup().name}}</span>
</uib-alert>
<button class="btn text-black btn-warning-full btn-sm m-t-n-sm" ng-click="group.change = !group.change" ng-show="!user.subscribed_plan.name" translate>{{ 'i_want_to_change_group' }}</button>
<button class="btn text-black btn-warning-full btn-sm m-t-n-sm"
ng-click="group.change = !group.change"
ng-hide="user.subscribed_plan.name || user.role === 'admin'"
translate>
{{ 'i_want_to_change_group' }}
</button>
</div>
<div ng-show="group.change">
<select class="form-control" ng-options="g.id as g.name for g in groups" ng-model="userGroup"></select>

View File

@ -42,6 +42,10 @@
ng-minlength="8"/>
</div>
<a href="#" ng-click="openResetPassword($event)" class="text-xs">{{ 'password_forgotten' | translate }}</a>
<div class="alert alert-warning m-t-sm m-b-none text-xs p-sm" ng-show='isCapsLockOn' role="alert">
<i class="fa fa-warning"></i>
{{ 'caps_lock_is_on' | translate }}
</div>
</div>
</div>

View File

@ -12,8 +12,8 @@ class API::AdminsController < API::ApiController
@admin = User.new(admin_params.merge(password: generated_password))
@admin.send :set_slug
# we associate any random group to the admin as it is mandatory for users but useless for admins
@admin.group = Group.first
# we associate the admin group to prevent linking any other 'normal' group (which won't be deletable afterwards)
@admin.group = Group.find_by(slug: 'admins')
# if the authentication is made through an SSO, generate a migration token
unless AuthProvider.active.providable_type == DatabaseProvider.name

View File

@ -2,7 +2,8 @@ class API::AvailabilitiesController < API::ApiController
include FablabConfiguration
before_action :authenticate_user!, except: [:public]
before_action :set_availability, only: [:show, :update, :destroy, :reservations]
before_action :set_availability, only: [:show, :update, :destroy, :reservations, :lock]
before_action :define_max_visibility, only: [:machine, :trainings, :spaces]
respond_to :json
def index
@ -20,22 +21,32 @@ class API::AvailabilitiesController < API::ApiController
def public
start_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:start])
end_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:end]).end_of_day
@reservations = Reservation.includes(:slots, user: [:profile]).references(:slots, :user).where('slots.start_at >= ? AND slots.end_at <= ?', start_date, end_date)
@reservations = Reservation.includes(:slots, user: [:profile]).references(:slots, :user)
.where('slots.start_at >= ? AND slots.end_at <= ?', start_date, end_date)
# request for 1 single day
if in_same_day(start_date, end_date)
# trainings, events
@training_and_event_availabilities = Availability.includes(:tags, :trainings, :event, :slots).where(available_type: %w(training event))
.where('start_at >= ? AND end_at <= ?', start_date, end_date)
.where(lock: false)
# machines
@machine_availabilities = Availability.includes(:tags, :machines).where(available_type: 'machines')
.where('start_at >= ? AND end_at <= ?', start_date, end_date)
.where(lock: false)
@machine_slots = []
@machine_availabilities.each do |a|
a.machines.each do |machine|
if params[:m] and params[:m].include?(machine.id.to_s)
((a.end_at - a.start_at)/ApplicationHelper::SLOT_DURATION.minutes).to_i.times do |i|
slot = Slot.new(start_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes, end_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes + ApplicationHelper::SLOT_DURATION.minutes, availability_id: a.id, availability: a, machine: machine, title: machine.name)
slot = Slot.new(
start_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes,
end_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes + ApplicationHelper::SLOT_DURATION.minutes,
availability_id: a.id,
availability: a,
machine: machine,
title: machine.name
)
slot = verify_machine_is_reserved(slot, @reservations, current_user, '')
@machine_slots << slot
end
@ -46,6 +57,7 @@ class API::AvailabilitiesController < API::ApiController
# spaces
@space_availabilities = Availability.includes(:tags, :spaces).where(available_type: 'space')
.where('start_at >= ? AND end_at <= ?', start_date, end_date)
.where(lock: false)
if params[:s]
@space_availabilities.where(available_id: params[:s])
@ -56,7 +68,14 @@ class API::AvailabilitiesController < API::ApiController
space = a.spaces.first
((a.end_at - a.start_at)/ApplicationHelper::SLOT_DURATION.minutes).to_i.times do |i|
if (a.start_at + (i * ApplicationHelper::SLOT_DURATION).minutes) > Time.now
slot = Slot.new(start_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes, end_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes + ApplicationHelper::SLOT_DURATION.minutes, availability_id: a.id, availability: a, space: space, title: space.name)
slot = Slot.new(
start_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes,
end_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes + ApplicationHelper::SLOT_DURATION.minutes,
availability_id: a.id,
availability: a,
space: space,
title: space.name
)
slot = verify_space_is_reserved(slot, @reservations, current_user, '')
@space_slots << slot
end
@ -68,11 +87,12 @@ class API::AvailabilitiesController < API::ApiController
else
@availabilities = Availability.includes(:tags, :machines, :trainings, :spaces, :event, :slots)
.where('start_at >= ? AND end_at <= ?', start_date, end_date)
.where(lock: false)
@availabilities.each do |a|
if a.available_type == 'training' or a.available_type == 'event'
a = verify_training_event_is_reserved(a, @reservations, current_user)
elsif a.available_type == 'space'
a.is_reserved = is_reserved_availability(a, current_user.id)
a.is_reserved = is_reserved_availability(a, current_user)
end
end
end
@ -124,16 +144,27 @@ class API::AvailabilitiesController < API::ApiController
@slots = []
@reservations = Reservation.where('reservable_type = ? and reservable_id = ?', @machine.class.to_s, @machine.id).includes(:slots, user: [:profile]).references(:slots, :user).where('slots.start_at > ?', Time.now)
if @user.is_admin?
@availabilities = @machine.availabilities.includes(:tags).where("end_at > ? AND available_type = 'machines'", Time.now)
@availabilities = @machine.availabilities.includes(:tags)
.where("end_at > ? AND available_type = 'machines'", Time.now)
.where(lock: false)
else
end_at = 1.month.since
end_at = 3.months.since if is_subscription_year(@user)
@availabilities = @machine.availabilities.includes(:tags).where("end_at > ? AND end_at < ? AND available_type = 'machines'", Time.now, end_at).where('availability_tags.tag_id' => @user.tag_ids.concat([nil]))
end_at = @visi_max_other
end_at = @visi_max_year if is_subscription_year(@user)
@availabilities = @machine.availabilities.includes(:tags).where("end_at > ? AND end_at < ? AND available_type = 'machines'", Time.now, end_at)
.where('availability_tags.tag_id' => @user.tag_ids.concat([nil]))
.where(lock: false)
end
@availabilities.each do |a|
((a.end_at - a.start_at)/ApplicationHelper::SLOT_DURATION.minutes).to_i.times do |i|
if (a.start_at + (i * ApplicationHelper::SLOT_DURATION).minutes) > Time.now
slot = Slot.new(start_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes, end_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes + ApplicationHelper::SLOT_DURATION.minutes, availability_id: a.id, availability: a, machine: @machine, title: '')
slot = Slot.new(
start_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes,
end_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes + ApplicationHelper::SLOT_DURATION.minutes,
availability_id: a.id,
availability: a,
machine: @machine,
title: ''
)
slot = verify_machine_is_reserved(slot, @reservations, current_user, @current_user_role)
@slots << slot
end
@ -166,12 +197,17 @@ class API::AvailabilitiesController < API::ApiController
# who made the request?
# 1) an admin (he can see all future availabilities)
if current_user.is_admin?
@availabilities = @availabilities.includes(:tags, :slots, trainings: [:machines]).where('availabilities.start_at > ?', Time.now)
@availabilities = @availabilities.includes(:tags, :slots, trainings: [:machines])
.where('availabilities.start_at > ?', Time.now)
.where(lock: false)
# 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 = @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_at = @visi_max_year
end_at = @visi_max_year if can_show_slot_plus_three_months(@user)
@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]))
.where(lock: false)
end
# finally, we merge the availabilities with the reservations
@ -189,18 +225,32 @@ class API::AvailabilitiesController < API::ApiController
@current_user_role = current_user.is_admin? ? 'admin' : 'user'
@space = Space.friendly.find(params[:space_id])
@slots = []
@reservations = Reservation.where('reservable_type = ? and reservable_id = ?', @space.class.to_s, @space.id).includes(:slots, user: [:profile]).references(:slots, :user).where('slots.start_at > ?', Time.now)
if @user.is_admin?
@availabilities = @space.availabilities.includes(:tags).where("end_at > ? AND available_type = 'space'", Time.now)
@reservations = Reservation.where('reservable_type = ? and reservable_id = ?', @space.class.to_s, @space.id)
.includes(:slots, user: [:profile]).references(:slots, :user)
.where('slots.start_at > ?', Time.now)
if current_user.is_admin?
@availabilities = @space.availabilities.includes(:tags)
.where("end_at > ? AND available_type = 'space'", Time.now)
.where(lock: false)
else
end_at = 1.month.since
end_at = 3.months.since if is_subscription_year(@user)
@availabilities = @space.availabilities.includes(:tags).where("end_at > ? AND end_at < ? AND available_type = 'space'", Time.now, end_at).where('availability_tags.tag_id' => @user.tag_ids.concat([nil]))
end_at = @visi_max_other
end_at = @visi_max_year if is_subscription_year(@user)
@availabilities = @space.availabilities.includes(:tags)
.where("end_at > ? AND end_at < ? AND available_type = 'space'", Time.now, end_at)
.where('availability_tags.tag_id' => @user.tag_ids.concat([nil]))
.where(lock: false)
end
@availabilities.each do |a|
((a.end_at - a.start_at)/ApplicationHelper::SLOT_DURATION.minutes).to_i.times do |i|
if (a.start_at + (i * ApplicationHelper::SLOT_DURATION).minutes) > Time.now
slot = Slot.new(start_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes, end_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes + ApplicationHelper::SLOT_DURATION.minutes, availability_id: a.id, availability: a, space: @space, title: '')
slot = Slot.new(
start_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes,
end_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes + ApplicationHelper::SLOT_DURATION.minutes,
availability_id: a.id,
availability: a,
space: @space,
title: ''
)
slot = verify_space_is_reserved(slot, @reservations, @user, @current_user_role)
@slots << slot
end
@ -234,6 +284,15 @@ class API::AvailabilitiesController < API::ApiController
end
end
def lock
authorize @availability
if @availability.update_attributes(lock: lock_params)
render :show, status: :ok, location: @availability
else
render json: @availability.errors, status: :unprocessable_entity
end
end
private
def set_availability
@availability = Availability.find(params[:id])
@ -244,14 +303,22 @@ class API::AvailabilitiesController < API::ApiController
:machines_attributes => [:id, :_destroy])
end
def is_reserved_availability(availability, user_id)
def lock_params
params.require(:lock)
end
def is_reserved_availability(availability, user)
if user
reserved_slots = []
availability.slots.each do |s|
if s.canceled_at.nil?
reserved_slots << s
end
end
reserved_slots.map(&:reservations).flatten.map(&:user_id).include? user_id
reserved_slots.map(&:reservations).flatten.map(&:user_id).include? user.id
else
false
end
end
def is_reserved(start_at, reservations)
@ -371,4 +438,9 @@ class API::AvailabilitiesController < API::ApiController
end
end
end
def define_max_visibility
@visi_max_year = Setting.find_by(name: 'visibility_yearly').value.to_i.months.since
@visi_max_other = Setting.find_by(name: 'visibility_others').value.to_i.months.since
end
end

View File

@ -2,7 +2,12 @@ class API::GroupsController < API::ApiController
before_action :authenticate_user!, except: :index
def index
if current_user and current_user.is_admin?
@groups = Group.all
else
@groups = Group.where.not(slug: 'admins')
end
end
def create

View File

@ -0,0 +1,3 @@
# Raised when total of reservation isn't equal to the total of stripe's invoice
class InvoiceTotalDifferentError < StandardError
end

View File

@ -1,3 +0,0 @@
# Raised when total of reservation isnt equal total of strip's invoice
class InvoiceTotalDiffrentError < StandardError
end

View File

@ -0,0 +1,3 @@
# Raised when reserving on a locked availability
class LockedError < StandardError
end

View File

@ -80,6 +80,19 @@ module ApplicationHelper
nil
end
##
# Apply a correction for a future DateTime due to change in Daylight Saving Time (DST) period
# @param reference {ActiveSupport::TimeWithZone}
# @param datetime {DateTime}
# Inspired by https://stackoverflow.com/a/12065605
##
def dst_correction(reference, datetime)
res = datetime.in_time_zone(reference.time_zone.tzinfo.name)
res = res - 1.hour if res.dst? && !reference.dst?
res = res + 1.hour if reference.dst? && !res.dst?
res
end
private
## inspired by gems/actionview-4.2.5/lib/action_view/helpers/translation_helper.rb

View File

@ -1,5 +1,6 @@
class Event < ActiveRecord::Base
include NotifyWith::NotificationAttachedObject
include ApplicationHelper
has_one :event_image, as: :viewable, dependent: :destroy
accepts_nested_attributes_for :event_image, allow_destroy: true
@ -87,8 +88,10 @@ class Event < ActiveRecord::Base
r.events.each do |date|
days_diff = availability.end_at.day - availability.start_at.day
start_at = DateTime.new(date.year, date.month, date.day, availability.start_at.hour, availability.start_at.min, availability.start_at.sec, availability.start_at.zone)
start_at = dst_correction(availability.start_at,start_at)
end_date = date + days_diff.days
end_at = DateTime.new(end_date.year, end_date.month, end_date.day, availability.end_at.hour, availability.end_at.min, availability.end_at.sec, availability.end_at.zone)
end_at = dst_correction(availability.start_at,end_at)
if event_image
ei = EventImage.new(attachment: event_image.attachment)
end

View File

@ -27,6 +27,8 @@ class Plan < ActiveRecord::Base
validates :amount, :group, :base_name, presence: true
validates :interval_count, numericality: { only_integer: true, greater_than_or_equal_to: 1 }
validates :interval_count, numericality: { less_than: 12 }, if: Proc.new {|plan| plan.interval == 'month'}
validates :interval_count, numericality: { less_than: 52 }, if: Proc.new {|plan| plan.interval == 'week'}
validates :interval, inclusion: { in: %w(year month week) }
validates :base_name, :slug, presence: true

View File

@ -47,6 +47,13 @@ class Reservation < ActiveRecord::Base
plan = nil
end
# check that none of the reserved availabilities was locked
slots.each do |slot|
if slot.availability.lock
raise LockedError
end
end
case reservable
@ -260,7 +267,7 @@ class Reservation < ActiveRecord::Base
# this function only check reservation total is equal strip invoice total when
# pay only reservation not reservation + subscription
#if !is_equal_reservation_total_and_stp_invoice_total(stp_invoice, coupon_code)
#raise InvoiceTotalDiffrentError
#raise InvoiceTotalDifferentError
#end
stp_invoice.pay
card.delete if card

View File

@ -32,7 +32,9 @@ class Setting < ActiveRecord::Base
reminder_enable
reminder_delay
event_explications_alert
space_explications_alert )
space_explications_alert
visibility_yearly
visibility_others )
}
after_update :update_stylesheet if :value_changed?

View File

@ -1,5 +1,5 @@
class AvailabilityPolicy < ApplicationPolicy
%w(index? show? create? update? destroy? reservations? export?).each do |action|
%w(index? show? create? update? destroy? reservations? export? lock?).each do |action|
define_method action do
user.is_admin?
end

View File

@ -6,11 +6,12 @@ json.array!(@availabilities) do |availability|
json.available_type availability.available_type
json.machine_ids availability.machine_ids
json.training_ids availability.training_ids
json.backgroundColor 'white'
json.backgroundColor !availability.lock ? 'white' : '#f5f5f5'
json.borderColor availability_border_color(availability)
json.tag_ids availability.tag_ids
json.tags availability.tags do |t|
json.id t.id
json.name t.name
end
json.lock availability.lock
end

View File

@ -11,3 +11,4 @@ json.tags @availability.tags do |t|
json.id t.id
json.name t.name
end
json.lock @availability.lock

View File

@ -1,5 +1,11 @@
json.title notification.notification_type
if notification.attached_object.type == 'percent_off'
json.description t('.enjoy_a_discount_of_PERCENT_with_code_CODE',
PERCENT: notification.attached_object.percent_off,
CODE: notification.attached_object.code)
else
json.description t('.enjoy_a_discount_of_AMOUNT_with_code_CODE',
AMOUNT: number_to_currency(notification.attached_object.amount_off / 100.00),
CODE: notification.attached_object.code)
end
json.url notification_url(notification, format: :json)

View File

@ -15,7 +15,7 @@
<% end %>
<%
# we must tell the use if he could use the code just once or many times (in case we won't specify)
# we must tell the user if he can use the code just once or many times (== maximum wasn't specified)
usages = 999 # just a number > 1
if @attached_object.validity_per_user == 'once'
usages = 1

View File

@ -54,7 +54,8 @@
"messageformat": "=0.1.8",
"moment-timezone": "~0.5.0",
"ngFitText": "~4.1.1",
"angular-aside": "^1.3.2"
"angular-aside": "^1.3.2",
"ngCapsLock": "ng-caps-lock#^1.0.2"
},
"resolutions": {
"jquery": ">=1.10.2",

View File

@ -48,6 +48,17 @@ en:
unable_to_delete_the_slot_START-END_because_it_s_already_reserved_by_a_member: "Unable to delete the slot {{START}} - {{END}} because it's already reserved by a member" # angular interpolation
you_should_select_at_least_a_machine: "You should select at least one machine on this slot."
export_is_running_you_ll_be_notified_when_its_ready: "Export is running. You'll be notified when it's ready."
actions: "Actions"
block_reservations: "Block reservations"
do_you_really_want_to_block_this_slot: "Do you really want to block new reservations on this slot? It will become invisible to users."
locking_success: "Slot successfully locked, it won't appear any longer in the user calendar"
locking_failed: "An error occurred. Slot locking has failed"
allow_reservations: "Allow reservations"
do_you_really_want_to_allow_reservations: "Do you really want to allow booking again on this slot? It will become visible for the users."
unlocking_success: "Slot successfully unlocked, it will appear again in the user calendar"
unlocking_failed: "An error occurred. Slot unlocking has failed"
reservations_locked: "Booking is blocked"
unlockable_because_reservations: "Unable to block booking on this slot because some uncancelled reservations exist on it."
project_elements:
# management of the projects' components
@ -568,12 +579,16 @@ en:
title_of_the_about_page: "Title of the About page"
shift_enter_to_force_carriage_return: "SHIFT + ENTER to force carriage return"
input_the_main_content: "Input the main content"
drag_and_drop_to_insert_images: "Drap and drop to insert images"
input_the_fablab_contacts: "Input the FabLab contacts"
reservations: "Reservations"
reservations_parameters: "Reservations parameters"
confine_the_booking_agenda: "Confine the booking agenda"
opening_time: "Opening time"
closing_time: "Closing time"
max_visibility: "Maximum visibility (in months)"
visibility_for_yearly_members: "For currently running subscriptions, at least 1 year long"
visibility_for_other_members: "For all other members"
ability_for_the_users_to_move_their_reservations: "Ability for the users to move their reservations"
reservations_shifting: "Reservations shifting"
prior_period_(hours): "Prior period (hours)"
@ -608,6 +623,8 @@ en:
reminder_enable: "reservation reminding enabling"
reminder_delay: "delay before sending the reminder"
default_value_is_24_hours: "If the field is leaved empty: 24 hours."
visibility_yearly: "maximum visibility for annual subscribers"
visibility_others: "maximum visibility for other members"
open_api_clients:
add_new_client: "Create new API client"

View File

@ -48,6 +48,17 @@ fr:
unable_to_delete_the_slot_START-END_because_it_s_already_reserved_by_a_member: "Le créneau {{START}} - {{END}} n'a pu être supprimé car il est déjà réservé par un membre" # angular interpolation
you_should_select_at_least_a_machine: "Vous devriez sélectionne au moins une machine pour ce créneau."
export_is_running_you_ll_be_notified_when_its_ready: "L'export est en cours. Vous serez notifié lorsqu'il sera prêt."
actions: "Actions"
block_reservations: "Bloquer les réservations"
do_you_really_want_to_block_this_slot: "Êtes vous sur de vouloir bloquer les nouvelles réservations sur ce créneau ? Il deviendra alors invisible pour les utilisateurs."
locking_success: "Le créneau a bien été verrouillé, il n'apparaitra plus dans le calendrier utilisateur"
locking_failed: "Une erreur est survenue. Le verrouillage du créneau a échoué"
allow_reservations: "Autoriser les réservations"
do_you_really_want_to_allow_reservations: "Êtes vous sur de vouloir autoriser de nouveau la prise de réservations sur ce créneau ? Il deviendra alors visible pour les utilisateurs."
unlocking_success: "Le créneau a bien été déverrouillé, il apparaîtra de nouveau dans le calendrier utilisateur"
unlocking_failed: "Une erreur est survenue. Le déverrouillage du créneau a échoué"
reservations_locked: "Réservations bloquées"
unlockable_because_reservations: "Impossible de bloquer les réservations sur ce créneau car il existe des réservations non annulées sur celui-ci."
project_elements:
# gestion des éléments constituant les projets
@ -575,12 +586,16 @@ fr:
title_of_the_about_page: "Titre page A propos"
shift_enter_to_force_carriage_return: "MAJ. + ENTRÉE pour forcer le retour à la ligne"
input_the_main_content: "Saisir le contenu principal"
drag_and_drop_to_insert_images: "Faites un glisser-déposer pour insérer des images"
input_the_fablab_contacts: "Saisir les Contacts du FabLab"
reservations: "Réservations"
reservations_parameters: "Paramètres des réservations"
confine_the_booking_agenda: "Borner l'agenda de réservation"
opening_time: "Heure d'ouverture"
closing_time: "Heure de fermeture"
max_visibility: "Visibilité maximum (en mois)"
visibility_for_yearly_members: "Pour les abonnements en cours d'au moins 1 an"
visibility_for_other_members: "Pour tous les autres membres"
ability_for_the_users_to_move_their_reservations: "Possibilité pour l'utilisateur de déplacer ses réservations"
reservations_shifting: "Déplacement des réservations"
prior_period_(hours): "Délai préalable (en heures)"
@ -615,6 +630,8 @@ fr:
reminder_enable: "l'activation du rappel de réservation"
reminder_delay: "délai avant envoi de la notification de rappel"
default_value_is_24_hours: "Si aucune valeur n'est renseignée : 24 heures."
visibility_yearly: "la visibilité maximum pour les abonnées annuels"
visibility_others: "la visibilité maximum pour les autres membres"
open_api_clients:
add_new_client: "Créer un compte client"

View File

@ -48,6 +48,17 @@ pt:
unable_to_delete_the_slot_START-END_because_it_s_already_reserved_by_a_member: "Não é possível deletar o slot {{START}} - {{END}} porque já foi reservado por um membro" # angular interpolation
you_should_select_at_least_a_machine: "Você deveria selecionar a última máquina neste slot."
export_is_running_you_ll_be_notified_when_its_ready: "A Exportação está em andamento. Você será notificado quando terminar."
actions: "Ações"
block_reservations: "Impedir reservas"
do_you_really_want_to_block_this_slot: "Você realmente quer bloquear novas reservas neste slot? Isso se tornará invisível para os usuários."
locking_success: "Slot bloqueado com êxito, ela não aparecerá mais tempo no calendário do usuário"
locking_failed: "Um erro ocorreu. O bloqueio do slot falhou"
allow_reservations: "Permitir a reserva"
do_you_really_want_to_allow_reservations: "Você realmente quer permitir a reserva novamente neste slot? Será visível para os usuários."
unlocking_success: "Slot desbloqueado com sucesso, ele aparecerá novamente no calendário do usuário"
unlocking_failed: "Um erro ocorreu. O desbloqueio do slot falhou"
reservations_locked: "Reserva é bloqueado"
unlockable_because_reservations: "Não é possível bloquear a reserva neste slot porque existem algumas reservas não cancelados nele."
project_elements:
# management of the projects' components
@ -568,12 +579,16 @@ pt:
title_of_the_about_page: "Título da página sobre"
shift_enter_to_force_carriage_return: "SHIFT + ENTER para forçar o retorno"
input_the_main_content: "Introduza o conteúdo principal"
drag_and_drop_to_insert_images: "Arrastar e soltar para inserir imagens"
input_the_fablab_contacts: "Insira os contatos do FabLab"
reservations: "Reservas"
reservations_parameters: "Parâmetros das reservas"
confine_the_booking_agenda: "Confine a agenda de reserva"
opening_time: "Horário de abertura"
closing_time: "Horário de fechamento"
max_visibility: "Visibilidade máxima (em meses)"
visibility_for_yearly_members: "Para inscrições atuais de pelo menos 1 ano"
visibility_for_other_members: "Para todos os outros membros"
ability_for_the_users_to_move_their_reservations: "Habilidade para os usuários mover suas reservas"
reservations_shifting: "Mudança de reservas"
prior_period_(hours): "Período anterior (horas)"
@ -608,6 +623,8 @@ pt:
reminder_enable: "Recordar reserva ativo"
reminder_delay: "Atraso antes de enviar o lembrete"
default_value_is_24_hours: "Se o campo estiver vazio: 24 horas."
visibility_yearly: "visibilidade máxima para assinantes anuais"
visibility_others: "visibilidade máxima para outros membros"
open_api_clients:
add_new_client: "Criar novo cliente de API"

View File

@ -97,6 +97,7 @@ en:
not_registered_to_the_fablab: "Not registered to the Fablab?"
create_an_account: "Create an account"
wrong_email_or_password: "Wrong e-mail or password."
caps_lock_is_on: "Caps lock key is on."
# forgotten password modal
your_email_address_is_unknown: "Your e-mail address is unknown."

View File

@ -97,6 +97,7 @@ fr:
not_registered_to_the_fablab: "Vous n'êtes pas inscrit au FAB LAB ?"
create_an_account: "Créer un compte"
wrong_email_or_password: "Adresse courriel ou mot de passe incorrect."
caps_lock_is_on: "La touche de verrouillage des majuscules est activée."
# mot de passe oublié
your_email_address_is_unknown: "Votre adresse de courriel est inconnue."

View File

@ -97,6 +97,7 @@ pt:
not_registered_to_the_fablab: "Ainda não registrado no Fablab?"
create_an_account: "Criar conta"
wrong_email_or_password: "E-mail ou senha incorretos."
caps_lock_is_on: "A tecla Caps Lock está ativada."
# forgotten password modal
your_email_address_is_unknown: "Seu e-mail não está cadastrado."

View File

@ -424,7 +424,7 @@ en:
_subscription: "subscription"
cost_of_the_subscription: "Cost of the subscription"
confirm_and_pay: "Confirm and pay"
you_have_settled_the_following_machine_hours: "You have settled the following {TYPE, select, Machine{machine hours} Training{training} other{elements}}:" # messageFormat interpolation
you_have_settled_the_following_TYPE: "You have settled the following {TYPE, select, Machine{machine hours} Training{training} other{elements}}:" # messageFormat interpolation
you_have_settled_a_: "You have settled a"
total_: "TOTAL :"
thank_you_your_payment_has_been_successfully_registered: "Thank you. Your payment has been successfully registered !"

View File

@ -424,7 +424,7 @@ pt:
_subscription: "inscrição"
cost_of_the_subscription: "Custo da inscrição"
confirm_and_pay: "Confirmar e pagar"
you_have_settled_the_following_machine_hours: "Você liquidou o seguinte {TYPE, select, Machine{horas máquina} Training{training} other{elements}}:" # messageFormat interpolation
you_have_settled_the_following_TYPE: "Você liquidou o seguinte {TYPE, select, Machine{horas máquina} Training{training} other{elements}}:" # messageFormat interpolation
you_have_settled_a_: "Você tem liquidado:"
total_: "TOTAL :"
thank_you_your_payment_has_been_successfully_registered: "Obrigado. Seu pagamento foi registrado com sucesso !"

View File

@ -306,6 +306,7 @@ en:
download_here: "Download here"
notify_member_about_coupon:
enjoy_a_discount_of_PERCENT_with_code_CODE: "Enjoy a discount of %{PERCENT}% with code %{CODE}"
enjoy_a_discount_of_AMOUNT_with_code_CODE: "Enjoy a discount of %{AMOUNT} with code %{CODE}"
statistics:
# statistics tools for admins
@ -356,3 +357,7 @@ en:
# initial price's category for events, created to replace the old "reduced amount" property
reduced_fare: "Reduced fare"
reduced_fare_if_you_are_under_25_student_or_unemployed: "Reduced fare if you are under 25, student or unemployed."
group:
# name of the user's group for administrators
admins: 'Administrators'

View File

@ -306,6 +306,7 @@ fr:
download_here: "Téléchargez ici"
notify_member_about_coupon:
enjoy_a_discount_of_PERCENT_with_code_CODE: "Bénéficiez d'une remise de %{PERCENT} % avec le code %{CODE}"
enjoy_a_discount_of_AMOUNT_with_code_CODE: "Bénéficiez d'une remise de %{AMOUNT} avec le code %{CODE}"
statistics:
# outil de statistiques pour les administrateurs
@ -356,3 +357,7 @@ fr:
# catégorie initiale de prix pour les évènements, en remplacement de l'ancienne propriété "montant réduit"
reduced_fare: "Tarif réduit"
reduced_fare_if_you_are_under_25_student_or_unemployed: "Tarif réduit si vous avez moins de 25 ans, que vous êtes étudiant ou demandeur d'emploi."
group:
# nom du groupe utilisateur pour les administrateurs
admins: 'Administrateurs'

View File

@ -306,6 +306,7 @@ pt:
download_here: "Baixe aqui"
notify_member_about_coupon:
enjoy_a_discount_of_PERCENT_with_code_CODE: "Desfrute de um desconto de %{PERCENT}% com o código %{CODE}"
enjoy_a_discount_of_AMOUNT_with_code_CODE: "Desfrute de um desconto de %{AMOUNT} com o código %{CODE}"
statistics:
# statistics tools for admins
@ -356,3 +357,7 @@ pt:
# initial price's category for events, created to replace the old "reduced amount" property
reduced_fare: "Tarifa reduzida"
reduced_fare_if_you_are_under_25_student_or_unemployed: "Tarifa reduzida se tiver menos de 25 anos, estudante ou desempregado."
group:
# name of the user's group for administrators
admins: 'Administradores'

View File

@ -85,6 +85,7 @@ Rails.application.routes.draw do
get 'reservations', on: :member
get 'public', on: :collection
get '/export_index', action: 'export_availabilities', on: :collection
put ':id/lock', action: 'lock', on: :collection
end
resources :groups, only: [:index, :create, :update, :destroy]

View File

@ -0,0 +1,5 @@
class AddLockToAvailability < ActiveRecord::Migration
def change
add_column :availabilities, :lock, :boolean, default: false
end
end

View File

@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170227114634) do
ActiveRecord::Schema.define(version: 20170906100906) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -79,6 +79,7 @@ ActiveRecord::Schema.define(version: 20170227114634) do
t.datetime "updated_at"
t.integer "nb_total_places"
t.boolean "destroying", default: false
t.boolean "lock", default: false
end
create_table "availability_tags", force: :cascade do |t|

View File

@ -82,9 +82,13 @@ if Group.count == 0
])
end
unless Group.find_by(slug: 'admins')
Group.create! name: I18n.t('group.admins'), slug: 'admins'
end
# Create the default admin if none exists yet
if Role.where(name: 'admin').joins(:users).count === 0
admin = User.new(username: 'admin', email: ENV["ADMIN_EMAIL"], password: ENV["ADMIN_PASSWORD"], password_confirmation: Rails.application.secrets.admin_password, group_id: Group.first.id, profile_attributes: {first_name: 'admin', last_name: 'admin', gender: true, phone: '0123456789', birthday: Time.now})
admin = User.new(username: 'admin', email: ENV["ADMIN_EMAIL"], password: ENV["ADMIN_PASSWORD"], password_confirmation: Rails.application.secrets.admin_password, group_id: Group.find_by(slug: 'admins').id, profile_attributes: {first_name: 'admin', last_name: 'admin', gender: true, phone: '0123456789', birthday: Time.now})
admin.add_role 'admin'
admin.save!
end
@ -397,6 +401,18 @@ unless Setting.find_by(name: 'reminder_delay').try(:value)
setting.save
end
unless Setting.find_by(name: 'visibility_yearly').try(:value)
setting = Setting.find_or_initialize_by(name: 'visibility_yearly')
setting.value = '3'
setting.save
end
unless Setting.find_by(name: 'visibility_others').try(:value)
setting = Setting.find_or_initialize_by(name: 'visibility_others')
setting.value = '1'
setting.save
end
if StatisticCustomAggregation.count == 0
# available reservations hours for machines
machine_hours = StatisticType.find_by(key: 'hour', statistic_index_id: 2)

13
doc/archlinux_readme.md Normal file
View File

@ -0,0 +1,13 @@
# Specific instructions concerning installation on ArchLinux
## Ruby 2.3.0
Ruby 2.3.0 has a known issue with openSSL version > 1.0 (which is ArchLinux default).
To overpass this problem, you must install ruby with special indication of the openSSL installation to use.
```bash
sudo pacman -S gcc5
rvm pkg install openssl
CC=gcc-5 rvm install 2.3.0 -C --with-openssl-dir=$HOME/.rvm/usr
```
There's also an issue with openSSL and `puma` but this is fixed by using puma version > 3.

View File

@ -47,5 +47,31 @@ namespace :fablab do
end
end
end
task migrate_admins_group: :environment do
admins = Group.find_by(slug: 'admins')
User.all.each do |user|
if user.is_admin?
user.group = admins
user.save!
end
end
end
task recursive_events_over_DST: :environment do
include ApplicationHelper
groups = Event.group(:recurrence_id).count
groups.keys.each do |recurrent_event_id|
initial_event = Event.find(recurrent_event_id)
Event.where(recurrence_id: recurrent_event_id).where.not(id: recurrent_event_id).each do |event|
availability = event.availability
if initial_event.availability.start_at.hour != availability.start_at.hour
availability.start_at = dst_correction(initial_event.availability.start_at, availability.start_at)
availability.end_at = dst_correction(initial_event.availability.end_at, availability.end_at)
availability.save!
end
end
end
end
end
end

View File

@ -0,0 +1,23 @@
{
"name": "ngCapsLock",
"version": "1.0.2",
"main": "./ng-caps-lock.js",
"license": "MIT",
"authors": [
"Fábio Rodrigues <fabio.info@gmail.com>"
],
"dependencies": {
"angular": ">=1.1"
},
"homepage": "https://github.com/FabioMR/ng-caps-lock",
"_release": "1.0.2",
"_resolution": {
"type": "version",
"tag": "1.0.2",
"commit": "f28192faa10107db6b5b7db7a77109d36b16c0b9"
},
"_source": "https://github.com/FabioMR/ng-caps-lock.git",
"_target": "^1.0.2",
"_originalSource": "ng-caps-lock",
"_direct": true
}

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 FabioMR
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,25 @@
# ngCapsLock
ngCapsLock is a module for [AngularJS](http://angularjs.org/) to detect if caps-lock is on/off.
Getting Started
---------------
* Download ngCapsLock or install it with [Bower](http://bower.io/) via `bower install ng-caps-lock`
* Include the script tag on your page after the AngularJS script tags
<script type='text/javascript' src='path/to/angular.min.js'></script>
<script type='text/javascript' src='path/to/ng-caps-lock.min.js'></script>
* Ensure that your application module specifies `ngCapsLock` as a dependency:
angular.module('myApp', ['ngCapsLock']);
* Use the property `isCapsLockOn` on a `ng-show` directive.
<p class="caps-lock-alert" ng-show='isCapsLockOn'>Caps lock is on</p>
License
-------
ngCapsLock is licensed under the MIT license. See the LICENSE file for more details.

View File

@ -0,0 +1,13 @@
{
"name": "ngCapsLock",
"version": "1.0.2",
"main": "./ng-caps-lock.js",
"license": "MIT",
"authors": [
"Fábio Rodrigues <fabio.info@gmail.com>"
],
"dependencies": {
"angular": ">=1.1"
},
"homepage": "https://github.com/FabioMR/ng-caps-lock"
}

View File

@ -0,0 +1,61 @@
(function () {
'use strict';
angular.module('ngCapsLock', []).run(['$rootScope', '$document', '$timeout', function ($rootScope, $document, $timeout) {
var bindingForAppleDevice = function () {
$document.bind("keydown", function (event) {
if (event.keyCode === 20) { setCapsLockOn(true); }
});
$document.bind("keyup", function (event) {
if (event.keyCode === 20) { setCapsLockOn(false); }
});
$document.bind("keypress", function (event) {
var code = event.charCode || event.keyCode;
var shift = event.shiftKey;
if (code > 96 && code < 123) { setCapsLockOn(false); }
if (code > 64 && code < 91 && !shift) { setCapsLockOn(true); }
});
};
var bindingForOthersDevices = function () {
var isKeyPressed = true;
$document.bind("keydown", function (event) {
if (!isKeyPressed && event.keyCode === 20) {
isKeyPressed = true;
if ($rootScope.isCapsLockOn != null) { setCapsLockOn(!$rootScope.isCapsLockOn); }
}
});
$document.bind("keyup", function (event) {
if (event.keyCode === 20) { isKeyPressed = false; }
});
$document.bind("keypress", function (event) {
var code = event.charCode || event.keyCode;
var shift = event.shiftKey;
if (code > 96 && code < 123) { setCapsLockOn(shift); }
if (code > 64 && code < 91) { setCapsLockOn(!shift); }
});
};
if (/Mac|iPad|iPhone|iPod/.test(navigator.platform)) {
bindingForAppleDevice();
} else {
bindingForOthersDevices();
}
var setCapsLockOn = function (isOn) {
$timeout(function () {
$rootScope.isCapsLockOn = isOn;
});
};
}]);
}());

View File

@ -0,0 +1 @@
!function(){"use strict";angular.module("ngCapsLock",[]).run(["$rootScope","$document","$timeout",function(n,o,e){var i=function(){o.bind("keydown",function(n){20===n.keyCode&&c(!0)}),o.bind("keyup",function(n){20===n.keyCode&&c(!1)}),o.bind("keypress",function(n){var o=n.charCode||n.keyCode,e=n.shiftKey;o>96&&123>o&&c(!1),o>64&&91>o&&!e&&c(!0)})},t=function(){var e=!0;o.bind("keydown",function(o){e||20!==o.keyCode||(e=!0,null!=n.isCapsLockOn&&c(!n.isCapsLockOn))}),o.bind("keyup",function(n){20===n.keyCode&&(e=!1)}),o.bind("keypress",function(n){var o=n.charCode||n.keyCode,e=n.shiftKey;o>96&&123>o&&c(e),o>64&&91>o&&c(!e)})};/Mac|iPad|iPhone|iPod/.test(navigator.platform)?i():t();var c=function(o){e(function(){n.isCapsLockOn=o})}}])}();