1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-29 18:52:22 +01:00

Merge branch 'dev' for release 2.3.0

This commit is contained in:
Sylvain 2016-06-28 10:23:57 +02:00
commit 59efdcf880
96 changed files with 1689 additions and 154 deletions

1
.gitignore vendored
View File

@ -18,6 +18,7 @@
/public/uploads
/public/assets
/public/api
# Ignore application configurations
/config/application.yml

View File

@ -1,5 +1,17 @@
# Changelog Fab Manager
## v2.3.0 2016 June 28
- Public API with access management and online documentation
- Add json cache for machines, events, trainings
- Optimise sql query, avoid to N+1
- Projects URL are always composed with slug instead of ID
- Confirmation on project deletion
- Fix a bug: unable to deploy 2.2.0+ when PostgreSQL 'unaccent' extension was already active
- Fix a bug: some reservations was referencing reservables not present in database (#patch)
- [TODO DEPLOY] `bundle exec rake fablab:fix:reservations_not_existing_reservable` to apply #patch
- [TODO DEPLOY] `bundle install` and `rake db:migrate`
## v2.2.2 2016 June 23
- Fix some bugs: users with uncompleted account (sso imported) won't appear in statistics, in listings and in searches. Moreover, they won't block statistics generation
- Fix a bug: unable to display next results in statistics tables
@ -41,4 +53,4 @@
- Fix a bug: custom asset favicon-file favicon file is not set
- Fix a security issue: stripe card token is now checked on server side on new/renew subscription
- Translated notification e-mails into english language
- Subscription extension logic has been extracted into a microservice
- Subscription extension logic has been extracted into a microservice

View File

@ -16,7 +16,8 @@ gem 'therubyracer', '= 0.12.0', platforms: :ruby
# Use jquery as the JavaScript library
gem 'jquery-rails'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.0'
gem 'jbuilder', '~> 2.5'
gem 'jbuilder_cache_multi'
# bundle exec rake doc:rails generates the API under doc/api.
gem 'sdoc', '~> 0.4.0', group: :doc #TODO remove unused ?
@ -139,3 +140,7 @@ gem 'protected_attributes'
gem 'message_format'
gem 'openlab_ruby'
gem 'api-pagination'
gem 'has_secure_token'
gem 'apipie-rails'

View File

@ -42,6 +42,9 @@ GEM
tzinfo (~> 1.1)
addressable (2.3.8)
ansi (1.5.0)
api-pagination (4.3.0)
apipie-rails (0.3.6)
json
arel (6.0.3)
autoprefixer-rails (5.1.8)
execjs
@ -164,6 +167,8 @@ GEM
activerecord (>= 4.0.0)
globalid (0.3.6)
activesupport (>= 4.1.0)
has_secure_token (1.0.0)
activerecord (>= 3.0)
hashdiff (0.3.0)
hashie (3.4.2)
highline (1.7.1)
@ -179,9 +184,11 @@ GEM
multi_xml (>= 0.5.2)
i18n (0.7.0)
ice_nine (0.11.1)
jbuilder (2.2.12)
activesupport (>= 3.0.0, < 5)
jbuilder (2.5.0)
activesupport (>= 3.0.0, < 5.1)
multi_json (~> 1.2)
jbuilder_cache_multi (0.0.3)
jbuilder (>= 1.5.0, < 3)
jquery-rails (4.0.3)
rails-dom-testing (~> 1.0)
railties (>= 4.2.0)
@ -208,13 +215,13 @@ GEM
mime-types (2.99)
mini_magick (4.2.0)
mini_portile2 (2.0.0)
minitest (5.8.4)
minitest (5.9.0)
minitest-reporters (1.1.8)
ansi
builder
minitest (>= 5.0)
ruby-progressbar
multi_json (1.11.2)
multi_json (1.12.1)
multi_xml (0.5.5)
multipart-post (2.0.0)
naught (1.0.0)
@ -430,6 +437,8 @@ DEPENDENCIES
aasm
actionpack-page_caching
active_record_query_trace
api-pagination
apipie-rails
awesome_print
bootstrap-sass
byebug
@ -452,7 +461,9 @@ DEPENDENCIES
foreman
forgery
friendly_id (~> 5.1.0)
jbuilder (~> 2.0)
has_secure_token
jbuilder (~> 2.5)
jbuilder_cache_multi
jquery-rails
kaminari
letter_opener
@ -497,4 +508,4 @@ DEPENDENCIES
webmock
BUNDLED WITH
1.11.2
1.12.5

View File

@ -0,0 +1,69 @@
Application.Controllers.controller "OpenAPIClientsController", ["$scope", 'clientsPromise', 'growl', 'OpenAPIClient', 'dialogs', '_t'
, ($scope, clientsPromise, growl, OpenAPIClient, dialogs, _t) ->
### PUBLIC SCOPE ###
## clients list
$scope.clients = clientsPromise
$scope.order = null
$scope.clientFormVisible = false
$scope.client = {}
$scope.toggleForm = ->
$scope.clientFormVisible = !$scope.clientFormVisible
# Change the order criterion to the one provided
# @param orderBy {string} ordering criterion
##
$scope.setOrder = (orderBy)->
if $scope.order == orderBy
$scope.order = '-'+orderBy
else
$scope.order = orderBy
$scope.saveClient = (client)->
if client.id?
OpenAPIClient.update { id: client.id }, open_api_client: client, (clientResp)->
client = clientResp
growl.success(_t('client_successfully_updated'))
else
OpenAPIClient.save open_api_client: client, (client)->
$scope.clients.push client
growl.success(_t('client_successfully_created'))
$scope.clientFormVisible = false
$scope.clientForm.$setPristine()
$scope.client = {}
$scope.editClient = (client)->
$scope.clientFormVisible = true
$scope.client = client
$scope.deleteClient = (index)->
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_delete_this_open_api_client')
, ->
OpenAPIClient.delete { id: $scope.clients[index].id }, ->
$scope.clients.splice(index, 1)
growl.success(_t('client_successfully_deleted'))
$scope.resetToken = (client)->
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_revoke_this_open_api_access')
, ->
OpenAPIClient.resetToken { id: client.id }, {}, (clientResp)->
client.token = clientResp.token
growl.success(_t('access_successfully_revoked'))
]

View File

@ -175,8 +175,8 @@ Application.Controllers.controller 'NewPlanController', ['$scope', '$uibModal',
##
# Controller used in the plan edition form
##
Application.Controllers.controller 'EditPlanController', ['$scope', 'groups', 'plans', 'planPromise', 'machines', 'prices', 'partners', 'CSRF', '$state', '$stateParams', 'growl', '$filter', '_t', '$locale'
, ($scope, groups, plans, planPromise, machines, prices, partners, CSRF, $state, $stateParams, growl, $filter, _t, $locale) ->
Application.Controllers.controller 'EditPlanController', ['$scope', 'groups', 'plans', 'planPromise', 'machines', 'prices', 'partners', 'CSRF', '$state', '$stateParams', 'growl', '$filter', '_t', '$locale', 'Plan'
, ($scope, groups, plans, planPromise, machines, prices, partners, CSRF, $state, $stateParams, growl, $filter, _t, $locale, Plan) ->
@ -207,12 +207,13 @@ Application.Controllers.controller 'EditPlanController', ['$scope', 'groups', 'p
##
$scope.copyPricesFromPlan = ->
if $scope.plan.parent
parentPlan = $scope.getPlanFromId($scope.plan.parent)
for parentPrice in parentPlan.prices
for childKey, childPrice of $scope.plan.prices
if childPrice.priceable_type == parentPrice.priceable_type and childPrice.priceable_id == parentPrice.priceable_id
$scope.plan.prices[childKey].amount = parentPrice.amount
break
Plan.get {id: $scope.plan.parent}, (parentPlan) ->
for parentPrice in parentPlan.prices
for childKey, childPrice of $scope.plan.prices
if childPrice.priceable_type == parentPrice.priceable_type and childPrice.priceable_id == parentPrice.priceable_id
$scope.plan.prices[childKey].amount = parentPrice.amount
break
# if no plan were selected, unset every prices
else
for key, price of $scope.plan.prices
@ -257,4 +258,4 @@ Application.Controllers.controller 'EditPlanController', ['$scope', 'groups', 'p
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]
]

View File

@ -8,7 +8,7 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
### PUBLIC SCOPE ###
## List of machines prices (not considering any plan)
$scope.machinesPrices = machinesPricesPromise.prices
$scope.machinesPrices = machinesPricesPromise
## List of trainings pricing
$scope.trainingsPricings = trainingsPricingsPromise

View File

@ -96,6 +96,11 @@ Application.Controllers.controller "MainNavController", ["$scope", "$location",
linkText: 'customization'
linkIcon: 'gear'
}
{
state: 'app.admin.open_api_clients'
linkText: 'open_api_clients'
linkIcon: 'cloud'
}
].concat(Fablab.adminNavLinks)
$scope.adminNavLinks = Fablab.adminNavLinks

View File

@ -70,7 +70,7 @@ class ProjectsController
$('section[ui-view=main]').scrollTop(0, 200)
return
else
$state.go('app.public.projects_show', {id: content.id})
$state.go('app.public.projects_show', {id: content.slug})
@ -338,8 +338,8 @@ Application.Controllers.controller "EditProjectController", ["$scope", "$state",
##
# Controller used in the public project's details page
##
Application.Controllers.controller "ShowProjectController", ["$scope", "$state", "projectPromise", '$location', '$uibModal', '_t'
, ($scope, $state, projectPromise, $location, $uibModal, _t) ->
Application.Controllers.controller "ShowProjectController", ["$scope", "$state", "projectPromise", '$location', '$uibModal', 'dialogs', '_t'
, ($scope, $state, projectPromise, $location, $uibModal, dialogs, _t) ->
### PUBLIC SCOPE ###
@ -383,8 +383,14 @@ Application.Controllers.controller "ShowProjectController", ["$scope", "$state",
# check the permissions
if $scope.currentUser.role is 'admin' or $scope.projectDeletableBy($scope.currentUser)
# delete the project then refresh the projects list
$scope.project.$delete ->
$state.go('app.public.projects_list', {}, {reload: true})
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_delete_this_project')
, -> # cancel confirmed
$scope.project.$delete ->
$state.go('app.public.projects_list', {}, {reload: true})
else
console.error _t('unauthorized_operation')

View File

@ -330,7 +330,7 @@ angular.module('application.router', ['ui.router']).
controller: 'ReserveMachineController'
resolve:
plansPromise: ['Plan', (Plan)->
Plan.query(attributes_requested: "['machines_credits']").$promise
Plan.query().$promise
]
groupsPromise: ['Group', (Group)->
Group.query().$promise
@ -362,7 +362,6 @@ angular.module('application.router', ['ui.router']).
translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.machines_edit', 'app.shared.machine']).$promise
]
# trainings
.state 'app.logged.trainings_reserve',
url: '/trainings/reserve'
@ -375,7 +374,7 @@ angular.module('application.router', ['ui.router']).
Setting.get(name: 'training_explications_alert').$promise
]
plansPromise: ['Plan', (Plan)->
Plan.query(attributes_requested: "['trainings_credits']").$promise
Plan.query().$promise
]
groupsPromise: ['Group', (Group)->
Group.query().$promise
@ -423,7 +422,7 @@ angular.module('application.router', ['ui.router']).
Setting.get(name: 'subscription_explications_alert').$promise
]
plansPromise: ['Plan', (Plan)->
Plan.query(shallow: true).$promise
Plan.query().$promise
]
groupsPromise: ['Group', (Group)->
Group.query().$promise
@ -876,5 +875,19 @@ angular.module('application.router', ['ui.router']).
Translations.query('app.admin.settings').$promise
]
# OpenAPI Clients
.state 'app.admin.open_api_clients',
url: '/open_api_clients'
views:
'main@':
templateUrl: '<%= asset_path "admin/open_api_clients/index.html" %>'
controller: 'OpenAPIClientsController'
resolve:
clientsPromise: ['OpenAPIClient', (OpenAPIClient)->
OpenAPIClient.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query('app.admin.open_api_clients').$promise
]
]

View File

@ -0,0 +1,11 @@
'use strict'
Application.Services.factory 'OpenAPIClient', ["$resource", ($resource)->
$resource "/api/open_api_clients/:id",
{id: "@id"},
resetToken:
method: 'PATCH'
url: "/api/open_api_clients/:id/reset_token"
update:
method: 'PUT'
]

View File

@ -4,7 +4,7 @@ Application.Services.factory 'Price', ["$resource", ($resource)->
$resource "/api/prices/:id",
{},
query:
isArray: false
isArray: true
update:
method: 'PUT'
compute:

View File

@ -335,6 +335,11 @@ p, .widget p {
}
}
.exponent {
font-size: 0.7em;
vertical-align: super
}
@media screen and (min-width: $screen-lg-min) {
.b-r-lg {border-right: 1px solid $border-color; }
}

View File

@ -0,0 +1,83 @@
<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-7 b-l b-r-md">
<section class="heading-title">
<h1 translate>{{ 'open_api_clients' }}</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 href="<%= apipie_apipie_path({version: 'v1'}) %>" target="_blank" class="btn btn-info b-2x rounded m-t-sm">
<i class="fa fa-book" aria-hidden="true"></i>&nbsp;
<span translate>{{ 'api_documentation' }}</span>&nbsp;
<span class="exponent"><i class="fa fa-external-link" aria-hidden="true"></i></span>
</a>
</section>
</div>
</div>
</section>
<section class="m-lg">
<div class="row">
<div class="col-md-12">
<div class="col-md-12">
<button type="button" class="btn btn-warning m-t m-b" ng-click="toggleForm()" ng-show="!clientFormVisible" translate>{{ 'add_new_client' | translate }}</button>
<form role="form" id="clientForm" ng-show="clientFormVisible" name="clientForm" class="form-inline m-b m-t" novalidate>
<div class="form-group" ng-class="{'has-error': clientForm['client[name]'].$dirty && clientForm['client[name]'].$invalid}">
<input class="form-control" type="text" name="client[name]" ng-model="client.name" value="" placeholder="{{ 'client_name' | translate }}" required>
</div>
<button class="btn btn-default" ng-click="toggleForm()" name="button">{{ 'cancel' | translate }}</button>
<input type="submit" class="btn btn-warning" ng-disabled="!client.name || client.name.length == 0" ng-click="saveClient(client)" value="{{ 'save' | translate }}">
</form>
<table class="table">
<thead>
<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: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>
</tr>
</thead>
<tbody>
<tr ng-repeat="client in clients | orderBy: order">
<td>{{ client.name }}</td>
<td>{{ client.calls_count }}</td>
<td>{{ client.token }}</td>
<td>{{ client.created_at | amDateFormat: 'LL' }}</td>
<td>
<div class="buttons">
<button class="btn btn-default" ng-click="editClient(client)">
<i class="fa fa-pencil"></i> {{ 'edit' | translate }}
</button>
<button class="btn btn-default" ng-click="resetToken(client)">
<i class="fa fa-times"></i> {{ 'reset_token' | translate }}
</button>
<button class="btn btn-danger" ng-click="deleteClient($index)" ng-show="client.calls_count == 0">
<i class="fa fa-trash"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>

View File

@ -510,5 +510,3 @@
</div>
</div>

View File

@ -3,7 +3,7 @@ class API::AdminsController < API::ApiController
def index
authorize :admin
@admins = User.admins
@admins = User.includes(profile: [:user_avatar]).admins
end
def create

View File

@ -49,21 +49,22 @@ class API::AvailabilitiesController < API::ApiController
else
@user = current_user
end
@current_user_role = current_user.is_admin? ? 'admin' : 'user'
@machine = Machine.find(params[:machine_id])
@slots = []
@reservations = Reservation.where('reservable_type = ? and reservable_id = ?', @machine.class.to_s, @machine.id).joins(:slots).where('slots.start_at > ?', Time.now)
@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.where("end_at > ? AND available_type = 'machines'", Time.now)
@availabilities = @machine.availabilities.includes(:tags).where("end_at > ? AND available_type = 'machines'", Time.now)
else
end_at = 1.month.since
end_at = 3.months.since if is_subscription_year(@user)
@availabilities = @machine.availabilities.includes(:availability_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]))
@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
@availabilities.each do |a|
((a.end_at - a.start_at)/SLOT_DURATION.minutes).to_i.times do |i|
if (a.start_at + (i * SLOT_DURATION).minutes) > Time.now
slot = Slot.new(start_at: a.start_at + (i*SLOT_DURATION).minutes, end_at: a.start_at + (i*SLOT_DURATION).minutes + SLOT_DURATION.minutes, availability_id: a.id, machine: @machine, title: '')
slot = verify_machine_is_reserved(slot, @reservations)
slot = Slot.new(start_at: a.start_at + (i*SLOT_DURATION).minutes, end_at: a.start_at + (i*SLOT_DURATION).minutes + 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
end
@ -77,13 +78,13 @@ class API::AvailabilitiesController < API::ApiController
@user = current_user
end
@slots = []
@reservations = @user.reservations.where("reservable_type = 'Training'").joins(:slots).where('slots.start_at > ?', Time.now)
@reservations = @user.reservations.includes(:slots).references(:slots).where("reservable_type = 'Training' AND slots.start_at > ?", Time.now)
if @user.is_admin?
@availabilities = Availability.trainings.where('start_at > ?', Time.now)
@availabilities = Availability.includes(:tags, :slots, trainings: [:machines]).trainings.where('availabilities.start_at > ?', Time.now)
else
end_at = 1.month.since
end_at = 3.months.since if can_show_slot_plus_three_months(@user)
@availabilities = Availability.trainings.includes(:availability_tags).where('start_at > ? AND start_at < ?', Time.now, end_at).where('availability_tags.tag_id' => @user.tag_ids.concat([nil]))
@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]))
end
@availabilities.each do |a|
a = verify_training_is_reserved(a, @reservations)
@ -115,15 +116,14 @@ class API::AvailabilitiesController < API::ApiController
is_reserved
end
def verify_machine_is_reserved(slot, reservations)
user = current_user
def verify_machine_is_reserved(slot, reservations, user, user_role)
reservations.each do |r|
r.slots.each do |s|
if s.start_at == slot.start_at and s.canceled_at == nil
slot.id = s.id
slot.is_reserved = true
slot.title = t('availabilities.not_available')
slot.can_modify = true if user.is_admin?
slot.can_modify = true if user_role === 'admin'
slot.reservation = r
end
if s.start_at == slot.start_at and r.user == user and s.canceled_at == nil

View File

@ -4,13 +4,14 @@ class API::EventsController < API::ApiController
def index
@events = policy_scope(Event)
@total = @events.count
@events = @events.page(params[:page]).per(12)
@page = params[:page]
@events = @events.page(@page).per(12)
end
# GET /events/upcoming/:limit
def upcoming
limit = params[:limit]
@events = Event.includes(:event_image, :event_files, :availability)
@events = Event.includes(:event_image, :event_files, :availability, :categories)
.where('availabilities.start_at >= ?', Time.now)
.order('availabilities.start_at ASC').references(:availabilities).limit(limit)
end

View File

@ -17,7 +17,7 @@ class API::MembersController < API::ApiController
end
def last_subscribed
@query = User.active.with_role(:member).includes(:profile).where('is_allow_contact = true AND confirmed_at IS NOT NULL').order('created_at desc').limit(params[:last])
@query = User.active.with_role(:member).includes(profile: [:user_avatar]).where('is_allow_contact = true AND confirmed_at IS NOT NULL').order('created_at desc').limit(params[:last])
# remove unmerged profiles from list
@members = @query.to_a
@ -178,7 +178,7 @@ class API::MembersController < API::ApiController
order_key = 'users.id'
end
@query = User.includes(:profile, :group)
@query = User.includes(:profile, :group, :subscriptions)
.joins(:profile, :group, :roles, 'LEFT JOIN "subscriptions" ON "subscriptions"."user_id" = "users"."id" LEFT JOIN "plans" ON "plans"."id" = "subscriptions"."plan_id"')
.where("users.is_active = 'true' AND roles.name = 'member'")
.order("#{order_key} #{direction}")

View File

@ -0,0 +1,46 @@
class API::OpenAPIClientsController < API::ApiController
before_action :authenticate_user!
def index
authorize OpenAPI::Client
@clients = OpenAPI::Client.order(:created_at)
end
# add authorization
def create
@client = OpenAPI::Client.new(client_params)
authorize @client
if @client.save
render status: :created
else
render json: @client.errors, status: :unprocessable_entity
end
end
def update
@client = OpenAPI::Client.find(params[:id])
authorize @client
if @client.update(client_params)
render status: :ok
else
render json: @client.errors, status: :unprocessable_entity
end
end
def reset_token
@client = OpenAPI::Client.find(params[:id])
authorize @client
@client.regenerate_token
end
def destroy
@client = OpenAPI::Client.find(params[:id])
authorize @client
@client.destroy
head 204
end
private
def client_params
params.require(:open_api_client).permit(:name)
end
end

View File

@ -2,14 +2,9 @@
before_action :authenticate_user!, except: [:index]
def index
@attributes_requested = params[:attributes_requested]
@plans = Plan.all
@plans = Plan.includes(:plan_file)
@plans = @plans.where(group_id: params[:group_id]) if params[:group_id]
if params[:shallow]
render :shallow_index
else
render :index
end
render :index
end
def show

View File

@ -2,7 +2,7 @@ class API::TrainingsPricingsController < API::ApiController
before_action :authenticate_user!
def index
@trainings_pricings = TrainingsPricing.includes(:training)
@trainings_pricings = TrainingsPricing.all
end
def update

View File

@ -0,0 +1,43 @@
class OpenAPI::V1::BaseController < ActionController::Base
protect_from_forgery with: :null_session
before_action :authenticate
before_action :increment_calls_count
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from OpenAPI::ParameterError, with: :bad_request
rescue_from ActionController::ParameterMissing, with: :bad_request
helper_method :current_api_client
protected
def not_found
render json: { errors: ["Not found"] }, status: :not_found
end
def bad_request
render json: { errors: ["Bad request"] }, status: :bad_request
end
def authenticate
authenticate_token || render_unauthorized
end
def authenticate_token
authenticate_with_http_token do |token, options|
@open_api_client = OpenAPI::Client.find_by(token: token)
end
end
def current_api_client
@open_api_client
end
def render_unauthorized
render json: { errors: ['Bad credentials'] }, status: 401
end
private
def increment_calls_count
@open_api_client.increment_calls_count
end
end

View File

@ -0,0 +1,40 @@
class OpenAPI::V1::BookableMachinesController < OpenAPI::V1::BaseController
extend OpenAPI::ApiDoc
expose_doc
def index
raise ActionController::ParameterMissing if params[:user_id].blank?
@machines = Machine.all
@machines = @machines.where(id: params[:machine_id]) if params[:machine_id].present?
@machines = @machines.to_a
user = User.find(params[:user_id])
@machines.delete_if do |machine|
(machine.trainings.count != 0) and !user.is_training_machine?(machine)
end
@hours_remaining = Hash[@machines.map { |m| [m.id, 0] }]
if user.subscription
plan_id = user.subscription.plan_id
@machines.each do |machine|
credit = Credit.find_by(plan_id: plan_id, creditable: machine)
users_credit = user.users_credits.find_by(credit: credit) if credit
if credit
@hours_remaining[machine.id] = credit.hours - (users_credit.try(:hours_used) || 0)
else
@hours_remaining[machine.id] = 0
end
end
end
end
end

View File

@ -0,0 +1,18 @@
class OpenAPI::V1::EventsController < OpenAPI::V1::BaseController
extend OpenAPI::ApiDoc
expose_doc
def index
@events = Event.order(created_at: :desc)
if params[:page].present?
@events = @events.page(params[:page]).per(per_page)
paginate @events, per_page: per_page
end
end
private
def per_page
params[:per_page] || 20
end
end

View File

@ -0,0 +1,27 @@
class OpenAPI::V1::InvoicesController < OpenAPI::V1::BaseController
extend OpenAPI::ApiDoc
expose_doc
def index
@invoices = Invoice.order(created_at: :desc)
if params[:user_id].present?
@invoices = @invoices.where(user_id: params[:user_id])
end
if params[:page].present?
@invoices = @invoices.page(params[:page]).per(per_page)
paginate @invoices, per_page: per_page
end
end
def download
@invoice = Invoice.find(params[:id])
send_file File.join(Rails.root, @invoice.file), type: 'application/pdf', disposition: 'inline', filename: @invoice.filename
end
private
def per_page
params[:per_page] || 20
end
end

View File

@ -0,0 +1,8 @@
class OpenAPI::V1::MachinesController < OpenAPI::V1::BaseController
extend OpenAPI::ApiDoc
expose_doc
def index
@machines = Machine.order(:created_at)
end
end

View File

@ -0,0 +1,36 @@
class OpenAPI::V1::ReservationsController < OpenAPI::V1::BaseController
extend OpenAPI::ApiDoc
expose_doc
def index
@reservations = Reservation.order(created_at: :desc)
if params[:user_id].present?
@reservations = @reservations.where(user_id: params[:user_id])
else
@reservations = @reservations.includes(user: :profile)
end
if params[:reservable_type].present?
@reservations = @reservations.where(reservable_type: format_type(params[:reservable_type]))
end
if params[:reservable_id].present?
@reservations = @reservations.where(reservable_id: params[:reservable_id])
end
if params[:page].present?
@reservations = @reservations.page(params[:page]).per(per_page)
paginate @reservations, per_page: per_page
end
end
private
def format_type(type)
type.singularize.classify
end
def per_page
params[:per_page] || 20
end
end

View File

@ -0,0 +1,8 @@
class OpenAPI::V1::TrainingsController < OpenAPI::V1::BaseController
extend OpenAPI::ApiDoc
expose_doc
def index
@trainings = Training.order(:created_at)
end
end

View File

@ -0,0 +1,30 @@
class OpenAPI::V1::UserTrainingsController < OpenAPI::V1::BaseController
extend OpenAPI::ApiDoc
expose_doc
def index
@user_trainings = UserTraining.order(created_at: :desc)
if params[:user_id].present?
@user_trainings = @user_trainings.where(user_id: params[:user_id])
else
@user_trainings = @user_trainings.includes(user: :profile)
end
if params[:training_id].present?
@user_trainings = @user_trainings.where(training_id: params[:training_id])
else
@user_trainings = @user_trainings.includes(:training)
end
if params[:page].present?
@user_trainings = @user_trainings.page(params[:page]).per(per_page)
paginate @user_trainings, per_page: per_page
end
end
private
def per_page
params[:per_page] || 20
end
end

View File

@ -0,0 +1,27 @@
class OpenAPI::V1::UsersController < OpenAPI::V1::BaseController
extend OpenAPI::ApiDoc
expose_doc
def index
@users = User.order(created_at: :desc).includes(:group, :profile)
if params[:email].present?
email_param = params[:email].is_a?(String) ? params[:email].downcase : params[:email].map(&:downcase)
@users = @users.where(email: email_param)
end
if params[:user_id].present?
@users = @users.where(id: params[:user_id])
end
if params[:page].present?
@users = @users.page(params[:page]).per(per_page)
paginate @users, per_page: per_page
end
end
private
def per_page
params[:per_page] || 20
end
end

View File

@ -0,0 +1,60 @@
# app/concerns/controllers/api_doc.rb
#
# Controller extension with common API documentation shortcuts
#
module OpenAPI::ApiDoc
# Apipie doesn't allow to append anything to esisting
# description. It raises an error on double definition.
#
def append_desc(desc = "")
_apipie_dsl_data[:description] << desc << "\n"
end
# Converts passed +code+ to the markdown
# by prepending 4 spaces to each line
#
# @param code [String]
# @return [String]
#
def to_markdown_code(code)
code.split("\n").map do |line|
(" " * 4) + line
end.join("\n")
end
# Includes passed list of json schemas
# to method description
#
# @example
# include_response_schema 'users.json', '_user.json'
#
# @param schemas [Array<String>]
#
def include_response_schema(*schemas)
root = Rails.root.join('app/doc/responses')
_apipie_dsl_data[:description] = _apipie_dsl_data[:description].strip_heredoc
append_desc("## Response schema")
schemas.each do |relative_path|
append_desc MarkdownJsonSchema.read(relative_path)
end
end
# Exports all documentation from provided class
#
# @example
# class ProfilesController < ApplicationController
# extend Controllers::ApiDoc
# expose_doc
# # exports all docs from ProfilesDoc class
# # that must be inherired from ApplicationDoc
# end
#
# @see ApplicationDoc
#
def expose_doc(doc_name = "#{controller_path}_doc")
doc_klass = doc_name.classify.constantize
doc_klass.apply(self)
end
end

View File

@ -0,0 +1,92 @@
# app/docs/application_doc.rb
#
# A common class for defining API docs
#
# This class is abstract, to define your own doc
# for controller Api::ResourcesController, create a class
#
# class Api::ResourcesDoc < ApplicationDoc
# resource_description do
# # any method from Apipie
# end
#
# doc_for :action_name do
# # documentation for Api::ResourcesController#action_name
# # using Apipie methods
# end
# end
#
class OpenAPI::ApplicationDoc
extend OpenAPI::ApiDoc
class << self
# Stores provided resource description
# to include it later to the controller class
#
# @param block [Proc]
#
def resource_description(&block)
@_resource_description_block = block
end
# Returns stored resource description (or empty proc)
#
# @return [Proc]
#
def resource_description_block
@_resource_description_block || proc {}
end
# Defines documentation for provided +action_name+
#
# @param action_name [#to_s] should match controller action name
# @param block [Proc] documentation for +action_name+ action
#
def doc_for(action_name, &block)
docs[action_name] = block
end
# Returns mappign action_name => documentation
#
# @return [Hash]
#
def docs
@_docs ||= {}
end
def define_param_group(param_group_name, &block)
param_groups[param_group_name] = block
end
def param_groups
@_param_groups ||= {}
end
# Applies all defined DSL to provided controller class
#
# @param controller [ActionController::Base]
#
def apply(controller)
resource_description_block = self.resource_description_block
docs = self.docs
param_groups = self.param_groups
controller.class_eval do
resource_description(&resource_description_block)
param_groups.each do |param_group_name, block|
instance_eval do
def_param_group param_group_name, &block
end
end
docs.each do |action_name, block|
instance_eval(&block)
define_method(action_name) {}
end
end
end
end
end

View File

@ -0,0 +1,5 @@
class OpenAPI::V1::BaseDoc < OpenAPI::ApplicationDoc
API_VERSION = "v1"
FORMATS = ['json']
PER_PAGE_DEFAULT = 20
end

View File

@ -0,0 +1,72 @@
class OpenAPI::V1::BookableMachinesDoc < OpenAPI::V1::BaseDoc
resource_description do
short 'Bookable machines'
desc 'Machines that a given user is allowed to book (allowed to make a reservation)'
formats FORMATS
api_version API_VERSION
end
doc_for :index do
api :GET, "/#{API_VERSION}/bookable_machines", "Bookable machines index"
description "Machines that a given user is allowed to book."
param :user_id, Integer, required: true, desc: "Id of the given user."
example <<-EOS
# /open_api/v1/bookable_machines?user_id=522
{
"machines": [
{
"id": 3,
"name": "Shopbot / Grande fraiseuse",
"slug": "shopbot-grande-fraiseuse",
"updated_at": "2014-08-19T11:01:12.919+02:00",
"created_at": "2014-06-30T03:32:31.982+02:00",
"description": "La fraiseuse numériq ... ",
"spec": "Surface maximale de travail: 244 ... "
"hours_remaining": 0
},
{
"id": 5,
"name": "Petite Fraiseuse",
"slug": "petite-fraiseuse",
"updated_at": "2014-06-30T14:33:37.638+02:00",
"created_at": "2014-06-30T03:32:31.989+02:00",
"description": "La fraiseuse numérique Roland Modela MDX-20 ... ",
"spec": "Taille du plateau X/Y : 220 mm x 1 ... "
"hours_remaining": 0
},
{
"id": 2,
"name": "Découpeuse vinyle",
"slug": "decoupeuse-vinyle",
"updated_at": "2014-06-30T15:10:14.272+02:00",
"created_at": "2014-06-30T03:32:31.977+02:00",
"description": "La découpeuse Vinyle, Roland CAMM ...",
"spec": "Largeurs de support acceptées: de 50 mm à 70 ... 50 cm/sec ... mécanique: 0,0125 mm/pas\r\n",
"hours_remaining": 0
},
{
"id": 1,
"name": "Epilog EXT36 Laser",
"slug": "decoupeuse-laser",
"updated_at": "2015-02-17T11:06:00.495+01:00",
"created_at": "2014-06-30T03:32:31.972+02:00",
"description": "La découpeuse Laser, ... ",
"spec": "Puissance : 40W Surface de trav ... ",
"hours_remaining": 0
},
{
"id": 4,
"name": "Imprimante 3D - Ultimaker",
"slug": "imprimante-3d",
"updated_at": "2014-12-11T15:47:02.215+01:00",
"created_at": "2014-06-30T03:32:31.986+02:00",
"description": "L'imprimante 3D U ... ",
"spec": "Surface maximale de travai sés: PLA (en stock).",
"hours_remaining": 10
},
# ...
]
}
EOS
end
end

View File

@ -0,0 +1,30 @@
module OpenAPI::V1::Concerns::ParamGroups
extend ActiveSupport::Concern
included do
define_param_group :pagination do
param :page, Integer, desc: "Page number", optional: true
param :per_page, Integer, desc: "Number of objects per page. Default is #{OpenAPI::V1::BaseDoc::PER_PAGE_DEFAULT}.", optional: true
end
# define_param_group :order_type do
# param :order_type, ['asc', 'desc'], desc: "order type: descendant or ascendant. Default value is *desc*."
# end
#
# define_param_group :filter_by_tags do
# param :tagged_with, [String, Array], desc: 'If multiple tags are given, we use an *OR* function. See parameter *order_by_matching_tag_count* to order the result. It can also be a *comma* *separated* *string*. Example: tagged_with=science,museum'
# param :order_by_matching_tag_count, ['t',1,'true'], desc: "You can use this parameter if you are sending a parameter *tagged_with*. Send this parameter to order by number of matching tags (descendant): result will be sort firstly by matching tags and secondly by order given by *order_by* parameter. Default to *false*."
# end
#
# define_param_group :filter_by_blog do
# param :blog_slug, String, desc: "Send the blog's *slug* to only return articles belonging to specific blog."
# end
#
# define_param_group :filter_by_geolocation do
# param :latitude, Numeric, desc: "Latitude. Example: *45.166670*"
# param :longitude, Numeric, desc: "Longitude. Example: *5.7166700*"
# param :radius, Numeric, desc: "To be combined with parameters latitude and longitude. Default to *10*."
# param :order_by_distance, ['t',1,'true'], desc: "You can use this parameter if you are sending parameters *latitude* and *longitude*. Send this parameter to order by distance (descendant): result will be sort firstly by distance and secondly by order given by *order_by* parameter. Default to *false*."
# end
end
end

View File

@ -0,0 +1,45 @@
class OpenAPI::V1::EventsDoc < OpenAPI::V1::BaseDoc
resource_description do
short 'Events'
desc 'Events of Fab-manager'
formats FORMATS
api_version API_VERSION
end
include OpenAPI::V1::Concerns::ParamGroups
doc_for :index do
api :GET, "/#{API_VERSION}/events", "Events index"
param_group :pagination
description "Events index. Order by *created_at* desc."
example <<-EOS
# /open_api/v1/events?page=1&per_page=2
{
"events": [
{
"id": 183,
"title": "OPEN LAB",
"description": "Que vous soyez Fab user, visiteur, curieux ou bricoleur, latelier de fabrication numérique vous ouvre ses portes les mercredis soirs pour avancer vos projets ou rencontrer la «communauté» Fab Lab. \r\n\r\nCe soir, venez spécialement découvrir les machines à commandes numérique du Fab Lab de La Casemate, venez comprendre ce lieux ouvert à tous. \r\n\r\n\r\nVenez découvrir un concept, une organisation, des machines, pour stimuler votre sens de la créativité.",
"updated_at": "2016-04-25T10:49:40.055+02:00",
"created_at": "2016-04-25T10:49:40.055+02:00",
"amount": 0,
"reduced_amount": 0,
"nb_total_places": 18,
"nb_free_places": 16
},
{
"id": 182,
"title": "ATELIER SKATE : SEANCE 1",
"description": "Envie de rider à travers Grenoble sur une planche unique ? Envie de découvrir la fabrication éco-responsable d'un skate ? Alors bienvenue à l'atelier Skate Board du Fablab ! Encadré par Ivan Mago et l'équipe du FabLab, vous réaliserez votre planche (skate, longboard,...) depuis son design jusqu'à sa décoration sur 4 séances.\r\n\r\nLe tarif 50€ inclut la participation aux ateliers, l'utilisations des machines, et tout le matériel de fabrication (bois+colle+grip+vinyle).\r\n\r\nCette première séance sera consacré au design de votre planche et à la découpe des gabarits. N'hésitez pas à venir avec votre ordinateur et vos logiciels de création 2D si vous le souhaitez.\r\n\r\nNous vous attendons nombreux !",
"updated_at": "2016-04-11T17:40:15.146+02:00",
"created_at": "2016-04-11T17:40:15.146+02:00",
"amount": 5000,
"reduced_amount": null,
"nb_total_places": 8,
"nb_free_places": 0
}
]
}
EOS
end
end

View File

@ -0,0 +1,78 @@
class OpenAPI::V1::InvoicesDoc < OpenAPI::V1::BaseDoc
resource_description do
short 'Invoices'
desc 'Invoices'
formats FORMATS
api_version API_VERSION
end
include OpenAPI::V1::Concerns::ParamGroups
doc_for :index do
api :GET, "/#{API_VERSION}/invoices", "Invoices index"
description "Index of users' invoices, with optional pagination. Order by *created_at* descendant."
param_group :pagination
param :user_id, [Integer, Array], optional: true, desc: "Scope the request to one or various users."
example <<-EOS
# /open_api/v1/invoices?user_id=211&page=1&per_page=3
{
"invoices": [
{
"id": 2809,
"invoiced_id": 3257,
"user_id": 211,
"invoiced_type": "Reservation",
"stp_invoice_id": "in_187DLE4zBvgjueAZ6L7SyQlU",
"reference": "1605017/VL",
"total": 1000,
"type": null,
"description": null,
"invoice_url": "/open_api/v1/invoices/2809/download",
"invoiced": {
"created_at": "2016-05-04T01:54:16.686+02:00"
}
},
{
"id": 2783,
"invoiced_id": 3229,
"user_id": 211,
"invoiced_type": "Reservation",
"stp_invoice_id": "in_185Hmt4zBvgjueAZl5lio1pK",
"reference": "1604176/VL",
"total": 2000,
"type": null,
"description": null,
"invoice_url": "/open_api/v1/invoices/2783/download",
"invoiced": {
"created_at": "2016-04-28T18:14:52.524+02:00"
}
},
{
"id": 2773,
"invoiced_id": 3218,
"user_id": 211,
"invoiced_type": "Reservation",
"stp_invoice_id": "in_184oNK4zBvgjueAZJdOxHJjT",
"reference": "1604166/VL",
"total": 2000,
"type": null,
"description": null,
"invoice_url": "/open_api/v1/invoices/2773/download",
"invoiced": {
"created_at": "2016-04-27T10:50:30.806+02:00"
}
}
]
}
EOS
end
doc_for :download do
api :GET, "/#{API_VERSION}/invoices/:id/download", "Download an invoice"
param :id, Integer, desc: "Invoice id", required: true
example <<-EOS
# /open_api/v1/invoices/2809/download
EOS
end
end

View File

@ -0,0 +1,77 @@
class OpenAPI::V1::MachinesDoc < OpenAPI::V1::BaseDoc
resource_description do
short 'Machines'
desc 'Machines of Fab-manager'
formats FORMATS
api_version API_VERSION
end
doc_for :index do
api :GET, "/#{API_VERSION}/machines", "Machines index"
description "Machines index. Order by *created_at* ascendant."
example <<-EOS
# /open_api/v1/machines
{
"machines": [
{
"id": 1,
"name": "Epilog EXT36 Laser",
"slug": "decoupeuse-laser",
"updated_at": "2015-02-17T11:06:00.495+01:00",
"created_at": "2014-06-30T03:32:31.972+02:00",
"description": "La découpeuse Laser, EPILOG Legend 36EXT\r\n\r\nInformations générales :\r\nLa découpeuse laser vous permet de découper ou graver des matériaux. \r\n\r\nPour la découpe, il suffit d'apporter votre fichier vectorisé type illustrator, svg ou dxf avec des \"lignes de coupe\" d'une épaisseur inférieure à 0,01 mm et la machine s'occupera du reste!\r\n\r\nLa gravure est basée sur le spectre noir et blanc. Les nuances sont obtenues par différentes profondeurs de gravure correspondant aux niveaux de gris de votre image. Il suffit pour cela d'apporter une image scannée ou un fichier photo en noir et blanc pour pouvoir reproduire celle-ci sur votre support.\r\n\r\nTypes de matériaux gravables/découpeables ?\r\nDu bois au tissu, du plexiglass au cuir, cette machine permet de découper et graver la plupart des matériaux sauf les métaux. La gravure est néanmoins possible sur les métaux recouverts d'une couche de peinture ou les aluminiums anodisés. \r\nConcernant l'épaisseur des matériaux découpés, il est préférable de ne pas dépasser 5 mm pour le bois et 6 mm pour le plexiglass.\r\n",
"spec": "Puissance : 40W\r\nSurface de travail : 914x609 mm \r\nEpaisseur maximale de la matière : 305mm\r\nSource laser : tube laser type CO2\r\nContrôles de vitesse et de puissance : ces deux paramètres sont ajustables en fonction du matériau (de 1% à 100%) .\r\n"
},
{
"id": 2,
"name": "Découpeuse vinyle",
"slug": "decoupeuse-vinyle",
"updated_at": "2014-06-30T15:10:14.272+02:00",
"created_at": "2014-06-30T03:32:31.977+02:00",
"description": "La découpeuse Vinyle, Roland CAMM-1 GX24\r\n\r\nInformations générales :\r\nEnvie de réaliser un tee shirt personnalisé ? Un sticker à l'effigie votre groupe préféré? Un masque pour la réalisation d'un circuit imprimé? Pour cela, il suffit simplement de venir avec votre fichier vectorisé (ne pas oublier de vectoriser les textes) type illustrator svg ou dxf.\r\n \r\nMatériaux utilisés :\r\nCette machine permet de découper principalement : vinyle, vinyle réfléchissant et flex.\r\n",
"spec": "Largeurs de support acceptées: de 50 mm à 700 mm\r\nVitesse de découpe: 50 cm/sec\r\nRésolution mécanique: 0,0125 mm/pas\r\n"
},
{
"id": 3,
"name": "Shopbot / Grande fraiseuse",
"slug": "shopbot-grande-fraiseuse",
"updated_at": "2014-08-19T11:01:12.919+02:00",
"created_at": "2014-06-30T03:32:31.982+02:00",
"description": "La fraiseuse numérique ShopBot PRS standard\r\n\r\nInformations générales :\r\nCette machine est une fraiseuse 3 axes, idéale pour l'usinage de pièces de grandes dimensions. De la réalisation d'une chaise ou d'un meuble à la construction d'une maison ou d'un assemblage immense, le ShopBot ouvre de nombreuses portes à votre imagination ! \r\n\r\nMatériaux usinables :\r\nLes principaux matériaux usinables sont le bois, le plastique, le laiton et bien d'autres.\r\nCette machine n'usine pas les métaux.\r\n<object width=\"560\" height=\"315\"><param name=\"movie\" value=\"//www.youtube.com/v/3h8VPLNapag?hl=fr_FR&amp;version=3\"></param><param name=\"allowFullScreen\" value=\"true\"></param><param name=\"allowscriptaccess\" value=\"always\"></param><embed src=\"//www.youtube.com/v/3h8VPLNapag?hl=fr_FR&amp;version=3\" type=\"application/x-shockwave-flash\" width=\"560\" height=\"315\" allowscriptaccess=\"always\" allowfullscreen=\"true\"></embed></object>",
"spec": "Surface maximale de travail: 2440x1220x150 (Z) mm\r\nLogiciel utilisé: Partworks 2D & 3D\r\nRésolution mécanique: 0,015 mm\r\nPrécision de la position: +/- 0,127mm\r\nFormats acceptés: DXF, STL \r\n"
},
{
"id": 4,
"name": "Imprimante 3D - Ultimaker",
"slug": "imprimante-3d",
"updated_at": "2014-12-11T15:47:02.215+01:00",
"created_at": "2014-06-30T03:32:31.986+02:00",
"description": "L'imprimante 3D ULTIMAKER\r\n\r\nInformations générales :\r\nL'utimaker est une imprimante 3D peu chère utilisant une technologie FFF (Fused Filament Fabrication) avec extrusion thermoplastique.\r\nC'est une machine idéale pour réaliser rapidement des prototypes 3D dans des couleurs différentes.\r\n",
"spec": "Surface maximale de travail: 210x210x220mm \r\nRésolution méchanique: 0,02 mm \r\nPrécision de position: +/- 0,05 \r\nLogiciel utilisé: Cura\r\nFormats de fichier acceptés: STL \r\nMatériaux utilisés: PLA (en stock)."
},
{
"id": 5,
"name": "Petite Fraiseuse",
"slug": "petite-fraiseuse",
"updated_at": "2014-06-30T14:33:37.638+02:00",
"created_at": "2014-06-30T03:32:31.989+02:00",
"description": "La fraiseuse numérique Roland Modela MDX-20\r\n\r\nInformations générales :\r\nCette machine est utilisée pour l'usinage et le scannage 3D de précision. Elle permet principalement d'usiner des circuits imprimés et des moules de petite taille. Le faible diamètre des fraises utilisées (Ø 0,3 mm à Ø 6mm) implique que certains temps d'usinages peuvent êtres long (> 12h), c'est pourquoi cette fraiseuse peut être laissée en autonomie toute une nuit afin d'obtenir le plus précis des usinages au FabLab.\r\n\r\nMatériaux usinables :\r\nLes principaux matériaux usinables sont : bois, plâtre, résine, cire usinable, cuivre.\r\n",
"spec": "Taille du plateau X/Y : 220 mm x 160 mm\r\nVolume maximal de travail: 203,2 mm (X), 152,4 mm (Y), 60,5 mm (Z)\r\nPrécision usinage: 0,00625 mm\r\nPrécision scannage: réglable de 0,05 à 5 mm (axes X,Y) et 0,025 mm (axe Z)\r\nVitesse d'analyse (scannage): 4-15 mm/sec\r\n \r\n \r\nLogiciel utilisé pour le fraisage: Roland Modela player 4 \r\nLogiciel utilisé pour l'usinage de circuits imprimés: Cad.py (linux)\r\nFormats acceptés: STL,PNG 3D\r\nFormat d'exportation des données scannées: DXF, VRML, STL, 3DMF, IGES, Grayscale, Point Group et BMP\r\n"
},
#
# ....
#
{
"id": 18,
"name": "Canon IPF 750",
"slug": "canon-ipf-750",
"updated_at": "2015-10-12T18:00:24.254+02:00",
"created_at": "2015-10-12T18:00:24.254+02:00",
"description": "PROCHAINEMENT",
"spec": "36 pouces\r\nType d'encre: Encre pigment et colorant réactive, 5 couleurs (MBK x 2, BK, C, M, Y)\r\nRésolution d'impression maximale:\t2400 × 1200 dpi\r\nVitesse d'impression:\t(A0, Image polychrome)\r\nPapier ordinaire: 0:48 min (mode brouillon), 1:14 min (mode standard)\r\nPapier couché: 1:14 min (mode brouillon), 2:26 min (mode standard), 3:51 min (mode qualité élevée)"
}
]
}
EOS
end
end

View File

@ -0,0 +1,90 @@
class OpenAPI::V1::ReservationsDoc < OpenAPI::V1::BaseDoc
resource_description do
short 'Reservations'
desc 'Reservations made by users'
formats FORMATS
api_version API_VERSION
end
include OpenAPI::V1::Concerns::ParamGroups
doc_for :index do
api :GET, "/#{API_VERSION}/reservations", "Reservations index"
description "Index of reservations made by users, with optional pagination. Order by *created_at* descendant."
param_group :pagination
param :user_id, [Integer, Array], optional: true, desc: "Scope the request to one or various users."
param :reservable_type, ['Event', 'Machine', 'Training'], optional: true, desc: "Scope the request to a specific type of reservable."
param :reservable_id, [Integer, Array], optional: true, desc: "Scope the request to one or various reservables."
example <<-EOS
# /open_api/v1/reservations?reservable_type=Event&page=1&per_page=3
{
"reservations": [
{
"id": 3253,
"user_id": 1744,
"reservable_id": 162,
"reservable_type": "Event",
"updated_at": "2016-05-03T14:14:00.141+02:00",
"created_at": "2016-05-03T14:14:00.141+02:00",
"user": {
"id": 1744,
"email": "xxxxxxxxxxxx",
"created_at": "2016-05-03T13:51:03.223+02:00",
"full_name": "xxxxxxxxxxxx"
},
"reservable": {
"id": 162,
"title": "INITIATION FAB LAB",
"description": "A partir de 15 ans : \r\n\r\nDécouvrez le Fab Lab, familiarisez-vous avec les découpeuses laser, les imprimantes 3D, la découpeuse vinyle ... ! Fabriquez un objet simple, à ramener chez vous ! \r\n\r\nAdoptez la Fab Lab attitude !",
"updated_at": "2016-03-21T15:55:56.306+01:00",
"created_at": "2016-03-21T15:55:56.306+01:00"
}
},
{
"id": 3252,
"user_id": 1514,
"reservable_id": 137,
"reservable_type": "Event",
"updated_at": "2016-05-03T13:54:54.072+02:00",
"created_at": "2016-05-03T13:54:54.072+02:00",
"user": {
"id": 1514,
"email": "xxxxxxxxxxxx",
"created_at": "2016-02-24T08:45:09.050+01:00",
"full_name": "xxxxxxxxxxxx"
},
"reservable": {
"id": 137,
"title": "INITIATION FAB LAB",
"description": "A partir de 15 ans : \r\n\r\nDécouvrez le Fab Lab, familiarisez-vous avec les découpeuses laser, les imprimantes 3D, la découpeuse vinyle ... ! Fabriquez un objet simple, à ramener chez vous ! \r\n\r\nAdoptez la Fab Lab attitude !",
"updated_at": "2016-05-03T13:53:47.172+02:00",
"created_at": "2016-03-07T15:58:14.113+01:00"
}
},
{
"id": 3251,
"user_id": 1743,
"reservable_id": 162,
"reservable_type": "Event",
"updated_at": "2016-05-03T12:28:50.487+02:00",
"created_at": "2016-05-03T12:28:50.487+02:00",
"user": {
"id": 1743,
"email": "xxxxxxxxxxxx",
"created_at": "2016-05-03T12:24:38.724+02:00",
"full_name": "xxxxxxxxxxxx"
},
"reservable": {
"id": 162,
"title": "INITIATION FAB LAB",
"description": "A partir de 15 ans : \r\n\r\nDécouvrez le Fab Lab, familiarisez-vous avec les découpeuses laser, les imprimantes 3D, la découpeuse vinyle ... ! Fabriquez un objet simple, à ramener chez vous ! \r\n\r\nAdoptez la Fab Lab attitude !",
"updated_at": "2016-03-21T15:55:56.306+01:00",
"created_at": "2016-03-21T15:55:56.306+01:00"
}
}
]
}
EOS
end
end

View File

@ -0,0 +1,74 @@
class OpenAPI::V1::TrainingsDoc < OpenAPI::V1::BaseDoc
resource_description do
short 'Trainings'
desc 'Trainings of Fab-manager'
formats FORMATS
api_version API_VERSION
end
doc_for :index do
api :GET, "/#{API_VERSION}/trainings", "Trainings index"
description "Trainings index. Order by *created_at* ascendant."
example <<-EOS
# /open_api/v1/trainings
{
"trainings": [
{
"id": 1,
"name": "Formation Imprimante 3D",
"slug": "formation-imprimante-3d",
"updated_at": "2015-02-05T13:49:15.025+01:00",
"created_at": "2014-06-30T03:32:32.126+02:00",
"nb_total_places": 8,
"description": null
},
{
"id": 2,
"name": "Formation Laser / Vinyle",
"slug": "formation-laser-vinyle",
"updated_at": "2015-02-05T13:49:19.046+01:00",
"created_at": "2014-06-30T03:32:32.138+02:00",
"nb_total_places": 8,
"description": null
},
{
"id": 3,
"name": "Formation Petite fraiseuse numerique",
"slug": "formation-petite-fraiseuse-numerique",
"updated_at": "2015-02-05T13:49:23.040+01:00",
"created_at": "2014-06-30T03:32:32.164+02:00",
"nb_total_places": 8,
"description": null
},
{
"id": 4,
"name": "Formation Shopbot Grande Fraiseuse",
"slug": "formation-shopbot-grande-fraiseuse",
"updated_at": "2015-02-03T10:22:21.908+01:00",
"created_at": "2014-06-30T03:32:32.168+02:00",
"nb_total_places": 6,
"description": null
},
{
"id": 5,
"name": "Formation logiciel 2D",
"slug": "formation-logiciel-2d",
"updated_at": "2015-02-05T13:49:27.460+01:00",
"created_at": "2014-06-30T09:37:42.778+02:00",
"nb_total_places": 8,
"description": null
},
{
"id": 6,
"name": "Pas de Reservation",
"slug": "pas-de-reservation",
"updated_at": "2014-07-22T14:18:11.784+02:00",
"created_at": "2014-07-22T14:18:11.784+02:00",
"nb_total_places": null,
"description": null
}
]
}
EOS
end
end

View File

@ -0,0 +1,99 @@
class OpenAPI::V1::UserTrainingsDoc < OpenAPI::V1::BaseDoc
resource_description do
short 'User trainings'
desc 'Trainings validated by users'
formats FORMATS
api_version API_VERSION
end
include OpenAPI::V1::Concerns::ParamGroups
doc_for :index do
api :GET, "/#{API_VERSION}/user_trainings", "User trainings index"
description "Index of trainings accomplished by users, with optional pagination. Order by *created_at* descendant."
param_group :pagination
param :training_id, [Integer, Array], optional: true, desc: "Scope the request to one or various trainings."
param :user_id, [Integer, Array], optional: true, desc: "Scope the request to one or various users."
example <<-EOS
# /open_api/v1/user_trainings?training_id[]=3&training_id[]=4&page=1&per_page=2
{
"user_trainings": [
{
"id": 720,
"user_id": 1340,
"training_id": 3,
"updated_at": "2016-05-03T14:16:38.373+02:00",
"created_at": "2016-05-03T14:16:38.373+02:00",
"user": {
"id": 1340,
"email": "xxxxxxxxxxx",
"created_at": "2015-12-20T11:30:32.670+01:00",
"full_name": "xxxxxxxxxxx"
}
},
{
"id": 719,
"user_id": 1118,
"training_id": 4,
"updated_at": "2016-04-29T16:55:24.651+02:00",
"created_at": "2016-04-29T16:55:24.651+02:00",
"user": {
"id": 1118,
"email": "xxxxxxxxxxx",
"created_at": "2015-10-08T19:18:26.188+02:00",
"full_name": "xxxxxxxxxxx"
}
}
]
}
# /open_api/v1/user_trainings?user_id=1340&page=1&per_page=3
{
"user_trainings": [
{
"id": 720,
"user_id": 1340,
"training_id": 3,
"updated_at": "2016-05-03T14:16:38.373+02:00",
"created_at": "2016-05-03T14:16:38.373+02:00",
"training": {
"id": 3,
"name": "Formation Petite fraiseuse numerique",
"slug": "formation-petite-fraiseuse-numerique",
"updated_at": "2015-02-05T13:49:23.040+01:00",
"created_at": "2014-06-30T03:32:32.164+02:00"
}
},
{
"id": 700,
"user_id": 1340,
"training_id": 2,
"updated_at": "2016-04-19T22:02:17.083+02:00",
"created_at": "2016-04-19T22:02:17.083+02:00",
"training": {
"id": 2,
"name": "Formation Laser / Vinyle",
"slug": "formation-laser-vinyle",
"updated_at": "2015-02-05T13:49:19.046+01:00",
"created_at": "2014-06-30T03:32:32.138+02:00"
}
},
{
"id": 694,
"user_id": 1340,
"training_id": 1,
"updated_at": "2016-04-13T09:22:49.633+02:00",
"created_at": "2016-04-13T09:22:49.633+02:00",
"training": {
"id": 1,
"name": "Formation Imprimante 3D",
"slug": "formation-imprimante-3d",
"updated_at": "2015-02-05T13:49:15.025+01:00",
"created_at": "2014-06-30T03:32:32.126+02:00"
}
}
]
}
EOS
end
end

View File

@ -0,0 +1,97 @@
class OpenAPI::V1::UsersDoc < OpenAPI::V1::BaseDoc
resource_description do
short 'Users'
desc 'Users of Fab-manager'
formats FORMATS
api_version API_VERSION
end
include OpenAPI::V1::Concerns::ParamGroups
doc_for :index do
api :GET, "/#{API_VERSION}/users", "Users index"
description "Users index, with optional pagination. Order by *created_at* descendant."
param_group :pagination
param :email, [String, Array], optional: true, desc: "Filter users by *email* using strict matching."
param :user_id, [Integer, Array], optional: true, desc: "Filter users by *id* using strict matching."
example <<-EOS
# /open_api/v1/users?page=1&per_page=4
{
"users": [
{
"id": 1746,
"email": "xxxxxxx@xxxx.com",
"created_at": "2016-05-04T17:21:48.403+02:00",
"full_name": "xxxx xxxx",
"group": {
"id": 1,
"name": "standard, association",
"slug": "standard"
}
},
{
"id": 1745,
"email": "xxxxxxx@gmail.com",
"created_at": "2016-05-03T15:21:13.125+02:00",
"full_name": "xxxxx xxxxx",
"group": {
"id": 2,
"name": "étudiant, - de 25 ans, enseignant, demandeur d'emploi",
"slug": "student"
}
},
{
"id": 1744,
"email": "xxxxxxx@gmail.com",
"created_at": "2016-05-03T13:51:03.223+02:00",
"full_name": "xxxxxxx xxxx",
"group": {
"id": 1,
"name": "standard, association",
"slug": "standard"
}
},
{
"id": 1743,
"email": "xxxxxxxx@setecastronomy.eu",
"created_at": "2016-05-03T12:24:38.724+02:00",
"full_name": "xxx xxxxxxx",
"group": {
"id": 1,
"name": "standard, association",
"slug": "standard"
}
}
]
}
# /open_api/v1/users?user_id[]=1746&user_id[]=1745
{
"users": [
{
"id": 1746,
"email": "xxxxxxxxxxxx",
"created_at": "2016-05-04T17:21:48.403+02:00",
"full_name": "xxxx xxxxxx",
"group": {
"id": 1,
"name": "standard, association",
"slug": "standard"
}
},
{
"id": 1745,
"email": "xxxxxxxxx@gmail.com",
"created_at": "2016-05-03T15:21:13.125+02:00",
"full_name": "xxxxx xxxxxx",
"group": {
"id": 2,
"name": "étudiant, - de 25 ans, enseignant, demandeur d'emploi",
"slug": "student"
}
}
]
}
EOS
end
end

View File

@ -16,7 +16,7 @@ class Availability < ActiveRecord::Base
accepts_nested_attributes_for :tags, allow_destroy: true
scope :machines, -> { where(available_type: 'machines') }
scope :trainings, -> { where(available_type: 'training') }
scope :trainings, -> { includes(:trainings).where(available_type: 'training') }
attr_accessor :is_reserved, :slot_id, :can_modify
@ -51,9 +51,10 @@ class Availability < ActiveRecord::Base
# if haven't defined a nb_total_places, places are unlimited
def is_completed
return false if nb_total_places.blank?
nb_total_places <= slots.where(canceled_at: nil).size
nb_total_places <= slots.to_a.select {|s| s.canceled_at == nil }.size
end
# TODO: refactoring this function for avoid N+1 query
def nb_total_places
if read_attribute(:nb_total_places).present?
read_attribute(:nb_total_places)

View File

@ -7,6 +7,7 @@ class Event < ActiveRecord::Base
accepts_nested_attributes_for :event_files, allow_destroy: true, reject_if: :all_blank
has_and_belongs_to_many :categories, join_table: :events_categories
validates :categories, presence: true
has_many :reservations, as: :reservable, dependent: :destroy
belongs_to :availability, dependent: :destroy
accepts_nested_attributes_for :availability
@ -15,6 +16,8 @@ class Event < ActiveRecord::Base
after_create :event_recurrence
before_save :update_nb_free_places
# update event updated_at for index cache
after_save -> { self.touch }
def name
title
@ -33,9 +36,9 @@ class Event < ActiveRecord::Base
end
end
def reservations
Reservation.where(reservable: self)
end
# def reservations
# Reservation.where(reservable: self)
# end
private
def event_recurrence

5
app/models/open_api.rb Normal file
View File

@ -0,0 +1,5 @@
module OpenAPI
def self.table_name_prefix
'open_api_'
end
end

View File

@ -0,0 +1,4 @@
class OpenAPI::CallsCountTracing < ActiveRecord::Base
belongs_to :client, foreign_key: :open_api_client_id
validates :client, :at, presence: true
end

View File

@ -0,0 +1,9 @@
class OpenAPI::Client < ActiveRecord::Base
has_many :calls_count_tracings, foreign_key: :open_api_client_id, dependent: :destroy
has_secure_token
validates :name, presence: true
def increment_calls_count
update_column(:calls_count, calls_count+1)
end
end

View File

@ -0,0 +1 @@
class OpenAPI::ParameterError < StandardError; end

View File

@ -2,12 +2,12 @@ class EventPolicy < ApplicationPolicy
class Scope < Scope
def resolve
if user.nil? or (user and !user.is_admin?)
scope.includes(:event_image, :event_files, :availability)
scope.includes(:event_image, :event_files, :availability, :categories)
.where('availabilities.start_at >= ?', Time.now)
.order('availabilities.start_at ASC')
.references(:availabilities)
else
scope.includes(:event_image, :event_files, :availability)
scope.includes(:event_image, :event_files, :availability, :categories)
.order('availabilities.start_at DESC')
.references(:availabilities)
end

View File

@ -0,0 +1,21 @@
class OpenAPI::ClientPolicy < ApplicationPolicy
def index?
user.has_role? :admin
end
def create?
user.has_role? :admin
end
def update?
user.has_role? :admin
end
def reset_token?
user.has_role? :admin
end
def destroy?
user.has_role? :admin and record.calls_count == 0
end
end

View File

@ -17,7 +17,7 @@ json.array!(@slots) do |slot|
json.user do
json.id slot.reservation.user.id
json.name slot.reservation.user.profile.full_name
end if slot.reservation # ... if the slot was reserved
end if @current_user_role == 'admin' and slot.reservation # ... if the slot was reserved
json.tag_ids slot.availability.tag_ids
json.tags slot.availability.tags do |t|
json.id t.id

View File

@ -2,26 +2,21 @@ json.array!(@availabilities) do |a|
json.id a.id
json.slot_id a.slot_id if a.slot_id
if a.is_reserved
json.is_reserved true
json.title "#{a.trainings[0].name}' - #{t('trainings.i_ve_reserved')}"
json.borderColor '#b2e774'
elsif a.is_completed
json.is_completed true
json.title "#{a.trainings[0].name} - #{t('trainings.completed')}"
json.borderColor '#eeeeee'
else
json.title a.trainings[0].name
json.borderColor '#bd7ae9'
end
json.start a.start_at.iso8601
json.end a.end_at.iso8601
json.is_reserved a.is_reserved
json.backgroundColor 'white'
json.borderColor a.is_reserved ? '#b2e774' : '#bd7ae9'
if a.is_reserved
json.borderColor '#b2e774'
elsif a.is_completed
json.borderColor '#eeeeee'
else
json.borderColor '#bd7ae9'
end
json.can_modify a.can_modify
json.is_completed a.is_completed
json.nb_total_places a.nb_total_places
json.training do
@ -32,7 +27,6 @@ json.array!(@availabilities) do |a|
json.id m.id
json.name m.name
end
json.amount a.trainings.first.amount_by_group(@user.group_id).amount_by_plan(nil)/100.0 if @user
end
json.tag_ids a.tag_ids
json.tags a.tags do |t|

View File

@ -1,7 +1,8 @@
json.array!(@events) do |event|
json.partial! 'api/events/event', event: event
json.event_image_small event.event_image.attachment.small.url if event.event_image
json.url event_url(event, format: :json)
json.nb_total_events @total
json.cache! [@events, @page] do
json.array!(@events) do |event|
json.partial! 'api/events/event', event: event
json.event_image_small event.event_image.attachment.small.url if event.event_image
json.url event_url(event, format: :json)
json.nb_total_events @total
end
end

View File

@ -1,6 +1,7 @@
json.array!(@events) do |event|
json.partial! 'api/events/event', event: event
json.event_image_medium event.event_image.attachment.medium.url if event.event_image
json.url event_url(event, format: :json)
json.cache! @events do
json.array!(@events) do |event|
json.partial! 'api/events/event', event: event
json.event_image_medium event.event_image.attachment.medium.url if event.event_image
json.url event_url(event, format: :json)
end
end

View File

@ -1 +1,3 @@
json.partial! 'api/groups/group', collection: @groups, as: :group
json.cache! @groups do
json.partial! 'api/groups/group', collection: @groups, as: :group
end

View File

@ -1,13 +1,7 @@
user_is_admin = (current_user and current_user.is_admin?)
json.array!(@machines) do |machine|
json.extract! machine, :id, :name, :description, :spec, :slug
json.url machine_url(machine, format: :json)
json.machine_image machine.machine_image.attachment.medium.url if machine.machine_image
json.current_user_is_training current_user.is_training_machine?(machine) if current_user
json.current_user_training_reservation do
json.partial! 'api/reservations/reservation', reservation: current_user.training_reservation_by_machine(machine)
end if current_user and !current_user.is_training_machine?(machine) and current_user.training_reservation_by_machine(machine)
json.plan_ids machine.plan_ids if user_is_admin
json.cache! @machines do
json.array!(@machines) do |machine|
json.extract! machine, :id, :name, :description, :spec, :slug
json.url machine_url(machine, format: :json)
json.machine_image machine.machine_image.attachment.medium.url if machine.machine_image
end
end

View File

@ -11,16 +11,6 @@ json.current_user_training_reservation do
json.partial! 'api/reservations/reservation', reservation: current_user.training_reservation_by_machine(@machine)
end if current_user and !current_user.is_training_machine?(@machine) and current_user.training_reservation_by_machine(@machine)
json.amount_by_group Group.all do |g|
json.id g.id
json.name g.name
json.not_subscribe_amount @machine.not_subscribe_price(g.id).amount/100.0
json.amount_by_plan @machine.prices_by_group(g.id) do |price|
json.plan_id price.plan_id
json.amount price.amount/100.0
end
end
json.machine_projects @machine.projects.published.last(10) do |p|
json.id p.id
json.name p.name

View File

@ -0,0 +1 @@
json.extract! client, :id, :name, :calls_count, :token, :created_at

View File

@ -0,0 +1 @@
json.partial! 'api/open_api_clients/client', client: @client

View File

@ -0,0 +1,3 @@
json.array! @clients do |client|
json.partial! 'api/open_api_clients/client', client: client
end

View File

@ -0,0 +1 @@
json.partial! 'api/open_api_clients/client', client: @client

View File

@ -0,0 +1 @@
json.partial! 'api/open_api_clients/client', client: @client

View File

@ -6,23 +6,8 @@ json.plan_file_attributes do
json.attachment_identifier plan.plan_file.attachment_identifier
end if plan.plan_file
json.prices plan.prices do |price|
json.extract! price, :id, :group_id, :plan_id, :priceable_type, :priceable_id
json.amount price.amount / 100.0
json.priceable_name price.priceable.name
end
json.partners plan.partners do |partner|
json.first_name partner.first_name
json.last_name partner.last_name
json.email partner.email
end if plan.respond_to?(:partners)
json.training_credits plan.training_credits do |tc|
json.training_id tc.creditable_id
end if attribute_requested?(@attributes_requested, 'trainings_credits')
json.machine_credits plan.machine_credits do |mc|
json.machine_id mc.creditable_id
json.hours mc.hours
end if attribute_requested?(@attributes_requested, 'machines_credits')

View File

@ -1 +1,5 @@
json.partial! 'api/plans/plan', collection: @plans, as: :plan
json.array!(@plans) do |plan|
json.extract! plan, :id, :base_name, :name, :interval, :interval_count, :group_id, :training_credit_nb, :description, :type, :ui_weight
json.amount (plan.amount / 100.00)
json.plan_file_url plan.plan_file.attachment_url if plan.plan_file
end

View File

@ -1 +1,3 @@
json.prices @prices, partial: 'api/prices/price', as: :price
json.cache! @prices do
json.partial! 'api/prices/price', collection: @prices, as: :price
end

View File

@ -1,19 +1,13 @@
json.array!(@trainings) do |training|
json.id training.id
json.name training.name
json.description training.description
json.machine_ids training.machine_ids
json.availabilities training.availabilities do |a|
json.id a.id
json.start_at a.start_at.iso8601
json.end_at a.end_at.iso8601
json.reservation_users a.slots.map do |slot|
json.id slot.reservation.user.id
json.full_name slot.reservation.user.profile.full_name
json.is_valid slot.reservation.user.trainings.include?(training)
end
end if attribute_requested?(@requested_attributes, 'availabilities')
json.nb_total_places training.nb_total_places
role = (current_user and current_user.is_admin?) ? 'admin' : 'user'
json.plan_ids training.plan_ids if current_user and current_user.is_admin?
json.cache! [@trainings, role] do
json.array!(@trainings) do |training|
json.id training.id
json.name training.name
json.description training.description
json.machine_ids training.machine_ids
json.nb_total_places training.nb_total_places
json.plan_ids training.plan_ids if role === 'admin'
end
end

View File

@ -1,5 +1,5 @@
json.extract! @training, :id, :name, :machine_ids, :nb_total_places
json.availabilities @training.availabilities.order('start_at DESC') do |a|
json.availabilities @training.availabilities do |a|
json.id a.id
json.start_at a.start_at.iso8601
json.end_at a.end_at.iso8601

View File

@ -1,5 +1,2 @@
json.extract! trainings_pricing, :id, :group_id, :training_id
json.amount trainings_pricing.amount / 100.0
json.training do
json.name trainings_pricing.training.name
end

View File

@ -0,0 +1,5 @@
json.machines @machines do |machine|
json.partial! 'open_api/v1/machines/machine', machine: machine
json.extract! machine, :description, :spec
json.hours_remaining @hours_remaining[machine.id]
end

View File

@ -0,0 +1 @@
json.extract! event, :id, :title, :description, :updated_at, :created_at

View File

@ -0,0 +1,4 @@
json.events @events do |event|
json.partial! 'open_api/v1/events/event', event: event
json.extract! event, :amount, :reduced_amount, :nb_total_places, :nb_free_places
end

View File

@ -0,0 +1,8 @@
json.invoices @invoices do |invoice|
json.extract! invoice, :id, :invoiced_id, :user_id, :invoiced_type, :stp_invoice_id, :reference, :total, :type, :description
json.invoice_url download_open_api_v1_invoice_path(invoice)
json.invoiced do
json.created_at invoice.invoiced.created_at
end
end

View File

@ -0,0 +1 @@
json.extract! machine, :id, :name, :slug, :updated_at, :created_at

View File

@ -0,0 +1,4 @@
json.machines @machines do |machine|
json.partial! 'open_api/v1/machines/machine', machine: machine
json.extract! machine, :description, :spec
end

View File

@ -0,0 +1,19 @@
json.reservations @reservations do |reservation|
json.extract! reservation, :id, :user_id, :reservable_id, :reservable_type, :updated_at, :created_at
if reservation.association(:user).loaded?
json.user do
json.partial! 'open_api/v1/users/user', user: reservation.user
end
end
json.reservable do
if reservation.reservable_type == "Training"
json.partial! 'open_api/v1/trainings/training', training: reservation.reservable
elsif reservation.reservable_type == "Machine"
json.partial! 'open_api/v1/machines/machine', machine: reservation.reservable
elsif reservation.reservable_type == "Event"
json.partial! 'open_api/v1/events/event', event: reservation.reservable
end
end
end

View File

@ -0,0 +1 @@
json.extract! training, :id, :name, :slug, :updated_at, :created_at

View File

@ -0,0 +1,4 @@
json.trainings @trainings do |training|
json.partial! 'open_api/v1/trainings/training', training: training
json.extract! training, :nb_total_places, :description
end

View File

@ -0,0 +1,15 @@
json.user_trainings @user_trainings do |user_training|
json.extract! user_training, :id, :user_id, :training_id, :updated_at, :created_at
if user_training.association(:user).loaded?
json.user do
json.partial! 'open_api/v1/users/user', user: user_training.user
end
end
if user_training.association(:training).loaded?
json.training do
json.partial! 'open_api/v1/trainings/training', training: user_training.training
end
end
end

View File

@ -0,0 +1,15 @@
json.extract! user, :id, :email, :created_at
if user.association(:profile).loaded?
json.full_name user.profile.full_name
end
if user.association(:group).loaded?
json.group do
if user.group_id?
json.extract! user.group, :id, :name, :slug
else
json.nil!
end
end
end

View File

@ -0,0 +1,3 @@
json.users @users do |user|
json.partial! 'open_api/v1/users/user', user: user
end

View File

@ -0,0 +1,10 @@
class OpenAPITraceCallsCountWorker < ActiveJob::Base
include Sidekiq::Worker
sidekiq_options queue: 'default', retry: true
def perform
OpenAPI::Client.find_each do |client|
OpenAPI::CallsCountTracing.create!(client: client, calls_count: client.calls_count, at: DateTime.now)
end
end
end

View File

@ -36,7 +36,6 @@ module Fablab
#
config.i18n.default_locale = Rails.application.secrets.rails_locale
config.assets.paths << Rails.root.join('vendor', 'assets', 'components').to_s
# Do not swallow errors in after_commit/after_rollback callbacks.

View File

@ -11,7 +11,7 @@ Rails.application.configure do
# Show full error reports and disable caching.
config.consider_all_requests_local = true
config.action_controller.perform_caching = false
config.action_controller.perform_caching = true
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false

View File

@ -0,0 +1,19 @@
Apipie.configure do |config|
config.app_name = "Fab-manager"
config.api_base_url = "/open_api"
config.doc_base_url = "/open_api/doc"
# where is your API defined?
config.api_controllers_matcher = "#{Rails.root}/app/controllers/open_api/v1/*.rb"
config.validate = false
config.app_info['v1'] = <<-EOS
= Pagination
---
Pagination is done using headers. Following RFC-5988 standard for web linking.
It uses headers *Link*, *Total* and *Per-Page*.
= Authentification
---
Authentification is done using *Authorization* header.
You just have to set header *Authorization* to <tt>Token token=YOUR_TOKEN</tt> for every request.
EOS
end

View File

@ -468,3 +468,18 @@ en:
reservations_cancelling: "Reservations cancelling"
customization_of_SETTING_successfully_saved: "Customization of {{SETTING}} successfully saved." # angular interpolation
file_successfully_updated: "File successfully updated."
open_api_clients:
add_new_client: "Create new API client"
api_documentation: "API documentation"
open_api_clients: "OpenAPI clients"
calls_count: "calls count"
created_at: "Creation date"
reset_token: "revoke access"
client_name: "Client's name"
do_you_really_want_to_delete_this_open_api_client: "Do you really want to delete this OpenAPI client?"
do_you_really_want_to_revoke_this_open_api_access: "Do you really want to revoke this access ? It will erase and replace the current token."
client_successfully_created: "Client successfully created."
client_successfully_updated: "Client successfully updated."
client_successfully_deleted: "Client successfully deleted."
access_successfully_revoked: "Access successfully revoked."

View File

@ -468,3 +468,18 @@ fr:
reservations_cancelling: "Annulation des réservations"
customization_of_SETTING_successfully_saved: "La personnalisation de {{SETTING}} a bien été enregistrée." # angular interpolation
file_successfully_updated: "Le fichier a bien été mis à jour."
open_api_clients:
add_new_client: "Créer un compte client"
api_documentation: "Documentation de l'API"
open_api_clients: "Clients OpenAPI"
calls_count: "Nombre d'appels"
created_at: "Date de création"
reset_token: "Révoquer l'accès"
client_name: "Nom du client"
do_you_really_want_to_delete_this_open_api_client: "Voulez vous vraiment supprimer ce compte client OpenAPI ?"
do_you_really_want_to_revoke_this_open_api_access: "Voulez vous vraiment revoquer l'accès de ce compte OpenAPI ? Une confirmation aura pour effet la génération d'un nouveau token."
client_successfully_created: "Le compte client a bien été créé."
client_successfully_updated: "Les modifications ont été enregistrées."
client_successfully_deleted: "Le compte client a bien été supprimé."
access_successfully_revoked: "L'accès a bien été revoqué."

View File

@ -45,6 +45,7 @@ en:
manage_the_projects_elements: "Manage the Projects Elements"
statistics: "Statistics"
customization: "Customization"
open_api_clients: "OpenAPI clients"
# account creation modal
create_your_account: "Create your account"
@ -155,6 +156,7 @@ en:
tell_us_why_this_looks_abusive: "Tell us why this looks abusive"
message_is_required: "Message is required."
report: "Report"
do_you_really_want_to_delete_this_project: "Do you really want to delete this project?"
machines_list:
# list of machines

View File

@ -45,6 +45,7 @@ fr:
manage_the_projects_elements: "Gérer les éléments projets"
statistics: "Statistiques"
customization: "Personnalisation"
open_api_clients: "Clients OpenAPI"
# fenêtre de création de compte
create_your_account: "Créer votre compte"
@ -155,6 +156,7 @@ fr:
tell_us_why_this_looks_abusive: "Dites nous en quoi cela vous semble abusif"
message_is_required: "Le message est requis."
report: "Signaler"
do_you_really_want_to_delete_this_project: "Êtes-vous sur de vouloir supprimer ce projet ?"
machines_list:
# liste des machines

View File

@ -80,7 +80,7 @@ Rails.application.routes.draw do
end
resources :invoices, only: [:index, :show, :create] do
get ':id/download', action: 'download', on: :collection
get 'download', action: 'download', on: :member
post 'list', action: 'list', on: :collection
end
@ -97,11 +97,34 @@ Rails.application.routes.draw do
get 'active', action: 'active', on: :collection
end
resources :abuses, only: [:create]
resources :open_api_clients, only: [:index, :create, :update, :destroy] do
patch :reset_token, on: :member
end
# i18n
get 'translations/:locale/:state' => 'translations#show', :constraints => { :state => /[^\/]+/ } # allow dots in URL for 'state'
end
# open_api
namespace :open_api do
namespace :v1 do
scope only: :index do
resources :users
resources :trainings
resources :user_trainings
resources :reservations
resources :machines
resources :bookable_machines
resources :invoices do
get :download, on: :member
end
resources :events
resources :availabilities
end
end
end
%w(account event machine project subscription training user).each do |path|
post "/stats/#{path}/_search", to: "api/statistics##{path}"
end
@ -113,4 +136,5 @@ Rails.application.routes.draw do
mount Sidekiq::Web => '/admin/sidekiq'
end
apipie
end

View File

@ -15,4 +15,8 @@ generate_statistic:
class: "StatisticWorker"
queue: default
open_api_trace_calls_count:
cron: "0 4 * * 0" # every sunday at 4am
class: "OpenAPITraceCallsCountWorker"
<%= PluginRegistry.insert_code('yml.schedule') %>

View File

@ -0,0 +1,10 @@
class CreateOpenAPIClients < ActiveRecord::Migration
def change
create_table :open_api_clients do |t|
t.string :name
t.integer :calls_count, default: 0
t.string :token
t.timestamps null: false
end
end
end

View File

@ -0,0 +1,10 @@
class CreateOpenAPICallsCountTracings < ActiveRecord::Migration
def change
create_table :open_api_calls_count_tracings do |t|
t.belongs_to :open_api_client, foreign_key: true, index: true
t.integer :calls_count, null: false
t.datetime :at, null: false
t.timestamps null: false
end
end
end

View File

@ -2,8 +2,8 @@ class CreateUnaccentFunction < ActiveRecord::Migration
# PostgreSQL only
def up
execute 'CREATE EXTENSION unaccent;'
execute 'CREATE EXTENSION pg_trgm;'
execute 'CREATE EXTENSION IF NOT EXISTS unaccent;'
execute 'CREATE EXTENSION IF NOT EXISTS pg_trgm;'
execute "CREATE OR REPLACE FUNCTION f_unaccent(text)
RETURNS text AS
$func$

View File

@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20160526102307) do
ActiveRecord::Schema.define(version: 20160613093842) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -227,7 +227,7 @@ ActiveRecord::Schema.define(version: 20160526102307) do
t.boolean "is_read", default: false
t.datetime "created_at"
t.datetime "updated_at"
t.string "receiver_type", limit: 255
t.string "receiver_type"
t.boolean "is_send", default: false
t.jsonb "meta_data", default: {}
end
@ -456,8 +456,8 @@ ActiveRecord::Schema.define(version: 20160526102307) do
t.datetime "updated_at"
t.integer "availability_id"
t.datetime "ex_start_at"
t.datetime "canceled_at"
t.datetime "ex_end_at"
t.datetime "canceled_at"
t.boolean "offered", default: false
end
@ -615,7 +615,6 @@ ActiveRecord::Schema.define(version: 20160526102307) do
add_index "user_trainings", ["user_id"], name: "index_user_trainings_on_user_id", using: :btree
create_table "users", force: :cascade do |t|
t.string "username", limit: 255
t.string "email", limit: 255, default: "", null: false
t.string "encrypted_password", limit: 255, default: "", null: false
t.string "reset_password_token", limit: 255
@ -638,6 +637,7 @@ ActiveRecord::Schema.define(version: 20160526102307) do
t.boolean "is_allow_contact", default: true
t.integer "group_id"
t.string "stp_customer_id", limit: 255
t.string "username", limit: 255
t.string "slug", limit: 255
t.boolean "is_active", default: true
t.boolean "invoicing_disabled", default: false

12
lib/tasks/fablab/fix.rake Normal file
View File

@ -0,0 +1,12 @@
namespace :fablab do
namespace :fix do
task reservations_not_existing_reservable: :environment do
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Base.connection.execute(
'UPDATE reservations SET reservable_type = NULL, reservable_id = NULL'\
' WHERE NOT EXISTS (SELECT 1 FROM events WHERE events.id = reservations.reservable_id)'\
' AND reservations.reservable_type = \'Event\''
)
end
end
end