diff --git a/.fabmanager-version b/.fabmanager-version
new file mode 100644
index 000000000..197c4d5c2
--- /dev/null
+++ b/.fabmanager-version
@@ -0,0 +1 @@
+2.4.0
diff --git a/.gitignore b/.gitignore
index c3f1dd98e..ee0e0fc32 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,9 +30,14 @@
# PDF invoices
/invoices/*
+# XLSX exports
+/exports/*
+
+
.DS_Store
.vagrant
.docker
+# Plugins are versioned is their own repository
/plugins/*
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e02eb9107..7e6e5e2f3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,73 @@
# Changelog Fab Manager
+## v2.4.0 2016 October 4
+
+- RSS feeds to follow new projects and events published
+- Use slugs in projects URL opened from notifications
+- Ask for confirmation on machine deletion from the public view
+- Ability to delete a training from the public view for an admin
+- Project images will show in full-size on a click
+- Add a checkbox "I accept to receive informations from the FabLab" on Sign-up dialog and user's profile
+- Share project with Facebook/Twitter
+- Display fab-manager's version in "Powered by" label, when logged as admin
+- Load translation locales from subdirectories
+- Add wallet to user, client can pay total/partial reservation or subscription by wallet
+- Public calendar for show all trainings/machines/events
+- Display 'draft' badge on drafts in project galleries
+- Add a 'new project' button in dashboard/my projects
+- Open Projects: show the platform of origin even for local projects
+- Ability to use HTML in machine specs and description
+- Ability to manage project steps order
+- Trainings are associated with a picture and an HTML textual description
+- Public gallery of trainings with ability to view details or to book a training on its own calendar
+- Ability to switch back to all trainings booking view
+- Rename "Courses and Workshops" to "Events"
+- Admin: Events can be associated with a theme and an age range
+- Admin: Event categories, themes and age ranges can be customized
+- Filter events by category, theme and age range in public view
+- Ability to customise price's categories for the events
+- Events can be associated with many custom price's categories, instead of only one "reduced price"
+- Statistics views can trigger and display custom aggregations from ElasticSearch
+- Machine hours/Trainings statistics: display number of tickets/hours available for booking
+- Statistics will include informations abouts events category, theme and age range
+- Ability to export the current statistics table to an Excel file
+- Ability to export every statistics on a given dates range to an Excel file
+- More fields in members exports
+- Unified members, subscriptions and reservations exports with the new statistics exports
+- Excel exports are now asynchronously generated and cached on the server for future identical requests
+- Users have the ability to create an organizational profile when creating an account
+- Organization informations will be used in invoices generation, if present
+- Admins can create and enable/disable coupons. They can also notify an user about details of a coupon
+- Users and admins can apply coupons's discounts to their shopping cart
+- Send an email reminder and system notification some hours before a reservation happens
+- Admins can toggle reminders on/off and customize the delay
+- More file types allowed as project CAD attachements
+- Project CAD attachements are now checked by MIME type in addition of extension check
+- Project CAD attachement allowed are now configured in environment variables
+- Project CAD attachement extensions allowed are shown next to input field
+- Display strategy's name in SSO providers list
+- SSO: documentation improved with an usage example
+- SSO: mapped fields display their data type. Integers, booleans and dates allow some transformations.
+- Fix a bug: project drafts are shown on public profiles
+- Fix a bug: event category disappear when editing the event
+- Fix a bug: machine name is not shown in plan edition
+- Fix a bug: machine slots with tags are not displayed correctly on reservation calendar
+- Fix a bug: avatar, address and organization details mapping from SSO were broken
+- Fix a bug: in SSO configuration some valid endpoints were recognized as erroneous
+- Fix a bug: clicking on the text in stripe's payment modal, does not validate the checkbox
+- Fix a bug: move event reservation is not limited by admin settings (prior-delay & disable)
+- Fix a bug: UI issues on small devices (dashboard + admin views)
+- Fix a bug: embedded video not working in training/machine description
+- Fix a bug: reordering project's steps trigger the unsaved-warning dialog
+- Fix a bug: unable to compile assets in Docker with CoffeeScript error
+- Fix a bug: do not force HTTPS for URLs in production environments
+- [TODO DEPLOY] `rake fablab:es_build_availabilities_index`
+- [TODO DEPLOY] `rake fablab:es_add_event_filters`
+- [TODO DEPLOY] `rake db:migrate`
+- [TODO DEPLOY] `bundle install`
+- [TODO DEPLOY] add `EXCEL_DATE_FORMAT`, `ALLOWED_EXTENSIONS` and `ALLOWED_MIME_TYPES` environment variable in `application.yml`
+- [OPTIONAL] `rake fablab:fix:assign_category_to_uncategorized_events` (will put every non-categorized events into a new category called "No Category", to ease re-categorization)
+
## v2.3.1 2016 September 26
- Fix a bug: group cache filename too long
@@ -19,7 +87,7 @@
## v2.2.2 2016 June 23
- Fix some bugs: users with uncompleted account (sso imported) won't appear in statistics, in listings and in searches. Moreover, they won't block statistics generation
- Fix a bug: unable to display next results in statistics tables
-- Admin: Category is mandatory when creating a course/workshop (event)
+- Admin: Category is mandatory when creating an event
## v2.2.1 2016 June 22
- Fix a bug: field User.merged_at should not be allowed to be mapped in SSO
diff --git a/Dockerfile b/Dockerfile
index 3d0800729..3c74c2d2c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,18 +1,11 @@
FROM ruby:2.3
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
@@ -28,20 +21,11 @@ RUN bundle install --binstubs
# 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/exports
RUN mkdir -p /usr/src/app/log
RUN mkdir -p /usr/src/app/public/uploads
RUN mkdir -p /usr/src/app/public/assets
@@ -56,13 +40,15 @@ COPY . /usr/src/app
# Volumes
VOLUME /usr/src/app/invoices
+VOLUME /usr/src/app/exports
+VOLUME /usr/src/app/public
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
+# Expose port 3000 to the Docker host, so we can access it
# from the outside.
-EXPOSE 80 443
+EXPOSE 3000
# The main command to run when the container starts. Also
# tell the Rails dev server to bind to all interfaces by
diff --git a/Gemfile b/Gemfile
index 90f60d23e..b0250b9a7 100644
--- a/Gemfile
+++ b/Gemfile
@@ -144,3 +144,8 @@ gem 'openlab_ruby'
gem 'api-pagination'
gem 'has_secure_token'
gem 'apipie-rails'
+
+# XLS files generation
+gem 'rubyzip', '~> 1.1.0'
+gem 'axlsx', '2.1.0.pre'
+gem 'axlsx_rails'
diff --git a/Gemfile.lock b/Gemfile.lock
index 348501c84..6e602afad 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -54,6 +54,13 @@ GEM
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1)
+ axlsx (2.1.0.pre)
+ htmlentities (~> 4.3.1)
+ nokogiri (>= 1.4.1)
+ rubyzip (~> 1.1.7)
+ axlsx_rails (0.4.0)
+ axlsx (>= 2.0.1)
+ rails (>= 3.1)
bcrypt (3.1.10)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
@@ -87,13 +94,13 @@ GEM
cldr-plurals-runtime-rb (1.0.1)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
- coffee-rails (4.1.0)
+ coffee-rails (4.1.1)
coffee-script (>= 2.2.0)
- railties (>= 4.0.0, < 5.0)
- coffee-script (2.3.0)
+ railties (>= 4.0.0, < 5.1.x)
+ coffee-script (2.4.1)
coffee-script-source
execjs
- coffee-script-source (1.9.1)
+ coffee-script-source (1.10.0)
compass (1.0.3)
chunky_png (~> 1.2)
compass-core (~> 1.0.2)
@@ -150,7 +157,7 @@ GEM
multi_json
equalizer (0.0.11)
erubis (2.7.0)
- execjs (2.4.0)
+ execjs (2.7.0)
faker (1.4.3)
i18n (~> 0.5)
faraday (0.9.1)
@@ -174,6 +181,7 @@ GEM
highline (1.7.1)
hike (1.2.3)
hitimes (1.2.2)
+ htmlentities (4.3.4)
http (0.6.4)
http_parser.rb (~> 0.6.0)
http-cookie (1.0.2)
@@ -214,8 +222,8 @@ GEM
twitter_cldr (~> 3.1)
mime-types (2.99)
mini_magick (4.2.0)
- mini_portile2 (2.0.0)
- minitest (5.9.0)
+ mini_portile2 (2.1.0)
+ minitest (5.9.1)
minitest-reporters (1.1.8)
ansi
builder
@@ -233,8 +241,9 @@ GEM
net-ssh-gateway (1.2.0)
net-ssh (>= 2.6.5)
netrc (0.10.3)
- nokogiri (1.6.7.2)
- mini_portile2 (~> 2.0.0.rc2)
+ nokogiri (1.6.8)
+ mini_portile2 (~> 2.1.0)
+ pkg-config (~> 1.1.7)
notify_with (0.0.2)
jbuilder (~> 2.0)
rails (>= 4.2.0)
@@ -257,6 +266,7 @@ GEM
orm_adapter (0.5.0)
pdf-core (0.5.1)
pg (0.18.1)
+ pkg-config (1.1.7)
prawn (2.0.1)
pdf-core (~> 0.5.1)
ttfunk (~> 1.4.0)
@@ -305,7 +315,7 @@ GEM
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
raindrops (0.13.0)
- rake (11.1.2)
+ rake (11.3.0)
rb-fsevent (0.9.4)
rb-inotify (0.9.5)
ffi (>= 0.5.0)
@@ -325,6 +335,7 @@ GEM
netrc (~> 0.7)
rolify (4.0.0)
ruby-progressbar (1.7.5)
+ rubyzip (1.1.7)
rufus-scheduler (3.0.9)
tzinfo
rvm-capistrano (1.5.6)
@@ -440,6 +451,8 @@ DEPENDENCIES
api-pagination
apipie-rails
awesome_print
+ axlsx (= 2.1.0.pre)
+ axlsx_rails
bootstrap-sass
byebug
capistrano
@@ -488,6 +501,7 @@ DEPENDENCIES
recurrence
responders (~> 2.0)
rolify
+ rubyzip (~> 1.1.0)
rvm-capistrano
sass-rails (= 5.0.1)
sdoc (~> 0.4.0)
diff --git a/README.md b/README.md
index 94f47b3e9..743887833 100644
--- a/README.md
+++ b/README.md
@@ -6,14 +6,15 @@ FabManager is the FabLab management solution. It is web-based, open-source and t
##### Table of Contents
1. [Software stack](#software-stack)
2. [Contributing](#contributing)
-3. [Setup a production environment with Docker and CoreOS](#setup-a-production-environment)
+3. [Setup a production environment](#setup-a-production-environment)
4. [Setup a development environment](#setup-a-development-environment)
4.1. [General Guidelines](#general-guidelines)
4.2. [Environment Configuration](#environment-configuration)
5. [PostgreSQL](#postgresql)
5.1. [Install PostgreSQL 9.4 on Ubuntu/Debian](#postgresql-on-debian)
5.2. [Install and launch PostgreSQL on MacOS X](#postgresql-on-macosx)
-5.3. [Setup the FabManager database in PostgreSQL](#setup-fabmanager-in-postgresql)
+5.3. [Setup the FabManager database in PostgreSQL](#setup-fabmanager-in-postgresql)
+5.4. [PostgreSQL Limitations](#postgresql-limitations)
6. [ElasticSearch](#elasticsearch)
6.1. [Install ElasticSearch on Ubuntu/Debian](#elasticsearch-on-debian)
6.2. [Install ElasticSearch on MacOS X](#elasticsearch-on-macosx)
@@ -28,8 +29,9 @@ FabManager is the FabLab management solution. It is web-based, open-source and t
7.2.2. [Applying changes](#i18n-apply)
8. [Open Projects](#open-projects)
9. [Plugins](#plugins)
-10. [Known issues](#known-issues)
-11. [Related Documentation](#related-documentation)
+10. [Single Sign-On](#sso)
+11. [Known issues](#known-issues)
+12. [Related Documentation](#related-documentation)
@@ -38,7 +40,7 @@ FabManager is the FabLab management solution. It is web-based, open-source and t
FabManager is a Ruby on Rails / AngularJS web application that runs on the following software:
-- Ubuntu/Debian
+- Ubuntu LTS 14.04+ / Debian 8+
- Ruby 2.3
- Git 1.9.1+
- Redis 2.8.4+
@@ -54,13 +56,16 @@ Contributions are welcome. Please read [the contribution guidelines](CONTRIBUTIN
**IMPORTANT**: **do not** update Arshaw/fullCalendar.js as it contains a hack for the remove-event cross.
-## Setup a production environment with Docker and CoreOS
+## Setup a production environment
-[Docker Readme](docker/README.md)
+To run fab-manager as a production application, this is highly recommended to use [Docker](https://www.docker.com/).
+The procedure to follow is described in the [docker readme](docker/README.md).
## Setup a development environment
+In you only intend to run fab-manager on your local machine for testing purposes or to contribute to the project development, you can set it up with the following procedure.
+
### General Guidelines
@@ -74,15 +79,17 @@ Contributions are welcome. Please read [the contribution guidelines](CONTRIBUTIN
```
3. Install the software dependencies.
+ First install [PostgreSQL](#postgresql) and [ElasticSearch](#elasticsearch) as specified in their respective documentations.
+ Then install the other dependencies:
- For Ubuntu/Debian:
```bash
- sudo apt-get install libpq-dev postgresql-9.4 redis-server imagemagick
+ sudo apt-get install libpq-dev redis-server imagemagick
```
- For MacOS X:
```bash
- brew install postgresql redis imagemagick
+ brew install redis imagemagick
```
4. Init the RVM instance and check it was correctly configured
@@ -114,7 +121,7 @@ Contributions are welcome. Please read [the contribution guidelines](CONTRIBUTIN
# or use your favorite text editor instead of vi (nano, ne...)
```
-8. 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.
+8. Build the database. You may have to follow the steps described in [the PostgreSQL configuration chapter](#setup-fabmanager-in-postgresql) before, if you don't already had done it.
```bash
rake db:setup
@@ -147,11 +154,13 @@ If you are in a development environment, your can keep the default values, other
POSTGRES_HOST
DNS name or IP address of the server hosting the PostgreSQL database of the application (see [PostgreSQL](#postgresql)).
+This value is only used when deploying with Docker, otherwise this is configured in `config/database.yml`.
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.
+This value is only used when deploying with Docker, otherwise this is configured in `config/database.yml`.
REDIS_HOST
@@ -217,13 +226,42 @@ See https://help.disqus.com/customer/portal/articles/466208-what-s-a-shortname-
TWITTER_NAME
-Identifier of the Twitter account, for witch the last tweet will be displayed on the home page.
+Identifier of the Twitter account, from witch the last tweet will be fetched and displayed on the home page.
+It will also be used for [Twitter Card analytics](https://dev.twitter.com/cards/analytics).
TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET, TWITTER_ACCESS_TOKEN & TWITTER_ACCESS_TOKEN_SECRET
Keys and secrets to access the twitter API.
Retrieve them from https://apps.twitter.com
+ FACEBOOK_APP_ID
+
+This is optional. You can follow [this guide to get your personal App ID](https://developers.facebook.com/docs/apps/register).
+If you do so, you'll be able to customize and get statistics about project shares on Facebook.
+
+ LOG_LEVEL
+
+This parameter configures the logs verbosity.
+Available log levels can be found [here](http://guides.rubyonrails.org/debugging_rails_applications.html#log-levels).
+
+ ALLOWED_EXTENSIONS
+
+Exhaustive list of file's extensions available for public upload as project's CAO attachements.
+Each item in the list must be separated from the others by a space char.
+You will probably want to check that this list match the `ALLOWED_MIME_TYPES` values below.
+Please consider that allowing file archives (eg. ZIP) or binary executable (eg. EXE) may result in a **dangerous** security issue and must be avoided in any cases.
+
+ ALLOWED_MIME_TYPES
+
+Exhaustive list of file's mime-types available for public upload as project's CAO attachements.
+Each item in the list must be separated from the others by a space char.
+You will probably want to check that this list match the `ALLOWED_EXTENSIONS` values above.
+Please consider that allowing file archives (eg. application/zip) or binary executable (eg. application/exe) may result in a **dangerous** security issue and must be avoided in any cases.
+
+ Settings related to Open Projects
+
+See the [Open Projects](#open-projects) section for a detailed description of these parameters.
+
Settings related to i18n
See the [Settings](#i18n-settings) section of the [Internationalization (i18n)](#i18n) paragraph for a detailed description of these parameters.
@@ -237,6 +275,7 @@ See the [Settings](#i18n-settings) section of the [Internationalization (i18n)](
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)
+ - `deb http://apt.postgresql.org/pub/repos/apt/ xenial-pgdg main` (Ubuntu 16.04 Xenial)
- `deb http://apt.postgresql.org/pub/repos/apt/ jessie-pgdg main` (Debian 8 Jessie)
@@ -264,7 +303,7 @@ Otherwise, please follow the official instructions on the project's website.
```bash
brew update
- brew install postgres
+ brew install homebrew/versions/postgresql94
```
2. Launch PostgreSQL
@@ -282,42 +321,73 @@ Otherwise, please follow the official instructions on the project's website.
Before running `rake db:setup`, you have to make sure that the user configured in [config/database.yml](config/database.yml.default) for the `development` environment exists.
To create it, please follow these instructions:
-1. Login as the postgres user
+1. Run the PostgreSQL administration command line interface, logged as the postgres user
+ - For Ubuntu/Debian:
```bash
sudo -i -u postgres
- ```
-
-2. Run the PostgreSQL administration command line interface
-
- ```bash
psql
```
+ - For MacOS X:
-3. Create a new user in PostgreSQL (in this example, the user will be named `sleede`)
+ ```bash
+ sudo psql -U $(whoami) postgres
+ ```
+
+ If you get an error running this command, please check your [pg_hba.conf](https://www.postgresql.org/docs/current/static/auth-pg-hba-conf.html) file.
+
+2. Create a new user in PostgreSQL (in this example, the user will be named `sleede`)
```sql
CREATE USER sleede;
```
-4. Grant him the right to create databases
+3. Grant him the right to create databases
```sql
ALTER ROLE sleede WITH CREATEDB;
```
-5. Then, create the fablab_development and fablab_test databases
+4. Then, create the fabmanager_development and fabmanager_test databases
```sql
- CREATE DATABASE fablab_development OWNER sleede;
- CREATE DATABASE fablab_test OWNER sleede;
+ CREATE DATABASE fabmanager_development OWNER sleede;
+ CREATE DATABASE fabmanager_test OWNER sleede;
```
-6. To finish, attribute a password to this user
+5. To finish, attribute a password to this user
```sql
ALTER USER sleede WITH ENCRYPTED PASSWORD 'sleede';
```
+6. Finally, have a look at the [PostgreSQL Limitations](#postgresql-limitations) section or some errors will occurs preventing you from finishing the installation procedure.
+
+
+### PostgreSQL Limitations
+
+- While setting up the database, we'll need to activate two PostgreSQL extensions: [unaccent](https://www.postgresql.org/docs/current/static/unaccent.html) and [trigram](https://www.postgresql.org/docs/current/static/pgtrgm.html).
+ This can only be achieved if the user, configured in `config/database.yml`, was granted the _SUPERUSER_ role **OR** if these extensions were white-listed.
+ So here's your choices, mainly depending on your security requirements:
+ - Use the default PostgreSQL super-user (postgres) as the database user of fab-manager.
+ - Set your user as _SUPERUSER_; run the following command in `psql` (after replacing `sleede` with you user name):
+
+ ```sql
+ ALTER USER sleede WITH SUPERUSER;
+ ```
+
+ - Install and configure the PostgreSQL extension [pgextwlist](https://github.com/dimitri/pgextwlist).
+ Please follow the instructions detailed on the extension website to whitelist `unaccent` and `trigram` for the user configured in `config/database.yml`.
+- Some users may want to use another DBMS than PostgreSQL.
+ This is currently not supported, because of some PostgreSQL specific instructions that cannot be efficiently handled with the ActiveRecord ORM:
+ - `app/controllers/api/members_controllers.rb@list` is using `ILIKE`
+ - `app/controllers/api/invoices_controllers.rb@list` is using `ILIKE` and `date_trunc()`
+ - `db/migrate/20160613093842_create_unaccent_function.rb` is using [unaccent](https://www.postgresql.org/docs/current/static/unaccent.html) and [trigram](https://www.postgresql.org/docs/current/static/pgtrgm.html) modules and defines a PL/pgSQL function (`f_unaccent()`)
+ - `app/controllers/api/members_controllers.rb@search` is using `f_unaccent()` (see above) and `regexp_replace()`
+ - `db/migrate/20150604131525_add_meta_data_to_notifications.rb` is using [jsonb](https://www.postgresql.org/docs/9.4/static/datatype-json.html), a PostgreSQL 9.4+ datatype.
+ - `db/migrate/20160915105234_add_transformation_to_o_auth2_mapping.rb` is using [jsonb](https://www.postgresql.org/docs/9.4/static/datatype-json.html), a PostgreSQL 9.4+ datatype.
+- If you intend to contribute to the project code, you will need to run the test suite with `rake test`.
+ This also requires your user to have the _SUPERUSER_ role.
+ Please see the [known issues](#known-issues) section for more informations about this.
## ElasticSearch
@@ -354,7 +424,7 @@ For a more detailed guide concerning the ElasticSearch installation, please chec
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:
+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/Ubuntu 16.04), you will need to run:
```bash
# System V
@@ -364,6 +434,12 @@ For a more detailed guide concerning the ElasticSearch installation, please chec
sudo /bin/systemctl enable elasticsearch.service
```
+5. Restart the host operating system to complete the installation
+
+ ```bash
+ sudo reboot
+ ```
+
### Install ElasticSearch on MacOS X
@@ -387,17 +463,11 @@ brew install homebrew/versions/elasticsearch17
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.
+ 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 command.
```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
+ # Here for the 50 last days
+ rake fablab:generate_stats[50]
```
@@ -531,6 +601,11 @@ See https://angular-ui.github.io/bootstrap/#uibdateparser-s-format-codes for a l
**BEWARE**: years format with less than 4 digits will result in problems because the system won't be able to distinct dates with the same less significant digits, eg. 50 could mean 1950 or 2050.
+ EXCEL_DATE_FORMAT
+
+Date format for dates shown in exported Excel files (eg. statistics)
+See https://support.microsoft.com/en-us/kb/264372 for a list a available formats.
+
#### Applying changes
@@ -570,12 +645,21 @@ To install a plugin, you just have to copy the plugin folder which contains its
You can see an example on the [repo of navinum gamification plugin](https://github.com/LaCasemate/navinum-gamification)
+
+## Single Sign-On
+
+Fab-manager can be connected to a [Single Sign-On](https://en.wikipedia.org/wiki/Single_sign-on) server which will provide its own authentication for the platform's users.
+Currently OAuth 2 is the only supported protocol for SSO authentication.
+
+For an example of how to use configure a SSO in Fab-manager, please read [sso_with_github.md](doc/sso_with_github.md).
+Developers may find informations on how to implement their own authentication protocol in [sso_authentication.md](doc/sso_authentication.md).
+
## Known issues
- 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 happen 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.
+ This may happen 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:
@@ -583,9 +667,9 @@ You can see an example on the [repo of navinum gamification plugin](https://gith
web.1 | Exiting
worker.1 | ...lib/redis/client.rb...:in `_parse_options'
- This may happen 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.
+ This may happen 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.
- Due to a stripe limitation, you won't be ble to create plans longer than one year.
@@ -599,20 +683,22 @@ You can see an example on the [repo of navinum gamification plugin](https://gith
test_after_commit (1.0.0) lib/test_after_commit/database_statements.rb:11:in `block in transaction'
test_after_commit (1.0.0) lib/test_after_commit/database_statements.rb:5:in `transaction'
- This is due to an ActiveRecord behavior witch disable referential integrity in PostgreSQL to load the fixtures.
- PostgreSQL will prevent any users to disable referential integrity on the fly if they doesn't have the `SUPERUSER` role.
- To fix that, logon as the `postgres` user and run the PostgreSQL shell (see [Setup the FabManager database in PostgreSQL](#setup-fabmanager-in-postgresql) for an example).
- Then, run the following command (replace `sleede` with your test database user, as specified in your database.yml):
+ This is due to an ActiveRecord behavior witch disable referential integrity in PostgreSQL to load the fixtures.
+ PostgreSQL will prevent any users to disable referential integrity on the fly if they doesn't have the `SUPERUSER` role.
+ To fix that, logon as the `postgres` user and run the PostgreSQL shell (see [Setup the FabManager database in PostgreSQL](#setup-fabmanager-in-postgresql) for an example).
+ Then, run the following command (replace `sleede` with your test database user, as specified in your database.yml):
ALTER ROLE sleede WITH SUPERUSER;
- DO NOT do this in a production environment, as this would lead to a serious security issue.
+ DO NOT do this in a production environment, unless you know what you're doing: this could lead to a serious security issue.
-- Using another DBMS than PostgreSQL is not supported, because of some PostgreSQL specific instructions:
- - `app/controllers/api/members_controllers.rb@list` is using `ILIKE`
- - `app/controllers/api/invoices_controllers.rb@list` is using `ILIKE` and `date_trunc()`
- - `db/migrate/20160613093842_create_unaccent_function.rb` is using [unaccent](https://www.postgresql.org/docs/current/static/unaccent.html) and [trigram](https://www.postgresql.org/docs/current/static/pgtrgm.html) modules and defines a PL/pgSQL function (`f_unaccent()`)
- - `app/controllers/api/members_controllers.rb@search` is using `f_unaccent()` (see above) and `regexp_replace()`
+- With Ubuntu 16.04, ElasticSearch may refuse to start even after having configured the service with systemd.
+ To solve this issue, you may have to set `START_DAEMON` to `true` in `/etc/default/elasticsearch`.
+ Then reload ElasticSearch with:
+
+ ```bash
+ sudo systemctl restart elasticsearch.service
+ ```
## Related Documentation
diff --git a/app/assets/javascripts/app.js.erb b/app/assets/javascripts/app.js.erb
index ae0ab8122..f68dbebb5 100644
--- a/app/assets/javascripts/app.js.erb
+++ b/app/assets/javascripts/app.js.erb
@@ -20,7 +20,7 @@ angular.module('application', ['ngCookies', 'ngResource', 'ngSanitize', 'ngCooki
'ui.select', 'ui.calendar', 'angularMoment', 'Devise', 'DeviseModal', 'angular-growl', 'xeditable',
'checklist-model', 'unsavedChanges', 'angular-loading-bar', 'ngTouch', 'angular-google-analytics',
'angularUtils.directives.dirDisqus', 'summernote', 'elasticsearch', 'angular-medium-editor', 'naif.base64',
- 'minicolors', 'pascalprecht.translate', 'ngFitText']).
+ 'minicolors', 'pascalprecht.translate', 'ngFitText', 'ngAside']).
config(['$httpProvider', 'AuthProvider', "growlProvider", "unsavedWarningsConfigProvider", "AnalyticsProvider", "uibDatepickerPopupConfig", "$provide", "$translateProvider",
function($httpProvider, AuthProvider, growlProvider, unsavedWarningsConfigProvider, AnalyticsProvider, uibDatepickerPopupConfig, $provide, $translateProvider) {
@@ -125,6 +125,24 @@ config(['$httpProvider', 'AuthProvider', "growlProvider", "unsavedWarningsConfig
// see https://github.com/revolunet/angular-google-analytics#automatic-page-view-tracking
Analytics.pageView();
+
+ /**
+ * This helper method builds and return an array contaning every integers between
+ * the provided start and end.
+ * @param start {number}
+ * @param end {number}
+ * @return {Array} [start .. end]
+ */
+ $rootScope.intArray = function(start, end) {
+ var arr = [];
+ for (var i = start; i < end; i++) { arr.push(i); }
+ return arr;
+ };
+
}]).constant('angularMomentConfig', {
timezone: Fablab.timezone
});
+
+angular.isUndefinedOrNull = function(val) {
+ return angular.isUndefined(val) || val === null
+};
diff --git a/app/assets/javascripts/application.js.erb b/app/assets/javascripts/application.js.erb
index f36dbf35d..d9f900521 100644
--- a/app/assets/javascripts/application.js.erb
+++ b/app/assets/javascripts/application.js.erb
@@ -67,6 +67,7 @@
//= require messageformat/messageformat
//= require angular-translate-interpolation-messageformat/angular-translate-interpolation-messageformat
//= require ngFitText/dist/ng-FitText.min
+//= require angular-aside/dist/js/angular-aside
//= require_tree ./controllers
//= require_tree ./services
//= require_tree ./directives
diff --git a/app/assets/javascripts/controllers/admin/authentications.coffee b/app/assets/javascripts/controllers/admin/authentications.coffee.erb
similarity index 62%
rename from app/assets/javascripts/controllers/admin/authentications.coffee
rename to app/assets/javascripts/controllers/admin/authentications.coffee.erb
index 0c6055f21..6ef241387 100644
--- a/app/assets/javascripts/controllers/admin/authentications.coffee
+++ b/app/assets/javascripts/controllers/admin/authentications.coffee.erb
@@ -34,6 +34,102 @@ check_oauth2_id_is_mapped = (mappings) ->
return false
+##
+# Provides a set of common callback methods and data to the $scope parameter. These methods are used
+# in the various authentication providers' controllers.
+#
+# Provides :
+# - $scope.authMethods
+# - $scope.mappingFields
+# - $scope.cancel()
+# - $scope.defineDataMapping(mapping)
+#
+# Requires :
+# - mappingFieldsPromise: retrieved by AuthProvider.mapping_fields()
+# - $state (Ui-Router) [ 'app.admin.members' ]
+##
+class AuthenticationController
+ constructor: ($scope, $state, $uibModal, mappingFieldsPromise)->
+ ## list of supported authentication methods
+ $scope.authMethods = METHODS
+
+ ## list of fields that can be mapped through the SSO
+ $scope.mappingFields = mappingFieldsPromise
+
+ ##
+ # Changes the admin's view to the members list page
+ ##
+ $scope.cancel = ->
+ $state.go('app.admin.members')
+
+
+
+ ##
+ # Open a modal allowing to specify the data mapping for the given field
+ ##
+ $scope.defineDataMapping = (mapping) ->
+ $uibModal.open
+ templateUrl: '<%= asset_path "admin/authentications/_data_mapping.html" %>'
+ size: 'md'
+ resolve:
+ field: -> mapping
+ datatype: ->
+ for field in $scope.mappingFields[mapping.local_model]
+ if field[0] == mapping.local_field
+ return field[1]
+
+ controller: ['$scope', '$uibModalInstance', 'field', 'datatype', ($scope, $uibModalInstance, field, datatype) ->
+ ## parent field
+ $scope.field = field
+ ## expected data type
+ $scope.datatype = datatype
+ ## data transformation rules
+ $scope.transformation =
+ rules: field.transformation || {type: datatype}
+ ## available transformation formats
+ $scope.formats =
+ date: [
+ {
+ label: 'ISO 8601',
+ value: 'iso8601'
+ },
+ {
+ label: 'RFC 2822',
+ value: 'rfc2822'
+ },
+ {
+ label: 'RFC 3339',
+ value: 'rfc3339'
+ },
+ {
+ label: 'Timestamp (s)'
+ value: 'timestamp-s'
+ },
+ {
+ label: 'Timestamp (ms)',
+ value: 'timestamp-ms'
+ }
+ ]
+
+ ## Create a new mapping between anything and an expected integer
+ $scope.addIntegerMapping = ->
+ unless angular.isArray $scope.transformation.rules.mapping
+ $scope.transformation.rules.mapping = []
+ $scope.transformation.rules.mapping.push({from:'', to:0})
+
+ ## close and save the modifications
+ $scope.ok = ->
+ $uibModalInstance.close($scope.transformation.rules)
+
+ ## do not save the modifications
+ $scope.cancel = ->
+ $uibModalInstance.dismiss()
+ ]
+ .result['finally'](null).then (transfo_rules) ->
+ mapping.transformation = transfo_rules
+
+
+
##
# Page listing all authentication providers
@@ -103,15 +199,12 @@ Application.Controllers.controller "AuthentificationController", ["$scope", "$st
##
# 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
+Application.Controllers.controller "NewAuthenticationController", ["$scope", "$state", "$rootScope", "$uibModal", "dialogs", "growl", "mappingFieldsPromise", "authProvidersPromise", "AuthProvider", '_t'
+, ($scope, $state, $rootScope, $uibModal, dialogs, growl, mappingFieldsPromise, authProvidersPromise, AuthProvider, _t) ->
$scope.mode = 'creation'
+ ## default parameters for the new authentication provider
$scope.provider = {
name: '',
providable_type: '',
@@ -172,12 +265,8 @@ Application.Controllers.controller "NewAuthenticationController", ["$scope", "$s
- ##
- # Changes the admin's view to the members list page
- ##
- $scope.cancel = ->
- $state.go('app.admin.members')
-
+ ## Using the AuthenticationController
+ new AuthenticationController($scope, $state, $uibModal, mappingFieldsPromise)
]
@@ -185,17 +274,14 @@ Application.Controllers.controller "NewAuthenticationController", ["$scope", "$s
##
# 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) ->
+Application.Controllers.controller "EditAuthenticationController", ["$scope", "$state", "$stateParams", "$rootScope", "$uibModal", "dialogs", "growl", 'providerPromise', 'mappingFieldsPromise', 'AuthProvider', '_t'
+, ($scope, $state, $stateParams, $rootScope, $uibModal, dialogs, growl, providerPromise, mappingFieldsPromise, AuthProvider, _t) ->
+ ## parameters of the currently edited authentication provider
$scope.provider = providerPromise
- $scope.authMethods = METHODS
-
$scope.mode = 'edition'
- $scope.mappingFields = mappingFieldsPromise
-
##
# Update the current provider with the new inputs
##
@@ -210,10 +296,8 @@ Application.Controllers.controller "EditAuthenticationController", ["$scope", "$
, ->
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')
+
+ ## Using the AuthenticationController
+ new AuthenticationController($scope, $state, $uibModal, mappingFieldsPromise)
]
\ No newline at end of file
diff --git a/app/assets/javascripts/controllers/admin/calendar.coffee.erb b/app/assets/javascripts/controllers/admin/calendar.coffee.erb
index 81e38b54f..85dd92c1f 100644
--- a/app/assets/javascripts/controllers/admin/calendar.coffee.erb
+++ b/app/assets/javascripts/controllers/admin/calendar.coffee.erb
@@ -4,8 +4,8 @@
# 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) ->
+Application.Controllers.controller "AdminCalendarController", ["$scope", "$state", "$uibModal", "moment", "Availability", 'Slot', 'Setting', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', '_t', 'uiCalendarConfig', 'CalendarConfig'
+($scope, $state, $uibModal, moment, Availability, Slot, Setting, growl, dialogs, bookingWindowStart, bookingWindowEnd, machinesPromise, _t, uiCalendarConfig, CalendarConfig) ->
@@ -17,9 +17,6 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
# 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
@@ -36,40 +33,17 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
## bind the availabilities slots with full-Calendar events
$scope.eventSources = []
$scope.eventSources.push
- events: availabilitiesPromise
+ url: '/api/availabilities'
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
+ $scope.calendarConfig = CalendarConfig
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
+ minTime: moment.duration(moment(bookingWindowStart.setting.value).format('HH:mm:ss'))
+ maxTime: moment.duration(moment(bookingWindowEnd.setting.value).format('HH:mm:ss'))
select: (start, end, jsEvent, view) ->
calendarSelectCb(start, end, jsEvent, view)
eventClick: (event, jsEvent, view)->
@@ -77,10 +51,6 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
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'))
-
##
@@ -141,7 +111,7 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
# update the machine_ids attribute
$scope.availability.machine_ids = data.machine_ids
$scope.availability.title = data.title
- $scope.calendar.fullCalendar 'rerenderEvents'
+ uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
# notify the admin
growl.success(_t('the_machine_was_successfully_removed_from_the_slot'))
, (data, status) -> # failed
@@ -180,7 +150,7 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
end: -> end
# when the modal is closed, we send the slot to the server for saving
modalInstance.result.then (availability) ->
- $scope.calendar.fullCalendar 'renderEvent',
+ uiCalendarConfig.calendars.calendar.fullCalendar 'renderEvent',
id: availability.id
title: availability.title,
start: availability.start_at
@@ -189,12 +159,13 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
backgroundColor: availability.backgroundColor
borderColor: availability.borderColor
tag_ids: availability.tag_ids
+ tags: availability.tags
machine_ids: availability.machine_ids
, true
, ->
- $scope.calendar.fullCalendar('unselect')
+ uiCalendarConfig.calendars.calendar.fullCalendar('unselect')
- $scope.calendar.fullCalendar('unselect')
+ uiCalendarConfig.calendars.calendar.fullCalendar('unselect')
@@ -209,7 +180,7 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
# 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
+ uiCalendarConfig.calendars.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)
@@ -231,12 +202,12 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
# @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 += "#{tag.name} "
- element.find('.fc-title').append(" "+html)
+ if event.tags.length > 0
+ html = ''
+ for tag in event.tags
+ html += "#{tag.name} "
+ element.find('.fc-title').append(" "+html)
+ return
]
diff --git a/app/assets/javascripts/controllers/admin/coupons.coffee b/app/assets/javascripts/controllers/admin/coupons.coffee
new file mode 100644
index 000000000..eef8f658c
--- /dev/null
+++ b/app/assets/javascripts/controllers/admin/coupons.coffee
@@ -0,0 +1,122 @@
+### COMMON CODE ###
+
+# The validity per user defines how many time a user may ba able to use the same coupon
+# Here are the various options for this parameter
+userValidities = ['once', 'forever']
+
+
+##
+# Controller used in the coupon creation page
+##
+Application.Controllers.controller "NewCouponController", ["$scope", "$state",'Coupon', 'growl', '_t'
+, ($scope, $state, Coupon, growl, _t) ->
+
+ ## Values for the coupon currently created
+ $scope.coupon =
+ active: true
+
+ ## Options for the validity per user
+ $scope.validities = userValidities
+
+ ## Default parameters for AngularUI-Bootstrap datepicker (used for coupon validity limit selection)
+ $scope.datePicker =
+ format: Fablab.uibDateFormat
+ opened: false # default: datePicker is not shown
+ minDate: moment().toDate()
+ options:
+ startingDay: Fablab.weekStartingDay
+
+
+
+ ##
+ # Shows/hides the validity limit datepicker
+ # @param $event {Object} jQuery event object
+ ##
+ $scope.toggleDatePicker = ($event) ->
+ $event.preventDefault()
+ $event.stopPropagation()
+ $scope.datePicker.opened = !$scope.datePicker.opened
+
+
+
+ ##
+ # Callback to save the new coupon in $scope.coupon and redirect the user to the listing page
+ ##
+ $scope.saveCoupon = ->
+ Coupon.save coupon: $scope.coupon, (coupon) ->
+ $state.go('app.admin.pricing')
+ , (err)->
+ growl.error(_t('unable_to_create_the_coupon_check_code_already_used'))
+ console.error(err)
+]
+
+
+
+
+
+##
+# Controller used in the coupon edition page
+##
+Application.Controllers.controller "EditCouponController", ["$scope", "$state", 'Coupon', 'couponPromise', '_t'
+, ($scope, $state, Coupon, couponPromise, _t) ->
+
+ ### PUBLIC SCOPE ###
+
+
+ ## Used in the form to freeze unmodifiable fields
+ $scope.mode = 'EDIT'
+
+ ## Coupon to edit
+ $scope.coupon = couponPromise
+
+ ## Options for the validity per user
+ $scope.validities = userValidities
+
+ ## Default parameters for AngularUI-Bootstrap datepicker (used for coupon validity limit selection)
+ $scope.datePicker =
+ format: Fablab.uibDateFormat
+ opened: false # default: datePicker is not shown
+ minDate: moment().toDate()
+ options:
+ startingDay: Fablab.weekStartingDay
+
+
+
+ ##
+ # Shows/hides the validity limit datepicker
+ # @param $event {Object} jQuery event object
+ ##
+ $scope.toggleDatePicker = ($event) ->
+ $event.preventDefault()
+ $event.stopPropagation()
+ $scope.datePicker.opened = !$scope.datePicker.opened
+
+
+
+ ##
+ # Callback to save the coupon's changes to the API
+ ##
+ $scope.updateCoupon = ->
+ Coupon.update {id: $scope.coupon.id}, coupon: $scope.coupon, (coupon) ->
+ $state.go('app.admin.pricing')
+ , (err)->
+ growl.error(_t('unable_to_update_the_coupon_an_error_occurred'))
+ console.error(err)
+
+
+
+ ### PRIVATE SCOPE ###
+
+ ##
+ # Kind of constructor: these actions will be realized first when the controller is loaded
+ ##
+ initialize = ->
+ # parse the date if any
+ if (couponPromise.valid_until)
+ $scope.coupon.valid_until = moment(couponPromise.valid_until).toDate()
+
+
+
+ ## !!! MUST BE CALLED AT THE END of the controller
+ initialize()
+]
\ No newline at end of file
diff --git a/app/assets/javascripts/controllers/admin/events.coffee b/app/assets/javascripts/controllers/admin/events.coffee
deleted file mode 100644
index 571a1eab0..000000000
--- a/app/assets/javascripts/controllers/admin/events.coffee
+++ /dev/null
@@ -1,296 +0,0 @@
-'use strict'
-
-### COMMON CODE ###
-
-##
-# Provides a set of common properties and methods to the $scope parameter. They are used
-# in the various events' admin controllers.
-#
-# Provides :
-# - $scope.categories = [{Category}]
-# - $scope.datePicker = {}
-# - $scope.submited(content)
-# - $scope.cancel()
-# - $scope.addFile()
-# - $scope.deleteFile(file)
-# - $scope.fileinputClass(v)
-# - $scope.toggleStartDatePicker($event)
-# - $scope.toggleEndDatePicker($event)
-# - $scope.toggleRecurrenceEnd(e)
-#
-# Requires :
-# - $scope.event.event_files_attributes = []
-# - $state (Ui-Router) [ 'app.public.events_list' ]
-##
-class EventsController
- constructor: ($scope, $state, Event, Category) ->
-
- ## Retrieve the list of categories from the server (stage, atelier, ...)
- Category.query().$promise.then (data)->
- $scope.categories = data.map (d) ->
- id: d.id
- name: d.name
-
- ## default parameters for AngularUI-Bootstrap datepicker
- $scope.datePicker =
- format: Fablab.uibDateFormat
- startOpened: false # default: datePicker is not shown
- endOpened: false
- recurrenceEndOpened: false
- options:
- startingDay: Fablab.weekStartingDay
-
-
-
- ##
- # 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 project 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.public.events_list')
-
-
-
- ##
- # Changes the user's view to the events list page
- ##
- $scope.cancel = ->
- $state.go('app.public.events_list')
-
-
-
- ##
- # 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'
-
-
-
- ##
- # This will create a single new empty entry into the event's attachements list.
- ##
- $scope.addFile = ->
- $scope.event.event_files_attributes.push {}
-
-
-
- ##
- # This will remove the given file from the event's attachements list. If the file was previously uploaded
- # to the server, it will be marked for deletion on the server. Otherwise, it will be simply truncated from
- # the attachements array.
- # @param file {Object} the file to delete
- ##
- $scope.deleteFile = (file) ->
- index = $scope.event.event_files_attributes.indexOf(file)
- if file.id?
- file._destroy = true
- else
- $scope.event.event_files_attributes.splice(index, 1)
-
-
-
- ##
- # Show/Hide the "start" datepicker (open the drop down/close it)
- ##
- $scope.toggleStartDatePicker = ($event) ->
- $event.preventDefault()
- $event.stopPropagation()
- $scope.datePicker.startOpened = !$scope.datePicker.startOpened
-
-
-
- ##
- # Show/Hide the "end" datepicker (open the drop down/close it)
- ##
- $scope.toggleEndDatePicker = ($event) ->
- $event.preventDefault()
- $event.stopPropagation()
- $scope.datePicker.endOpened = !$scope.datePicker.endOpened
-
-
-
- ##
- # Masks/displays the recurrence pane allowing the admin to set the current event as recursive
- ##
- $scope.toggleRecurrenceEnd = (e)->
- e.preventDefault()
- e.stopPropagation()
- $scope.datePicker.recurrenceEndOpened = !$scope.datePicker.recurrenceEndOpened
-
-
-
-##
-# Controller used in the events listing page (admin view)
-##
-Application.Controllers.controller "AdminEventsController", ["$scope", "$state", 'Event', 'eventsPromise', ($scope, $state, Event, eventsPromise) ->
-
-
-
- ### PUBLIC SCOPE ###
-
- ## By default, the pagination mode is activated to limit the page size
- $scope.paginateActive = true
-
- ## The events displayed on the page
- $scope.events = eventsPromise
-
- ## Current virtual page
- $scope.page = 2
-
- ##
- # Adds a bucket of events to the bottom of the page, grouped by month
- ##
- $scope.loadMoreEvents = ->
- Event.query {page: $scope.page}, (data)->
- $scope.events = $scope.events.concat data
- paginationCheck(data, $scope.events)
- $scope.page += 1
-
-
-
- ### PRIVATE SCOPE ###
-
- ##
- # Kind of constructor: these actions will be realized first when the controller is loaded
- ##
- initialize = ->
- paginationCheck(eventsPromise, $scope.events)
-
-
- ##
- # 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()
-
-]
-
-
-
-##
-# 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
-]
-
-
-
-##
-# Controller used in the event creation page
-##
-Application.Controllers.controller "NewEventController", ["$scope", "$state", "$locale", 'Event', 'Category', 'CSRF', '_t'
-, ($scope, $state, $locale, Event, Category, CSRF, _t) ->
- CSRF.setMetaTags()
-
- ## API URL where the form will be posted
- $scope.actionUrl = "/api/events/"
-
- ## Form action on the above URL
- $scope.method = 'post'
-
- ## Default event parameters
- $scope.event =
- event_files_attributes: []
- start_date: new Date()
- end_date: new Date()
- start_time: new Date()
- end_time: new Date()
- all_day: 'false'
- recurrence: 'none'
- category_ids: []
-
- ## Possible types of recurrences for an event
- $scope.recurrenceTypes = [
- {label: _t('none'), value: 'none'},
- {label: _t('every_days'), value: 'day'},
- {label: _t('every_week'), value: 'week'},
- {label: _t('every_month'), value: 'month'},
- {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
- new EventsController($scope, $state, Event, Category)
-]
-
-
-
-##
-# Controller used in the events edition page
-##
-Application.Controllers.controller "EditEventController", ["$scope", "$state", "$stateParams", "$locale", 'Event', 'Category', 'CSRF', 'eventPromise'
-, ($scope, $state, $stateParams, $locale, Event, Category, CSRF, eventPromise) ->
-
- ### PUBLIC SCOPE ###
-
-
-
- ## API URL where the form will be posted
- $scope.actionUrl = "/api/events/" + $stateParams.id
-
- ## Form action on the above URL
- $scope.method = 'put'
-
- ## Retrieve the event details, in case of error the user is redirected to the events listing
- $scope.event = eventPromise
-
- ## currency symbol for the current locale (cf. angular-i18n)
- $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, Event, Category)
-
-
-
- ## !!! MUST BE CALLED AT THE END of the controller
- initialize()
-]
diff --git a/app/assets/javascripts/controllers/admin/events.coffee.erb b/app/assets/javascripts/controllers/admin/events.coffee.erb
new file mode 100644
index 000000000..4e2665f5d
--- /dev/null
+++ b/app/assets/javascripts/controllers/admin/events.coffee.erb
@@ -0,0 +1,495 @@
+'use strict'
+
+### COMMON CODE ###
+
+##
+# Provides a set of common properties and methods to the $scope parameter. They are used
+# in the various events' admin controllers.
+#
+# Provides :
+# - $scope.datePicker = {}
+# - $scope.submited(content)
+# - $scope.cancel()
+# - $scope.addFile()
+# - $scope.deleteFile(file)
+# - $scope.fileinputClass(v)
+# - $scope.toggleStartDatePicker($event)
+# - $scope.toggleEndDatePicker($event)
+# - $scope.toggleRecurrenceEnd(e)
+#
+# Requires :
+# - $scope.event.event_files_attributes = []
+# - $state (Ui-Router) [ 'app.public.events_list' ]
+##
+class EventsController
+ constructor: ($scope, $state) ->
+
+ ## default parameters for AngularUI-Bootstrap datepicker
+ $scope.datePicker =
+ format: Fablab.uibDateFormat
+ startOpened: false # default: datePicker is not shown
+ endOpened: false
+ recurrenceEndOpened: false
+ options:
+ startingDay: Fablab.weekStartingDay
+
+
+
+ ##
+ # 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 project 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.public.events_list')
+
+
+
+ ##
+ # Changes the user's view to the events list page
+ ##
+ $scope.cancel = ->
+ $state.go('app.public.events_list')
+
+
+
+ ##
+ # 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'
+
+
+
+ ##
+ # This will create a single new empty entry into the event's attachements list.
+ ##
+ $scope.addFile = ->
+ $scope.event.event_files_attributes.push {}
+
+
+
+ ##
+ # This will remove the given file from the event's attachements list. If the file was previously uploaded
+ # to the server, it will be marked for deletion on the server. Otherwise, it will be simply truncated from
+ # the attachements array.
+ # @param file {Object} the file to delete
+ ##
+ $scope.deleteFile = (file) ->
+ index = $scope.event.event_files_attributes.indexOf(file)
+ if file.id?
+ file._destroy = true
+ else
+ $scope.event.event_files_attributes.splice(index, 1)
+
+
+
+ ##
+ # Show/Hide the "start" datepicker (open the drop down/close it)
+ ##
+ $scope.toggleStartDatePicker = ($event) ->
+ $event.preventDefault()
+ $event.stopPropagation()
+ $scope.datePicker.startOpened = !$scope.datePicker.startOpened
+
+
+
+ ##
+ # Show/Hide the "end" datepicker (open the drop down/close it)
+ ##
+ $scope.toggleEndDatePicker = ($event) ->
+ $event.preventDefault()
+ $event.stopPropagation()
+ $scope.datePicker.endOpened = !$scope.datePicker.endOpened
+
+
+
+ ##
+ # Masks/displays the recurrence pane allowing the admin to set the current event as recursive
+ ##
+ $scope.toggleRecurrenceEnd = (e)->
+ e.preventDefault()
+ e.stopPropagation()
+ $scope.datePicker.recurrenceEndOpened = !$scope.datePicker.recurrenceEndOpened
+
+
+
+ ##
+ # Initialize a new price item in the additional prices list
+ ##
+ $scope.addPrice = ->
+ $scope.event.prices.push({
+ category: null,
+ amount: null,
+ })
+
+
+
+##
+# Controller used in the events listing page (admin view)
+##
+Application.Controllers.controller "AdminEventsController", ["$scope", "$state", 'dialogs', '$uibModal', 'growl', 'Event', 'Category', 'EventTheme', 'AgeRange', 'PriceCategory', 'eventsPromise', 'categoriesPromise', 'themesPromise', 'ageRangesPromise', 'priceCategoriesPromise', '_t'
+, ($scope, $state, dialogs, $uibModal, growl, Event, Category, EventTheme, AgeRange, PriceCategory, eventsPromise, categoriesPromise, themesPromise, ageRangesPromise, priceCategoriesPromise, _t) ->
+
+
+
+ ### PUBLIC SCOPE ###
+
+ ## By default, the pagination mode is activated to limit the page size
+ $scope.paginateActive = true
+
+ ## The events displayed on the page
+ $scope.events = eventsPromise
+
+ ## Current virtual page
+ $scope.page = 2
+
+ ## Temporary datastore for creating new elements
+ $scope.inserted =
+ category: null
+ theme: null
+ age_range: null
+
+ ## List of categories for the events
+ $scope.categories = categoriesPromise
+
+ ## List of events themes
+ $scope.themes = themesPromise
+
+ ## List of age ranges
+ $scope.ageRanges = ageRangesPromise
+
+ ## List of price categories for the events
+ $scope.priceCategories = priceCategoriesPromise
+
+ ##
+ # Adds a bucket of events to the bottom of the page, grouped by month
+ ##
+ $scope.loadMoreEvents = ->
+ Event.query {page: $scope.page}, (data)->
+ $scope.events = $scope.events.concat data
+ paginationCheck(data, $scope.events)
+ $scope.page += 1
+
+
+ ##
+ # Saves a new element / Update an existing one to the server (form validation callback)
+ # @param model {string} model name
+ # @param data {Object} element name
+ # @param [id] {number} element id, in case of update
+ ##
+ $scope.saveElement = (model, data, id) ->
+ if id?
+ getModel(model)[0].update {id: id}, data
+ else
+ getModel(model)[0].save data, (resp)->
+ getModel(model)[1][getModel(model)[1].length-1].id = resp.id
+
+
+
+ ##
+ # Deletes the element at the specified index
+ # @param model {string} model name
+ # @param index {number} element index in the $scope[model] array
+ ##
+ $scope.removeElement = (model, index) ->
+ if model == 'category' and getModel(model)[1].length == 1
+ growl.error(_t('at_least_one_category_is_required')+' '+_t('unable_to_delete_the_last_one'))
+ return false
+ if getModel(model)[1][index].related_to > 0
+ growl.error(_t('unable_to_delete_ELEMENT_already_in_use_NUMBER_times', {ELEMENT:model, NUMBER:getModel(model)[1][index].related_to}, "messageformat"))
+ return false
+ dialogs.confirm
+ resolve:
+ object: ->
+ title: _t('confirmation_required')
+ msg: _t('do_you_really_want_to_delete_this_ELEMENT', {ELEMENT:model}, "messageformat")
+ , -> # delete confirmed
+ getModel(model)[0].delete getModel(model)[1][index], null, ->
+ getModel(model)[1].splice(index, 1)
+ , ->
+ growl.error(_t('unable_to_delete_an_error_occured'))
+
+
+
+ ##
+ # Creates a new empty entry in the $scope[model] array
+ # @param model {string} model name
+ ##
+ $scope.addElement = (model) ->
+ $scope.inserted[model] =
+ name: ''
+ related_to: 0
+ getModel(model)[1].push($scope.inserted[model])
+
+
+
+ ##
+ # Removes the newly inserted but not saved element / Cancel the current element modification
+ # @param model {string} model name
+ # @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
+ # @param index {number} element index in the $scope[model] array
+ ##
+ $scope.cancelElement = (model, rowform, index) ->
+ if getModel(model)[1][index].id?
+ rowform.$cancel()
+ else
+ getModel(model)[1].splice(index, 1)
+
+
+
+ ##
+ # Open a modal dialog allowing the definition of a new price category.
+ # Save it once filled and handle the result.
+ ##
+ $scope.newPriceCategory = ->
+ $uibModal.open
+ templateUrl: '<%= asset_path "admin/events/price_form.html" %>'
+ size: 'md'
+ resolve:
+ category: -> {}
+ controller: 'PriceCategoryController'
+ .result['finally'](null).then (p_cat) ->
+ # save the price category to the API
+ PriceCategory.save p_cat, (cat) ->
+ $scope.priceCategories.push(cat)
+ growl.success(_t('price_category_successfully_created'))
+ , (err)->
+ growl.error(_t('unable_to_add_the_price_category_check_name_already_used'))
+ console.error(err)
+
+
+
+ ##
+ # Update the given price category with the new properties
+ # to specify in a modal dialog
+ # @param index {number} index of the caterory in the $scope.priceCategories array
+ # @param id {number} price category ID, must match the ID of the category at the index specified above
+ ##
+ $scope.editPriceCategory = (id, index) ->
+ if $scope.priceCategories[index].id != id
+ growl.error(_t('unexpected_error_occurred_please_refresh'))
+ else
+ $uibModal.open
+ templateUrl: '<%= asset_path "admin/events/price_form.html" %>'
+ size: 'md'
+ resolve:
+ category: -> $scope.priceCategories[index]
+ controller: 'PriceCategoryController'
+ .result['finally'](null).then (p_cat) ->
+ # update the price category to the API
+ PriceCategory.update {id: id}, {price_category: p_cat}, (cat) ->
+ $scope.priceCategories[index] = cat
+ growl.success(_t('price_category_successfully_updated'))
+ , (err)->
+ growl.error(_t('unable_to_update_the_price_category'))
+ console.error(err)
+
+
+
+ ##
+ # Delete the given price category from the API
+ # @param index {number} index of the caterory in the $scope.priceCategories array
+ # @param id {number} price category ID, must match the ID of the category at the index specified above
+ ##
+ $scope.removePriceCategory = (id, index) ->
+ if $scope.priceCategories[index].id != id
+ growl.error(_t('unexpected_error_occurred_please_refresh'))
+ else if $scope.priceCategories[index].events > 0
+ growl.error(_t('unable_to_delete_this_price_category_because_it_is_already_used'))
+ else
+ dialogs.confirm
+ resolve:
+ object: ->
+ title: _t('confirmation_required')
+ msg: _t('do_you_really_want_to_delete_this_price_category')
+ , -> # delete confirmed
+ PriceCategory.remove {id: id}, -> # successfully deleted
+ growl.success _t('price_category_successfully_deleted')
+ $scope.priceCategories.splice(index, 1)
+ , ->
+ growl.error _t('price_category_deletion_failed')
+
+
+
+ ### PRIVATE SCOPE ###
+
+ ##
+ # Kind of constructor: these actions will be realized first when the controller is loaded
+ ##
+ initialize = ->
+ paginationCheck(eventsPromise, $scope.events)
+
+
+ ##
+ # 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
+
+ ##
+ # Return the model and the datastore matching the given name
+ # @param name {string} 'category', 'theme' or 'age_range'
+ # @return {[Object, Array]} model and datastore
+ ##
+ getModel = (name) ->
+ switch name
+ when 'category' then [Category, $scope.categories]
+ when 'theme' then [EventTheme, $scope.themes]
+ when 'age_range' then [AgeRange, $scope.ageRanges]
+ else [null, []]
+
+
+ # init the controller (call at the end !)
+ 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
+]
+
+
+
+##
+# Controller used in the event creation page
+##
+Application.Controllers.controller "NewEventController", ["$scope", "$state", "$locale", 'CSRF', 'categoriesPromise', 'themesPromise', 'ageRangesPromise', 'priceCategoriesPromise', '_t'
+, ($scope, $state, $locale, CSRF, categoriesPromise, themesPromise, ageRangesPromise, priceCategoriesPromise, _t) ->
+ CSRF.setMetaTags()
+
+ ## API URL where the form will be posted
+ $scope.actionUrl = "/api/events/"
+
+ ## Form action on the above URL
+ $scope.method = 'post'
+
+ ## List of categories for the events
+ $scope.categories = categoriesPromise
+
+ ## List of events themes
+ $scope.themes = themesPromise
+
+ ## List of age ranges
+ $scope.ageRanges = ageRangesPromise
+
+ ## List of availables price's categories
+ $scope.priceCategories = priceCategoriesPromise
+
+ ## Default event parameters
+ $scope.event =
+ event_files_attributes: []
+ start_date: new Date()
+ end_date: new Date()
+ start_time: new Date()
+ end_time: new Date()
+ all_day: 'false'
+ recurrence: 'none'
+ category_id: null
+ prices: []
+
+ ## Possible types of recurrences for an event
+ $scope.recurrenceTypes = [
+ {label: _t('none'), value: 'none'},
+ {label: _t('every_days'), value: 'day'},
+ {label: _t('every_week'), value: 'week'},
+ {label: _t('every_month'), value: 'month'},
+ {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
+ new EventsController($scope, $state)
+]
+
+
+
+##
+# Controller used in the events edition page
+##
+Application.Controllers.controller "EditEventController", ["$scope", "$state", "$stateParams", "$locale", 'CSRF', 'eventPromise', 'categoriesPromise', 'themesPromise', 'ageRangesPromise', 'priceCategoriesPromise'
+, ($scope, $state, $stateParams, $locale, CSRF, eventPromise, categoriesPromise, themesPromise, ageRangesPromise, priceCategoriesPromise) ->
+
+ ### PUBLIC SCOPE ###
+
+
+
+ ## API URL where the form will be posted
+ $scope.actionUrl = "/api/events/" + $stateParams.id
+
+ ## Form action on the above URL
+ $scope.method = 'put'
+
+ ## Retrieve the event details, in case of error the user is redirected to the events listing
+ $scope.event = eventPromise
+
+ ## currency symbol for the current locale (cf. angular-i18n)
+ $scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM;
+
+ ## List of categories for the events
+ $scope.categories = categoriesPromise
+
+ ## List of availables price's categories
+ $scope.priceCategories = priceCategoriesPromise
+
+ ## List of events themes
+ $scope.themes = themesPromise
+
+ ## List of age ranges
+ $scope.ageRanges = ageRangesPromise
+
+
+
+ ### 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)
+
+
+
+ ## !!! MUST BE CALLED AT THE END of the controller
+ initialize()
+]
diff --git a/app/assets/javascripts/controllers/admin/graphs.coffee b/app/assets/javascripts/controllers/admin/graphs.coffee
index 529cbf7b4..4e24d1318 100644
--- a/app/assets/javascripts/controllers/admin/graphs.coffee
+++ b/app/assets/javascripts/controllers/admin/graphs.coffee
@@ -334,7 +334,7 @@ Application.Controllers.controller "GraphsController", ["$scope", "$state", "$ro
callback(results)
recursiveCb()
else # palmares (ranking)
- queryElasticRanking index.es_type_key, $scope.ranking.groupCriterion, $scope.ranking.sortCriterion, index.graph.limit, (results, error) ->
+ queryElasticRanking index.es_type_key, $scope.ranking.groupCriterion, $scope.ranking.sortCriterion, (results, error) ->
if (error)
callback([], error)
else
@@ -373,17 +373,18 @@ Application.Controllers.controller "GraphsController", ["$scope", "$state", "$ro
##
# 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 esType {string} elasticSearch document type (subscription|machine|training|...)
+ # @param groupKey {string} statistics subtype or custom field
+ # @param sortKey {string} statistics type or 'ca'
# @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) ->
+ queryElasticRanking = (esType, groupKey, sortKey, 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'
+ if !esType or !groupKey or !sortKey
callback([], '[graphsController::queryElasticRanking] Error: invalid parameters provided')
# run query
diff --git a/app/assets/javascripts/controllers/admin/invoices.coffee.erb b/app/assets/javascripts/controllers/admin/invoices.coffee.erb
index daa410c94..044e8fd05 100644
--- a/app/assets/javascripts/controllers/admin/invoices.coffee.erb
+++ b/app/assets/javascripts/controllers/admin/invoices.coffee.erb
@@ -133,6 +133,8 @@ Application.Controllers.controller "InvoicesController", ["$scope", "$state", 'I
sample = sample.replace(/X\[([^\]]+)\]/g, (match, p1, offset, string) ->
p1
)
+ # information about wallet (W[text]) - does not apply here
+ sample = sample.replace(/W\[([^\]]+)\]/g, "")
# information about refunds (R[text]) - does not apply here
sample = sample.replace(/R\[([^\]]+)\]/g, "")
sample
@@ -494,6 +496,7 @@ Application.Controllers.controller 'AvoirModalController', ["$scope", "$uibModal
{name: _t('by_cash'), value: 'cash'}
{name: _t('by_cheque'), value: 'cheque'}
{name: _t('by_transfer'), value: 'transfer'}
+ {name: _t('by_wallet'), value: 'wallet'}
]
## If a subscription was took with the current invoice, should it be canceled or not
diff --git a/app/assets/javascripts/controllers/admin/members.coffee.erb b/app/assets/javascripts/controllers/admin/members.coffee.erb
index 06516d796..75b30126c 100644
--- a/app/assets/javascripts/controllers/admin/members.coffee.erb
+++ b/app/assets/javascripts/controllers/admin/members.coffee.erb
@@ -105,8 +105,8 @@ class MembersController
##
# Controller used in the members/groups management page
##
-Application.Controllers.controller "AdminMembersController", ["$scope", 'membersPromise', 'adminsPromise', 'growl', 'Admin', 'dialogs', '_t', 'Member'
-, ($scope, membersPromise, adminsPromise, growl, Admin, dialogs, _t, Member) ->
+Application.Controllers.controller "AdminMembersController", ["$scope", 'membersPromise', 'adminsPromise', 'growl', 'Admin', 'dialogs', '_t', 'Member', 'Export'
+, ($scope, membersPromise, adminsPromise, growl, Admin, dialogs, _t, Member, Export) ->
@@ -204,6 +204,16 @@ Application.Controllers.controller "AdminMembersController", ["$scope", 'members
resetSearchMember()
memberSearch()
+ ##
+ # Callback to alert the admin that the export request was acknowledged and is
+ # processing right now.
+ ##
+ $scope.alertExport = (type) ->
+ Export.status({category: 'users', type: type}).then (res) ->
+ unless (res.data.exists)
+ growl.success _t('export_is_running_you_ll_be_notified_when_its_ready')
+
+
### PRIVATE SCOPE ###
@@ -260,8 +270,8 @@ Application.Controllers.controller "AdminMembersController", ["$scope", 'members
##
# Controller used in the member edition page
##
-Application.Controllers.controller "EditMemberController", ["$scope", "$state", "$stateParams", "Member", 'Training', 'dialogs', 'growl', 'Group', 'Subscription', 'CSRF', 'memberPromise', 'tagsPromise', '$uibModal', 'Plan', '$filter', '_t'
-, ($scope, $state, $stateParams, Member, Training, dialogs, growl, Group, Subscription, CSRF, memberPromise, tagsPromise, $uibModal, Plan, $filter, _t) ->
+Application.Controllers.controller "EditMemberController", ["$scope", "$state", "$stateParams", "Member", 'Training', 'dialogs', 'growl', 'Group', 'Subscription', 'CSRF', 'memberPromise', 'tagsPromise', '$uibModal', 'Plan', '$filter', '_t', 'walletPromise', 'transactionsPromise', 'activeProviderPromise', 'Wallet'
+, ($scope, $state, $stateParams, Member, Training, dialogs, growl, Group, Subscription, CSRF, memberPromise, tagsPromise, $uibModal, Plan, $filter, _t, walletPromise, transactionsPromise, activeProviderPromise, Wallet) ->
@@ -300,6 +310,17 @@ Application.Controllers.controller "EditMemberController", ["$scope", "$state",
## Profiles types (student/standard/...)
$scope.groups = []
+ ## the user wallet
+ $scope.wallet = walletPromise
+
+ ## user wallet transactions
+ $scope.transactions = transactionsPromise
+
+ ## used in wallet partial template to identify parent view
+ $scope.view = 'member_edit'
+
+ # current active authentication provider
+ $scope.activeProvider = activeProviderPromise
##
@@ -396,6 +417,39 @@ Application.Controllers.controller "EditMemberController", ["$scope", "$state",
$scope.subscription = subscription
+ $scope.createWalletCreditModal = (user, wallet)->
+ modalInstance = $uibModal.open
+ animation: true,
+ templateUrl: '<%= asset_path "wallet/credit_modal.html" %>'
+ controller: ['$scope', '$uibModalInstance', 'Wallet', '$locale', ($scope, $uibModalInstance, Wallet, $locale) ->
+
+ ## currency symbol for the current locale (cf. angular-i18n)
+ $scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM
+
+ ##
+ # Modal dialog validation callback
+ ##
+ $scope.ok = ->
+ Wallet.credit { id: wallet.id }, { amount: $scope.amount }, (_wallet)->
+
+ growl.success(_t('wallet_credit_successfully'))
+ $uibModalInstance.close(_wallet)
+ , (error)->
+ growl.error(_t('a_problem_occurred_for_wallet_credit'))
+
+ ##
+ # Modal dialog cancellation callback
+ ##
+ $scope.cancel = ->
+ $uibModalInstance.dismiss('cancel')
+ ]
+ # once the form was validated succesfully ...
+ modalInstance.result.then (wallet) ->
+ $scope.wallet = wallet
+ Wallet.transactions {id: wallet.id}, (transactions) ->
+ $scope.transactions = transactions
+
+
### PRIVATE SCOPE ###
diff --git a/app/assets/javascripts/controllers/admin/plans.coffee.erb b/app/assets/javascripts/controllers/admin/plans.coffee.erb
index d7b266f1a..8fb323c10 100644
--- a/app/assets/javascripts/controllers/admin/plans.coffee.erb
+++ b/app/assets/javascripts/controllers/admin/plans.coffee.erb
@@ -60,6 +60,18 @@ class PlanController
+ ##
+ # Retrieve the name of a machine from its ID
+ # @param machine_id {number} machine identifier
+ # @returns {string} Machine's name
+ ##
+ $scope.getMachineName = (machine_id) ->
+ for machine in $scope.machines
+ if machine.id == machine_id
+ return machine.name
+
+
+
diff --git a/app/assets/javascripts/controllers/admin/price_category.coffee b/app/assets/javascripts/controllers/admin/price_category.coffee
new file mode 100644
index 000000000..fab1fa7c4
--- /dev/null
+++ b/app/assets/javascripts/controllers/admin/price_category.coffee
@@ -0,0 +1,23 @@
+'use strict'
+
+##
+# Controller used in price category creation/edition form dialog
+##
+Application.Controllers.controller "PriceCategoryController", ["$scope", "$uibModalInstance", "category"
+, ($scope, $uibModalInstance, category) ->
+
+ ## Price category to edit/empty object for new category
+ $scope.category = category
+
+ ##
+ # Callback for form validation
+ ##
+ $scope.ok = ->
+ $uibModalInstance.close($scope.category)
+
+ ##
+ # Do not validate the modifications, hide the modal
+ ##
+ $scope.cancel = ->
+ $uibModalInstance.dismiss('cancel')
+]
\ No newline at end of file
diff --git a/app/assets/javascripts/controllers/admin/pricing.coffee.erb b/app/assets/javascripts/controllers/admin/pricing.coffee.erb
index 99f6b9475..4d69bfae7 100644
--- a/app/assets/javascripts/controllers/admin/pricing.coffee.erb
+++ b/app/assets/javascripts/controllers/admin/pricing.coffee.erb
@@ -3,8 +3,8 @@
##
# Controller used in the prices edition page
##
-Application.Controllers.controller "EditPricingController", ["$scope", "$state", '$uibModal', 'TrainingsPricing', '$filter', 'Credit', 'Pricing', 'Plan', 'plans', 'groups', 'growl', 'machinesPricesPromise', 'Price', 'dialogs', 'trainingsPricingsPromise', 'trainingsPromise', 'machineCreditsPromise', 'machinesPromise', 'trainingCreditsPromise', '_t'
-, ($scope, $state, $uibModal, TrainingsPricing, $filter, Credit, Pricing, Plan, plans, groups, growl, machinesPricesPromise, Price, dialogs, trainingsPricingsPromise, trainingsPromise, machineCreditsPromise, machinesPromise, trainingCreditsPromise, _t) ->
+Application.Controllers.controller "EditPricingController", ["$scope", "$state", '$uibModal', 'TrainingsPricing', '$filter', 'Credit', 'Pricing', 'Plan', 'Coupon', 'plans', 'groups', 'growl', 'machinesPricesPromise', 'Price', 'dialogs', 'trainingsPricingsPromise', 'trainingsPromise', 'machineCreditsPromise', 'machinesPromise', 'trainingCreditsPromise', 'couponsPromise', '_t'
+, ($scope, $state, $uibModal, TrainingsPricing, $filter, Credit, Pricing, Plan, Coupon, plans, groups, growl, machinesPricesPromise, Price, dialogs, trainingsPricingsPromise, trainingsPromise, machineCreditsPromise, machinesPromise, trainingCreditsPromise, couponsPromise, _t) ->
### PUBLIC SCOPE ###
## List of machines prices (not considering any plan)
@@ -34,6 +34,9 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
## List of machines
$scope.machines = machinesPromise
+ ## List of coupons
+ $scope.coupons = couponsPromise
+
## The plans list ordering. Default: by group
$scope.orderPlans = 'group_id'
@@ -275,7 +278,7 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
##
$scope.deletePlan = (plans, id) ->
if typeof id != 'number'
- console.error('[editPricingController::deletePlan] Error: invalid id parameter')
+ console.error('[EditPricingController::deletePlan] Error: invalid id parameter')
else
# open a confirmation dialog
dialogs.confirm
@@ -287,10 +290,10 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
# 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)
+ $scope.plans.splice(findItemIdxById(plans, id), 1)
, (error) ->
- console.error('[editPricingController::deletePlan] Error: '+error.statusText) if error.statusText
+ console.error('[EditPricingController::deletePlan] Error: '+error.statusText) if error.statusText
growl.error(_t('unable_to_delete_the_specified_subscription_an_error_occurred'))
@@ -308,12 +311,70 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
- ### PRIVATE SCOPE ###
+ ##
+ # Delete a coupon from the server's database and, in case of success, from the list in memory
+ # @param coupons {Array