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

release 2.0 rc

This commit is contained in:
cyril 2016-03-23 18:39:41 +01:00
parent 5fff77d906
commit d1450f65f6
1766 changed files with 401929 additions and 42596 deletions

33
.dockerignore Normal file
View File

@ -0,0 +1,33 @@
# Ignore bundler config.
.bundle
vendor/cache
config/database.yml
config/application.yml
# Ignore the default SQLite database.
db/*.sqlite3
db/*.sqlite3-journal
# Ignore all logfiles and tempfiles.
log
tmp
public/uploads
public/assets
*.DS_Store
.idea
# PDF invoices
invoices
.DS_Store
.vagrant
Vagrantfile
.git*
Dockerfile
docker-compose*

19
.gitignore vendored
View File

@ -13,17 +13,24 @@
/db/*.sqlite3-journal /db/*.sqlite3-journal
# Ignore all logfiles and tempfiles. # Ignore all logfiles and tempfiles.
/log/* /log/*.log
!/log/.keep
/tmp /tmp
# uploads and public assets
/public/uploads /public/uploads
/public/assets /public/assets
# MacOS and IDE files # Ignore application configuration
/config/application.yml
*.DS_Store
.idea .idea
# PDF invoices
/invoices/*
/config/database.yml
/config/application.yml
.DS_Store .DS_Store
# machine specific database config .vagrant
/config/database.yml

View File

@ -1 +1 @@
fabmanager fablab

View File

@ -1 +1 @@
ruby-2.2.1 ruby-2.2.3

132
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,132 @@
# Contributing to FabManager
♥ [FabManager](http://www.fab-manager.com) and want to get involved?
Thanks! There are plenty of ways you can help!
Please take a moment to review this document in order to make the contribution process easy and effective for everyone
involved.
Following these guidelines helps to communicate that you respect the time of the developers managing and developing
this open source project. In return, they should reciprocate that respect in addressing your issue or assessing
patches and features.
## Using the issue tracker
The [issue tracker](https://github.com/LaCasemate/fab-manager/issues) is the preferred channel for [bug reports](#bugs),
[features requests](#features) and [submitting pull requests](#pull-requests), but please respect the following
restrictions:
* Please **do not** use the issue tracker for personal support requests (use [the forum](http://www.fab-manager.com/forum)).
* Please **do not** derail or troll issues. Keep the discussion on topic and respect the opinions of others.
* Please **do not** open issues or pull requests regarding the code in plugins or third parties software, (open them
in their respective repositories).
<a name="bugs"></a>
## Bug reports
A bug is a _demonstrable problem_ that is caused by the code in the repository. Good bug reports are extremely
helpful - thank you!
Guidelines for bug reports:
1. **Use the GitHub issue search** &mdash; check if the issue has already been reported.
2. **Check if the issue has been fixed** &mdash; try to reproduce it using the latest `master` or development
branch in the repository.
3. **Isolate the problem** &mdash; ideally create a [reduced test case](https://css-tricks.com/reduced-test-cases/)
and a live example.
A good bug report shouldn't leave others needing to chase you up for more information. Please try to be as detailed
as possible in your report. What is your environment? What steps will reproduce the issue? What browser(s) and OS
experience the problem? What would you expect to be the outcome? All these details will help people to fix any potential
bugs.
Example:
> Short and descriptive example bug report title
>
> A summary of the issue and the browser/OS environment in which it occurs. If suitable, include the steps required
> to reproduce the bug.
>
> 1. This is the first step
> 2. This is the second step
> 3. Further steps, etc.
>
> `<url>` - a link to the reduced test case
>
> Any other information you want to share that is relevant to the issue being reported. This might include the lines of
> code that you have identified as causing the bug, and potential solutions (and your opinions on their merits).
<a name="features"></a>
## Feature requests
Feature requests are welcome. But take a moment to find out whether your idea fits with the scope and aims of the
project. It's up to *you* to make a strong case to convince the project's developers of the merits of this feature.
Please provide as much detail and context as possible.
<a name="pull-requests"></a>
## Pull requests
Good pull requests - patches, improvements, new features - are a fantastic help. They should remain focused in scope
and avoid containing unrelated commits.
**Please ask first** before embarking on any significant pull request (e.g. implementing features, refactoring code,
porting to a different language), otherwise you risk spending a lot of time working on something that the project's
developers might not want to merge into the project.
Please adhere to the coding conventions used throughout a project (indentation, accurate comments, etc.) and any other
requirements (such as test coverage).
Adhering to the following process is the best way to get your work included in the project:
1. [Fork](https://help.github.com/articles/fork-a-repo/) the project, clone your fork, and configure the remotes:
```bash
# Clone your fork of the repo into the current directory
git clone https://github.com/<your-username>/fab-manager.git
# Navigate to the newly cloned directory
cd fab-manager
# Assign the original repo to a remote called "upstream"
git remote add upstream https://github.com/LaCasemate/fab-manager.git
```
2. If you cloned a while ago, get the latest changes from upstream:
```bash
git checkout master
git pull upstream master
```
3. Create a new topic branch (off the main project development branch) to contain your feature, change, or fix:
```bash
git checkout -b <topic-branch-name>
```
4. Commit your changes in logical chunks. Please adhere to these [git commit message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
or your code is unlikely be merged into the main project. Use Git's [interactive rebase](https://help.github.com/articles/about-git-rebase/)
feature to tidy up your commits before making them public.
5. Locally merge (or rebase) the upstream development branch into your topic branch:
```bash
git pull [--rebase] upstream master
```
6. Push your topic branch up to your fork:
```bash
git push origin <topic-branch-name>
```
7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) with a clear title and description.
**IMPORTANT**: By submitting a patch, you agree to allow the project owners to license your work under the terms of
the [GNU Affero General Public License](LICENSE.md).

71
Dockerfile Normal file
View File

@ -0,0 +1,71 @@
FROM ruby:2.2
MAINTAINER peng@sleede.com
# cf: nginx Dockerfile : https://github.com/nginxinc/docker-nginx
RUN apt-key adv --keyserver hkp://pgp.mit.edu:80 --recv-keys 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62
RUN echo "deb http://nginx.org/packages/mainline/debian/ jessie nginx" >> /etc/apt/sources.list
ENV NGINX_VERSION 1.9.7-1~jessie
# Install apt based dependencies required to run Rails as
# well as RubyGems. As the Ruby image itself is based on a
# Debian image, we use apt-get to install those.
RUN apt-get update && \
apt-get install -y \
nginx=${NGINX_VERSION} \
nodejs \
supervisor
# throw errors if Gemfile has been modified since Gemfile.lock
RUN bundle config --global frozen 1
# Run Bundle in a cache efficient way
WORKDIR /tmp
COPY Gemfile /tmp/
COPY Gemfile.lock /tmp/
RUN bundle install
# Clean up APT when done.
#RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Nginx
# Remove the default site
RUN rm /etc/nginx/conf.d/default.conf
# forward request and error logs to docker log collector
RUN ln -sf /dev/stdout /var/log/nginx/access.log
RUN ln -sf /dev/stderr /var/log/nginx/error.log
# Web app
RUN mkdir -p /usr/src/app
RUN mkdir -p /usr/src/app/config
RUN mkdir -p /usr/src/app/invoices
RUN mkdir -p /usr/src/app/log
RUN mkdir -p /usr/src/app/public/uploads
RUN mkdir -p /usr/src/app/public/assets
RUN mkdir -p /usr/src/app/tmp/sockets
RUN mkdir -p /usr/src/app/tmp/pids
WORKDIR /usr/src/app
COPY docker/database.yml /usr/src/app/config/database.yml
COPY . /usr/src/app
# Volumes
VOLUME /usr/src/app/invoices
VOLUME /usr/src/app/public/uploads
VOLUME /usr/src/app/public/assets
VOLUME /var/log/supervisor
# Expose port 80 and ssl 443 to the Docker host, so we can access it
# from the outside.
EXPOSE 80 443
# The main command to run when the container starts. Also
# tell the Rails dev server to bind to all interfaces by
# default.
COPY docker/supervisor.conf /etc/supervisor/conf.d/fablab.conf
CMD ["/usr/bin/supervisord"]

45
Gemfile
View File

@ -1,7 +1,7 @@
source 'https://rubygems.org' source 'https://rubygems.org'
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '4.2.1' gem 'rails', '4.2.5'
# Use SCSS for stylesheets # Use SCSS for stylesheets
gem 'sass-rails', '5.0.1' gem 'sass-rails', '5.0.1'
gem 'compass-rails', '2.0.4' gem 'compass-rails', '2.0.4'
@ -11,21 +11,22 @@ gem 'uglifier', '>= 1.3.0'
# Use CoffeeScript for .js.coffee assets and views # Use CoffeeScript for .js.coffee assets and views
gem 'coffee-rails', '~> 4.1.0' gem 'coffee-rails', '~> 4.1.0'
# See https://github.com/sstephenson/execjs#readme for more supported runtimes # See https://github.com/sstephenson/execjs#readme for more supported runtimes
gem 'therubyracer', platforms: :ruby gem 'therubyracer', '= 0.12.0', platforms: :ruby
# Use jquery as the JavaScript library # Use jquery as the JavaScript library
gem 'jquery-rails' gem 'jquery-rails'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.0' gem 'jbuilder', '~> 2.0'
# bundle exec rake doc:rails generates the API under doc/api. # bundle exec rake doc:rails generates the API under doc/api.
gem 'sdoc', '~> 0.4.0', group: :doc gem 'sdoc', '~> 0.4.0', group: :doc #TODO remove unused ?
gem 'forgery' gem 'forgery'
gem 'responders', '~> 2.0' gem 'responders', '~> 2.0'
group :development, :test do group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console # Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug' # comment over to use visual debugger (eg. RubyMine), uncomment to use manual debugging
# gem 'byebug'
# Access an IRB console on exception pages or by using <%= console %> in views # Access an IRB console on exception pages or by using <%= console %> in views
gem 'web-console', '~> 2.0' gem 'web-console', '~> 2.0'
@ -38,6 +39,8 @@ group :development, :test do
gem 'spring-commands-rspec' gem 'spring-commands-rspec'
gem 'guard-rspec', require: false gem 'guard-rspec', require: false
gem 'railroady'
end end
group :development do group :development do
@ -51,6 +54,9 @@ group :development do
gem 'capistrano' gem 'capistrano'
gem 'rvm-capistrano', require: false gem 'rvm-capistrano', require: false
gem 'capistrano-sidekiq', require: false gem 'capistrano-sidekiq', require: false
gem 'capistrano-maintenance', '0.0.5', require: false
gem 'active_record_query_trace'
end end
group :test do group :test do
@ -70,6 +76,9 @@ gem 'pg'
gem 'devise' gem 'devise'
gem 'devise-async' gem 'devise-async'
gem 'omniauth'
gem 'omniauth-oauth2'
gem 'rolify' gem 'rolify'
gem 'kaminari' gem 'kaminari'
@ -79,7 +88,8 @@ gem 'figaro'
gem 'bootstrap-sass' gem 'bootstrap-sass'
gem 'font-awesome-rails' gem 'font-awesome-rails'
gem 'angularjs-rails' #using bower instead
#gem 'angularjs-rails'
# Image processing ruby wrapper for ImageMagick # Image processing ruby wrapper for ImageMagick
gem 'mini_magick' gem 'mini_magick'
@ -101,13 +111,32 @@ gem 'sinatra', require: false
# Recurring jobs for Sidekiq # Recurring jobs for Sidekiq
gem 'sidekiq-cron' gem 'sidekiq-cron'
gem 'stripe', '1.30.2'
gem 'recurrence' gem 'recurrence'
# Fork de la gem avec support Attachments gem 'newrelic_rpm'
gem 'mandrill_dm', github: 'AbleTech/mandrill_dm'
gem 'disqus_api' # PDF
gem 'prawn'
gem 'prawn-table'
gem 'elasticsearch-rails'
gem 'elasticsearch-model'
gem 'elasticsearch-persistence'
gem 'notify_with' gem 'notify_with'
gem 'pundit' gem 'pundit'
gem 'oj'
gem 'actionpack-page_caching'
gem 'rails-observers'
gem 'chroma'
gem 'protected_attributes'
gem 'message_format'

View File

@ -1,56 +1,55 @@
GIT
remote: git://github.com/AbleTech/mandrill_dm.git
revision: 2bbb35dd81887bb915f606699d63a723b450711d
specs:
mandrill_dm (1.1.0)
mandrill-api (~> 1.0.51)
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
aasm (4.1.0) aasm (4.1.0)
actionmailer (4.2.1) actionmailer (4.2.5)
actionpack (= 4.2.1) actionpack (= 4.2.5)
actionview (= 4.2.1) actionview (= 4.2.5)
activejob (= 4.2.1) activejob (= 4.2.5)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 1.0, >= 1.0.5) rails-dom-testing (~> 1.0, >= 1.0.5)
actionpack (4.2.1) actionpack (4.2.5)
actionview (= 4.2.1) actionview (= 4.2.5)
activesupport (= 4.2.1) activesupport (= 4.2.5)
rack (~> 1.6) rack (~> 1.6)
rack-test (~> 0.6.2) rack-test (~> 0.6.2)
rails-dom-testing (~> 1.0, >= 1.0.5) rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.1) rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (4.2.1) actionpack-page_caching (1.0.2)
activesupport (= 4.2.1) actionpack (>= 4.0.0, < 5)
actionview (4.2.5)
activesupport (= 4.2.5)
builder (~> 3.1) builder (~> 3.1)
erubis (~> 2.7.0) erubis (~> 2.7.0)
rails-dom-testing (~> 1.0, >= 1.0.5) rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.1) rails-html-sanitizer (~> 1.0, >= 1.0.2)
activejob (4.2.1) active_record_query_trace (1.4)
activesupport (= 4.2.1) activejob (4.2.5)
activesupport (= 4.2.5)
globalid (>= 0.3.0) globalid (>= 0.3.0)
activemodel (4.2.1) activemodel (4.2.5)
activesupport (= 4.2.1) activesupport (= 4.2.5)
builder (~> 3.1) builder (~> 3.1)
activerecord (4.2.1) activerecord (4.2.5)
activemodel (= 4.2.1) activemodel (= 4.2.5)
activesupport (= 4.2.1) activesupport (= 4.2.5)
arel (~> 6.0) arel (~> 6.0)
activesupport (4.2.1) activesupport (4.2.5)
i18n (~> 0.7) i18n (~> 0.7)
json (~> 1.7, >= 1.7.7) json (~> 1.7, >= 1.7.7)
minitest (~> 5.1) minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4) thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1) tzinfo (~> 1.1)
addressable (2.3.8) addressable (2.3.8)
angularjs-rails (1.3.15) arel (6.0.3)
arel (6.0.0)
autoprefixer-rails (5.1.8) autoprefixer-rails (5.1.8)
execjs execjs
json json
awesome_print (1.6.1) awesome_print (1.6.1)
axiom-types (0.1.1)
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1)
bcrypt (3.1.10) bcrypt (3.1.10)
binding_of_caller (0.7.2) binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
@ -59,14 +58,15 @@ GEM
sass (>= 3.2.19) sass (>= 3.2.19)
buftok (0.2.0) buftok (0.2.0)
builder (3.2.2) builder (3.2.2)
byebug (4.0.4) camertron-eprun (1.1.0)
columnize (= 0.9.0)
capistrano (2.15.5) capistrano (2.15.5)
highline highline
net-scp (>= 1.0.0) net-scp (>= 1.0.0)
net-sftp (>= 2.0.0) net-sftp (>= 2.0.0)
net-ssh (>= 2.0.14) net-ssh (>= 2.0.14)
net-ssh-gateway (>= 1.1.0) net-ssh-gateway (>= 1.1.0)
capistrano-maintenance (0.0.5)
capistrano (~> 2.0)
capistrano-sidekiq (0.5.2) capistrano-sidekiq (0.5.2)
capistrano capistrano
sidekiq sidekiq
@ -77,8 +77,12 @@ GEM
mime-types (>= 1.16) mime-types (>= 1.16)
celluloid (0.16.0) celluloid (0.16.0)
timers (~> 4.0.0) timers (~> 4.0.0)
chroma (0.0.1)
chunky_png (1.3.4) chunky_png (1.3.4)
cldr-plurals-runtime-rb (1.0.1)
coderay (1.1.0) coderay (1.1.0)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
coffee-rails (4.1.0) coffee-rails (4.1.0)
coffee-script (>= 2.2.0) coffee-script (>= 2.2.0)
railties (>= 4.0.0, < 5.0) railties (>= 4.0.0, < 5.0)
@ -86,7 +90,6 @@ GEM
coffee-script-source coffee-script-source
execjs execjs
coffee-script-source (1.9.1) coffee-script-source (1.9.1)
columnize (0.9.0)
compass (1.0.3) compass (1.0.3)
chunky_png (~> 1.2) chunky_png (~> 1.2)
compass-core (~> 1.0.2) compass-core (~> 1.0.2)
@ -103,9 +106,11 @@ GEM
compass (~> 1.0.0) compass (~> 1.0.0)
sass-rails (<= 5.0.1) sass-rails (<= 5.0.1)
sprockets (< 2.13) sprockets (< 2.13)
connection_pool (2.1.3) connection_pool (2.2.0)
database_cleaner (1.4.1) database_cleaner (1.4.1)
debug_inspector (0.0.2) debug_inspector (0.0.2)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
devise (3.4.1) devise (3.4.1)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
@ -116,13 +121,30 @@ GEM
devise-async (0.9.0) devise-async (0.9.0)
devise (~> 3.2) devise (~> 3.2)
diff-lcs (1.2.5) diff-lcs (1.2.5)
disqus_api (0.0.4) domain_name (0.5.25)
activesupport (>= 3.0.0) unf (>= 0.0.5, < 1.0.0)
faraday (>= 0.8) elasticsearch (1.0.12)
faraday_middleware (>= 0.9) elasticsearch-api (= 1.0.12)
elasticsearch-transport (= 1.0.12)
elasticsearch-api (1.0.12)
multi_json
elasticsearch-model (0.1.7)
activesupport (> 3)
elasticsearch (> 0.4)
hashie
elasticsearch-persistence (0.1.7)
activemodel (> 3)
activesupport (> 3)
elasticsearch (> 0.4)
elasticsearch-model (>= 0.1)
hashie
virtus
elasticsearch-rails (0.1.7)
elasticsearch-transport (1.0.12)
faraday
multi_json
equalizer (0.0.11) equalizer (0.0.11)
erubis (2.7.0) erubis (2.7.0)
excon (0.45.1)
execjs (2.4.0) execjs (2.4.0)
factory_girl (4.5.0) factory_girl (4.5.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
@ -133,8 +155,6 @@ GEM
i18n (~> 0.5) i18n (~> 0.5)
faraday (0.9.1) faraday (0.9.1)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
faraday_middleware (0.9.1)
faraday (>= 0.7.4, < 0.10)
ffi (1.9.8) ffi (1.9.8)
figaro (1.1.0) figaro (1.1.0)
thor (~> 0.14) thor (~> 0.14)
@ -146,7 +166,7 @@ GEM
formatador (0.2.5) formatador (0.2.5)
friendly_id (5.1.0) friendly_id (5.1.0)
activerecord (>= 4.0.0) activerecord (>= 4.0.0)
globalid (0.3.3) globalid (0.3.6)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
guard (2.12.5) guard (2.12.5)
formatador (>= 0.2.4) formatador (>= 0.2.4)
@ -162,13 +182,17 @@ GEM
guard (~> 2.1) guard (~> 2.1)
guard-compat (~> 1.1) guard-compat (~> 1.1)
rspec (>= 2.99.0, < 4.0) rspec (>= 2.99.0, < 4.0)
hashie (3.4.2)
highline (1.7.1) highline (1.7.1)
hike (1.2.3) hike (1.2.3)
hitimes (1.2.2) hitimes (1.2.2)
http (0.6.4) http (0.6.4)
http_parser.rb (~> 0.6.0) http_parser.rb (~> 0.6.0)
http-cookie (1.0.2)
domain_name (~> 0.5)
http_parser.rb (0.6.0) http_parser.rb (0.6.0)
i18n (0.7.0) i18n (0.7.0)
ice_nine (0.11.1)
jbuilder (2.2.12) jbuilder (2.2.12)
activesupport (>= 3.0.0, < 5) activesupport (>= 3.0.0, < 5)
multi_json (~> 1.2) multi_json (~> 1.2)
@ -176,7 +200,8 @@ GEM
rails-dom-testing (~> 1.0) rails-dom-testing (~> 1.0)
railties (>= 4.2.0) railties (>= 4.2.0)
thor (>= 0.14, < 2.0) thor (>= 0.14, < 2.0)
json (1.8.2) json (1.8.3)
jwt (1.5.1)
kaminari (0.16.3) kaminari (0.16.3)
actionpack (>= 3.0.0) actionpack (>= 3.0.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
@ -185,27 +210,27 @@ GEM
addressable (~> 2.3) addressable (~> 2.3)
letter_opener (1.3.0) letter_opener (1.3.0)
launchy (~> 2.2) launchy (~> 2.2)
libv8 (3.16.14.7) libv8 (3.16.14.11)
listen (2.10.0) listen (2.10.0)
celluloid (~> 0.16.0) celluloid (~> 0.16.0)
rb-fsevent (>= 0.9.3) rb-fsevent (>= 0.9.3)
rb-inotify (>= 0.9) rb-inotify (>= 0.9)
loofah (2.0.1) loofah (2.0.3)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
lumberjack (1.0.9) lumberjack (1.0.9)
mail (2.6.3) mail (2.6.3)
mime-types (>= 1.16, < 3) mime-types (>= 1.16, < 3)
mandrill-api (1.0.53)
excon (>= 0.16.0, < 1.0)
json (>= 1.7.7, < 2.0)
memoizable (0.4.2) memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
message_format (0.0.3)
twitter_cldr (~> 3.1)
method_source (0.8.2) method_source (0.8.2)
mime-types (2.4.3) mime-types (2.99)
mini_magick (4.2.0) mini_magick (4.2.0)
mini_portile (0.6.2) mini_portile (0.6.2)
minitest (5.5.1) minitest (5.8.3)
multi_json (1.11.0) multi_json (1.11.2)
multi_xml (0.5.5)
multipart-post (2.0.0) multipart-post (2.0.0)
naught (1.0.0) naught (1.0.0)
nenv (0.2.0) nenv (0.2.0)
@ -216,7 +241,9 @@ GEM
net-ssh (2.9.2) net-ssh (2.9.2)
net-ssh-gateway (1.2.0) net-ssh-gateway (1.2.0)
net-ssh (>= 2.6.5) net-ssh (>= 2.6.5)
nokogiri (1.6.6.2) netrc (0.10.3)
newrelic_rpm (3.11.1.284)
nokogiri (1.6.6.4)
mini_portile (~> 0.6.0) mini_portile (~> 0.6.0)
notiffany (0.0.6) notiffany (0.0.6)
nenv (~> 0.1) nenv (~> 0.1)
@ -225,8 +252,28 @@ GEM
jbuilder (~> 2.0) jbuilder (~> 2.0)
rails (>= 4.2.0) rails (>= 4.2.0)
responders (~> 2.0) responders (~> 2.0)
oauth2 (1.0.0)
faraday (>= 0.8, < 0.10)
jwt (~> 1.0)
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (~> 1.2)
oj (2.12.8)
omniauth (1.2.2)
hashie (>= 1.2, < 4)
rack (~> 1.0)
omniauth-oauth2 (1.3.1)
oauth2 (~> 1.0)
omniauth (~> 1.2)
orm_adapter (0.5.0) orm_adapter (0.5.0)
pdf-core (0.5.1)
pg (0.18.1) pg (0.18.1)
prawn (2.0.1)
pdf-core (~> 0.5.1)
ttfunk (~> 1.4.0)
prawn-table (0.2.1)
protected_attributes (1.1.3)
activemodel (>= 4.0.1, < 5.0)
pry (0.10.1) pry (0.10.1)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.8.1) method_source (~> 0.8.1)
@ -235,38 +282,41 @@ GEM
rack (>= 1.1, < 2.0) rack (>= 1.1, < 2.0)
pundit (1.0.0) pundit (1.0.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
rack (1.6.0) rack (1.6.4)
rack-protection (1.5.3) rack-protection (1.5.3)
rack rack
rack-test (0.6.3) rack-test (0.6.3)
rack (>= 1.0) rack (>= 1.0)
rails (4.2.1) railroady (1.4.0)
actionmailer (= 4.2.1) rails (4.2.5)
actionpack (= 4.2.1) actionmailer (= 4.2.5)
actionview (= 4.2.1) actionpack (= 4.2.5)
activejob (= 4.2.1) actionview (= 4.2.5)
activemodel (= 4.2.1) activejob (= 4.2.5)
activerecord (= 4.2.1) activemodel (= 4.2.5)
activesupport (= 4.2.1) activerecord (= 4.2.5)
activesupport (= 4.2.5)
bundler (>= 1.3.0, < 2.0) bundler (>= 1.3.0, < 2.0)
railties (= 4.2.1) railties (= 4.2.5)
sprockets-rails sprockets-rails
rails-deprecated_sanitizer (1.0.3) rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha) activesupport (>= 4.2.0.alpha)
rails-dom-testing (1.0.6) rails-dom-testing (1.0.7)
activesupport (>= 4.2.0.beta, < 5.0) activesupport (>= 4.2.0.beta, < 5.0)
nokogiri (~> 1.6.0) nokogiri (~> 1.6.0)
rails-deprecated_sanitizer (>= 1.0.1) rails-deprecated_sanitizer (>= 1.0.1)
rails-html-sanitizer (1.0.2) rails-html-sanitizer (1.0.2)
loofah (~> 2.0) loofah (~> 2.0)
rails-observers (0.1.2)
activemodel (~> 4.0)
rails_12factor (0.0.3) rails_12factor (0.0.3)
rails_serve_static_assets rails_serve_static_assets
rails_stdout_logging rails_stdout_logging
rails_serve_static_assets (0.0.4) rails_serve_static_assets (0.0.4)
rails_stdout_logging (0.0.3) rails_stdout_logging (0.0.3)
railties (4.2.1) railties (4.2.5)
actionpack (= 4.2.1) actionpack (= 4.2.5)
activesupport (= 4.2.1) activesupport (= 4.2.5)
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0) thor (>= 0.18.1, < 2.0)
raindrops (0.13.0) raindrops (0.13.0)
@ -281,9 +331,13 @@ GEM
redis (3.2.1) redis (3.2.1)
redis-namespace (1.5.2) redis-namespace (1.5.2)
redis (~> 3.0, >= 3.0.4) redis (~> 3.0, >= 3.0.4)
ref (1.0.5) ref (2.0.0)
responders (2.1.0) responders (2.1.0)
railties (>= 4.2.0, < 5) railties (>= 4.2.0, < 5)
rest-client (1.8.0)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 3.0)
netrc (~> 0.7)
rolify (4.0.0) rolify (4.0.0)
rspec (3.2.0) rspec (3.2.0)
rspec-core (~> 3.2.0) rspec-core (~> 3.2.0)
@ -324,7 +378,7 @@ GEM
activerecord (~> 4) activerecord (~> 4)
activesupport (~> 4) activesupport (~> 4)
shellany (0.0.1) shellany (0.0.1)
sidekiq (3.3.3) sidekiq (3.3.4)
celluloid (>= 0.16.0) celluloid (>= 0.16.0)
connection_pool (>= 2.1.1) connection_pool (>= 2.1.1)
json json
@ -340,19 +394,22 @@ GEM
rack-protection (~> 1.4) rack-protection (~> 1.4)
tilt (>= 1.3, < 3) tilt (>= 1.3, < 3)
slop (3.6.0) slop (3.6.0)
spring (1.3.3) spring (1.3.5)
spring-commands-rspec (1.0.4) spring-commands-rspec (1.0.4)
spring (>= 0.9.1) spring (>= 0.9.1)
sprockets (2.12.3) sprockets (2.12.4)
hike (~> 1.2) hike (~> 1.2)
multi_json (~> 1.0) multi_json (~> 1.0)
rack (~> 1.0) rack (~> 1.0)
tilt (~> 1.1, != 1.3.0) tilt (~> 1.1, != 1.3.0)
sprockets-rails (2.2.4) sprockets-rails (2.3.3)
actionpack (>= 3.0) actionpack (>= 3.0)
activesupport (>= 3.0) activesupport (>= 3.0)
sprockets (>= 2.8, < 4.0) sprockets (>= 2.8, < 4.0)
therubyracer (0.12.1) stripe (1.30.2)
json (~> 1.8.1)
rest-client (~> 1.4)
therubyracer (0.12.0)
libv8 (~> 3.16.14.0) libv8 (~> 3.16.14.0)
ref ref
thor (0.19.1) thor (0.19.1)
@ -360,6 +417,7 @@ GEM
tilt (1.4.1) tilt (1.4.1)
timers (4.0.1) timers (4.0.1)
hitimes hitimes
ttfunk (1.4.0)
twitter (5.14.0) twitter (5.14.0)
addressable (~> 2.3) addressable (~> 2.3)
buftok (~> 0.2.0) buftok (~> 0.2.0)
@ -373,6 +431,11 @@ GEM
simple_oauth (~> 0.3.0) simple_oauth (~> 0.3.0)
twitter-text (1.11.0) twitter-text (1.11.0)
unf (~> 0.1.0) unf (~> 0.1.0)
twitter_cldr (3.2.1)
camertron-eprun
cldr-plurals-runtime-rb (~> 1.0.0)
json
tzinfo
tzinfo (1.2.2) tzinfo (1.2.2)
thread_safe (~> 0.1) thread_safe (~> 0.1)
uglifier (2.7.1) uglifier (2.7.1)
@ -385,6 +448,11 @@ GEM
kgio (~> 2.6) kgio (~> 2.6)
rack rack
raindrops (~> 0.7) raindrops (~> 0.7)
virtus (1.0.5)
axiom-types (~> 0.1)
coercible (~> 1.0)
descendants_tracker (~> 0.0, >= 0.0.3)
equalizer (~> 0.0, >= 0.0.9)
warden (1.2.3) warden (1.2.3)
rack (>= 1.0) rack (>= 1.0)
web-console (2.1.2) web-console (2.1.2)
@ -398,19 +466,23 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
aasm aasm
angularjs-rails actionpack-page_caching
active_record_query_trace
awesome_print awesome_print
bootstrap-sass bootstrap-sass
byebug
capistrano capistrano
capistrano-maintenance (= 0.0.5)
capistrano-sidekiq capistrano-sidekiq
carrierwave carrierwave
chroma
coffee-rails (~> 4.1.0) coffee-rails (~> 4.1.0)
compass-rails (= 2.0.4) compass-rails (= 2.0.4)
database_cleaner database_cleaner
devise devise
devise-async devise-async
disqus_api elasticsearch-model
elasticsearch-persistence
elasticsearch-rails
factory_girl_rails factory_girl_rails
faker faker
figaro figaro
@ -423,13 +495,22 @@ DEPENDENCIES
jquery-rails jquery-rails
kaminari kaminari
letter_opener letter_opener
mandrill_dm! message_format
mini_magick mini_magick
newrelic_rpm
notify_with notify_with
oj
omniauth
omniauth-oauth2
pg pg
prawn
prawn-table
protected_attributes
puma puma
pundit pundit
rails (= 4.2.1) railroady
rails (= 4.2.5)
rails-observers
rails_12factor rails_12factor
recurrence recurrence
responders (~> 2.0) responders (~> 2.0)
@ -444,9 +525,13 @@ DEPENDENCIES
sinatra sinatra
spring spring
spring-commands-rspec spring-commands-rspec
therubyracer stripe (= 1.30.2)
therubyracer (= 0.12.0)
twitter twitter
twitter-text twitter-text
uglifier (>= 1.3.0) uglifier (>= 1.3.0)
unicorn unicorn
web-console (~> 2.0) web-console (~> 2.0)
BUNDLED WITH
1.10.6

View File

@ -1,3 +1,53 @@
Copyright (C) 2015 La Casemate
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
FabManager uses some external components, which are licenced under the
terms of [Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0):
- [jasny-bootstrap](https://github.com/jasny/bootstrap/)
- [elasticsearch](https://github.com/elasticsearch/bower-elasticsearch-js)
- [nvd3](https://github.com/novus/nvd3)
- [angular-bootstrap-switch](https://github.com/frapontillo/angular-bootstrap-switch)
- [elasticsearch-rails](https://github.com/elastic/elasticsearch-rails)
- [elasticsearch-model](https://github.com/elastic/elasticsearch-rails/tree/master/elasticsearch-model)
- [elasticsearch-persistence](https://github.com/elastic/elasticsearch-rails/tree/master/elasticsearch-persistence)
- font [Open Sans](http://www.fontsquirrel.com/fonts/open-sans)
Some other used libraries/components are licenced under the terms of the
[General Public License version 2](http://www.gnu.org/licenses/old-licenses/gpl-2.0-faq.en.html):
- [ruby](https://www.ruby-lang.org)
- [railroady](https://github.com/preston/railroady)
- [unicorn](https://github.com/defunkt/unicorn)
- [prawn](https://github.com/prawnpdf/prawn)
- [prawn-table](https://github.com/prawnpdf/prawn-table)
Errors and omissions excepted, the other external libraries used in this
project are licenced under the terms of the [MIT Licence](https://opensource.org/licenses/MIT).
Please refer to the libraries documentation for more informations about
their licences.
Complete lists of used libraries are available in `bower.json` for the
EcmaScript libraries and in `Gemfile` for Ruby libraries.
GNU AFFERO GENERAL PUBLIC LICENSE GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007 Version 3, 19 November 2007
@ -616,47 +666,4 @@ an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee. copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>.

View File

@ -1,2 +1,2 @@
web: bundle exec rails server puma -p $PORT web: bundle exec rails server puma -p $PORT -b0.0.0.0
worker: bundle exec sidekiq -C ./config/sidekiq.yml worker: bundle exec sidekiq -C ./config/sidekiq.yml

583
README.md
View File

@ -1,148 +1,533 @@
# README # FabManager
This project is the FabLab Manager web application. FabManager is the FabLab management solution. It is web-based, open-source and totally free.
The purpose of this web application is to allow users to document their FabLab projects. The FabLab also have the ability
to plan some events (workshops or courses) and to expose them to its users.
This product can be extended to be used as a complete internal management system for a FabLab. ##### Table of Contents
1. [Software stack](#software-stack)
The underlying technologies are: 2. [Contributing](#contributing)
- `Ruby on Rails` for the backend application (server RESTful API) 3. [Setup a development environment](#setup-a-development-environment)
- `AngularJS` for the frontend application (web-based graphical user interface) 3.1 [General Guidelines](#general-guidelines)
3.2 [Environment Configuration](#environment-configuration)
4. [PostgreSQL](#postgresql)
4.1 [Install PostgreSQL 9.4 on Ubuntu/Debian](#postgresql-on-debian)
4.2 [Install and launch PostgreSQL on MacOS X](#postgresql-on-macosx)
4.3 [Setup the FabManager database in PostgreSQL](#setup-fabmanager-in-postgresql)
5. [ElasticSearch](#elasticsearch)
5.1 [Install ElasticSearch on Ubuntu/Debian](#elasticsearch-on-debian)
5.2 [Install ElasticSearch on MacOS X](#elasticsearch-on-macosx)
5.3 [Setup ElasticSearch for the FabManager](#setup-fabmanager-in-elasticsearch)
6. [Internationalization (i18n)](#i18n)
6.1 [Translation](#i18n-translation)
6.1.1 [Front-end translations](#i18n-translation-front)
6.1.2 [Back-end translations](#i18n-translation-back)
6.2 [Configuration](#i18n-configuration)
6.2.1 [Settings](#i18n-settings)
6.2.2 [Applying changes](#i18n-apply)
7. [Known issues](#known-issues)
8. [Related Documentation](#related-documentation)
## 1. Configuration <a name="software-stack"></a>
## Software stack
The following files must be filled with the correct configuration to allow FabManager to run correctly: FabManager is a Ruby on Rails / AngularJS web application that runs on the following software:
- config/environments/production.rb - Ubuntu/Debian
- `mandrill` -> change this if you're using a different mailing system - Ruby 2.2.3
- Git 1.9.1+
- config/environments/staging.rb - Redis 2.8.4+
- `config.action_mailer.default_url_options` -> change the URL according to the staging deployment url - Sidekiq 3.3.4+
- `mandrill` -> change this if you're using a different mailing system - Elasticsearch 1.7
- PostgreSQL 9.4
- config/application.yml <a name="contributing"></a>
- `DEVISE_KEY` -> generate any secret phrase to secure the Devise authentication. You can use the `$ rake secret` command for this purpose. ## Contributing
- `SECRET_KEY_BASE` -> generate any secret phrase here to prevent XSS attacks. You can use the `$ rake secret` command for this purpose.
- `DEFAULT_MAIL_FROM` -> default e-mail address from which the emails are sent
- `MANDRILL_USERNAME` -> if you plan to use mandrill
- `MANDRILL_APIKEY` -> if you plan to use mandrill
- `TWITTER_NAME` -> twitter api configuration
- `TWITTER_CONSUMER_KEY` -> twitter api configuration
- `TWITTER_CONSUMER_SECRET` -> twitter api configuration
- `TWITTER_ACCESS_TOKEN` -> twitter api configuration
- `TWITTER_ACCESS_TOKEN_SECRET` -> twitter api configuration
- `GOOGLE_ANALYTICS_ACCOUNT` -> Google analytics account identifier (if you want to use GA)
- `APPLICATION_ROOT_URL` -> The public URL where you application is deployed in production (eg. fablab.lacasemate.com)
- config/mandrill.rb Contributions are welcome. Please read [the contribution guidelines](CONTRIBUTING.md) for more information about the contribution process.
You may change this if you don't want to use mandrill as your production mailing system
- config/database.yml.default **IMPORTANT**: **do not** update Arshaw/fullCalendar.js as it contains a hack for the remove-event cross.
Copy/Paste this file to `config/database.yml` and modify the configuration according to your postgreSQL configuration
- config/disqus_api.yml <a name="setup-a-development-environment"></a>
Insert here your identifiers for the Disqus API ## Setup a development environment
<a name="general-guidelines"></a>
### General Guidelines
1. Install RVM with the ruby version specified in the [.ruby-version file](.ruby-version).
## 2. Setup a development environment For more details about the process, Please read the [official RVM documentation](http://rvm.io/rvm/install).
1. Install RVM with latest ruby version
See http://rvm.io/rvm/install
2. Retrieve the project from Git 2. Retrieve the project from Git
`$ git clone git@github.com:LaCasemate/fab-manager.git`
3. Install the dependencies ```bash
- Ubuntu: `$ sudo apt-get install libpq-dev postgresql redis-server imagemagick` git clone https://github.com/LaCasemate/fab-manager.git
- MacOS: `$ brew install postgresql redis imagemagick` ```
3. Install the software dependencies.
- For Ubuntu/Debian:
```bash
sudo apt-get install libpq-dev postgresql-9.4 redis-server imagemagick
```
- For MacOS X:
4. Init the RVM instance and check it was correctly configured ```bash
``` brew install postgresql redis imagemagick
$ cd fab-manager ```
$ rvm current
``` 4. Init the RVM instance and check it was correctly configured
```bash
cd fab-manager
rvm current
# Must print ruby-X.Y.Z@fablab (where X.Y.Z match the version in .ruby-version)
```
5. Setup the project requirements 5. Install bundler in the current RVM gemset
`$ bundle install`
```bash
gem install bundler
```
6. Install the required ruby gems
```bash
bundle install
```
6. Build the database. You may have to configure your postgreSQL instance before, as described in chapter `3.2 Setup the FabManager database in PostgreSQL` 7. Build the database. You may have to follow the steps described in [the PostgreSQL installation chapter](#postgresql) before, if you don't already have a working installation of PostgreSQL.
`$ rake db:setup`
```bash
rake db:setup
```
7. Create the pids folder used by sidekiq. If you want to use a different location, you can configure it in `config/sidekiq.yml` 8. Create the pids folder used by Sidekiq. If you want to use a different location, you can configure it in `config/sidekiq.yml`
`$ mkdir -p tmp/pids`
```bash
mkdir -p tmp/pids
```
9. Create the default configuration file **and configure it !** (see the [Environment Configuration](#environment-configuration) section)
```bash
cp config/application.yml.default config/application.yml
vi config/application.yml
# or use your favorite editor instead of vi (nano, ne...)
```
8. Configure the application environment variables, as explained in chapter `1. Configuration` 10. Start the development web server
9. Start the development web server ```bash
`$ foreman s -p 3000` foreman s -p 3000
```
11. You should now be able to access your local development FabManager instance by accessing `http://localhost:3000` in your web browser.
12. You can login as the default administrator using the following credentials:
- user: admin@fab-manager.com
- password: adminadmin
<a name="environment-configuration"></a>
### Environment Configuration
The settings in `config/application.yml` configure the environment variables of the application.
If you are in a development environment, your can keep the default values, otherwise, in production, values must be configured carefully.
#### POSTGRES_HOST
DNS name or IP address of the server hosting the PostgreSQL database of the application (see [PostgreSQL](#postgresql)).
#### POSTGRES_PASSWORD
Password for the PostgreSQL user, as specified in `database.yml`.
Please see [Setup the FabManager database in PostgreSQL](#setup-fabmanager-in-postgresql) for informations on how to create a user and set his password.
#### REDIS_HOST
DNS name or IP address of the server hosting the redis database.
#### ELASTICSEARCH_HOST
DNS name or IP address of the server hosting the elasticSearch database.
#### SECRET_KEY_BASE
Used by the authentication system to generate random tokens, eg. for resetting passwords.
Used by Rails to generate the integrity of signed cookies.
You can generate such a random key by running `rake secret`.
#### STRIPE_API_KEY & STRIPE_PUBLISHABLE_KEY
Key an secret used to identify you Stripe account through the API.
Retrieve them from https://dashboard.stripe.com/account/apikeys.
#### STRIPE_CURRENCY
Currency used by stripe to charge the final customer.
See https://support.stripe.com/questions/which-currencies-does-stripe-support for a list of available 3-letters ISO code.
#### INVOICE_PREFIX
When payments are done on the platform, an invoice will be generate as a PDF file.
This value configure the prefix of the PDF file name.
#### FABLAB_WITHOUT_PLANS
If set to 'true', the subscription plans will be fully disabled and invisible in the application.
#### DEFAULT_MAIL_FROM
When sending notification mails, the platform will use this address to identify the sender.
#### DELIVERY_METHOD
Configure the Rails' Action Mailer delivery method.
See http://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration for more details.
#### DEFAULT_HOST, DEFAULT_PROTOCOL, SMTP_ADDRESS, SMTP_PORT, SMTP_USER_NAME & SMTP_PASSWORD
When DELIVERY_METHOD is set to **smtp**, configure the SMTP server parameters.
See http://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration for more details.
DEFAULT_HOST is also used to configure Google Analytics.
#### GA_ID
Identifier of your Google Analytics account.
#### DISQUS_SHORTNAME
Unique identifier of your [Disqus](http://www.disqus.com) forum.
Disquq forums are used to allow visitors to comment on projects.
See https://help.disqus.com/customer/portal/articles/466208-what-s-a-shortname- for more informations.
#### TWITTER_NAME
Identifier of the Twitter account, for witch the last tweet will be displayed on the home page.
#### TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET, TWITTER_ACCESS_TOKEN & TWITTER_ACCESS_TOKEN_SECRET
Keys and secrets to access the twitter API.
#### Settings related to i18n
See the [Settings](#i18n-settings) section of the [Internationalization (i18n)](#i18n) paragraph for a detailed description of these parameters.
<a name="postgresql"></a>
## PostgreSQL
## 3. PostgreSQL <a name="postgresql-on-debian"></a>
### Install PostgreSQL 9.4 on Ubuntu/Debian
### 3.1 Launch PostgreSQL on MacOS 1. Create the file `/etc/apt/sources.list.d/pgdg.list`, and append it one the following lines:
- `deb http://apt.postgresql.org/pub/repos/apt/ trusty-pgdg main` (Ubuntu 14.04 Trusty)
$ ln -sfv /usr/local/opt/postgresql/*.plist ~/Library/LaunchAgents - `deb http://apt.postgresql.org/pub/repos/apt/ jessie-pgdg main` (Debian 8 Jessie)
$ launchctl load ~/Library/LaunchAgents/homebrew.mxcl.postgresql.plist
The first command will start postgresql at login with launchd. The second will load postgresql now.
### 3.2 Setup the FabManager database in PostgreSQL 2. Import the repository signing key, and update the package lists
```bash
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
sudo apt-get update
```
3. Install PostgreSQL 9.4
```bash
sudo apt-get install postgresql-9.4
```
<a name="postgresql-on-macosx"></a>
### Install and launch PostgreSQL on MacOS X
This assumes you have [Homebrew](http://brew.sh/) installed on your system.
Otherwise, please follow the official instructions on the project's website.
1. Update brew and install PostgreSQL
```bash
brew update
brew install postgres
```
2. Launch PostgreSQL
```bash
# Start postgresql at login with launchd
ln -sfv /usr/local/opt/postgresql/*.plist ~/Library/LaunchAgents
# Load PostgreSQL now
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.postgresql.plist
```
<a name="setup-fabmanager-in-postgresql"></a>
### Setup the FabManager database in PostgreSQL
Before running `rake db:setup`, you have to make sure that the user configured in [config/database.yml](config/database.yml) for the `development` environment exists.
To create it, please follow these instructions:
1. Login as the postgres user 1. Login as the postgres user
`$ sudo -i -u postgres`
2. Run the postgreSQL administration command line interface ```bash
`$ psql` sudo -i -u postgres
```
2. Run the PostgreSQL administration command line interface
```bash
psql
```
3. Create a new user in postgres (in this example, the user will be named "sleede") 3. Create a new user in PostgreSQL (in this example, the user will be named `sleede`)
`# CREATE USER sleede;`
```sql
CREATE USER sleede;
```
4. Grant him the right to create databases 4. Grant him the right to create databases
`# ALTER ROLE sleede WITH CREATEDB;`
```sql
ALTER ROLE sleede WITH CREATEDB;
```
5. Then create the fablab database 5. Then, create the fablab_development and fablab_test databases
`# CREATE DATABASE fabmanager_development OWNER sleede;`
```sql
CREATE DATABASE fablab_development OWNER sleede;
CREATE DATABASE fablab_test OWNER sleede;
```
6. To finish, attribute a password to this user 6. To finish, attribute a password to this user
`# ALTER USER sleede WITH ENCRYPTED PASSWORD 'sleede';`
## 4. Known issue ```sql
ALTER USER sleede WITH ENCRYPTED PASSWORD 'sleede';
```
You may encounter the following error message when running the application for the first time: <a name="elasticsearch"></a>
## ElasticSearch
```bash ElasticSearch is a powerful search engine based on Apache Lucene combined with a NoSQL database used as a cache to index data and quickly process complex requests on it.
Uncaught exception: FATAL: authentification peer échouée pour l'utilisateur « USERNAME »
Exiting In FabManager, it is used for the admin's statistics module and to perform searches in projects.
.rvm/gems/ruby-2.2.1@fabmanager/gems/activerecord-4.2.1/lib/active_record/connection_adapters/postgresql_adapter.rb:651:in `initialize'
... <a name="elasticsearch-on-debian"></a>
``` ### Install ElasticSearch on Ubuntu/Debian
To solve this issue, edit your `/etc/postgresql/9.4/main/pg_hba.conf` as root and replace the following: For a more detailed guide concerning the ElasticSearch installation, please check the [official documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/setup.html)
1. Install the OpenJDK's Java Runtime Environment (JRE). ElasticSearch recommends that you install Java 8 update 20 or later.
Please check that your distribution's version meet this requirement.
```bash ```bash
# comment over or replace... sudo apt-get install openjdk-8-jre
local all all peer
# ...by the following:
local all all trust
``` ```
Then, restart postgreSQL to validate the modification (`sudo service postgresql restart`). 1. Create the file `/etc/apt/sources.list.d/elasticsearch-1.x.list`, and append it the following line:
`deb http://packages.elastic.co/elasticsearch/1.x/debian stable main`
2. Import the repository signing key, and update the package lists
```bash
wget -qO - https://packages.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -
sudo apt-get update
```
3. Install ElasticSearch 1.7
```bash
sudo apt-get install elasticsearch
```
4. To automatically start ElasticSearch during bootup, then, depending if your system is compatible with SysV (eg. Ubuntu 14.04) or uses systemd (eg. Debian 8), you will need to run:
```bash
# System V
sudo update-rc.d elasticsearch defaults 95 10
# *** OR *** (systemd)
sudo /bin/systemctl daemon-reload
sudo /bin/systemctl enable elasticsearch.service
```
<a name="elasticsearch-on-macosx"></a>
### Install ElasticSearch on MacOS X
This assumes you have [Homebrew](http://brew.sh/) installed on your system.
Otherwise, please follow the official instructions on the project's website.
```bash
brew update
brew install homebrew/versions/elasticsearch17
```
<a name="setup-fabmanager-in-elasticsearch"></a>
### Setup ElasticSearch for the FabManager
1. Launch the associated rake tasks in the project folder.
This will create the fields mappings in ElasticSearch DB
```bash
rake fablab:es_build_stats
```
2. Every nights, the statistics for the day that just ended are built automatically at 01:00 (AM).
See [schedule.yml](config/schedule.yml) to modify this behavior.
If the scheduled task wasn't executed for any reason (eg. you are in a dev environment and your computer was turned off at 1 AM), you can force the statistics data generation in ElasticSearch, running the following commands in a rails console.
```bash
rails c
```
```ruby
# Here for the 200 last days
200.times.each do |i|
StatisticService.new.generate_statistic({start_date: i.day.ago.beginning_of_day,end_date: i.day.ago.end_of_day})
end
```
<a name="i18n"></a>
## Internationalization (i18n)
The FabManager application can only run in a single language but this language can easily be changed.
<a name="i18n-translation"></a>
### Translation
Check the files located in `config/locales`:
- Front app translations (angular.js) are located in `config/locales/app.scope.XX.yml`.
Where scope has one the following meaning :
- admin: translations of the administrator views (manage and configure the FabLab).
- logged: translations of the end-user's views accessible only to connected users.
- public: translation of end-user's views publicly accessible to anyone.
- shared: translations shared by many views (like forms or buttons).
- Back app translations (Ruby on Rails) are located in `config/locales/XX.yml`.
- Emails translations are located in `config/locales/mails.XX.yml`.
- Messages related to the authentication system are located in `config/locales/devise.XX.yml`.
If you plan to translate the application to a new locale, please consider that the reference translation is French.
Indeed, in some cases, the English texts/sentences can seems confuse or lack of context as they were originally translated from French.
To prevent syntax mistakes while translating locale files, we **STRONGLY advise** you to use a text editor witch support syntax coloration for YML and Ruby.
<a name="i18n-translation-front"></a>
#### Front-end translations
Front-end translations uses [angular-translate](http://angular-translate.github.io) with some interpolations interpreted by angular.js and other interpreted by [MessageFormat](https://github.com/SlexAxton/messageformat.js/).
**These two kinds of interpolation use a near but different syntax witch SHOULD NOT be confused.**
Please refer to the official [angular-translate documentation](http://angular-translate.github.io/docs/#/guide/14_pluralization) before translating.
<a name="i18n-translation-back"></a>
#### Back-end translations
Back-end translations uses the [Ruby on Rails syntax](http://guides.rubyonrails.org/i18n.html) but some complex interpolations are interpreted by [MessageFormat](https://github.com/format-message/message-format-rb) and are marked as it in comments.
**DO NOT confuse the syntaxes.**
In each cases, some inline comments are included in the localisation files.
They can be recognized as they start with the sharp character (#).
These comments are not required to be translated, they are intended to help the translator to have some context informations about the sentence to translate.
<a name="i18n-configuration"></a>
### Configuration
Locales configurations are made in `config/application.yml`.
If you are in a development environment, your can keep the default values, otherwise, in production, values must be configured carefully.
<a name="i18n-settings"></a>
#### Settings
##### RAILS_LOCALE
Be sure that `config/locales/rails.XX.yml` exists, where `XX` match your configured rails_locale.
You can find templates of these files at https://github.com/svenfuchs/rails-i18n/tree/rails-4-x/rails/locale.
Be aware that **this file MUST contain the CURRENCY symbol used to generate invoices** (among other things).
Default is **en**.
##### MOMENT_LOCALE
Configure the moment.js library for l10n.
See `vendor/assets/components/moment/locale/*.js` for a list of available locales.
Default is **en** (even if it's not listed).
##### SUMMERNOTE_LOCALE
Configure the javascript summernote editor for l10n.
See `vendor/assets/components/summernote/lang/summernote-*.js` for a list of available locales.
Default is **en-US** (even if it's not listed).
##### ANGULAR_LOCALE
Configure the locale for angular-i18n.
Please, be aware that **the configured locale will imply the CURRENCY displayed to front-end users.**
_Eg.: configuring **fr-fr** will set the currency symbol to **€** but **fr-ca** will set **$** as currency symbol, so setting the `angular_locale` to simple **fr** (without country indication) will probably not do what you expect._
See `vendor/assets/components/angular-i18n/angular-locale_*.js` for a list of available locales. Default is **en**.
##### MESSAGEFORMAT_LOCALE
Configure the messageformat.js library, used by angular-translate.
See vendor/assets/components/messageformat/locale/*.js for a list of available locales.
##### FULLCALENDAR_LOCALE
Configure the fullCalendar JS agenda library.
See `vendor/assets/components/fullcalendar/dist/lang/*.js` for a list of available locales. Default is **en** (even if it's not listed).
##### ELASTICSEARCH_LANGUAGE_ANALYZER
This configure the language analyzer for indexing and searching in projects with ElasticSearch.
See https://www.elastic.co/guide/en/elasticsearch/reference/1.7/analysis-lang-analyzer.html for a list of available analyzers (check that the doc version match your installed elasticSearch version).
##### TIME_ZONE
In Rails: set Time.zone default to the specified zone and make Active Record auto-convert to this zone. Run `rake time:zones:all` for a list of available time zone names.
Default is **UTC**.
##### WEEK_STARTING_DAY
Configure the first day of the week in your locale zone (generally monday or sunday).
##### D3_DATE_FORMAT
Date format for dates displayed in statistics charts.
See https://github.com/mbostock/d3/wiki/Time-Formatting#format for available formats.
## 5. Related Documentation <a name="i18n-apply"></a>
- Angular-Bootstrap: http://angular-ui.github.io/bootstrap/ #### Applying changes
After modifying any values concerning the localisation, restart the application (ie. web server) to apply these changes in the i18n configuration.
## 6. Translations <a name="known-issues"></a>
- French translation is available on the branches [master](../../tree/master) and [dev](../../tree/dev) ## Known issues
- English translation is available on the branch [english](../../tree/english)
- When browsing a machine page, you may encounter an "InterceptError" in the console and the loading bar will stop loading before reaching its ending.
This may append if the machine was created through a seed file without any image.
To solve this, simply add an image to the machine's profile and refresh the web page.
- When starting the Ruby on Rails server (eg. `foreman s`) you may receive the following error:
worker.1 | invalid url: redis::6379
web.1 | Exiting
worker.1 | ...lib/redis/client.rb...:in `_parse_options'
This may happens when the `application.yml` file is missing.
To solve this issue copy `config/application.yml.default` to `config/application.yml`.
This is required before the first start.
<a name="related-documentation"></a>
## Related Documentation
- [Ruby 2.2.3](http://ruby-doc.org/core-2.2.3/)
- [Ruby on Rails](http://api.rubyonrails.org)
- [AngularJS](https://docs.angularjs.org/api)
- [Angular-Bootstrap](http://angular-ui.github.io/bootstrap/)
- [ElasticSearch 1.7](https://www.elastic.co/guide/en/elasticsearch/reference/1.7/index.html)

73
Vagrantfile vendored Normal file
View File

@ -0,0 +1,73 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# All Vagrant configuration is done below. The "2" in Vagrant.configure
# configures the configuration version (we support older styles for
# backwards compatibility). Please don't change it unless you know what
# you're doing.
Vagrant.configure(2) do |config|
# The most common configuration options are documented and commented below.
# For a complete reference, please see the online documentation at
# https://docs.vagrantup.com.
# Every Vagrant development environment requires a box. You can search for
# boxes at https://atlas.hashicorp.com/search.
config.vm.box = "ubuntu/trusty64"
# Disable automatic box update checking. If you disable this, then
# boxes will only be checked for updates when the user runs
# `vagrant box outdated`. This is not recommended.
# config.vm.box_check_update = false
# Create a forwarded port mapping which allows access to a specific port
# within the machine from a port on the host machine. In the example below,
# accessing "localhost:8080" will access port 80 on the guest machine.
config.vm.network "forwarded_port", guest: 3000, host: 3000 # rails/puma
config.vm.network "forwarded_port", guest: 9200, host: 9200 # elasticsearch
config.vm.network "forwarded_port", guest: 5432, host: 5432 # postgreSQL
# Create a private network, which allows host-only access to the machine
# using a specific IP.
# config.vm.network "private_network", ip: "192.168.33.10"
# Create a public network, which generally matched to bridged network.
# Bridged networks make the machine appear as another physical device on
# your network.
# config.vm.network "public_network"
# Share an additional folder to the guest VM. The first argument is
# the path on the host to the actual folder. The second argument is
# the path on the guest to mount the folder. And the optional third
# argument is a set of non-required options.
# config.vm.synced_folder "../data", "/vagrant_data"
# Provider-specific configuration so you can fine-tune various
# backing providers for Vagrant. These expose provider-specific options.
# Example for VirtualBox:
#
config.vm.provider "virtualbox" do |vb|
# Display the VirtualBox GUI when booting the machine
# vb.gui = true
# Customize the amount of memory on the VM:
vb.memory = 512
end
#
# View the documentation for the provider you are using for more
# information on available options.
# Define a Vagrant Push strategy for pushing to Atlas. Other push strategies
# such as FTP and Heroku are also available. See the documentation at
# https://docs.vagrantup.com/v2/push/atlas.html for more information.
# config.push.define "atlas" do |push|
# push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME"
# end
# Enable provisioning with a shell script. Additional provisioners such as
# Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
# documentation for more information about their specific syntax and use.
# config.vm.provision "shell", inline: <<-SHELL
# sudo apt-get update
# sudo apt-get install -y apache2
# SHELL
end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 613 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 845 B

BIN
app/assets/images/visa.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -16,48 +16,73 @@ Application.Directives = angular.module('application.directives', []);
angular.module('application', ['ngCookies', 'ngResource', 'ngSanitize', 'ngAnimate', 'ngCookies', 'ui.router', 'ui.bootstrap', angular.module('application', ['ngCookies', 'ngResource', 'ngSanitize', 'ngAnimate', 'ngCookies', 'ui.router', 'ui.bootstrap',
'ngUpload', 'duScroll', 'application.filters','application.services', 'application.directives', 'ngUpload', 'duScroll', 'application.filters','application.services', 'application.directives',
'application.constants', 'application.controllers', 'application.router', 'ui.select2', 'angularMoment', 'frapontillo.bootstrap-switch', 'application.constants', 'application.controllers', 'application.router',
'Devise', 'DeviseModal', 'angular-growl', 'xeditable', 'checklist-model', 'unsavedChanges', 'angular-loading-bar', 'ui.select', 'ui.calendar', 'angularMoment', 'Devise', 'DeviseModal', 'angular-growl', 'xeditable',
'ngTouch', 'angular-google-analytics', 'angularUtils.directives.dirDisqus', 'summernote']). 'checklist-model', 'unsavedChanges', 'angular-loading-bar', 'ngTouch', 'angular-google-analytics',
config(['$locationProvider', '$httpProvider', 'AuthProvider', "growlProvider", "unsavedWarningsConfigProvider", "AnalyticsProvider", "datepickerPopupConfig", 'angularUtils.directives.dirDisqus', 'summernote', 'elasticsearch', 'angular-medium-editor', 'naif.base64',
function($locationProvider, $httpProvider, AuthProvider, growlProvider, unsavedWarningsConfigProvider, AnalyticsProvider, datepickerPopupConfig) { 'minicolors', 'pascalprecht.translate']).
config(['$httpProvider', 'AuthProvider', "growlProvider", "unsavedWarningsConfigProvider", "AnalyticsProvider", "uibDatepickerPopupConfig", "$provide", "$translateProvider",
function($httpProvider, AuthProvider, growlProvider, unsavedWarningsConfigProvider, AnalyticsProvider, uibDatepickerPopupConfig, $provide, $translateProvider) {
<% if Rails.env.production? and ENV["GOOGLE_ANALYTICS_ACCOUNT"] != 'UA-YOUR_ID_HERE' and ENV["GOOGLE_ANALYTICS_ACCOUNT"] != nil %> // Google analytics
AnalyticsProvider.setAccount('<%= ENV["GOOGLE_ANALYTICS_ACCOUNT"] %>'); <% if Rails.env.production? %>
AnalyticsProvider.setAccount(Fablab.gaId);
// track all routes (or not) // track all routes (or not)
AnalyticsProvider.trackPages(true); AnalyticsProvider.trackPages(true);
AnalyticsProvider.setDomainName('<%= ENV["APPLICATION_ROOT_URL"] %>'); AnalyticsProvider.setDomainName(Fablab.defaultHost);
AnalyticsProvider.useAnalytics(true); AnalyticsProvider.useAnalytics(true);
AnalyticsProvider.setPageEvent('$stateChangeSuccess'); AnalyticsProvider.setPageEvent('$stateChangeSuccess');
<% else %> <% else %>
AnalyticsProvider.setAccount('DISABLED'); AnalyticsProvider.setAccount('DISABLED');
<% end %> <% end %>
datepickerPopupConfig.closeText = "Fermer"; // Custom messages for the date-picker widget
datepickerPopupConfig.cleartext = "Effacer"; uibDatepickerPopupConfig.closeText = Fablab.translations.app.shared.buttons.close;
datepickerPopupConfig.currentText = "Aujourd'hui"; uibDatepickerPopupConfig.clearText = Fablab.translations.app.shared.buttons.clear;
uibDatepickerPopupConfig.currentText = Fablab.translations.app.shared.buttons.today;
// custom message for angular-unsavedChanges // Custom messages for angular-unsavedChanges
unsavedWarningsConfigProvider.navigateMessage = "Vous perdrez les modifications non enregistrées si vous quittez cette page"; unsavedWarningsConfigProvider.navigateMessage = Fablab.translations.app.shared.messages.you_will_lose_any_unsaved_modification_if_you_quit_this_page;
unsavedWarningsConfigProvider.reloadMessage = "Vous perdrez les modifications non enregistrées si vous rechargez cette page"; unsavedWarningsConfigProvider.reloadMessage = Fablab.translations.app.shared.messages.you_will_lose_any_unsaved_modification_if_you_reload_this_page;
// Set how long the popup messages (growl) will remain
growlProvider.globalTimeToLive(5000); growlProvider.globalTimeToLive(5000);
growlProvider.globalEnableHtml(true);
$locationProvider.hashPrefix('!'); // Configure the i18n module to load the partial translations from the given API URL
$translateProvider.useLoader('$translatePartialLoader', {
urlTemplate: '/api/translations/{lang}/{part}'
});
// Enable the cache to speed-up the loading times on already seen pages
$translateProvider.useLoaderCache(true);
// Secure i18n module against XSS attacks by escaping the output
$translateProvider.useSanitizeValueStrategy('escapeParameters');
// Enable the MessageFormat interpolation (used for pluralization)
$translateProvider.addInterpolation('$translateMessageFormatInterpolation');
// Set the langage of the instance (from ruby configuration)
$translateProvider.preferredLanguage(Fablab.locale);
}]).run(["$rootScope", "$log", "AuthService", "Auth", "amMoment", "$state", "editableOptions", "$location", "Analytics", function($rootScope, $log, AuthService, Auth, amMoment, $state, editableOptions, $location, Analytics){ }]).run(["$rootScope", "$log", "AuthService", "Auth", "amMoment", "$state", "editableOptions",
function($rootScope, $log, AuthService, Auth, amMoment, $state, editableOptions) {
amMoment.changeLocale('fr'); // Angular-moment (date-time manipulations library)
amMoment.changeLocale(Fablab.moment_locale);
// Angular-xeditable (click-to-edit elements, used in admin backoffice)
editableOptions.theme = 'bs3'; editableOptions.theme = 'bs3';
// Alter the UI-Router's $state, registering into some informations concerning the previous $state.
// This is used to allow the user to navigate to the previous state
$rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams){ $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams){
$state.prevState = fromState; $state.prevState = fromState;
$state.prevParams = fromParams; $state.prevParams = fromParams;
}); });
// Global config: if true, the whole 'Plans & Subscriptions' feature will be disabled in the application
$rootScope.fablabWithoutPlans = Fablab.withoutPlans;
// Global function to allow the user to navigate to the previous screen (ie. $state).
// If no previous $state were recorded, navigate to the home page
$rootScope.backPrevLocation = function(event){ $rootScope.backPrevLocation = function(event){
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -67,8 +92,9 @@ config(['$locationProvider', '$httpProvider', 'AuthProvider', "growlProvider", "
$state.go($state.prevState, $state.prevParams); $state.go($state.prevState, $state.prevParams);
}; };
// Configuration of the summernote editor (used in project edition)
$rootScope.summernoteOpts = { $rootScope.summernoteOpts = {
lang: 'fr-FR', lang: Fablab.summernote_locale,
height: 200, height: 200,
toolbar: [ toolbar: [
['style', ['style']], ['style', ['style']],
@ -85,25 +111,16 @@ config(['$locationProvider', '$httpProvider', 'AuthProvider', "growlProvider", "
maximumImageFileSize: 4096 maximumImageFileSize: 4096
}; };
}]).filter('array', function() { // Prevent the usage of the application for members with incomplete profiles: they will be redirected to
return function(arrayLength) { // the 'profile completion' page. This is especially useful for user's accounts imported through SSO.
if (arrayLength) { $rootScope.$on('$stateChangeStart', function (event, toState) {
arrayLength = Math.ceil(arrayLength); Auth.currentUser().then(function(currentUser) {
var arr = new Array(arrayLength), i = 0; if (currentUser.need_completion && toState.name != 'app.logged.profileCompletion') {
for (; i < arrayLength; i++) { $state.go('app.logged.profileCompletion');
arr[i] = i; }
} });
return arr; });
}
}; }]).constant('angularMomentConfig', {
}).directive('datepickerPopup', function (){ timezone: Fablab.timezone
// fixes https://github.com/angular-ui/bootstrap/issues/2659 });
return {
restrict: 'EAC',
require: 'ngModel',
link: function(scope, element, attr, controller) {
//remove the default formatter from the input directive to prevent conflict
controller.$formatters.shift();
}
}
});

View File

@ -20,7 +20,6 @@
//= require jquery-ui/ui/jquery.ui.droppable //= require jquery-ui/ui/jquery.ui.droppable
//= require jquery-ui/ui/jquery.ui.resizable //= require jquery-ui/ui/jquery.ui.resizable
//= require angular //= require angular
//= require angular-i18n/angular-locale_fr-fr.js
//= require angular-cookies //= require angular-cookies
//= require angular-resource //= require angular-resource
//= require angular-sanitize //= require angular-sanitize
@ -29,18 +28,18 @@
//= require angular-touch //= require angular-touch
//= require angular-ui-router/release/angular-ui-router //= require angular-ui-router/release/angular-ui-router
//= require angular-bootstrap/ui-bootstrap-tpls //= require angular-bootstrap/ui-bootstrap-tpls
//= require select2/select2 //= require angular-ui-select/dist/select
//= require select2/select2_locale_fr
//= require angular-ui-select2/src/select2
//= require moment/moment //= require moment/moment
//= require moment/locale/fr //= require moment-timezone/builds/moment-timezone-with-data-2010-2020
//= require angular-ui-calendar/src/calendar
//= require fullcalendar/dist/fullcalendar
//= require angular-moment/angular-moment //= require angular-moment/angular-moment
//= require ngUpload/ng-upload //= require ngUpload/ng-upload
//= require jasny-bootstrap/js/fileinput //= require jasny-bootstrap/js/fileinput
//= require holderjs/holder //= require holderjs/holder
//= require angular-devise/lib/devise //= require angular-devise/lib/devise
//= require devise-modal //= require devise-modal
//= require angular-growl/build/angular-growl //= require angular-growl-v2/build/angular-growl
//= require angular-xeditable/dist/js/xeditable //= require angular-xeditable/dist/js/xeditable
//= require checklist-model/checklist-model //= require checklist-model/checklist-model
//= require angular-unsavedChanges/src/unsavedChanges //= require angular-unsavedChanges/src/unsavedChanges
@ -50,13 +49,25 @@
//= require dirDisqus //= require dirDisqus
//= require humanize //= require humanize
//= require underscore/underscore //= require underscore/underscore
//= require elasticsearch/elasticsearch.angular
//= require d3/d3
//= require nvd3/build/nv.d3.js
//= require app //= require app
//= require router //= require router
//= require medium-editor/dist/js/medium-editor
//= require angular-medium-editor/dist/angular-medium-editor
//= require bootstrap-switch/dist/js/bootstrap-switch.min
//= require angular-bootstrap-switch/dist/angular-bootstrap-switch.min
//= require angular-base64-upload/dist/angular-base64-upload.min
//= require summernote/dist/summernote
//= require angular-summernote/dist/angular-summernote
//= require jquery-minicolors/jquery.minicolors.js
//= require angular-minicolors/angular-minicolors.js
//= require angular-translate/angular-translate
//= require angular-translate-loader-partial/angular-translate-loader-partial
//= require messageformat/messageformat
//= require angular-translate-interpolation-messageformat/angular-translate-interpolation-messageformat
//= require_tree ./controllers //= require_tree ./controllers
//= require_tree ./services //= require_tree ./services
//= require_tree ./directives //= require_tree ./directives
//= require_tree ./filters //= require_tree ./filters
//= require summernote/dist/summernote
//= require summernote/lang/summernote-fr-FR
//= require summernote/plugin/summernote-ext-video
//= require angular-summernote/dist/angular-summernote

View File

@ -0,0 +1,19 @@
'use strict'
Application.Controllers.controller "AboutController", ['$scope', 'Setting', 'CustomAsset', ($scope, Setting, CustomAsset)->
### PUBLIC SCOPE ###
Setting.get { name: 'about_title'}, (data)->
$scope.aboutTitle = data.setting
Setting.get { name: 'about_body'}, (data)->
$scope.aboutBody = data.setting
Setting.get { name: 'about_contacts'}, (data)->
$scope.aboutContacts = data.setting
# retrieve the CGU
CustomAsset.get {name: 'cgu-file'}, (cgu) ->
$scope.cgu = cgu.custom_asset
]

View File

@ -0,0 +1,221 @@
'use strict'
### COMMON CODE ###
## list of supported authentication methods
METHODS = {
'DatabaseProvider' : 'Base de données locale',
#'OAuthProvider' : 'OAuth 1.0',
'OAuth2Provider' : 'OAuth 2.0',
#'LdapProvider' : 'LDAP'
}
##
# Iterate through the provided array and return the index of the requested element
# @param elements {Array} array of objects with property 'id'
# @param id {Number} id of the element to retrieve in the list
# @returns {Number} index of the requested element, in the provided array
##
findIdxById = (elements, id)->
(elements.map (elem)->
elem.id
).indexOf(id)
##
# For OAuth2 ententications, mapping the user's ID is mendatory. This function will check that this mapping
# is effective and will return false otherwise
# @param mappings {Array<Object>} expected: $scope.provider.providable_attributes.o_auth2_mappings_attributes
# @returns {Boolean} true if the mapping is declared
##
check_oauth2_id_is_mapped = (mappings) ->
for mapping in mappings
if mapping.local_model == 'user' and mapping.local_field == 'uid' and not mapping._destroy
return true
return false
##
# Page listing all authentication providers
##
Application.Controllers.controller "AuthentificationController", ["$scope", "$state", "$rootScope", "dialogs", "growl", "authProvidersPromise", 'AuthProvider', '_t'
, ($scope, $state, $rootScope, dialogs, growl, authProvidersPromise, AuthProvider, _t) ->
### PUBLIC SCOPE ###
## full list of authentication providers
$scope.providers = authProvidersPromise
##
# Translate the classname into an explicit textual message
# @param type {string} Ruby polymorphic model classname
# @returns {string}
##
$scope.getType = (type) ->
text = METHODS[type]
if typeof text != 'undefined'
return text
else
return _t('unknown')+type
##
# Translate the status string into an explicit textual message
# @param status {string} active | pending | previous
# @returns {string}
##
$scope.getState = (status) ->
switch status
when 'active' then _t('active')
when 'pending' then _t('pending')
when 'previous' then _t('previous_provider')
else _t('unknown')+status
##
# Ask for confirmation then delete the specified provider
# @param providers {Array} full list of authentication providers
# @param provider {Object} provider to delete
##
$scope.destroyProvider = (providers, provider) ->
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_delete_the_TYPE_authentication_provider_NAME', {TYPE:$scope.getType(provider.providable_type), NAME:provider.name})
, ->
# the admin has confirmed, delete
AuthProvider.delete id: provider.id
, ->
providers.splice(findIdxById(providers, provider.id), 1)
growl.success(_t('authentication_provider_successfully_deleted'))
, ->
growl.error(_t('an_error_occurred_unable_to_delete_the_specified_provider'))
]
##
# Page to add a new authentication provider
##
Application.Controllers.controller "NewAuthenticationController", ["$scope", "$state", "$rootScope", "dialogs", "growl", "mappingFieldsPromise", "authProvidersPromise", "AuthProvider", '_t'
, ($scope, $state, $rootScope, dialogs, growl, mappingFieldsPromise, authProvidersPromise, AuthProvider, _t) ->
$scope.authMethods = METHODS
$scope.mappingFields = mappingFieldsPromise
$scope.mode = 'creation'
$scope.provider = {
name: '',
providable_type: '',
providable_attributes: {}
}
##
# Initialize some provider's specific properties when selecting the provider type
##
$scope.updateProvidable = ->
# === OAuth2Provider ===
if $scope.provider.providable_type == 'OAuth2Provider'
if typeof $scope.provider.providable_attributes.o_auth2_mappings_attributes == 'undefined'
$scope.provider.providable_attributes['o_auth2_mappings_attributes'] = []
# Add others providers initializers here if needed ...
##
# Validate and save the provider parameters in database
##
$scope.registerProvider = ->
# === DatabaseProvider ===
if $scope.provider.providable_type == 'DatabaseProvider'
# prevent from adding mode than 1
for provider in authProvidersPromise
if provider.providable_type == 'DatabaseProvider'
growl.error _t('a_local_database_provider_already_exists_unable_to_create_another')
return false
AuthProvider.save auth_provider: $scope.provider, (provider) ->
growl.success _t('local_provider_successfully_saved')
$state.go('app.admin.members')
# === OAuth2Provider ===
else if $scope.provider.providable_type == 'OAuth2Provider'
# check the ID mapping
unless check_oauth2_id_is_mapped($scope.provider.providable_attributes.o_auth2_mappings_attributes)
growl.error(_t('it_is_required_to_set_the_matching_between_User.uid_and_the_API_to_add_this_provider'))
return false
# discourage the use of unsecure SSO
unless $scope.provider.providable_attributes.base_url.indexOf('https://') > -1
dialogs.confirm
size: 'l'
resolve:
object: ->
title: _t('security_issue_detected')
msg: _t('beware_the_oauth2_authenticatoin_provider_you_are_about_to_add_isnt_using_HTTPS') +
_t('this_is_a_serious_security_issue_on_internet_and_should_never_be_used_except_for_testing_purposes') +
_t('do_you_really_want_to_continue')
, -> # unsecured http confirmed
AuthProvider.save auth_provider: $scope.provider, (provider) ->
growl.success _t('unsecured_oauth2_provider_successfully_added')
$state.go('app.admin.members')
else
AuthProvider.save auth_provider: $scope.provider, (provider) ->
growl.success _t('oauth2_provider_successfully_added')
$state.go('app.admin.members')
##
# Changes the admin's view to the members list page
##
$scope.cancel = ->
$state.go('app.admin.members')
]
##
# Page to edit an already added authentication provider
##
Application.Controllers.controller "EditAuthenticationController", ["$scope", "$state", "$stateParams", "$rootScope", "dialogs", "growl", 'providerPromise', 'mappingFieldsPromise', 'AuthProvider', '_t'
, ($scope, $state, $stateParams, $rootScope, dialogs, growl, providerPromise, mappingFieldsPromise, AuthProvider, _t) ->
$scope.provider = providerPromise
$scope.authMethods = METHODS
$scope.mode = 'edition'
$scope.mappingFields = mappingFieldsPromise
##
# Update the current provider with the new inputs
##
$scope.updateProvider = ->
# check the ID mapping
unless check_oauth2_id_is_mapped($scope.provider.providable_attributes.o_auth2_mappings_attributes)
growl.error(_t('it_is_required_to_set_the_matching_between_User.uid_and_the_API_to_add_this_provider'))
return false
AuthProvider.update {id: $scope.provider.id}, {auth_provider: $scope.provider}, (provider) ->
growl.success(_t('provider_successfully_updated'))
$state.go('app.admin.members')
, ->
growl.error(_t('an_error_occurred_unable_to_update_the_provider'))
##
# Changes the admin's view to the members list page
##
$scope.cancel = ->
$state.go('app.admin.members')
]

View File

@ -0,0 +1,403 @@
'use strict'
##
# Controller used in the calendar management page
##
Application.Controllers.controller "AdminCalendarController", ["$scope", "$state", "$uibModal", "moment", "Availability", 'Slot', 'Setting', 'growl', 'dialogs', 'availabilitiesPromise', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', '_t'
($scope, $state, $uibModal, moment, Availability, Slot, Setting, growl, dialogs, availabilitiesPromise, bookingWindowStart, bookingWindowEnd, machinesPromise, _t) ->
### PRIVATE STATIC CONSTANTS ###
# The calendar is divided in slots of 30 minutes
BASE_SLOT = '00:30:00'
# The bookings can be positioned every half hours
BOOKING_SNAP = '00:30:00'
# The calendar will be initialized positioned under 9:00 AM
DEFAULT_CALENDAR_POSITION = '09:00:00'
# We do not allow the creation of slots that are not a multiple of 60 minutes
SLOT_MULTIPLE = 60
### PUBLIC SCOPE ###
## list of the FabLab machines
$scope.machines = machinesPromise
## currently selected availability
$scope.availability = null
## bind the availabilities slots with full-Calendar events
$scope.eventSources = []
$scope.eventSources.push
events: availabilitiesPromise
textColor: 'black'
## after fullCalendar loads, provides access to its methods through $scope.calendar.fullCalendar()
$scope.calendar = null
## fullCalendar (v2) configuration
$scope.calendarConfig =
timezone: Fablab.timezone
lang: Fablab.fullcalendar_locale
header:
left: 'month agendaWeek'
center: 'title'
right: 'today prev,next'
firstDay: 1 # Week start on monday (France)
scrollTime: DEFAULT_CALENDAR_POSITION
slotDuration: BASE_SLOT
snapDuration: BOOKING_SNAP
allDayDefault: false
minTime: "00:00:00"
maxTime: "24:00:00"
height: 'auto'
buttonIcons:
prev: 'left-single-arrow'
next: 'right-single-arrow'
timeFormat:
agenda:'H:mm'
month: 'H(:mm)'
axisFormat: 'H:mm'
allDaySlot: false
defaultView: 'agendaWeek'
selectable: true
selecHelper: true
select: (start, end, jsEvent, view) ->
calendarSelectCb(start, end, jsEvent, view)
eventClick: (event, jsEvent, view)->
calendarEventClickCb(event, jsEvent, view)
eventRender: (event, element, view) ->
eventRenderCb(event, element)
## fullCalendar time bounds (up & down)
$scope.calendarConfig.minTime = moment.duration(moment(bookingWindowStart.setting.value).format('HH:mm:ss'))
$scope.calendarConfig.maxTime = moment.duration(moment(bookingWindowEnd.setting.value).format('HH:mm:ss'))
##
# Open a confirmation modal to cancel the booking of a user for the currently selected event.
# @param slot {Object} reservation slot of a user, inherited from $resource
##
$scope.cancelBooking = (slot) ->
# open a confirmation dialog
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t("do_you_really_want_to_cancel_the_USER_s_reservation_the_DATE_at_TIME_concerning_RESERVATION"
, { GENDER:getGender($scope.currentUser), USER:slot.user.name, DATE:moment(slot.start_at).format('L'), TIME:moment(slot.start_at).format('LT'), RESERVATION:slot.reservable.name }
, 'messageformat')
, ->
# the admin has confirmed, cancel the subscription
Slot.cancel {id: slot.slot_id}
, (data, status) -> # success
# update the canceled_at attribute
for resa in $scope.reservations
if resa.slot_id == data.id
resa.canceled_at = data.canceled_at
break
# notify the admin
growl.success(_t('reservation_was_successfully_cancelled'))
, (data, status) -> # failed
growl.error(_t('reservation_cancellation_failed'))
##
# Open a confirmation modal to remove a machine for the currently selected availability,
# except if it is the last machine of the reservation.
# @param machine {Object} must contain the machine ID and name
##
$scope.removeMachine = (machine) ->
if $scope.availability.machine_ids.length == 1
growl.error(_t('unable_to_remove_the_last_machine_of_the_slot_delete_the_slot_rather'))
else
# open a confirmation dialog
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_remove_MACHINE_from_this_slot', {GENDER:getGender($scope.currentUser), MACHINE:machine.name}, "messageformat") + ' ' +
_t('this_will_prevent_any_new_reservation_on_this_slot_but_wont_cancel_those_existing') + ' ' +
_t('beware_this_cannot_be_reverted')
, ->
# the admin has confirmed, remove the machine
machines = $scope.availability.machine_ids
for key, m_id in machines
if m_id == machine.id
machines.splice(key, 1)
Availability.update {id: $scope.availability.id}, {availability: {machines_attributes: [{id: machine.id, _destroy: true}]}}
, (data, status) -> # success
# update the machine_ids attribute
$scope.availability.machine_ids = data.machine_ids
$scope.availability.title = data.title
$scope.calendar.fullCalendar 'rerenderEvents'
# notify the admin
growl.success(_t('the_machine_was_successfully_removed_from_the_slot'))
, (data, status) -> # failed
growl.error(_t('deletion_failed'))
### PRIVATE SCOPE ###
##
# Return an enumerable meaninful string for the gender of the provider user
# @param user {Object} Database user record
# @return {string} 'male' or 'female'
##
getGender = (user) ->
if user.profile
if user.profile.gender == "true" then 'male' else 'female'
else 'other'
# Triggered when the admin drag on the agenda to create a new reservable slot.
# @see http://fullcalendar.io/docs/selection/select_callback/
##
calendarSelectCb = (start, end, jsEvent, view) ->
start = moment.tz(start.toISOString(), Fablab.timezone)
end = moment.tz(end.toISOString(), Fablab.timezone)
# first we check that the selected slot is an N-hours multiple (ie. not decimal)
if Number.isInteger(parseInt((end.valueOf() - start.valueOf()) / (SLOT_MULTIPLE * 1000), 10)/SLOT_MULTIPLE)
today = new Date()
if (parseInt((start.valueOf() - today) / (60 * 1000), 10) >= 0)
# then we open a modal window to let the admin specify the slot type
modalInstance = $uibModal.open
templateUrl: '<%= asset_path "admin/calendar/eventModal.html" %>'
controller: 'CreateEventModalController'
resolve:
start: -> start
end: -> end
# when the modal is closed, we send the slot to the server for saving
modalInstance.result.then (availability) ->
$scope.calendar.fullCalendar 'renderEvent',
id: availability.id
title: availability.title,
start: availability.start_at
end: availability.end_at
textColor: 'black'
backgroundColor: availability.backgroundColor
borderColor: availability.borderColor
tag_ids: availability.tag_ids
machine_ids: availability.machine_ids
, true
, ->
$scope.calendar.fullCalendar('unselect')
$scope.calendar.fullCalendar('unselect')
##
# Triggered when the admin clicks on a availability slot in the agenda.
# @see http://fullcalendar.io/docs/mouse/eventClick/
##
calendarEventClickCb = (event, jsEvent, view) ->
$scope.availability = event
# if the user has clicked on the delete event button, delete the event
if ($(jsEvent.target).hasClass('remove-event'))
Availability.delete id: event.id, ->
$scope.calendar.fullCalendar 'removeEvents', event.id
for _event, i in $scope.eventSources[0].events
if _event.id == event.id
$scope.eventSources[0].events.splice(i,1)
growl.success(_t('the_slot_START-END_has_been_successfully_deleted', {START:+moment(event.start).format('LL LT'), END:moment(event.end).format('LT')}))
,->
growl.error(_t('unable_to_delete_the_slot_START-END_because_it_s_already_reserved_by_a_member', {START:+moment(event.start).format('LL LT'), END:moment(event.end).format('LT')}))
# if the user has only clicked on the event, display its reservations
else
Availability.reservations {id: event.id}, (reservations) ->
$scope.reservations = reservations
##
# Triggered when fullCalendar tries to graphicaly render an event block.
# Append the event tag into the block, just after the event title.
# @see http://fullcalendar.io/docs/event_rendering/eventRender/
##
eventRenderCb = (event, element) ->
if event.tag_ids.length > 0
Availability.get {id: event.id}, (avail) ->
html = ''
for tag in avail.tags
html += "<span class='label label-success text-white'>#{tag.name}</span> "
element.find('.fc-title').append("<br/>"+html)
]
##
# Controller used in the slot creation modal window
##
Application.Controllers.controller 'CreateEventModalController', ["$scope", "$uibModalInstance", "moment", "start", "end", "Machine", "Availability", "Training", 'Tag', 'growl', '_t', ($scope, $uibModalInstance, moment, start, end, Machine, Availability, Training, Tag, growl, _t) ->
## $uibModal parameter
$scope.start = start
## $uibModal parameter
$scope.end = end
## machines list
$scope.machines = []
## trainings list
$scope.trainings = []
## machines associated with the created slot
$scope.selectedMachines = []
## the user is not able to edit the ending time of the availability, unless he set the type to 'training'
$scope.endDateReadOnly = true
## timepickers configuration
$scope.timepickers =
start:
hstep: 1
mstep: 5
end:
hstep: 1
mstep: 5
## slot details
$scope.availability =
start_at: start
end_at: end
available_type: 'machines' # default
##
# Adds or removes the provided machine from the current slot
# @param machine {Object}
##
$scope.toggleSelection = (machine)->
index = $scope.selectedMachines.indexOf(machine)
if index > -1
$scope.selectedMachines.splice(index, 1)
else
$scope.selectedMachines.push(machine)
##
# Callback for the modal window validation: save the slot and closes the modal
##
$scope.ok = ->
if $scope.availability.available_type == "machines"
if $scope.selectedMachines.length > 0
$scope.availability.machine_ids = $scope.selectedMachines.map (m) -> m.id
else
growl.error(_t('you_should_link_a_training_or_a_machine_to_this_slot'))
return
else
$scope.availability.training_ids = [$scope.selectedTraining.id]
Availability.save
availability: $scope.availability
, (availability) ->
$uibModalInstance.close(availability)
##
# Callback to cancel the slot creation
##
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
##
# Switches the slot type : machine availability or training availability
##
$scope.changeAvailableType = ->
if $scope.availability.available_type == "machines"
$scope.availability.available_type = "training"
else
$scope.availability.available_type = "machines"
##
# For training avaiabilities, set the maximum number of people allowed to register on this slot
##
$scope.setNbTotalPlaces = ->
$scope.availability.nb_total_places = $scope.selectedTraining.nb_total_places
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
Machine.query().$promise.then (data)->
$scope.machines = data.map (d) ->
id: d.id
name: d.name
Training.query().$promise.then (data)->
$scope.trainings = data.map (d) ->
id: d.id
name: d.name
nb_total_places: d.nb_total_places
if $scope.trainings.length > 0
$scope.selectedTraining = $scope.trainings[0]
$scope.setNbTotalPlaces()
Tag.query().$promise.then (data) ->
$scope.tags = data
## When we configure a machine availability, do not let the user change the end time, as the total
## time must be dividable by 60 minutes (base slot duration). For training availabilities, the user
## can configure any duration as it does not matters.
$scope.$watch 'availability.available_type', (newValue, oldValue, scope) ->
if newValue == 'machines'
$scope.endDateReadOnly = true
diff = moment($scope.end).diff($scope.start, 'hours') # the result is rounded down by moment.js
$scope.end = moment($scope.start).add(diff, 'hours').toDate()
$scope.availability.end_at = $scope.end
else
$scope.endDateReadOnly = false
## When the start date is changed, if we are configuring a machine availability,
## maintain the relative length of the slot (ie. change the end time accordingly)
$scope.$watch 'start', (newValue, oldValue, scope) ->
# for machine availabilities, adjust the end time
if $scope.availability.available_type == 'machines'
end = moment($scope.end)
end.add(moment(newValue).diff(oldValue), 'milliseconds')
$scope.end = end.toDate()
else # for training availabilities
# prevent the admin from setting the begining after the and
if moment(newValue).add(1, 'hour').isAfter($scope.end)
$scope.start = oldValue
# update availability object
$scope.availability.start_at = $scope.start
## Maintain consistency between the end time and the date object in the availability object
$scope.$watch 'end', (newValue, oldValue, scope) ->
## we prevent the admin from setting the end of the availability before its begining
if moment($scope.start).add(1, 'hour').isAfter(newValue)
$scope.end = oldValue
# update availability object
$scope.availability.end_at = $scope.end
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]

View File

@ -14,8 +14,8 @@
# - $scope.addFile() # - $scope.addFile()
# - $scope.deleteFile(file) # - $scope.deleteFile(file)
# - $scope.fileinputClass(v) # - $scope.fileinputClass(v)
# - $scope.openStartDatePicker($event) # - $scope.toggleStartDatePicker($event)
# - $scope.openEndDatePicker($event) # - $scope.toggleEndDatePicker($event)
# - $scope.toggleRecurrenceEnd(e) # - $scope.toggleRecurrenceEnd(e)
# #
# Requires : # Requires :
@ -23,7 +23,7 @@
# - $state (Ui-Router) [ 'app.public.events_list' ] # - $state (Ui-Router) [ 'app.public.events_list' ]
## ##
class EventsController class EventsController
constructor: ($scope, $state, Event, Category) -> constructor: ($scope, $state, $locale, Event, Category) ->
## Retrieve the list of categories from the server (stage, atelier, ...) ## Retrieve the list of categories from the server (stage, atelier, ...)
Category.query().$promise.then (data)-> Category.query().$promise.then (data)->
@ -33,12 +33,12 @@ class EventsController
## default parameters for AngularUI-Bootstrap datepicker ## default parameters for AngularUI-Bootstrap datepicker
$scope.datePicker = $scope.datePicker =
format: 'dd/MM/yyyy' format: $locale.DATETIME_FORMATS.shortDate
startOpened: false # default: datePicker is not shown startOpened: false # default: datePicker is not shown
endOpened: false endOpened: false
recurrenceEndOpened: false recurrenceEndOpened: false
options: options:
startingDay: 1 # France: the week starts on monday startingDay: Fablab.weekStartingDay
@ -136,21 +136,20 @@ class EventsController
## ##
# Controller used in the events listing page (admin view) # Controller used in the events listing page (admin view)
## ##
Application.Controllers.controller "adminEventsController", ["$scope", "$state", 'Event', ($scope, $state, Event) -> Application.Controllers.controller "AdminEventsController", ["$scope", "$state", 'Event', 'eventsPromise', ($scope, $state, Event, eventsPromise) ->
### PUBLIC SCOPE ### ### PUBLIC SCOPE ###
## The events displayed on the page
$scope.events = []
## By default, the pagination mode is activated to limit the page size ## By default, the pagination mode is activated to limit the page size
$scope.paginateActive = true $scope.paginateActive = true
## The currently displayed page number ## The events displayed on the page
$scope.page = 1 $scope.events = eventsPromise
## Current virtual page
$scope.page = 2
## ##
# Adds a bucket of events to the bottom of the page, grouped by month # Adds a bucket of events to the bottom of the page, grouped by month
@ -158,10 +157,7 @@ Application.Controllers.controller "adminEventsController", ["$scope", "$state",
$scope.loadMoreEvents = -> $scope.loadMoreEvents = ->
Event.query {page: $scope.page}, (data)-> Event.query {page: $scope.page}, (data)->
$scope.events = $scope.events.concat data $scope.events = $scope.events.concat data
if data.length paginationCheck(data, $scope.events)
$scope.paginateActive = false if $scope.events.length >= data[0].nb_total_events
else
$scope.paginateActive = false
$scope.page += 1 $scope.page += 1
@ -172,10 +168,40 @@ Application.Controllers.controller "adminEventsController", ["$scope", "$state",
# Kind of constructor: these actions will be realized first when the controller is loaded # Kind of constructor: these actions will be realized first when the controller is loaded
## ##
initialize = -> initialize = ->
$scope.loadMoreEvents() paginationCheck(eventsPromise, $scope.events)
## !!! MUST BE CALLED AT THE END of the controller
##
# Check if all events are already displayed OR if the button 'load more events'
# is required
# @param lastEvents {Array} last events loaded onto the diplay (ie. last "page")
# @param events {Array} full list of events displayed on the page (not only the last retrieved)
##
paginationCheck = (lastEvents, events)->
if lastEvents.length > 0
$scope.paginateActive = false if events.length >= lastEvents[0].nb_total_events
else
$scope.paginateActive = false
# init the controller (call at the end !)
initialize() initialize()
]
##
# Controller used in the reservations listing page for a specific event
##
Application.Controllers.controller "ShowEventReservationsController", ["$scope", 'eventPromise', 'reservationsPromise', ($scope, eventPromise, reservationsPromise) ->
## retrieve the event from the ID provided in the current URL
$scope.event = eventPromise
## list of reservations for the current event
$scope.reservations = reservationsPromise
] ]
@ -183,7 +209,8 @@ Application.Controllers.controller "adminEventsController", ["$scope", "$state",
## ##
# Controller used in the event creation page # Controller used in the event creation page
## ##
Application.Controllers.controller "newEventController", ["$scope", "$state", 'Event', 'Category', 'CSRF', ($scope, $state, Event, Category, CSRF) -> Application.Controllers.controller "NewEventController", ["$scope", "$state", "$locale", 'Event', 'Category', 'CSRF', '_t'
, ($scope, $state, $locale, Event, Category, CSRF, _t) ->
CSRF.setMetaTags() CSRF.setMetaTags()
## API URL where the form will be posted ## API URL where the form will be posted
@ -204,15 +231,18 @@ Application.Controllers.controller "newEventController", ["$scope", "$state", 'E
## Possible types of recurrences for an event ## Possible types of recurrences for an event
$scope.recurrenceTypes = [ $scope.recurrenceTypes = [
{label: 'Aucune', value: 'none'}, {label: _t('none'), value: 'none'},
{label: 'Tous les jours', value: 'day'}, {label: _t('every_days'), value: 'day'},
{label: 'Chaque semaine', value: 'week'}, {label: _t('every_week'), value: 'week'},
{label: 'Chaque mois', value: 'month'}, {label: _t('every_month'), value: 'month'},
{label: 'Chaque année', value: 'year'} {label: _t('every_year'), value: 'year'}
] ]
## currency symbol for the current locale (cf. angular-i18n)
$scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM;
## Using the EventsController ## Using the EventsController
new EventsController($scope, $state, Event, Category) new EventsController($scope, $state, $locale, Event, Category)
] ]
@ -220,8 +250,12 @@ Application.Controllers.controller "newEventController", ["$scope", "$state", 'E
## ##
# Controller used in the events edition page # Controller used in the events edition page
## ##
Application.Controllers.controller "editEventController", ["$scope", "$state", "$stateParams", 'Event', 'Category', 'CSRF', ($scope, $state, $stateParams, Event, Category, CSRF) -> Application.Controllers.controller "EditEventController", ["$scope", "$state", "$stateParams", "$locale", 'Event', 'Category', 'CSRF', 'eventPromise'
CSRF.setMetaTags() , ($scope, $state, $stateParams, $locale, Event, Category, CSRF, eventPromise) ->
### PUBLIC SCOPE ###
## API URL where the form will be posted ## API URL where the form will be posted
$scope.actionUrl = "/api/events/" + $stateParams.id $scope.actionUrl = "/api/events/" + $stateParams.id
@ -230,13 +264,32 @@ Application.Controllers.controller "editEventController", ["$scope", "$state", "
$scope.method = 'put' $scope.method = 'put'
## Retrieve the event details, in case of error the user is redirected to the events listing ## Retrieve the event details, in case of error the user is redirected to the events listing
Event.get {id: $stateParams.id} $scope.event = eventPromise
, (event)->
$scope.event = event
return
, ->
$state.go('app.public.events_list')
## Using the EventsController ## currency symbol for the current locale (cf. angular-i18n)
new EventsController($scope, $state, Event, Category) $scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM;
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
CSRF.setMetaTags()
# init the dates to JS objects
$scope.event.start_date = moment($scope.event.start_date).toDate()
$scope.event.end_date = moment($scope.event.end_date).toDate()
## Using the EventsController
new EventsController($scope, $state, $locale, Event, Category)
## !!! MUST BE CALLED AT THE END of the controller
initialize()
] ]

View File

@ -0,0 +1,648 @@
'use strict'
Application.Controllers.controller "GraphsController", ["$scope", "$state", '$locale', "$rootScope", 'es', 'Statistics', '_t'
, ($scope, $state, $locale, $rootScope, es, Statistics, _t) ->
### PRIVATE STATIC CONSTANTS ###
## height of the HTML/SVG charts elements in pixels
CHART_HEIGHT = 500
## Label of the charts' horizontal axes
X_AXIS_LABEL = _t('date')
## Label of the charts' vertical axes
Y_AXIS_LABEL = _t('number')
## Colors for the line charts. Each new line uses the next color in this array
CHART_COLORS = ['#b35a94', '#1c5794', '#00b49e', '#6fac48', '#ebcf4a', '#fd7e33', '#ca3436', '#a26e3a']
### PUBLIC SCOPE ###
## ui-view transitions optimization: if true, the charts will never be refreshed
$scope.preventRefresh = false
## statistics structure in elasticSearch
$scope.statistics = []
## statistics data recovered from elasticSearch
$scope.data = null
## default interval: one day
$scope.display =
interval: 'week'
## active tab will be set here
$scope.selectedIndex = null
## for palmares graphs, filters values are stored here
$scope.ranking =
sortCriterion: 'ca'
groupCriterion: 'subType'
## default: we do not open the datepicker menu
$scope.datePicker =
show: false
## datePicker parameters for interval beginning
$scope.datePickerStart =
format: $locale.DATETIME_FORMATS.shortDate
opened: false # default: datePicker is not shown
minDate: null
maxDate: moment().subtract(1, 'day').toDate()
selected: moment().utc().subtract(1, 'months').subtract(1, 'day').startOf('day').toDate()
options:
startingDay: Fablab.weekStartingDay
## datePicker parameters for interval ending
$scope.datePickerEnd =
format: $locale.DATETIME_FORMATS.shortDate
opened: false # default: datePicker is not shown
minDate: null
maxDate: moment().subtract(1, 'day').toDate()
selected: moment().subtract(1, 'day').endOf('day').toDate()
options:
startingDay: Fablab.weekStartingDay
##
# Callback to open the datepicker (interval start)
# @param {Object} jQuery event object
##
$scope.toggleStartDatePicker = ($event) ->
toggleDatePicker($event, $scope.datePickerStart)
##
# Callback to open the datepicker (interval end)
# @param {Object} jQuery event object
##
$scope.toggleEndDatePicker = ($event) ->
toggleDatePicker($event, $scope.datePickerEnd)
##
# Callback called when the active tab is changed.
# Recover the current tab and store its value in $scope.selectedIndex
# @param tab {Object} elasticsearch statistic structure
##
$scope.setActiveTab = (tab) ->
$scope.selectedIndex = tab
$scope.ranking.groupCriterion = 'subType'
if tab.ca
$scope.ranking.sortCriterion = 'ca'
else
$scope.ranking.sortCriterion = tab.types[0].key
refreshChart()
##
# Callback to close the date-picking popup and refresh the results
##
$scope.validateDateChange = ->
$scope.datePicker.show = false
refreshChart()
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
Statistics.query (stats) ->
$scope.statistics = stats
# watch the interval changes to refresh the graph
$scope.$watch (scope) ->
return scope.display.interval
, (newValue, oldValue) ->
refreshChart()
$scope.$watch (scope) ->
return scope.ranking.sortCriterion
, (newValue, oldValue) ->
refreshChart()
$scope.$watch (scope) ->
return scope.ranking.groupCriterion
, (newValue, oldValue) ->
refreshChart()
refreshChart()
# workaround for angular-bootstrap::tabs behavior: on tab deletion, another tab will be selected
# which will cause every tabs to reload, one by one, when the view is closed
$rootScope.$on '$stateChangeStart', (event, toState, toParams, fromState, fromParams) ->
if fromState.name == 'app.admin.stats_graphs' and Object.keys(fromParams).length == 0
$scope.preventRefresh = true
##
# Generic function to toggle a bootstrap datePicker
# @param $event {Object} jQuery event object
# @param datePicker {Object} settings object of the concerned datepicker. Must have an 'opened' property
##
toggleDatePicker = ($event, datePicker) ->
$event.preventDefault()
$event.stopPropagation()
datePicker.opened = !datePicker.opened
##
# Query elasticSearch according to the current parameters and update the chart
##
refreshChart = ->
if $scope.selectedIndex and !$scope.preventRefresh
query $scope.selectedIndex, (aggregations, error)->
if error
console.error(error)
else
if $scope.selectedIndex.graph.chart_type != 'discreteBarChart'
$scope.data = formatAggregations(aggregations)
angular.forEach $scope.data, (datum, key) ->
updateChart($scope.selectedIndex.graph.chart_type, datum, key)
else
$scope.data = formatRankingAggregations(aggregations, $scope.selectedIndex.graph.limit, $scope.ranking.groupCriterion)
updateChart($scope.selectedIndex.graph.chart_type, $scope.data.ranking, $scope.selectedIndex.es_type_key)
##
# Callback used in NVD3 to print timestamps as literal dates on the X axis
##
xAxisTickFormatFunction = (d, x, y) ->
### WARNING !! These tests (typeof/instanceof) may become broken on nvd3 update ###
if $scope.display.interval == 'day'
if typeof d == 'number' or d instanceof Date
d3.time.format(Fablab.d3DateFormat) moment(d).toDate()
else # typeof d == 'string'
d
else if $scope.display.interval == 'week'
if typeof x == 'number' or d instanceof Date
d3.time.format(_t('week_short')+' %U') moment(d).toDate()
else if typeof d == 'number'
_t('week_of_START_to_END', {START:moment(d).format('L'), END:moment(d).add(6, 'days').format('L')})
else # typeof d == 'string'
d
else if $scope.display.interval == 'month'
if typeof d == 'number'
label = moment(d).format('MMMM YYYY')
label.substr(0,1).toUpperCase()+label.substr(1).toLowerCase()
else # typeof d == 'string'
d
##
# Format aggregations as retuned by elasticSearch to an understandable format for NVD3
# @param aggs {Object} as returned by elasticsearch
##
formatAggregations = (aggs) ->
format = {}
angular.forEach aggs, (type, type_key) -> # go through aggs[$TYPE] where $TYPE = month|year|hour|booking|...
format[type_key] = []
if type.subgroups
angular.forEach type.subgroups.buckets, (subgroup) -> # go through aggs.$TYPE.subgroups.buckets where each bucket represent a $SUBTYPE
angular.forEach $scope.selectedIndex.types, (cur_type) -> # in the mean time, go through the types of the current index (active tab) ...
if cur_type.key == type_key # ... looking for the type matching $TYPE
for it_st in [0.. cur_type.subtypes.length-1] by 1 # when we've found it, iterate over its subtypes ...
cur_subtype = cur_type.subtypes[it_st]
if subgroup.key == cur_subtype.key # ... which match $SUBTYPE
# then we construct NVD3 dataSource according to these informations
dataSource =
values: []
key: cur_subtype.label
total : 0
color: CHART_COLORS[it_st]
area: true
# finally, we iterate over 'intervals' buckets witch contains
# per date aggregations for our current dataSource
angular.forEach subgroup.intervals.buckets, (interval) ->
dataSource.values.push
x: interval.key
y: interval.total.value
dataSource.total += parseInt(interval.total.value)
dataSource.key += ' (' + dataSource.total + ')'
format[type_key].push dataSource
format
##
# Format aggregations for ranking charts to an understandable format for NVD3
# @param aggs {Object} as returned by elasticsearch
# @param limit {number} limit the number of stats in the bar chart
# @param typeKey {String} field name witch results are grouped by
##
formatRankingAggregations = (aggs, limit, typeKey) ->
format =
ranking: []
it = 0
while (it < aggs.subgroups.buckets.length)
bucket = aggs.subgroups.buckets[it]
dataSource =
values: []
key: getRankingLabel(bucket.key, typeKey)
color: CHART_COLORS[it]
area: true
dataSource.values.push
x: getRankingLabel(bucket.key, typeKey)
y: bucket.total.value
format.ranking.push(dataSource)
it++
getY = (object)->
object.values[0].y
format.ranking = stableSort(format.ranking, 'DESC', getY).slice(0, limit)
for i in [0..format.ranking.length] by 1
if typeof format.ranking[i] == 'undefined' then format.ranking.splice(i,1)
format
##
# For BarCharts, return the label for a given bar
# @param key {string} raw value of the label
# @param typeKey {string} name of the field the results are grouped by
##
getRankingLabel = (key, typeKey) ->
if $scope.selectedIndex
if (typeKey == 'subType')
for type in $scope.selectedIndex.types
for subtype in type.subtypes
if (subtype.key == key)
return subtype.label
else
for field in $scope.selectedIndex.additional_fields
if (field.key == typeKey)
switch field.data_type
when 'date' then return moment(key).format('LL')
when 'list' then return key.name
else return key
##
# Prepare the elasticSearch query for the stats matching the current controller's parameters
# @param index {{id:{number}, es_type_key:{string}, label:{string}, table:{boolean}, additional_fields:{Array},
# types:{Array}, graph:{Object}}} elasticSearch type in stats index to query
# @param callback {function} function be to run after results were retrieved,
# it will receive two parameters : results {Array}, error {String} (if any)
##
query = (index, callback) ->
# invalid callback handeling
if typeof(callback) != "function"
console.error('[graphsController::query] Error: invalid callback provided')
return
if !index
callback([], '[graphsController::query] Error: invalid index provided')
return
if index.graph.chart_type != 'discreteBarChart'
# list statistics types
stat_types = []
for t in index.types
if t.graph
stat_types.push(t.key)
# exception handeling
if stat_types.length == 0
callback([], "Error: Unable to retrieve any graphical statistic types in the provided index")
type_it = 0
results = {}
error = ''
recursiveCb = ->
if type_it < stat_types.length
queryElasticStats index.es_type_key, stat_types[type_it], (prevResults, prevError)->
if (prevError)
console.error('[graphsController::query] '+prevError)
error += '\n'+prevError
results[stat_types[type_it]] = prevResults
type_it++
recursiveCb()
else
callback(results)
recursiveCb()
else # palmares (ranking)
queryElasticRanking index.es_type_key, $scope.ranking.groupCriterion, $scope.ranking.sortCriterion, index.graph.limit, (results, error) ->
if (error)
callback([], error)
else
callback(results)
##
# Run the elasticSearch query to retreive the /stats/type aggregations
# @param esType {String} elasticSearch document type (subscription|machine|training|...)
# @param statType {String} statistics type (year|month|hour|booking|...)
# @param callback {function} function be to run after results were retrieved,
# it will receive two parameters : results {Array}, error {String} (if any)
##
queryElasticStats = (esType, statType, callback) ->
# handle invalid callback
if typeof(callback) != "function"
console.error('[graphsController::queryElasticStats] Error: invalid callback provided')
return
if !esType or !statType
callback([], '[graphsController::queryElasticStats] Error: invalid parameters provided')
# run query
es.search
"index": "stats"
"type": esType
"searchType": "count"
"body": buildElasticAggregationsQuery(statType, $scope.display.interval, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected))
, (error, response) ->
if (error)
callback([], "Error: something unexpected occurred during elasticSearch query: "+error)
else
callback(response.aggregations)
##
# For ranking displays, run the elasticSearch query to retreive the /stats/type aggregations
# @param esType {String} elasticSearch document type (subscription|machine|training|...)
# @param statType {String} statistics type (year|month|hour|booking|...)
# @param callback {function} function be to run after results were retrieved,
# it will receive two parameters : results {Array}, error {String} (if any)
##
queryElasticRanking = (esType, groupKey, sortKey, limit, callback) ->
# handle invalid callback
if typeof(callback) != "function"
console.error('[graphsController::queryElasticRanking] Error: invalid callback provided')
return
if !esType or !groupKey or !sortKey or typeof limit != 'number'
callback([], '[graphsController::queryElasticRanking] Error: invalid parameters provided')
# run query
es.search
"index": "stats"
"type": esType
"searchType": "count"
"body": buildElasticAggregationsRankingQuery(groupKey, sortKey, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected))
, (error, response) ->
if (error)
callback([], "Error: something unexpected occurred during elasticSearch query: "+error)
else
callback(response.aggregations)
##
# Parse a final elastic results bucket and return a D3 compatible object
# @param bucket {{key_as_string:{String}, key:{Number}, doc_count:{Number}, total:{{value:{Number}}}}} interval bucket
##
parseElasticBucket = (bucket) ->
[ bucket.key, bucket.total.value ]
##
# Build an object representing the content of the REST-JSON query to elasticSearch, based on the parameters
# currently defined for data aggegations.
# @param type {String} statistics type (visit|rdv|rating|ca|plan|account|search|...)
# @param interval {String} statistics interval (year|quarter|month|week|day|hour|minute|second)
# @param intervalBegin {moment} statitics interval beginning (moment.js type)
# @param intervalEnd {moment} statitics interval ending (moment.js type)
##
buildElasticAggregationsQuery = (type, interval, intervalBegin, intervalEnd) ->
q =
"query":
"bool":
"must": [
{
"match":
"type": type
}
{
"range":
"date":
"gte": intervalBegin.format()
"lte": intervalEnd.format()
}
]
"aggregations":
"subgroups":
"terms":
"field": "subType" #TODO allow aggregate by custom field
"aggregations":
"intervals":
"date_histogram":
"field": "date"
"interval": interval
"min_doc_count": 0
"extended_bounds":
"min": intervalBegin.valueOf()
"max": intervalEnd.valueOf()
"aggregations":
"total":
"sum":
"field": "stat"
# scale weeks on sunday as nvd3 supports only these weeks
if interval == 'week'
q.aggregations.subgroups.aggregations.intervals.date_histogram['post_offset'] = '-1d'
q.aggregations.subgroups.aggregations.intervals.date_histogram['pre_offset'] = '-1d'
# scale days to UTC time
else if interval == 'day'
offset = moment().utcOffset()
q.aggregations.subgroups.aggregations.intervals.date_histogram['post_offset'] = (-offset)+'m'
q
##
# Build an object representing the content of the REST-JSON query to elasticSearch, based on the parameters
# currently defined for data aggegations.
# @param groupKey {String} statistics subtype or custom field
# @param sortKey {String} statistics type or 'ca'
# @param intervalBegin {moment} statitics interval beginning (moment.js type)
# @param intervalEnd {moment} statitics interval ending (moment.js type)
##
buildElasticAggregationsRankingQuery = (groupKey, sortKey, intervalBegin, intervalEnd) ->
q =
"query":
"bool":
"must": [
{
"range":
"date":
"gte": intervalBegin.format()
"lte": intervalEnd.format()
}
{
"term":
"type": "booking"
}
]
"aggregations":
"subgroups":
"terms":
"field": "subType"
"aggregations":
"total":
"sum":
"field": "stat"
# we group the results by the custom given key (eg. by event date)
q.aggregations.subgroups.terms =
field: groupKey
size: 0
# results must be sorted and limited later by angular
if sortKey != 'ca'
angular.forEach q.query.bool.must, (must) ->
if must.term
must.term.type = sortKey
else
q.aggregations.subgroups.aggregations.total.sum.field = sortKey
q
##
# Redraw the NDV3 chart using the provided data
# @param chart_type {String} stackedAreaChart|discreteBarChart|lineChart
# @param data {Array} array of NVD3 dataSources
# @param type {String} which chart to update (statistic type key)
##
updateChart = (chart_type, data, type) ->
id = "#chart-"+type+" svg"
# clean old charts
d3.selectAll(id+" > *").remove()
nv.addGraph ->
# no data or many dates, display line charts
if data.length == 0 or (data[0].values.length > 1 and (chart_type != 'discreteBarChart'))
if chart_type == 'stackedAreaChart'
chart = nv.models.stackedAreaChart().useInteractiveGuideline(true)
else
chart = nv.models.lineChart().useInteractiveGuideline(true)
if data.length > 0
if $scope.display.interval == 'day'
setTimeScale(chart.xAxis, chart.xScale, [d3.time.day, data[0].values.length])
else if $scope.display.interval == 'week'
setTimeScale(chart.xAxis, chart.xScale, [d3.time.week, data[0].values.length])
else if $scope.display.interval == 'month'
setTimeScale(chart.xAxis, chart.xScale, [d3.time.month, data[0].values.length])
chart.xAxis.tickFormat(xAxisTickFormatFunction)
chart.yAxis.tickFormat(d3.format('d'))
chart.xAxis.axisLabel(X_AXIS_LABEL)
chart.yAxis.axisLabel(Y_AXIS_LABEL)
# only one date, display histograms
else
chart = nv.models.discreteBarChart()
chart.tooltip.enabled(false)
chart.showValues(true)
chart.x (d) -> d.label
chart.y (d) -> d.value
data = prepareDataForBarChart(data, type)
# common for each charts
chart.margin({left: 100, right: 100})
chart.noData(_t('no_data_for_this_period'))
chart.height( CHART_HEIGHT )
# add new chart to the page
d3.select(id).datum(data).transition().duration(350).call(chart)
# resize the graph when the page is resized
nv.utils.windowResize(chart.update)
# return the chart
chart
##
# Given an NVD3 line chart axis, scale it to display ordinated dates, according to the given arguments
##
setTimeScale = (nvd3Axis, nvd3Scale, argsArray) ->
scale = d3.time.scale()
nvd3Axis.scale(scale)
nvd3Scale(scale)
if (not argsArray and not argsArray.length)
oldTicks = nvd3Axis.axis.ticks
nvd3Axis.axis.ticks = ->
oldTicks.apply(nvd3Axis.axis, argsArray)
##
# Translate line chart data in dates row to bar chart data, one bar per type.
##
prepareDataForBarChart = (data, type) ->
newData = [
key: type
values: []
]
for info in data
if info
newData[0].values.push
"label": info.key
"value": info.values[0].y
"color": info.color
newData
##
# Sort the provided array, in the specified order, on the value returned by the callback.
# This is a stable-sorting algorithm implementation, ie. two call with the same array will return the same results
# orders, especially with equal values.
# @param array {Array} the array to sort
# @param order {string} 'ASC' or 'DESC'
# @param getValue {function} the callback which will return the value on which the sort will occurs
# @returns {Array}
##
stableSort = (array, order, getValue) ->
# prepare sorting
keys_order = []
result = []
for i in [0..array.length] by 1
keys_order[array[i]] = i;
result.push(array[i]);
# callback for javascript native Array.sort()
sort_fc = (a, b) ->
val_a = getValue(a)
val_b = getValue(b)
if val_a == val_b
return keys_order[a] - keys_order[b]
if val_a < val_b
if order == 'ASC' then return -1
else return 1
else
if order == 'ASC' then return 1
else return -1
# finish the sort
result.sort(sort_fc)
return result
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]

View File

@ -0,0 +1,64 @@
Application.Controllers.controller "GroupsController", ["$scope", 'groupsPromise', 'Group', 'growl', '_t', ($scope, groupsPromise, Group, growl, _t) ->
## List of users groups
$scope.groups = groupsPromise
##
# Removes the newly inserted but not saved group / Cancel the current group modification
# @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
# @param index {number} group index in the $scope.groups array
##
$scope.cancelGroup = (rowform, index) ->
if $scope.groups[index].id?
rowform.$cancel()
else
$scope.groups.splice(index, 1)
##
# Creates a new empty entry in the $scope.groups array
##
$scope.addGroup = ->
$scope.inserted =
name: ''
$scope.groups.push($scope.inserted)
##
# Saves a new group / Update an existing group to the server (form validation callback)
# @param data {Object} group name
# @param [data] {number} group id, in case of update
##
$scope.saveGroup = (data, id) ->
if id?
Group.update {id: id}, { group: data }, (response) ->
growl.success(_t('changes_successfully_saved'))
, (error) ->
growl.error(_t('an_error_occurred_while_saving_changes'))
else
Group.save { group: data }, (resp)->
growl.success(_t('new_group_successfully_saved'))
$scope.groups[$scope.groups.length-1].id = resp.id
, (error) ->
growl.error(_t('an_error_occurred_when_saving_the_new_group'))
$scope.groups.splice($scope.groups.length-1, 1)
##
# Deletes the group at the specified index
# @param index {number} group index in the $scope.groups array
##
$scope.removeGroup = (index) ->
Group.delete { id: $scope.groups[index].id }, (resp) ->
growl.success(_t('group_successfully_deleted'))
$scope.groups.splice(index, 1)
, (error) ->
growl.error(_t('unable_to_delete_group_because_some_users_and_or_groups_are_still_linked_to_it'))
]

View File

@ -0,0 +1,492 @@
'use strict'
##
# Controller used in the admin invoices listing page
##
Application.Controllers.controller "InvoicesController", ["$scope", "$state", 'Invoice', '$uibModal', "growl", "$filter", 'Setting', 'settings', '_t'
, ($scope, $state, Invoice, $uibModal, growl, $filter, Setting, settings, _t) ->
### PUBLIC SCOPE ###
## List of all users invoices
$scope.invoices = Invoice.query()
## Default invoices ordering/sorting
$scope.orderInvoice = '-reference'
## Invoices parameters
$scope.invoice =
logo: null
reference:
model: ''
help: null
templateUrl: 'editReference.html'
code:
model: ''
active: true
templateUrl: 'editCode.html'
number:
model: ''
help: null
templateUrl: 'editNumber.html'
VAT:
rate: 19.6
active: false
templateUrl: 'editVAT.html'
text:
content: ''
legals:
content: ''
## Placeholding date for the invoice creation
$scope.today = moment()
## Placeholding date for the reservation begin
$scope.inOneWeek = moment().add(1, 'week').startOf('hour')
## Placeholding date for the reservation end
$scope.inOneWeekAndOneHour = moment().add(1, 'week').add(1, 'hour').startOf('hour')
##
# Change the invoices ordering criterion to the one provided
# @param orderBy {string} ordering criterion
##
$scope.setOrderInvoice = (orderBy)->
if $scope.orderInvoice == orderBy
$scope.orderInvoice = '-'+orderBy
else
$scope.orderInvoice = orderBy
##
# Open a modal window asking the admin the details to refund the user about the provided invoice
# @param invoice {Object} invoice inherited from angular's $resource
##
$scope.generateAvoirForInvoice = (invoice)->
# open modal
modalInstance = $uibModal.open
templateUrl: '<%= asset_path "admin/invoices/avoirModal.html" %>'
controller: 'AvoirModalController'
resolve:
invoice: -> invoice
# once done, update the invoice model and inform the admin
modalInstance.result.then (res) ->
$scope.invoices.unshift res.avoir
Invoice.get {id: invoice.id}, (data) ->
invoice.has_avoir = data.has_avoir
growl.success(_t('refund_invoice_successfully_created'))
##
# Generate an invoice reference sample from the parametrized model
# @returns {string} invoice reference sample
##
$scope.mkReference = ->
sample = $scope.invoice.reference.model
if sample
# invoice number per day (dd..dd)
sample = sample.replace(/d+(?![^\[]*])/g, (match, offset, string) ->
padWithZeros(2, match.length)
)
# invoice number per month (mm..mm)
sample = sample.replace(/m+(?![^\[]*])/g, (match, offset, string) ->
padWithZeros(12, match.length)
)
# invoice number per year (yy..yy)
sample = sample.replace(/y+(?![^\[]*])/g, (match, offset, string) ->
padWithZeros(8, match.length)
)
# date informations
sample = sample.replace(/[YMD]+(?![^\[]*])/g, (match, offset, string) ->
$scope.today.format(match)
)
# information about online selling (X[text])
sample = sample.replace(/X\[([^\]]+)\]/g, (match, p1, offset, string) ->
p1
)
# information about refunds (R[text]) - does not apply here
sample = sample.replace(/R\[([^\]]+)\]/g, "")
sample
##
# Generate an order nmuber sample from the parametrized model
# @returns {string} invoice reference sample
##
$scope.mkNumber = ->
sample = $scope.invoice.number.model
if sample
# global order number (nn..nn)
sample = sample.replace(/n+(?![^\[]*])/g, (match, offset, string) ->
padWithZeros(327, match.length)
)
# order number per year (yy..yy)
sample = sample.replace(/y+(?![^\[]*])/g, (match, offset, string) ->
padWithZeros(8, match.length)
)
# order number per month (mm..mm)
sample = sample.replace(/m+(?![^\[]*])/g, (match, offset, string) ->
padWithZeros(12, match.length)
)
# order number per day (dd..dd)
sample = sample.replace(/d+(?![^\[]*])/g, (match, offset, string) ->
padWithZeros(2, match.length)
)
# date informations
sample = sample.replace(/[YMD]+(?![^\[]*])/g, (match, offset, string) ->
$scope.today.format(match)
)
sample
##
# Open a modal dialog allowing the user to edit the invoice reference generation template
##
$scope.openEditReference = ->
modalInstance = $uibModal.open
animation: true,
templateUrl: $scope.invoice.reference.templateUrl,
size: 'lg',
resolve:
model: ->
$scope.invoice.reference.model
controller: ($scope, $uibModalInstance, model) ->
$scope.model = model
$scope.ok = ->
$uibModalInstance.close($scope.model)
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
modalInstance.result.then (model) ->
Setting.update { name: 'invoice_reference' }, { value: model }, (data)->
$scope.invoice.reference.model = model
growl.success(_t('invoice_reference_successfully_saved'))
, (error)->
growl.error(_t('an_error_occurred_while_saving_invoice_reference'))
console.error(error)
##
# Open a modal dialog allowing the user to edit the invoice code
##
$scope.openEditCode = ->
modalInstance = $uibModal.open
animation: true,
templateUrl: $scope.invoice.code.templateUrl,
size: 'lg',
resolve:
model: ->
$scope.invoice.code.model
active: ->
$scope.invoice.code.active
controller: ($scope, $uibModalInstance, model, active) ->
$scope.codeModel = model
$scope.isSelected = active
$scope.ok = ->
$uibModalInstance.close({model: $scope.codeModel, active: $scope.isSelected})
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
modalInstance.result.then (result) ->
Setting.update { name: 'invoice_code-value' }, { value: result.model }, (data)->
$scope.invoice.code.model = result.model
if result.active
growl.success(_t('invoicing_code_succesfully_saved'))
, (error)->
growl.error(_t('an_error_occurred_while_saving_the_invoicing_code'))
console.error(error)
Setting.update { name: 'invoice_code-active' }, { value: if result.active then "true" else "false" }, (data)->
$scope.invoice.code.active = result.active
if result.active
growl.success(_t('code_successfully_activated'))
else
growl.success(_t('code_successfully_disabled'))
, (error)->
growl.error(_t('an_error_occurred_while_activating_the_invoicing_code'))
console.error(error)
##
# Open a modal dialog allowing the user to edit the invoice number
##
$scope.openEditInvoiceNb = ->
modalInstance = $uibModal.open
animation: true,
templateUrl: $scope.invoice.number.templateUrl,
size: 'lg',
resolve:
model: ->
$scope.invoice.number.model
controller: ($scope, $uibModalInstance, model) ->
$scope.model = model
$scope.ok = ->
$uibModalInstance.close($scope.model)
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
modalInstance.result.then (model) ->
Setting.update { name: 'invoice_order-nb' }, { value: model }, (data)->
$scope.invoice.number.model = model
growl.success(_t('order_number_successfully_saved'))
, (error)->
growl.error(_t('an_error_occurred_while_saving_the_order_number'))
console.error(error)
##
# Open a modal dialog allowing the user to edit the VAT parameters for the invoices
# The VAT can be disabled and its rate can be configured
##
$scope.openEditVAT = ->
modalInstance = $uibModal.open
animation: true,
templateUrl: $scope.invoice.VAT.templateUrl,
size: 'lg',
resolve:
rate: ->
$scope.invoice.VAT.rate
active: ->
$scope.invoice.VAT.active
controller: ($scope, $uibModalInstance, rate, active) ->
$scope.rate = rate
$scope.isSelected = active
$scope.ok = ->
$uibModalInstance.close({rate: $scope.rate, active: $scope.isSelected})
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
modalInstance.result.then (result) ->
Setting.update { name: 'invoice_VAT-rate' }, { value: result.rate+"" }, (data)->
$scope.invoice.VAT.rate = result.rate
if result.active
growl.success(_t('VAT_rate_successfully_saved'))
, (error)->
growl.error(_t('an_error_occurred_while_saving_the_VAT_rate'))
console.error(error)
Setting.update { name: 'invoice_VAT-active' }, { value: if result.active then "true" else "false" }, (data)->
$scope.invoice.VAT.active = result.active
if result.active
growl.success(_t('VAT_successfully_activated'))
else
growl.success(_t('VAT_successfully_disabled'))
, (error)->
growl.error(_t('an_error_occurred_while_activating_the_VAT'))
console.error(error)
##
# Callback to save the value of the text zone when editing is done
##
$scope.textEditEnd = (event) ->
parsed = parseHtml($scope.invoice.text.content)
Setting.update { name: 'invoice_text' }, { value: parsed }, (data)->
$scope.invoice.text.content = parsed
growl.success(_t('text_successfully_saved'))
, (error)->
growl.error(_t('an_error_occurred_while_saving_the_text'))
console.error(error)
##
# Callback to save the value of the legal informations zone when editing is done
##
$scope.legalsEditEnd = (event) ->
parsed = parseHtml($scope.invoice.legals.content)
Setting.update { name: 'invoice_legals' }, { value: parsed }, (data)->
$scope.invoice.legals.content = parsed
growl.success(_t('address_and_legal_information_successfully_saved'))
, (error)->
growl.error(_t('an_error_occurred_while_saving_the_address_and_the_legal_information'))
console.error(error)
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
# retrieve settings from the DB through the API
$scope.invoice.legals.content = settings['invoice_legals']
$scope.invoice.text.content = settings['invoice_text']
$scope.invoice.VAT.rate = parseFloat(settings['invoice_VAT-rate'])
$scope.invoice.VAT.active = (settings['invoice_VAT-active'] == "true")
$scope.invoice.number.model = settings['invoice_order-nb']
$scope.invoice.code.model = settings['invoice_code-value']
$scope.invoice.code.active = (settings['invoice_code-active'] == "true")
$scope.invoice.reference.model = settings['invoice_reference']
$scope.invoice.logo =
filetype: 'image/png'
filename: 'logo.png'
base64: settings['invoice_logo']
# Watch the logo, when a change occurs, save it
$scope.$watch 'invoice.logo', ->
if $scope.invoice.logo and $scope.invoice.logo.filesize
Setting.update { name: 'invoice_logo' }, { value: $scope.invoice.logo.base64 }, (data)->
growl.success(_t('logo_successfully_saved'))
, (error)->
growl.error(_t('an_error_occurred_while_saving_the_logo'))
console.error(error)
##
# Output the given integer with leading zeros. If the given value is longer than the given
# length, it will be truncated.
# @param value {number} the integer to pad
# @param length {number} the length of the resulting string.
##
padWithZeros = (value, length) ->
(1e15+value+"").slice(-length)
##
# Remove every unsupported html tag from the given html text (like <p>, <span>, ...).
# The supported tags are <b>, <u>, <i> and <br>.
# @param html {string} single line html text
# @return {string} multi line simplified html text
##
parseHtml = (html) ->
html = html.replace(/<\/?(\w+)((\s+\w+(\s*=\s*(?:".*?"|'.*?'|[^'">\s]+))?)+\s*|\s*)\/?>/g, (match, p1, offset, string) ->
if p1 in ['b', 'u', 'i', 'br']
match
else
''
)
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]
##
# Controller used in the invoice refunding modal window
##
Application.Controllers.controller 'AvoirModalController', ["$scope", "$uibModalInstance", '$locale', "invoice", "Invoice", "growl", '_t'
, ($scope, $uibModalInstance, $locale, invoice, Invoice, growl, _t) ->
### PUBLIC SCOPE ###
## invoice linked to the current refund
$scope.invoice = invoice
## Associative array containing invoice_item ids associated with boolean values
$scope.partial = {}
## Default refund parameters
$scope.avoir =
invoice_id: invoice.id
subscription_to_expire: false
invoice_items_ids: []
## Possible refunding methods
$scope.avoirModes = [
{name: _t('none'), value: 'none'}
{name: _t('by_cash'), value: 'cash'}
{name: _t('by_cheque'), value: 'cheque'}
{name: _t('by_transfer'), value: 'transfer'}
]
## If a subscription was took with the current invoice, should it be canceled or not
$scope.subscriptionExpireOptions = {}
$scope.subscriptionExpireOptions[_t('yes')] = true
$scope.subscriptionExpireOptions[_t('no')] = false
## AngularUI-Bootstrap datepicker parameters to define when to refund
$scope.datePicker =
format: $locale.DATETIME_FORMATS.shortDate
opened: false # default: datePicker is not shown
options:
startingDay: Fablab.weekStartingDay
##
# Callback to open the datepicker
##
$scope.openDatePicker = ($event) ->
$event.preventDefault()
$event.stopPropagation()
$scope.datePicker.opened = true
##
# Validate the refunding and generate a refund invoice
##
$scope.ok = ->
# check that at least 1 element of the invoice is refunded
$scope.avoir.invoice_items_ids = []
for itemId, refundItem of $scope.partial
$scope.avoir.invoice_items_ids.push(parseInt(itemId)) if refundItem
if $scope.avoir.invoice_items_ids.length is 0
growl.error(_t('you_must_select_at_least_one_element_to_create_a_refund'))
else
Invoice.save {avoir: $scope.avoir}, (avoir) ->
# success
$uibModalInstance.close({avoir:avoir, invoice:$scope.invoice})
, (err) ->
# failed
growl.error(_t('unable_to_create_the_refund'))
##
# Cancel the refund, dismiss the modal window
##
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
## if the invoice was payed with stripe, allow to refund through stripe
Invoice.get {id: invoice.id}, (data) ->
$scope.invoice = data
# default : all elements of the invoice are refund
for item in data.items
$scope.partial[item.id] = (typeof item.avoir_item_id isnt 'number')
if invoice.stripe
$scope.avoirModes.push {name: _t('online_payment'), value: 'stripe'}
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]

View File

@ -1,153 +0,0 @@
'use strict'
### COMMON CODE ###
##
# Provides a set of common properties and methods to the $scope parameter. They are used
# in the various members' admin controllers.
#
# Provides :
# - $scope.groups = [{Group}]
# - $scope.datePicker = {}
# - $scope.submited(content)
# - $scope.cancel()
# - $scope.fileinputClass(v)
# - $scope.openDatePicker($event)
#
# Requires :
# - $state (Ui-Router) [ 'app.admin.members' ]
##
class MembersController
constructor: ($scope, $state, Group) ->
## Retrieve the profiles groups (eg. students ...)
Group.query (groups) ->
$scope.groups = groups
$scope.user.group_id = $scope.groups[0].id
## Default parameters for AngularUI-Bootstrap datepicker
$scope.datePicker =
format: 'dd/MM/yyyy'
opened: false # default: datePicker is not shown
options:
startingDay: 1 # France: the week starts on monday
##
# Shows the birth day datepicker
# @param $event {Object} jQuery event object
##
$scope.openDatePicker = ($event) ->
$event.preventDefault()
$event.stopPropagation()
$scope.datePicker.opened = true
##
# For use with ngUpload (https://github.com/twilson63/ngUpload).
# Intended to be the callback when an upload is done: any raised error will be stacked in the
# $scope.alerts array. If everything goes fine, the user is redirected to the members listing page.
# @param content {Object} JSON - The upload's result
##
$scope.submited = (content) ->
if !content.id?
$scope.alerts = []
angular.forEach content, (v, k)->
angular.forEach v, (err)->
$scope.alerts.push
msg: k+': '+err,
type: 'danger'
else
$state.go('app.admin.members')
##
# Changes the admin's view to the members list page
##
$scope.cancel = ->
$state.go('app.admin.members')
##
# For use with 'ng-class', returns the CSS class name for the uploads previews.
# The preview may show a placeholder or the content of the file depending on the upload state.
# @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules)
##
$scope.fileinputClass = (v)->
if v
'fileinput-exists'
else
'fileinput-new'
##
# Controller used in the member edition page
##
Application.Controllers.controller "editMemberController", ["$scope", "$state", "$stateParams", "Member", 'dialogs', 'growl', 'Group', 'CSRF', ($scope, $state, $stateParams, Member, dialogs, growl, Group, CSRF) ->
CSRF.setMetaTags()
### PUBLIC SCOPE ###
## API URL where the form will be posted
$scope.actionUrl = "/api/members/" + $stateParams.id
## Form action on the above URL
$scope.method = 'patch'
## The user to edit
$scope.user = {}
## Profiles types (student/standard/...)
$scope.groups = []
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
## Retrieve the member's profile details
Member.get {id: $stateParams.id}, (resp)->
$scope.user = resp
## Using the MembersController
new MembersController($scope, $state, Group)
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]
##
# Controller used in the member's creation page (admin view)
##
Application.Controllers.controller "newMemberController", ["$scope", "$state", "$stateParams", "Member", 'Group', 'CSRF', ($scope, $state, $stateParams, Member, Group, CSRF) ->
CSRF.setMetaTags()
### PUBLIC SCOPE ###
## API URL where the form will be posted
$scope.actionUrl = "/api/members"
## Form action on the above URL
$scope.method = 'post'
## Default member's profile parameters
$scope.user =
plan_interval: ''
## Using the MembersController
new MembersController($scope, $state, Group)
]

View File

@ -0,0 +1,439 @@
'use strict'
### COMMON CODE ###
##
# Provides a set of common properties and methods to the $scope parameter. They are used
# in the various members' admin controllers.
#
# Provides :
# - $scope.groups = [{Group}]
# - $scope.trainings = [{Training}]
# - $scope.plans = []
# - $scope.datePicker = {}
# - $scope.submited(content)
# - $scope.cancel()
# - $scope.fileinputClass(v)
# - $scope.openDatePicker($event)
# - $scope.openSubscriptionDatePicker($event)
#
# Requires :
# - $state (Ui-Router) [ 'app.admin.members' ]
##
class MembersController
constructor: ($scope, $state, $locale, Group, Training) ->
## Retrieve the profiles groups (eg. students ...)
Group.query (groups) ->
$scope.groups = groups
## Retrieve the list the available trainings
Training.query().$promise.then (data)->
$scope.trainings = data.map (d) ->
id: d.id
name: d.name
## Default parameters for AngularUI-Bootstrap datepicker
$scope.datePicker =
format: $locale.DATETIME_FORMATS.shortDate
opened: false # default: datePicker is not shown
subscription_date_opened: false
options:
startingDay: Fablab.weekStartingDay
##
# Shows the birth day datepicker
# @param $event {Object} jQuery event object
##
$scope.openDatePicker = ($event) ->
$event.preventDefault()
$event.stopPropagation()
$scope.datePicker.opened = true
##
# Shows the end of subscription datepicker
# @param $event {Object} jQuery event object
##
$scope.openSubscriptionDatePicker = ($event) ->
$event.preventDefault()
$event.stopPropagation()
$scope.datePicker.subscription_date_opened = true
##
# For use with ngUpload (https://github.com/twilson63/ngUpload).
# Intended to be the callback when an upload is done: any raised error will be stacked in the
# $scope.alerts array. If everything goes fine, the user is redirected to the members listing page.
# @param content {Object} JSON - The upload's result
##
$scope.submited = (content) ->
if !content.id?
$scope.alerts = []
angular.forEach content, (v, k)->
angular.forEach v, (err)->
$scope.alerts.push
msg: k+': '+err,
type: 'danger'
else
$state.go('app.admin.members')
##
# Changes the admin's view to the members list page
##
$scope.cancel = ->
$state.go('app.admin.members')
##
# For use with 'ng-class', returns the CSS class name for the uploads previews.
# The preview may show a placeholder or the content of the file depending on the upload state.
# @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules)
##
$scope.fileinputClass = (v)->
if v
'fileinput-exists'
else
'fileinput-new'
##
# Controller used in the members/groups management page
##
Application.Controllers.controller "AdminMembersController", ["$scope", 'membersPromise', 'adminsPromise', 'growl', 'Admin', 'dialogs', '_t'
, ($scope, membersPromise, adminsPromise, growl, Admin, dialogs, _t) ->
### PUBLIC SCOPE ###
## members list
$scope.members = membersPromise
## admins list
$scope.admins = adminsPromise.admins
## Members ordering/sorting. Default: not sorted
$scope.orderMember = null
## Admins ordering/sorting. Default: not sorted
$scope.orderAdmin = null
##
# Change the members ordering criterion to the one provided
# @param orderBy {string} ordering criterion
##
$scope.setOrderMember = (orderBy)->
if $scope.orderMember == orderBy
$scope.orderMember = '-'+orderBy
else
$scope.orderMember = orderBy
##
# Change the admins ordering criterion to the one provided
# @param orderBy {string} ordering criterion
##
$scope.setOrderAdmin = (orderAdmin)->
if $scope.orderAdmin == orderAdmin
$scope.orderAdmin = '-'+orderAdmin
else
$scope.orderAdmin = orderAdmin
##
# Ask for confirmation then delete the specified administrator
# @param admins {Array} full list of administrators
# @param admin {Object} administrator to delete
##
$scope.destroyAdmin = (admins, admin)->
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_delete_this_administrator_this_cannot_be_undone')
, -> # cancel confirmed
Admin.delete id: admin.id, ->
admins.splice(findAdminIdxById(admins, admin.id), 1)
growl.success(_t('administrator_successfully_deleted'))
, (error)->
growl.error(_t('unable_to_delete_the_administrator'))
### PRIVATE SCOPE ###
##
# Iterate through the provided array and return the index of the requested admin
# @param admins {Array} full list of users with role 'admin'
# @param id {Number} user id of the admin to retrieve in the list
# @returns {Number} index of the requested admin, in the provided array
##
findAdminIdxById = (admins, id)->
(admins.map (admin)->
admin.id
).indexOf(id)
]
##
# Controller used in the member edition page
##
Application.Controllers.controller "EditMemberController", ["$scope", "$state", "$stateParams", '$locale', "Member", 'Training', 'dialogs', 'growl', 'Group', 'Subscription', 'CSRF', 'memberPromise', 'tagsPromise', '$uibModal', 'Plan', '$filter', '_t'
, ($scope, $state, $stateParams, $locale, Member, Training, dialogs, growl, Group, Subscription, CSRF, memberPromise, tagsPromise, $uibModal, Plan, $filter, _t) ->
### PUBLIC SCOPE ###
## API URL where the form will be posted
$scope.actionUrl = "/api/members/" + $stateParams.id
## Form action on the above URL
$scope.method = 'patch'
## List of tags associables with user
$scope.tags = tagsPromise
## The user to edit
$scope.user = memberPromise
## the user subscription
if $scope.user.subscribed_plan? and $scope.user.subscription?
$scope.subscription = $scope.user.subscription
$scope.subscription.expired_at = $scope.subscription.expired_at
else
Plan.query group_id: $scope.user.group_id, (plans)->
$scope.plans = plans
for plan in $scope.plans
plan.nameToDisplay = $filter('humanReadablePlanName')(plan)
## Available trainings list
$scope.trainings = []
## Profiles types (student/standard/...)
$scope.groups = []
##
# Open a modal dialog, allowing the admin to extend the current user's subscription (freely or not)
# @param subscription {Object} User's subscription object
# @param free {boolean} True if the extent is offered, false otherwise
##
$scope.updateSubscriptionModal = (subscription, free)->
modalInstance = $uibModal.open
animation: true,
templateUrl: '<%= asset_path "admin/subscriptions/expired_at_modal.html" %>'
size: 'lg',
controller: ['$scope', '$uibModalInstance', 'Subscription', ($scope, $uibModalInstance, Subscription) ->
$scope.new_expired_at = angular.copy(subscription.expired_at)
$scope.free = free
$scope.datePicker =
opened: false
format: $locale.DATETIME_FORMATS.shortDate
options:
startingDay: Fablab.weekStartingDay
minDate: new Date
$scope.openDatePicker = (ev)->
ev.preventDefault();
ev.stopPropagation();
$scope.datePicker.opened = true
$scope.ok = ->
Subscription.update { id: subscription.id }, { subscription: { expired_at: $scope.new_expired_at, free: free } }, (_subscription)->
growl.success(_t('you_successfully_changed_the_expiration_date_of_the_user_s_subscription'))
$uibModalInstance.close(_subscription)
, (error)->
growl.error(_t('a_problem_occurred_while_saving_the_date'))
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
]
# once the form was validated succesfully ...
modalInstance.result.then (subscription) ->
$scope.subscription.expired_at = subscription.expired_at
##
# Open a modal dialog allowing the admin to set a subscription for the given user.
# @param user {Object} User object, user currently reviewed, as recovered from GET /api/members/:id
# @param plans {Array} List of plans, availables for the currently reviewed user, as recovered from GET /api/plans
##
$scope.createSubscriptionModal = (user, plans)->
modalInstance = $uibModal.open
animation: true,
templateUrl: '<%= asset_path "admin/subscriptions/create_modal.html" %>'
size: 'lg',
controller: ['$scope', '$uibModalInstance', 'Subscription', 'Group', ($scope, $uibModalInstance, Subscription, Group) ->
## selected user
$scope.user = user
## available plans for the selected user
$scope.plans = plans
##
# Generate a string identifying the given plan by literal humain-readable name
# @param plan {Object} Plan object, as recovered from GET /api/plan/:id
# @param groups {Array} List of Groups objects, as recovered from GET /api/groups
# @param short {boolean} If true, the generated name will contains the group slug, otherwise the group full name
# will be included.
# @returns {String}
##
$scope.humanReadablePlanName = (plan, groups, short)->
"#{$filter('humanReadablePlanName')(plan, groups, short)}"
##
# Modal dialog validation callback
##
$scope.ok = ->
$scope.subscription.user_id = user.id
Subscription.save { }, { subscription: $scope.subscription }, (_subscription)->
growl.success(_t('subscription_successfully_purchased'))
$uibModalInstance.close(_subscription)
$state.reload()
, (error)->
growl.error(_t('a_problem_occurred_while_taking_the_subscription'))
##
# Modal dialog cancellation callback
##
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
]
# once the form was validated succesfully ...
modalInstance.result.then (subscription) ->
$scope.subscription = subscription
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
CSRF.setMetaTags()
# init the birth date to JS object
$scope.user.profile.birthday = moment($scope.user.profile.birthday).toDate()
## the user subscription
if $scope.user.subscribed_plan? and $scope.user.subscription?
$scope.subscription = $scope.user.subscription
$scope.subscription.expired_at = $scope.subscription.expired_at
else
Plan.query group_id: $scope.user.group_id, (plans)->
$scope.plans = plans
for plan in $scope.plans
plan.nameToDisplay = "#{plan.base_name} - #{plan.interval}"
# Using the MembersController
new MembersController($scope, $state, $locale, Group, Training)
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]
##
# Controller used in the member's creation page (admin view)
##
Application.Controllers.controller "NewMemberController", ["$scope", "$state", "$locale", "$stateParams", "Member", 'Training', 'Group', 'CSRF'
, ($scope, $state, $locale, $stateParams, Member, Training, Group, CSRF) ->
CSRF.setMetaTags()
### PUBLIC SCOPE ###
## API URL where the form will be posted
$scope.actionUrl = "/api/members"
## Form action on the above URL
$scope.method = 'post'
## Default member's profile parameters
$scope.user =
plan_interval: ''
## Using the MembersController
new MembersController($scope, $state, $locale, Group, Training)
]
##
# Controller used in the admin's creation page (admin view)
##
Application.Controllers.controller 'NewAdminController', ['$state', '$scope', '$locale', 'Admin', 'growl', ($state, $scope, $locale, Admin, growl)->
## default admin profile
$scope.admin =
profile_attributes:
gender: true
## Default parameters for AngularUI-Bootstrap datepicker
$scope.datePicker =
format: $locale.DATETIME_FORMATS.shortDate
opened: false
options:
startingDay: Fablab.weekStartingDay
##
# Shows the birth day datepicker
# @param $event {Object} jQuery event object
##
$scope.openDatePicker = ($event)->
$scope.datePicker.opened = true
##
# Send the new admin, currently stored in $scope.admin, to the server for database saving
##
$scope.saveAdmin = ->
Admin.save {}, { admin: $scope.admin }, ->
growl.success(_t('administrator_successfully_created_he_will_receive_his_connection_directives_by_email', {GENDER:getGender($scope.admin)}, "messageformat"))
$state.go('app.admin.members')
, (error)->
console.log(error)
### PRIVATE SCOPE ###
##
# Return an enumerable meaninful string for the gender of the provider user
# @param user {Object} Database user record
# @return {string} 'male' or 'female'
##
getGender = (user) ->
if user.profile_attributes
if user.profile_attributes.gender then 'male' else 'female'
else 'other'
]

View File

@ -0,0 +1,260 @@
'use strict'
### COMMON CODE ###
class PlanController
constructor: ($scope, groups, plans, machines, prices, partners, CSRF) ->
# protection against request forgery
CSRF.setMetaTags()
## groups list
$scope.groups = groups
## plans list
$scope.plans = plans
## machines list
$scope.machines = machines
## users with role 'partner', notifiables for a partner plan
$scope.partners = partners.users
## Subscriptions prices, machines prices and training prices, per groups
$scope.group_pricing = prices
##
# For use with 'ng-class', returns the CSS class name for the uploads previews.
# The preview may show a placeholder or the content of the file depending on the upload state.
# @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules)
##
$scope.fileinputClass = (v)->
if v
'fileinput-exists'
else
'fileinput-new'
##
# Mark the provided file for deletion
# @param file {Object}
##
$scope.deleteFile = (file) ->
if file? and file.id?
file._destroy = true
##
# Retrieve a plan from its numeric identifier
# @param id {number} plan ID
# @returns {Object} Plan, inherits from $resource
##
$scope.getPlanFromId = (id) ->
for plan in $scope.plans
if plan.id == id
return plan
##
# Controller used in the plan creation form
##
Application.Controllers.controller 'NewPlanController', ['$scope', '$uibModal', 'groups', 'plans', 'machines', 'prices', 'partners', 'CSRF', '$state', 'growl', '_t', '$locale'
, ($scope, $uibModal, groups, plans, machines, prices, partners, CSRF, $state, growl, _t, $locale) ->
### PRIVATE STATIC CONSTANTS ###
## when creating a new contact for a partner plan, this ID will be sent to the server
NEW_PARTNER_ID: null
### PUBLIC SCOPE ###
## current form is used to create a new plan
$scope.mode = 'creation'
## prices bindings
$scope.prices =
training: {}
machine: {}
## form inputs bindings
$scope.plan =
type: null
group_id: null
interval: null
intervalCount: 0
amount: null
isRolling: false
partnerId: null
partnerContact: null
ui_weight: 0
## API URL where the form will be posted
$scope.actionUrl = "/api/plans/"
## HTTP method for the rest API
$scope.method = 'POST'
## currency symbol for the current locale (cf. angular-i18n)
$scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM;
##
# Checks if the partner contact is a valid data. Used in the form validation process
# @returns {boolean}
##
$scope.partnerIsValid = ->
($scope.plan.type == "Plan") or ($scope.plan.partnerId or ($scope.plan.partnerContact and $scope.plan.partnerContact.email))
##
# Open a modal dialog allowing the admin to create a new partner user
##
$scope.openPartnerNewModal = (subscription)->
modalInstance = $uibModal.open
animation: true,
templateUrl: '<%= asset_path "shared/_partner_new_modal.html" %>'
size: 'lg',
controller: ['$scope', '$uibModalInstance', 'User', ($scope, $uibModalInstance, User) ->
$scope.partner = {}
$scope.ok = ->
User.save {}, { user: $scope.partner }, (user)->
$scope.partner.id = user.id
$scope.partner.name = "#{user.first_name} #{user.last_name}"
$uibModalInstance.close($scope.partner)
, (error)->
growl.error(_t('unable_to_save_this_user_check_that_there_isnt_an_already_a_user_with_the_same_name'))
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
]
# once the form was validated succesfully ...
modalInstance.result.then (partner) ->
$scope.partners.push(partner)
$scope.plan.partnerId = partner.id
##
# Display some messages and redirect the user, once the form was submitted, depending on the result status
# (failed/succeeded).
# @param content {Object}
##
$scope.afterSubmit = (content) ->
if !content.id? and !content.plan_ids?
growl.error(_t('unable_to_create_the_subscription_please_try_again'))
else
growl.success(_t('successfully_created_subscription(s)_dont_forget_to_redefine_prices'))
if content.plan_ids?
$state.go('app.admin.pricing')
else
if content.id?
$state.go('app.admin.plans.edit', {id: content.id})
new PlanController($scope, groups, plans, machines, prices, partners, CSRF)
]
##
# 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) ->
### PUBLIC SCOPE ###
$scope.groups = groups
## current form is used for edition mode
$scope.mode = 'edition'
## edited plan data
$scope.plan = planPromise
$scope.plan.type = "Plan" if $scope.plan.type == null
## API URL where the form will be posted
$scope.actionUrl = "/api/plans/" + $stateParams.id
## HTTP method for the rest API
$scope.method = 'PATCH'
## currency symbol for the current locale (cf. angular-i18n)
$scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM;
##
# If a parent plan was set ($scope.plan.parent), the prices will be copied from this parent plan into
# the current plan prices list. Otherwise, the current plan prices will be erased.
##
$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
# if no plan were selected, unset every prices
else
for key, price of $scope.plan.prices
$scope.plan.prices[key].amount = 0
##
# Display some messages once the form was submitted, depending on the result status (failed/succeeded)
# @param content {Object}
##
$scope.afterSubmit = (content) ->
if !content.id? and !content.plan_ids?
growl.error(_t('unable_to_save_subscription_changes_please_try_again'))
else
growl.success(_t('subscription_successfully_changed'))
$state.go('app.admin.pricing')
##
# Generate a string identifying the given plan by literal humain-readable name
# @param plan {Object} Plan object, as recovered from GET /api/plan/:id
# @param groups {Array} List of Groups objects, as recovered from GET /api/groups
# @param short {boolean} If true, the generated name will contains the group slug, otherwise the group full name
# will be included.
# @returns {String}
##
$scope.humanReadablePlanName = (plan, groups, short)->
"#{$filter('humanReadablePlanName')(plan, groups, short)}"
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
# Using the PlansController
new PlanController($scope, groups, plans, machines, prices, partners, CSRF)
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]

View File

@ -0,0 +1,383 @@
'use strict'
##
# Controller used in the prices edition page
##
Application.Controllers.controller "EditPricingController", ["$scope", "$state", '$uibModal', 'Training', 'TrainingsPricing', 'Machine', '$filter', 'Credit', 'Pricing', 'Plan', 'plans', 'groups', 'growl', 'machinesPricesPromise', 'Price', 'dialogs', 'trainingsPricingsPromise', '_t'
, ($scope, $state, $uibModal, Training, TrainingsPricing, Machine, $filter, Credit, Pricing, Plan, plans, groups, growl, machinesPricesPromise, Price, dialogs, trainingsPricingsPromise, _t) ->
### PUBLIC SCOPE ###
## List of machines prices (not considering any plan)
$scope.machinesPrices = machinesPricesPromise.prices
## List of trainings pricing
$scope.trainingsPricings = trainingsPricingsPromise
## List of available subscriptions plans (eg. student/month, PME/year ...)
$scope.plans = plans
## List of groups (eg. normal, student ...)
$scope.groups = groups
## Associate free machine hours with subscriptions
$scope.machineCredits = []
## Array of associations (plan <-> training)
$scope.trainingCredits = []
## Associate a plan with all its trainings ids
$scope.trainingCreditsGroups = {}
## List of trainings
$scope.trainings = []
## List of machines
$scope.machines = []
## The plans list ordering. Default: by group
$scope.orderPlans = 'group_id'
## Status of the drop-down menu in Credits tab
$scope.status =
isopen: false
$scope.findTrainingsPricing = (trainingsPricings, trainingId, groupId)->
for trainingsPricing in trainingsPricings
if trainingsPricing.training_id == trainingId and trainingsPricing.group_id == groupId
return trainingsPricing
$scope.updateTrainingsPricing = (data, trainingsPricing)->
if data?
TrainingsPricing.update({ id: trainingsPricing.id }, { trainings_pricing: { amount: data } }).$promise
else
_t('please_specify_a_number')
##
# Retrieve a plan from its given identifier and returns it
# @param id {number} plan ID
# @returns {Object} Plan, inherits from $resource
##
$scope.getPlanFromId = (id) ->
for plan in $scope.plans
if plan.id == parseInt(id)
return plan
##
# Retrieve a group from its given identifier and returns it
# @param id {number} group ID
# @returns {Object} Group, inherits from $resource
##
$scope.getGroupFromId = (groups, id) ->
for group in groups
if group.id == parseInt(id)
return group
##
# Returns a human readable string of named trainings, according to the provided array.
# $scope.trainings may contains the full list of training. The returned string will only contains the trainings
# whom ID are given in the provided parameter
# @param trainings {Array<number>} trainings IDs array
##
$scope.showTrainings = (trainings) ->
unless angular.isArray(trainings) and trainings.length > 0
return _t('none')
selected = []
angular.forEach $scope.trainings, (t) ->
if trainings.indexOf(t.id) >= 0
selected.push t.name
return if selected.length then selected.join(' | ') else _t('none')
##
# Validation callback when editing training's credits. Save the changes.
# @param newdata {Object} training and associated plans
# @param planId {number|string} plan id
##
$scope.saveTrainingCredits = (newdata, planId) ->
# save the number of credits
Plan.update {id: planId},
training_credit_nb: newdata.training_credits
, angular.noop() # do nothing in case of success
, (error) ->
growl.error(_t('an_error_occurred_while_saving_the_number_of_credits'))
# save the associated trainings
angular.forEach $scope.trainingCreditsGroups, (original, key) ->
if parseInt(key) == parseInt(planId) # we've got the original data
if original.join('_') != newdata.training_ids.join('_') # if any changes
# iterate through the previous credits to remove
angular.forEach original, (oldTrainingId) ->
if newdata.training_ids.indexOf(oldTrainingId) == -1
tc = findTrainingCredit(oldTrainingId, planId)
if tc
tc.$delete {}
, ->
$scope.trainingCredits.splice($scope.trainingCredits.indexOf(tc), 1)
$scope.trainingCreditsGroups[planId].splice($scope.trainingCreditsGroups[planId].indexOf(tc.id), 1)
, (error) ->
growl.error(_t('an_error_occurred_while_deleting_credit_with_the_TRAINING', {TRAINING:tc.creditable.name}))
else
growl.error(_t('an_error_occurred_unable_to_find_the_credit_to_revoke'))
# iterate through the new credits to add
angular.forEach newdata.training_ids, (newTrainingId) ->
if original.indexOf(newTrainingId) == -1
Credit.save
credit:
creditable_id: newTrainingId
creditable_type: 'Training'
plan_id: planId
, (newTc) -> # success
$scope.trainingCredits.push(newTc)
$scope.trainingCreditsGroups[newTc.plan_id].push(newTc.creditable_id)
, (error) -> # failed
training = getTrainingFromId(newTrainingId)
growl.error(_t('an_error_occurred_while_creating_credit_with_the_TRAINING', {TRAINING: training.name}))
console.error(error)
##
# Cancel the current training credit modification
# @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
##
$scope.cancelTrainingCredit = (rowform) ->
rowform.$cancel()
##
# Create a new empty entry in the $scope.machineCredits array
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.addMachineCredit = (e)->
e.preventDefault()
e.stopPropagation()
$scope.inserted =
creditable_type: 'Machine'
$scope.machineCredits.push($scope.inserted)
$scope.status.isopen = !$scope.status.isopen
##
# In the Credits tab, while editing a machine credit row, select the current machine from the
# drop-down list of machines as the current item.
# @param credit {Object} credit object, inherited from $resource
##
$scope.showCreditableName = (credit) ->
selected = _t('not_set')
if credit and credit.creditable_id
angular.forEach $scope.machines, (m)->
if m.id == credit.creditable_id
selected = m.name+' ( id. '+m.id+' )'
return selected
##
# Validation callback when editing machine's credits. Save the changes.
# This will prevent the creation of two credits associating the same machine and plan.
# @param data {Object} machine, associated plan and number of credit hours.
# @param [id] {number} credit id for edition, create a new credit object if not provided
##
$scope.saveMachineCredit = (data, id) ->
for mc in $scope.machineCredits
if mc.plan_id == data.plan_id and mc.creditable_id == data.creditable_id and (id == null or mc.id != id)
growl.error(_t('error_a_credit_linking_this_machine_with_that_subscription_already_exists'))
unless id
$scope.machineCredits.pop()
return false
if id?
Credit.update {id: id}, credit: data, ->
growl.success(_t('changes_have_been_successfully_saved'))
else
data.creditable_type = 'Machine'
Credit.save
credit: data
, (resp) ->
$scope.machineCredits[$scope.machineCredits.length-1].id = resp.id
growl.success(_t('credit_was_successfully_saved'))
##
# Removes the newly inserted but not saved machine credit / Cancel the current machine credit modification
# @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
# @param index {number} theme index in the $scope.machineCredits array
##
$scope.cancelMachineCredit = (rowform, index) ->
if $scope.machineCredits[index].id?
rowform.$cancel()
else
$scope.machineCredits.splice(index, 1)
##
# Deletes the machine credit at the specified index
# @param index {number} machine credit index in the $scope.machineCredits array
##
$scope.removeMachineCredit = (index) ->
Credit.delete $scope.machineCredits[index]
$scope.machineCredits.splice(index, 1)
##
# If the plan does not have a type, return a default value for display purposes
# @param type {string|undefined|null} plan's type (eg. 'partner')
# @returns {string}
##
$scope.getPlanType = (type) ->
if type == 'PartnerPlan'
return _t('partner')
else return _t('standard')
##
# Change the plans ordering criterion to the one provided
# @param orderBy {string} ordering criterion
##
$scope.setOrderPlans = (orderBy) ->
if $scope.orderPlans == orderBy
$scope.orderPlans = '-'+orderBy
else
$scope.orderPlans = orderBy
##
# Retrieve a price from prices array by a machineId and a groupId
##
$scope.findPriceBy = (prices, machineId, groupId)->
for price in prices
if price.priceable_id == machineId and price.group_id == groupId
return price
##
# update a price for a machine and a group, not considering any plan
##
$scope.updatePrice = (data, price)->
if data?
Price.update({ id: price.id }, { price: { amount: data } }).$promise
else
_t('please_specify_a_number')
##
# Delete the specified subcription plan
# @param id {number} plan id
##
$scope.deletePlan = (plans, id) ->
if typeof id != 'number'
console.error('[editPricingController::deletePlan] Error: invalid id parameter')
else
# open a confirmation dialog
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_delete_this_subscription_plan')
, ->
# the admin has confirmed, delete the plan
Plan.delete {id: id}, (res) ->
growl.success(_t('subscription_plan_was_successfully_deleted'))
$scope.plans.splice(findPlanIdxById(plans, id), 1)
, (error) ->
console.error('[editPricingController::deletePlan] Error: '+error.statusText) if error.statusText
growl.error(_t('unable_to_delete_the_specified_subscription_an_error_occurred'))
##
# Generate a string identifying the given plan by literal humain-readable name
# @param plan {Object} Plan object, as recovered from GET /api/plan/:id
# @param groups {Array} List of Groups objects, as recovered from GET /api/groups
# @param short {boolean} If true, the generated name will contains the group slug, otherwise the group full name
# will be included.
# @returns {String}
##
$scope.humanReadablePlanName = (plan, groups, short)->
"#{$filter('humanReadablePlanName')(plan, groups, short)}"
### PRIVATE SCOPE ###
findPlanIdxById = (plans, id)->
(plans.map (plan)->
plan.id
).indexOf(id)
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
Credit.query({creditable_type: 'Training'}).$promise.then (data)->
$scope.trainingCredits = data
$scope.trainingCreditsGroups = groupCreditsByPlan(data)
## adds empty array for plan which hasn't any credits yet
for plan in $scope.plans
unless $scope.trainingCreditsGroups[plan.id]?
$scope.trainingCreditsGroups[plan.id] = []
Credit.query({creditable_type: 'Machine'}).$promise.then (data)->
$scope.machineCredits = data
Training.query().$promise.then (data)->
$scope.trainings = data
Machine.query().$promise.then (data)->
$scope.machines = data
##
# Group the given credits array into a map associating the plan ID with its associated trainings/machines
# @return {Object} the association map
##
groupCreditsByPlan = (credits) ->
creditsMap = {}
angular.forEach credits, (c) ->
unless creditsMap[c.plan_id]
creditsMap[c.plan_id] = []
creditsMap[c.plan_id].push(c.creditable_id)
creditsMap
##
# Iterate through $scope.traininfCredits to find the credit matching the given criterion
# @param trainingId {number|string} training ID
# @param planId {number|string} plan ID
##
findTrainingCredit = (trainingId, planId) ->
trainingId = parseInt(trainingId)
planId = parseInt(planId)
for credit in $scope.trainingCredits
if credit.plan_id == planId and credit.creditable_id == trainingId
return credit
##
# Retrieve a training from its given identifier and returns it
# @param id {number} training ID
# @returns {Object} Training inherited from $resource
##
getTrainingFromId = (id) ->
for training in $scope.trainings
if training.id == parseInt(id)
return training
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]

View File

@ -1,17 +1,16 @@
'use strict' 'use strict'
Application.Controllers.controller "projectElementsController", ["$scope", "$state", 'Component', 'Licence', 'Theme', ($scope, $state, Component, Licence, Theme) -> Application.Controllers.controller "ProjectElementsController", ["$scope", "$state", 'Component', 'Licence', 'Theme', 'componentsPromise', 'licencesPromise', 'themesPromise'
, ($scope, $state, Component, Licence, Theme, componentsPromise, licencesPromise, themesPromise) ->
## Materials list (plastic, wood ...) ## Materials list (plastic, wood ...)
$scope.components = Component.query() $scope.components = componentsPromise
## Licences list (Creative Common ...) ## Licences list (Creative Common ...)
$scope.licences = Licence.query() $scope.licences = licencesPromise
## Themes list (cooking, sport ...) ## Themes list (cooking, sport ...)
$scope.themes = Theme.query() $scope.themes = themesPromise
## ##
# Saves a new component / Update an existing material to the server (form validation callback) # Saves a new component / Update an existing material to the server (form validation callback)
@ -153,5 +152,3 @@ Application.Controllers.controller "projectElementsController", ["$scope", "$sta
else else
$scope.licences.splice(index, 1) $scope.licences.splice(index, 1)
] ]

View File

@ -0,0 +1,196 @@
'use strict'
Application.Controllers.controller "SettingsController", ["$scope", 'Setting', 'growl', 'settingsPromise', 'cgvFile', 'cguFile', 'logoFile', 'logoBlackFile', 'faviconFile', 'CSRF', '_t'
($scope, Setting, growl, settingsPromise, cgvFile, cguFile, logoFile, logoBlackFile, faviconFile, CSRF, _t) ->
### PUBLIC SCOPE ###
## timepickers steps configuration
$scope.timepicker =
hstep: 1
mstep: 15
## API URL where the upload forms will be posted
$scope.actionUrl =
cgu: "/api/custom_assets"
cgv: "/api/custom_assets"
logo: "/api/custom_assets"
logoBlack: "/api/custom_assets"
favicon: "/api/custom_assets"
## Form actions on the above URL
$scope.methods =
cgu: "post"
cgv: "post"
logo: "post"
logoBlack: "post"
favicon: "post"
## Are we uploading the files currently (if so, display the loader)
$scope.loader =
cgu: false
cgv: false
## various parametrable settings
$scope.twitterSetting = { name: 'twitter_name', value: settingsPromise.twitter_name }
$scope.aboutTitleSetting = { name: 'about_title', value: settingsPromise.about_title }
$scope.aboutBodySetting = { name: 'about_body', value: settingsPromise.about_body }
$scope.aboutContactsSetting = { name: 'about_contacts', value: settingsPromise.about_contacts }
$scope.homeBlogpostSetting = { name: 'home_blogpost', value: settingsPromise.home_blogpost }
$scope.machineExplicationsAlert = { name: 'machine_explications_alert', value: settingsPromise.machine_explications_alert }
$scope.trainingExplicationsAlert = { name: 'training_explications_alert', value: settingsPromise.training_explications_alert }
$scope.trainingInformationMessage = { name: 'training_information_message', value: settingsPromise.training_information_message}
$scope.subscriptionExplicationsAlert = { name: 'subscription_explications_alert', value: settingsPromise.subscription_explications_alert }
$scope.eventReducedAmountAlert = { name: 'event_reduced_amount_alert', value: settingsPromise.event_reduced_amount_alert }
$scope.windowStart = { name: 'booking_window_start', value: settingsPromise.booking_window_start }
$scope.windowEnd = { name: 'booking_window_end', value: settingsPromise.booking_window_end }
$scope.mainColorSetting = { name: 'main_color', value: settingsPromise.main_color }
$scope.secondColorSetting = { name: 'secondary_color', value: settingsPromise.secondary_color }
$scope.fablabName = { name: 'fablab_name', value: settingsPromise.fablab_name }
$scope.nameGenre = { name: 'name_genre', value: settingsPromise.name_genre }
$scope.cguFile = cguFile.custom_asset
$scope.cgvFile = cgvFile.custom_asset
$scope.customLogo = logoFile.custom_asset
$scope.customLogoBlack = logoBlackFile.custom_asset
$scope.customFavicon = faviconFile.custom_asset
$scope.enableMove =
name: 'booking_move_enable'
value: (settingsPromise.booking_move_enable == "true")
$scope.moveDelay =
name: 'booking_move_delay'
value: parseInt(settingsPromise.booking_move_delay)
$scope.enableCancel =
name: 'booking_cancel_enable'
value: (settingsPromise.booking_cancel_enable == "true")
$scope.cancelDelay =
name: 'booking_cancel_delay'
value: parseInt(settingsPromise.booking_cancel_delay)
##
# For use with 'ng-class', returns the CSS class name for the uploads previews.
# The preview may show a placeholder or the content of the file depending on the upload state.
# @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules)
##
$scope.fileinputClass = (v)->
if v
'fileinput-exists'
else
'fileinput-new'
##
# Callback to save the setting value to the database
# @param setting {{value:*, name:string}} note that the value will be stringified
##
$scope.save = (setting)->
# trim empty html
if setting.value == "<br>" or setting.value == "<p><br></p>"
setting.value = ""
# convert dates to ISO format
if setting.value instanceof Date
setting.value = setting.value.toISOString()
if setting.value isnt null
value = setting.value.toString()
else
value = setting.value
Setting.update { name: setting.name }, { value: value }, (data)->
growl.success(_t('customization_of_SETTING_successfully_saved', {SETTING:setting.name}))
, (error)->
console.log(error)
##
# For use with ngUpload (https://github.com/twilson63/ngUpload).
# Intended to be the callback when the upload is done: Any raised error will be displayed in a growl
# message. If everything goes fine, a growl success message is shown.
# @param content {Object} JSON - The upload's result
##
$scope.submited = (content) ->
if !content.custom_asset?
$scope.alerts = []
angular.forEach content, (v, k)->
angular.forEach v, (err)->
growl.error(err)
else
growl.success(_t('file_successfully_updated'))
if content.custom_asset.name is 'cgu-file'
$scope.cguFile = content.custom_asset
$scope.methods.cgu = 'put'
$scope.actionUrl.cgu += '/cgu-file' unless $scope.actionUrl.cgu.indexOf('/cgu-file') > 0
$scope.loader.cgu = false
else if content.custom_asset.name is 'cgv-file'
$scope.cgvFile = content.custom_asset
$scope.methods.cgv = 'put'
$scope.actionUrl.cgv += '/cgv-file' unless $scope.actionUrl.cgv.indexOf('/cgv-file') > 0
$scope.loader.cgv = false
else if content.custom_asset.name is 'logo-file'
$scope.logoFile = content.custom_asset
$scope.methods.logo = 'put'
$scope.actionUrl.logo += '/logo-file' unless $scope.actionUrl.logo.indexOf('/logo-file') > 0
else if content.custom_asset.name is 'logo-black-file'
$scope.logoBlackFile = content.custom_asset
$scope.methods.logoBlack = 'put'
$scope.actionUrl.logoBlack += '/logo-black-file' unless $scope.actionUrl.logoBlack.indexOf('/logo-black-file') > 0
else if content.custom_asset.name is 'favicon-file'
$scope.faviconFile = content.custom_asset
$scope.methods.favicon = 'put'
$scope.actionUrl.favicon += '/favicon-file' unless $scope.actionUrl.favicon.indexOf('/favicon-file') > 0
##
# @param target {String} 'cgu' | 'cgv'
##
$scope.addLoader = (target) ->
$scope.loader[target] = true
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
# set the authenticity tokens in the forms
CSRF.setMetaTags()
# we prevent the admin from setting the closing time before the opening time
$scope.$watch 'windowEnd.value', (newValue, oldValue, scope) ->
if $scope.windowStart and moment($scope.windowStart.value).isAfter(newValue)
$scope.windowEnd.value = oldValue
# change form methods to PUT if items already exists
if cguFile.custom_asset
$scope.methods.cgu = 'put'
$scope.actionUrl.cgu += '/cgu-file'
if cgvFile.custom_asset
$scope.methods.cgv = 'put'
$scope.actionUrl.cgv += '/cgv-file'
if logoFile.custom_asset
$scope.methods.logo = 'put'
$scope.actionUrl.logo += '/logo-file'
if logoBlackFile.custom_asset
$scope.methods.logoBlack = 'put'
$scope.actionUrl.logoBlack += '/logo-black-file'
if faviconFile.custom_asset
$scope.methods.favicon = 'put'
$scope.actionUrl.favicon += '/favicon-file'
# init the controller (call at the end !)
initialize()
]

View File

@ -0,0 +1,453 @@
'use strict'
Application.Controllers.controller "StatisticsController", ["$scope", "$state", "$rootScope", "$locale", "Statistics", "es", "Member", '_t'
, ($scope, $state, $rootScope, $locale, Statistics, es, Member, _t) ->
### PUBLIC SCOPE ###
## ui-view transitions optimization: if true, the stats will never be refreshed
$scope.preventRefresh = false
## statistics structure in elasticSearch
$scope.statistics = []
## fablab users list
$scope.members = []
## statistics data recovered from elasticSearch
$scope.data = null
## configuration of the widget allowing to pick the ages range
$scope.agePicker =
show: false
start: null
end: null
## total CA for the current view
$scope.sumCA = 0
## average users' age for the current view
$scope.averageAge = 0
## total of the stat field for non simple types
$scope.sumStat = 0
## default: results are not sorted
$scope.sorting =
ca: 'none'
## active tab will be set here
$scope.selectedIndex = null
## type filter binding
$scope.type =
selected: null
active: null
## selected custom filter
$scope.customFilter =
show: false
criterion: {}
value : null
exclude: false
datePicker:
format: $locale.DATETIME_FORMATS.shortDate
opened: false # default: datePicker is not shown
minDate: null
maxDate: moment().toDate()
options:
startingDay: 1 # France: the week starts on monday
## available custom filters
$scope.filters = []
## default: we do not open the datepicker menu
$scope.datePicker =
show: false
## datePicker parameters for interval beginning
$scope.datePickerStart =
format: $locale.DATETIME_FORMATS.shortDate
opened: false # default: datePicker is not shown
minDate: null
maxDate: moment().subtract(1, 'day').toDate()
selected: moment().utc().subtract(1, 'months').subtract(1, 'day').startOf('day').toDate()
options:
startingDay: Fablab.weekStartingDay
## datePicker parameters for interval ending
$scope.datePickerEnd =
format: $locale.DATETIME_FORMATS.shortDate
opened: false # default: datePicker is not shown
minDate: null
maxDate: moment().subtract(1, 'day').toDate()
selected: moment().subtract(1, 'day').endOf('day').toDate()
options:
startingDay: Fablab.weekStartingDay
##
# Callback to open the datepicker (interval start)
# @param $event {Object} jQuery event object
##
$scope.toggleStartDatePicker = ($event) ->
toggleDatePicker($event, $scope.datePickerStart)
##
# Callback to open the datepicker (interval end)
# @param $event {Object} jQuery event object
##
$scope.toggleEndDatePicker = ($event) ->
toggleDatePicker($event, $scope.datePickerEnd)
##
# Callback to open the datepicker (custom filter)
# @param $event {Object} jQuery event object
##
$scope.toggleCustomDatePicker = ($event) ->
toggleDatePicker($event, $scope.customFilter.datePicker)
##
# Callback called when the active tab is changed.
# recover the current tab and store its value in $scope.selectedIndex
# @param tab {Object} elasticsearch statistic structure
##
$scope.setActiveTab = (tab) ->
$scope.selectedIndex = tab
$scope.type.selected = tab.types[0]
$scope.type.active = $scope.type.selected
$scope.customFilter.criterion = {}
$scope.customFilter.value = null
$scope.customFilter.exclude = false
$scope.sorting.ca = 'none'
buildCustomFiltersList()
refreshStats()
##
# Callback to validate the filters and send a new request to elastic
##
$scope.validateFilterChange = ->
$scope.agePicker.show = false
$scope.customFilter.show = false
$scope.type.active = $scope.type.selected
buildCustomFiltersList()
refreshStats()
##
# Callback to validate the dates range and refresh the data from elastic
##
$scope.validateDateChange = ->
$scope.datePicker.show = false
refreshStats()
##
# Parse the given date and return a user-friendly string
# @param date {Date} JS date or ant moment.js compatible date string
##
$scope.formatDate = (date) ->
moment(date).format("LL")
##
# Parse the sex and return a user-friendly string
# @param sex {string} 'male' | 'female'
##
$scope.formatSex = (sex) ->
if sex == 'male'
return _t('man')
if sex == 'female'
return t('woman')
##
# Retrieve the label for the given subtype in the current type
# @param key {string} statistic subtype key
##
$scope.formatSubtype = (key) ->
label = ""
angular.forEach $scope.type.active.subtypes, (subtype) ->
if subtype.key == key
label = subtype.label
label
##
# Helper usable in ng-switch to determine the input type to display for custom filter value
# @param filter {Object} custom filter criterion
##
$scope.getCustomValueInputType = (filter) ->
if filter and filter.values
if typeof(filter.values[0]) == 'string'
return filter.values[0]
else if typeof(filter.values[0] == 'object')
return 'input_select'
else
'input_text'
##
# Change the sorting order and refresh the results to match the new order
# @param filter {Object} any filter
##
$scope.toggleSorting = (filter) ->
switch $scope.sorting[filter]
when 'none' then $scope.sorting[filter] = 'asc'
when 'asc' then $scope.sorting[filter] = 'desc'
when 'desc' then $scope.sorting[filter] = 'none'
refreshStats()
##
# Return the user's name from his given ID
# @param id {number} user ID
##
$scope.getUserNameFromId = (id) ->
if $scope.members.length == 0
return "ID "+id
else
for member in $scope.members
if member.id == id
return member.name
return "ID "+id
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
Statistics.query (stats) ->
$scope.statistics = stats
Member.query (members) ->
$scope.members = members
# workaround for angular-bootstrap::tabs behavior: on tab deletion, another tab will be selected
# which will cause every tabs to reload, one by one, when the view is closed
$rootScope.$on '$stateChangeStart', (event, toState, toParams, fromState, fromParams) ->
if fromState.name == 'app.admin.statistics' and Object.keys(fromParams).length == 0
$scope.preventRefresh = true
##
# Generic function to toggle a bootstrap datePicker
# @param $event {Object} jQuery event object
# @param datePicker {Object} settings object of the concerned datepicker. Must have an 'opened' property
##
toggleDatePicker = ($event, datePicker) ->
$event.preventDefault()
$event.stopPropagation()
datePicker.opened = !datePicker.opened
##
# Force update the statistics table, querying elasticSearch according to the current config values
##
refreshStats = ->
if $scope.selectedIndex and !$scope.preventRefresh
$scope.data = []
$scope.sumCA = 0
$scope.averageAge = 0
$scope.sumStat = 0
custom = null
if $scope.customFilter.criterion and $scope.customFilter.criterion.key and $scope.customFilter.value
custom = {}
custom.key = $scope.customFilter.criterion.key
custom.value = $scope.customFilter.value
custom.exclude = $scope.customFilter.exclude
queryElasticStats $scope.selectedIndex.es_type_key, $scope.type.active.key, custom, (res, err)->
if (err)
console.error("[statisticsController::refreshStats] Unable to refresh due to "+err)
else
$scope.data = res.hits
sumCA = 0
sumAge = 0
sumStat = 0
if $scope.data.length > 0
angular.forEach $scope.data, (datum) ->
if datum._source.ca
sumCA += parseInt(datum._source.ca)
if datum._source.age
sumAge += parseInt(datum._source.age)
if datum._source.stat
sumStat += parseInt(datum._source.stat)
sumAge /= $scope.data.length
$scope.sumCA = sumCA
$scope.averageAge = Math.round(sumAge*100)/100
$scope.sumStat = sumStat
##
# Run the elasticSearch query to retreive the /stats/type aggregations
# @param index {String} elasticSearch document type (account|event|machine|project|subscription|training)
# @param type {String} statistics type (month|year|booking|hour|user|project)
# @param custom {{key:{string}, value:{string}}|null} custom filter property or null to disable this filter
# @param callback {function} function be to run after results were retrieved, it will receive
# two parameters : results {Array}, error {String} (if any)
##
queryElasticStats = (index, type, custom, callback) ->
# handle invalid callback
if typeof(callback) != "function"
console.error('[statisticsController::queryElasticStats] Error: invalid callback provided')
return
# run query
es.search
"index": "stats"
"type": index
"size": 1000000000
"body": buildElasticDataQuery(type, custom, $scope.agePicker.start, $scope.agePicker.end, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected), $scope.sorting)
, (error, response) ->
if (error)
callback([], "Error: something unexpected occurred during elasticSearch query: "+error)
else
callback(response.hits)
##
# Build an object representing the content of the REST-JSON query to elasticSearch,
# based on the provided parameters for row data recovering.
# @param type {String} statistics type (month|year|booking|hour|user|project)
# @param custom {{key:{string}, value:{string}}|null} custom filter property or null to disable this filter
# @param ageMin {Number|null} filter by age: range lower value OR null to do not filter
# @param ageMax {Number|null} filter by age: range higher value OR null to do not filter
# @param intervalBegin {moment} statitics interval beginning (moment.js type)
# @param intervalEnd {moment} statitics interval ending (moment.js type)
# @param sortings {Array|null} elasticSearch criteria for sorting the results
##
buildElasticDataQuery = (type, custom, ageMin, ageMax, intervalBegin, intervalEnd, sortings) ->
q =
"query":
"bool":
"must": [
{
"term":
"type": type
}
{
"range":
"date":
"gte": intervalBegin.format()
"lte": intervalEnd.format()
}
]
# optional date range
if ageMin && ageMax
q.query.bool.must.push
"range":
"age":
"gte": ageMin
"lte": ageMax
# optional criterion
if custom
criterion = {
"match" : {}
}
switch $scope.getCustomValueInputType($scope.customFilter.criterion)
when 'input_date' then criterion.match[custom.key] = moment(custom.value).format('YYYY-MM-DD')
when 'input_select' then criterion.match[custom.key] = custom.value.key
when 'input_list' then criterion.match[custom.key+".name"] = custom.value
else criterion.match[custom.key] = custom.value
if (custom.exclude)
q = "query": {
"filtered": {
"query": q.query,
"filter": {
"not": {
"term": criterion.match
}
}
}
}
else
q.query.bool.must.push(criterion)
if sortings
q["sort"] = buildElasticSortCriteria(sortings)
q
##
# Parse the provided criteria array and return the corresponding elasticSearch syntax
# @param criteria {Array} array of {key_to_sort:order}
##
buildElasticSortCriteria = (criteria) ->
crits = []
angular.forEach criteria, (value, key) ->
if typeof value != 'undefined' and value != null and value != 'none'
c = {}
c[key] = {'order': value}
crits.push(c)
crits
##
# Fullfil the list of available options in the custom filter panel. The list will be based on common
# properties and on index-specific properties (additional_fields)
##
buildCustomFiltersList = ->
$scope.filters = []
filters = [
{key: 'date', label: _t('date'), values: ['input_date']},
{key: 'userId', label: _t('user_id'), values: ['input_number']},
{key: 'gender', label: _t('gender'), values: [{key:'male', label:_t('man')}, {key:'female', label:_t('woman')}]},
{key: 'age', label: _t('age'), values: ['input_number']},
{key: 'subType', label: _t('type'), values: $scope.type.active.subtypes},
{key: 'ca', label: _t('revenue'), values: ['input_number']}
]
$scope.filters = filters
if !$scope.type.active.simple
f = {key: 'stat', label: $scope.type.active.label, values: ['input_number']}
$scope.filters.push(f)
angular.forEach $scope.selectedIndex.additional_fields, (field) ->
filter = {key: field.key, label: field.label, values:[]}
switch field.data_type
when 'index' then filter.values.push('input_number')
when 'number' then filter.values.push('input_number')
when 'date' then filter.values.push('input_date')
when 'list' then filter.values.push('input_list')
else filter.values.push('input_text')
$scope.filters.push(filter)
# init the controller (call at the end !)
initialize()
]

View File

@ -0,0 +1,65 @@
Application.Controllers.controller "TagsController", ["$scope", 'tagsPromise', 'Tag', 'growl', '_t', ($scope, tagsPromise, Tag, growl, _t) ->
## List of users's tags
$scope.tags = tagsPromise
##
# Removes the newly inserted but not saved tag / Cancel the current tag modification
# @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
# @param index {number} tag index in the $scope.tags array
##
$scope.cancelTag = (rowform, index) ->
if $scope.tags[index].id?
rowform.$cancel()
else
$scope.tags.splice(index, 1)
##
# Creates a new empty entry in the $scope.tags array
##
$scope.addTag = ->
$scope.inserted =
name: ''
$scope.tags.push($scope.inserted)
##
# Saves a new tag / Update an existing tag to the server (form validation callback)
# @param data {Object} tag name
# @param [data] {number} tag id, in case of update
##
$scope.saveTag = (data, id) ->
if id?
Tag.update {id: id}, { tag: data }, (response) ->
growl.success(_t('changes_successfully_saved'))
, (error) ->
growl.error(_t('an_error_occurred_while_saving_changes'))
else
Tag.save { tag: data }, (resp)->
growl.success(_t('new_tag_successfully_saved'))
$scope.tags[$scope.tags.length-1].id = resp.id
, (error) ->
growl.error(_t('an_error_occurred_while_saving_the_new_tag'))
$scope.tags.splice($scope.tags.length-1, 1)
##
# Deletes the tag at the specified index
# @param index {number} tag index in the $scope.tags array
##
$scope.removeTag = (index) ->
# TODO add confirmation : les utilisateurs seront déasociés
Tag.delete { id: $scope.tags[index].id }, (resp) ->
growl.success(_t('tag_successfully_deleted'))
$scope.tags.splice(index, 1)
, (error) ->
growl.error(_t('an_error_occurred_and_the_tag_deletion_failed'))
]

View File

@ -0,0 +1,230 @@
'use strict'
Application.Controllers.controller "TrainingsController", ["$scope", "$state", "$uibModal", 'Training', 'trainingsPromise', 'machinesPromise', '_t', 'growl'
, ($scope, $state, $uibModal, Training, trainingsPromise, machinesPromise, _t, growl) ->
### PUBLIC SCOPE ###
## list of trainings
$scope.trainings = trainingsPromise
## simplified list of machines
$scope.machines = machinesPromise
## list of training availabilies, grouped by date
$scope.groupedAvailabilities = {}
## default: accordions are not open
$scope.accordions = {}
## Binding for the parseInt function
$scope.parseInt = parseInt
##
# In the trainings listing tab, return the stringified list of machines associated with the provided training
# @param training {Object} Training object, inherited from $resource
# @returns {string}
##
$scope.showMachines = (training) ->
selected = []
angular.forEach $scope.machines, (m) ->
if (training.machine_ids.indexOf(m.id) >= 0)
selected.push(m.name)
return if selected.length then selected.join(', ') else _t('none')
##
# Create a new empty training object and append it to the $scope.trainings list
##
$scope.addTraining = ->
$scope.inserted =
name: ''
machine_ids: []
$scope.trainings.push($scope.inserted)
##
# Saves a new training / Update an existing training to the server (form validation callback)
# @param data {Object} training name, associated machine(s) and default places number
# @param id {number} training id, in case of update
##
$scope.saveTraining = (data, id) ->
if id?
Training.update {id: id},
training: data
else
Training.save
training: data
, (resp) ->
$scope.trainings[$scope.trainings.length-1] = resp
console.log(resp)
##
# Removes the newly inserted but not saved training / Cancel the current training modification
# @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
# @param index {number} training index in the $scope.trainings array
##
$scope.cancelTraining = (rowform, index) ->
if $scope.trainings[index].id?
rowform.$cancel()
else
$scope.trainings.splice(index, 1)
##
# In the trainings monitoring tab, callback to open a modal window displaying the current bookings for the
# provided training slot. The admin will be then able to validate the training for the users that followed
# the training.
# @param training {Object} Training object, inherited from $resource
# @param availability {Object} time slot when the training occurs
##
$scope.showReservations = (training, availability) ->
$uibModal.open
templateUrl: '<%= asset_path "admin/trainings/validTrainingModal.html" %>'
controller: ['$scope', '$uibModalInstance', ($scope, $uibModalInstance) ->
$scope.availability = availability
$scope.usersToValid = []
##
# Mark/unmark the provided user for training validation
# @param user {Object} from the availability.reservation_users list
##
$scope.toggleSelection = (user) ->
index = $scope.usersToValid.indexOf(user)
if index > -1
$scope.usersToValid.splice(index, 1)
else
$scope.usersToValid.push user
##
# Validates the modifications (training validations) and save them to the server
##
$scope.ok = ->
users = $scope.usersToValid.map (u) ->
u.id
Training.update {id: training.id},
training:
users: users
, -> # success
angular.forEach $scope.usersToValid, (u) ->
u.is_valid = true
$scope.usersToValid = []
$uibModalInstance.close(training)
##
# Cancel the modifications and close the modal window
##
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
]
##
# Delete the provided training and, in case of sucess, remove it from the trainings list afterwards
# @param index {number} index of the provided training in $scope.trainings
# @param training {Object} training to delete
##
$scope.removeTraining = (index, training)->
training.$delete ->
$scope.trainings.splice(index, 1)
growl.info(_t('training_successfully_deleted'))
, (error)->
growl.warning(_t('unable_to_delete_the_training_because_some_users_alredy_booked_it'))
##
# Open the modal to edit description of the training
# @param training {Object} Training to edit description
##
$scope.openModalToSetDescription = (training)->
$uibModal.open(
templateUrl: "<%= asset_path 'admin/trainings/modal_edit.html' %>"
controller: ['$scope', '$uibModalInstance', 'Training', 'growl', ($scope, $uibModalInstance, Training, growl)->
$scope.training = training
$scope.save = ->
Training.update id: training.id, { training: { description: $scope.training.description } }, (training)->
$uibModalInstance.close()
growl.success(_t('description_was_successfully_saved'))
return
]
)
##
# Takes a month number and return its localized literal name
# @param {Number} from 0 to 11
# @returns {String} eg. 'janvier'
##
$scope.formatMonth = (number) ->
number = parseInt(number)
moment().month(number).format('MMMM')
##
# Given a day, month and year, return a localized literal name for the day
# @param day {Number} from 1 to 31
# @param month {Number} from 0 to 11
# @param year {Number} Gregorian's year number
# @returns {String} eg. 'mercredi 12'
##
$scope.formatDay = (day, month, year) ->
day = parseInt(day)
month = parseInt(month)
year = parseInt(year)
moment({year: year, month:month, day:day}).format('dddd D')
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
$scope.groupedAvailabilities = groupAvailabilities($scope.trainings)
##
# Group the trainings availabilites by trainings and by dates and return the resulting tree
# @param trainings {Array} $scope.trainings is expected here
# @returns {Object} Tree constructed as /training_name/year/month/day/[availabilities]
##
groupAvailabilities = (trainings) ->
tree = {}
for training in trainings
tree[training.name] = {}
tree[training.name].training = training
for availability in training.availabilities
start = moment(availability.start_at)
# init the tree structure
if typeof tree[training.name][start.year()] == 'undefined'
tree[training.name][start.year()] = {}
if typeof tree[training.name][start.year()][start.month()] == 'undefined'
tree[training.name][start.year()][start.month()] = {}
if typeof tree[training.name][start.year()][start.month()][start.date()] == 'undefined'
tree[training.name][start.year()][start.month()][start.date()] = []
# add the availability at its right place
tree[training.name][start.year()][start.month()][start.date()].push( availability )
tree
# init the controller (call at the end !)
initialize()
]

View File

@ -1,6 +1,7 @@
'use strict' 'use strict'
Application.Controllers.controller 'ApplicationController', ["$rootScope", "$scope", "Session", "AuthService", "Auth", "$modal", "$state", 'growl', 'Notification', '$interval', ($rootScope, $scope, Session, AuthService, Auth, $modal, $state, growl, Notification, $interval) -> Application.Controllers.controller 'ApplicationController', ["$rootScope", "$scope", "$window", "Session", "AuthService", "Auth", "$uibModal", "$state", 'growl', 'Notification', '$interval', "Setting", '$locale', '_t'
, ($rootScope, $scope, $window, Session, AuthService, Auth, $uibModal, $state, growl, Notification, $interval, Setting, $locale, _t) ->
@ -18,14 +19,14 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
# @param user {Object} Rails/Devise user # @param user {Object} Rails/Devise user
## ##
$scope.setCurrentUser = (user) -> $scope.setCurrentUser = (user) ->
$scope.currentUser = user $rootScope.currentUser = user
Session.create(user); Session.create(user);
getNotifications() getNotifications()
## ##
# Login callback # Login callback
# @param e {Object} jQuery event # @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
# @param callback {function} # @param callback {function}
## ##
$scope.login = (e, callback) -> $scope.login = (e, callback) ->
@ -36,14 +37,14 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
## ##
# Logout callback # Logout callback
# @param e {Object} jQuery event # @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
## ##
$scope.logout = (e) -> $scope.logout = (e) ->
e.preventDefault() e.preventDefault()
Auth.logout().then (oldUser) -> Auth.logout().then (oldUser) ->
# console.log(oldUser.name + " you're signed out now."); # console.log(oldUser.name + " you're signed out now.");
Session.destroy() Session.destroy()
$scope.currentUser = null $rootScope.currentUser = null
$rootScope.toCheckNotifications = false $rootScope.toCheckNotifications = false
$scope.notifications = [] $scope.notifications = []
$state.go('app.public.home') $state.go('app.public.home')
@ -54,21 +55,21 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
## ##
# Open the modal window allowing the user to create an account. # Open the modal window allowing the user to create an account.
# @param e {Object} jQuery event # @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
## ##
$scope.signup = (e) -> $scope.signup = (e) ->
e.preventDefault() if e e.preventDefault() if e
$modal.open $uibModal.open
templateUrl: '<%= asset_path "shared/signupModal.html" %>' templateUrl: '<%= asset_path "shared/signupModal.html" %>'
size: 'md' size: 'md'
controller: ['$scope', '$modalInstance', 'Group', ($scope, $modalInstance, Group) -> controller: ['$scope', '$uibModalInstance', 'Group', 'CustomAsset', ($scope, $uibModalInstance, Group, CustomAsset) ->
# default parameters for the date picker in the account creation modal # default parameters for the date picker in the account creation modal
$scope.datePicker = $scope.datePicker =
format: 'dd/MM/yyyy' format: $locale.DATETIME_FORMATS.shortDate
opened: false opened: false
options: options:
startingDay: 1 startingDay: Fablab.weekStartingDay
# callback to open the date picker (account creation modal) # callback to open the date picker (account creation modal)
$scope.openDatePicker = ($event) -> $scope.openDatePicker = ($event) ->
@ -80,6 +81,10 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
Group.query (groups) -> Group.query (groups) ->
$scope.groups = groups $scope.groups = groups
# retrieve the CGU
CustomAsset.get {name: 'cgu-file'}, (cgu) ->
$scope.cgu = cgu.custom_asset
# default user's parameters # default user's parameters
$scope.user = $scope.user =
is_allow_contact: true is_allow_contact: true
@ -95,7 +100,7 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
$scope.alerts = [] $scope.alerts = []
Auth.register($scope.user).then (user) -> Auth.register($scope.user).then (user) ->
# creation successful # creation successful
$modalInstance.close(user) $uibModalInstance.close(user)
, (error) -> , (error) ->
# creation failed... # creation failed...
angular.forEach error.data.errors, (v, k) -> angular.forEach error.data.errors, (v, k) ->
@ -114,10 +119,10 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
# @param token {string} security token for password changing. The user should have recieved it by mail # @param token {string} security token for password changing. The user should have recieved it by mail
## ##
$scope.editPassword = (token) -> $scope.editPassword = (token) ->
$modal.open $uibModal.open
templateUrl: '<%= asset_path "shared/passwordEditModal.html" %>' templateUrl: '<%= asset_path "shared/passwordEditModal.html" %>'
size: 'md' size: 'md'
controller: ['$scope', '$modalInstance', '$http', ($scope, $modalInstance, $http) -> controller: ['$scope', '$uibModalInstance', '$http', '_t', ($scope, $uibModalInstance, $http, _t) ->
$scope.user = $scope.user =
reset_password_token: token reset_password_token: token
$scope.alerts = [] $scope.alerts = []
@ -127,7 +132,7 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
$scope.changePassword = -> $scope.changePassword = ->
$scope.alerts = [] $scope.alerts = []
$http.put('/users/password.json', {user: $scope.user}).success (data) -> $http.put('/users/password.json', {user: $scope.user}).success (data) ->
$modalInstance.close() $uibModalInstance.close()
.error (data) -> .error (data) ->
angular.forEach data.errors, (v, k) -> angular.forEach data.errors, (v, k) ->
angular.forEach v, (err) -> angular.forEach v, (err) ->
@ -136,20 +141,20 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
type: 'danger' type: 'danger'
] ]
.result['finally'](null).then (user) -> .result['finally'](null).then (user) ->
growl.addInfoMessage('Votre mot de passe a bien été modifié.') growl.success(_t('your_password_was_successfully_changed'))
Auth.login().then (user) -> Auth.login().then (user) ->
$scope.setCurrentUser(user) $scope.setCurrentUser(user)
, (error) -> , (error) ->
# Authentication failed... # Authentication failed...
## ##
# Compact/Expend the width of the left navigation bar # Compact/Expend the width of the left navigation bar
# @param e {Object} jQuery event object # @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
## ##
$scope.toggleNavSize = (event) -> $scope.toggleNavSize = (event) ->
if typeof event == 'undefined' if typeof event == 'undefined'
console.error '[applicationController::toggleNavSize] Missing event parameter' console.error '[ApplicationController::toggleNavSize] Missing event parameter'
return return
toggler = $(event.target) toggler = $(event.target)
@ -184,14 +189,16 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
### PRIVATE SCOPE ### ### PRIVATE SCOPE ###
## ##
# Kind of constructor: these actions will be realized first when the controller is loaded # Kind of constructor: these actions will be realized first when the controller is loaded
## ##
initialize = -> initialize = ->
# try to retrieve any currently logged user # try to retrieve any currently logged user
Auth.login().then (user) -> Auth.login().then (user) ->
$scope.setCurrentUser(user) $scope.setCurrentUser(user)
if user.need_completion
$state.transitionTo('app.logged.profileCompletion')
, (error) -> , (error) ->
# Authentication failed... # Authentication failed...
$rootScope.toCheckNotifications = false $rootScope.toCheckNotifications = false
@ -205,11 +212,17 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
event.preventDefault() event.preventDefault()
if AuthService.isAuthenticated() if AuthService.isAuthenticated()
# user is not allowed # user is not allowed
console.log('user is not allowed') console.error('[ApplicationController::initialize] user is not allowed')
else else
# user is not logged in # user is not logged in
openLoginModal(toState, toParams) openLoginModal(toState, toParams)
Setting.get { name: 'fablab_name' }, (data)->
$scope.fablabName = data.setting.value
Setting.get { name: 'name_genre' }, (data)->
$scope.nameGenre = data.setting.value
# shorthands # shorthands
$scope.isAuthenticated = Auth.isAuthenticated; $scope.isAuthenticated = Auth.isAuthenticated;
@ -223,7 +236,7 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
## ##
getNotifications = -> getNotifications = ->
$rootScope.toCheckNotifications = true $rootScope.toCheckNotifications = true
unless $rootScope.checkNotificationsIsInit or !$scope.currentUser unless $rootScope.checkNotificationsIsInit or !$rootScope.currentUser
$scope.notifications = Notification.query {is_read: false} $scope.notifications = Notification.query {is_read: false}
$scope.$watch 'notifications', (newValue, oldValue) -> $scope.$watch 'notifications', (newValue, oldValue) ->
diff = [] diff = []
@ -239,7 +252,7 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
angular.forEach diff, (notification, key) -> angular.forEach diff, (notification, key) ->
growl.addInfoMessage(notification.message.description) growl.info(notification.message.description)
, true , true
@ -257,35 +270,39 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
# Open the modal window allowing the user to log in. # Open the modal window allowing the user to log in.
## ##
openLoginModal = (toState, toParams, callback) -> openLoginModal = (toState, toParams, callback) ->
$modal.open <% active_provider = AuthProvider.active %>
<% if active_provider.providable_type != DatabaseProvider.name %>
$window.location.href = '<%=user_omniauth_authorize_path(AuthProvider.active.strategy_name.to_sym)%>'
<% else %>
$uibModal.open
templateUrl: '<%= asset_path "shared/deviseModal.html" %>' templateUrl: '<%= asset_path "shared/deviseModal.html" %>'
size: 'sm' size: 'sm'
controller: ['$scope', '$modalInstance', ($scope, $modalInstance) -> controller: ['$scope', '$uibModalInstance', '_t', ($scope, $uibModalInstance, _t) ->
user = $scope.user = {} user = $scope.user = {}
$scope.login = () -> $scope.login = () ->
Auth.login(user).then (user) -> Auth.login(user).then (user) ->
# Authentification succeeded ... # Authentification succeeded ...
$modalInstance.close(user) $uibModalInstance.close(user)
if callback and typeof callback is "function" if callback and typeof callback is "function"
callback(user) callback(user)
, (error) -> , (error) ->
# Authentication failed... # Authentication failed...
$scope.alerts = [] $scope.alerts = []
$scope.alerts.push $scope.alerts.push
msg: 'E-mail ou mot de passe incorrect.' msg: _t('wrong_email_or_password')
type: 'danger' type: 'danger'
# handle modal behaviors. The provided reason will be used to define the following actions # handle modal behaviors. The provided reason will be used to define the following actions
$scope.dismiss = -> $scope.dismiss = ->
$modalInstance.dismiss('cancel') $uibModalInstance.dismiss('cancel')
$scope.openSignup = (e) -> $scope.openSignup = (e) ->
e.preventDefault() e.preventDefault()
$modalInstance.dismiss('signup') $uibModalInstance.dismiss('signup')
$scope.openResetPassword = (e) -> $scope.openResetPassword = (e) ->
e.preventDefault() e.preventDefault()
$modalInstance.dismiss('resetPassword') $uibModalInstance.dismiss('resetPassword')
] ]
# what to do when the modal is closed # what to do when the modal is closed
@ -303,25 +320,26 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
$scope.signup() $scope.signup()
else if reason is 'resetPassword' else if reason is 'resetPassword'
# open the 'reset password' modal # open the 'reset password' modal
$modal.open $uibModal.open
templateUrl: '<%= asset_path "shared/passwordNewModal.html" %>' templateUrl: '<%= asset_path "shared/passwordNewModal.html" %>'
size: 'sm' size: 'sm'
controller: ['$scope', '$modalInstance', '$http', ($scope, $modalInstance, $http) -> controller: ['$scope', '$uibModalInstance', '$http', ($scope, $uibModalInstance, $http) ->
$scope.user = {email: ''} $scope.user = {email: ''}
$scope.sendReset = () -> $scope.sendReset = () ->
$scope.alerts = [] $scope.alerts = []
$http.post('/users/password.json', {user: $scope.user}).success -> $http.post('/users/password.json', {user: $scope.user}).success ->
$modalInstance.close() $uibModalInstance.close()
.error -> .error ->
$scope.alerts.push $scope.alerts.push
msg: "Votre adresse email n'existe pas." msg: _t('your_email_address_is_unknown')
type: 'danger' type: 'danger'
] ]
.result['finally'](null).then -> .result['finally'](null).then ->
growl.addInfoMessage('Vous allez recevoir sous quelques minutes un e-mail vous indiquant comment réinitialiser votre mot de passe.') growl.info(_t('you_will_receive_in_a_moment_an_email_with_instructions_to_reset_your_password'))
# otherwise the user just closed the modal # otherwise the user just closed the modal
<% end %>

View File

@ -1,43 +1,7 @@
'use strict' 'use strict'
## Application.Controllers.controller "DashboardController", ["$scope", 'memberPromise', ($scope, memberPromise) ->
# Controller used on the private projects listing page (my dashboard/projects)
##
Application.Controllers.controller "dashboardProjectsController", ["$scope", 'Member', ($scope, Member) ->
## Current user's profile ## Current user's profile
$scope.user = Member.get {id: $scope.currentUser.id} $scope.user = memberPromise
]
##
# Controller used on the personal trainings page (my dashboard/trainings)
##
Application.Controllers.controller "dashboardTrainingsController", ["$scope", 'Member', ($scope, Member) ->
## Current user's profile
$scope.user = Member.get {id: $scope.currentUser.id}
]
##
# Controller used on the private events page (my dashboard/events)
##
Application.Controllers.controller "dashboardEventsController", ["$scope", 'Member', ($scope, Member) ->
## Current user's profile
$scope.user = Member.get {id: $scope.currentUser.id}
]
##
# Controller used on the personal invoices listing page (my dashboard/invoices)
##
Application.Controllers.controller "dashboardInvoicesController", ["$scope", 'Member', ($scope, Member) ->
## Current user's profile
$scope.user = Member.get {id: $scope.currentUser.id}
] ]

View File

@ -1,71 +1,72 @@
'use strict' 'use strict'
Application.Controllers.controller "eventsController", ["$scope", "$state", 'Event', ($scope, $state, Event) -> Application.Controllers.controller "EventsController", ["$scope", "$state", 'Event', ($scope, $state, Event) ->
### PRIVATE STATIC CONSTANTS ### ### PRIVATE STATIC CONSTANTS ###
# Number of events added to the page when the user clicks on 'load next events' # Number of events added to the page when the user clicks on 'load next events'
EVENTS_PER_PAGE = 12 EVENTS_PER_PAGE = 12
### PUBLIC SCOPE ### ### PUBLIC SCOPE ###
## The events displayed on the page ## The events displayed on the page
$scope.events = [] $scope.events = []
## By default, the pagination mode is activated to limit the page size ## By default, the pagination mode is activated to limit the page size
$scope.paginateActive = true $scope.paginateActive = true
## The currently displayed page number ## The currently displayed page number
$scope.page = 1 $scope.page = 1
## ##
# Adds EVENTS_PER_PAGE events to the bottom of the page, grouped by month # Adds EVENTS_PER_PAGE events to the bottom of the page, grouped by month
## ##
$scope.loadMoreEvents = -> $scope.loadMoreEvents = ->
Event.query {page: $scope.page}, (data) -> Event.query {page: $scope.page}, (data) ->
$scope.events = $scope.events.concat data $scope.events = $scope.events.concat data
if data.length > 0 if data.length > 0
$scope.paginateActive = false if ($scope.page-2)*EVENTS_PER_PAGE+data.length >= data[0].nb_total_events $scope.paginateActive = false if ($scope.page-2)*EVENTS_PER_PAGE+data.length >= data[0].nb_total_events
$scope.eventsGroupByMonth = _.groupBy($scope.events, (obj) -> $scope.eventsGroupByMonth = _.groupBy($scope.events, (obj) ->
_.map ['month', 'year'], (key, value) -> obj[key] _.map ['month', 'year'], (key, value) -> obj[key]
) )
$scope.monthOrder = _.sortBy _.keys($scope.eventsGroupByMonth), (k)-> $scope.monthOrder = _.sortBy _.keys($scope.eventsGroupByMonth), (k)->
monthYearArray = k.split(',') monthYearArray = k.split(',')
date = new Date() date = new Date()
date.setMonth(monthYearArray[0]) date.setMonth(monthYearArray[0])
date.setYear(monthYearArray[1]) date.setYear(monthYearArray[1])
return -date.getTime() return -date.getTime()
else else
$scope.paginateActive = false $scope.paginateActive = false
$scope.page += 1 $scope.page += 1
## ##
# Callback to redirect the user to the specified event page # Callback to redirect the user to the specified event page
# @param event {{id:number}} # @param event {{id:number}}
## ##
$scope.showEvent = (event) -> $scope.showEvent = (event) ->
$state.go('app.public.events_show', {id: event.id}) $state.go('app.public.events_show', {id: event.id})
### PRIVATE SCOPE ### ### PRIVATE SCOPE ###
## ##
# Kind of constructor: these actions will be realized first when the controller is loaded # Kind of constructor: these actions will be realized first when the controller is loaded
## ##
initialize = -> initialize = ->
$scope.loadMoreEvents() $scope.loadMoreEvents()
## !!! MUST BE CALLED AT THE END of the controller
initialize() ## !!! MUST BE CALLED AT THE END of the controller
initialize()
] ]
@ -74,47 +75,440 @@ Application.Controllers.controller "eventsController", ["$scope", "$state", 'Eve
Application.Controllers.controller "showEventController", ["$scope", "$state", "$stateParams", "Event", '$modal', 'Member', ($scope, $state, $stateParams, Event, $modal, Member) -> Application.Controllers.controller "ShowEventController", ["$scope", "$state", "$stateParams", "Event", '$uibModal', 'Member', 'Reservation', 'Price', 'CustomAsset', 'eventPromise', 'reducedAmountAlert', 'growl', '_t'
($scope, $state, $stateParams, Event, $uibModal, Member, Reservation, Price, CustomAsset, eventPromise, reducedAmountAlert, growl, _t) ->
### PUBLIC SCOPE ### ### PUBLIC SCOPE ###
$scope.reducedAmountAlert = reducedAmountAlert.setting.value
## current event details ## reservations for the currently shown event
$scope.event = {} $scope.reservations = []
## user to deal with
$scope.ctrl =
member: {}
## parameters for a new reservation
$scope.reserve =
nbPlaces: []
nbReducedPlaces: []
nbReservePlaces: 0
nbReserveReducedPlaces: 0
toReserve: false
amountTotal : 0
## # get the details for the current event (event's id is recovered from the current URL)
# Callback to delete the provided event (admins only) $scope.event = eventPromise
# @param event {$resource} angular's Event $resource
##
$scope.deleteEvent = (event) ->
event.$delete ->
$state.go('app.public.events_list')
### PRIVATE SCOPE ### ##
# Callback to delete the provided event (admins only)
## # @param event {$resource} angular's Event $resource
# Kind of constructor: these actions will be realized first when the controller is loaded ##
## $scope.deleteEvent = (event) ->
initialize = -> event.$delete ->
$state.go('app.public.events_list')
# get the details for the current event (event's id is recovered from the current URL)
Event.get {id: $stateParams.id}
, (data) ->
$scope.event = data
if !$scope.event.reduced_amount
$scope.event.reduced_amount = 0
return
, ->
$state.go('app.public.events_list')
## !!! MUST BE CALLED AT THE END of the controller ##
initialize() # Callback to call when the number of places change in the current booking
##
$scope.changeNbPlaces = ->
reste = $scope.event.nb_free_places - $scope.reserve.nbReservePlaces
$scope.reserve.nbReducedPlaces = [0..reste]
$scope.computeEventAmount()
##
# Callback to call when the number of discounted places change in the current booking
##
$scope.changeNbReducedPlaces = ->
reste = $scope.event.nb_free_places - $scope.reserve.nbReserveReducedPlaces
$scope.reserve.nbPlaces = [0..reste]
$scope.computeEventAmount()
##
# Callback to reset the current reservation parameters
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.cancelReserve = (e)->
e.preventDefault()
resetEventReserve()
##
# Callback to allow the user to set the details for his reservation
##
$scope.reserveEvent = ->
if $scope.event.nb_total_places > 0
$scope.reserveSuccess = false
if !$scope.isAuthenticated()
$scope.login null, (user)->
$scope.reserve.toReserve = !$scope.reserve.toReserve
if user.role isnt 'admin'
$scope.ctrl.member = user
else
Member.query (members) ->
$scope.members = members
else
$scope.reserve.toReserve = !$scope.reserve.toReserve
##
# Callback to deal with the reservations of the user selected in the dropdown list instead of the current user's
# reservations. (admins only)
##
$scope.updateMember = ->
resetEventReserve()
$scope.reserveSuccess = false
if $scope.ctrl.member
getReservations($scope.event.id, 'Event', $scope.ctrl.member.id)
##
# Callback to trigger the payment process of the current reservation
##
$scope.payEvent = ->
# first, we check that a user was selected
if Object.keys($scope.ctrl.member).length > 0
reservation = mkReservation($scope.ctrl.member, $scope.reserve, $scope.event)
if $scope.currentUser.role isnt 'admin' and $scope.reserve.amountTotal > 0
payByStripe(reservation)
else
if $scope.currentUser.role is 'admin' or $scope.reserve.amountTotal is 0
payOnSite(reservation)
else
# otherwise we alert, this error musn't occur when the current user is not admin
growl.error(_t('please_select_a_member_first'))
##
# Callback to validate the booking of a free event
##
$scope.validReserveEvent = ->
reservation =
user_id: $scope.ctrl.member.id
reservable_id: $scope.event.id
reservable_type: 'Event'
slots_attributes: []
nb_reserve_places: $scope.reserve.nbReservePlaces
nb_reserve_reduced_places: $scope.reserve.nbReserveReducedPlaces
reservation.slots_attributes.push
start_at: $scope.event.start_date
end_at: $scope.event.end_date
availability_id: $scope.event.availability.id
$scope.attempting = true
Reservation.save reservation: reservation, (reservation) ->
afterPayment(reservation)
$scope.attempting = false
, (response)->
$scope.alerts = []
$scope.alerts.push
msg: response.data.card[0]
type: 'danger'
$scope.attempting = false
##
# Callback to alter an already booked reservation date. A modal window will be opened to allow the user to choose
# a new date for his reservation (if any available)
# @param reservation {{id:number, reservable_id:number, nb_reserve_places:number, nb_reserve_reduced_places:number}}
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.modifyReservation = (reservation, e)->
e.preventDefault()
e.stopPropagation()
index = $scope.reservations.indexOf(reservation)
$uibModal.open
templateUrl: '<%= asset_path "events/modify_event_reservation_modal.html" %>'
resolve:
event: -> $scope.event
reservation: -> reservation
controller: ['$scope', '$uibModalInstance', 'event', 'reservation', 'Reservation', ($scope, $uibModalInstance, event, reservation, Reservation) ->
# we copy the controller's resolved parameters into the scope
$scope.event = event
$scope.reservation = angular.copy reservation
# set the reservable_id to the first available event
for e in event.recurrence_events
if e.nb_free_places > (reservation.nb_reserve_places + reservation.nb_reserve_reduced_places)
$scope.reservation.reservable_id = e.id
break
# Callback to validate the new reservation's date
$scope.ok = ->
eventToPlace = null
angular.forEach event.recurrence_events, (e)->
if e.id is parseInt($scope.reservation.reservable_id, 10)
eventToPlace = e
$scope.reservation.slots[0].start_at = eventToPlace.start_date
$scope.reservation.slots[0].end_at = eventToPlace.end_date
$scope.reservation.slots[0].availability_id = eventToPlace.availability_id
$scope.reservation.slots_attributes = $scope.reservation.slots
$scope.attempting = true
Reservation.update {id: reservation.id}, {reservation: $scope.reservation}, (reservation) ->
$uibModalInstance.close(reservation)
$scope.attempting = true
, (response)->
$scope.alerts = []
angular.forEach response, (v, k)->
angular.forEach v, (err)->
$scope.alerts.push({msg: k+': '+err, type: 'danger'})
$scope.attempting = false
# Callback to cancel the modification
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
]
.result['finally'](null).then (reservation)->
$scope.reservations.splice(index, 1)
$scope.event.nb_free_places = $scope.event.nb_free_places + reservation.nb_reserve_places + reservation.nb_reserve_reduced_places
angular.forEach $scope.event.recurrence_events, (e)->
if e.id is parseInt(reservation.reservable_id, 10)
e.nb_free_places = e.nb_free_places - reservation.nb_reserve_places - reservation.nb_reserve_reduced_places
##
# Checks if the provided reservation is able to be modified
# @param reservation {{nb_reserve_places:number, nb_reserve_reduced_places:number}}
##
$scope.reservationCanModify = (reservation)->
isAble = false
angular.forEach $scope.event.recurrence_events, (e)->
isAble = true if e.nb_free_places > (reservation.nb_reserve_places + reservation.nb_reserve_reduced_places)
isAble
##
# Compute the total amount for the current reservation according to the previously set parameters
# and assign the result in $scope.reserve.amountTotal
##
$scope.computeEventAmount = ->
# first we check that a user was selected
if Object.keys($scope.ctrl.member).length > 0
r = mkReservation($scope.ctrl.member, $scope.reserve, $scope.event)
Price.compute {reservation: r}, (res) ->
$scope.reserve.amountTotal = res.price
else
$scope.reserve.amountTotal = null
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
# gather the current user or the list of users if the current user is an admin
if $scope.currentUser
if $scope.currentUser.role isnt 'admin'
$scope.ctrl.member = $scope.currentUser
else
Member.query (members) ->
$scope.members = members
# check that the event's reduced rate is initialized
if !$scope.event.reduced_amount
$scope.event.reduced_amount = 0
# initialize the "reserve" object with the event's data
$scope.reserve.nbPlaces = [0..$scope.event.nb_free_places]
$scope.reserve.nbReducedPlaces = [0..$scope.event.nb_free_places]
# if non-admin, get the current user's reservations into $scope.reservations
if $scope.currentUser
getReservations($scope.event.id, 'Event', $scope.currentUser.id)
##
# Retrieve the reservations for the couple event / user
# @param reservable_id {number} the current event id
# @param reservable_type {string} 'Event'
# @param user_id {number} the user's id (current or managed)
##
getReservations = (reservable_id, reservable_type, user_id)->
Reservation.query(reservable_id: reservable_id, reservable_type: reservable_type, user_id: user_id).$promise.then (reservations)->
$scope.reservations = reservations
##
# Create an hash map implementing the Reservation specs
# @param member {Object} User as retreived from the API: current user / selected user if current is admin
# @param reserve {Object} Reservation parameters (places...)
# @param event {Object} Current event (Atelier/Stage)
# @return {{user_id:Number, reservable_id:Number, reservable_type:String, slots_attributes:Array<Object>, nb_reserve_places:Number, nb_reserve_reduced_places:Number}}
##
mkReservation = (member, reserve, event) ->
reservation =
user_id: member.id
reservable_id: event.id
reservable_type: 'Event'
slots_attributes: []
nb_reserve_places: reserve.nbReservePlaces
nb_reserve_reduced_places: reserve.nbReserveReducedPlaces
reservation.slots_attributes.push
start_at: event.start_date
end_at: event.end_date
availability_id: event.availability.id
offered: event.offered || false
reservation
##
# Set the current reservation to the default values. This implies to reservation form to be hidden.
##
resetEventReserve = ->
if $scope.event
$scope.reserve =
nbPlaces: [0..$scope.event.nb_free_places]
nbReducedPlaces: [0..$scope.event.nb_free_places]
nbReservePlaces: 0
nbReserveReducedPlaces: 0
toReserve: false
amountTotal : 0
$scope.event.offered = false
##
# Open a modal window which trigger the stripe payment process
# @param reservation {Object} to book
##
payByStripe = (reservation) ->
$uibModal.open
templateUrl: '<%= asset_path "stripe/payment_modal.html" %>'
size: 'md'
resolve:
reservation: ->
reservation
price: ->
Price.compute({reservation: reservation}).$promise
cgv: ->
CustomAsset.get({name: 'cgv-file'}).$promise
objectToPay: ->
eventToReserve: $scope.event
reserve: $scope.reserve
member: $scope.ctrl.member
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'cgv', 'Auth', 'Reservation', 'growl', ($scope, $uibModalInstance, $state, reservation, price, cgv, Auth, Reservation, growl) ->
# Price
$scope.amount = price.price
# CGV
$scope.cgv = cgv.custom_asset
# Reservation
$scope.reservation = reservation
# Callback for the stripe payment authorization
$scope.payment = (status, response) ->
if response.error
growl.error(response.error.message)
else
$scope.attempting = true
$scope.reservation.card_token = response.id
Reservation.save reservation: $scope.reservation, (reservation) ->
$uibModalInstance.close(reservation)
, (response)->
$scope.alerts = []
$scope.alerts.push
msg: response.data.card[0]
type: 'danger'
$scope.attempting = false
]
.result['finally'](null).then (reservation)->
afterPayment(reservation)
##
# Open a modal window which trigger the local payment process
# @param reservation {Object} to book
##
payOnSite = (reservation) ->
$uibModal.open
templateUrl: '<%= asset_path "shared/valid_reservation_modal.html" %>'
size: 'sm'
resolve:
reservation: ->
reservation
price: ->
Price.compute({reservation: reservation}).$promise
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation) ->
# Price
$scope.amount = price.price
# Reservation
$scope.reservation = reservation
# Button label
if $scope.amount > 0
$scope.validButtonName = _t('confirm_(payment_on_site)')
else
$scope.validButtonName = _t('confirm')
# Callback to validate the payment
$scope.ok = ->
$scope.attempting = true
Reservation.save reservation: $scope.reservation, (reservation) ->
$uibModalInstance.close(reservation)
$scope.attempting = true
, (response)->
$scope.alerts = []
angular.forEach response, (v, k)->
angular.forEach v, (err)->
$scope.alerts.push
msg: k+': '+err
type: 'danger'
$scope.attempting = false
# Callback to cancel the payment
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
]
.result['finally'](null).then (reservation)->
afterPayment(reservation)
##
# What to do after the payment was successful
# @param resveration {Object} booked reservation
##
afterPayment = (reservation)->
$scope.event.nb_free_places = $scope.event.nb_free_places - reservation.nb_reserve_places - reservation.nb_reserve_reduced_places
resetEventReserve()
$scope.reserveSuccess = true
$scope.reservations.push reservation
if $scope.currentUser.role == 'admin'
$scope.ctrl.member = null
## !!! MUST BE CALLED AT THE END of the controller
initialize()
] ]

View File

@ -1,35 +1,34 @@
'use strict' 'use strict'
Application.Controllers.controller "homeController", ['$scope', '$stateParams', 'Member', 'Twitter', 'Project', 'Event', ($scope, $stateParams, Member, Twitter, Project, Event) -> Application.Controllers.controller "HomeController", ['$scope', '$stateParams', 'Twitter', 'lastMembersPromise', 'lastProjectsPromise', 'upcomingEventsPromise', 'homeBlogpostPromise', 'twitterNamePromise', ($scope, $stateParams, Twitter, lastMembersPromise, lastProjectsPromise, upcomingEventsPromise, homeBlogpostPromise, twitterNamePromise)->
### PRIVATE STATIC CONSTANTS ###
# The 4 last users will be displayed on the home page
LAST_MEMBERS_LIMIT = 4
# Only the last tweet is shown
LAST_TWEETS_LIMIT = 1
# The 3 closest events are shown
LAST_EVENTS_LIMIT = 3
### PUBLIC SCOPE ### ### PUBLIC SCOPE ###
## The last registered members who confirmed their addresses ## The last registered members who confirmed their addresses
$scope.last_members = [] $scope.lastMembers = lastMembersPromise
## The last tweets from the Fablab official twitter account ## The last tweets from the Fablab official twitter account
$scope.last_tweets = [] $scope.lastTweets = []
## The last projects published/documented on the plateform ## The last projects published/documented on the plateform
$scope.last_projects = [] $scope.lastProjects = lastProjectsPromise
## The closest upcoming events ## The closest upcoming events
$scope.upcoming_events = [] $scope.upcomingEvents = upcomingEventsPromise
## The admin blogpost
$scope.homeBlogpost = homeBlogpostPromise.setting.value
## Twitter username
$scope.twitterName = twitterNamePromise.setting.value
##
# Test if the provided event run on a single day or not
# @param event {Object} single event from the $scope.upcomingEvents array
# @returns {boolean} false if the event runs on more that 1 day
##
$scope.isOneDayEvent = (event) ->
moment(event.start_date).isSame(event.end_date, 'day')
@ -39,20 +38,15 @@ Application.Controllers.controller "homeController", ['$scope', '$stateParams',
# Kind of constructor: these actions will be realized first when the controller is loaded # Kind of constructor: these actions will be realized first when the controller is loaded
## ##
initialize = -> initialize = ->
# display the reset password dialog if the parameter was provided # we retrieve tweets from here instead of ui-router's promise because, if adblock stop the API request,
# this prevent the whole home page to be blocked
$scope.lastTweets = Twitter.query(limit: 1)
# if we recieve a token to reset the password as GET parameter, trigger the
# changePassword modal from the parent controller
if $stateParams.reset_password_token if $stateParams.reset_password_token
$scope.$parent.editPassword($stateParams.reset_password_token) $scope.$parent.editPassword($stateParams.reset_password_token)
# initialize the homepage data
Member.lastSubscribed {limit: LAST_MEMBERS_LIMIT}, (members) ->
$scope.last_members = members
Twitter.query {limit: LAST_TWEETS_LIMIT}, (tweets) ->
$scope.last_tweets = tweets
Project.lastPublished (projects) ->
$scope.last_projects = projects
Event.upcoming {limit: LAST_EVENTS_LIMIT}, (events) ->
$scope.upcoming_events = events
## !!! MUST BE CALLED AT THE END of the controller ## !!! MUST BE CALLED AT THE END of the controller

View File

@ -74,19 +74,96 @@ class MachinesController
##
# Manages the transition when a user clicks on the reservation button.
# According to the status of user currently logged into the system, redirect him to the reservation page,
# or display a modal window asking him to complete a training before he can book a machine reservation.
# @param machine {{id:number}} An object containg the id of the machine to book,
# the object will be completed before the fonction returns.
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
##
_reserveMachine = (machine, e) ->
_this = this
e.preventDefault()
e.stopPropagation()
# retrieve the full machine object
machine = _this.Machine.get {id: machine.id}, ->
# if the currently logged'in user has completed the training for this machine, or this machine does not require
# a prior training, just redirect him to the machine's booking page
if machine.current_user_is_training or machine.trainings.length == 0
_this.$state.go('app.logged.machines_reserve', {id: machine.id})
else
# otherwise, if a user is authenticated ...
if _this.$scope.isAuthenticated()
# ... and have booked a training for this machine, tell him that he must wait for an admin to validate
# the training before he can book the reservation
if machine.current_user_training_reservation
_this.$uibModal.open
templateUrl: '<%= asset_path "machines/training_reservation_modal.html" %>'
controller: ['$scope', '$uibModalInstance', '$state', ($scope, $uibModalInstance, $state) ->
$scope.machine = machine
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
]
# ... but does not have booked the training, tell him to register for a training session first
else
_this.$uibModal.open
templateUrl: '<%= asset_path "machines/request_training_modal.html" %>'
controller: ['$scope', '$uibModalInstance', '$state', ($scope, $uibModalInstance, $state) ->
$scope.machine = machine
$scope.member = _this.$scope.currentUser
# transform the name of the trainings associated with the machine to integrate them in a sentence
$scope.humanizeTrainings = ->
text = ''
angular.forEach $scope.machine.trainings, (training) ->
if text.length > 0
text += _this._t('_or_the_')
text += training.name.substr(0,1).toLowerCase() + training.name.substr(1)
text
# modal is close with validation
$scope.ok = ->
$state.go('app.logged.trainings_reserve')
$uibModalInstance.close(machine)
# modal is closed with escaping
$scope.cancel = (e)->
e.preventDefault()
$uibModalInstance.dismiss('cancel')
]
# if the user is not logged, open the login modal window
else
_this.$scope.login()
## ##
# Controller used in the public listing page, allowing everyone to see the list of machines # Controller used in the public listing page, allowing everyone to see the list of machines
## ##
Application.Controllers.controller "machinesController", ["$scope", "$state", 'Machine', '$modal', ($scope, $state, Machine, $modal) -> Application.Controllers.controller "MachinesController", ["$scope", "$state", '_t', 'Machine', '$uibModal', 'machinesPromise', ($scope, $state, _t, Machine, $uibModal, machinesPromise) ->
## Retrieve the list of machines ## Retrieve the list of machines
$scope.machines = Machine.query() $scope.machines = machinesPromise
## ##
# Redirect the user to the machine details page # Redirect the user to the machine details page
## ##
$scope.showMachine = (machine) -> $scope.showMachine = (machine) ->
$state.go('app.public.machines_show', {id: machine.slug}) $state.go('app.public.machines_show', {id: machine.slug})
##
# Callback to book a reservation for the current machine
##
$scope.reserveMachine = _reserveMachine.bind
$scope: $scope
$state: $state
_t: _t
$uibModal: $uibModal
Machine: Machine
] ]
@ -94,7 +171,7 @@ Application.Controllers.controller "machinesController", ["$scope", "$state", 'M
## ##
# Controller used in the machine creation page (admin) # Controller used in the machine creation page (admin)
## ##
Application.Controllers.controller "newMachineController", ["$scope", "$state", 'CSRF', ($scope, $state, CSRF) -> Application.Controllers.controller "NewMachineController", ["$scope", "$state", 'CSRF',($scope, $state, CSRF) ->
CSRF.setMetaTags() CSRF.setMetaTags()
## API URL where the form will be posted ## API URL where the form will be posted
@ -116,8 +193,11 @@ Application.Controllers.controller "newMachineController", ["$scope", "$state",
## ##
# Controller used in the machine edition page (admin) # Controller used in the machine edition page (admin)
## ##
Application.Controllers.controller "editMachineController", ["$scope", "$state", '$stateParams', 'Machine', 'CSRF', ($scope, $state, $stateParams, Machine, CSRF) -> Application.Controllers.controller "EditMachineController", ["$scope", '$state', '$stateParams', 'machinePromise', 'CSRF', ($scope, $state, $stateParams, machinePromise, CSRF) ->
CSRF.setMetaTags()
### PUBLIC SCOPE ###
## API URL where the form will be posted ## API URL where the form will be posted
$scope.actionUrl = "/api/machines/" + $stateParams.id $scope.actionUrl = "/api/machines/" + $stateParams.id
@ -126,14 +206,24 @@ Application.Controllers.controller "editMachineController", ["$scope", "$state",
$scope.method = "put" $scope.method = "put"
## Retrieve the details for the machine id in the URL, if an error occurs redirect the user to the machines list ## Retrieve the details for the machine id in the URL, if an error occurs redirect the user to the machines list
$scope.machine = Machine.get {id: $stateParams.id} $scope.machine = machinePromise
, ->
return
, ->
$state.go('app.public.machines_list')
## Using the MachinesController
new MachinesController($scope, $state)
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
CSRF.setMetaTags()
## Using the MachinesController
new MachinesController($scope, $state)
## !!! MUST BE CALLED AT THE END of the controller
initialize()
] ]
@ -141,14 +231,11 @@ Application.Controllers.controller "editMachineController", ["$scope", "$state",
## ##
# Controller used in the machine details page (public) # Controller used in the machine details page (public)
## ##
Application.Controllers.controller "showMachineController", ['$scope', '$state', '$modal', '$stateParams', 'Machine', ($scope, $state, $modal, $stateParams, Machine) -> Application.Controllers.controller "ShowMachineController", ['$scope', '$state', '$uibModal', '$stateParams', '_t', 'Machine', 'growl', 'machinePromise'
, ($scope, $state, $uibModal, $stateParams, _t, Machine, growl, machinePromise) ->
## Retrieve the details for the machine id in the URL, if an error occurs redirect the user to the machines list ## Retrieve the details for the machine id in the URL, if an error occurs redirect the user to the machines list
$scope.machine = Machine.get {id: $stateParams.id} $scope.machine = machinePromise
, ->
return
, ->
$state.go('app.public.machines_list')
## ##
# Callback to delete the current machine (admins only) # Callback to delete the current machine (admins only)
@ -156,9 +243,730 @@ Application.Controllers.controller "showMachineController", ['$scope', '$state',
$scope.delete = (machine) -> $scope.delete = (machine) ->
# check the permissions # check the permissions
if $scope.currentUser.role isnt 'admin' if $scope.currentUser.role isnt 'admin'
console.error 'Unauthorized operation' console.error _t('unauthorized_operation')
else else
# delete the machine then redirect to the machines listing # delete the machine then redirect to the machines listing
machine.$delete -> machine.$delete ->
$state.go('app.public.machines_list') $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
##
$scope.reserveMachine = _reserveMachine.bind
$scope: $scope
$state: $state
_t: _t
$uibModal: $uibModal
Machine: Machine
]
##
# Controller used in the machine reservation page (for logged users who have completed the training and admins).
# This controller workflow is pretty similar to the trainings reservation controller.
##
Application.Controllers.controller "ReserveMachineController", ["$scope", "$state", '$stateParams', "$uibModal", '_t', "moment", 'Machine', 'Auth', 'dialogs', '$timeout', 'Price', 'Member', 'Availability', 'Slot', 'Setting', 'CustomAsset', 'plansPromise', 'groupsPromise', 'growl', 'settingsPromise',
($scope, $state, $stateParams, $uibModal, _t, moment, Machine, Auth, dialogs, $timeout, Price, Member, Availability, Slot, Setting, CustomAsset, plansPromise, groupsPromise, growl, settingsPromise) ->
### PRIVATE STATIC CONSTANTS ###
# The calendar is divided in slots of 60 minutes
BASE_SLOT = '01:00:00'
# The calendar will be initialized positioned under 9:00 AM
DEFAULT_CALENDAR_POSITION = '09:00:00'
# The user is unable to modify his already booked reservation 1 day before it occurs
PREVENT_BOOKING_MODIFICATION_DELAY = 1
# Slot already booked by the current user
FREE_SLOT_BORDER_COLOR = '#e4cd78'
# Slot already booked by another user
UNAVAILABLE_SLOT_BORDER_COLOR = '#1d98ec'
# Slot free to be booked
BOOKED_SLOT_BORDER_COLOR = '#b2e774'
### PUBLIC SCOPE ###
## after fullCalendar loads, provides access to its methods through $scope.calendar.fullCalendar()
$scope.calendar = null
## bind the machine availabilities with full-Calendar events
$scope.eventSources = []
## fullCalendar event. The last selected slot that the user want to book
$scope.slotToPlace = null
## fullCalendar event. An already booked slot that the user want to modify
$scope.slotToModify = null
## indicates the state of the current view : calendar or plans informations
$scope.plansAreShown = false
## will store the user's plan if he choosed to buy one
$scope.selectedPlan = null
## array of fullCalendar events. Slots where the user want to book
$scope.eventsReserved = []
## total amount of the bill to pay
$scope.amountTotal = 0
## is the user allowed to change the date of his booking
$scope.enableBookingMove = true
## how many hours before the reservation, the user is still allowed to change his booking
$scope.moveBookingDelay = 24
## list of plans, classified by group
$scope.plansClassifiedByGroup = []
for group in groupsPromise
groupObj = { id: group.id, name: group.name, plans: [] }
for plan in plansPromise
groupObj.plans.push(plan) if plan.group_id == group.id
$scope.plansClassifiedByGroup.push(groupObj)
## the user to deal with, ie. the current user for non-admins
$scope.ctrl =
member: {}
## fablab users list
$scope.members = []
## current machine to reserve
$scope.machine = {}
## fullCalendar (v2) configuration
$scope.calendarConfig =
timezone: Fablab.timezone
lang: Fablab.fullcalendar_locale
header:
left: 'month agendaWeek'
center: 'title'
right: 'today prev,next'
firstDay: 1 # Week start on monday (France)
scrollTime: DEFAULT_CALENDAR_POSITION
slotDuration: BASE_SLOT
allDayDefault: false
minTime: '00:00:00'
maxTime: '24:00:00'
height: 'auto'
buttonIcons:
prev: 'left-single-arrow'
next: 'right-single-arrow'
timeFormat:
agenda:'H:mm'
month: 'H(:mm)'
axisFormat: 'H:mm'
allDaySlot: false
defaultView: 'agendaWeek'
editable: false
eventClick: (event, jsEvent, view) ->
calendarEventClickCb(event, jsEvent, view)
eventRender: (event, element, view) ->
eventRenderCb(event, element)
## Global config: message to the end user concerning the subscriptions rules
$scope.subscriptionExplicationsAlert = settingsPromise.subscription_explications_alert
## Gloabl config: message to the end user concerning the machine bookings
$scope.machineExplicationsAlert = settingsPromise.machine_explications_alert
## Global config: is the user authorized to change his bookings slots?
$scope.enableBookingMove = (settingsPromise.booking_move_enable == "true")
## Global config: delay in hours before a booking while changing the booking slot is forbidden
$scope.moveBookingDelay = parseInt(settingsPromise.booking_move_delay)
## Global config: is the user authorized to cancel his bookings?
$scope.enableBookingCancel = (settingsPromise.booking_cancel_enable == "true")
## Global config: delay in hours before a booking while the cancellation is forbidden
$scope.cancelBookingDelay = parseInt(settingsPromise.booking_cancel_delay)
## Global config: calendar window in the morning
$scope.calendarConfig.minTime = moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss'))
## Global config: calendar window in the evening
$scope.calendarConfig.maxTime = moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss'))
##
# Cancel the current booking modification, removing the previously booked slot from the selection
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.removeSlotToModify = (e) ->
e.preventDefault()
if $scope.slotToPlace
$scope.slotToPlace.backgroundColor = 'white'
$scope.slotToPlace.title = ''
$scope.slotToPlace = null
$scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available')
$scope.slotToModify.backgroundColor = 'white'
$scope.slotToModify = null
$scope.calendar.fullCalendar 'rerenderEvents'
##
# When modifying an already booked reservation, cancel the choice of the new slot
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.removeSlotToPlace = (e)->
e.preventDefault()
$scope.slotToPlace.backgroundColor = 'white'
$scope.slotToPlace.title = ''
$scope.slotToPlace = null
$scope.calendar.fullCalendar 'rerenderEvents'
##
# When modifying an already booked reservation, confirm the modification.
##
$scope.modifyMachineSlot = ->
Slot.update {id: $scope.slotToModify.id},
slot:
start_at: $scope.slotToPlace.start
end_at: $scope.slotToPlace.end
availability_id: $scope.slotToPlace.availability_id
, -> # success
$scope.modifiedSlots =
newReservedSlot: $scope.slotToPlace
oldReservedSlot: $scope.slotToModify
$scope.slotToPlace.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available')
$scope.slotToPlace.backgroundColor = 'white'
$scope.slotToPlace.borderColor = $scope.slotToModify.borderColor
$scope.slotToPlace.id = $scope.slotToModify.id
$scope.slotToPlace.is_reserved = true
$scope.slotToPlace.can_modify = true
$scope.slotToPlace = null
$scope.slotToModify.backgroundColor = 'white'
$scope.slotToModify.title = ''
$scope.slotToModify.borderColor = FREE_SLOT_BORDER_COLOR
$scope.slotToModify.id = null
$scope.slotToModify.is_reserved = false
$scope.slotToModify.can_modify = false
$scope.slotToModify = null
$scope.calendar.fullCalendar 'rerenderEvents'
, (err) -> # failure
growl.error(_t('unable_to_change_the_reservation'))
console.error(err)
##
# Cancel the current booking modification, reseting the whole process
##
$scope.cancelModifyMachineSlot = ->
$scope.slotToPlace.backgroundColor = 'white'
$scope.slotToPlace.title = ''
$scope.slotToPlace = null
$scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available')
$scope.slotToModify.backgroundColor = 'white'
$scope.slotToModify = null
$scope.calendar.fullCalendar 'rerenderEvents'
##
# Callback to deal with the reservations of the user selected in the dropdown list instead of the current user's
# reservations. (admins only)
##
$scope.updateMember = ->
$scope.paidMachineSlots = null
$scope.plansAreShown = false
$scope.selectedPlan = null
updateCartPrice()
##
# Add the provided slot to the shopping cart (state transition from free to 'about to be reserved')
# and increment the total amount of the cart if needed.
# @param machineSlot {Object} fullCalendar event object
##
$scope.validMachineSlot = (machineSlot)->
machineSlot.isValid = true
updateCartPrice()
##
# Remove the provided slot from the shopping cart (state transition from 'about to be reserved' to free)
# and decrement the total amount of the cart if needed.
# @param machineSlot {Object} fullCalendar event object
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.removeMachineSlot = (machineSlot, e)->
e.preventDefault() if e
machineSlot.backgroundColor = 'white'
machineSlot.borderColor = FREE_SLOT_BORDER_COLOR
machineSlot.title = ''
machineSlot.isValid = false
if machineSlot.machine.is_reduced_amount
angular.forEach $scope.ctrl.member.machine_credits, (credit)->
if credit.machine_id = machineSlot.machine.id
credit.hours_used--
machineSlot.machine.is_reduced_amount = false
index = $scope.eventsReserved.indexOf(machineSlot)
$scope.eventsReserved.splice(index, 1)
if $scope.eventsReserved.length == 0
if $scope.plansAreShown
$scope.selectedPlan = null
$scope.plansAreShown = false
updateCartPrice()
$timeout ->
$scope.calendar.fullCalendar 'refetchEvents'
$scope.calendar.fullCalendar 'rerenderEvents'
##
# Checks that every selected slots were added to the shopping cart. Ie. will return false if
# any checked slot was not validated by the user.
##
$scope.machineSlotsValid = ->
isValid = true
angular.forEach $scope.eventsReserved, (m)->
isValid = false if !m.isValid
isValid
##
# Changes the user current view from the plan subsription screen to the machine reservation agenda
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.doNotSubscribePlan = (e)->
e.preventDefault()
$scope.plansAreShown = false
$scope.selectPlan($scope.selectedPlan)
##
# Validates the shopping chart and redirect the user to the payment step
##
$scope.payMachine = ->
# first, we check that a user was selected
if Object.keys($scope.ctrl.member).length > 0
reservation = mkReservation($scope.ctrl.member, $scope.eventsReserved, $scope.selectedPlan)
if $scope.currentUser.role isnt 'admin' and $scope.amountTotal > 0
payByStripe(reservation)
else
if $scope.currentUser.role is 'admin' or $scope.amountTotal is 0
payOnSite(reservation)
else
# otherwise we alert, this error musn't occur when the current user is not admin
growl.error(_t('please_select_a_member_first'))
##
# Switch the user's view from the reservation agenda to the plan subscription
##
$scope.showPlans = ->
$scope.plansAreShown = true
##
# Add the provided plan to the current shopping cart
# @param plan {Object} the plan to subscribe
##
$scope.selectPlan = (plan) ->
if $scope.isAuthenticated()
angular.forEach $scope.eventsReserved, (machineSlot)->
angular.forEach $scope.ctrl.member.machine_credits, (credit)->
if credit.machine_id = machineSlot.machine.id
credit.hours_used = 0
machineSlot.machine.is_reduced_amount = false
if $scope.selectedPlan != plan
$scope.selectedPlan = plan
else
$scope.selectedPlan = null
updateCartPrice()
else
$scope.login null, ->
$scope.selectedPlan = plan
updateCartPrice()
##
# Checks if $scope.slotToModify and $scope.slotToPlace have tag incompatibilities
# @returns {boolean} true in case of incompatibility
##
$scope.tagMissmatch = ->
for tag in $scope.slotToModify.tags
if tag.id not in $scope.slotToPlace.tag_ids
return true
false
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
Availability.machine {machineId: $stateParams.id}, (availabilities) ->
$scope.eventSources.push
events: availabilities
textColor: 'black'
if $scope.currentUser.role isnt 'admin'
$scope.ctrl.member = $scope.currentUser
else
Member.query {requested_attributes:'[subscription,credits]'}, (members) ->
$scope.members = members
$scope.machine = Machine.get {id: $stateParams.id}
, ->
return
, ->
$state.go('app.public.machines_list')
##
# Create an hash map implementing the Reservation specs
# @param member {Object} User as retreived from the API: current user / selected user if current is admin
# @param slots {Array<Object>} Array of fullCalendar events: slots selected on the calendar
# @param [plan] {Object} Plan as retrived from the API: plan to buy with the current reservation
# @return {{user_id:Number, reservable_id:Number, reservable_type:String, slots_attributes:Array<Object>, plan_id:Number|null}}
##
mkReservation = (member, slots, plan = null) ->
reservation =
user_id: member.id
reservable_id: (slots[0].machine.id if slots.length > 0)
reservable_type: 'Machine'
slots_attributes: []
plan_id: (plan.id if plan)
angular.forEach slots, (slot, key) ->
reservation.slots_attributes.push
start_at: slot.start
end_at: slot.end
availability_id: slot.availability_id
offered: slot.offered || false
reservation
##
# Update the total price of the current selection/reservation
##
updateCartPrice = ->
if Object.keys($scope.ctrl.member).length > 0
r = mkReservation($scope.ctrl.member, $scope.eventsReserved, $scope.selectedPlan)
Price.compute {reservation: r}, (res) ->
$scope.amountTotal = res.price
setSlotsDetails(res.details)
else
# otherwise we alert, this error musn't occur when the current user is not admin
growl.warning(_t('please_select_a_member_first'))
$scope.amountTotal = null
setSlotsDetails = (details) ->
angular.forEach $scope.eventsReserved, (slot) ->
angular.forEach details.slots, (s) ->
if moment(s.start_at).isSame(slot.start)
slot.promo = s.promo
slot.price = s.price
##
# Triggered when the user click on a reservation slot in the agenda.
# Defines the behavior to adopt depending on the slot status (already booked, free, ready to be reserved ...),
# the user's subscription (current or about to be took) and the time (the user cannot modify a booked reservation
# if it's too late).
##
calendarEventClickCb = (event, jsEvent, view) ->
if !event.is_reserved && !$scope.slotToModify
index = $scope.eventsReserved.indexOf(event)
if index == -1
event.backgroundColor = FREE_SLOT_BORDER_COLOR
event.title = _t('i_reserve')
$scope.eventsReserved.push event
else
$scope.removeMachineSlot(event)
$scope.paidMachineSlots = null
$scope.selectedPlan = null
$scope.modifiedSlots = null
else if !event.is_reserved && $scope.slotToModify
if $scope.slotToPlace
$scope.slotToPlace.backgroundColor = 'white'
$scope.slotToPlace.title = ''
$scope.slotToPlace = event
event.backgroundColor = '#bbb'
event.title = _t('i_shift')
else if event.is_reserved and (slotCanBeModified(event) or slotCanBeCanceled(event)) and !$scope.slotToModify and $scope.eventsReserved.length == 0
event.movable = slotCanBeModified(event)
event.cancelable = slotCanBeCanceled(event)
dialogs.confirm
templateUrl: '<%= asset_path "shared/confirm_modify_slot_modal.html" %>'
resolve:
object: -> event
, (type) ->
if type == 'move'
$scope.modifiedSlots = null
$scope.slotToModify = event
event.backgroundColor = '#eee'
event.title = _t('i_change')
$scope.calendar.fullCalendar 'rerenderEvents'
else if type == 'cancel'
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_cancel_this_reservation')
, -> # cancel confirmed
Slot.cancel {id: event.id}, -> # successfully canceled
growl.success _t('reservation_was_cancelled_successfully')
$scope.canceledSlot = event
$scope.canceledSlot.backgroundColor = 'white'
$scope.canceledSlot.title = ''
$scope.canceledSlot.borderColor = FREE_SLOT_BORDER_COLOR
$scope.canceledSlot.id = null
$scope.canceledSlot.is_reserved = false
$scope.canceledSlot.can_modify = false
$scope.canceledSlot = null
$scope.calendar.fullCalendar 'rerenderEvents'
, -> # error while canceling
growl.error _t('cancellation_failed')
, ->
$scope.paidMachineSlots = null
$scope.selectedPlan = null
$scope.modifiedSlots = null
$scope.calendar.fullCalendar 'rerenderEvents'
updateCartPrice()
##
# Triggered when fullCalendar tries to graphicaly render an event block.
# Append the event tag into the block, just after the event title.
# @see http://fullcalendar.io/docs/event_rendering/eventRender/
##
eventRenderCb = (event, element) ->
if $scope.currentUser.role is 'admin' and event.tags.length > 0
html = ''
for tag in event.tags
html += "<span class='label label-success text-white' title='#{tag.name}'>#{tag.name}</span>"
element.find('.fc-time').append(html)
##
# Open a modal window that allows the user to process a credit card payment for his current shopping cart.
##
payByStripe = (reservation) ->
$uibModal.open
templateUrl: '<%= asset_path "stripe/payment_modal.html" %>'
size: 'md'
resolve:
reservation: ->
reservation
price: ->
Price.compute({reservation: reservation}).$promise
cgv: ->
CustomAsset.get({name: 'cgv-file'}).$promise
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'cgv', 'Auth', 'Reservation', ($scope, $uibModalInstance, $state, reservation, price, cgv, Auth, Reservation) ->
# Price
$scope.amount = price.price
# CGV
$scope.cgv = cgv.custom_asset
# Reservation
$scope.reservation = reservation
##
# Callback to process the payment with Stripe, triggered on button click
##
$scope.payment = (status, response) ->
if response.error
growl.error(response.error.message)
else
$scope.attempting = true
$scope.reservation.card_token = response.id
Reservation.save reservation: $scope.reservation, (reservation) ->
$uibModalInstance.close(reservation)
, (response)->
$scope.alerts = []
$scope.alerts.push
msg: response.data.card[0]
type: 'danger'
$scope.attempting = false
]
.result['finally'](null).then (reservation)->
afterPayment(reservation)
##
# Open a modal window that allows the user to process a local payment for his current shopping cart (admin only).
##
payOnSite = (reservation) ->
$uibModal.open
templateUrl: '<%= asset_path "shared/valid_reservation_modal.html" %>'
size: 'sm'
resolve:
reservation: ->
reservation
price: ->
Price.compute({reservation: reservation}).$promise
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation) ->
# Price
$scope.amount = price.price
# Reservation
$scope.reservation = reservation
# Button label
if $scope.amount > 0
$scope.validButtonName = _t('confirm_(payment_on_site)')
else
$scope.validButtonName = _t('confirm')
##
# Callback to process the local payment, triggered on button click
##
$scope.ok = ->
$scope.attempting = true
Reservation.save reservation: $scope.reservation, (reservation) ->
$uibModalInstance.close(reservation)
$scope.attempting = true
, (response)->
$scope.alerts = []
$scope.alerts.push({msg: _t('a_problem_occured_during_the_payment_process_please_try_again_later'), type: 'danger' })
$scope.attempting = false
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
]
.result['finally'](null).then (reservation)->
afterPayment(reservation)
##
# Determines if the provided booked slot is able to be modified by the user.
# @param slot {Object} fullCalendar event object
##
slotCanBeModified = (slot)->
return true if $scope.currentUser.role is 'admin'
slotStart = moment(slot.start)
now = moment()
if slot.can_modify and $scope.enableBookingMove and slotStart.diff(now, "hours") >= $scope.moveBookingDelay
return true
else
return false
##
# Determines if the provided booked slot is able to be canceled by the user.
# @param slot {Object} fullCalendar event object
##
slotCanBeCanceled = (slot) ->
return true if $scope.currentUser.role is 'admin'
slotStart = moment(slot.start)
now = moment()
if slot.can_modify and $scope.enableBookingCancel and slotStart.diff(now, "hours") >= $scope.cancelBookingDelay
return true
else
return false
##
# Once the reservation is booked (payment process successfully completed), change the event style
# in fullCalendar, update the user's subscription and free-credits if needed
# @param reservation {Object}
##
afterPayment = (reservation)->
angular.forEach $scope.eventsReserved, (machineSlot, key) ->
machineSlot.is_reserved = true
machineSlot.can_modify = true
if $scope.currentUser.role isnt 'admin'
machineSlot.title = _t('i_ve_reserved')
machineSlot.borderColor = BOOKED_SLOT_BORDER_COLOR
updateMachineSlot(machineSlot, reservation, $scope.currentUser)
else
machineSlot.title = _t('not_available')
machineSlot.borderColor = UNAVAILABLE_SLOT_BORDER_COLOR
updateMachineSlot(machineSlot, reservation, $scope.ctrl.member)
machineSlot.backgroundColor = 'white'
$scope.paidMachineSlots = $scope.eventsReserved
$scope.eventsReserved = []
if $scope.selectedPlan
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan)
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan)
$scope.plansAreShown = false
$scope.calendar.fullCalendar 'refetchEvents'
$scope.calendar.fullCalendar 'rerenderEvents'
##
# After payment, update the id of the newly reserved slot with the id returned by the server.
# This will allow the user to modify the reservation he just booked. The associated user will also be registered
# with the slot.
# @param slot {Object}
# @param reservation {Object}
# @param user {Object} user associated with the slot
##
updateMachineSlot = (slot, reservation, user)->
angular.forEach reservation.slots, (s)->
if slot.start.isSame(s.start_at)
slot.id = s.id
slot.user = user
##
# Search for the requested plan in the provided array and return its price.
# @param plansArray {Array} full list of plans
# @param planId {Number} plan identifier
# @returns {Number|null} price of the given plan or null if not found
##
findAmountByPlanId = (plansArray, planId)->
for plan in plansArray
return plan.amount if plan.plan_id == planId
return null
## !!! MUST BE CALLED AT THE END of the controller
initialize()
] ]

View File

@ -3,55 +3,97 @@
## ##
# Navigation controller. List the links availables in the left navigation pane and their icon. # Navigation controller. List the links availables in the left navigation pane and their icon.
## ##
Application.Controllers.controller "mainNavController", ["$scope", "$location", "$cookies", ($scope, $location, $cookies) -> Application.Controllers.controller "MainNavController", ["$scope", "$location", "$cookies", ($scope, $location, $cookies) ->
## Common links (public application) ## Common links (public application)
$scope.navLinks = [ $scope.navLinks = [
{ {
state: 'app.public.home' state: 'app.public.home'
linkText: 'Accueil' linkText: 'home'
linkIcon: 'home' linkIcon: 'home'
} }
{ {
state: 'app.public.machines_list' state: 'app.public.machines_list'
linkText: 'Liste des machines' linkText: 'reserve_a_machine'
linkIcon: 'gears' linkIcon: 'calendar'
}
{
state: 'app.logged.trainings_reserve'
linkText: 'trainings_registrations'
linkIcon: 'graduation-cap'
} }
{ {
state: 'app.public.events_list' state: 'app.public.events_list'
linkText: 'Liste des stages et ateliers' linkText: 'courses_and_workshops_registrations'
linkIcon: 'tags' linkIcon: 'tags'
} }
{ {
state: 'app.public.projects_list' state: 'app.public.projects_list'
linkText: 'Galerie de projets' linkText: 'projects_gallery'
linkIcon: 'th' linkIcon: 'th'
} }
] ]
## Admin links (backoffice application) unless Fablab.withoutPlans
$scope.navLinks.push({
state: 'app.public.plans'
linkText: 'subscriptions'
linkIcon: 'credit-card'
})
$scope.adminNavLinks = [ $scope.adminNavLinks = [
{
state: 'app.admin.trainings'
linkText: 'trainings_monitoring'
linkIcon: 'graduation-cap'
}
{
state: 'app.admin.calendar'
linkText: 'manage_the_calendar'
linkIcon: 'calendar'
}
{ {
state: 'app.admin.members' state: 'app.admin.members'
linkText: 'Suivi utilisateurs' linkText: 'manage_the_users'
linkIcon: 'users' linkIcon: 'users'
} }
{
state: 'app.admin.invoices'
linkText: 'manage_the_invoices'
linkIcon: 'file-pdf-o'
}
{
state: 'app.admin.pricing'
linkText: 'subscriptions_and_prices'
linkIcon: 'money'
}
{ {
state: 'app.admin.events' state: 'app.admin.events'
linkText: 'Suivi stages et ateliers' linkText: 'courses_and_workshops_monitoring'
linkIcon: 'tags' linkIcon: 'tags'
} }
{ {
state: 'app.public.machines_list' state: 'app.public.machines_list'
linkText: 'Gérer les machines' linkText: 'manage_the_machines'
linkIcon: 'cogs' linkIcon: 'cogs'
} }
{ {
state: 'app.admin.project_elements' state: 'app.admin.project_elements'
linkText: 'Gérer les éléments Projets' linkText: 'manage_the_projects_elements'
linkIcon: 'tasks' linkIcon: 'tasks'
} }
{
state: 'app.admin.statistics'
linkText: 'statistics'
linkIcon: 'bar-chart-o'
}
{
state: 'app.admin.settings'
linkText: 'customization'
linkIcon: 'gear'
}
] ]
] ]

View File

@ -3,23 +3,11 @@
## ##
# Controller used in the members listing page # Controller used in the members listing page
## ##
Application.Controllers.controller "membersController", ["$scope", "$state", 'Member', ($scope, $state, Member) -> Application.Controllers.controller "MembersController", ["$scope", 'membersPromise', ($scope, membersPromise) ->
## members list ## members list
$scope.members = Member.query() $scope.members = membersPromise
## Merbers ordering/sorting. Default: not sorted
$scope.orderMember = null
##
# Change the members ordering criterion to the one provided
# @param orderBy {string} ordering criterion
##
$scope.setOrderMember = (orderBy)->
if $scope.orderMember == orderBy
$scope.orderMember = '-'+orderBy
else
$scope.orderMember = orderBy
] ]
@ -27,24 +15,71 @@ Application.Controllers.controller "membersController", ["$scope", "$state", 'Me
## ##
# Controller used when editing the current user's profile # Controller used when editing the current user's profile
## ##
Application.Controllers.controller "editProfileController", ["$scope", "$state", "Member", "Auth", 'growl', 'dialogs', 'CSRF', ($scope, $state, Member, Auth, growl, dialogs, CSRF) -> Application.Controllers.controller "EditProfileController", ["$scope", "$rootScope", "$state", "$window", '$locale', "Member", "Auth", "Session", "activeProviderPromise", 'growl', 'dialogs', 'CSRF', 'memberPromise', 'groups', '_t'
CSRF.setMetaTags() , ($scope, $rootScope, $state, $window, $locale, Member, Auth, Session, activeProviderPromise, growl, dialogs, CSRF, memberPromise, groups, _t) ->
### PUBLIC SCOPE ###
## API URL where the form will be posted ## API URL where the form will be posted
$scope.actionUrl = "/api/members/" + $scope.currentUser.id $scope.actionUrl = "/api/members/" + $scope.currentUser.id
## list of groups
$scope.groups = groups
## Form action on the above URL ## Form action on the above URL
$scope.method = 'patch' $scope.method = 'patch'
## Current user's profile ## Current user's profile
$scope.user = Member.get {id: $scope.currentUser.id} $scope.user = memberPromise
## default : do not show the group changing form
$scope.group =
change: false
## group ID of the current/selected user
$scope.userGroup = memberPromise.group_id
## active authentication provider parameters
$scope.activeProvider = activeProviderPromise
## allow the user to change his password except if he connect from an SSO
$scope.preventPassword = false
## mapping of fields to disable
$scope.preventField = {}
## Angular-Bootstrap datepicker configuration for birthday ## Angular-Bootstrap datepicker configuration for birthday
$scope.datePicker = $scope.datePicker =
format: 'dd/MM/yyyy' format: $locale.DATETIME_FORMATS.shortDate
opened: false # default: datePicker is not shown opened: false # default: datePicker is not shown
options: options:
startingDay: 1 # France: the week starts on monday startingDay: Fablab.weekStartingDay
##
# Return the group object, identified by the ID set in $scope.userGroup
##
$scope.getUserGroup = ->
for group in $scope.groups
if group.id == $scope.userGroup
return group
##
# Change the group of the current user to the one set in $scope.userGroup
##
$scope.selectGroup = ->
Member.update {id: $scope.user.id}, {user: {group_id: $scope.userGroup}}, (user) ->
$scope.user = user
$scope.group.change = false
growl.success(_t('your_group_has_been_successfully_changed'))
, (err) ->
growl.error(_t('an_unexpected_error_prevented_your_group_from_being_changed'))
console.error(err)
@ -81,10 +116,32 @@ Application.Controllers.controller "editProfileController", ["$scope", "$state",
Auth._currentUser.name = content.name Auth._currentUser.name = content.name
$scope.currentUser = content $scope.currentUser = content
Auth._currentUser = content Auth._currentUser = content
$rootScope.currentUser = content
$state.go('app.public.home') $state.go('app.public.home')
##
# Ask for confirmation then delete the current user's account
# @param user {Object} the current user (to delete)
##
$scope.deleteUser = (user)->
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_delete_your_account')+' '+_t('all_data_relative_to_your_projects_will_be_lost')
, -> # cancel confirmed
Member.remove { id: user.id }, ->
Auth.logout().then ->
$state.go('app.public.home')
growl.success(_t('your_user_account_has_been_successfully_deleted_goodbye'))
, (error)->
console.log(error)
growl.error(_t('an_error_occured_preventing_your_account_from_being_deleted'))
## ##
# For use with 'ng-class', returns the CSS class name for the uploads previews. # For use with 'ng-class', returns the CSS class name for the uploads previews.
# The preview may show a placeholder or the content of the file depending on the upload state. # The preview may show a placeholder or the content of the file depending on the upload state.
@ -95,6 +152,52 @@ Application.Controllers.controller "editProfileController", ["$scope", "$state",
'fileinput-exists' 'fileinput-exists'
else else
'fileinput-new' 'fileinput-new'
##
# Check if the of the properties editable by the user are linked to the SSO
# @return {boolean} true if some editable fields are mapped with the SSO, false otherwise
##
$scope.hasSsoFields = ->
# if check if keys > 1 because there's a minimum of 1 mapping (id <-> provider-uid)
# so the user may want to edit his profile on the SSO if at least 2 mappings exists
Object.keys($scope.preventField).length > 1
##
# Disconnect and re-connect the user to the SSO to force the synchronisation of the profile's data
##
$scope.syncProfile = ->
Auth.logout().then (oldUser) ->
Session.destroy()
$rootScope.currentUser = null
$rootScope.toCheckNotifications = false
$scope.notifications = []
$window.location.href = $scope.activeProvider.link_to_sso_connect
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
CSRF.setMetaTags()
# init the birth date to JS object
$scope.user.profile.birthday = moment($scope.user.profile.birthday).toDate()
if $scope.activeProvider.providable_type != 'DatabaseProvider'
$scope.preventPassword = true
# bind fields protection with sso fields
angular.forEach activeProviderPromise.mapping, (map) ->
$scope.preventField[map] = true
## !!! MUST BE CALLED AT THE END of the controller
initialize()
] ]
@ -102,8 +205,8 @@ Application.Controllers.controller "editProfileController", ["$scope", "$state",
## ##
# Controller used on the public user's profile page (seeing another user's profile) # Controller used on the public user's profile page (seeing another user's profile)
## ##
Application.Controllers.controller "showProfileController", ["$scope", "$stateParams", 'Member', ($scope, $stateParams, Member) -> Application.Controllers.controller "ShowProfileController", ["$scope", "$stateParams", 'Member', 'memberPromise', ($scope, $stateParams, Member, memberPromise) ->
## Selected user's profile (id from the current URL) ## Selected user's profile (id from the current URL)
$scope.user = Member.get {id: $stateParams.id} $scope.user = memberPromise
] ]

View File

@ -4,7 +4,7 @@
# Controller used in notifications page # Controller used in notifications page
# inherits $scope.$parent.notifications (unread notifications) from ApplicationController # inherits $scope.$parent.notifications (unread notifications) from ApplicationController
## ##
Application.Controllers.controller "notificationsController", ["$scope", 'Notification', ($scope, Notification) -> Application.Controllers.controller "NotificationsController", ["$scope", 'Notification', ($scope, Notification) ->
@ -32,7 +32,7 @@ Application.Controllers.controller "notificationsController", ["$scope", 'Notifi
# Mark the provided notification as read, updating its status on the server and moving it # Mark the provided notification as read, updating its status on the server and moving it
# to the already read notifications list. # to the already read notifications list.
# @param notification {{id:number}} the notification to mark as read # @param notification {{id:number}} the notification to mark as read
# @param e {Object} jQuery event object # @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
## ##
$scope.markAsRead = (notification, e) -> $scope.markAsRead = (notification, e) ->
e.preventDefault() e.preventDefault()

View File

@ -0,0 +1,232 @@
'use strict'
Application.Controllers.controller "PlansIndexController", ["$scope", "$state", '$uibModal', 'Auth', 'dialogs', 'growl', 'plansPromise', 'groupsPromise', 'Subscription', 'Member', 'subscriptionExplicationsPromise', '_t'
, ($scope, $state, $uibModal, Auth, dialogs, growl, plansPromise, groupsPromise, Subscription, Member, subscriptionExplicationsPromise, _t) ->
### PUBLIC SCOPE ###
## list of groups
$scope.groups = groupsPromise
## default : do not show the group changing form
$scope.changeGroup = false
## group ID of the current/selected user
$scope.userGroup = null
## list of plans, classified by group
$scope.plansClassifiedByGroup = []
for group in groupsPromise
groupObj = { id: group.id, name: group.name, plans: [] }
for plan in plansPromise
groupObj.plans.push(plan) if plan.group_id == group.id
$scope.plansClassifiedByGroup.push(groupObj)
## user to deal with
$scope.ctrl =
member: null
member_id: null
## already subscribed plan of the current user
$scope.paidPlan = null
## plan to subscribe (shopping cart)
$scope.selectedPlan = null
##
$scope.subscriptionExplicationsAlert = subscriptionExplicationsPromise.setting.value
##
# Callback to deal with the subscription of the user selected in the dropdown list instead of the current user's
# subscription. (admins only)
##
$scope.updateMember = ->
$scope.selectedPlan = null
$scope.paidPlan = null
$scope.userGroup = $scope.ctrl.member.group_id
$scope.changeGroup = false
##
# Add the provided plan to the shopping basket
# @param plan {Object} The plan to subscribe to
##
$scope.selectPlan = (plan) ->
if $scope.isAuthenticated()
if $scope.selectedPlan != plan
$scope.selectedPlan = plan
else
$scope.selectedPlan = null
else
$scope.login()
##
# Callback to trigger the payment process of the subscription
##
$scope.openSubscribePlanModal = ->
if $scope.currentUser.role isnt 'admin'
payByStripe()
else
payOnSite()
##
# Return the group object, identified by the ID set in $scope.userGroup
##
$scope.getUserGroup = ->
for group in $scope.groups
if group.id == $scope.userGroup
return group
##
# Change the group of the current/selected user to the one set in $scope.userGroup
##
$scope.selectGroup = ->
Member.update {id: $scope.ctrl.member.id}, {user: {group_id: $scope.userGroup}}, (user) ->
$scope.ctrl.member = user
$scope.changeGroup = false
if $scope.currentUser.role isnt 'admin'
$scope.currentUser = user
growl.success(_t('your_group_was_successfully_changed'))
else
growl.success(_t('the_user_s_group_was_successfully_changed'))
, (err) ->
if $scope.currentUser.role isnt 'admin'
growl.error(_t('an_error_prevented_your_group_from_being_changed'))
else
growl.error(_t('an_error_prevented_to_change_the_user_s_group'))
console.error(err)
##
# Return an enumerable meaninful string for the gender of the provider user
# @param user {Object} Database user record
# @return {string} 'male' or 'female'
##
$scope.getGender = (user) ->
if user.profile
if user.profile.gender == "true" then 'male' else 'female'
else 'other'
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
if $scope.currentUser
if $scope.currentUser.role isnt 'admin'
$scope.ctrl.member = $scope.currentUser
$scope.paidPlan = $scope.currentUser.subscribed_plan
$scope.userGroup = $scope.currentUser.group_id
else
Member.query {requested_attributes:'[subscription]'}, (members) ->
membersNoPlan = []
angular.forEach members, (v)->
membersNoPlan.push v unless v.subscribed_plan
$scope.members = membersNoPlan
$scope.$on 'devise:new-session', (event, user)->
$scope.ctrl.member = user
$scope.isInFuture = (dateTime)->
if moment().diff(moment(dateTime)) < 0
true
else
false
##
# Open a modal window which trigger the stripe payment process
##
payByStripe = ->
$uibModal.open
templateUrl: '<%= asset_path "stripe/payment_modal.html" %>'
size: 'md'
resolve:
selectedPlan: -> $scope.selectedPlan
member: -> $scope.ctrl.member
controller: ['$scope', '$uibModalInstance', '$state', 'selectedPlan', 'member', 'Subscription', 'CustomAsset', ($scope, $uibModalInstance, $state, selectedPlan, member, Subscription, CustomAsset) ->
$scope.amount = selectedPlan.amount
$scope.selectedPlan = selectedPlan
# retrieve the CGV
CustomAsset.get {name: 'cgv-file'}, (cgv) ->
$scope.cgv = cgv.custom_asset
$scope.payment = (status, response) ->
if response.error
growl.error(response.error.message)
else
$scope.attempting = true
Subscription.save
subscription:
plan_id: selectedPlan.id
user_id: member.id
card_token: response.id
, (data, status) -> # success
$uibModalInstance.close(data)
, (data, status) -> # failed
$scope.alerts = []
$scope.alerts.push({msg: _t('an_error_occured_during_the_payment_process_please_try_again_later'), type: 'danger' })
$scope.attempting = false
]
.result['finally'](null).then (subscription)->
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan)
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan)
$scope.paidPlan = angular.copy($scope.selectedPlan)
$scope.selectedPlan = null
##
# Open a modal window which trigger the local payment process
##
payOnSite = ->
$uibModal.open
templateUrl: '<%= asset_path "plans/payment_modal.html" %>'
size: 'sm'
resolve:
selectedPlan: -> $scope.selectedPlan
member: -> $scope.ctrl.member
controller: ['$scope', '$uibModalInstance', '$state', 'selectedPlan', 'member', 'Subscription', ($scope, $uibModalInstance, $state, selectedPlan, member, Subscription) ->
$scope.plan = selectedPlan
$scope.member = member
$scope.ok = ->
$scope.attempting = true
Subscription.save
subscription:
plan_id: selectedPlan.id
user_id: member.id
, (data, status) -> # success
$uibModalInstance.close(data)
, (data, status) -> # failed
$scope.alerts = []
$scope.alerts.push({msg: _t('an_error_occured_during_the_payment_process_please_try_again_later'), type: 'danger' })
$scope.attempting = false
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
]
.result['finally'](null).then (reservation)->
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan)
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan)
index = $scope.members.indexOf($scope.ctrl.member)
$scope.members.splice(index, 1)
$scope.ctrl.member = null
$scope.paidPlan = angular.copy($scope.selectedPlan)
$scope.selectedPlan = null
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]

View File

@ -0,0 +1,152 @@
'use strict'
Application.Controllers.controller "CompleteProfileController", ["$scope", "$rootScope", "$state", "_t", "$locale", "growl", "CSRF", "Auth", "Member", "settingsPromise", "activeProviderPromise", "groupsPromise", "cguFile", "memberPromise"
, ($scope, $rootScope, $state, _t, $locale, growl, CSRF, Auth, Member, settingsPromise, activeProviderPromise, groupsPromise, cguFile, memberPromise) ->
### PUBLIC SCOPE ###
## API URL where the form will be posted
$scope.actionUrl = "/api/members/" + memberPromise.id
## Form action on the above URL
$scope.method = 'patch'
## genre of the application name (eg. "_le_ Fablab" or "_la_ Fabrique")
$scope.nameGenre = settingsPromise.name_genre
## name of the current fablab application (eg. "Fablab de la Casemate")
$scope.fablabName = settingsPromise.fablab_name
## informations from the current SSO provider
$scope.activeProvider = activeProviderPromise
## list of user's groups (student/standard/...)
$scope.groups = groupsPromise
## current user, contains informations retrieved from the SSO
$scope.user = memberPromise
## disallow the user to change his password as he connect from SSO
$scope.preventPassword = true
## mapping of fields to disable
$scope.preventField = {}
## CGU
$scope.cgu = cguFile.custom_asset
## Angular-Bootstrap datepicker configuration for birthday
$scope.datePicker =
format: $locale.DATETIME_FORMATS.shortDate
opened: false # default: datePicker is not shown
options:
startingDay: Fablab.weekStartingDay
##
# Callback to diplay the datepicker as a dropdown when clicking on the input field
# @param $event {Object} jQuery event object
##
$scope.openDatePicker = ($event) ->
$event.preventDefault()
$event.stopPropagation()
$scope.datePicker.opened = true
##
# For use with ngUpload (https://github.com/twilson63/ngUpload).
# Intended to be the callback when the upload is done: any raised error will be stacked in the
# $scope.alerts array. If everything goes fine, the user's profile is updated and the user is
# redirected to the home page
# @param content {Object} JSON - The upload's result
##
$scope.submited = (content) ->
if !content.id?
$scope.alerts = []
angular.forEach content, (v, k)->
angular.forEach v, (err)->
$scope.alerts.push
msg: k+': '+err,
type: 'danger'
else
$scope.user.profile.user_avatar = content.profile.user_avatar
Auth._currentUser.profile.user_avatar = content.profile.user_avatar
$scope.user.name = content.name
Auth._currentUser.name = content.name
$scope.user = content
Auth._currentUser = content
$rootScope.currentUser = content
$state.go('app.public.home')
##
# For use with 'ng-class', returns the CSS class name for the uploads previews.
# The preview may show a placeholder or the content of the file depending on the upload state.
# @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules)
##
$scope.fileinputClass = (v)->
if v
'fileinput-exists'
else
'fileinput-new'
##
# Merge the current user into the account with the given auth_token
##
$scope.registerAuthToken = ->
Member.merge {id: $rootScope.currentUser.id}, {user: {auth_token: $scope.user.auth_token}}, (user) ->
$scope.user = user
Auth._currentUser = user
$rootScope.currentUser = user
$state.go('app.public.home')
, (err) ->
if err.data.error
growl.error(err.data.error)
else
growl.error(_t('an_unexpected_error_occurred_check_your_authentication_code'))
console.error(err)
##
# Return the email given by the SSO provider, parsed if needed
# @return {String} E-mail of the current user
##
$scope.ssoEmail = ->
email = memberPromise.email
if email
duplicate = email.match(/^<([^>]+)>.{20}-duplicate$/)
if duplicate
return duplicate[1]
email
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
CSRF.setMetaTags()
# init the birth date to JS object
$scope.user.profile.birthday = moment($scope.user.profile.birthday).toDate()
# bind fields protection with sso fields
angular.forEach activeProviderPromise.mapping, (map) ->
$scope.preventField[map] = true
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]

View File

@ -144,30 +144,28 @@ class ProjectsController
## ##
# Controller used on projects listing page # Controller used on projects listing page
## ##
Application.Controllers.controller "projectsController", ["$scope", "$state", 'Project', 'Machine', 'Theme', 'Component', ($scope, $state, Project, Machine, Theme, Component) -> Application.Controllers.controller "ProjectsController", ["$scope", "$state", 'Project', 'machinesPromise', 'themesPromise', 'componentsPromise'
, ($scope, $state, Project, machinesPromise, themesPromise, componentsPromise) ->
### PRIVATE STATIC CONSTANTS ### ### PRIVATE STATIC CONSTANTS ###
# Number of notifications added to the page when the user clicks on 'load next notifications' # Number of notifications added to the page when the user clicks on 'load next notifications'
PROJECTS_PER_PAGE = 12 PROJECTS_PER_PAGE = 12
### PUBLIC SCOPE ### ### PUBLIC SCOPE ###
$scope.search = { q: "", from: undefined, machine_id: undefined, component_id: undefined, theme_id: undefined }
## list of projects to display ## list of projects to display
$scope.projects = [] $scope.projects = []
## list of machines / used for filtering ## list of machines / used for filtering
$scope.machines = [] $scope.machines = machinesPromise
## list of themes / used for filtering ## list of themes / used for filtering
$scope.themes = Theme.query() $scope.themes = themesPromise
## list of components / used for filtering ## list of components / used for filtering
$scope.components = Component.query() $scope.components = componentsPromise
## By default, the pagination mode is activated to limit the page size ## By default, the pagination mode is activated to limit the page size
$scope.paginateActive = true $scope.paginateActive = true
@ -175,19 +173,31 @@ Application.Controllers.controller "projectsController", ["$scope", "$state", 'P
## The currently displayed page number ## The currently displayed page number
$scope.page = 1 $scope.page = 1
$scope.resetFilters = ->
$scope.search.q = ""
$scope.search.from = undefined
$scope.search.machine_id = undefined
$scope.search.component_id = undefined
$scope.search.theme_id = undefined
$scope.triggerSearch()
$scope.triggerSearch = ->
Project.search { search: $scope.search, page: 1 }, (projects)->
$scope.projects = projects
if projects.length < PROJECTS_PER_PAGE
$scope.paginateActive = false
else
$scope.paginateActive = true
$scope.page = 2
##
# Request the server to retrieve the next undisplayed projects and add them
# to the local projects list.
##
$scope.loadMoreProjects = -> $scope.loadMoreProjects = ->
Project.query {page: $scope.page}, (projects) -> # Project.query {page: $scope.page}, (projects) ->
$scope.projects = $scope.projects.concat projects # $scope.projects = $scope.projects.concat projects
$scope.paginateActive = false if projects.length < PROJECTS_PER_PAGE # $scope.paginateActive = false if projects.length < PROJECTS_PER_PAGE
Project.search { search: $scope.search, page: $scope.page }, (projects)->
$scope.page += 1 $scope.projects = $scope.projects.concat projects
$scope.paginateActive = false if projects.length < PROJECTS_PER_PAGE
$scope.page += 1
## ##
@ -199,37 +209,9 @@ Application.Controllers.controller "projectsController", ["$scope", "$state", 'P
## ## initialization
# Callback to delete the provided project. Then, the projects list page is refreshed (admins only) $scope.triggerSearch()
##
$scope.delete = (project) ->
# check the permissions
if $scope.currentUser.role isnt 'admin'
console.error 'Unauthorized operation'
else
# delete the project then refresh the projects list
project.$delete ->
$state.go('app.public.projects_list', {}, {reload: true})
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
Machine.query().$promise.then (data)->
$scope.machines = data.map (d) ->
id: d.id
name: d.name
$scope.loadMoreProjects()
## !!! MUST BE CALLED AT THE END of the controller
initialize()
] ]
@ -237,7 +219,8 @@ Application.Controllers.controller "projectsController", ["$scope", "$state", 'P
## ##
# Controller used in the project creation page # Controller used in the project creation page
## ##
Application.Controllers.controller "newProjectController", ["$scope", "$state", 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', ($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, CSRF) -> Application.Controllers.controller "NewProjectController", ["$scope", "$state", 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF'
, ($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, CSRF) ->
CSRF.setMetaTags() CSRF.setMetaTags()
## API URL where the form will be posted ## API URL where the form will be posted
@ -246,9 +229,6 @@ Application.Controllers.controller "newProjectController", ["$scope", "$state",
## Form action on the above URL ## Form action on the above URL
$scope.method = 'post' $scope.method = 'post'
## Button litteral text value
$scope.submitName = 'Enregistrer comme brouillon'
## Default project parameters ## Default project parameters
$scope.project = $scope.project =
project_steps_attributes: [] project_steps_attributes: []
@ -271,7 +251,8 @@ Application.Controllers.controller "newProjectController", ["$scope", "$state",
## ##
# Controller used in the project edition page # Controller used in the project edition page
## ##
Application.Controllers.controller "editProjectController", ["$scope", "$state", '$stateParams', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', ($scope, $state, $stateParams, Project, Machine, Member, Component, Theme, Licence, $document, CSRF) -> Application.Controllers.controller "EditProjectController", ["$scope", "$state", '$stateParams', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', 'projectPromise'
, ($scope, $state, $stateParams, Project, Machine, Member, Component, Theme, Licence, $document, CSRF, projectPromise) ->
CSRF.setMetaTags() CSRF.setMetaTags()
## API URL where the form will be posted ## API URL where the form will be posted
@ -280,15 +261,8 @@ Application.Controllers.controller "editProjectController", ["$scope", "$state",
## Form action on the above URL ## Form action on the above URL
$scope.method = 'put' $scope.method = 'put'
## Button litteral text value
$scope.submitName = 'Enregistrer'
## Retrieve the project's details, if an error occured, redirect the user to the projects list page ## Retrieve the project's details, if an error occured, redirect the user to the projects list page
$scope.project = Project.get {id: $stateParams.id} $scope.project = projectPromise
, -> # success
return
, -> # failed
$state.go('app.public.projects_list')
## Other members list (project collaborators) ## Other members list (project collaborators)
Member.query().$promise.then (data)-> Member.query().$promise.then (data)->
@ -307,18 +281,15 @@ Application.Controllers.controller "editProjectController", ["$scope", "$state",
## ##
# Controller used in the public project's details page # Controller used in the public project's details page
## ##
Application.Controllers.controller "showProjectController", ["$scope", "$state", "$stateParams", "Project", '$location', ($scope, $state, $stateParams, Project, $location) -> Application.Controllers.controller "ShowProjectController", ["$scope", "$state", "projectPromise", '$location', '$uibModal', '_t'
, ($scope, $state, projectPromise, $location, $uibModal, _t) ->
### PUBLIC SCOPE ### ### PUBLIC SCOPE ###
## Will be set to true once the project details are loaded. Used to load the Disqus plugin at the right moment
$scope.contentLoaded = false
## Store the project's details ## Store the project's details
$scope.project = {} $scope.project = projectPromise
$scope.projectUrl = $location.absUrl()
$scope.disqusShortname = Fablab.disqusShortname
## ##
@ -336,23 +307,63 @@ Application.Controllers.controller "showProjectController", ["$scope", "$state",
### PRIVATE SCOPE ### ##
# Test if the provided user has the deletion rights on the current project
# @param [user] {{id:number}} (optional) the user to check rights
# @returns boolean
##
$scope.projectDeletableBy = (user) ->
return false if not user?
return true if $scope.project.author_id == user.id
## ##
# Kind of constructor: these actions will be realized first when the controller is loaded # Callback to delete the current project. Then, the user is redirected to the projects list page,
# which is refreshed. Admins and project owner only are allowed to delete a project
## ##
initialize = -> $scope.deleteProject = ->
## Retrieve the project content # check the permissions
$scope.project = Project.get {id: $stateParams.id} if $scope.currentUser.role is 'admin' or $scope.projectDeletableBy($scope.currentUser)
, -> # success # delete the project then refresh the projects list
$scope.contentLoaded = true $scope.project.$delete ->
$scope.project_url = $location.absUrl() $state.go('app.public.projects_list', {}, {reload: true})
return else
, -> # failed, redirect the user to the projects listing console.error _t('unauthorized_operation')
$state.go('app.public.projects_list')
##
# Open a modal box containg a form that allow the end-user to signal an abusive content
# @param e {Object} jQuery event
##
$scope.signalAbuse = (e) ->
e.preventDefault() if e
$uibModal.open
templateUrl: '<%= asset_path "shared/signalAbuseModal.html" %>'
size: 'md'
resolve:
project: -> $scope.project
controller: ['$scope', '$uibModalInstance', '_t', 'growl', 'Abuse', 'project', ($scope, $uibModalInstance, _t, growl, Abuse, project) ->
# signaler's profile & signalement infos
$scope.signaler = {
signaled_type: 'Project'
signaled_id: project.id
}
# callback for signaling cancellation
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
# callback for form validation
$scope.ok = ->
Abuse.save {}, {abuse: $scope.signaler}, (res) ->
# creation successful
growl.success(_t('your_report_was_successful_thanks'))
$uibModalInstance.close(res)
, (error) ->
# creation failed...
growl.error(_t('an_error_occured_while_sending_your_report'))
]
## !!! MUST BE CALLED AT THE END of the controller
initialize()
] ]

View File

@ -0,0 +1,658 @@
'use strict'
##
# Controller used in the training reservation agenda page.
# This controller is very similar to the machine reservation controller with one major difference: here, ONLY ONE
# training can be reserved during the reservation process (the shopping cart may contains only one training and a subscription).
##
Application.Controllers.controller "ReserveTrainingController", ["$scope", "$state", '$stateParams', "$uibModal", 'Auth', 'dialogs', '$timeout', 'Price', 'Availability', 'Slot', 'Member', 'Setting', 'CustomAsset', '$compile', 'availabilityTrainingsPromise', 'plansPromise', 'groupsPromise', 'growl', 'settingsPromise', '_t',
($scope, $state, $stateParams, $uibModal, Auth, dialogs, $timeout, Price, Availability, Slot, Member, Setting, CustomAsset, $compile, availabilityTrainingsPromise, plansPromise, groupsPromise, growl, settingsPromise, _t) ->
### PRIVATE STATIC CONSTANTS ###
# The calendar is divided in slots of 60 minutes
BASE_SLOT = '01:00:00'
# The calendar will be initialized positioned under 9:00 AM
DEFAULT_CALENDAR_POSITION = '09:00:00'
# The user is unable to modify his already booked reservation 1 day before it occurs
PREVENT_BOOKING_MODIFICATION_DELAY = 1
# Color of the selected event backgound
SELECTED_EVENT_BG_COLOR = '#ffdd00'
# Slot already booked by the current user
FREE_SLOT_BORDER_COLOR = '#bd7ae9'
### PUBLIC SCOPE ###
## after fullCalendar loads, provides access to its methods through $scope.calendar.fullCalendar()
$scope.calendar = null
## bind the trainings availabilities with full-Calendar events
$scope.eventSources = [ { events: availabilityTrainingsPromise, textColor: 'black' } ]
## the user to deal with, ie. the current user for non-admins
$scope.ctrl =
member: {}
## the full list of members, used by admin to select a user to interact with
$scope.members = []
## list of plans, classified by group
$scope.plansClassifiedByGroup = []
for group in groupsPromise
groupObj = { id: group.id, name: group.name, plans: [] }
for plan in plansPromise
groupObj.plans.push(plan) if plan.group_id == group.id
$scope.plansClassifiedByGroup.push(groupObj)
## indicates the state of the current view : calendar or plans informations
$scope.plansAreShown = false
## indicates if the selected training was validated (ie. added to the shopping cart)
$scope.trainingIsValid = false
## contains the selected training once it was payed, allows to display a firendly end-of-shopping message
$scope.paidTraining = null
## will store the user's plan if he choosed to buy one
$scope.selectedPlan = null
## fullCalendar event. Training slot that the user want to book
$scope.selectedTraining = null
## fullCalendar event. An already booked slot that the user want to modify
$scope.slotToModify = null
## Once a training reservation was modified, will contains {newReservedSlot:{}, oldReservedSlot:{}}
$scope.modifiedSlots = null
## fullCalendar (v2) configuration
$scope.calendarConfig =
timezone: Fablab.timezone
lang: Fablab.fullcalendar_locale
header:
left: 'month agendaWeek'
center: 'title'
right: 'today prev,next'
firstDay: 1 # Week start on monday (France)
scrollTime: DEFAULT_CALENDAR_POSITION
slotDuration: BASE_SLOT
allDayDefault: false
minTime: '00:00:00'
maxTime: '24:00:00'
height: 'auto'
buttonIcons:
prev: 'left-single-arrow'
next: 'right-single-arrow'
timeFormat:
agenda:'H:mm'
month: 'H(:mm)'
axisFormat: 'H:mm'
allDaySlot: false
defaultView: 'agendaWeek'
editable: false
eventClick: (event, jsEvent, view) ->
calendarEventClickCb(event, jsEvent, view)
eventAfterAllRender: (view)->
$scope.events = $scope.calendar.fullCalendar 'clientEvents'
eventRender: (event, element, view) ->
eventRenderCb(event, element, view)
## Custom settings
$scope.subscriptionExplicationsAlert = settingsPromise.subscription_explications_alert
$scope.trainingExplicationsAlert = settingsPromise.training_explications_alert
$scope.trainingInformationMessage = settingsPromise.training_information_message
$scope.enableBookingMove = (settingsPromise.booking_move_enable == "true")
$scope.moveBookingDelay = parseInt(settingsPromise.booking_move_delay)
$scope.enableBookingCancel = (settingsPromise.booking_cancel_enable == "true")
$scope.cancelBookingDelay = parseInt(settingsPromise.booking_cancel_delay)
$scope.calendarConfig.minTime = moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss'))
$scope.calendarConfig.maxTime = moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss'))
##
# Callback to deal with the reservations of the user selected in the dropdown list instead of the current user's
# reservations. (admins only)
##
$scope.updateMember = ->
if $scope.ctrl.member
Availability.trainings {member_id: $scope.ctrl.member.id}, (trainings) ->
$scope.calendar.fullCalendar 'removeEvents'
$scope.eventSources.push
events: trainings
textColor: 'black'
$scope.trainingIsValid = false
$scope.paidTraining = null
$scope.plansAreShown = false
$scope.selectedPlan = null
$scope.selectedTraining = null
$scope.slotToModify = null
$scope.modifiedSlots = null
##
# Callback to mark the selected training as validated (add it to the shopping cart).
##
$scope.validTraining = ->
$scope.trainingIsValid = true
$scope.updatePrices()
##
# Remove the training from the shopping cart
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.removeTraining = (e) ->
e.preventDefault()
$scope.selectedTraining.backgroundColor = 'white'
$scope.selectedTraining = null
$scope.plansAreShown = false
$scope.selectedPlan = null
$scope.trainingIsValid = false
$timeout ->
$scope.calendar.fullCalendar 'refetchEvents'
$scope.calendar.fullCalendar 'rerenderEvents'
##
# Validates the shopping chart and redirect the user to the payment step
##
$scope.payTraining = ->
# first, we check that a user was selected
if Object.keys($scope.ctrl.member).length > 0
reservation = mkReservation($scope.ctrl.member, $scope.selectedTraining, $scope.selectedPlan)
if $scope.currentUser.role isnt 'admin' and $scope.amountTotal > 0
payByStripe(reservation)
else
if $scope.currentUser.role is 'admin' or $scope.amountTotal is 0
payOnSite(reservation)
else
# otherwise we alert, this error musn't occur when the current user is not admin
growl.error(_t('please_select_a_member_first'))
##
# Add the provided plan to the current shopping cart
# @param plan {Object} the plan to subscribe
##
$scope.selectPlan = (plan) ->
if $scope.isAuthenticated()
if $scope.selectedPlan != plan
$scope.selectedPlan = plan
$scope.updatePrices()
else
$scope.selectedPlan = null
$scope.updatePrices()
else
$scope.login null, ->
$scope.selectedPlan = plan
$scope.updatePrices()
##
# Changes the user current view from the plan subsription screen to the machine reservation agenda
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.doNotSubscribePlan = (e)->
e.preventDefault()
$scope.plansAreShown = false
$scope.selectedPlan = null
$scope.updatePrices()
##
# Switch the user's view from the reservation agenda to the plan subscription
##
$scope.showPlans = ->
$scope.plansAreShown = true
##
# Cancel the current booking modification, removing the previously booked slot from the selection
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.removeSlotToModify = (e) ->
e.preventDefault()
if $scope.slotToPlace
$scope.slotToPlace.backgroundColor = 'white'
$scope.slotToPlace.title = $scope.slotToPlace.training.name
$scope.slotToPlace = null
$scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then $scope.slotToModify.training.name + " - " + _t('i_ve_reserved') else $scope.slotToModify.training.name
$scope.slotToModify.backgroundColor = 'white'
$scope.slotToModify = null
$scope.calendar.fullCalendar 'rerenderEvents'
##
# When modifying an already booked reservation, cancel the choice of the new slot
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.removeSlotToPlace = (e)->
e.preventDefault()
$scope.slotToPlace.backgroundColor = 'white'
$scope.slotToPlace.title = $scope.slotToPlace.training.name
$scope.slotToPlace = null
$scope.calendar.fullCalendar 'rerenderEvents'
##
# When modifying an already booked reservation, confirm the modification.
##
$scope.modifyTrainingSlot = ->
Slot.update {id: $scope.slotToModify.slot_id},
slot:
start_at: $scope.slotToPlace.start
end_at: $scope.slotToPlace.end
availability_id: $scope.slotToPlace.id
, -> # success
$scope.modifiedSlots =
newReservedSlot: $scope.slotToPlace
oldReservedSlot: $scope.slotToModify
$scope.slotToPlace.title = if $scope.currentUser.role isnt 'admin' then $scope.slotToPlace.training.name + " - " + _t('i_ve_reserved') else $scope.slotToPlace.training.name
$scope.slotToPlace.backgroundColor = 'white'
$scope.slotToPlace.borderColor = $scope.slotToModify.borderColor
$scope.slotToPlace.slot_id = $scope.slotToModify.slot_id
$scope.slotToPlace.is_reserved = true
$scope.slotToPlace.can_modify = true
$scope.slotToPlace = null
$scope.slotToModify.backgroundColor = 'white'
$scope.slotToModify.title = $scope.slotToModify.training.name
$scope.slotToModify.borderColor = FREE_SLOT_BORDER_COLOR
$scope.slotToModify.slot_id = null
$scope.slotToModify.is_reserved = false
$scope.slotToModify.can_modify = false
$scope.slotToModify.is_completed = false if $scope.slotToModify.is_completed
$scope.slotToModify = null
$scope.calendar.fullCalendar 'rerenderEvents'
, -> # failure
growl.error('an_error_occured_preventing_the_booked_slot_from_being_modified')
##
# Cancel the current booking modification, reseting the whole process
##
$scope.cancelModifyMachineSlot = ->
$scope.slotToPlace.backgroundColor = 'white'
$scope.slotToPlace.title = $scope.slotToPlace.training.name
$scope.slotToPlace = null
$scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then $scope.slotToModify.training.name + " - " + _t('i_ve_reserved') else $scope.slotToModify.training.name
$scope.slotToModify.backgroundColor = 'white'
$scope.slotToModify = null
$scope.calendar.fullCalendar 'rerenderEvents'
##
# Update the prices, based on the current selection
##
$scope.updatePrices = ->
if Object.keys($scope.ctrl.member).length > 0
r = mkReservation($scope.ctrl.member, $scope.selectedTraining, $scope.selectedPlan)
Price.compute {reservation: r}, (res) ->
$scope.amountTotal = res.price
else
$scope.amountTotal = null
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
if $scope.currentUser.role isnt 'admin'
Member.get id: $scope.currentUser.id, (member) ->
$scope.ctrl.member = member
else
Member.query {requested_attributes:'[subscription,credits]'}, (members) ->
$scope.members = members
##
# Create an hash map implementing the Reservation specs
# @param member {Object} User as retreived from the API: current user / selected user if current is admin
# @param training {Object} fullCalendar event: training slot selected on the calendar
# @param [plan] {Object} Plan as retrived from the API: plan to buy with the current reservation
# @return {{user_id:Number, reservable_id:Number, reservable_type:String, slots_attributes:Array<Object>, plan_id:Number|null}}
##
mkReservation = (member, training, plan = null) ->
reservation =
user_id: member.id
reservable_id: training.training.id
reservable_type: 'Training'
slots_attributes: []
plan_id: (plan.id if plan)
reservation.slots_attributes.push
start_at: training.start
end_at: training.end
availability_id: training.id
offered: training.offered || false
reservation
##
# Triggered when the user clicks on a reservation slot in the agenda.
# Defines the behavior to adopt depending on the slot status (already booked, free, ready to be reserved ...),
# the user's subscription (current or about to be took) and the time (the user cannot modify a booked reservation
# if it's too late).
# @see http://fullcalendar.io/docs/mouse/eventClick/
##
calendarEventClickCb = (event, jsEvent, view) ->
if $scope.ctrl.member
# reserve a training if this training will not be reserved and is not about to move and not is completed
if !event.is_reserved && !$scope.slotToModify && !event.is_completed
if event != $scope.selectedTraining
$scope.selectedTraining = event
$scope.selectedTraining.offered = false
event.backgroundColor = SELECTED_EVENT_BG_COLOR
computeTrainingAmount($scope.selectedTraining)
else
$scope.selectedTraining = null
event.backgroundColor = 'white'
$scope.trainingIsValid = false
$scope.paidTraining = null
$scope.selectedPlan = null
$scope.modifiedSlots = null
# clean all others events background
angular.forEach $scope.events, (e)->
if event.id != e.id
e.backgroundColor = 'white'
$scope.calendar.fullCalendar 'rerenderEvents'
# two if below for move training reserved
# if training isnt reserved and have a training to modify and same training and not complete
else if !event.is_reserved && $scope.slotToModify && slotCanBePlaced(event)
if $scope.slotToPlace
$scope.slotToPlace.backgroundColor = 'white'
$scope.slotToPlace.title = event.training.name
$scope.slotToPlace = event
event.backgroundColor = '#bbb'
event.title = event.training.name + ' - ' + _t('i_shift')
$scope.calendar.fullCalendar 'rerenderEvents'
# if training reserved can modify
else if event.is_reserved and (slotCanBeModified(event) or slotCanBeCanceled(event)) and !$scope.slotToModify and !$scope.selectedTraining
event.movable = slotCanBeModified(event)
event.cancelable = slotCanBeCanceled(event)
if $scope.currentUser.role is 'admin'
event.user =
name: $scope.ctrl.member.name
dialogs.confirm
templateUrl: '<%= asset_path "shared/confirm_modify_slot_modal.html" %>'
resolve:
object: -> event
, (type) -> # success
if type == 'move'
$scope.modifiedSlots = null
$scope.slotToModify = event
event.backgroundColor = '#eee'
event.title = event.training.name + ' - ' + _t('i_change')
$scope.calendar.fullCalendar 'rerenderEvents'
else if type == 'cancel'
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_cancel_this_reservation')
, -> # cancel confirmed
Slot.cancel {id: event.slot_id}, -> # successfully canceled
growl.success _t('reservation_was_successfully_cancelled')
$scope.canceledSlot = event
$scope.canceledSlot.backgroundColor = 'white'
$scope.canceledSlot.title = event.training.name
$scope.canceledSlot.borderColor = FREE_SLOT_BORDER_COLOR
$scope.canceledSlot.slot_id = null
$scope.canceledSlot.is_reserved = false
$scope.canceledSlot.can_modify = false
$scope.canceledSlot.is_completed = false if event.is_completed
$scope.canceledSlot = null
$scope.calendar.fullCalendar 'rerenderEvents'
, -> # error while canceling
growl.error _t('cancellation_failed')
, -> # canceled
$scope.paidMachineSlots = null
$scope.selectedPlan = null
$scope.modifiedSlots = null
##
# When events are rendered, adds attributes for popover and compile
# @see http://fullcalendar.io/docs/event_rendering/eventRender/
##
eventRenderCb = (event, element, view)->
element.attr(
'uib-popover': event.training.description
'popover-trigger': 'mouseenter'
'popover-append-to-body': true
)
$compile(element)($scope)
##
# Open a modal window that allows the user to process a credit card payment for his current shopping cart.
##
payByStripe = (reservation) ->
$uibModal.open
templateUrl: '<%= asset_path "stripe/payment_modal.html" %>'
size: 'md'
resolve:
reservation: ->
reservation
price: ->
Price.compute({reservation: reservation}).$promise
cgv: ->
CustomAsset.get({name: 'cgv-file'}).$promise
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'cgv', 'Auth', 'Reservation', ($scope, $uibModalInstance, $state, reservation, price, cgv, Auth, Reservation) ->
# Price
$scope.amount = price.price
# CGV
$scope.cgv = cgv.custom_asset
# Reservation
$scope.reservation = reservation
##
# Callback to process the payment with Stripe, triggered on button click
##
$scope.payment = (status, response) ->
if response.error
growl.error(response.error.message)
else
$scope.attempting = true
$scope.reservation.card_token = response.id
Reservation.save reservation: $scope.reservation, (reservation) ->
$uibModalInstance.close(reservation)
, (response)->
$scope.alerts = []
if response.data.card
$scope.alerts.push
msg: response.data.card[0]
type: 'danger'
else
$scope.alerts.push({msg: _t('a_problem_occured_during_the_payment_process_please_try_again_later'), type: 'danger' })
$scope.attempting = false
]
.result['finally'](null).then (reservation)->
afterPayment(reservation)
##
# Open a modal window that allows the user to process a local payment for his current shopping cart (admin only).
##
payOnSite = (reservation) ->
$uibModal.open
templateUrl: '<%= asset_path "shared/valid_reservation_modal.html" %>'
size: 'sm'
resolve:
reservation: ->
reservation
price: ->
Price.compute({reservation: reservation}).$promise
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation) ->
# Price
$scope.amount = price.price
# Reservation
$scope.reservation = reservation
# Button label
if $scope.amount > 0
$scope.validButtonName = _t('confirm_(payment_on_site)')
else
$scope.validButtonName = _t('confirm')
##
# Callback to process the local payment, triggered on button click
##
$scope.ok = ->
$scope.attempting = true
Reservation.save reservation: $scope.reservation, (reservation) ->
$uibModalInstance.close(reservation)
$scope.attempting = true
, (response)->
$scope.alerts = []
$scope.alerts.push({msg: _t('a_problem_occured_during_the_payment_process_please_try_again_later'), type: 'danger' })
$scope.attempting = false
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
]
.result['finally'](null).then (reservation)->
afterPayment(reservation)
##
# Computes the training amount depending of the member's credit
# @param training {Object} training slot
##
computeTrainingAmount = (training)->
# first we check that a user was selected
if Object.keys($scope.ctrl.member).length > 0
r = mkReservation($scope.ctrl.member, training) # reservation without any Plan -> we get the training price
Price.compute {reservation: r}, (res) ->
$scope.selectedTrainingAmount = res.price
else
$scope.selectedTrainingAmount = null
##
# Once the reservation is booked (payment process successfully completed), change the event style
# in fullCalendar, update the user's subscription and free-credits if needed
# @param reservation {Object}
##
afterPayment = (reservation)->
$scope.paidTraining = $scope.selectedTraining
$scope.paidTraining.backgroundColor = 'white'
$scope.paidTraining.is_reserved = true
$scope.paidTraining.can_modify = true
updateTrainingSlotId($scope.paidTraining, reservation)
$scope.paidTraining.borderColor = '#b2e774'
$scope.paidTraining.title = $scope.paidTraining.training.name + " - " + _t('i_ve_reserved')
$scope.selectedTraining = null
$scope.trainingIsValid = false
if $scope.selectedPlan
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan)
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan)
$scope.plansAreShown = false
$scope.selectedPlan = null
$scope.ctrl.member.training_credits = angular.copy(reservation.user.training_credits)
$scope.ctrl.member.machine_credits = angular.copy(reservation.user.machine_credits)
Auth._currentUser.training_credits = angular.copy(reservation.user.training_credits)
Auth._currentUser.machine_credits = angular.copy(reservation.user.machine_credits)
$scope.calendar.fullCalendar 'refetchEvents'
$scope.calendar.fullCalendar 'rerenderEvents'
##
# Determines if the provided booked slot is able to be modified by the user.
# @param slot {Object} fullCalendar event object
##
slotCanBeModified = (slot)->
return true if $scope.currentUser.role is 'admin'
slotStart = moment(slot.start)
now = moment(new Date())
if slot.can_modify and $scope.enableBookingMove and slotStart.diff(now, "hours") >= $scope.moveBookingDelay
return true
else
return false
##
# Determines if the provided booked slot is able to be canceled by the user.
# @param slot {Object} fullCalendar event object
##
slotCanBeCanceled = (slot) ->
return true if $scope.currentUser.role is 'admin'
slotStart = moment(slot.start)
now = moment()
if slot.can_modify and $scope.enableBookingCancel and slotStart.diff(now, "hours") >= $scope.cancelBookingDelay
return true
else
return false
##
# For booking modifications, checks that the newly selected slot is valid
# @param slot {Object} fullCalendar event object
##
slotCanBePlaced = (slot)->
if slot.training.id == $scope.slotToModify.training.id and !slot.is_completed
return true
else
return false
##
# After payment, update the id of the newly reserved slot with the id returned by the server.
# This will allow the user to modify the reservation he just booked.
# @param slot {Object}
# @param reservation {Object}
##
updateTrainingSlotId = (slot, reservation)->
angular.forEach reservation.slots, (s)->
if slot.start_at == slot.start_at
slot.slot_id = s.id
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]

View File

@ -28,7 +28,7 @@ Application.Directives.directive('bsJasnyFileinput', [function(){
ngModelCtrl.$setValidity('filetype', true); ngModelCtrl.$setValidity('filetype', true);
else else
ngModelCtrl.$setValidity('filetype', false); ngModelCtrl.$setValidity('filetype', false);
} };
} }
$scope.$apply(); $scope.$apply();
}); });

View File

@ -0,0 +1,20 @@
Application.Directives.directive 'confirmationNeeded', [->
return {
priority: 1
terminal: true
link: (scope, element, attrs)->
msg = attrs.confirmationNeeded || "Are you sure?"
clickAction = attrs.ngClick
element.bind 'click', ->
if attrs.confirmationNeededIf?
confirmNeededIf = scope.$eval(attrs.confirmationNeededIf)
if confirmNeededIf == true
if ( window.confirm(msg) )
scope.$eval(clickAction)
else
scope.$eval(clickAction)
else
if ( window.confirm(msg) )
scope.$eval(clickAction)
}
]

View File

@ -21,6 +21,8 @@ Application.Directives.directive 'bsHolder', [ ->
{ {
link: (scope, element, attrs) -> link: (scope, element, attrs) ->
Holder.addTheme("icon", { background: "white", foreground: "#e9e9e9", size: 80, font: "FontAwesome"}) Holder.addTheme("icon", { background: "white", foreground: "#e9e9e9", size: 80, font: "FontAwesome"})
.addTheme("icon-xs", { background: "white", foreground: "#e0e0e0", size: 20, font: "FontAwesome"})
.addTheme("icon-black-xs", { background: "black", foreground: "white", size: 20, font: "FontAwesome"})
.addTheme("avatar", { background: "#eeeeee", foreground: "#555555", size: 16, font: "FontAwesome"}) .addTheme("avatar", { background: "#eeeeee", foreground: "#555555", size: 16, font: "FontAwesome"})
.run(element[0]) .run(element[0])
return return
@ -66,3 +68,38 @@ Application.Directives.directive "disableAnimation", ($animate) ->
attrs.$observe "disableAnimation", (value) -> attrs.$observe "disableAnimation", (value) ->
$animate.enabled not value, elem $animate.enabled not value, elem
##
# Isolate a form's scope from its parent : no nested validation
##
Application.Directives.directive 'isolateForm', [ ->
{
restrict: 'A',
require: '?form'
link: (scope, elm, attrs, ctrl) ->
return unless ctrl
# Do a copy of the controller
ctrlCopy = {}
angular.copy(ctrl, ctrlCopy)
# Get the form's parent
parent = elm.parent().controller('form')
# Remove parent link to the controller
parent.$removeControl(ctrl)
# Replace form controller with a "isolated form"
isolatedFormCtrl =
$setValidity: (validationToken, isValid, control) ->
ctrlCopy.$setValidity(validationToken, isValid, control);
parent.$setValidity(validationToken, true, ctrl);
$setDirty: ->
elm.removeClass('ng-pristine').addClass('ng-dirty');
ctrl.$dirty = true;
ctrl.$pristine = false;
angular.extend(ctrl, isolatedFormCtrl)
}
]

View File

@ -0,0 +1,24 @@
'use strict';
// https://github.com/gtramontina/stripe-angular
Application.Directives.directive('stripeForm', ['$window',
function($window) {
var directive = { restrict: 'A' };
directive.link = function(scope, element, attributes) {
var form = angular.element(element);
form.bind('submit', function() {
var button = form.find('button');
button.prop('disabled', true);
$window.Stripe.createToken(form[0], function() {
var args = arguments;
scope.$apply(function() {
scope[attributes.stripeForm].apply(scope, args);
});
//button.prop('disabled', false);
});
});
};
return directive;
}]);

View File

@ -0,0 +1,34 @@
'use strict'
Application.Directives.directive 'url', [ ->
URL_REGEXP = /^(https?:\/\/)([\da-z\.-]+)\.([-a-z0-9\.]{2,30})([\/\w \.-]*)*\/?$/
{
require: 'ngModel'
link: (scope, element, attributes, ctrl) ->
ctrl.$validators.url = (modelValue, viewValue) ->
if ctrl.$isEmpty(modelValue)
return true
if URL_REGEXP.test(viewValue)
return true
# otherwise, this is invalid
return false
}
]
Application.Directives.directive 'endpoint', [ ->
ENDPOINT_REGEXP = /^\/([-._~:?#\[\]@!$&'()*+,;=%\w]+\/?)*$/
{
require: 'ngModel'
link: (scope, element, attributes, ctrl) ->
ctrl.$validators.endpoint = (modelValue, viewValue) ->
if ctrl.$isEmpty(modelValue)
return true
if ENDPOINT_REGEXP.test(viewValue)
return true
# otherwise, this is invalid
return false
}
]

View File

@ -1,7 +1,19 @@
'use strict' 'use strict'
Application.Filters.filter 'array', [ ->
(arrayLength) ->
if (arrayLength)
arrayLength = Math.ceil(arrayLength)
arr = new Array(arrayLength)
for i in [0 ... arrayLength]
arr[i] = i
arr
]
# filter for projects and trainings # filter for projects and trainings
Application.Controllers.filter "machineFilter", [ -> Application.Filters.filter "machineFilter", [ ->
(elements, selectedMachine) -> (elements, selectedMachine) ->
if !angular.isUndefined(elements) and !angular.isUndefined(selectedMachine) and elements? and selectedMachine? if !angular.isUndefined(elements) and !angular.isUndefined(selectedMachine) and elements? and selectedMachine?
filteredElements = [] filteredElements = []
@ -13,7 +25,7 @@ Application.Controllers.filter "machineFilter", [ ->
elements elements
] ]
Application.Controllers.filter "projectMemberFilter", [ "Auth", (Auth)-> Application.Filters.filter "projectMemberFilter", [ "Auth", (Auth)->
(projects, selectedMember) -> (projects, selectedMember) ->
if !angular.isUndefined(projects) and angular.isDefined(selectedMember) and projects? and selectedMember? and selectedMember != "" if !angular.isUndefined(projects) and angular.isDefined(selectedMember) and projects? and selectedMember? and selectedMember != ""
filteredProject = [] filteredProject = []
@ -32,7 +44,7 @@ Application.Controllers.filter "projectMemberFilter", [ "Auth", (Auth)->
projects projects
] ]
Application.Controllers.filter "themeFilter", [ -> Application.Filters.filter "themeFilter", [ ->
(projects, selectedTheme) -> (projects, selectedTheme) ->
if !angular.isUndefined(projects) and !angular.isUndefined(selectedTheme) and projects? and selectedTheme? if !angular.isUndefined(projects) and !angular.isUndefined(selectedTheme) and projects? and selectedTheme?
filteredProjects = [] filteredProjects = []
@ -44,7 +56,7 @@ Application.Controllers.filter "themeFilter", [ ->
projects projects
] ]
Application.Controllers.filter "componentFilter", [ -> Application.Filters.filter "componentFilter", [ ->
(projects, selectedComponent) -> (projects, selectedComponent) ->
if !angular.isUndefined(projects) and !angular.isUndefined(selectedComponent) and projects? and selectedComponent? if !angular.isUndefined(projects) and !angular.isUndefined(selectedComponent) and projects? and selectedComponent?
filteredProjects = [] filteredProjects = []
@ -56,7 +68,7 @@ Application.Controllers.filter "componentFilter", [ ->
projects projects
] ]
Application.Controllers.filter "projectsByAuthor", [ -> Application.Filters.filter "projectsByAuthor", [ ->
(projects, authorId) -> (projects, authorId) ->
if !angular.isUndefined(projects) and angular.isDefined(authorId) and projects? and authorId? and authorId != "" if !angular.isUndefined(projects) and angular.isDefined(authorId) and projects? and authorId? and authorId != ""
filteredProject = [] filteredProject = []
@ -68,7 +80,7 @@ Application.Controllers.filter "projectsByAuthor", [ ->
projects projects
] ]
Application.Controllers.filter "projectsCollabored", [ -> Application.Filters.filter "projectsCollabored", [ ->
(projects, memberId) -> (projects, memberId) ->
if !angular.isUndefined(projects) and angular.isDefined(memberId) and projects? and memberId? and memberId != "" if !angular.isUndefined(projects) and angular.isDefined(memberId) and projects? and memberId? and memberId != ""
filteredProject = [] filteredProject = []
@ -81,24 +93,84 @@ Application.Controllers.filter "projectsCollabored", [ ->
] ]
# depend on humanize.js lib in /vendor # depend on humanize.js lib in /vendor
Application.Controllers.filter "humanize", [ -> Application.Filters.filter "humanize", [ ->
(element, param) -> (element, param) ->
Humanize.truncate(element, param, null) Humanize.truncate(element, param, null)
] ]
Application.Controllers.filter "breakFilter", [ -> Application.Filters.filter "breakFilter", [ ->
(text) -> (text) ->
if text != undefined if text != undefined
text.replace(/\n/g, '<br />') text.replace(/\n/g, '<br />')
] ]
Application.Controllers.filter "toTrusted", [ "$sce", ($sce) -> Application.Filters.filter "toTrusted", [ "$sce", ($sce) ->
(text) -> (text) ->
$sce.trustAsHtml text $sce.trustAsHtml text
] ]
Application.Controllers.filter "eventsFilter", [ -> Application.Filters.filter "planIntervalFilter", [ ->
(interval, intervalCount) ->
if typeof intervalCount != 'number'
switch interval
when 'day' then return 'jour'
when 'week' then return 'semaine'
when 'month' then return 'mois'
when 'year' then return 'année'
else
if intervalCount == 1
switch interval
when 'day' then return 'un jour'
when 'week' then return 'une semaine'
when 'month' then return 'un mois'
when 'year' then return 'un an'
else
switch interval
when 'day' then return intervalCount+ ' jours'
when 'week' then return intervalCount+ ' semaines'
when 'month' then return intervalCount+ ' mois'
when 'year' then return intervalCount+ ' ans'
]
Application.Filters.filter "humanReadablePlanName", ['$filter', ($filter)->
(plan, groups, short) ->
if plan?
result = plan.base_name
if groups?
for group in groups
if group.id == plan.group_id
if short?
result += " - #{group.slug}"
else
result += " - #{group.name}"
result += " - #{$filter('planIntervalFilter')(plan.interval, plan.interval_count)}"
result
]
Application.Filters.filter "trainingReservationsFilter", [ ->
(elements, selectedScope) ->
if !angular.isUndefined(elements) and !angular.isUndefined(selectedScope) and elements? and selectedScope?
filteredElements = []
angular.forEach elements, (element)->
switch selectedScope
when "future"
if new Date(element.start_at) > new Date
filteredElements.push(element)
when "passed"
if new Date(element.start_at) <= new Date and !element.is_valid
filteredElements.push(element)
when "valided"
if new Date(element.start_at) <= new Date and element.is_valid
filteredElements.push(element)
else
return []
filteredElements
else
elements
]
Application.Filters.filter "eventsReservationsFilter", [ ->
(elements, selectedScope) -> (elements, selectedScope) ->
if !angular.isUndefined(elements) and !angular.isUndefined(selectedScope) and elements? and selectedScope? and selectedScope != "" if !angular.isUndefined(elements) and !angular.isUndefined(selectedScope) and elements? and selectedScope? and selectedScope != ""
filteredElements = [] filteredElements = []
@ -117,3 +189,43 @@ Application.Controllers.filter "eventsFilter", [ ->
else else
elements elements
] ]
Application.Filters.filter "groupFilter", [ ->
(elements, member) ->
if !angular.isUndefined(elements) and !angular.isUndefined(member) and elements? and member?
filteredElements = []
angular.forEach elements, (element)->
if member.group_id == element.id
filteredElements.push(element)
filteredElements
else
elements
]
Application.Filters.filter "groupByFilter", [ ->
_.memoize (elements, field)->
_.groupBy(elements, field)
]
Application.Filters.filter "capitalize", [->
(text)->
"#{text.charAt(0).toUpperCase()}#{text.slice(1).toLowerCase()}"
]
Application.Filters.filter 'reverse', [ ->
(items) ->
unless angular.isArray(items)
return items
items.slice().reverse()
]
Application.Filters.filter 'toArray', [ ->
(obj) ->
return obj unless (obj instanceof Object)
_.map obj, (val, key) ->
if angular.isObject(val)
Object.defineProperty(val, '$key', {__proto__: null, value: key})
]

View File

@ -1,222 +1,844 @@
angular.module('application.router', ['ui.router']). angular.module('application.router', ['ui.router']).
config ['$stateProvider', '$urlRouterProvider', '$locationProvider', ($stateProvider, $urlRouterProvider, $locationProvider) -> config ['$stateProvider', '$urlRouterProvider', '$locationProvider', ($stateProvider, $urlRouterProvider, $locationProvider) ->
$locationProvider.hashPrefix('!') $locationProvider.hashPrefix('!')
$urlRouterProvider.otherwise("/") $urlRouterProvider.otherwise("/")
# abstract root parents states # abstract root parents states
# these states controls the access rights to the various routes inherited from them # these states controls the access rights to the various routes inherited from them
$stateProvider $stateProvider
.state 'app', .state 'app',
abstract: true abstract: true
views: views:
'header': { templateUrl: '<%= asset_path "shared/header.html" %>' } 'header':
'leftnav': templateUrl: '<%= asset_path "shared/header.html" %>'
templateUrl: '<%= asset_path "shared/leftnav.html" %>' 'leftnav':
controller: 'mainNavController' templateUrl: '<%= asset_path "shared/leftnav.html" %>'
'main': controller: 'MainNavController'
templateUrl: '<%= asset_path "home.html" %>' 'main': {}
controller: 'homeController' resolve:
.state 'app.public', logoFile: ['CustomAsset', (CustomAsset) ->
abstract: true CustomAsset.get({name: 'logo-file'}).$promise
.state 'app.logged', ]
abstract: true logoBlackFile: ['CustomAsset', (CustomAsset) ->
data: CustomAsset.get({name: 'logo-black-file'}).$promise
authorizedRoles: ['member', 'admin'] ]
resolve: commonTranslations: [ 'Translations', (Translations) ->
currentUser: ['Auth', (Auth)-> Translations.query(['app.public.common', 'app.shared.buttons', 'app.shared.elements']).$promise
Auth.currentUser() ]
] onEnter: ['$rootScope', 'logoFile', 'logoBlackFile', ($rootScope, logoFile, logoBlackFile) ->
onEnter: ["currentUser", "$rootScope", (currentUser, $rootScope)-> ## Application logo
$rootScope.currentUser = currentUser $rootScope.logo = logoFile.custom_asset
] $rootScope.logoBlack = logoBlackFile.custom_asset
.state 'app.admin', ]
abstract: true .state 'app.public',
data: abstract: true
authorizedRoles: ['admin'] .state 'app.logged',
resolve: abstract: true
currentUser: ['Auth', (Auth)-> data:
Auth.currentUser() authorizedRoles: ['member', 'admin']
] resolve:
onEnter: ["currentUser", "$rootScope", (currentUser, $rootScope)-> currentUser: ['Auth', (Auth)->
$rootScope.currentUser = currentUser Auth.currentUser()
] ]
onEnter: ["$state", "$timeout", "currentUser", "$rootScope", ($state, $timeout, currentUser, $rootScope)->
$rootScope.currentUser = currentUser
]
.state 'app.admin',
abstract: true
data:
authorizedRoles: ['admin']
resolve:
currentUser: ['Auth', (Auth)->
Auth.currentUser()
]
onEnter: ["$state", "$timeout", "currentUser", "$rootScope", ($state, $timeout, currentUser, $rootScope)->
$rootScope.currentUser = currentUser
]
# main pages # main pages
.state 'app.public.about', .state 'app.public.about',
url: '/about' url: '/about'
views: views:
'content@': { templateUrl: '<%= asset_path "shared/about.html" %>' } 'content@':
.state 'app.public.home', templateUrl: '<%= asset_path "shared/about.html" %>'
url: '/?reset_password_token' controller: 'AboutController'
views: resolve:
'main': translations: [ 'Translations', (Translations) ->
templateUrl: '<%= asset_path "home.html" %>' Translations.query('app.public.about').$promise
controller: 'homeController' ]
.state 'app.public.home',
url: '/?reset_password_token'
views:
'main@':
templateUrl: '<%= asset_path "home.html" %>'
controller: 'HomeController'
resolve:
lastMembersPromise: ['Member', (Member)->
Member.lastSubscribed(limit: 4).$promise
]
lastProjectsPromise: ['Project', (Project)->
Project.lastPublished().$promise
]
upcomingEventsPromise: ['Event', (Event)->
Event.upcoming(limit: 3).$promise
]
homeBlogpostPromise: ['Setting', (Setting)->
Setting.get(name: 'home_blogpost').$promise
]
twitterNamePromise: ['Setting', (Setting)->
Setting.get(name: 'twitter_name').$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query('app.public.home').$promise
]
# profile completion (SSO import passage point)
# dashboard .state 'app.logged.profileCompletion',
.state 'app.logged.dashboard_profile', url: '/profile_completion'
url: '/dashboard/profile' views:
views: 'main@':
'main@': templateUrl: '<%= asset_path "profile/complete.html"%>'
templateUrl: '<%= asset_path "dashboard/profile.html" %>' controller: 'CompleteProfileController'
controller: 'editProfileController' resolve:
.state 'app.logged.dashboard_projects', settingsPromise: ['Setting', (Setting)->
url: '/dashboard/projects' Setting.query(names: "['fablab_name', 'name_genre']").$promise
views: ]
'main@': activeProviderPromise: ['AuthProvider', (AuthProvider) ->
templateUrl: '<%= asset_path "dashboard/projects.html" %>' AuthProvider.active().$promise
controller: 'dashboardProjectsController' ]
groupsPromise: ['Group', (Group)->
Group.query().$promise
# members ]
.state 'app.logged.members_show', cguFile: ['CustomAsset', (CustomAsset) ->
url: '/members/:id' CustomAsset.get({name: 'cgu-file'}).$promise
views: ]
'main@': memberPromise: ['Member', 'currentUser', (Member, currentUser)->
templateUrl: '<%= asset_path "members/show.html" %>' Member.get(id: currentUser.id).$promise
controller: 'showProfileController' ]
.state 'app.logged.members', translations: [ 'Translations', (Translations) ->
url: '/members' Translations.query(['app.logged.profileCompletion', 'app.shared.user']).$promise
views: ]
'main@':
templateUrl: '<%= asset_path "members/index.html" %>'
controller: 'membersController'
# projects
.state 'app.public.projects_list',
url: '/projects'
views:
'main@':
templateUrl: '<%= asset_path "projects/index.html" %>'
controller: 'projectsController'
.state 'app.public.projects_show',
url: '/projects/:id'
views:
'main@':
templateUrl: '<%= asset_path "projects/show.html" %>'
controller: 'showProjectController'
.state 'app.logged.projects_new',
url: '/projects/new'
views:
'main@':
templateUrl: '<%= asset_path "projects/new.html" %>'
controller: 'newProjectController'
.state 'app.logged.projects_edit',
url: '/projects/:id/edit'
views:
'main@':
templateUrl: '<%= asset_path "projects/edit.html" %>'
controller: 'editProjectController'
# machines # dashboard
.state 'app.public.machines_list', .state 'app.logged.dashboard',
url: '/machines' abstract: true
views: url: '/dashboard'
'main@': resolve:
templateUrl: '<%= asset_path "machines/index.html" %>' memberPromise: ['Member', 'currentUser', (Member, currentUser)->
controller: 'machinesController' Member.get(id: currentUser.id).$promise
.state 'app.public.machines_show', ]
url: '/machines/:id' .state 'app.logged.dashboard.profile',
views: url: '/profile'
'main@': views:
templateUrl: '<%= asset_path "machines/show.html" %>' 'main@':
controller: 'showMachineController' templateUrl: '<%= asset_path "dashboard/profile.html" %>'
.state 'app.admin.machines_new', controller: 'EditProfileController'
url: '/machines/new' resolve:
views: groups: ['Group', (Group)->
'main@': Group.query().$promise
templateUrl: '<%= asset_path "machines/new.html" %>' ]
controller: 'newMachineController' activeProviderPromise: ['AuthProvider', (AuthProvider) ->
.state 'app.admin.machines_edit', AuthProvider.active().$promise
url: '/machines/:id/edit' ]
views: translations: [ 'Translations', (Translations) ->
'main@': Translations.query(['app.logged.dashboard.profile', 'app.shared.user']).$promise
templateUrl: '<%= asset_path "machines/edit.html" %>' ]
controller: 'editMachineController' .state 'app.logged.dashboard.projects',
url: '/projects'
views:
'main@':
templateUrl: '<%= asset_path "dashboard/projects.html" %>'
controller: 'DashboardController'
resolve:
translations: [ 'Translations', (Translations) ->
Translations.query('app.logged.dashboard.projects').$promise
]
.state 'app.logged.dashboard.trainings',
url: '/trainings'
views:
'main@':
templateUrl: '<%= asset_path "dashboard/trainings.html" %>'
controller: 'DashboardController'
resolve:
translations: [ 'Translations', (Translations) ->
Translations.query('app.logged.dashboard.trainings').$promise
]
.state 'app.logged.dashboard.events',
url: '/events'
views:
'main@':
templateUrl: '<%= asset_path "dashboard/events.html" %>'
controller: 'DashboardController'
resolve:
translations: [ 'Translations', (Translations) ->
Translations.query('app.logged.dashboard.events').$promise
]
.state 'app.logged.dashboard.invoices',
url: '/invoices'
views:
'main@':
templateUrl: '<%= asset_path "dashboard/invoices.html" %>'
controller: 'DashboardController'
resolve:
translations: [ 'Translations', (Translations) ->
Translations.query('app.logged.dashboard.invoices').$promise
]
# notifications # members
.state 'app.logged.notifications', .state 'app.logged.members_show',
url: '/notifications' url: '/members/:id'
views: views:
'main@': 'main@':
templateUrl: '<%= asset_path "notifications/index.html" %>' templateUrl: '<%= asset_path "members/show.html" %>'
controller: 'notificationsController' controller: 'ShowProfileController'
resolve:
memberPromise: ['$stateParams', 'Member', ($stateParams, Member)->
Member.get(id: $stateParams.id).$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query('app.logged.members_show').$promise
]
.state 'app.logged.members',
url: '/members'
views:
'main@':
templateUrl: '<%= asset_path "members/index.html" %>'
controller: 'MembersController'
resolve:
membersPromise: ['Member', (Member)->
Member.query({requested_attributes:'[profile]'}).$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query('app.logged.members').$promise
]
# projects
.state 'app.public.projects_list',
url: '/projects'
views:
'main@':
templateUrl: '<%= asset_path "projects/index.html" %>'
controller: 'ProjectsController'
resolve:
themesPromise: ['Theme', (Theme)->
Theme.query().$promise
]
componentsPromise: ['Component', (Component)->
Component.query().$promise
]
machinesPromise: ['Machine', (Machine)->
Machine.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query('app.public.projects_list').$promise
]
.state 'app.logged.projects_new',
url: '/projects/new'
views:
'main@':
templateUrl: '<%= asset_path "projects/new.html" %>'
controller: 'NewProjectController'
resolve:
translations: [ 'Translations', (Translations) ->
Translations.query(['app.logged.projects_new', 'app.shared.project']).$promise
]
.state 'app.public.projects_show',
url: '/projects/:id'
views:
'main@':
templateUrl: '<%= asset_path "projects/show.html" %>'
controller: 'ShowProjectController'
resolve:
projectPromise: ['$stateParams', 'Project', ($stateParams, Project)->
Project.get(id: $stateParams.id).$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query('app.public.projects_show').$promise
]
.state 'app.logged.projects_edit',
url: '/projects/:id/edit'
views:
'main@':
templateUrl: '<%= asset_path "projects/edit.html" %>'
controller: 'EditProjectController'
resolve:
projectPromise: ['$stateParams', 'Project', ($stateParams, Project)->
Project.get(id: $stateParams.id).$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.logged.projects_edit', 'app.shared.project']).$promise
]
# events # machines
.state 'app.public.events_list', .state 'app.public.machines_list',
url: '/events' url: '/machines'
views: views:
'main@': 'main@':
templateUrl: '<%= asset_path "events/index.html" %>' templateUrl: '<%= asset_path "machines/index.html" %>'
controller: 'eventsController' controller: 'MachinesController'
.state 'app.public.events_show', resolve:
url: '/events/:id' machinesPromise: ['Machine', (Machine)->
views: Machine.query().$promise
'main@': ]
templateUrl: '<%= asset_path "events/show.html" %>' translations: [ 'Translations', (Translations) ->
controller: 'showEventController' Translations.query(['app.public.machines_list', 'app.shared.training_reservation_modal', 'app.shared.request_training_modal']).$promise
]
.state 'app.admin.machines_new',
url: '/machines/new'
views:
'main@':
templateUrl: '<%= asset_path "machines/new.html" %>'
controller: 'NewMachineController'
resolve:
translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.machines_new', 'app.shared.machine']).$promise
]
.state 'app.public.machines_show',
url: '/machines/:id'
views:
'main@':
templateUrl: '<%= asset_path "machines/show.html" %>'
controller: 'ShowMachineController'
resolve:
machinePromise: ['Machine', '$stateParams', (Machine, $stateParams)->
Machine.get(id: $stateParams.id).$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.public.machines_show', 'app.shared.training_reservation_modal', 'app.shared.request_training_modal']).$promise
]
.state 'app.logged.machines_reserve',
url: '/machines/:id/reserve'
views:
'main@':
templateUrl: '<%= asset_path "machines/reserve.html" %>'
controller: 'ReserveMachineController'
resolve:
plansPromise: ['Plan', (Plan)->
Plan.query(attributes_requested: "['machines_credits']").$promise
]
groupsPromise: ['Group', (Group)->
Group.query().$promise
]
settingsPromise: ['Setting', (Setting)->
Setting.query(names: "['machine_explications_alert',
'booking_window_start',
'booking_window_end',
'booking_move_enable',
'booking_move_delay',
'booking_cancel_enable',
'booking_cancel_delay',
'subscription_explications_alert']").$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.logged.machines_reserve', 'app.shared.plan_subscribe', 'app.shared.member_select',
'app.shared.stripe', 'app.shared.valid_reservation_modal', 'app.shared.confirm_modify_slot_modal']).$promise
]
.state 'app.admin.machines_edit',
url: '/machines/:id/edit'
views:
'main@':
templateUrl: '<%= asset_path "machines/edit.html" %>'
controller: 'EditMachineController'
resolve:
machinePromise: ['Machine', '$stateParams', (Machine, $stateParams)->
Machine.get(id: $stateParams.id).$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.machines_edit', 'app.shared.machine']).$promise
]
# trainings
.state 'app.logged.trainings_reserve',
url: '/trainings/reserve'
views:
'main@':
templateUrl: '<%= asset_path "trainings/reserve.html" %>'
controller: 'ReserveTrainingController'
resolve:
explicationAlertPromise: ['Setting', (Setting)->
Setting.get(name: 'training_explications_alert').$promise
]
plansPromise: ['Plan', (Plan)->
Plan.query(attributes_requested: "['trainings_credits']").$promise
]
groupsPromise: ['Group', (Group)->
Group.query().$promise
]
availabilityTrainingsPromise: ['Availability', (Availability)->
Availability.trainings().$promise
]
settingsPromise: ['Setting', (Setting)->
Setting.query(names: "['booking_window_start',
'booking_window_end',
'booking_move_enable',
'booking_move_delay',
'booking_cancel_enable',
'booking_cancel_delay',
'subscription_explications_alert',
'training_explications_alert',
'training_information_message']").$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.logged.trainings_reserve', 'app.shared.plan_subscribe', 'app.shared.member_select',
'app.shared.stripe', 'app.shared.valid_reservation_modal', 'app.shared.confirm_modify_slot_modal']).$promise
]
# notifications
.state 'app.logged.notifications',
url: '/notifications'
views:
'main@':
templateUrl: '<%= asset_path "notifications/index.html" %>'
controller: 'NotificationsController'
resolve:
translations: [ 'Translations', (Translations) ->
Translations.query('app.logged.notifications').$promise
]
# pricing
.state 'app.public.plans',
url: '/plans'
abstract: Fablab.withoutPlans
views:
'main@':
templateUrl: '<%= asset_path "plans/index.html" %>'
controller: 'PlansIndexController'
resolve:
subscriptionExplicationsPromise: ['Setting', (Setting)->
Setting.get(name: 'subscription_explications_alert').$promise
]
plansPromise: ['Plan', (Plan)->
Plan.query(shallow: true).$promise
]
groupsPromise: ['Group', (Group)->
Group.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.public.plans', 'app.shared.member_select', 'app.shared.stripe']).$promise
]
# events
.state 'app.public.events_list',
url: '/events'
views:
'main@':
templateUrl: '<%= asset_path "events/index.html" %>'
controller: 'EventsController'
resolve:
translations: [ 'Translations', (Translations) ->
Translations.query('app.public.events_list').$promise
]
.state 'app.public.events_show',
url: '/events/:id'
views:
'main@':
templateUrl: '<%= asset_path "events/show.html" %>'
controller: 'ShowEventController'
resolve:
eventPromise: ['Event', '$stateParams', (Event, $stateParams)->
Event.get(id: $stateParams.id).$promise
]
reducedAmountAlert: ['Setting', (Setting)->
Setting.get(name: 'event_reduced_amount_alert').$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.public.events_show', 'app.shared.member_select', 'app.shared.stripe', 'app.shared.valid_reservation_modal']).$promise
]
# --- namespace /admin/... ---
# calendar
.state 'app.admin.calendar',
url: '/admin/calendar'
views:
'main@':
templateUrl: '<%= asset_path "admin/calendar/calendar.html" %>'
controller: 'AdminCalendarController'
resolve:
availabilitiesPromise: ['Availability', (Availability)->
Availability.query().$promise
]
bookingWindowStart: ['Setting', (Setting)->
Setting.get(name: 'booking_window_start').$promise
]
bookingWindowEnd: ['Setting', (Setting)->
Setting.get(name: 'booking_window_end').$promise
]
machinesPromise: ['Machine', (Machine) ->
Machine.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query('app.admin.calendar').$promise
]
# project's elements
.state 'app.admin.project_elements',
url: '/admin/project_elements'
views:
'main@':
templateUrl: '<%= asset_path "admin/project_elements/index.html" %>'
controller: 'ProjectElementsController'
resolve:
componentsPromise: ['Component', (Component)->
Component.query().$promise
]
licencesPromise: ['Licence', (Licence)->
Licence.query().$promise
]
themesPromise: ['Theme', (Theme)->
Theme.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query('app.admin.project_elements').$promise
]
# trainings
.state 'app.admin.trainings',
url: '/admin/trainings'
views:
'main@':
templateUrl: '<%= asset_path "admin/trainings/index.html" %>'
controller: 'TrainingsController'
resolve:
trainingsPromise: ['Training', (Training)->
Training.query().$promise
]
machinesPromise: ['Machine', (Machine)->
Machine.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query('app.admin.trainings').$promise
]
# events
.state 'app.admin.events',
url: '/admin/events'
views:
'main@':
templateUrl: '<%= asset_path "admin/events/index.html" %>'
controller: 'AdminEventsController'
resolve:
eventsPromise: ['Event', (Event)->
Event.query(page: 1).$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query('app.admin.events').$promise
]
.state 'app.admin.events_new',
url: '/admin/events/new'
views:
'main@':
templateUrl: '<%= asset_path "events/new.html" %>'
controller: 'NewEventController'
resolve:
translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.events_new', 'app.shared.event']).$promise
]
.state 'app.admin.events_edit',
url: '/admin/events/:id/edit'
views:
'main@':
templateUrl: '<%= asset_path "events/edit.html" %>'
controller: 'EditEventController'
resolve:
eventPromise: ['Event', '$stateParams', (Event, $stateParams)->
Event.get(id: $stateParams.id).$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.events_edit', 'app.shared.event']).$promise
]
.state 'app.admin.event_reservations',
url: '/admin/events/:id/reservations'
views:
'main@':
templateUrl: '<%= asset_path "admin/events/reservations.html" %>'
controller: 'ShowEventReservationsController'
resolve:
eventPromise: ['Event', '$stateParams', (Event, $stateParams)->
Event.get(id: $stateParams.id).$promise
]
reservationsPromise: ['Reservation', '$stateParams', (Reservation, $stateParams)->
Reservation.query(reservable_id: $stateParams.id, reservable_type: 'Event').$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query('app.admin.event_reservations').$promise
]
# pricing
.state 'app.admin.pricing',
url: '/admin/pricing'
views:
'main@':
templateUrl: '<%= asset_path "admin/pricing/index.html" %>'
controller: 'EditPricingController'
resolve:
plans: ['Plan', (Plan) ->
Plan.query().$promise
]
groups: ['Group', (Group) ->
Group.query().$promise
]
machinesPricesPromise: ['Price', (Price)->
Price.query(priceable_type: 'Machine', plan_id: 'null').$promise
]
trainingsPricingsPromise: ['TrainingsPricing', (TrainingsPricing)->
TrainingsPricing.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query('app.admin.pricing').$promise
]
# plans
.state 'app.admin.plans',
abstract: true
resolve:
prices: ['Pricing', (Pricing) ->
Pricing.query().$promise
]
machines: ['Machine', (Machine) ->
Machine.query().$promise
]
groups: ['Group', (Group) ->
Group.query().$promise
]
plans: ['Plan', (Plan) ->
Plan.query().$promise
]
partners: ['User', (User) ->
User.query({role: 'partner'}).$promise
]
.state 'app.admin.plans.new',
url: '/admin/plans/new'
views:
'main@':
templateUrl: '<%= asset_path "admin/plans/new.html" %>'
controller: 'NewPlanController'
resolve:
translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.plans.new', 'app.shared.plan']).$promise
]
.state 'app.admin.plans.edit',
url: '/admin/plans/:id/edit'
views:
'main@':
templateUrl: '<%= asset_path "admin/plans/edit.html" %>'
controller: 'EditPlanController'
resolve:
planPromise: ['Plan', '$stateParams', (Plan, $stateParams) ->
Plan.get({id: $stateParams.id}).$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.plans.edit', 'app.shared.plan']).$promise
]
# --- namespace /admin/... ---
# project's elements # invoices
.state 'app.admin.project_elements', .state 'app.admin.invoices',
url: '/admin/project_elements' url: '/admin/invoices'
views: views:
'main@': 'main@':
templateUrl: '<%= asset_path "admin/project_elements/index.html" %>' templateUrl: '<%= asset_path "admin/invoices/index.html" %>'
controller: 'projectElementsController' controller: 'InvoicesController'
resolve:
settings: ['Setting', (Setting)->
Setting.query(names: "[
'invoice_legals',
'invoice_text',
'invoice_VAT-rate',
'invoice_VAT-active',
'invoice_order-nb',
'invoice_code-value',
'invoice_code-active',
'invoice_reference',
'invoice_logo'
]").$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query('app.admin.invoices').$promise
]
# events # members
.state 'app.admin.events', .state 'app.admin.members',
url: '/admin/events' url: '/admin/members'
views: views:
'main@': 'main@':
templateUrl: '<%= asset_path "admin/events/index.html" %>' templateUrl: '<%= asset_path "admin/members/index.html" %>'
controller: 'adminEventsController' controller: 'AdminMembersController'
.state 'app.admin.events_new', 'groups@app.admin.members':
url: '/admin/events/new' templateUrl: '<%= asset_path "admin/groups/index.html" %>'
views: controller: 'GroupsController'
'main@': 'tags@app.admin.members':
templateUrl: '<%= asset_path "events/new.html" %>' templateUrl: '<%= asset_path "admin/tags/index.html" %>'
controller: 'newEventController' controller: 'TagsController'
.state 'app.admin.events_edit', 'authentification@app.admin.members':
url: '/admin/events/:id/edit' templateUrl: '<%= asset_path "admin/authentications/index.html" %>'
views: controller: 'AuthentificationController'
'main@': resolve:
templateUrl: '<%= asset_path "events/edit.html" %>' membersPromise: ['Member', (Member)->
controller: 'editEventController' Member.query({requested_attributes:'[profile,group,subscription]'}).$promise
]
adminsPromise: ['Admin', (Admin)->
Admin.query().$promise
]
groupsPromise: ['Group', (Group)->
Group.query().$promise
]
tagsPromise: ['Tag', (Tag)->
Tag.query().$promise
]
authProvidersPromise: ['AuthProvider', (AuthProvider)->
AuthProvider.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query('app.admin.members').$promise
]
.state 'app.admin.members_new',
url: '/admin/members/new'
views:
'main@':
templateUrl: '<%= asset_path "admin/members/new.html" %>'
controller: 'NewMemberController'
resolve:
translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.members_new', 'app.shared.user', 'app.shared.user_admin']).$promise
]
.state 'app.admin.members_edit',
url: '/admin/members/:id/edit'
views:
'main@':
templateUrl: '<%= asset_path "admin/members/edit.html" %>'
controller: 'EditMemberController'
resolve:
memberPromise: ['Member', '$stateParams', (Member, $stateParams)->
Member.get(id: $stateParams.id).$promise
]
tagsPromise: ['Tag', (Tag)->
Tag.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.members_edit', 'app.shared.user', 'app.shared.user_admin']).$promise
]
.state 'app.admin.admins_new',
url: '/admin/admins/new'
views:
'main@':
templateUrl: '<%= asset_path "admin/admins/new.html" %>'
controller: 'NewAdminController'
resolve:
translations: [ 'Translations', (Translations) ->
Translations.query('app.admin.admins_new').$promise
]
# members # authentification providers
.state 'app.admin.members', .state 'app.admin.authentication_new',
url: '/admin/members' url: '/admin/authentications/new'
views: views:
'main@': 'main@':
templateUrl: '<%= asset_path "admin/members/index.html" %>' templateUrl: '<%= asset_path "admin/authentications/new.html" %>'
controller: 'membersController' controller: 'NewAuthenticationController'
.state 'app.admin.members_new', resolve:
url: '/admin/members/new' mappingFieldsPromise: ['AuthProvider', (AuthProvider)->
views: AuthProvider.mapping_fields().$promise
'main@': ]
templateUrl: '<%= asset_path "admin/members/new.html" %>' authProvidersPromise: ['AuthProvider', (AuthProvider)->
controller: 'newMemberController' AuthProvider.query().$promise
.state 'app.admin.members_edit', ]
url: '/admin/members/:id/edit' translations: [ 'Translations', (Translations) ->
views: Translations.query(['app.admin.authentication_new', 'app.shared.authentication', 'app.shared.oauth2']).$promise
'main@': ]
templateUrl: '<%= asset_path "admin/members/edit.html" %>' .state 'app.admin.authentication_edit',
controller: 'editMemberController' url: '/admin/authentications/:id/edit'
views:
'main@':
templateUrl: '<%= asset_path "admin/authentications/edit.html" %>'
controller: 'EditAuthenticationController'
resolve:
providerPromise: ['AuthProvider', '$stateParams', (AuthProvider, $stateParams)->
AuthProvider.get(id: $stateParams.id).$promise
]
mappingFieldsPromise: ['AuthProvider', (AuthProvider)->
AuthProvider.mapping_fields().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.authentication_edit', 'app.shared.authentication', 'app.shared.oauth2']).$promise
]
# statistics
.state 'app.admin.statistics',
url: '/admin/statistics'
views:
'main@':
templateUrl: '<%= asset_path "admin/statistics/index.html" %>'
controller: 'StatisticsController'
resolve:
translations: [ 'Translations', (Translations) ->
Translations.query('app.admin.statistics').$promise
]
.state 'app.admin.stats_graphs',
url: '/admin/statistics/evolution'
views:
'main@':
templateUrl: '<%= asset_path "admin/statistics/graphs.html" %>'
controller: 'GraphsController'
resolve:
translations: [ 'Translations', (Translations) ->
Translations.query('app.admin.stats_graphs').$promise
]
# configurations
.state 'app.admin.settings',
url: '/admin/settings'
views:
'main@':
templateUrl: '<%= asset_path "admin/settings/index.html" %>'
controller: 'SettingsController'
resolve:
settingsPromise: ['Setting', (Setting)->
Setting.query(names: "[
'twitter_name',
'about_title',
'about_body',
'about_contacts',
'home_blogpost',
'machine_explications_alert',
'training_explications_alert',
'training_information_message',
'subscription_explications_alert',
'event_reduced_amount_alert',
'booking_window_start',
'booking_window_end',
'booking_move_enable',
'booking_move_delay',
'booking_cancel_enable',
'booking_cancel_delay',
'main_color',
'secondary_color',
'fablab_name',
'name_genre'
]").$promise
]
cguFile: ['CustomAsset', (CustomAsset) ->
CustomAsset.get({name: 'cgu-file'}).$promise
]
cgvFile: ['CustomAsset', (CustomAsset) ->
CustomAsset.get({name: 'cgv-file'}).$promise
]
faviconFile: ['CustomAsset', (CustomAsset) ->
CustomAsset.get({name: 'favicon-file'}).$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query('app.admin.settings').$promise
]
] ]

View File

@ -0,0 +1,6 @@
'use strict'
Application.Services.factory '_t', ["$filter", ($filter)->
(key, interpolation = undefined, options = undefined) ->
$filter('translate')(key, interpolation, options)
]

View File

@ -0,0 +1,8 @@
'use strict'
Application.Services.factory 'Abuse', ["$resource", ($resource)->
$resource "/api/abuses/:id",
{id: "@id"},
update:
method: 'PUT'
]

View File

@ -0,0 +1,8 @@
'use strict'
Application.Services.factory 'Admin', ["$resource", ($resource)->
$resource "/api/admins/:id",
{id: "@id"},
query:
isArray: false
]

View File

@ -0,0 +1,14 @@
'use strict'
Application.Services.factory 'AuthProvider', ["$resource", ($resource)->
$resource "/api/auth_providers/:id",
{id: "@id"},
update:
method: 'PUT'
mapping_fields:
method: 'GET'
url: '/api/auth_providers/mapping_fields'
active:
method: 'GET'
url: '/api/auth_providers/active'
]

View File

@ -0,0 +1,21 @@
'use strict'
Application.Services.factory 'Availability', ["$resource", ($resource)->
$resource "/api/availabilities/:id",
{id: "@id"},
machine:
method: 'GET'
url: '/api/availabilities/machines/:machineId'
params: {machineId: "@machineId"}
isArray: true
reservations:
method: 'GET'
url: '/api/availabilities/:id/reservations'
isArray: true
trainings:
method: 'GET'
url: '/api/availabilities/trainings'
isArray: true
update:
method: 'PUT'
]

View File

@ -0,0 +1,8 @@
'use strict'
Application.Services.factory 'Credit', ["$resource", ($resource)->
$resource "/api/credits/:id",
{id: "@id"},
update:
method: 'PUT'
]

View File

@ -0,0 +1,6 @@
'use strict'
Application.Services.factory 'CustomAsset', ["$resource", ($resource)->
$resource "/api/custom_assets/:name",
{name: "@name"}
]

View File

@ -1,6 +1,6 @@
'use strict' 'use strict'
Application.Services.factory 'dialogs', ["$modal", ($modal) -> Application.Services.factory 'dialogs', ["$uibModal", ($uibModal) ->
confirm: (options, success, error)-> confirm: (options, success, error)->
defaultOpts = defaultOpts =
templateUrl: '<%= asset_path "shared/confirm_modal.html" %>' templateUrl: '<%= asset_path "shared/confirm_modal.html" %>'
@ -8,20 +8,20 @@ Application.Services.factory 'dialogs', ["$modal", ($modal) ->
resolve: resolve:
object: -> object: ->
title: 'Titre de confirmation' title: 'Titre de confirmation'
msg: 'Message de confiramtion' msg: 'Message de confirmation'
controller: ['$scope', '$modalInstance', '$state', 'object', ($scope, $modalInstance, $state, object) -> controller: ['$scope', '$uibModalInstance', '$state', 'object', ($scope, $uibModalInstance, $state, object) ->
$scope.object = object $scope.object = object
$scope.ok = -> $scope.ok = (info) ->
$modalInstance.close() $uibModalInstance.close( info )
$scope.cancel = -> $scope.cancel = ->
$modalInstance.dismiss('cancel') $uibModalInstance.dismiss('cancel')
] ]
angular.extend(defaultOpts, options) if angular.isObject options angular.extend(defaultOpts, options) if angular.isObject options
$modal.open defaultOpts $uibModal.open defaultOpts
.result['finally'](null).then -> .result['finally'](null).then (info)->
if angular.isFunction(success) if angular.isFunction(success)
success() success(info)
, -> , (reason)->
if angular.isFunction(error) if angular.isFunction(error)
error() error(reason)
] ]

View File

@ -0,0 +1,3 @@
Application.Services.service('es', function (esFactory) {
return esFactory({ host: window.location.origin });
});

View File

@ -2,5 +2,7 @@
Application.Services.factory 'Group', ["$resource", ($resource)-> Application.Services.factory 'Group', ["$resource", ($resource)->
$resource "/api/groups/:id", $resource "/api/groups/:id",
{id: "@id"} {id: "@id"},
update:
method: 'PUT'
] ]

View File

@ -0,0 +1,8 @@
'use strict'
Application.Services.factory 'Invoice', ["$resource", ($resource)->
$resource "/api/invoices/:id",
{id: "@id"},
update:
method: 'PUT'
]

View File

@ -3,9 +3,14 @@
Application.Services.factory 'Member', ["$resource", ($resource)-> Application.Services.factory 'Member', ["$resource", ($resource)->
$resource "/api/members/:id", $resource "/api/members/:id",
{id: "@id"}, {id: "@id"},
update:
method: 'PUT'
lastSubscribed: lastSubscribed:
method: 'GET' method: 'GET'
url: '/api/last_subscribed/:limit' url: '/api/last_subscribed/:limit'
params: {limit: "@limit"} params: {limit: "@limit"}
isArray: true isArray: true
merge:
method: 'PUT'
url: '/api/members/:id/merge'
] ]

View File

@ -0,0 +1,8 @@
'use strict'
Application.Services.factory 'Plan', ["$resource", ($resource)->
$resource "/api/plans/:id",
{id: "@id"},
update:
method: 'PUT'
]

View File

@ -0,0 +1,14 @@
'use strict'
Application.Services.factory 'Price', ["$resource", ($resource)->
$resource "/api/prices/:id",
{},
query:
isArray: false
update:
method: 'PUT'
compute:
method: 'POST'
url: '/api/prices/compute'
isArray: false
]

View File

@ -0,0 +1,8 @@
'use strict'
Application.Services.factory 'Pricing', ["$resource", ($resource)->
$resource "/api/pricing",
{},
update:
method: 'PUT'
]

View File

@ -7,4 +7,8 @@ Application.Services.factory 'Project', ["$resource", ($resource)->
method: 'GET' method: 'GET'
url: '/api/projects/last_published' url: '/api/projects/last_published'
isArray: true isArray: true
search:
method: 'GET'
url: '/api/projects/search'
isArray: true
] ]

View File

@ -0,0 +1,8 @@
'use strict'
Application.Services.factory 'Reservation', ["$resource", ($resource)->
$resource "/api/reservations/:id",
{id: "@id"},
update:
method: 'PUT'
]

View File

@ -0,0 +1,10 @@
'use strict'
Application.Services.factory 'Setting', ["$resource", ($resource)->
$resource "/api/settings/:name",
{name: "@name"},
update:
method: 'PUT'
query:
isArray: false
]

View File

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

View File

@ -0,0 +1,5 @@
'use strict'
Application.Services.factory 'Statistics', ["$resource", ($resource)->
$resource "/api/statistics"
]

View File

@ -0,0 +1,8 @@
'use strict'
Application.Services.factory 'Subscription', ["$resource", ($resource)->
$resource "/api/subscriptions/:id",
{id: "@id"},
update:
method: 'PUT'
]

View File

@ -0,0 +1,8 @@
'use strict'
Application.Services.factory 'Tag', ["$resource", ($resource)->
$resource "/api/tags/:id",
{id: "@id"},
update:
method: 'PUT'
]

View File

@ -0,0 +1,8 @@
'use strict'
Application.Services.factory 'Training', ["$resource", ($resource)->
$resource "/api/trainings/:id",
{id: "@id"},
update:
method: 'PUT'
]

View File

@ -0,0 +1,8 @@
'use strict'
Application.Services.factory 'TrainingsPricing', ["$resource", ($resource)->
$resource "/api/trainings_pricings/:id",
{},
update:
method: 'PUT'
]

View File

@ -0,0 +1,13 @@
'use strict'
Application.Services.factory 'Translations', ["$translatePartialLoader", "$translate", ($translatePartialLoader, $translate)->
return {
query: (stateName) ->
if angular.isArray (stateName)
angular.forEach stateName, (state) ->
$translatePartialLoader.addPart(state)
else
$translatePartialLoader.addPart(stateName)
$translate.refresh()
}
]

View File

@ -0,0 +1,8 @@
'use strict'
Application.Services.factory 'User', ["$resource", ($resource)->
$resource "/api/users",
{},
query:
isArray: false
]

View File

@ -16,7 +16,7 @@ h1, .page-title {
font-weight: 900; font-weight: 900;
} }
h2 { h2 {
color: $red; //color: $red;
line-height: rem-calc(24); line-height: rem-calc(24);
font-weight: 900; font-weight: 900;
} }
@ -26,7 +26,7 @@ h5 {
display: inline-block; display: inline-block;
position: relative; position: relative;
line-height: rem-calc(18); line-height: rem-calc(18);
color: $red; //color: $red;
font-size: rem-calc(16); font-size: rem-calc(16);
&:after { &:after {
position: absolute; position: absolute;
@ -35,7 +35,7 @@ h5 {
content: ''; content: '';
width: 35%; width: 35%;
height: 1px; height: 1px;
background-color: $red; //background-color: $red;
} }
} }
@ -43,12 +43,12 @@ h5 {
// ------------------------- // -------------------------
a { a {
color: $link-color; //color: $link-color;
text-decoration: none; text-decoration: none;
} }
a:hover, a:hover,
a:focus { a:focus {
color: $link-hover-color; //color: $link-hover-color;
text-decoration: none; text-decoration: none;
} }
@ -127,7 +127,8 @@ dd {
// transition:0.5s linear all; // transition:0.5s linear all;
// } // }
[ui-view].ng-enter, [ui-view].ng-leave { // only for main content
#content-main.ng-enter, #content-main.ng-leave {
position: absolute; position: absolute;
left: 0; left: 0;
right: 0; right: 0;

View File

@ -17,8 +17,8 @@
.btn-warning-full { .btn-warning-full {
outline: 0; outline: 0;
text-transform: uppercase; text-transform: uppercase;
border: 3px solid $yellow; //border: 3px solid $yellow;
background-color: $yellow; //background-color: $yellow;
&:hover { &:hover {
background-color: white; background-color: white;
} }
@ -41,4 +41,18 @@
.btn-inactive{ .btn-inactive{
-webkit-box-shadow: none !important; -webkit-box-shadow: none !important;
box-shadow: none !important; box-shadow: none !important;
} }
.btn-loading:after {
margin-left: 1em;
display: inline-block;
content: "\f110";
font-family: FontAwesome;
-webkit-animation:spin 4s linear infinite;
-moz-animation:spin 4s linear infinite;
animation:spin 4s linear infinite;
}
@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } }
@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } }
@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } }

View File

@ -1,8 +1,9 @@
.bg-light { background-color: $brand-light; } .bg-light { background-color: $brand-light; }
.bg-red { background-color: $red; color: white; } //.bg-red { background-color: $red; color: white; }
.bg-red-dark { background-color: $red-dark; } //.bg-red-dark { background-color: $red-dark; }
.bg-yellow { background-color: $yellow !important; } //.bg-yellow { background-color: $yellow !important; }
.bg-token { background-color: rgba(230, 208, 137, 0.49); }
.bg-machine { background-color: $beige; } .bg-machine { background-color: $beige; }
.bg-formation { background-color: $violet; } .bg-formation { background-color: $violet; }
.bg-atelier { background-color: $blue; } .bg-atelier { background-color: $blue; }
@ -30,7 +31,7 @@
.text-black-light { color: #424242 !important; } .text-black-light { color: #424242 !important; }
.text-gray { color: #5a5a5a !important; } .text-gray { color: #5a5a5a !important; }
.text-white { color: #fff !important; } .text-white { color: #fff !important; }
.text-yellow { color: $yellow !important; } //.text-yellow { color: $yellow !important; }
.text-blue { color: $blue; } .text-blue { color: $blue; }
.text-muted { color: $text-muted; } .text-muted { color: $text-muted; }
.text-danger, .red { color: $red !important; } .text-danger, .red { color: $red !important; }

View File

@ -7,11 +7,11 @@
font-weight: 600; font-weight: 600;
color: black; color: black;
} }
h1 { h1 {
font-size: rem-calc(16); text-transform: uppercase; font-size: rem-calc(16); text-transform: uppercase;
} }
h2 { font-weight: bold; } h2 { font-weight: bold; }
h3 { color: $red; } //h3 { color: $red; }
h4 { h4 {
font-size: rem-calc(12); font-size: rem-calc(12);
margin: 8px 0; margin: 8px 0;
@ -50,13 +50,14 @@
left: 0; left: 0;
right: 0; right: 0;
margin: 0 auto; margin: 0 auto;
max-height: 44px;
} }
h1 { h1 {
margin: 25px 0 20px 0; margin: 25px 0 20px 0;
font-weight: bold; font-weight: bold;
text-transform: uppercase; text-transform: uppercase;
text-align: center; text-align: center;
color: $red; //color: $red;
} }
} }
@ -129,14 +130,14 @@
} }
.article-thumbnail { .article-thumbnail {
max-height: 400px; // max-height: 400px;
overflow: hidden; overflow: hidden;
} }
} }
.label-staging { .label-staging {
position: absolute; position: absolute;
top: 50px; top: 50px;
} }
.notification-open { .notification-open {
@ -197,6 +198,7 @@
margin: 10px 0; margin: 10px 0;
font-size: rem-calc(16); font-size: rem-calc(16);
text-transform: uppercase; text-transform: uppercase;
color: black;
} }
.content { .content {
padding: 15px 0; padding: 15px 0;
@ -206,7 +208,7 @@
display: inline-block; display: inline-block;
background: white; background: white;
@include border-radius(50%); @include border-radius(50%);
border: 3px solid $yellow; border: 3px solid;// $yellow;
} }
.price { .price {
position: relative; position: relative;
@ -238,15 +240,15 @@
background-color: white; background-color: white;
padding-left: 30px; padding-left: 30px;
padding-right: 30px; padding-right: 30px;
&:hover { background-color: $yellow; } //&:hover { background-color: $yellow; }
} }
} }
} }
.well { .well {
&.well-warning { &.well-warning {
border-color: #ffdc4e; //border-color: #ffdc4e;
background-color: #ffdc4e; //background-color: #ffdc4e;
@include border-radius(3px); @include border-radius(3px);
padding: 5px 10px; padding: 5px 10px;
} }
@ -324,10 +326,10 @@
.block-link { .block-link {
cursor: pointer; cursor: pointer;
&:hover { background-color: $yellow; } //&:hover { background-color: $yellow; }
} }
.form-control.form-control-ui-select .select2-choices .select2-search-choice { .form-control .ui-select-choices, .form-control .ui-select-match {
font-size: 85% !important; font-size: 85% !important;
} }
@ -351,16 +353,15 @@
.about-picture { .about-picture {
padding: 70px 0; padding: 70px 0;
height: 326px; height: 326px;
background: white asset-url("about-fablab.jpg") no-repeat;
background-size: cover; background-size: cover;
margin-bottom: 30px; margin-bottom: 30px;
} }
.about-title { .about-title, .about-title p {
margin: 0; margin: 0;
font-size: rem-calc(50); font-size: rem-calc(50);
line-height: rem-calc(48); line-height: rem-calc(48);
color: #fff; color: #fff;
font-weight: 900; //black font-weight: 900; //black
} }
.about-title-aside { .about-title-aside {
@ -393,7 +394,7 @@
} }
.event:hover { .event:hover {
background-color: #cb1117; //background-color: #cb1117;
color: white; color: white;
} }
@ -441,3 +442,25 @@ padding: 10px;
} }
} }
// angular-bootstrap accordions (enlightened version)
.light-accordion > .panel-heading {
padding-top: 0.2em;
padding-bottom: 0.2em;
}
.light-accordion > .panel-heading > .panel-title {
font-size: 12pt;
}
.app-generator {
position: absolute;
bottom: 0; right: 10px;
z-index: 100;
padding: 3px 15px;
border: 1px solid $border-color;
border-top-left-radius: 8px;
background: $bg-gray;
@media only screen and (max-width: 768px) {
display: none;
}
}

View File

@ -76,11 +76,17 @@
cursor: pointer; cursor: pointer;
color: black; color: black;
&:hover { &:hover {
background-color: $yellow; //background-color: $yellow;
} }
i:before { content: "\f177"; } i:before { content: "\f177"; }
} }
} }
.heading-icon {
width: 100%;
padding: 35px 40%;
display: inline-block;
color: black;
}
.heading-title { .heading-title {
overflow: hidden; overflow: hidden;
height: 94px; height: 94px;
@ -341,4 +347,131 @@ body.container{
} }
} }
} }
}
.customMenuButton {
min-width: 15em;
max-width: 15em;
overflow-x: hidden;
}
.customMenuInput {
width:100% !important;
}
.reservation-canceled {
color: #606060;
border-radius: 0.2em;
background-color: #e4e4e4;
padding: 0.7em 0.7em;
font-size: 90%;
display:inline-block;
vertical-align:middle;
.reservation-time {
color: #606060;
}
&:before {
content: "Annulée";
display: inline-block;
background-color: #c44242;
border-radius: 0.25em;
padding: 0.1em 0.5em;
font-weight: bold;
color: #fff;
float: left;
margin-right: 1em;
}
}
.custom-logo-container {
max-width: 240px;
height: 100%;
.custom-logo {
height: 100px;
width: 100%;
position: relative;
background-size: cover;
background-repeat: no-repeat;
border: 1px dashed #c4c4c4;
border-radius: 0.7em;
padding: 1.6em;
margin-left: 1em;
img {
display: block;
width: auto;
max-height: 44px;
max-width: 100%;
margin:auto;
}
&:hover .tools-box {
opacity: 1;
}
.tools-box {
opacity: 0;
position: absolute;
bottom: 10px;
left: 0;
right: 0;
text-align: center;
}
}
.bg-dark {
background-color: #000;
opacity: 0.9;
}
}
.custom-favicon-container {
max-width: 70px;
height: 100%;
.custom-favicon {
height: 70px;
width: 100%;
position: relative;
background-size: cover;
background-repeat: no-repeat;
border: 1px dashed #c4c4c4;
border-radius: 0.7em;
padding: 1.6em;
margin-left: 1em;
img {
display: block;
width: auto;
max-height: 16px;
max-width: 16px;
margin:auto;
}
&:hover .tools-box {
opacity: 1;
}
.tools-box {
opacity: 0;
position: absolute;
bottom: -7px;
left: 51px;
right: 0;
text-align: center;
}
}
}
.flash-message {
position: absolute;
top: 1%;
z-index: 1001;
width: 33%;
left: 33%;
} }

View File

@ -425,7 +425,7 @@
#nav { #nav {
// border-right: 1px solid $red-dark; // border-right: 1px solid $red-dark;
.nav { .nav {
background-color: $red; //background-color: $red;
> li { > li {
> a { > a {
padding: 13px 17px; padding: 13px 17px;
@ -433,11 +433,11 @@
color: white; color: white;
&:hover, &:hover,
&:focus, &.active { &:focus, &.active {
background-color: $red-light; //background-color: $red-light;
color: white; color: white;
} }
&.active { &.active {
border-left: 3px solid #870003; border-left: 3px solid;// #870003;
} }
} }
} }

View File

@ -1,37 +1,108 @@
// medium editor placeholder
.medium-editor-placeholder {
min-height: 30px; // fix for firefox
}
//xeditable
.editable-buttons{
button[type=submit].btn-primary{
@extend .btn-warning;
}
}
//summernote //summernote
.note-editor .note-editable { .note-editor .note-editable {
background-color: white; background-color: white;
} }
// Growl // Growl
.growl { .growl {
top: 90px; top: 90px;
z-index: 1100; z-index: 1100;
} }
// fullcalendar
.fc-view-container .fc-body tr {
// UI Select height: 40px !important;
.form-control {
&.form-control-ui-select {
height: auto;
.select2-choices {
border: none;
background: transparent;
.select2-search-choice {
@extend .label;
padding-left: .9em;
font-size: 100%;
font-weight: normal;
}
}
}
} }
.fc-toolbar {
height: 40px;
background-color: #fff;
}
.fc-toolbar .fc-button {
background: #F2F2F2;
border: none;
box-shadow: none;
text-shadow: none;
margin: 0;
height: 40px;
line-height: 18px;
padding: 10px;
//&:hover, &:active, &.fc-state-active { background-color: $yellow; }
}
.fc-toolbar h2 {
font-size: 15px;
line-height: 40px;
margin: 0;
}
.fc-view-container .fc-widget-header,
.fc-view-container .fc-widget-content {
border-color: #e8e8e8;
font-weight: normal;
}
.fc-content-skeleton .fc-event {
padding: 2px;
border-left: solid 3px;
}
.fc-event {
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
}
.fc-event .fc-time span, .fc-event .fc-title {
font-size: rem-calc(10);
line-height: rem-calc(12);
}
.fc-event .fc-time span.label {
font-size: rem-calc(8);
margin-left: 0.7em;
}
// croix de suppression pour un créneau de disponibilité
.remove-event {
position: absolute;
float: right;
right: 0;
top: 0;
padding: 0;
font-size: 11px;
color: black;
cursor: pointer;
z-index: 9999;
text-align: right;
.training-reserve &, .machine-reserve & { display: none; }
}
.fc-v-event.fc-end {
border-bottom-width: 2px;
}
.fc-divider {
display: none !important;
}
@ -59,6 +130,7 @@
line-height: rem-calc(24); line-height: rem-calc(24);
color: white; color: white;
font-weight: 800; font-weight: 800;
text-align: center;
a { a {
color: white; color: white;
&:hover { color: $yellow; } &:hover { color: $yellow; }
@ -77,7 +149,7 @@
background: none; background: none;
@include border-radius($border-radius-base); @include border-radius($border-radius-base);
&:hover, &:focus { &:hover, &:focus {
color: $yellow; //color: $yellow;
} }
.glyphicon-chevron-left { .glyphicon-chevron-left {
@ -99,6 +171,31 @@
display: none; display: none;
} }
// .carousel-control {
// // position: absolute;
// display: block;
// margin-bottom: -20px;
// padding: 20px;
// color: white;
// width: 58px;
// height: 58px;
// border: 3px solid white;
// border-radius: 50%;
// .glyphicon-chevron-right:before {
// // //Reset the icon
// // content: " ";
// // //Give layout
// // display:block;
// // //Your image as background
// // background:url('http://yourarrow.png') no-repeat;
// // //To show full image set the dimensions
// // width:30px;
// // height:30px;
// }
// }
.banner { } .banner { }

View File

@ -1,3 +1,3 @@
/* /*
* Require here your print media stylesheets *= require fullcalendar/dist/fullcalendar.print
*/ */

View File

@ -111,7 +111,7 @@ p, .widget p {
.b{border: 1px solid rgba(0, 0, 0, 0.05)} .b{border: 1px solid rgba(0, 0, 0, 0.05)}
.b-a{border: 1px solid $border-color} .b-a{border: 1px solid $border-color}
.b-t{border-top: 1px solid $border-color} .b-t{border-top: 1px solid $border-color}
.b-r{border-right: 1px solid $border-color} .b-r{border-right: 1px solid $border-color !important;}
.b-b{border-bottom: 1px solid $border-color} .b-b{border-bottom: 1px solid $border-color}
.b-l{border-left: 1px solid $border-color} .b-l{border-left: 1px solid $border-color}
.b-light{border-color: darken($brand-light, 5%)} .b-light{border-color: darken($brand-light, 5%)}

View File

@ -1,17 +1,23 @@
/* /*
*= require_self *= require_self
*= require select2/select2 *= require angular-ui-select/dist/select
*= require fullcalendar/dist/fullcalendar
*= require jasny-bootstrap/dist/css/jasny-bootstrap *= require jasny-bootstrap/dist/css/jasny-bootstrap
*= require angular-growl/build/angular-growl.min.css *= require angular-growl-v2/build/angular-growl
*= require angular-xeditable/dist/css/xeditable *= require angular-xeditable/dist/css/xeditable
*= require angular-loading-bar/src/loading-bar *= require angular-loading-bar/src/loading-bar
*= require nvd3/build/nv.d3
*= require font-awesome *= require font-awesome
*= require medium-editor/dist/css/medium-editor
*= require medium-editor/dist/css/themes/default
*= require bootstrap-switch/dist/css/bootstrap3/bootstrap-switch.min
*= require summernote/dist/summernote *= require summernote/dist/summernote
*= require jquery-minicolors/jquery.minicolors.css
*/ */
@import "app.functions"; @import "app.functions";
@import "bootstrap-compass";
@import "bootstrap-sprockets";
@import "compass"; @import "compass";
@import "bootstrap_and_overrides"; @import "bootstrap_and_overrides";
@ -25,5 +31,6 @@
@import "app.buttons"; @import "app.buttons";
@import "app.components"; @import "app.components";
@import "app.plugins"; @import "app.plugins";
@import "modules/invoice";
@import "app.responsive"; @import "app.responsive";

View File

@ -78,10 +78,10 @@ $link-hover-decoration: underline;
// Semibold = 600, Bold = 700, ExtraB = 800 // Semibold = 600, Bold = 700, ExtraB = 800
$font-family-sans-serif: "Open Sans", Helvetica, Arial, sans-serif !default; $font-family-sans-serif: 'proxima-nova', 'Open Sans', Helvetica, Arial, sans-serif !default;
$font-proxima-condensed: "Open Sans Condensed", Helvetica, Arial, sans-serif !default; $font-proxima-condensed: 'proxima-nova-condensed', 'Open Sans Condensed', Helvetica, Arial, sans-serif !default;
$font-family-serif: Georgia, "Times New Roman", Times, serif !default; $font-family-serif: Georgia, 'Times New Roman', Times, serif !default;
$font-felt: "Loved by the King", sans-serif; $font-felt: 'felt-tip-roman', 'Loved by the King', cursive, sans-serif;
//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`. //** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
// $font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace !default; // $font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace !default;
@ -952,16 +952,16 @@ $hr-border: $gray-lighter !default;
@import "bootstrap/input-groups"; @import "bootstrap/input-groups";
@import "bootstrap/navs"; @import "bootstrap/navs";
@import "bootstrap/navbar"; @import "bootstrap/navbar";
// @import "bootstrap/breadcrumbs"; @import "bootstrap/breadcrumbs";
@import "bootstrap/pagination"; @import "bootstrap/pagination";
// @import "bootstrap/pager"; @import "bootstrap/pager";
@import "bootstrap/labels"; @import "bootstrap/labels";
@import "bootstrap/badges"; @import "bootstrap/badges";
//@import "bootstrap/jumbotron"; @import "bootstrap/jumbotron";
@import "bootstrap/thumbnails"; @import "bootstrap/thumbnails";
@import "bootstrap/alerts"; @import "bootstrap/alerts";
@import "bootstrap/progress-bars"; @import "bootstrap/progress-bars";
// @import "bootstrap/media"; @import "bootstrap/media";
@import "bootstrap/list-group"; @import "bootstrap/list-group";
@import "bootstrap/panels"; @import "bootstrap/panels";
@import "bootstrap/responsive-embed"; @import "bootstrap/responsive-embed";

View File

@ -0,0 +1,181 @@
// admin invoices
.invoice-placeholder {
width: 80%;
max-width: 800px;
height: 100%;
margin: auto;
margin-top: 2em;
border-width: 1px;
border-style: solid;
border-color: #aeaeae #979797 #7b7b7b;
box-shadow: 2px 3px 6px 0 #898989,
-2px 3px 6px 0 #898989;
padding: 2em;
.invoice-buyer-infos {
float: right;
text-align: right;
margin-left: 1em;
}
.invoice-logo {
height: 6em;
width: 100%;
position: relative;
background-size: cover;
background-repeat: no-repeat;
img {
display: block;
width: auto;
max-height: 100%;
max-width: 60%;
}
&:hover .tools-box {
opacity: 1;
}
.tools-box {
opacity: 0;
position: absolute;
bottom: 10px;
left: 0;
right: 0;
text-align: center;
}
}
.invoice-object, .invoice-data, .invoice-data p, .invoice-text, .invoice-legals {
margin-top: 2em;
}
.invoice-data table {
width: 100%
}
.invoice-data tr, .invoice-data th, .invoice-data td {
border: 1px solid;
padding: 4px;
}
.invoice-text {
font-weight: bold;
}
.invoice-legals {
text-align: right;
}
.invoice-editable:hover {
background-color: $yellow;
overflow-x: hidden;
}
.invoice-activable {
font-style: italic;
color: #c4c4c4;
overflow-x: hidden;
}
.invoice-activable:hover {
background-color: $yellow;
border: 1px dashed #c4c4c4;
}
&:after {
content:"";
display:block;
margin-top:30%;
}
.vat-line {
background-color: #e4e4e4;
text-align: right;
}
.bold {
font-weight: bold;
}
.italic {
font-style: italic;
}
.right {
text-align: right;
}
}
.custom-invoice {
.modal-header {
@extend .modal-header;
// padding-left: 4em;
text-align: center;
background-color: #e6e6e6;
}
.modal-body {
.elements ul {
@extend .list-unstyled;
}
.elements li {
@extend .btn;
@extend .btn-default;
width: 100%;
}
table.invoice-element-legend {
min-width: 15em;
}
.invoice-element-legend tr, .invoice-element-legend th, .invoice-element-legend td {
border: 1px solid;
padding: 4px;
}
.bottom-notes {
font-style: italic;
}
}
}
.partial-avoir-table tr {
float:left;
.input-col { min-width: 2em; }
.label-col { min-width: 18em; }
.amount-col { min-width: 6em; }
}
.partial-avoir-selected-item {
background-color: $yellow;
display: block;
position: relative;
&:after {
content:"Rembourser";
display:inline-block;
position: absolute;
right: 0.8em;
top: 1.8em;
color: $red;
width: 6.7em;
transform: rotate(-22.5deg);
overflow: visible;
font-weight: bold;
border: 1px solid;
padding: 0 4px;
border-radius: 5px;
font-size: small;
}
}

Some files were not shown because too many files have changed in this diff Show More